feat: 完成token功能
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
|
||||
|
||||
/// <summary>
|
||||
/// 创建Token输入
|
||||
/// </summary>
|
||||
public class TokenCreateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称(同一用户不能重复)
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "名称不能为空")]
|
||||
[StringLength(100, ErrorMessage = "名称长度不能超过100个字符")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间(空为永不过期)
|
||||
/// </summary>
|
||||
public DateTime? ExpireTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包额度限制(空为不限制)
|
||||
/// </summary>
|
||||
public long? PremiumQuotaLimit { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
|
||||
|
||||
/// <summary>
|
||||
/// Token列表输出
|
||||
/// </summary>
|
||||
public class TokenListOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// Token Id
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Token密钥
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间(空为永不过期)
|
||||
/// </summary>
|
||||
public DateTime? ExpireTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包额度限制(空为不限制)
|
||||
/// </summary>
|
||||
public long? PremiumQuotaLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包已使用额度
|
||||
/// </summary>
|
||||
public long PremiumUsedQuota { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否禁用
|
||||
/// </summary>
|
||||
public bool IsDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreationTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Token;
|
||||
|
||||
/// <summary>
|
||||
/// 编辑Token输入
|
||||
/// </summary>
|
||||
public class TokenUpdateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Token Id
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Id不能为空")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称(同一用户不能重复)
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "名称不能为空")]
|
||||
[StringLength(100, ErrorMessage = "名称长度不能超过100个字符")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间(空为永不过期)
|
||||
/// </summary>
|
||||
public DateTime? ExpireTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包额度限制(空为不限制)
|
||||
/// </summary>
|
||||
public long? PremiumQuotaLimit { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包不同Token用量占比DTO(饼图)
|
||||
/// </summary>
|
||||
public class TokenPremiumUsageDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Token Id
|
||||
/// </summary>
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Token名称
|
||||
/// </summary>
|
||||
public string TokenName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Token消耗量
|
||||
/// </summary>
|
||||
public long Tokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 占比(百分比)
|
||||
/// </summary>
|
||||
public decimal Percentage { get; set; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Token服务
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class TokenService : ApplicationService
|
||||
{
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
private readonly TokenManager _tokenManager;
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="tokenRepository"></param>
|
||||
/// <param name="tokenManager"></param>
|
||||
public TokenService(ISqlSugarRepository<TokenAggregateRoot> tokenRepository, TokenManager tokenManager)
|
||||
public TokenService(
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
|
||||
{
|
||||
_tokenRepository = tokenRepository;
|
||||
_tokenManager = tokenManager;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取token
|
||||
/// 获取当前用户的Token列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
public async Task<TokenOutput> GetAsync()
|
||||
[HttpGet("token/list")]
|
||||
public async Task<List<TokenListOutput>> 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<TokenListOutput>();
|
||||
}
|
||||
|
||||
// 获取尊享包模型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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建token
|
||||
/// 创建Token
|
||||
/// </summary>
|
||||
/// <exception cref="UserFriendlyException"></exception>
|
||||
[Authorize]
|
||||
public async Task CreateAsync()
|
||||
[HttpPost("token")]
|
||||
public async Task<TokenListOutput> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑Token
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除Token
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用Token
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用Token
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MessageAggregateRoot> _messageRepository;
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
|
||||
public UsageStatisticsService(
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository)
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
|
||||
{
|
||||
_messageRepository = messageRepository;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_tokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<PremiumTokenUsageGetListOutput>(total,
|
||||
entities.Adapt<List<PremiumTokenUsageGetListOutput>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户尊享包不同Token用量占比(饼图)
|
||||
/// </summary>
|
||||
/// <returns>各Token的尊享模型用量及占比</returns>
|
||||
[HttpGet("usage-statistics/premium-token-usage/by-token")]
|
||||
public async Task<List<TokenPremiumUsageDto>> 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<TokenPremiumUsageDto>();
|
||||
}
|
||||
|
||||
// 获取用户的所有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;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
}
|
||||
|
||||
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<Guid>
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Token密钥Id(通过API调用时记录,Web调用为Guid.Empty)
|
||||
/// </summary>
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||
public string? Content { get; set; }
|
||||
|
||||
|
||||
@@ -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<Guid>
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token密钥
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重置token
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public void ResetToken()
|
||||
[SugarColumn(Length = 100)]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间(空为永不过期)
|
||||
/// </summary>
|
||||
public DateTime? ExpireTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包额度限制(空为不限制)
|
||||
/// </summary>
|
||||
public long? PremiumQuotaLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否禁用
|
||||
/// </summary>
|
||||
public bool IsDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 检查Token是否可用
|
||||
/// </summary>
|
||||
public bool IsAvailable()
|
||||
{
|
||||
this.Token = GenerateToken();
|
||||
if (IsDisabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExpireTime.HasValue && ExpireTime.Value < DateTime.Now)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用Token
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
IsDisabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用Token
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
IsDisabled = false;
|
||||
}
|
||||
|
||||
private string GenerateToken(int length = 36)
|
||||
|
||||
@@ -7,16 +7,22 @@ namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
/// 用量统计
|
||||
/// </summary>
|
||||
[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<Guid>
|
||||
{
|
||||
public UsageStatisticsAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
public UsageStatisticsAggregateRoot(Guid? userId, string modelId)
|
||||
public UsageStatisticsAggregateRoot(Guid? userId, string modelId, Guid tokenId)
|
||||
{
|
||||
UserId = userId;
|
||||
ModelId = modelId;
|
||||
TokenId = tokenId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,6 +35,11 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
/// </summary>
|
||||
public string ModelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Token密钥Id(通过API调用时记录,Web调用为Guid.Empty)
|
||||
/// </summary>
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对话次数
|
||||
/// </summary>
|
||||
|
||||
@@ -120,12 +120,14 @@ public class AiGateWayManager : DomainService
|
||||
/// <param name="request"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="tokenId">Token Id(Web端传null或Guid.Empty)</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <param name="request"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="tokenId">Token Id(Web端传null或Guid.Empty)</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="tokenId">Token Id(Web端传null或Guid.Empty)</param>
|
||||
/// <exception cref="BusinessException"></exception>
|
||||
/// <exception cref="Exception"></exception>
|
||||
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
|
||||
/// 向量生成
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="tokenId">Token Id(Web端传null或Guid.Empty)</param>
|
||||
/// <exception cref="Exception"></exception>
|
||||
/// <exception cref="BusinessException"></exception>
|
||||
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
|
||||
/// <param name="request"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="tokenId">Token Id(Web端传null或Guid.Empty)</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <param name="request"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="tokenId">Token Id(Web端传null或Guid.Empty)</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
|
||||
@@ -19,28 +19,30 @@ public class AiMessageManager : DomainService
|
||||
/// <summary>
|
||||
/// 创建系统消息
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="sessionId">会话Id</param>
|
||||
/// <param name="input">消息输入</param>
|
||||
/// <param name="tokenId">Token Id(Web端传Guid.Empty)</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建系统消息
|
||||
/// 创建用户消息
|
||||
/// </summary>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="sessionId">会话Id</param>
|
||||
/// <param name="input">消息输入</param>
|
||||
/// <param name="tokenId">Token Id(Web端传Guid.Empty)</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Token验证结果
|
||||
/// </summary>
|
||||
public class TokenValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Token Id
|
||||
/// </summary>
|
||||
public Guid TokenId { get; set; }
|
||||
}
|
||||
|
||||
public class TokenManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
|
||||
public TokenManager(ISqlSugarRepository<TokenAggregateRoot> tokenRepository)
|
||||
public TokenManager(
|
||||
ISqlSugarRepository<TokenAggregateRoot> tokenRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
|
||||
{
|
||||
_tokenRepository = tokenRepository;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
}
|
||||
|
||||
public async Task<string?> 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<Guid> GetUserIdAsync(string? token)
|
||||
/// <summary>
|
||||
/// 验证Token并返回用户Id和TokenId
|
||||
/// </summary>
|
||||
/// <param name="token">Token密钥</param>
|
||||
/// <param name="modelId">模型Id(用于判断是否是尊享模型需要检查额度)</param>
|
||||
/// <returns>Token验证结果</returns>
|
||||
public async Task<TokenValidationResult> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Token的尊享包已使用额度
|
||||
/// </summary>
|
||||
private async Task<long> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户的Token(兼容旧接口,返回第一个可用的Token)
|
||||
/// </summary>
|
||||
[Obsolete("请使用 ValidateTokenAsync 方法")]
|
||||
public async Task<string?> GetAsync(Guid userId)
|
||||
{
|
||||
var entity = await _tokenRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && !x.IsDisabled)
|
||||
.OrderBy(x => x.CreationTime)
|
||||
.FirstAsync();
|
||||
|
||||
return entity?.Token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户Id(兼容旧接口)
|
||||
/// </summary>
|
||||
[Obsolete("请使用 ValidateTokenAsync 方法")]
|
||||
public async Task<Guid> GetUserIdAsync(string? token)
|
||||
{
|
||||
var result = await ValidateTokenAsync(token);
|
||||
return result.UserId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ public class UsageStatisticsManager : DomainService
|
||||
private IDistributedLockProvider DistributedLock =>
|
||||
LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user