小程序0元支付处理

This commit is contained in:
马梓彭 2026-08-07 10:13:17 +08:00
parent 95325a7fa6
commit 4a9a0dfd60
3 changed files with 69 additions and 11 deletions

View File

@ -738,6 +738,21 @@ public class PaymentInfoDto
/// 微信支付参数
/// </summary>
public object? WechatPayParams { get; set; }
/// <summary>
/// 是否为 0 元成交(优惠券全额抵扣 / 免费赛事)
/// </summary>
/// <remarks>
/// 前端务必判断:IsFree == true 或 PaymentStatus == 1 时,订单已成交,
/// 不要再调起微信支付(此时 WechatPayParams 为 null,调起会报“支付失败”),
/// 应直接跳转支付成功页。
/// </remarks>
public bool IsFree { get; set; }
/// <summary>
/// 支付状态 (0-待支付/待审核 1-已支付/已成交)
/// </summary>
public int PaymentStatus { get; set; }
}
/// <summary>

View File

@ -776,7 +776,9 @@ public class EventClientController : IDynamicApiController
BankAccountName = merchant?.MerchantName,
BankAccountNo = merchant?.AccountNumber,
BankName = merchant?.BankName,
WechatPayParams = unifiedResult.WechatPayParams
WechatPayParams = unifiedResult.WechatPayParams,
IsFree = unifiedResult.IsFree,
PaymentStatus = unifiedResult.PaymentStatus
};
return paymentInfo;

View File

@ -299,8 +299,7 @@ public class UnifiedRegistrationService : ITransient
if (registration.PaymentStatus == 1)
{
_logger.LogInformation("报名已成交,幂等返回,报名ID: {RegistrationId}", registration.Id);
var paidOrder = await _orderRepository.GetFirstAsync(o =>
o.RegistrationId == registration.Id && !o.DeleteMark);
var paidOrder = await GetSettledOrderAsync(registration.Id);
var paidAmount = paidOrder?.ActualAmount ?? 0;
return new UnifiedPaymentOutput
{
@ -996,15 +995,28 @@ public class UnifiedRegistrationService : ITransient
{
var now = DateTime.Now;
// 1. 确保有订单号(预报名创建时 OrderNo 为空字符串)
// 注意:0元成交不涉及外部支付接口,订单号首次生成后复用即可。
// 与付费支付的"强制重新生成"策略不同,因为无需应对微信支付的参数一致性约束。
// 幂等保护已在 ProcessPaymentAsync:L299 完成,此处实际每个报名订单只执行一次。
if (string.IsNullOrEmpty(registration.OrderNo))
// 1. 作废该报名下遗留的「原价待付」订单,并强制重新生成订单号。
// 场景:用户先未选券打开支付页 → payment-info 走付费分支,已生成 OrderNo 并建了原价待付单
// (还调过微信统一下单);随后选上全额券再次进来变成 0 元成交。
// 若沿用旧 OrderNo,会出现两条 OrderType=1 且单号相同的订单,导致:
// ① 按 OrderNo 反查订单(如 HandlePaymentSuccessAsync)取错行;
// ② 算实付金额时取到原价待付单 → 退款按原价计算,实付 0 却退原价,造成资损。
var staleOrders = await _orderRepository.GetListAsync(o =>
o.RegistrationId == registration.Id && o.OrderType == 1
&& !o.DeleteMark && o.OrderStatus == OrderStatus.Pending);
foreach (var stale in staleOrders)
{
registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
stale.OrderStatus = OrderStatus.Cancelled;
stale.Remark = (stale.Remark ?? "") + " [系统]优惠券全额抵扣/免费成交,原价待付单作废";
stale.UpdatedTime = now;
await _orderRepository.UpdateAsync(stale);
_logger.LogInformation("作废原价待付订单,报名ID: {RegistrationId}, 订单号: {OrderNo}",
registration.Id, stale.OrderNo);
}
// 强制重新生成订单号:保证 0 元成交单的单号唯一,与付费分支的策略保持一致
registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
// 2. 报名记录置为已支付(免费成交)
registration.PaymentStatus = 1;
registration.PaymentMethod = 2; // 免费赛事统一按线上支付方式,满足小程序列表放行条件
@ -1081,6 +1093,35 @@ public class UnifiedRegistrationService : ITransient
registration.Id, registration.OrderNo);
}
/// <summary>
/// 获取报名对应的「有效收款订单」,用于判定实付金额。
/// </summary>
/// <remarks>
/// 一个报名下可能残留多条 OrderType=1 的订单(典型:用户先未选券打开支付页生成了原价待付单,
/// 之后再用全额券 0 元成交)。直接用 GetFirstAsync 取不带排序、不筛状态,
/// 可能取到原价待付单,把实付金额误判为原价 —— 退款就会按原价退给实付 0 元的用户,造成资损。
/// 因此这里优先取「已支付」单(0 元成交单同样是 Paid),按支付时间取最新;
/// 没有已支付单时(如线下转账待审核)再回退到最近一条未取消的订单,保持原有行为不变。
/// </remarks>
private async Task<YD_Event.Core.Entities.Order?> GetSettledOrderAsync(long registrationId)
{
var orders = await _orderRepository.GetListAsync(o =>
o.RegistrationId == registrationId && o.OrderType == 1 && !o.DeleteMark);
if (orders == null || orders.Count == 0) return null;
var paid = orders
.Where(o => o.OrderStatus == OrderStatus.Paid)
.OrderByDescending(o => o.PaymentTime ?? o.CreatedTime)
.FirstOrDefault();
if (paid != null) return paid;
return orders
.Where(o => o.OrderStatus != OrderStatus.Cancelled)
.OrderByDescending(o => o.CreatedTime)
.FirstOrDefault();
}
/// <summary>
/// 创建订单记录
/// </summary>
@ -3387,7 +3428,7 @@ public class UnifiedRegistrationService : ITransient
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId);
// 查询订单获取实际支付金额(考虑优惠券抵扣)
var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == input.RegistrationId && x.OrderType == 1 && !x.DeleteMark);
var order = await GetSettledOrderAsync(input.RegistrationId);
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
var originalAmount = registration.TotalAmount;
@ -3765,7 +3806,7 @@ public class UnifiedRegistrationService : ITransient
// 计算实际退款金额
// 【重要】始终信任后端计算结果,前端计算只用于展示
// 不接受前端传递的退款金额,避免资金风险
var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registration.Id && o.OrderType == 1 && !o.DeleteMark);
var order = await GetSettledOrderAsync(registration.Id);
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
var originalAmount = registration.TotalAmount;