feat: 完成尊享服务

This commit is contained in:
ccnetcore
2025-10-12 16:42:26 +08:00
parent 5934056fe6
commit 4d09243efd
7 changed files with 541 additions and 52 deletions

View File

@@ -28,18 +28,22 @@ public class PayService : ApplicationService, IPayService
private readonly ILogger<PayService> _logger; private readonly ILogger<PayService> _logger;
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository; private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
private readonly IRechargeService _rechargeService; private readonly IRechargeService _rechargeService;
private readonly PremiumPackageManager _premiumPackageManager;
public PayService( public PayService(
AlipayManager alipayManager, AlipayManager alipayManager,
PayManager payManager, PayManager payManager,
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository, ILogger<PayService> logger,
IRechargeService rechargeService) ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
IRechargeService rechargeService,
PremiumPackageManager premiumPackageManager)
{ {
_alipayManager = alipayManager; _alipayManager = alipayManager;
_payManager = payManager; _payManager = payManager;
_logger = logger; _logger = logger;
_payOrderRepository = payOrderRepository; _payOrderRepository = payOrderRepository;
_rechargeService = rechargeService; _rechargeService = rechargeService;
_premiumPackageManager = premiumPackageManager;
} }
/// <summary> /// <summary>
@@ -51,7 +55,7 @@ public class PayService : ApplicationService, IPayService
[HttpPost("pay/Order")] [HttpPost("pay/Order")]
public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input) public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input)
{ {
// 1. 通过PayManager创建订单 // 1. 通过PayManager创建订单内部会验证VIP资格
var order = await _payManager.CreateOrderAsync(input.GoodsType); var order = await _payManager.CreateOrderAsync(input.GoodsType);
// 2. 通过AlipayManager发起页面支付 // 2. 通过AlipayManager发起页面支付
@@ -92,7 +96,6 @@ public class PayService : ApplicationService, IPayService
// 2. 验证签名 // 2. 验证签名
await _alipayManager.VerifyNotifyAsync(notifyData); await _alipayManager.VerifyNotifyAsync(notifyData);
// 3. 记录支付通知 // 3. 记录支付通知
await _payManager.RecordPayNoticeAsync(notifyData, signStr); await _payManager.RecordPayNoticeAsync(notifyData, signStr);
@@ -108,16 +111,40 @@ public class PayService : ApplicationService, IPayService
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus); _logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
//5.充值Vip // 5. 根据商品类型进行不同的处理
await _rechargeService.RechargeVipAsync(new RechargeCreateInput if (order.GoodsType.IsPremiumPackage())
{ {
UserId = order.UserId, // 处理尊享包商品:创建尊享包记录
RechargeAmount = order.TotalAmount, await _premiumPackageManager.CreatePremiumPackageAsync(
Content = order.GoodsName, order.UserId,
Months = order.GoodsType.GetValidMonths(), order.GoodsType,
Remark = "自助充值", order.TotalAmount,
ContactInfo = null 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 else
{ {

View File

@@ -33,26 +33,103 @@ public class DisplayNameAttribute : Attribute
} }
} }
/// <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>
/// 商品枚举 /// 商品枚举
/// </summary> /// </summary>
public enum GoodsTypeEnum public enum GoodsTypeEnum
{ {
// VIP服务
[Price(29.9, 1)] [Price(29.9, 1)]
[DisplayName("YiXinVip 1 month")] [DisplayName("YiXinVip 1 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip1 = 1, YiXinVip1 = 1,
[Price(83.7, 3)] [Price(83.7, 3)]
[DisplayName("YiXinVip 3 month")] [DisplayName("YiXinVip 3 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip3 = 3, YiXinVip3 = 3,
[Price(155.4, 6)] [Price(155.4, 6)]
[DisplayName("YiXinVip 6 month")] [DisplayName("YiXinVip 6 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip6 = 6, YiXinVip6 = 6,
[Price(183.2, 8)] [Price(183.2, 8)]
[DisplayName("YiXinVip 8 month")] [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)] // [Price(197.1, 9)]
// [DisplayName("YiXinVip 9 month")] // [DisplayName("YiXinVip 9 month")]
// YiXinVip9 = 9 // YiXinVip9 = 9
@@ -138,4 +215,48 @@ public static class GoodsTypeEnumExtensions
var validMonths = goodsType.GetValidMonths(); var validMonths = goodsType.GetValidMonths();
return validMonths > 0 ? totalPrice / validMonths : 0m; 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;
}
} }

View File

@@ -22,7 +22,6 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
{ {
options.Endpoint = "https://api.anthropic.com/"; options.Endpoint = "https://api.anthropic.com/";
} }
var client = httpClientFactory.CreateClient(); var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string> var headers = new Dictionary<string, string>

View File

@@ -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;
}
}

View File

@@ -560,29 +560,30 @@ public class AiGateWayManager : DomainService
{ {
_logger.LogError(e, $"Ai对话异常"); _logger.LogError(e, $"Ai对话异常");
var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}"; var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
var model = new AnthropicStreamDto throw new UserFriendlyException(errorContent);
{ // var model = new AnthropicStreamDto
Message = new AnthropicChatCompletionDto // {
{ // Message = new AnthropicChatCompletionDto
content = // {
[ // content =
new AnthropicChatCompletionDtoContent // [
{ // new AnthropicChatCompletionDtoContent
text = errorContent, // {
} // text = errorContent,
], // }
}, // ],
Error = new AnthropicStreamErrorDto // },
{ // Error = new AnthropicStreamErrorDto
Type = null, // {
Message = errorContent // Type = null,
} // Message = errorContent
}; // }
var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings // };
{ // var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
ContractResolver = new CamelCasePropertyNamesContractResolver() // {
}); // ContractResolver = new CamelCasePropertyNamesContractResolver()
await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions); // });
// await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
} }
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,

View File

@@ -4,6 +4,7 @@ using Volo.Abp.Users;
using Yi.Framework.AiHub.Domain.Entities.Pay; using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.AiHub.Domain.Extensions;
namespace Yi.Framework.AiHub.Domain.Managers; namespace Yi.Framework.AiHub.Domain.Managers;
@@ -38,6 +39,15 @@ public class PayManager : DomainService
throw new UserFriendlyException("用户未登录"); throw new UserFriendlyException("用户未登录");
} }
// 如果是尊享包商品需要验证用户是否为VIP
if (goodsType.IsPremiumPackage())
{
if (!_currentUser.IsAiVip())
{
throw new UserFriendlyException("购买尊享包需要VIP资格请先开通VIP");
}
}
// 生成订单号 // 生成订单号
var outTradeNo = GenerateOutTradeNo(); var outTradeNo = GenerateOutTradeNo();

View File

@@ -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;
}
}