Merge branch 'premium' into ai-hub
This commit is contained in:
@@ -8,4 +8,29 @@ public class AiUserRoleMenuDto:UserRoleMenuDto
|
||||
/// 是否绑定服务号
|
||||
/// </summary>
|
||||
public bool IsBindFuwuhao { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为VIP用户
|
||||
/// </summary>
|
||||
public bool IsVip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// VIP到期时间
|
||||
/// </summary>
|
||||
public DateTime? VipExpireTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包总Token数
|
||||
/// </summary>
|
||||
public long PremiumTotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包已使用Token数
|
||||
/// </summary>
|
||||
public long PremiumUsedTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包剩余Token数
|
||||
/// </summary>
|
||||
public long PremiumRemainingTokens { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||
|
||||
/// <summary>
|
||||
/// 商品列表输出DTO
|
||||
/// </summary>
|
||||
public class GoodsListOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 商品名称
|
||||
/// </summary>
|
||||
public string GoodsName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品原价
|
||||
/// </summary>
|
||||
public decimal OriginalPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品实际价格(折扣后的价格)
|
||||
/// </summary>
|
||||
public decimal GoodsPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型
|
||||
/// </summary>
|
||||
public GoodsTypeEnum GoodsType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品备注
|
||||
/// </summary>
|
||||
public string Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 折扣金额(仅尊享包)
|
||||
/// </summary>
|
||||
public decimal? DiscountAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 折扣说明(仅尊享包)
|
||||
/// </summary>
|
||||
public string? DiscountDescription { get; set; }
|
||||
}
|
||||
@@ -30,4 +30,10 @@ public interface IPayService : IApplicationService
|
||||
/// <param name="input">查询订单状态输入</param>
|
||||
/// <returns>订单状态信息</returns>
|
||||
Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品列表
|
||||
/// </summary>
|
||||
/// <returns>商品列表</returns>
|
||||
Task<List<GoodsListOutput>> GetGoodsListAsync();
|
||||
}
|
||||
@@ -8,6 +8,8 @@ using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
|
||||
@@ -15,11 +17,19 @@ public class AiAccountService : ApplicationService
|
||||
{
|
||||
private IAccountService _accountService;
|
||||
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
||||
private ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
|
||||
private ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
|
||||
public AiAccountService(IAccountService accountService, ISqlSugarRepository<AiUserExtraInfoEntity> userRepository)
|
||||
public AiAccountService(
|
||||
IAccountService accountService,
|
||||
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository,
|
||||
ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_userRepository = userRepository;
|
||||
_rechargeRepository = rechargeRepository;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +43,54 @@ public class AiAccountService : ApplicationService
|
||||
var userId = CurrentUser.GetId();
|
||||
var userAccount = await _accountService.GetAsync(null, null, userId: CurrentUser.GetId());
|
||||
var output = userAccount.Adapt<AiUserRoleMenuDto>();
|
||||
|
||||
// 是否绑定服务号
|
||||
output.IsBindFuwuhao = await _userRepository.IsAnyAsync(x => userId == x.UserId);
|
||||
|
||||
// 是否为VIP用户
|
||||
output.IsVip = CurrentUser.IsAiVip();
|
||||
|
||||
// 获取VIP到期时间
|
||||
if (output.IsVip)
|
||||
{
|
||||
var recharges = await _rechargeRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.ToListAsync();
|
||||
|
||||
if (recharges.Any())
|
||||
{
|
||||
// 如果有任何一个充值记录的过期时间为null,说明是永久VIP
|
||||
if (recharges.Any(x => !x.ExpireDateTime.HasValue))
|
||||
{
|
||||
output.VipExpireTime = null; // 永久VIP
|
||||
}
|
||||
else
|
||||
{
|
||||
// 取最大的过期时间
|
||||
output.VipExpireTime = recharges
|
||||
.Where(x => x.ExpireDateTime.HasValue)
|
||||
.Max(x => x.ExpireDateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取尊享包Token信息
|
||||
var premiumPackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
if (premiumPackages.Any())
|
||||
{
|
||||
// 过滤掉已过期的包
|
||||
var validPackages = premiumPackages
|
||||
.Where(p => p.IsAvailable())
|
||||
.ToList();
|
||||
|
||||
output.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
|
||||
output.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
|
||||
output.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,18 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
@@ -22,10 +26,13 @@ public class OpenApiService : ApplicationService
|
||||
private readonly AiGateWayManager _aiGateWayManager;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
|
||||
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
|
||||
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager)
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager,
|
||||
IAccountService accountService, PremiumPackageManager premiumPackageManager)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
@@ -33,6 +40,8 @@ public class OpenApiService : ApplicationService
|
||||
_aiGateWayManager = aiGateWayManager;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
_aiBlacklistManager = aiBlacklistManager;
|
||||
_accountService = accountService;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,7 +129,7 @@ public class OpenApiService : ApplicationService
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Anthropic对话
|
||||
/// Anthropic对话(尊享服务专用)
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
@@ -132,6 +141,27 @@ public class OpenApiService : ApplicationService
|
||||
var httpContext = this._httpContextAccessor.HttpContext;
|
||||
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
||||
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||
|
||||
// 验证用户是否为VIP
|
||||
var userInfo = await _accountService.GetAsync(null, null, userId);
|
||||
if (userInfo == null)
|
||||
{
|
||||
throw new UserFriendlyException("用户信息不存在");
|
||||
}
|
||||
|
||||
// 检查是否为VIP(使用RoleCodes判断)
|
||||
if (!userInfo.RoleCodes.Contains(AiHubConst.VipRole) && userInfo.User.UserName != "cc")
|
||||
{
|
||||
throw new UserFriendlyException("该接口为尊享服务专用,需要VIP权限才能使用");
|
||||
}
|
||||
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||||
if (availableTokens <= 0)
|
||||
{
|
||||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||
}
|
||||
|
||||
//ai网关代理httpcontext
|
||||
if (input.Stream)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ using Yi.Framework.AiHub.Domain.Entities.Pay;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
@@ -28,18 +29,22 @@ public class PayService : ApplicationService, IPayService
|
||||
private readonly ILogger<PayService> _logger;
|
||||
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
|
||||
private readonly IRechargeService _rechargeService;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
|
||||
public PayService(
|
||||
AlipayManager alipayManager,
|
||||
PayManager payManager,
|
||||
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
|
||||
IRechargeService rechargeService)
|
||||
ILogger<PayService> logger,
|
||||
ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
|
||||
IRechargeService rechargeService,
|
||||
PremiumPackageManager premiumPackageManager)
|
||||
{
|
||||
_alipayManager = alipayManager;
|
||||
_payManager = payManager;
|
||||
_logger = logger;
|
||||
_payOrderRepository = payOrderRepository;
|
||||
_rechargeService = rechargeService;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,7 +56,7 @@ public class PayService : ApplicationService, IPayService
|
||||
[HttpPost("pay/Order")]
|
||||
public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input)
|
||||
{
|
||||
// 1. 通过PayManager创建订单
|
||||
// 1. 通过PayManager创建订单(内部会验证VIP资格)
|
||||
var order = await _payManager.CreateOrderAsync(input.GoodsType);
|
||||
|
||||
// 2. 通过AlipayManager发起页面支付
|
||||
@@ -92,7 +97,6 @@ public class PayService : ApplicationService, IPayService
|
||||
// 2. 验证签名
|
||||
await _alipayManager.VerifyNotifyAsync(notifyData);
|
||||
|
||||
|
||||
// 3. 记录支付通知
|
||||
await _payManager.RecordPayNoticeAsync(notifyData, signStr);
|
||||
|
||||
@@ -108,16 +112,40 @@ public class PayService : ApplicationService, IPayService
|
||||
|
||||
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
|
||||
|
||||
//5.充值Vip
|
||||
await _rechargeService.RechargeVipAsync(new RechargeCreateInput
|
||||
// 5. 根据商品类型进行不同的处理
|
||||
if (order.GoodsType.IsPremiumPackage())
|
||||
{
|
||||
UserId = order.UserId,
|
||||
RechargeAmount = order.TotalAmount,
|
||||
Content = order.GoodsName,
|
||||
Months = order.GoodsType.GetValidMonths(),
|
||||
Remark = "自助充值",
|
||||
ContactInfo = null
|
||||
});
|
||||
// 处理尊享包商品:创建尊享包记录
|
||||
await _premiumPackageManager.CreatePremiumPackageAsync(
|
||||
order.UserId,
|
||||
order.GoodsType,
|
||||
order.TotalAmount,
|
||||
expireMonths: null // 尊享包不设置过期时间,或者可以根据需求设置
|
||||
);
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {order.UserId} 购买尊享包成功,订单号:{outTradeNo},商品:{order.GoodsName}");
|
||||
}
|
||||
else if (order.GoodsType.IsVipService())
|
||||
{
|
||||
// 处理VIP服务商品:充值VIP
|
||||
await _rechargeService.RechargeVipAsync(new RechargeCreateInput
|
||||
{
|
||||
UserId = order.UserId,
|
||||
RechargeAmount = order.TotalAmount,
|
||||
Content = order.GoodsName,
|
||||
Months = order.GoodsType.GetValidMonths(),
|
||||
Remark = "自助充值",
|
||||
ContactInfo = null
|
||||
});
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {order.UserId} 充值VIP成功,订单号:{outTradeNo},月数:{order.GoodsType.GetValidMonths()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"未知的商品类型:{order.GoodsType},订单号:{outTradeNo}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -158,6 +186,85 @@ public class PayService : ApplicationService, IPayService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品列表
|
||||
/// </summary>
|
||||
/// <returns>商品列表</returns>
|
||||
[HttpGet("pay/GoodsList")]
|
||||
public async Task<List<GoodsListOutput>> GetGoodsListAsync()
|
||||
{
|
||||
var goodsList = new List<GoodsListOutput>();
|
||||
|
||||
// 获取当前用户的累加充值金额(仅已登录用户)
|
||||
decimal totalRechargeAmount = 0m;
|
||||
if (CurrentUser.IsAuthenticated)
|
||||
{
|
||||
totalRechargeAmount = await _payManager.GetUserTotalRechargeAmountAsync(CurrentUser.GetId());
|
||||
}
|
||||
|
||||
// 遍历所有商品枚举
|
||||
foreach (GoodsTypeEnum goodsType in Enum.GetValues(typeof(GoodsTypeEnum)))
|
||||
{
|
||||
var originalPrice = goodsType.GetTotalAmount();
|
||||
decimal actualPrice = originalPrice;
|
||||
decimal? discountAmount = null;
|
||||
string? discountDescription = null;
|
||||
|
||||
// 如果是尊享包商品,计算折扣
|
||||
if (goodsType.IsPremiumPackage() && CurrentUser.IsAuthenticated)
|
||||
{
|
||||
discountAmount = goodsType.CalculateDiscount(totalRechargeAmount);
|
||||
actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount);
|
||||
|
||||
if (discountAmount > 0)
|
||||
{
|
||||
discountDescription = $"已优惠 ¥{discountAmount:F2}(累计充值每10元减1元,最多减20元)";
|
||||
}
|
||||
else
|
||||
{
|
||||
discountDescription = "累计充值每10元可减1元,最多减20元";
|
||||
}
|
||||
}
|
||||
|
||||
var goodsItem = new GoodsListOutput
|
||||
{
|
||||
GoodsName = goodsType.GetDisplayName(),
|
||||
OriginalPrice = originalPrice,
|
||||
GoodsPrice = actualPrice,
|
||||
GoodsType = goodsType,
|
||||
Remark = GetGoodsRemark(goodsType),
|
||||
DiscountAmount = discountAmount,
|
||||
DiscountDescription = discountDescription
|
||||
};
|
||||
|
||||
goodsList.Add(goodsItem);
|
||||
}
|
||||
|
||||
return goodsList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品备注信息
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>商品备注</returns>
|
||||
private string GetGoodsRemark(GoodsTypeEnum goodsType)
|
||||
{
|
||||
if (goodsType.IsPremiumPackage())
|
||||
{
|
||||
var tokenAmount = goodsType.GetTokenAmount();
|
||||
return $"尊享包服务,提供 {tokenAmount:N0} Tokens(需要VIP资格)";
|
||||
}
|
||||
else if (goodsType.IsVipService())
|
||||
{
|
||||
var validMonths = goodsType.GetValidMonths();
|
||||
var monthlyPrice = goodsType.GetMonthlyPrice();
|
||||
return $"VIP服务,有效期 {validMonths} 个月,月均价 ¥{monthlyPrice:F2}";
|
||||
}
|
||||
|
||||
return "未知商品类型";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取交易状态描述
|
||||
/// </summary>
|
||||
@@ -183,4 +290,4 @@ public class PayService : ApplicationService, IPayService
|
||||
}
|
||||
return TradeStatusEnum.WAIT_TRADE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public class PriceAttribute : Attribute
|
||||
{
|
||||
public decimal Price { get; }
|
||||
public int ValidMonths { get; }
|
||||
|
||||
|
||||
public PriceAttribute(double price, int validMonths)
|
||||
{
|
||||
Price = (decimal)price;
|
||||
@@ -26,56 +26,98 @@ public class PriceAttribute : Attribute
|
||||
public class DisplayNameAttribute : Attribute
|
||||
{
|
||||
public string DisplayName { get; }
|
||||
|
||||
|
||||
public DisplayNameAttribute(string displayName)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型特性
|
||||
/// 用于标识商品是VIP服务还是尊享包服务
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class GoodsCategoryAttribute : Attribute
|
||||
{
|
||||
public GoodsCategoryType Category { get; }
|
||||
|
||||
public GoodsCategoryAttribute(GoodsCategoryType category)
|
||||
{
|
||||
Category = category;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 商品类别类型
|
||||
/// </summary>
|
||||
public enum GoodsCategoryType
|
||||
{
|
||||
/// <summary>
|
||||
/// VIP服务
|
||||
/// </summary>
|
||||
VipService = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包服务
|
||||
/// </summary>
|
||||
PremiumPackage = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token数量特性
|
||||
/// 用于标识尊享包的token数量
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class TokenAmountAttribute : Attribute
|
||||
{
|
||||
public long TokenAmount { get; }
|
||||
|
||||
public TokenAmountAttribute(long tokenAmount)
|
||||
{
|
||||
TokenAmount = tokenAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 商品枚举
|
||||
/// </summary>
|
||||
public enum GoodsTypeEnum
|
||||
{
|
||||
// VIP服务
|
||||
[Price(29.9, 1)]
|
||||
[DisplayName("YiXinVip 1 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
YiXinVip1 = 1,
|
||||
|
||||
|
||||
[Price(83.7, 3)]
|
||||
[DisplayName("YiXinVip 3 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
YiXinVip3 = 3,
|
||||
|
||||
|
||||
[Price(155.4, 6)]
|
||||
[DisplayName("YiXinVip 6 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
YiXinVip6 = 6,
|
||||
|
||||
|
||||
[Price(183.2, 8)]
|
||||
[DisplayName("YiXinVip 8 month")]
|
||||
YiXinVip8 = 8
|
||||
// [Price(197.1, 9)]
|
||||
// [DisplayName("YiXinVip 9 month")]
|
||||
// YiXinVip9 = 9
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
YiXinVip8 = 8,
|
||||
|
||||
// 尊享包服务 - 需要VIP资格才能购买
|
||||
[Price(188.9, 0)]
|
||||
[DisplayName("Premium Package 5000W Tokens")]
|
||||
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||
[TokenAmount(5000)]
|
||||
PremiumPackage5000W = 101,
|
||||
|
||||
[Price(248.9, 0)]
|
||||
[DisplayName("Premium Package 10000W Tokens")]
|
||||
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||
[TokenAmount(10000)]
|
||||
PremiumPackage10000W = 102,
|
||||
|
||||
// [Price(0.01, 1)]
|
||||
// [DisplayName("YiXinVip Test")]
|
||||
// YiXinVipTest = 0,
|
||||
//
|
||||
// [Price(0.01, 1)]
|
||||
// [DisplayName("YiXinVip 1 month")]
|
||||
// YiXinVip1 = 1,
|
||||
//
|
||||
// [Price(0.01, 3)]
|
||||
// [DisplayName("YiXinVip 3 month")]
|
||||
// YiXinVip3 = 3,
|
||||
//
|
||||
// [Price(0.01, 6)]
|
||||
// [DisplayName("YiXinVip 6 month")]
|
||||
// YiXinVip6 = 6,
|
||||
//
|
||||
// [Price(0.01, 10)]
|
||||
// [DisplayName("YiXinVip 10 month")]
|
||||
// YiXinVip10 = 10
|
||||
}
|
||||
|
||||
public static class GoodsTypeEnumExtensions
|
||||
@@ -91,7 +133,7 @@ public static class GoodsTypeEnumExtensions
|
||||
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
|
||||
return priceAttribute?.Price ?? 0m;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品价格描述
|
||||
/// </summary>
|
||||
@@ -102,7 +144,7 @@ public static class GoodsTypeEnumExtensions
|
||||
var price = goodsType.GetTotalAmount();
|
||||
return $"¥{price:F1}";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品名称
|
||||
/// </summary>
|
||||
@@ -114,7 +156,7 @@ public static class GoodsTypeEnumExtensions
|
||||
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
|
||||
return displayNameAttribute?.DisplayName ?? goodsType.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品有效月份
|
||||
/// </summary>
|
||||
@@ -126,7 +168,7 @@ public static class GoodsTypeEnumExtensions
|
||||
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
|
||||
return priceAttribute?.ValidMonths ?? 1;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品月均价格
|
||||
/// </summary>
|
||||
@@ -138,4 +180,86 @@ public static class GoodsTypeEnumExtensions
|
||||
var validMonths = goodsType.GetValidMonths();
|
||||
return validMonths > 0 ? totalPrice / validMonths : 0m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品类别
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>商品类别</returns>
|
||||
public static GoodsCategoryType GetGoodsCategory(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||
var categoryAttribute = fieldInfo?.GetCustomAttribute<GoodsCategoryAttribute>();
|
||||
return categoryAttribute?.Category ?? GoodsCategoryType.VipService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为尊享包商品
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>是否为尊享包</returns>
|
||||
public static bool IsPremiumPackage(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
return goodsType.GetGoodsCategory() == GoodsCategoryType.PremiumPackage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为VIP服务商品
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>是否为VIP服务</returns>
|
||||
public static bool IsVipService(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
return goodsType.GetGoodsCategory() == GoodsCategoryType.VipService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取尊享包Token数量
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>Token数量</returns>
|
||||
public static long GetTokenAmount(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||
var tokenAttribute = fieldInfo?.GetCustomAttribute<TokenAmountAttribute>();
|
||||
return tokenAttribute?.TokenAmount ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算折扣金额(仅用于尊享包)
|
||||
/// 规则:每累加充值10元,减少1元,最多减少20元
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <param name="totalRechargeAmount">用户累加充值金额</param>
|
||||
/// <returns>折扣金额</returns>
|
||||
public static decimal CalculateDiscount(this GoodsTypeEnum goodsType, decimal totalRechargeAmount)
|
||||
{
|
||||
// 只有尊享包才有折扣
|
||||
if (!goodsType.IsPremiumPackage())
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
|
||||
// 每10元减1元
|
||||
var discountAmount = Math.Floor(totalRechargeAmount / 10m);
|
||||
|
||||
// 最多减少20元
|
||||
return Math.Min(discountAmount, 20m);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取折扣后的价格(仅用于尊享包)
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <param name="totalRechargeAmount">用户累加充值金额</param>
|
||||
/// <returns>折扣后的价格</returns>
|
||||
public static decimal GetDiscountedPrice(this GoodsTypeEnum goodsType, decimal totalRechargeAmount)
|
||||
{
|
||||
var originalPrice = goodsType.GetTotalAmount();
|
||||
var discount = goodsType.CalculateDiscount(totalRechargeAmount);
|
||||
var discountedPrice = originalPrice - discount;
|
||||
|
||||
// 确保价格不为负数,至少为0.01元
|
||||
return Math.Max(discountedPrice, 0.01m);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
|
||||
{
|
||||
options.Endpoint = "https://api.anthropic.com/";
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var headers = new Dictionary<string, string>
|
||||
@@ -77,7 +76,7 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
|
||||
|
||||
throw new Exception("OpenAI对话异常" + response.StatusCode.ToString());
|
||||
}
|
||||
|
||||
|
||||
var value =
|
||||
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
|
||||
cancellationToken: cancellationToken);
|
||||
@@ -95,7 +94,7 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
|
||||
{
|
||||
options.Endpoint = "https://api.anthropic.com/";
|
||||
}
|
||||
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var headers = new Dictionary<string, string>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包聚合根
|
||||
/// 用于给VIP扩展额外购买尊享token包
|
||||
/// </summary>
|
||||
[SugarTable("Ai_PremiumPackage")]
|
||||
[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)]
|
||||
public class PremiumPackageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public PremiumPackageAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
public PremiumPackageAggregateRoot(Guid userId, long totalTokens, string packageName)
|
||||
{
|
||||
UserId = userId;
|
||||
TotalTokens = totalTokens;
|
||||
RemainingTokens = totalTokens;
|
||||
PackageName = packageName;
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包名称
|
||||
/// </summary>
|
||||
public string PackageName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总用量(总token数)
|
||||
/// </summary>
|
||||
public long TotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余用量(剩余token数)
|
||||
/// </summary>
|
||||
public long RemainingTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用token数
|
||||
/// </summary>
|
||||
public long UsedTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期时间
|
||||
/// </summary>
|
||||
public DateTime? ExpireDateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否激活
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 购买金额
|
||||
/// </summary>
|
||||
public decimal PurchaseAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消耗token
|
||||
/// </summary>
|
||||
/// <param name="tokenCount">消耗的token数量</param>
|
||||
/// <returns>是否消耗成功</returns>
|
||||
public bool ConsumeTokens(long tokenCount)
|
||||
{
|
||||
if (RemainingTokens < tokenCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RemainingTokens -= tokenCount;
|
||||
UsedTokens += tokenCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可用
|
||||
/// </summary>
|
||||
/// <returns>是否可用</returns>
|
||||
public bool IsAvailable()
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RemainingTokens <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用尊享包
|
||||
/// </summary>
|
||||
public void Deactivate()
|
||||
{
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活尊享包
|
||||
/// </summary>
|
||||
public void Activate()
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置到期时间
|
||||
/// </summary>
|
||||
/// <param name="expireDateTime">到期时间</param>
|
||||
public void SetExpireDateTime(DateTime expireDateTime)
|
||||
{
|
||||
ExpireDateTime = expireDateTime;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class AiGateWayManager : DomainService
|
||||
private readonly AiMessageManager _aiMessageManager;
|
||||
private readonly UsageStatisticsManager _usageStatisticsManager;
|
||||
private readonly ISpecialCompatible _specialCompatible;
|
||||
private PremiumPackageManager? _premiumPackageManager;
|
||||
|
||||
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger,
|
||||
AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager,
|
||||
@@ -43,6 +44,9 @@ public class AiGateWayManager : DomainService
|
||||
_specialCompatible = specialCompatible;
|
||||
}
|
||||
|
||||
private PremiumPackageManager PremiumPackageManager =>
|
||||
_premiumPackageManager ??= LazyServiceProvider.LazyGetRequiredService<PremiumPackageManager>();
|
||||
|
||||
/// <summary>
|
||||
/// 获取模型
|
||||
/// </summary>
|
||||
@@ -510,6 +514,17 @@ public class AiGateWayManager : DomainService
|
||||
});
|
||||
|
||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
var totalTokens = (data.TokenUsage?.InputTokens ?? 0) + (data.TokenUsage?.OutputTokens ?? 0);
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
|
||||
if (!consumeSuccess)
|
||||
{
|
||||
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败,消耗token数: {totalTokens}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await response.WriteAsJsonAsync(data, cancellationToken);
|
||||
@@ -560,29 +575,30 @@ public class AiGateWayManager : DomainService
|
||||
{
|
||||
_logger.LogError(e, $"Ai对话异常");
|
||||
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||
var model = new AnthropicStreamDto
|
||||
{
|
||||
Message = new AnthropicChatCompletionDto
|
||||
{
|
||||
content =
|
||||
[
|
||||
new AnthropicChatCompletionDtoContent
|
||||
{
|
||||
text = errorContent,
|
||||
}
|
||||
],
|
||||
},
|
||||
Error = new AnthropicStreamErrorDto
|
||||
{
|
||||
Type = null,
|
||||
Message = errorContent
|
||||
}
|
||||
};
|
||||
var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
|
||||
throw new UserFriendlyException(errorContent);
|
||||
// var model = new AnthropicStreamDto
|
||||
// {
|
||||
// Message = new AnthropicChatCompletionDto
|
||||
// {
|
||||
// content =
|
||||
// [
|
||||
// new AnthropicChatCompletionDtoContent
|
||||
// {
|
||||
// text = errorContent,
|
||||
// }
|
||||
// ],
|
||||
// },
|
||||
// Error = new AnthropicStreamErrorDto
|
||||
// {
|
||||
// Type = null,
|
||||
// Message = errorContent
|
||||
// }
|
||||
// };
|
||||
// var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
|
||||
// {
|
||||
// ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
// });
|
||||
// await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
|
||||
}
|
||||
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||
@@ -602,6 +618,20 @@ public class AiGateWayManager : DomainService
|
||||
});
|
||||
|
||||
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (userId.HasValue && tokenUsage is not null)
|
||||
{
|
||||
var totalTokens = tokenUsage.TotalTokens??0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
|
||||
if (!consumeSuccess)
|
||||
{
|
||||
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败,消耗token数: {totalTokens}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Anthropic格式Http响应
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Pay;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
|
||||
@@ -15,14 +17,18 @@ public class PayManager : DomainService
|
||||
private readonly ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> _payNoticeRepository;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
|
||||
private readonly ISqlSugarRepository<AiRechargeAggregateRoot, Guid> _rechargeRepository;
|
||||
|
||||
public PayManager(
|
||||
ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> payNoticeRepository,
|
||||
ICurrentUser currentUser, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
|
||||
ICurrentUser currentUser,
|
||||
ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
|
||||
ISqlSugarRepository<AiRechargeAggregateRoot, Guid> rechargeRepository)
|
||||
{
|
||||
_payNoticeRepository = payNoticeRepository;
|
||||
_currentUser = currentUser;
|
||||
_payOrderRepository = payOrderRepository;
|
||||
_rechargeRepository = rechargeRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,18 +44,43 @@ public class PayManager : DomainService
|
||||
throw new UserFriendlyException("用户未登录");
|
||||
}
|
||||
|
||||
var userId = _currentUser.GetId();
|
||||
|
||||
// 如果是尊享包商品,需要验证用户是否为VIP
|
||||
if (goodsType.IsPremiumPackage())
|
||||
{
|
||||
if (!_currentUser.IsAiVip())
|
||||
{
|
||||
throw new UserFriendlyException("购买尊享包需要VIP资格,请先开通VIP");
|
||||
}
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
var outTradeNo = GenerateOutTradeNo();
|
||||
|
||||
// 获取商品信息
|
||||
var goodsName = goodsType.GetDisplayName();
|
||||
var totalAmount = goodsType.GetTotalAmount();
|
||||
|
||||
// 计算订单金额(尊享包使用折扣价格,VIP服务使用原价)
|
||||
decimal totalAmount;
|
||||
if (goodsType.IsPremiumPackage())
|
||||
{
|
||||
// 获取用户累加充值金额
|
||||
var totalRechargeAmount = await GetUserTotalRechargeAmountAsync(userId);
|
||||
// 使用折扣后的价格
|
||||
totalAmount = goodsType.GetDiscountedPrice(totalRechargeAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// VIP服务使用原价
|
||||
totalAmount = goodsType.GetTotalAmount();
|
||||
}
|
||||
|
||||
// 创建订单实体
|
||||
var payOrder = new PayOrderAggregateRoot
|
||||
{
|
||||
OutTradeNo = outTradeNo,
|
||||
UserId = _currentUser.GetId(),
|
||||
UserId = userId,
|
||||
UserName = _currentUser.UserName ?? string.Empty,
|
||||
TotalAmount = totalAmount,
|
||||
GoodsName = goodsName,
|
||||
@@ -135,4 +166,19 @@ public class PayManager : DomainService
|
||||
}
|
||||
return TradeStatusEnum.WAIT_TRADE;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户累加充值金额
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>累加充值金额</returns>
|
||||
public async Task<decimal> GetUserTotalRechargeAmountAsync(Guid userId)
|
||||
{
|
||||
var totalAmount = await _rechargeRepository
|
||||
._DbQueryable
|
||||
.Where(x => x.UserId == userId )
|
||||
.SumAsync(x => x.RechargeAmount);
|
||||
|
||||
return totalAmount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包管理器
|
||||
/// </summary>
|
||||
public class PremiumPackageManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> _premiumPackageRepository;
|
||||
private readonly ILogger<PremiumPackageManager> _logger;
|
||||
|
||||
public PremiumPackageManager(
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> premiumPackageRepository,
|
||||
ILogger<PremiumPackageManager> logger)
|
||||
{
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为用户创建尊享包
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <param name="totalAmount">支付金额</param>
|
||||
/// <param name="expireMonths">过期月数,0或null表示永久</param>
|
||||
/// <returns></returns>
|
||||
public async Task<PremiumPackageAggregateRoot> CreatePremiumPackageAsync(
|
||||
Guid userId,
|
||||
GoodsTypeEnum goodsType,
|
||||
decimal totalAmount,
|
||||
int? expireMonths = null)
|
||||
{
|
||||
if (!goodsType.IsPremiumPackage())
|
||||
{
|
||||
throw new UserFriendlyException($"商品类型 {goodsType} 不是尊享包商品");
|
||||
}
|
||||
|
||||
var tokenAmount = goodsType.GetTokenAmount();
|
||||
var packageName = goodsType.GetDisplayName();
|
||||
|
||||
var premiumPackage = new PremiumPackageAggregateRoot(userId, tokenAmount, packageName)
|
||||
{
|
||||
PurchaseAmount = totalAmount
|
||||
};
|
||||
|
||||
// 设置到期时间
|
||||
if (expireMonths.HasValue && expireMonths.Value > 0)
|
||||
{
|
||||
premiumPackage.SetExpireDateTime(DateTime.Now.AddMonths(expireMonths.Value));
|
||||
}
|
||||
|
||||
await _premiumPackageRepository.InsertAsync(premiumPackage);
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {userId} 购买尊享包成功: {packageName}, Token数量: {tokenAmount}, 金额: {totalAmount}");
|
||||
|
||||
return premiumPackage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 消耗用户尊享包的Token
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="tokenCount">需要消耗的Token数量</param>
|
||||
/// <returns>是否消耗成功</returns>
|
||||
public async Task<bool> ConsumeTokensAsync(Guid userId, long tokenCount)
|
||||
{
|
||||
// 获取用户所有可用的尊享包,按剩余token升序排列(优先消耗快用完的)
|
||||
var availablePackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
|
||||
.OrderBy(x => x.RemainingTokens)
|
||||
.ToListAsync();
|
||||
|
||||
if (!availablePackages.Any())
|
||||
{
|
||||
_logger.LogWarning($"用户 {userId} 没有可用的尊享包");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 过滤掉已过期的包
|
||||
var validPackages = availablePackages
|
||||
.Where(p => p.IsAvailable())
|
||||
.ToList();
|
||||
|
||||
if (!validPackages.Any())
|
||||
{
|
||||
_logger.LogWarning($"用户 {userId} 的尊享包已全部过期");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算总可用Token
|
||||
var totalAvailableTokens = validPackages.Sum(p => p.RemainingTokens);
|
||||
if (totalAvailableTokens < tokenCount)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
$"用户 {userId} 尊享包Token不足,需要: {tokenCount}, 可用: {totalAvailableTokens}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从可用的包中逐个扣除Token
|
||||
var remainingToConsume = tokenCount;
|
||||
foreach (var package in validPackages)
|
||||
{
|
||||
if (remainingToConsume <= 0)
|
||||
break;
|
||||
|
||||
var toConsume = Math.Min(remainingToConsume, package.RemainingTokens);
|
||||
if (package.ConsumeTokens(toConsume))
|
||||
{
|
||||
await _premiumPackageRepository.UpdateAsync(package);
|
||||
remainingToConsume -= toConsume;
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {userId} 从尊享包 {package.Id} 消耗 {toConsume} tokens, 剩余: {package.RemainingTokens}");
|
||||
}
|
||||
}
|
||||
|
||||
return remainingToConsume == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户可用的尊享包总Token数
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>可用Token总数</returns>
|
||||
public async Task<long> GetAvailableTokensAsync(Guid userId)
|
||||
{
|
||||
var packages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
|
||||
.ToListAsync();
|
||||
|
||||
return packages
|
||||
.Where(p => p.IsAvailable())
|
||||
.Sum(p => p.RemainingTokens);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户的所有尊享包
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>尊享包列表</returns>
|
||||
public async Task<List<PremiumPackageAggregateRoot>> GetUserPremiumPackagesAsync(Guid userId)
|
||||
{
|
||||
return await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用过期的尊享包
|
||||
/// </summary>
|
||||
/// <returns>停用的包数量</returns>
|
||||
public async Task<int> DeactivateExpiredPackagesAsync()
|
||||
{
|
||||
_logger.LogInformation("开始执行尊享包过期自动停用任务");
|
||||
|
||||
var now = DateTime.Now;
|
||||
var expiredPackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.IsActive && x.ExpireDateTime.HasValue && x.ExpireDateTime.Value < now)
|
||||
.ToListAsync();
|
||||
|
||||
if (!expiredPackages.Any())
|
||||
{
|
||||
_logger.LogInformation("没有找到过期的尊享包");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var package in expiredPackages)
|
||||
{
|
||||
package.Deactivate();
|
||||
await _premiumPackageRepository.UpdateAsync(package);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"成功停用 {expiredPackages.Count} 个过期的尊享包");
|
||||
return expiredPackages.Count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user