From 4a9a0dfd60fd55b3e3abe53862cfce5bb3f1b678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Fri, 7 Aug 2026 10:13:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F0=E5=85=83=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Client/Dtos/EventClientDtos.cs | 15 +++++ .../Client/EventClientController.cs | 4 +- .../Event/UnifiedRegistrationService.cs | 61 ++++++++++++++++--- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/YD_Event.Application/Client/Dtos/EventClientDtos.cs b/YD_Event.Application/Client/Dtos/EventClientDtos.cs index 82f6ced..cd76ac5 100644 --- a/YD_Event.Application/Client/Dtos/EventClientDtos.cs +++ b/YD_Event.Application/Client/Dtos/EventClientDtos.cs @@ -738,6 +738,21 @@ public class PaymentInfoDto /// 微信支付参数 /// public object? WechatPayParams { get; set; } + + /// + /// 是否为 0 元成交(优惠券全额抵扣 / 免费赛事) + /// + /// + /// 前端务必判断:IsFree == true 或 PaymentStatus == 1 时,订单已成交, + /// 不要再调起微信支付(此时 WechatPayParams 为 null,调起会报“支付失败”), + /// 应直接跳转支付成功页。 + /// + public bool IsFree { get; set; } + + /// + /// 支付状态 (0-待支付/待审核 1-已支付/已成交) + /// + public int PaymentStatus { get; set; } } /// diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index 1ed629a..da34a6f 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -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; diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index bd0e440..2293015 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -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); } + /// + /// 获取报名对应的「有效收款订单」,用于判定实付金额。 + /// + /// + /// 一个报名下可能残留多条 OrderType=1 的订单(典型:用户先未选券打开支付页生成了原价待付单, + /// 之后再用全额券 0 元成交)。直接用 GetFirstAsync 取不带排序、不筛状态, + /// 可能取到原价待付单,把实付金额误判为原价 —— 退款就会按原价退给实付 0 元的用户,造成资损。 + /// 因此这里优先取「已支付」单(0 元成交单同样是 Paid),按支付时间取最新; + /// 没有已支付单时(如线下转账待审核)再回退到最近一条未取消的订单,保持原有行为不变。 + /// + private async Task 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(); + } + /// /// 创建订单记录 /// @@ -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;