feat: 支付完成后自动为用户充值VIP并支持按商品类型计算有效期

This commit is contained in:
ccnetcore
2025-08-13 22:19:31 +08:00
parent 3a60bcc174
commit 40c0a5ac64
8 changed files with 98 additions and 24 deletions

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay; namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
@@ -10,5 +11,6 @@ public class CreateOrderInput
/// <summary> /// <summary>
/// 商品类型 /// 商品类型
/// </summary> /// </summary>
[Required]
public GoodsTypeEnum GoodsType { get; set; } public GoodsTypeEnum GoodsType { get; set; }
} }

View File

@@ -25,9 +25,10 @@ public class RechargeCreateInput
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 到期时间为空表示永久VIP /// VIP月数(为空或0表示永久VIP
/// </summary> /// </summary>
public DateTime? ExpireDateTime { get; set; } [Range(0, int.MaxValue, ErrorMessage = "月数必须大于等于0")]
public int? Months { get; set; }
/// <summary> /// <summary>
/// 备注 /// 备注

View File

@@ -1,4 +1,6 @@
namespace Yi.Framework.AiHub.Application.Contracts.IServices; using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
public interface IRechargeService public interface IRechargeService
{ {
@@ -6,4 +8,11 @@ public interface IRechargeService
/// 移除用户vip及角色 /// 移除用户vip及角色
/// </summary> /// </summary>
Task RemoveVipRoleByExpireAsync(); Task RemoveVipRoleByExpireAsync();
/// <summary>
/// 给用户充值VIP
/// </summary>
/// <param name="input">充值输入参数</param>
/// <returns></returns>
Task RechargeVipAsync(RechargeCreateInput input);
} }

View File

