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 14df22fb..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; @@ -185,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 "未知商品类型"; + } + /// /// 获取交易状态描述 /// 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 fa17a001..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 @@ -106,53 +106,18 @@ public enum GoodsTypeEnum YiXinVip8 = 8, // 尊享包服务 - 需要VIP资格才能购买 - [Price(49.9, 0)] - [DisplayName("Premium Package 5M Tokens")] + [Price(188.9, 0)] + [DisplayName("Premium Package 5000W Tokens")] [GoodsCategory(GoodsCategoryType.PremiumPackage)] - [TokenAmount(5_000_000)] - PremiumPackage5M = 101, + [TokenAmount(5000)] + PremiumPackage5000W = 101, - [Price(99.9, 0)] - [DisplayName("Premium Package 10M Tokens")] + [Price(248.9, 0)] + [DisplayName("Premium Package 10000W 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, - // - // [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 + [TokenAmount(10000)] + PremiumPackage10000W = 102, + } public static class GoodsTypeEnumExtensions @@ -259,4 +224,42 @@ public static class GoodsTypeEnumExtensions 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/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index d0e2ca9b..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); @@ -603,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 288ee54c..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,6 +1,7 @@ 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; @@ -16,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; } /// @@ -39,6 +44,8 @@ public class PayManager : DomainService throw new UserFriendlyException("用户未登录"); } + var userId = _currentUser.GetId(); + // 如果是尊享包商品,需要验证用户是否为VIP if (goodsType.IsPremiumPackage()) { @@ -53,13 +60,27 @@ public class PayManager : DomainService // 获取商品信息 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, @@ -145,4 +166,19 @@ public class PayManager : DomainService } return TradeStatusEnum.WAIT_TRADE; } + + /// + /// 获取用户累加充值金额 + /// + /// 用户ID + /// 累加充值金额 + public async Task GetUserTotalRechargeAmountAsync(Guid userId) + { + var totalAmount = await _rechargeRepository + ._DbQueryable + .Where(x => x.UserId == userId ) + .SumAsync(x => x.RechargeAmount); + + return totalAmount; + } }