diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenCreateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenCreateInput.cs new file mode 100644 index 00000000..ca6866ea --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenCreateInput.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +/// +/// 创建Token输入 +/// +public class TokenCreateInput +{ + /// + /// 名称(同一用户不能重复) + /// + [Required(ErrorMessage = "名称不能为空")] + [StringLength(100, ErrorMessage = "名称长度不能超过100个字符")] + public string Name { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenGetListOutputDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenGetListOutputDto.cs new file mode 100644 index 00000000..6358985b --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenGetListOutputDto.cs @@ -0,0 +1,47 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +/// +/// Token列表输出 +/// +public class TokenGetListOutputDto +{ + /// + /// Token Id + /// + public Guid Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// Token密钥 + /// + public string ApiKey { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } + + /// + /// 尊享包已使用额度 + /// + public long PremiumUsedQuota { get; set; } + + /// + /// 是否禁用 + /// + public bool IsDisabled { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenSelectListOutputDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenSelectListOutputDto.cs new file mode 100644 index 00000000..445ef6f0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenSelectListOutputDto.cs @@ -0,0 +1,9 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +public class TokenSelectListOutputDto +{ + public Guid TokenId { get; set; } + public string Name { get; set; } + + public bool IsDisabled { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenUpdateInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenUpdateInput.cs new file mode 100644 index 00000000..e149d6d3 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenUpdateInput.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token; + +/// +/// 编辑Token输入 +/// +public class TokenUpdateInput +{ + /// + /// Token Id + /// + [Required(ErrorMessage = "Id不能为空")] + public Guid Id { get; set; } + + /// + /// 名称(同一用户不能重复) + /// + [Required(ErrorMessage = "名称不能为空")] + [StringLength(100, ErrorMessage = "名称长度不能超过100个字符")] + public string Name { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/TokenPremiumUsageDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/TokenPremiumUsageDto.cs new file mode 100644 index 00000000..77bbbf18 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/TokenPremiumUsageDto.cs @@ -0,0 +1,27 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +/// +/// 尊享包不同Token用量占比DTO(饼图) +/// +public class TokenPremiumUsageDto +{ + /// + /// Token Id + /// + public Guid TokenId { get; set; } + + /// + /// Token名称 + /// + public string TokenName { get; set; } + + /// + /// Token消耗量 + /// + public long Tokens { get; set; } + + /// + /// 占比(百分比) + /// + public decimal Percentage { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/UsageStatisticsGetInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/UsageStatisticsGetInput.cs new file mode 100644 index 00000000..98ed1fd4 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/UsageStatisticsGetInput.cs @@ -0,0 +1,9 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +public class UsageStatisticsGetInput +{ + /// + /// tokenId + /// + public Guid? TokenId { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs index add4d348..99be00a6 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IUsageStatisticsService.cs @@ -11,13 +11,13 @@ public interface IUsageStatisticsService /// 获取当前用户近7天的Token消耗统计 /// /// 每日Token使用量列表 - Task> GetLast7DaysTokenUsageAsync(); + Task> GetLast7DaysTokenUsageAsync(UsageStatisticsGetInput input); /// /// 获取当前用户各个模型的Token消耗量及占比 /// /// 模型Token使用量列表 - Task> GetModelTokenUsageAsync(); + Task> GetModelTokenUsageAsync(UsageStatisticsGetInput input); /// /// 获取当前用户尊享服务Token用量统计 diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs index f6d266e2..be069631 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs @@ -135,7 +135,7 @@ public class AiChatService : ApplicationService //ai网关代理httpcontext await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - CurrentUser.Id, sessionId, cancellationToken); + CurrentUser.Id, sessionId, null, cancellationToken); } @@ -172,6 +172,6 @@ public class AiChatService : ApplicationService //ai网关代理httpcontext await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - CurrentUser.Id, null, cancellationToken); + CurrentUser.Id, null, null, cancellationToken); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs index 65d30166..23e7e666 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs @@ -1,55 +1,245 @@ -using Microsoft.AspNetCore.Authorization; +using Dm.util; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SqlSugar; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos.Token; +using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Extensions; -using Yi.Framework.AiHub.Domain.Managers; +using Yi.Framework.AiHub.Domain.Shared.Consts; +using Yi.Framework.Ddd.Application.Contracts; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; +/// +/// Token服务 +/// +[Authorize] public class TokenService : ApplicationService { private readonly ISqlSugarRepository _tokenRepository; - private readonly TokenManager _tokenManager; + private readonly ISqlSugarRepository _usageStatisticsRepository; - /// - /// 构造函数 - /// - /// - /// - public TokenService(ISqlSugarRepository tokenRepository, TokenManager tokenManager) + public TokenService( + ISqlSugarRepository tokenRepository, + ISqlSugarRepository usageStatisticsRepository) { _tokenRepository = tokenRepository; - _tokenManager = tokenManager; + _usageStatisticsRepository = usageStatisticsRepository; } /// - /// 获取token + /// 获取当前用户的Token列表 /// - /// - [Authorize] - public async Task GetAsync() + [HttpGet("token/list")] + public async Task> GetListAsync([FromQuery] PagedAllResultRequestDto input) { - return new TokenOutput + RefAsync total = 0; + var userId = CurrentUser.GetId(); + + var tokens = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.CreationTime) + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + + if (!tokens.Any()) { - ApiKey = await _tokenManager.GetAsync(CurrentUser.GetId()) - }; + return new PagedResultDto(); + } + + // 获取尊享包模型ID列表 + var premiumModelIds = PremiumPackageConst.ModeIds; + + // 批量查询所有Token的尊享包已使用额度 + var tokenIds = tokens.Select(t => t.Id).ToList(); + var usageStats = await _usageStatisticsRepository._DbQueryable + .Where(x => x.UserId == userId && tokenIds.Contains(x.TokenId) && premiumModelIds.Contains(x.ModelId)) + .GroupBy(x => x.TokenId) + .Select(g => new + { + TokenId = g.TokenId, + UsedQuota = SqlFunc.AggregateSum(g.TotalTokenCount) + }) + .ToListAsync(); + + var result = tokens.Select(t => + { + var usedQuota = usageStats.FirstOrDefault(u => u.TokenId == t.Id)?.UsedQuota ?? 0; + return new TokenGetListOutputDto + { + Id = t.Id, + Name = t.Name, + ApiKey = t.Token, + ExpireTime = t.ExpireTime, + PremiumQuotaLimit = t.PremiumQuotaLimit, + PremiumUsedQuota = usedQuota, + IsDisabled = t.IsDisabled, + CreationTime = t.CreationTime + }; + }).ToList(); + + return new PagedResultDto(total, result); } - /// - /// 创建token - /// - /// - [Authorize] - public async Task CreateAsync() + [HttpGet("token/select-list")] + public async Task> GetSelectListAsync() { + var userId = CurrentUser.GetId(); + var tokens = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId) + .OrderBy(x => x.IsDisabled) + .OrderByDescending(x => x.CreationTime) + .Select(x => new TokenSelectListOutputDto + { + TokenId = x.Id, + Name = x.Name, + IsDisabled = x.IsDisabled + }).ToListAsync(); + + tokens.Insert(0,new TokenSelectListOutputDto + { + TokenId = Guid.Empty, + Name = "默认", + IsDisabled = false + }); + return tokens; + } + + + /// + /// 创建Token + /// + [HttpPost("token")] + public async Task CreateAsync([FromBody] TokenCreateInput input) + { + var userId = CurrentUser.GetId(); + + // 检查用户是否为VIP if (!CurrentUser.IsAiVip()) { throw new UserFriendlyException("充值成为Vip,畅享第三方token服务"); } - await _tokenManager.CreateAsync(CurrentUser.GetId()); + // 检查名称是否重复 + var exists = await _tokenRepository._DbQueryable + .AnyAsync(x => x.UserId == userId && x.Name == input.Name); + if (exists) + { + throw new UserFriendlyException($"名称【{input.Name}】已存在,请使用其他名称"); + } + + var token = new TokenAggregateRoot(userId, input.Name) + { + ExpireTime = input.ExpireTime, + PremiumQuotaLimit = input.PremiumQuotaLimit + }; + + await _tokenRepository.InsertAsync(token); + + return new TokenGetListOutputDto + { + Id = token.Id, + Name = token.Name, + ApiKey = token.Token, + ExpireTime = token.ExpireTime, + PremiumQuotaLimit = token.PremiumQuotaLimit, + PremiumUsedQuota = 0, + IsDisabled = token.IsDisabled, + CreationTime = token.CreationTime + }; + } + + /// + /// 编辑Token + /// + [HttpPut("token")] + public async Task UpdateAsync([FromBody] TokenUpdateInput input) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == input.Id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + // 检查名称是否重复(排除自己) + var exists = await _tokenRepository._DbQueryable + .AnyAsync(x => x.UserId == userId && x.Name == input.Name && x.Id != input.Id); + if (exists) + { + throw new UserFriendlyException($"名称【{input.Name}】已存在,请使用其他名称"); + } + + token.Name = input.Name; + token.ExpireTime = input.ExpireTime; + token.PremiumQuotaLimit = input.PremiumQuotaLimit; + + await _tokenRepository.UpdateAsync(token); + } + + /// + /// 删除Token + /// + [HttpDelete("token/{id}")] + public async Task DeleteAsync(Guid id) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + await _tokenRepository.DeleteAsync(token); + } + + /// + /// 启用Token + /// + [HttpPost("token/{id}/enable")] + public async Task EnableAsync(Guid id) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + token.Enable(); + await _tokenRepository.UpdateAsync(token); + } + + /// + /// 禁用Token + /// + [HttpPost("token/{id}/disable")] + public async Task DisableAsync(Guid id) + { + var userId = CurrentUser.GetId(); + + var token = await _tokenRepository._DbQueryable + .FirstAsync(x => x.Id == id && x.UserId == userId); + + if (token is null) + { + throw new UserFriendlyException("Token不存在或无权限操作"); + } + + token.Disable(); + await _tokenRepository.UpdateAsync(token); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs index 64c71e10..f04ecf45 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FileMaster/FileMasterService.cs @@ -76,12 +76,12 @@ public class FileMasterService : ApplicationService if (input.Stream == true) { await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - userId, null, cancellationToken); + userId, null, null, cancellationToken); } else { await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, - null, + null, null, cancellationToken); } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs index 23cf473c..60145a29 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs @@ -55,7 +55,9 @@ public class OpenApiService : ApplicationService { //前面都是校验,后面才是真正的调用 var httpContext = this._httpContextAccessor.HttpContext; - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); //如果是尊享包服务,需要校验是是否尊享包足够 @@ -68,17 +70,17 @@ public class OpenApiService : ApplicationService throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包"); } } - + //ai网关代理httpcontext if (input.Stream == true) { await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - userId, null, cancellationToken); + userId, null, tokenId, cancellationToken); } else { await _aiGateWayManager.CompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, - null, + null, tokenId, cancellationToken); } } @@ -93,9 +95,11 @@ public class OpenApiService : ApplicationService { var httpContext = this._httpContextAccessor.HttpContext; Intercept(httpContext); - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); - await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input); + await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input, tokenId); } /// @@ -108,9 +112,11 @@ public class OpenApiService : ApplicationService { var httpContext = this._httpContextAccessor.HttpContext; Intercept(httpContext); - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); - await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input); + await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input, tokenId); } @@ -151,7 +157,9 @@ public class OpenApiService : ApplicationService { //前面都是校验,后面才是真正的调用 var httpContext = this._httpContextAccessor.HttpContext; - var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + var tokenValidation = await _tokenManager.ValidateTokenAsync(GetTokenByHttpContext(httpContext), input.Model); + var userId = tokenValidation.UserId; + var tokenId = tokenValidation.TokenId; await _aiBlacklistManager.VerifiyAiBlacklist(userId); // 验证用户是否为VIP @@ -178,12 +186,12 @@ public class OpenApiService : ApplicationService if (input.Stream) { await _aiGateWayManager.AnthropicCompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, - userId, null, cancellationToken); + userId, null, tokenId, cancellationToken); } else { await _aiGateWayManager.AnthropicCompleteChatForStatisticsAsync(_httpContextAccessor.HttpContext, input, userId, - null, + null, tokenId, cancellationToken); } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs index feedf690..bffce1e6 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs @@ -9,7 +9,9 @@ using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; using Yi.Framework.AiHub.Application.Contracts.IServices; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.Chat; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Extensions; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.Ddd.Application.Contracts; using Yi.Framework.SqlSugarCore.Abstractions; @@ -24,22 +26,25 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic private readonly ISqlSugarRepository _messageRepository; private readonly ISqlSugarRepository _usageStatisticsRepository; private readonly ISqlSugarRepository _premiumPackageRepository; + private readonly ISqlSugarRepository _tokenRepository; public UsageStatisticsService( ISqlSugarRepository messageRepository, ISqlSugarRepository usageStatisticsRepository, - ISqlSugarRepository premiumPackageRepository) + ISqlSugarRepository premiumPackageRepository, + ISqlSugarRepository tokenRepository) { _messageRepository = messageRepository; _usageStatisticsRepository = usageStatisticsRepository; _premiumPackageRepository = premiumPackageRepository; + _tokenRepository = tokenRepository; } /// /// 获取当前用户近7天的Token消耗统计 /// /// 每日Token使用量列表 - public async Task> GetLast7DaysTokenUsageAsync() + public async Task> GetLast7DaysTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); var endDate = DateTime.Today; @@ -50,6 +55,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic .Where(x => x.UserId == userId) .Where(x => x.Role == "assistant" || x.Role == "system") .Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1)) + .WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId) .GroupBy(x => x.CreationTime.Date) .Select(g => new { @@ -79,17 +85,19 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic /// 获取当前用户各个模型的Token消耗量及占比 /// /// 模型Token使用量列表 - public async Task> GetModelTokenUsageAsync() + public async Task> GetModelTokenUsageAsync([FromQuery]UsageStatisticsGetInput input) { var userId = CurrentUser.GetId(); - // 从UsageStatistics表获取各模型的token消耗统计 + // 从UsageStatistics表获取各模型的token消耗统计(按ModelId聚合,因为同一模型可能有多个TokenId的记录) var modelUsages = await _usageStatisticsRepository._DbQueryable .Where(x => x.UserId == userId) + .WhereIF(input.TokenId.HasValue,x => x.TokenId == input.TokenId) + .GroupBy(x => x.ModelId) .Select(x => new { - x.ModelId, - x.TotalTokenCount + ModelId = x.ModelId, + TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount) }) .ToListAsync(); @@ -164,4 +172,54 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic return new PagedResultDto(total, entities.Adapt>()); } + + /// + /// 获取当前用户尊享包不同Token用量占比(饼图) + /// + /// 各Token的尊享模型用量及占比 + [HttpGet("usage-statistics/premium-token-usage/by-token")] + public async Task> GetPremiumTokenUsageByTokenAsync() + { + var userId = CurrentUser.GetId(); + var premiumModelIds = PremiumPackageConst.ModeIds; + + // 从UsageStatistics表获取尊享模型的token消耗统计(按TokenId聚合) + var tokenUsages = await _usageStatisticsRepository._DbQueryable + .Where(x => x.UserId == userId && premiumModelIds.Contains(x.ModelId)) + .GroupBy(x => x.TokenId) + .Select(x => new + { + TokenId = x.TokenId, + TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount) + }) + .ToListAsync(); + + if (!tokenUsages.Any()) + { + return new List(); + } + + // 获取用户的所有Token信息用于名称映射 + var tokenIds = tokenUsages.Select(x => x.TokenId).ToList(); + var tokens = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId && tokenIds.Contains(x.Id)) + .Select(x => new { x.Id, x.Name }) + .ToListAsync(); + + var tokenNameDict = tokens.ToDictionary(x => x.Id, x => x.Name); + + // 计算总token数 + var totalTokens = tokenUsages.Sum(x => x.TotalTokenCount); + + // 计算各Token占比 + var result = tokenUsages.Select(x => new TokenPremiumUsageDto + { + TokenId = x.TokenId, + TokenName = x.TokenId == Guid.Empty ? "默认" : (tokenNameDict.TryGetValue(x.TokenId, out var name) ? name : "其他"), + Tokens = x.TotalTokenCount, + Percentage = totalTokens > 0 ? Math.Round((decimal)x.TotalTokenCount / totalTokens * 100, 2) : 0 + }).OrderByDescending(x => x.Tokens).ToList(); + + return result; + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs index bb1b97f7..69a43156 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Chat/MessageAggregateRoot.cs @@ -20,10 +20,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot } public MessageAggregateRoot(Guid? userId, Guid? sessionId, string content, string role, string modelId, - ThorUsageResponse? tokenUsage) + ThorUsageResponse? tokenUsage, Guid? tokenId = null) { UserId = userId; SessionId = sessionId; + TokenId = tokenId ?? Guid.Empty; //如果没有会话,不存储对话内容 Content = sessionId is null ? null : content; Role = role; @@ -59,6 +60,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot public Guid? UserId { get; set; } public Guid? SessionId { get; set; } + /// + /// Token密钥Id(通过API调用时记录,Web调用为Guid.Empty) + /// + public Guid TokenId { get; set; } + [SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)] public string? Content { get; set; } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs index 96377eb7..d5217ca9 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/OpenApi/TokenAggregateRoot.cs @@ -5,27 +5,84 @@ using Volo.Abp.Domain.Entities.Auditing; namespace Yi.Framework.AiHub.Domain.Entities.OpenApi; [SugarTable("Ai_Token")] +[SugarIndex($"index_{{table}}_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)] public class TokenAggregateRoot : FullAuditedAggregateRoot { public TokenAggregateRoot() { } - public TokenAggregateRoot(Guid userId) + public TokenAggregateRoot(Guid userId, string name) { - this.UserId = userId; - this.Token = GenerateToken(); + UserId = userId; + Name = name; + Token = GenerateToken(); + IsDisabled = false; } + /// + /// Token密钥 + /// public string Token { get; set; } + + /// + /// 用户Id + /// public Guid UserId { get; set; } /// - /// 重置token + /// 名称 /// - public void ResetToken() + [SugarColumn(Length = 100)] + public string Name { get; set; } + + /// + /// 过期时间(空为永不过期) + /// + public DateTime? ExpireTime { get; set; } + + /// + /// 尊享包额度限制(空为不限制) + /// + public long? PremiumQuotaLimit { get; set; } + + /// + /// 是否禁用 + /// + public bool IsDisabled { get; set; } + + /// + /// 检查Token是否可用 + /// + public bool IsAvailable() { - this.Token = GenerateToken(); + if (IsDisabled) + { + return false; + } + + if (ExpireTime.HasValue && ExpireTime.Value < DateTime.Now) + { + return false; + } + + return true; + } + + /// + /// 禁用Token + /// + public void Disable() + { + IsDisabled = true; + } + + /// + /// 启用Token + /// + public void Enable() + { + IsDisabled = false; } private string GenerateToken(int length = 36) diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs index 6f9e6aa3..6008f343 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/UsageStatisticsAggregateRoot.cs @@ -7,16 +7,22 @@ namespace Yi.Framework.AiHub.Domain.Entities; /// 用量统计 /// [SugarTable("Ai_UsageStatistics")] +[SugarIndex($"index_{{table}}_{nameof(UserId)}_{nameof(ModelId)}_{nameof(TokenId)}", + nameof(UserId), OrderByType.Asc, + nameof(ModelId), OrderByType.Asc, + nameof(TokenId), OrderByType.Asc +)] public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot { public UsageStatisticsAggregateRoot() { } - public UsageStatisticsAggregateRoot(Guid? userId, string modelId) + public UsageStatisticsAggregateRoot(Guid? userId, string modelId, Guid tokenId) { UserId = userId; ModelId = modelId; + TokenId = tokenId; } /// @@ -29,6 +35,11 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot /// public string ModelId { get; set; } + /// + /// Token密钥Id(通过API调用时记录,Web调用为Guid.Empty) + /// + public Guid TokenId { get; set; } + /// /// 对话次数 /// diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index ffb70822..4e2d3645 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs @@ -120,12 +120,14 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task CompleteChatForStatisticsAsync(HttpContext httpContext, ThorChatCompletionsRequest request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { _specialCompatible.Compatible(request); @@ -145,7 +147,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault().Content ?? string.Empty, ModelId = request.Model, TokenUsage = data.Usage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, new MessageInputDto @@ -154,9 +156,9 @@ public class AiGateWayManager : DomainService sessionId is null ? "不予存储" : data.Choices?.FirstOrDefault()?.Delta.Content ?? string.Empty, ModelId = request.Model, TokenUsage = data.Usage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage); + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage, tokenId); // 扣减尊享token包用量 if (PremiumPackageConst.ModeIds.Contains(request.Model)) @@ -179,6 +181,7 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task CompleteChatStreamForStatisticsAsync( @@ -186,6 +189,7 @@ public class AiGateWayManager : DomainService ThorChatCompletionsRequest request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { var response = httpContext.Response; @@ -288,7 +292,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty, ModelId = request.Model, TokenUsage = tokenUsage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto @@ -296,9 +300,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(), ModelId = request.Model, TokenUsage = tokenUsage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage); + await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage, tokenId); // 扣减尊享token包用量 if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model)) @@ -319,10 +323,11 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task CreateImageForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId, - ImageCreateRequest request) + ImageCreateRequest request, Guid? tokenId = null) { try { @@ -350,7 +355,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Prompt, ModelId = model, TokenUsage = response.Usage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto @@ -358,9 +363,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : response.Results?.FirstOrDefault()?.Url, ModelId = model, TokenUsage = response.Usage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage); + await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage, tokenId); // 扣减尊享token包用量 if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model)) @@ -384,13 +389,14 @@ public class AiGateWayManager : DomainService /// 向量生成 /// /// + /// /// /// - /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task EmbeddingForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId, - ThorEmbeddingInput input) + ThorEmbeddingInput input, Guid? tokenId = null) { try { @@ -474,7 +480,7 @@ public class AiGateWayManager : DomainService // TokenUsage = usage // }); - await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage); + await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage, tokenId); } catch (ThorRateLimitException) { @@ -522,12 +528,14 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task AnthropicCompleteChatForStatisticsAsync(HttpContext httpContext, AnthropicInput request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { _specialCompatible.AnthropicCompatible(request); @@ -549,7 +557,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.FirstOrDefault()?.Content ?? string.Empty, ModelId = request.Model, TokenUsage = data.TokenUsage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, new MessageInputDto @@ -557,9 +565,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : data.content?.FirstOrDefault()?.text, ModelId = request.Model, TokenUsage = data.TokenUsage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage); + await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage, tokenId); // 扣减尊享token包用量 var totalTokens = data.TokenUsage.TotalTokens ?? 0; @@ -579,6 +587,7 @@ public class AiGateWayManager : DomainService /// /// /// + /// Token Id(Web端传null或Guid.Empty) /// /// public async Task AnthropicCompleteChatStreamForStatisticsAsync( @@ -586,6 +595,7 @@ public class AiGateWayManager : DomainService AnthropicInput request, Guid? userId = null, Guid? sessionId = null, + Guid? tokenId = null, CancellationToken cancellationToken = default) { var response = httpContext.Response; @@ -627,7 +637,7 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty, ModelId = request.Model, TokenUsage = tokenUsage, - }); + }, tokenId); await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, new MessageInputDto @@ -635,9 +645,9 @@ public class AiGateWayManager : DomainService Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(), ModelId = request.Model, TokenUsage = tokenUsage - }); + }, tokenId); - await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage); + await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage, tokenId); // 扣减尊享token包用量 if (userId.HasValue && tokenUsage is not null) diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs index 2620c2a2..95730424 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiMessageManager.cs @@ -19,28 +19,30 @@ public class AiMessageManager : DomainService /// /// 创建系统消息 /// - /// - /// - /// + /// 用户Id + /// 会话Id + /// 消息输入 + /// Token Id(Web端传Guid.Empty) /// - public async Task CreateSystemMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input) + public async Task CreateSystemMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input, Guid? tokenId = null) { input.Role = "system"; - var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); + var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId, input.TokenUsage, tokenId); await _repository.InsertAsync(message); } - + /// - /// 创建系统消息 + /// 创建用户消息 /// - /// - /// - /// + /// 用户Id + /// 会话Id + /// 消息输入 + /// Token Id(Web端传Guid.Empty) /// - public async Task CreateUserMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input) + public async Task CreateUserMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input, Guid? tokenId = null) { input.Role = "user"; - var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage); + var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId, input.TokenUsage, tokenId); await _repository.InsertAsync(message); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs index 1a0ead20..dd2ae774 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/TokenManager.cs @@ -1,64 +1,134 @@ -using Volo.Abp.Domain.Services; -using Volo.Abp.Users; +using SqlSugar; +using Volo.Abp.Domain.Services; +using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.OpenApi; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Domain.Managers; +/// +/// Token验证结果 +/// +public class TokenValidationResult +{ + /// + /// 用户Id + /// + public Guid UserId { get; set; } + + /// + /// Token Id + /// + public Guid TokenId { get; set; } +} + public class TokenManager : DomainService { private readonly ISqlSugarRepository _tokenRepository; + private readonly ISqlSugarRepository _usageStatisticsRepository; - public TokenManager(ISqlSugarRepository tokenRepository) + public TokenManager( + ISqlSugarRepository tokenRepository, + ISqlSugarRepository usageStatisticsRepository) { _tokenRepository = tokenRepository; + _usageStatisticsRepository = usageStatisticsRepository; } - public async Task GetAsync(Guid userId) - { - var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId); - if (entity is not null) - { - return entity.Token; - } - else - { - return null; - } - } - - public async Task CreateAsync(Guid userId) - { - var entity = await _tokenRepository._DbQueryable.FirstAsync(x => x.UserId == userId); - if (entity is not null) - { - entity.ResetToken(); - await _tokenRepository.UpdateAsync(entity); - } - else - { - var token = new TokenAggregateRoot(userId); - await _tokenRepository.InsertAsync(token); - } - } - - public async Task GetUserIdAsync(string? token) + /// + /// 验证Token并返回用户Id和TokenId + /// + /// Token密钥 + /// 模型Id(用于判断是否是尊享模型需要检查额度) + /// Token验证结果 + public async Task ValidateTokenAsync(string? token, string? modelId = null) { if (token is null) { throw new UserFriendlyException("当前请求未包含token", "401"); } - if (token.StartsWith("yi-")) + if (!token.StartsWith("yi-")) { - var entity = await _tokenRepository._DbQueryable.Where(x => x.Token == token).FirstAsync(); - if (entity is null) - { - throw new UserFriendlyException("当前请求token无效", "401"); - } - - return entity.UserId; + throw new UserFriendlyException("当前请求token非法", "401"); } - throw new UserFriendlyException("当前请求token非法", "401"); + + var entity = await _tokenRepository._DbQueryable + .Where(x => x.Token == token) + .FirstAsync(); + + if (entity is null) + { + throw new UserFriendlyException("当前请求token无效", "401"); + } + + // 检查Token是否被禁用 + if (entity.IsDisabled) + { + throw new UserFriendlyException("当前Token已被禁用,请启用后再使用", "403"); + } + + // 检查Token是否过期 + if (entity.ExpireTime.HasValue && entity.ExpireTime.Value < DateTime.Now) + { + throw new UserFriendlyException("当前Token已过期,请更新过期时间或创建新的Token", "403"); + } + + // 如果是尊享模型且Token设置了额度限制,检查是否超限 + if (!string.IsNullOrEmpty(modelId) && + PremiumPackageConst.ModeIds.Contains(modelId) && + entity.PremiumQuotaLimit.HasValue) + { + var usedQuota = await GetTokenPremiumUsedQuotaAsync(entity.UserId, entity.Id); + if (usedQuota >= entity.PremiumQuotaLimit.Value) + { + throw new UserFriendlyException($"当前Token的尊享包额度已用完(已使用:{usedQuota},限制:{entity.PremiumQuotaLimit.Value}),请调整额度限制或使用其他Token", "403"); + } + } + + return new TokenValidationResult + { + UserId = entity.UserId, + TokenId = entity.Id + }; } -} \ No newline at end of file + + /// + /// 获取Token的尊享包已使用额度 + /// + private async Task GetTokenPremiumUsedQuotaAsync(Guid userId, Guid tokenId) + { + var premiumModelIds = PremiumPackageConst.ModeIds; + + var usedQuota = await _usageStatisticsRepository._DbQueryable + .Where(x => x.UserId == userId && x.TokenId == tokenId && premiumModelIds.Contains(x.ModelId)) + .SumAsync(x => x.TotalTokenCount); + + return usedQuota; + } + + /// + /// 获取用户的Token(兼容旧接口,返回第一个可用的Token) + /// + [Obsolete("请使用 ValidateTokenAsync 方法")] + public async Task GetAsync(Guid userId) + { + var entity = await _tokenRepository._DbQueryable + .Where(x => x.UserId == userId && !x.IsDisabled) + .OrderBy(x => x.CreationTime) + .FirstAsync(); + + return entity?.Token; + } + + /// + /// 获取用户Id(兼容旧接口) + /// + [Obsolete("请使用 ValidateTokenAsync 方法")] + public async Task GetUserIdAsync(string? token) + { + var result = await ValidateTokenAsync(token); + return result.UserId; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs index 682c4b96..a04d1ff5 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/UsageStatisticsManager.cs @@ -18,8 +18,10 @@ public class UsageStatisticsManager : DomainService private IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetRequiredService(); - public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage) + public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage, Guid? tokenId = null) { + var actualTokenId = tokenId ?? Guid.Empty; + long inputTokenCount = tokenUsage?.PromptTokens ?? tokenUsage?.InputTokens ?? 0; @@ -28,10 +30,10 @@ public class UsageStatisticsManager : DomainService ?? tokenUsage?.OutputTokens ?? 0; - await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}")) + await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}:{actualTokenId}:{modelId}")) { - var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId); - //存在数据,更细 + var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId && x.TokenId == actualTokenId); + //存在数据,更新 if (entity is not null) { entity.AddOnceChat(inputTokenCount, outputTokenCount); @@ -40,7 +42,7 @@ public class UsageStatisticsManager : DomainService //不存在插入 else { - var usage = new UsageStatisticsAggregateRoot(userId, modelId); + var usage = new UsageStatisticsAggregateRoot(userId, modelId, actualTokenId); usage.AddOnceChat(inputTokenCount, outputTokenCount); await _repository.InsertAsync(usage); } diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index a7d136b7..6b76a323 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -31,6 +31,8 @@ using Yi.Abp.SqlsugarCore; using Yi.Framework.AiHub.Application; using Yi.Framework.AiHub.Application.Services; using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Chat; +using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AspNetCore; using Yi.Framework.AspNetCore.Authentication.OAuth; using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee; @@ -355,10 +357,9 @@ namespace Yi.Abp.Web var app = context.GetApplicationBuilder(); app.UseRouting(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); - // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); //跨域 app.UseCors(DefaultCorsPolicyName); diff --git a/Yi.Ai.Vue3/src/api/model/index.ts b/Yi.Ai.Vue3/src/api/model/index.ts index e6da3f77..91748029 100644 --- a/Yi.Ai.Vue3/src/api/model/index.ts +++ b/Yi.Ai.Vue3/src/api/model/index.ts @@ -1,9 +1,8 @@ import type { GetSessionListVO } from './types'; -import { get, post } from '@/utils/request'; +import { del, get, post, put } from '@/utils/request'; // 获取当前用户的模型列表 export function getModelList() { - // return get('/system/model/modelList'); return get('/ai-chat/model').json(); } // 申请ApiKey @@ -21,10 +20,99 @@ export function getRechargeLog() { } // 查询用户近7天token消耗 -export function getLast7DaysTokenUsage() { - return get('/usage-statistics/last7Days-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getLast7DaysTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/last7Days-token-usage?tokenId=${tokenId}` + : '/usage-statistics/last7Days-token-usage'; + return get(url).json(); } // 查询用户token消耗各模型占比 -export function getModelTokenUsage() { - return get('/usage-statistics/model-token-usage').json(); +// tokenId: 可选,传入则查询该token的用量,不传则查询全部 +export function getModelTokenUsage(tokenId?: string) { + const url = tokenId + ? `/usage-statistics/model-token-usage?tokenId=${tokenId}` + : '/usage-statistics/model-token-usage'; + return get(url).json(); } + +// 获取当前用户得token列表 +export function getTokenList(params?: { + skipCount?: number; + maxResultCount?: number; + orderByColumn?: string; + isAsc?: string; +}) { + // 构建查询参数 + const queryParams = new URLSearchParams(); + if (params?.skipCount !== undefined) { + queryParams.append('SkipCount', params.skipCount.toString()); + } + if (params?.maxResultCount !== undefined) { + queryParams.append('MaxResultCount', params.maxResultCount.toString()); + } + if (params?.orderByColumn) { + queryParams.append('OrderByColumn', params.orderByColumn); + } + if (params?.isAsc) { + queryParams.append('IsAsc', params.isAsc); + } + + const queryString = queryParams.toString(); + const url = queryString ? `/token/list?${queryString}` : '/token/list'; + + return get(url).json(); +} + +// 创建token +export function createToken(data: any) { + return post('/token', data).json(); +} + +// 编辑token +export function editToken(data: any) { + return put('/token', data).json(); +} + +// 删除token +export function deleteToken(id: string) { + return del(`/token/${id}`).json(); +} + +// 启用token +export function enableToken(id: string) { + return post(`/token/${id}/enable`).json(); +} + +// 禁用token +export function disableToken(id: string) { + return post(`/token/${id}/disable`).json(); +} + +// 新增接口2 +// 获取可选择的token信息 +export function getSelectableTokenInfo() { + return get('/token/select-list').json(); +} +/* +返回数据 + [ + { + "tokenId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "string", + "isDisabled": true + } + ] */ +// 获取当前用户尊享包不同token用量占比(饼图) +export function getPremiumPackageTokenUsage() { + return get('/usage-statistics/premium-token-usage/by-token').json(); +} +/* 返回数据 + [ + { + "tokenId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "tokenName": "string", + "tokens": 0, + "percentage": 0 + } + ] */ diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue index d229c51d..e1025d90 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue @@ -1,46 +1,102 @@ -