@@ -14,6 +14,7 @@ using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
namespace Yi.Framework.AiHub.Application.Services; namespace Yi.Framework.AiHub.Application.Services;
@@ -26,16 +27,19 @@ public class PayService : ApplicationService, IPayService
private readonly PayManager _payManager; private readonly PayManager _payManager;
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;
public PayService( public PayService(
AlipayManager alipayManager, AlipayManager alipayManager,
PayManager payManager, PayManager payManager,
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository) ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository,
IRechargeService rechargeService)
{ {
_alipayManager = alipayManager; _alipayManager = alipayManager;
_payManager = payManager; _payManager = payManager;
_logger = logger; _logger = logger;
_payOrderRepository = payOrderRepository; _payOrderRepository = payOrderRepository;
_rechargeService = rechargeService;
} }
/// <summary> /// <summary>
@@ -89,7 +93,7 @@ public class PayService : ApplicationService, IPayService
// 3. 记录支付通知 // 3. 记录支付通知
await _payManager.RecordPayNoticeAsync(notifyData,signStr); await _payManager.RecordPayNoticeAsync(notifyData, signStr);
// 4. 更新订单状态 // 4. 更新订单状态
var outTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty); var outTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty);
@@ -99,9 +103,20 @@ public class PayService : ApplicationService, IPayService
if (!string.IsNullOrEmpty(outTradeNo) && !string.IsNullOrEmpty(tradeStatus)) if (!string.IsNullOrEmpty(outTradeNo) && !string.IsNullOrEmpty(tradeStatus))
{ {
var status = ParseTradeStatus(tradeStatus); var status = ParseTradeStatus(tradeStatus);
await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo); var order = await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo);
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus); _logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
//5.充值Vip
await _rechargeService.RechargeVipAsync(new RechargeCreateInput
{
UserId = order.UserId,
RechargeAmount = order.TotalAmount,
Content = order.GoodsName,
Months = order.GoodsType.GetValidMonths(),
Remark = "自助充值",
ContactInfo = null
});
} }
else else
{ {

View File

@@ -13,7 +13,7 @@ using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services namespace Yi.Framework.AiHub.Application.Services
{ {
public class RechargeService : ApplicationService,IRechargeService public class RechargeService : ApplicationService, IRechargeService
{ {
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository; private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
@@ -57,13 +57,33 @@ namespace Yi.Framework.AiHub.Application.Services
[HttpPost("recharge/vip")] [HttpPost("recharge/vip")]
public async Task RechargeVipAsync(RechargeCreateInput input) public async Task RechargeVipAsync(RechargeCreateInput input)
{ {
DateTime? expireDateTime = null;
// 如果传入了月数,计算过期时间
if (input.Months.HasValue && input.Months.Value > 0)
{
// 直接查询该用户最大的过期时间
var maxExpireTime = await _repository._DbQueryable
.Where(x => x.UserId == input.UserId && x.ExpireDateTime.HasValue)
.MaxAsync(x => x.ExpireDateTime);
// 如果最大过期时间大于现在时间,从最大过期时间开始计算
// 否则从当天开始计算
DateTime baseDateTime = maxExpireTime.HasValue && maxExpireTime.Value > DateTime.Now
? maxExpireTime.Value
: DateTime.Now;
// 计算新的过期时间
expireDateTime = baseDateTime.AddMonths(input.Months.Value);
}
// 如果月数为空或0表示永久VIPExpireDateTime保持为null
// 创建充值记录 // 创建充值记录
var rechargeRecord = new AiRechargeAggregateRoot var rechargeRecord = new AiRechargeAggregateRoot
{ {
UserId = input.UserId, UserId = input.UserId,
RechargeAmount = input.RechargeAmount, RechargeAmount = input.RechargeAmount,
Content = input.Content, Content = input.Content,
ExpireDateTime = input.ExpireDateTime, ExpireDateTime = expireDateTime,
Remark = input.Remark, Remark = input.Remark,
ContactInfo = input.ContactInfo ContactInfo = input.ContactInfo
}; };

View File

@@ -10,10 +10,12 @@ namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public class PriceAttribute : Attribute public class PriceAttribute : Attribute
{ {
public decimal Price { get; } public decimal Price { get; }
public int ValidMonths { get; }
public PriceAttribute(double price) public PriceAttribute(double price, int validMonths)
{ {
Price = (decimal)price; Price = (decimal)price;
ValidMonths = validMonths;
} }
} }
@@ -36,23 +38,23 @@ public class DisplayNameAttribute : Attribute
/// </summary> /// </summary>
public enum GoodsTypeEnum public enum GoodsTypeEnum
{ {
[Price(0.01)] [Price(0.01, 1)]
[DisplayName("YiXinVip Test")] [DisplayName("YiXinVip Test")]
YiXinVipTest = 0, YiXinVipTest = 0,
[Price(29.9)] [Price(29.9, 1)]
[DisplayName("YiXinVip 1 month")] [DisplayName("YiXinVip 1 month")]
YiXinVip1 = 1, YiXinVip1 = 1,
[Price(80.7)] [Price(80.7, 3)]
[DisplayName("YiXinVip 3 month")] [DisplayName("YiXinVip 3 month")]
YiXinVip3 = 3, YiXinVip3 = 3,
[Price(143.9)] [Price(143.9, 6)]
[DisplayName("YiXinVip 6 month")] [DisplayName("YiXinVip 6 month")]
YiXinVip6 = 6, YiXinVip6 = 6,
[Price(199.9)] [Price(199.9, 10)]
[DisplayName("YiXinVip 10 month")] [DisplayName("YiXinVip 10 month")]
YiXinVip10 = 10 YiXinVip10 = 10
} }
@@ -93,4 +95,28 @@ public static class GoodsTypeEnumExtensions
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>(); var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.DisplayName ?? goodsType.ToString(); return displayNameAttribute?.DisplayName ?? goodsType.ToString();
} }
/// <summary>
/// 获取商品有效月份
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>有效月份</returns>
public static int GetValidMonths(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.ValidMonths ?? 1;
}
/// <summary>
/// 获取商品月均价格
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>月均价格</returns>
public static decimal GetMonthlyPrice(this GoodsTypeEnum goodsType)
{
var totalPrice = goodsType.GetTotalAmount();
var validMonths = goodsType.GetValidMonths();
return validMonths > 0 ? totalPrice / validMonths : 0m;
}
} }

View File

@@ -1,6 +1,7 @@
using Alipay.EasySDK.Factory; using Alipay.EasySDK.Factory;
using Alipay.EasySDK.Kernel.Util; using Alipay.EasySDK.Kernel.Util;
using Alipay.EasySDK.Payment.Page.Models; using Alipay.EasySDK.Payment.Page.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
@@ -9,10 +10,12 @@ namespace Yi.Framework.AiHub.Domain.Alipay;
public class AlipayManager : DomainService public class AlipayManager : DomainService
{ {
private readonly ILogger<AlipayManager> _logger; private readonly ILogger<AlipayManager> _logger;
private readonly IConfiguration _configuration;
public AlipayManager(ILogger<AlipayManager> logger) public AlipayManager(ILogger<AlipayManager> logger, IConfiguration configuration)
{ {
_logger = logger; _logger = logger;
_configuration = configuration;
} }
/// <summary> /// <summary>
@@ -27,7 +30,7 @@ public class AlipayManager : DomainService
{ {
// 2. 发起API调用以创建当面付收款二维码为例 // 2. 发起API调用以创建当面付收款二维码为例
var response = Factory.Payment.Page() var response = Factory.Payment.Page()
.Pay(productName, orderNumber, totalAmount.ToString(), "https://ccnetcore.com/pay/sucess"); .Pay(productName, orderNumber, totalAmount.ToString(), _configuration["Alipay:ReturnUrl"]);
// 3. 处理响应或异常 // 3. 处理响应或异常
if (ResponseChecker.Success(response)) if (ResponseChecker.Success(response))
{ {

View File

@@ -69,7 +69,7 @@ public class PayManager : DomainService
/// <param name="tradeStatus">交易状态</param> /// <param name="tradeStatus">交易状态</param>
/// <param name="tradeNo">支付宝交易号</param> /// <param name="tradeNo">支付宝交易号</param>
/// <returns></returns> /// <returns></returns>
public async Task UpdateOrderStatusAsync(string outTradeNo, TradeStatusEnum tradeStatus, string? tradeNo = null) public async Task<PayOrderAggregateRoot> UpdateOrderStatusAsync(string outTradeNo, TradeStatusEnum tradeStatus, string? tradeNo = null)
{ {
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == outTradeNo); var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == outTradeNo);
if (order == null) if (order == null)
@@ -84,6 +84,7 @@ public class PayManager : DomainService
} }
await _payOrderRepository.UpdateAsync(order); await _payOrderRepository.UpdateAsync(order);
return order;
} }
@@ -128,13 +129,10 @@ public class PayManager : DomainService
/// <returns></returns> /// <returns></returns>
private TradeStatusEnum ParseTradeStatus(string tradeStatus) private TradeStatusEnum ParseTradeStatus(string tradeStatus)
{ {
return tradeStatus switch if (Enum.TryParse<TradeStatusEnum>(tradeStatus, out var result))
{ {
"WAIT_BUYER_PAY" => TradeStatusEnum.WAIT_BUYER_PAY, return result;
"TRADE_SUCCESS" => TradeStatusEnum.TRADE_SUCCESS, }
"TRADE_FINISHED" => TradeStatusEnum.TRADE_FINISHED, return TradeStatusEnum.WAIT_TRADE;
"TRADE_CLOSED" => TradeStatusEnum.TRADE_CLOSED,
_ => TradeStatusEnum.WAIT_TRADE
};
} }
} }