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..14df22fb 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 @@ -28,18 +28,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 +55,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 +96,6 @@ public class PayService : ApplicationService, IPayService // 2. 验证签名 await _alipayManager.VerifyNotifyAsync(notifyData); - // 3. 记录支付通知 await _payManager.RecordPayNoticeAsync(notifyData, signStr); @@ -108,16 +111,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 { @@ -183,4 +210,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..fa17a001 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,37 +26,114 @@ 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 + [GoodsCategory(GoodsCategoryType.VipService)] + YiXinVip8 = 8, + + // 尊享包服务 - 需要VIP资格才能购买 + [Price(49.9, 0)] + [DisplayName("Premium Package 5M Tokens")] + [GoodsCategory(GoodsCategoryType.PremiumPackage)] + [TokenAmount(5_000_000)] + PremiumPackage5M = 101, + + [Price(99.9, 0)] + [DisplayName("Premium Package 10M Tokens")] + [GoodsCategory(GoodsCategoryType.PremiumPackage)] + [TokenAmount(10_000_000)] + PremiumPackage10M = 102, + + [Price(199.9, 0)] + [DisplayName("Premium Package 25M Tokens")] + [GoodsCategory(GoodsCategoryType.PremiumPackage)] + [TokenAmount(25_000_000)] + PremiumPackage25M = 103, + + [Price(399.9, 0)] + [DisplayName("Premium Package 50M Tokens")] + [GoodsCategory(GoodsCategoryType.PremiumPackage)] + [TokenAmount(50_000_000)] + PremiumPackage50M = 104 + // [Price(197.1, 9)] // [DisplayName("YiXinVip 9 month")] // YiXinVip9 = 9 - + // [Price(0.01, 1)] // [DisplayName("YiXinVip Test")] // YiXinVipTest = 0, @@ -91,7 +168,7 @@ public static class GoodsTypeEnumExtensions var priceAttribute = fieldInfo?.GetCustomAttribute(); return priceAttribute?.Price ?? 0m; } - + /// /// 获取商品价格描述 /// @@ -102,7 +179,7 @@ public static class GoodsTypeEnumExtensions var price = goodsType.GetTotalAmount(); return $"¥{price:F1}"; } - + /// /// 获取商品名称 /// @@ -114,7 +191,7 @@ public static class GoodsTypeEnumExtensions var displayNameAttribute = fieldInfo?.GetCustomAttribute(); return displayNameAttribute?.DisplayName ?? goodsType.ToString(); } - + /// /// 获取商品有效月份 /// @@ -126,7 +203,7 @@ public static class GoodsTypeEnumExtensions var priceAttribute = fieldInfo?.GetCustomAttribute(); return priceAttribute?.ValidMonths ?? 1; } - + /// /// 获取商品月均价格 /// @@ -138,4 +215,48 @@ 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; + } } 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..d0e2ca9b 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 @@ -560,29 +560,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, 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..288ee54c 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 @@ -4,6 +4,7 @@ using Volo.Abp.Users; 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; @@ -38,6 +39,15 @@ public class PayManager : DomainService throw new UserFriendlyException("用户未登录"); } + // 如果是尊享包商品,需要验证用户是否为VIP + if (goodsType.IsPremiumPackage()) + { + if (!_currentUser.IsAiVip()) + { + throw new UserFriendlyException("购买尊享包需要VIP资格,请先开通VIP"); + } + } + // 生成订单号 var outTradeNo = GenerateOutTradeNo(); @@ -135,4 +145,4 @@ public class PayManager : DomainService } return TradeStatusEnum.WAIT_TRADE; } -} \ No newline at end of file +} 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; + } +}