diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/AiUserRoleMenuDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/AiUserRoleMenuDto.cs index d3297f59..fa054cca 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/AiUserRoleMenuDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/AiUserRoleMenuDto.cs @@ -8,4 +8,29 @@ public class AiUserRoleMenuDto:UserRoleMenuDto /// 是否绑定服务号 /// public bool IsBindFuwuhao { get; set; } + + /// + /// 是否为VIP用户 + /// + public bool IsVip { get; set; } + + /// + /// VIP到期时间 + /// + public DateTime? VipExpireTime { get; set; } + + /// + /// 尊享包总Token数 + /// + public long PremiumTotalTokens { get; set; } + + /// + /// 尊享包已使用Token数 + /// + public long PremiumUsedTokens { get; set; } + + /// + /// 尊享包剩余Token数 + /// + public long PremiumRemainingTokens { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Pay/GoodsListOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Pay/GoodsListOutput.cs new file mode 100644 index 00000000..f6696356 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Pay/GoodsListOutput.cs @@ -0,0 +1,44 @@ +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay; + +/// +/// 商品列表输出DTO +/// +public class GoodsListOutput +{ + /// + /// 商品名称 + /// + public string GoodsName { get; set; } + + /// + /// 商品原价 + /// + public decimal OriginalPrice { get; set; } + + /// + /// 商品实际价格(折扣后的价格) + /// + public decimal GoodsPrice { get; set; } + + /// + /// 商品类型 + /// + public GoodsTypeEnum GoodsType { get; set; } + + /// + /// 商品备注 + /// + public string Remark { get; set; } + + /// + /// 折扣金额(仅尊享包) + /// + public decimal? DiscountAmount { get; set; } + + /// + /// 折扣说明(仅尊享包) + /// + public string? DiscountDescription { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IPayService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IPayService.cs index 1f303501..55edcacf 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IPayService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IPayService.cs @@ -30,4 +30,10 @@ public interface IPayService : IApplicationService /// 查询订单状态输入 /// 订单状态信息 Task QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input); + + /// + /// 获取商品列表 + /// + /// 商品列表 + Task> GetGoodsListAsync(); } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiAccountService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiAccountService.cs index e5290f0d..c1a15efc 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiAccountService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiAccountService.cs @@ -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 _userRepository; + private ISqlSugarRepository _rechargeRepository; + private ISqlSugarRepository _premiumPackageRepository; - public AiAccountService(IAccountService accountService, ISqlSugarRepository userRepository) + public AiAccountService( + IAccountService accountService, + ISqlSugarRepository userRepository, + ISqlSugarRepository rechargeRepository, + ISqlSugarRepository premiumPackageRepository) { _accountService = accountService; _userRepository = userRepository; + _rechargeRepository = rechargeRepository; + _premiumPackageRepository = premiumPackageRepository; } /// @@ -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(); + + // 是否绑定服务号 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; } } \ No newline at end of file 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 7c004ac6..8fba666b 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 @@ -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 _aiModelRepository; private readonly AiBlacklistManager _aiBlacklistManager; + private readonly IAccountService _accountService; + private readonly PremiumPackageManager _premiumPackageManager; public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger logger, TokenManager tokenManager, AiGateWayManager aiGateWayManager, - ISqlSugarRepository aiModelRepository, AiBlacklistManager aiBlacklistManager) + ISqlSugarRepository 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; } /// @@ -120,7 +129,7 @@ public class OpenApiService : ApplicationService /// - /// Anthropic对话 + /// Anthropic对话(尊享服务专用) /// /// /// @@ -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) { diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/PayService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/PayService.cs index ec4b3d4b..872a1959 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/PayService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/PayService.cs @@ -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 _logger; private readonly ISqlSugarRepository _payOrderRepository; private readonly IRechargeService _rechargeService; + private readonly PremiumPackageManager _premiumPackageManager; public PayService( AlipayManager alipayManager, PayManager payManager, - ILogger logger, ISqlSugarRepository payOrderRepository, - IRechargeService rechargeService) + ILogger logger, + ISqlSugarRepository payOrderRepository, + IRechargeService rechargeService, + PremiumPackageManager premiumPackageManager) { _alipayManager = alipayManager; _payManager = payManager; _logger = logger; _payOrderRepository = payOrderRepository; _rechargeService = rechargeService; + _premiumPackageManager = premiumPackageManager; } /// @@ -51,7 +56,7 @@ public class PayService : ApplicationService, IPayService [HttpPost("pay/Order")] public async Task 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 }; } + /// + /// 获取商品列表 + /// + /// 商品列表 + [HttpGet("pay/GoodsList")] + public async Task> GetGoodsListAsync() + { + var goodsList = new List(); + + // 获取当前用户的累加充值金额(仅已登录用户) + 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; + } + + /// + /// 获取商品备注信息 + /// + /// 商品类型 + /// 商品备注 + 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 "未知商品类型"; + } + /// /// 获取交易状态描述 /// @@ -183,4 +290,4 @@ public class PayService : ApplicationService, IPayService } return TradeStatusEnum.WAIT_TRADE; } -} \ No newline at end of file +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/GoodsTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/GoodsTypeEnum.cs index 711b3d07..0f1016ff 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/GoodsTypeEnum.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/GoodsTypeEnum.cs @@ -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; } } +/// +/// 商品类型特性 +/// 用于标识商品是VIP服务还是尊享包服务 +/// +[AttributeUsage(AttributeTargets.Field)] +public class GoodsCategoryAttribute : Attribute +{ + public GoodsCategoryType Category { get; } + + public GoodsCategoryAttribute(GoodsCategoryType category) + { + Category = category; + } +} + +/// +/// 商品类别类型 +/// +public enum GoodsCategoryType +{ + /// + /// VIP服务 + /// + VipService = 1, + + /// + /// 尊享包服务 + /// + PremiumPackage = 2 +} + +/// +/// Token数量特性 +/// 用于标识尊享包的token数量 +/// +[AttributeUsage(AttributeTargets.Field)] +public class TokenAmountAttribute : Attribute +{ + public long TokenAmount { get; } + + public TokenAmountAttribute(long tokenAmount) + { + TokenAmount = tokenAmount; + } +} + /// /// 商品枚举 /// 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(); return priceAttribute?.Price ?? 0m; } - + /// /// 获取商品价格描述 /// @@ -102,7 +144,7 @@ public static class GoodsTypeEnumExtensions var price = goodsType.GetTotalAmount(); return $"¥{price:F1}"; } - + /// /// 获取商品名称 /// @@ -114,7 +156,7 @@ public static class GoodsTypeEnumExtensions var displayNameAttribute = fieldInfo?.GetCustomAttribute(); return displayNameAttribute?.DisplayName ?? goodsType.ToString(); } - + /// /// 获取商品有效月份 /// @@ -126,7 +168,7 @@ public static class GoodsTypeEnumExtensions var priceAttribute = fieldInfo?.GetCustomAttribute(); return priceAttribute?.ValidMonths ?? 1; } - + /// /// 获取商品月均价格 /// @@ -138,4 +180,86 @@ public static class GoodsTypeEnumExtensions var validMonths = goodsType.GetValidMonths(); return validMonths > 0 ? totalPrice / validMonths : 0m; } + + /// + /// 获取商品类别 + /// + /// 商品类型 + /// 商品类别 + public static GoodsCategoryType GetGoodsCategory(this GoodsTypeEnum goodsType) + { + var fieldInfo = goodsType.GetType().GetField(goodsType.ToString()); + var categoryAttribute = fieldInfo?.GetCustomAttribute(); + return categoryAttribute?.Category ?? GoodsCategoryType.VipService; + } + + /// + /// 是否为尊享包商品 + /// + /// 商品类型 + /// 是否为尊享包 + public static bool IsPremiumPackage(this GoodsTypeEnum goodsType) + { + return goodsType.GetGoodsCategory() == GoodsCategoryType.PremiumPackage; + } + + /// + /// 是否为VIP服务商品 + /// + /// 商品类型 + /// 是否为VIP服务 + public static bool IsVipService(this GoodsTypeEnum goodsType) + { + return goodsType.GetGoodsCategory() == GoodsCategoryType.VipService; + } + + /// + /// 获取尊享包Token数量 + /// + /// 商品类型 + /// Token数量 + public static long GetTokenAmount(this GoodsTypeEnum goodsType) + { + var fieldInfo = goodsType.GetType().GetField(goodsType.ToString()); + var tokenAttribute = fieldInfo?.GetCustomAttribute(); + return tokenAttribute?.TokenAmount ?? 0; + } + + /// + /// 计算折扣金额(仅用于尊享包) + /// 规则:每累加充值10元,减少1元,最多减少20元 + /// + /// 商品类型 + /// 用户累加充值金额 + /// 折扣金额 + 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); + } + + /// + /// 获取折扣后的价格(仅用于尊享包) + /// + /// 商品类型 + /// 用户累加充值金额 + /// 折扣后的价格 + 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); + } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs index 7573bfec..e08b95e2 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorClaude/Chats/AnthropicChatCompletionsService.cs @@ -22,7 +22,6 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor { options.Endpoint = "https://api.anthropic.com/"; } - var client = httpClientFactory.CreateClient(); var headers = new Dictionary @@ -77,7 +76,7 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor throw new Exception("OpenAI对话异常" + response.StatusCode.ToString()); } - + var value = await response.Content.ReadFromJsonAsync(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 diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/PremiumPackageAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/PremiumPackageAggregateRoot.cs new file mode 100644 index 00000000..2bda12f6 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/PremiumPackageAggregateRoot.cs @@ -0,0 +1,147 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Yi.Framework.AiHub.Domain.Entities; + +/// +/// 尊享包聚合根 +/// 用于给VIP扩展额外购买尊享token包 +/// +[SugarTable("Ai_PremiumPackage")] +[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)] +public class PremiumPackageAggregateRoot : FullAuditedAggregateRoot +{ + public PremiumPackageAggregateRoot() + { + } + + public PremiumPackageAggregateRoot(Guid userId, long totalTokens, string packageName) + { + UserId = userId; + TotalTokens = totalTokens; + RemainingTokens = totalTokens; + PackageName = packageName; + IsActive = true; + } + + /// + /// 用户ID + /// + public Guid UserId { get; set; } + + /// + /// 包名称 + /// + public string PackageName { get; set; } + + /// + /// 总用量(总token数) + /// + public long TotalTokens { get; set; } + + /// + /// 剩余用量(剩余token数) + /// + public long RemainingTokens { get; set; } + + /// + /// 已使用token数 + /// + public long UsedTokens { get; set; } + + /// + /// 到期时间 + /// + public DateTime? ExpireDateTime { get; set; } + + /// + /// 是否激活 + /// + public bool IsActive { get; set; } + + /// + /// 购买金额 + /// + public decimal PurchaseAmount { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 消耗token + /// + /// 消耗的token数量 + /// 是否消耗成功 + 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; + } + + /// + /// 检查是否可用 + /// + /// 是否可用 + public bool IsAvailable() + { + if (!IsActive) + { + return false; + } + + if (RemainingTokens <= 0) + { + return false; + } + + if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now) + { + return false; + } + + return true; + } + + /// + /// 停用尊享包 + /// + public void Deactivate() + { + IsActive = false; + } + + /// + /// 激活尊享包 + /// + public void Activate() + { + IsActive = true; + } + + /// + /// 设置到期时间 + /// + /// 到期时间 + public void SetExpireDateTime(DateTime expireDateTime) + { + ExpireDateTime = expireDateTime; + } +} 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 8905f2c0..22c31057 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 @@ -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 aiAppRepository, ILogger logger, AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager, @@ -43,6 +44,9 @@ public class AiGateWayManager : DomainService _specialCompatible = specialCompatible; } + private PremiumPackageManager PremiumPackageManager => + _premiumPackageManager ??= LazyServiceProvider.LazyGetRequiredService(); + /// /// 获取模型 /// @@ -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响应 diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PayManager.cs index 4939005b..220c218a 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PayManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PayManager.cs @@ -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 _payNoticeRepository; private readonly ICurrentUser _currentUser; private readonly ISqlSugarRepository _payOrderRepository; + private readonly ISqlSugarRepository _rechargeRepository; public PayManager( ISqlSugarRepository payNoticeRepository, - ICurrentUser currentUser, ISqlSugarRepository payOrderRepository) + ICurrentUser currentUser, + ISqlSugarRepository payOrderRepository, + ISqlSugarRepository rechargeRepository) { _payNoticeRepository = payNoticeRepository; _currentUser = currentUser; _payOrderRepository = payOrderRepository; + _rechargeRepository = rechargeRepository; } /// @@ -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; } -} \ No newline at end of file + + /// + /// 获取用户累加充值金额 + /// + /// 用户ID + /// 累加充值金额 + public async Task GetUserTotalRechargeAmountAsync(Guid userId) + { + var totalAmount = await _rechargeRepository + ._DbQueryable + .Where(x => x.UserId == userId ) + .SumAsync(x => x.RechargeAmount); + + return totalAmount; + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PremiumPackageManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PremiumPackageManager.cs new file mode 100644 index 00000000..f666bd96 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/PremiumPackageManager.cs @@ -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; + +/// +/// 尊享包管理器 +/// +public class PremiumPackageManager : DomainService +{ + private readonly ISqlSugarRepository _premiumPackageRepository; + private readonly ILogger _logger; + + public PremiumPackageManager( + ISqlSugarRepository premiumPackageRepository, + ILogger logger) + { + _premiumPackageRepository = premiumPackageRepository; + _logger = logger; + } + + /// + /// 为用户创建尊享包 + /// + /// 用户ID + /// 商品类型 + /// 支付金额 + /// 过期月数,0或null表示永久 + /// + public async Task 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; + } + + /// + /// 消耗用户尊享包的Token + /// + /// 用户ID + /// 需要消耗的Token数量 + /// 是否消耗成功 + public async Task 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; + } + + /// + /// 获取用户可用的尊享包总Token数 + /// + /// 用户ID + /// 可用Token总数 + public async Task 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); + } + + /// + /// 获取用户的所有尊享包 + /// + /// 用户ID + /// 尊享包列表 + public async Task> GetUserPremiumPackagesAsync(Guid userId) + { + return await _premiumPackageRepository._DbQueryable + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.CreationTime) + .ToListAsync(); + } + + /// + /// 停用过期的尊享包 + /// + /// 停用的包数量 + public async Task 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; + } +}