小程序0元支付处理
This commit is contained in:
parent
95325a7fa6
commit
4a9a0dfd60
@ -738,6 +738,21 @@ public class PaymentInfoDto
|
|||||||
/// 微信支付参数
|
/// 微信支付参数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object? WechatPayParams { get; set; }
|
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>
|
/// <summary>
|
||||||
|
|||||||
@ -776,7 +776,9 @@ public class EventClientController : IDynamicApiController
|
|||||||
BankAccountName = merchant?.MerchantName,
|
BankAccountName = merchant?.MerchantName,
|
||||||
BankAccountNo = merchant?.AccountNumber,
|
BankAccountNo = merchant?.AccountNumber,
|
||||||
BankName = merchant?.BankName,
|
BankName = merchant?.BankName,
|
||||||
WechatPayParams = unifiedResult.WechatPayParams
|
WechatPayParams = unifiedResult.WechatPayParams,
|
||||||
|
IsFree = unifiedResult.IsFree,
|
||||||
|
PaymentStatus = unifiedResult.PaymentStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
return paymentInfo;
|
return paymentInfo;
|
||||||
|
|||||||
@ -299,8 +299,7 @@ public class UnifiedRegistrationService : ITransient
|
|||||||
if (registration.PaymentStatus == 1)
|
if (registration.PaymentStatus == 1)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("报名已成交,幂等返回,报名ID: {RegistrationId}", registration.Id);
|
_logger.LogInformation("报名已成交,幂等返回,报名ID: {RegistrationId}", registration.Id);
|
||||||
var paidOrder = await _orderRepository.GetFirstAsync(o =>
|
var paidOrder = await GetSettledOrderAsync(registration.Id);
|
||||||
o.RegistrationId == registration.Id && !o.DeleteMark);
|
|
||||||
var paidAmount = paidOrder?.ActualAmount ?? 0;
|
var paidAmount = paidOrder?.ActualAmount ?? 0;
|
||||||
return new UnifiedPaymentOutput
|
return new UnifiedPaymentOutput
|
||||||
{
|
{
|
||||||
@ -996,15 +995,28 @@ public class UnifiedRegistrationService : ITransient
|
|||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
// 1. 确保有订单号(预报名创建时 OrderNo 为空字符串)
|
// 1. 作废该报名下遗留的「原价待付」订单,并强制重新生成订单号。
|
||||||
// 注意:0元成交不涉及外部支付接口,订单号首次生成后复用即可。
|
// 场景:用户先未选券打开支付页 → payment-info 走付费分支,已生成 OrderNo 并建了原价待付单
|
||||||
// 与付费支付的"强制重新生成"策略不同,因为无需应对微信支付的参数一致性约束。
|
// (还调过微信统一下单);随后选上全额券再次进来变成 0 元成交。
|
||||||
// 幂等保护已在 ProcessPaymentAsync:L299 完成,此处实际每个报名订单只执行一次。
|
// 若沿用旧 OrderNo,会出现两条 OrderType=1 且单号相同的订单,导致:
|
||||||
if (string.IsNullOrEmpty(registration.OrderNo))
|
// ① 按 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. 报名记录置为已支付(免费成交)
|
// 2. 报名记录置为已支付(免费成交)
|
||||||
registration.PaymentStatus = 1;
|
registration.PaymentStatus = 1;
|
||||||
registration.PaymentMethod = 2; // 免费赛事统一按线上支付方式,满足小程序列表放行条件
|
registration.PaymentMethod = 2; // 免费赛事统一按线上支付方式,满足小程序列表放行条件
|
||||||
@ -1081,6 +1093,35 @@ public class UnifiedRegistrationService : ITransient
|
|||||||
registration.Id, registration.OrderNo);
|
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>
|
||||||
/// 创建订单记录
|
/// 创建订单记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -3387,7 +3428,7 @@ public class UnifiedRegistrationService : ITransient
|
|||||||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId);
|
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 actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
|
||||||
var originalAmount = 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 actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
|
||||||
var originalAmount = registration.TotalAmount;
|
var originalAmount = registration.TotalAmount;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user