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/TokenListOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenListOutput.cs
new file mode 100644
index 00000000..2a91ad63
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Token/TokenListOutput.cs
@@ -0,0 +1,47 @@
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
+
+///
+/// Token列表输出
+///
+public class TokenListOutput
+{
+ ///
+ /// 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/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/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..9f818ebd 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,216 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using SqlSugar;
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.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()
{
- return new TokenOutput
+ var userId = CurrentUser.GetId();
+
+ var tokens = await _tokenRepository._DbQueryable
+ .Where(x => x.UserId == userId)
+ .OrderByDescending(x => x.CreationTime)
+ .ToListAsync();
+
+ if (!tokens.Any())
{
- ApiKey = await _tokenManager.GetAsync(CurrentUser.GetId())
- };
+ return new List();
+ }
+
+ // 获取尊享包模型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 TokenListOutput
+ {
+ 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 result;
}
///
- /// 创建token
+ /// 创建Token
///
- ///
- [Authorize]
- public async Task CreateAsync()
+ [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 TokenListOutput
+ {
+ Id = token.Id,
+ Name = token.Name,
+ ApiKey = token.Token,
+ ExpireTime = token.ExpireTime,
+ PremiumQuotaLimit = token.PremiumQuotaLimit,
+ PremiumUsedQuota = 0,
+ IsDisabled = token.IsDisabled,
+ CreationTime = token.CreationTime
+ };
}
-}
\ No newline at end of file
+
+ ///
+ /// 编辑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);
+ }
+}
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..8391b5af 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,15 +26,18 @@ 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;
}
///
@@ -83,13 +88,14 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
{
var userId = CurrentUser.GetId();
- // 从UsageStatistics表获取各模型的token消耗统计
+ // 从UsageStatistics表获取各模型的token消耗统计(按ModelId聚合,因为同一模型可能有多个TokenId的记录)
var modelUsages = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId)
+ .GroupBy(x => x.ModelId)
.Select(x => new
{
- x.ModelId,
- x.TotalTokenCount
+ ModelId = x.ModelId,
+ TotalTokenCount = SqlFunc.AggregateSum(x.TotalTokenCount)
})
.ToListAsync();
@@ -164,4 +170,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 ? "Web端" : (tokenNameDict.TryGetValue(x.TokenId, out var name) ? name : "未知Token"),
+ 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);
}