From 2768c4ceefd9c6189b33e0ce28e4409db3446e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 6 Aug 2026 16:49:46 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=98=E6=83=A0?= =?UTF-8?q?=E5=88=B8=E8=B6=B3=E9=A2=9D=E6=8A=B5=E6=89=A3=E6=8A=A5=E5=90=8D?= =?UTF-8?q?=E8=B4=B9=E5=90=8E=E5=87=BA=E7=8E=B0=E7=9A=84=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/UnifiedRegistrationService.cs | 132 ++++++++++++++++-- 1 file changed, 118 insertions(+), 14 deletions(-) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 306f28c..cdc8fc2 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -156,11 +156,17 @@ public class UnifiedRegistrationService : ITransient registration.TotalPersonCount = totalPersonCount; await _registrationRepository.UpdateAsync(registration); - // 5. 如果是免费赛事,直接完成支付 + // 5. 如果是免费赛事,直接完成支付(无优惠券,原价即0) if (totalAmount <= 0) { _logger.LogInformation("免费赛事,直接完成支付,报名ID: {RegistrationId}", registration.Id); - await HandleFreePaymentAsync(registration, null, input.CreatedUserId); + var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId); + if (eventInfo == null) + { + throw Oops.Bah("赛事信息不存在"); + } + await HandleFreePaymentAsync(registration, eventInfo, totalAmount, 0, + null, null, null, input.CreatedUserId); } _logger.LogInformation("统一报名处理完成,报名ID: {RegistrationId}", registration.Id); @@ -286,6 +292,28 @@ public class UnifiedRegistrationService : ITransient throw Oops.Bah("赛事信息不存在"); } + // 2.1 幂等短路:已成交(含 0 元券全额抵扣)直接返回。 + // 关键:必须在重新计算金额之前短路。0 元成交时优惠券已被核销(Status=1), + // 若重入再走 CalculateFinalAmountAsync,会因查不到可用券而把 finalAmount 回退为原价, + // 从而落入付费分支:覆盖 PaymentStatus(1→0)、重复建单、重复调起微信支付 → 重复扣款/列表不显示。 + if (registration.PaymentStatus == 1) + { + _logger.LogInformation("报名已成交,幂等返回,报名ID: {RegistrationId}", registration.Id); + var paidOrder = await _orderRepository.GetFirstAsync(o => + o.RegistrationId == registration.Id && !o.DeleteMark); + var paidAmount = paidOrder?.ActualAmount ?? 0; + return new UnifiedPaymentOutput + { + RegistrationId = registration.Id, + OrderNo = registration.OrderNo, + OriginalAmount = paidOrder?.OriginalAmount ?? registration.TotalAmount, + DiscountAmount = paidOrder?.DiscountAmount ?? 0, + ActualAmount = paidAmount, + PaymentStatus = 1, + IsFree = paidAmount <= 0 + }; + } + // 3. 计算优惠后金额(每次都重新计算,确保金额正确) var calculateResult = await CalculateFinalAmountAsync( registration.TotalAmount, input.CouponId, input.OperatorUserId); @@ -308,13 +336,15 @@ public class UnifiedRegistrationService : ITransient CouponRuleNote = usedCoupon != null ? "优惠券使用后不予退还" : null }; - // 6. 处理免费情况 + // 6. 处理免费情况(0 元券全额抵扣等):与付费成交对齐——生成订单+支付流水并完成核销,避免状态孤岛 if (finalAmount <= 0) { if (registration.PaymentStatus != 1) { - await HandleFreePaymentAsync(registration, usedUserCoupon, input.OperatorUserId); + await HandleFreePaymentAsync(registration, eventInfo, originalAmount, discountAmount, + input.CouponId, usedCoupon, usedUserCoupon, input.OperatorUserId); } + output.OrderNo = registration.OrderNo; output.PaymentStatus = 1; return output; } @@ -954,24 +984,98 @@ public class UnifiedRegistrationService : ITransient } /// - /// 处理免费支付 + /// 处理免费支付(0 元券全额抵扣 / 免费赛事成交) + /// 与线上支付回调 对齐: + /// 生成订单(已支付)+支付流水(金额0),并完整核销优惠券,避免“付了钱但查不到”的状态孤岛。 /// - private async Task HandleFreePaymentAsync(EventRegistration registration, YD_Event.Core.Entities.UserCoupon? userCoupon, long operatorUserId) + private async Task HandleFreePaymentAsync( + EventRegistration registration, EventInfo eventInfo, + decimal originalAmount, decimal discountAmount, + long? couponId, YD_Event.Core.Entities.Coupon? coupon, + YD_Event.Core.Entities.UserCoupon? userCoupon, long operatorUserId) { + var now = DateTime.Now; + + // 1. 确保有订单号(预报名创建时 OrderNo 为空字符串) + if (string.IsNullOrEmpty(registration.OrderNo)) + { + registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); + } + + // 2. 报名记录置为已支付(免费成交) + registration.PaymentStatus = 1; + registration.PaymentMethod = 2; // 免费赛事统一按线上支付方式,满足小程序列表放行条件 + registration.PaymentMethodType = PaymentMethodType.WeChatPay; + registration.PaymentTime = now; + registration.UpdatedTime = now; + registration.UpdatedUserId = operatorUserId; + await _registrationRepository.UpdateAsync(registration); + + // 3. 生成订单并标记为已支付(实付 0),消除状态孤岛 + var order = await CreateOrderAsync(registration, eventInfo, originalAmount, discountAmount, + 0, couponId, operatorUserId); + order.OrderStatus = OrderStatus.Paid; + order.PaymentTime = now; + order.UpdatedTime = now; + await _orderRepository.UpdateAsync(order); + + // 4. 写支付流水(金额 0),与微信回调路径保持一致 + var payment = new EventRegistrationPayment + { + Id = _idGenerator.NewLong(), + RegistrationId = registration.Id, + TransactionNo = "", + PaymentChannel = 1, + PaymentAmount = 0, + PaymentTime = now, + PaymentStatus = 1, + RefundStatus = 0, + PayeeAccount = "", + CreatedUserId = registration.CreatedUserId, + CreatedTime = now + }; + await _paymentRepository.InsertAsync(payment); + + // 5. 完整核销优惠券(补齐 OrderId / UsedQuantity / 兑换码,与银行转账、微信回调路径一致) if (userCoupon != null) { userCoupon.Status = 1; - userCoupon.UseTime = DateTime.Now; + userCoupon.UseTime = now; + userCoupon.OrderId = order.Id; + userCoupon.UpdatedTime = now; + userCoupon.UpdatedUserId = operatorUserId; await _userCouponRepository.UpdateAsync(userCoupon); + + // 兑换码标记已使用 + if (userCoupon.RedemptionCodeId.HasValue) + { + var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc => + rc.Id == userCoupon.RedemptionCodeId.Value && rc.Status == 0 && !rc.DeleteMark); + if (redemptionCode != null) + { + redemptionCode.Status = 1; + redemptionCode.UsedTime = now; + redemptionCode.UpdatedTime = now; + redemptionCode.UpdatedUserId = operatorUserId; + await _redemptionCodeRepository.UpdateAsync(redemptionCode); + } + } + + // 优惠券已使用数量 +1 + var couponEntity = coupon ?? (couponId.HasValue + ? await _couponRepository.GetByIdAsync(couponId.Value) + : null); + if (couponEntity != null) + { + couponEntity.UsedQuantity++; + couponEntity.UpdatedTime = now; + couponEntity.UpdatedUserId = operatorUserId; + await _couponRepository.UpdateAsync(couponEntity); + } } - registration.PaymentStatus = 1; - registration.PaymentMethod = 2; // 免费赛事设置为线上支付方式 - registration.PaymentMethodType = PaymentMethodType.WeChatPay; // 默认微信支付 - registration.PaymentTime = DateTime.Now; - registration.UpdatedTime = DateTime.Now; - registration.UpdatedUserId = operatorUserId; - await _registrationRepository.UpdateAsync(registration); + _logger.LogInformation("免费成交处理完成(0元/券全额抵扣),报名ID: {RegistrationId}, 订单号: {OrderNo}", + registration.Id, registration.OrderNo); } /// From 8d812bf3df2862cc461d29db87399fc7250d54f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 6 Aug 2026 22:58:21 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E5=AE=9E=E4=BB=98=E9=87=91=E9=A2=9D0?= =?UTF-8?q?=E9=80=80=E6=AC=BE=E6=B5=81=E7=A8=8B=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/UnifiedRegistrationService.cs | 153 +++++++++++------- 1 file changed, 92 insertions(+), 61 deletions(-) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index cdc8fc2..44a3258 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -997,6 +997,9 @@ public class UnifiedRegistrationService : ITransient var now = DateTime.Now; // 1. 确保有订单号(预报名创建时 OrderNo 为空字符串) + // 注意:0元成交不涉及外部支付接口,订单号首次生成后复用即可。 + // 与付费支付的"强制重新生成"策略不同,因为无需应对微信支付的参数一致性约束。 + // 幂等保护已在 ProcessPaymentAsync:L299 完成,此处实际每个报名订单只执行一次。 if (string.IsNullOrEmpty(registration.OrderNo)) { registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); @@ -3765,16 +3768,32 @@ public class UnifiedRegistrationService : ITransient var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registration.Id && o.OrderType == 1 && !o.DeleteMark); var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount; var originalAmount = registration.TotalAmount; - - // 先计算按原价应该退多少 - decimal originalRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons); - - // 按比例计算实际退款金额 - decimal actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0; - actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount); - actualRefundAmount = Math.Min(actualRefundAmount, actualPaidAmount); - - _logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}", + + decimal originalRefundAmount; + decimal actualRefundAmount; + + // 0 元成交(优惠券全额抵扣 / 免费赛事):实付为 0,无款可退,直接短路。 + // 不走按比例折算,避免 originalAmount>0 而 actualPaidAmount 取值异常时算出正的退款额造成资损。 + // 注意:优惠券已核销,按"使用后不予退还"规则不做回退。 + if (actualPaidAmount <= 0) + { + originalRefundAmount = 0; + actualRefundAmount = 0; + _logger.LogInformation("0元成交无需退款:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付=0", + registration.Id, originalAmount); + } + else + { + // 先计算按原价应该退多少 + originalRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons); + + // 按比例计算实际退款金额 + actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0; + actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount); + actualRefundAmount = Math.Max(0, Math.Min(actualRefundAmount, actualPaidAmount)); + } + + _logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}", registration.Id, originalAmount, actualPaidAmount, originalRefundAmount, actualRefundAmount); bool isPartialCancel = input.CancelType == 1; @@ -3818,49 +3837,56 @@ public class UnifiedRegistrationService : ITransient long? refundOrderId = null; string? refundOrderNo = null; - // 第一步:先创建退款订单(如果有退款金额的话) - if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0) + // 第一步:先创建退款订单 + // 只要是"有金额"的报名单(原价 > 0),取消时一律建退款订单(OrderType=2),即使实退为 0。 + // 原因:OrderType==2 是订单列表/详情展示退款轨迹的唯一依据,且软删除的人员、项目要挂到 + // refundOrderId 上才能追溯。券全额抵扣(实付 0、实退 0)若不建单,取消后无据可查 —— 与 + // 0 元成交不建订单是同一类状态孤岛。是否真正打款由第二步按 actualRefundAmount > 0 判断,与建单解耦。 + // 复用上面已按 OrderType==1 && !DeleteMark 过滤好的 order,避免按 OrderNo 反查捞到退款单。 + if (registration.TotalAmount > 0 && order != null) { - var originalOrder = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo); - if (originalOrder != null) + var newRefundOrder = new Core.Entities.Order { - var newRefundOrder = new Core.Entities.Order - { - Id = _idGenerator.NewLong(), - OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), - OrderType = 2, - MerchantId = originalOrder.MerchantId, - MerchantCertificationId = originalOrder.MerchantCertificationId, - EventId = originalOrder.EventId, - RegistrationId = originalOrder.RegistrationId, - RelatedOrderId = originalOrder.Id, - OriginalAmount = originalOrder.ActualAmount, - ActualAmount = actualRefundAmount, - DiscountAmount = 0, - OrderStatus = OrderStatus.Refunded, - PaymentTime = now, - Remark = input.CancelReason, - CreatedTime = now, - CreatedUserId = operatorUserId, - Status = AvailabilityStatus.Enable - }; - - // 线下支付时保存收款人信息 - if (registration.PaymentMethod == 1) - { - newRefundOrder.PayeeName = input.PayeeName; - newRefundOrder.PayeeBankName = input.PayeeBankName; - newRefundOrder.PayeeBankAccount = input.PayeeBankAccount; - } - - await _orderRepository.InsertAsync(newRefundOrder); - refundOrderId = newRefundOrder.Id; - refundOrderNo = newRefundOrder.OrderNo; - output.RefundNo = newRefundOrder.OrderNo; - - _logger.LogInformation("退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}", - registration.Id, originalOrder.OrderNo, newRefundOrder.OrderNo, actualRefundAmount); + Id = _idGenerator.NewLong(), + OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), + OrderType = 2, + MerchantId = order.MerchantId, + MerchantCertificationId = order.MerchantCertificationId, + EventId = order.EventId, + RegistrationId = order.RegistrationId, + RelatedOrderId = order.Id, + OriginalAmount = order.ActualAmount, + ActualAmount = actualRefundAmount, + DiscountAmount = 0, + // 实退 0 记为"已取消",与真实退款区分,避免对账时把 0 元单误读为已退款 + OrderStatus = actualRefundAmount > 0 ? OrderStatus.Refunded : OrderStatus.Cancelled, + PaymentTime = now, + Remark = input.CancelReason, + CreatedTime = now, + CreatedUserId = operatorUserId, + Status = AvailabilityStatus.Enable + }; + + // 线下支付时保存收款人信息 + if (registration.PaymentMethod == 1) + { + newRefundOrder.PayeeName = input.PayeeName; + newRefundOrder.PayeeBankName = input.PayeeBankName; + newRefundOrder.PayeeBankAccount = input.PayeeBankAccount; } + + await _orderRepository.InsertAsync(newRefundOrder); + refundOrderId = newRefundOrder.Id; + refundOrderNo = newRefundOrder.OrderNo; + output.RefundNo = newRefundOrder.OrderNo; + + _logger.LogInformation("退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}, 订单状态={OrderStatus}", + registration.Id, order.OrderNo, newRefundOrder.OrderNo, actualRefundAmount, newRefundOrder.OrderStatus); + } + else if (registration.TotalAmount > 0) + { + _logger.LogWarning("未找到原订单,无法创建退款订单:报名ID={RegistrationId}, 订单号={OrderNo}", + registration.Id, registration.OrderNo); } if (registration.PaymentMethod == 1) @@ -4020,22 +4046,21 @@ public class UnifiedRegistrationService : ITransient } } - // 第二步:创建退款记录和执行微信退款(如果是线上支付) - if (!string.IsNullOrEmpty(refundOrderNo)) + // 第二步:执行实际打款并创建退款流水记录 + // 退款单据在第一步已经落库(含实退 0 的留痕单),这里只负责真正的资金动作: + // actualRefundAmount == 0 时不得调用微信退款接口(金额 0 会被微信拒绝),也无需退款流水。 + if (!string.IsNullOrEmpty(refundOrderNo) && actualRefundAmount > 0) { if (registration.PaymentMethod == 1) { // 线下支付:创建退款记录 - _logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}", + _logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}", registration.Id, refundOrderNo, actualRefundAmount); - if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0) - { - // 创建线下支付退款记录 - await CreateRefundRecordAsync(registration, eventInfo, null, refundOrderNo, - null, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId, - input.PayeeName, input.PayeeBankName, input.PayeeBankAccount); - } + // 创建线下支付退款记录 + await CreateRefundRecordAsync(registration, eventInfo, null, refundOrderNo, + null, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId, + input.PayeeName, input.PayeeBankName, input.PayeeBankAccount); } else { @@ -4073,12 +4098,18 @@ public class UnifiedRegistrationService : ITransient await CreateRefundRecordAsync(registration, eventInfo, wechatConfig, refundOrderNo, refundResult.RefundId, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId); - _logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}", + _logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}", registration.Id, refundOrderNo, actualRefundAmount); } } } } + else if (!string.IsNullOrEmpty(refundOrderNo)) + { + // 实退为 0(优惠券全额抵扣、或按规则本次不退款):只保留退款单据留痕,不动资金 + _logger.LogInformation("退款金额为0,仅生成退款单据不调用支付渠道:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 实付={ActualPaidAmount}", + registration.Id, refundOrderNo, actualPaidAmount); + } // 不修改原订单状态,只创建退款订单记录 // if (!isPartialCancel || registration.PaymentStatus == 3 || registration.PaymentStatus == 4) From 95325a7fa6ea2dc09242b6e61ce1ec856223c62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 6 Aug 2026 23:38:04 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E9=80=80=E6=AC=BE=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YD_Event.Application/Event/UnifiedRegistrationService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 44a3258..bd0e440 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -3858,8 +3858,8 @@ public class UnifiedRegistrationService : ITransient OriginalAmount = order.ActualAmount, ActualAmount = actualRefundAmount, DiscountAmount = 0, - // 实退 0 记为"已取消",与真实退款区分,避免对账时把 0 元单误读为已退款 - OrderStatus = actualRefundAmount > 0 ? OrderStatus.Refunded : OrderStatus.Cancelled, + // 统一用 Refunded,实退 0 的留痕单也要能被列表查到: + OrderStatus = OrderStatus.Refunded, PaymentTime = now, Remark = input.CancelReason, CreatedTime = now, 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 4/8] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F0=E5=85=83?= =?UTF-8?q?=E6=94=AF=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; From 6f288a7eccffad14755e6f853c5fc9203c7de5ce 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:41:01 +0800 Subject: [PATCH 5/8] =?UTF-8?q?Revert=20"=E5=B0=8F=E7=A8=8B=E5=BA=8F0?= =?UTF-8?q?=E5=85=83=E6=94=AF=E4=BB=98=E5=A4=84=E7=90=86"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4a9a0dfd60fd55b3e3abe53862cfce5bb3f1b678. --- .../Client/Dtos/EventClientDtos.cs | 15 ----- .../Client/EventClientController.cs | 4 +- .../Event/UnifiedRegistrationService.cs | 61 +++---------------- 3 files changed, 11 insertions(+), 69 deletions(-) diff --git a/YD_Event.Application/Client/Dtos/EventClientDtos.cs b/YD_Event.Application/Client/Dtos/EventClientDtos.cs index cd76ac5..82f6ced 100644 --- a/YD_Event.Application/Client/Dtos/EventClientDtos.cs +++ b/YD_Event.Application/Client/Dtos/EventClientDtos.cs @@ -738,21 +738,6 @@ 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 da34a6f..1ed629a 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -776,9 +776,7 @@ public class EventClientController : IDynamicApiController BankAccountName = merchant?.MerchantName, BankAccountNo = merchant?.AccountNumber, BankName = merchant?.BankName, - WechatPayParams = unifiedResult.WechatPayParams, - IsFree = unifiedResult.IsFree, - PaymentStatus = unifiedResult.PaymentStatus + WechatPayParams = unifiedResult.WechatPayParams }; return paymentInfo; diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 2293015..bd0e440 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -299,7 +299,8 @@ public class UnifiedRegistrationService : ITransient if (registration.PaymentStatus == 1) { _logger.LogInformation("报名已成交,幂等返回,报名ID: {RegistrationId}", registration.Id); - var paidOrder = await GetSettledOrderAsync(registration.Id); + var paidOrder = await _orderRepository.GetFirstAsync(o => + o.RegistrationId == registration.Id && !o.DeleteMark); var paidAmount = paidOrder?.ActualAmount ?? 0; return new UnifiedPaymentOutput { @@ -995,28 +996,15 @@ public class UnifiedRegistrationService : ITransient { var now = DateTime.Now; - // 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) + // 1. 确保有订单号(预报名创建时 OrderNo 为空字符串) + // 注意:0元成交不涉及外部支付接口,订单号首次生成后复用即可。 + // 与付费支付的"强制重新生成"策略不同,因为无需应对微信支付的参数一致性约束。 + // 幂等保护已在 ProcessPaymentAsync:L299 完成,此处实际每个报名订单只执行一次。 + if (string.IsNullOrEmpty(registration.OrderNo)) { - stale.OrderStatus = OrderStatus.Cancelled; - stale.Remark = (stale.Remark ?? "") + " [系统]优惠券全额抵扣/免费成交,原价待付单作废"; - stale.UpdatedTime = now; - await _orderRepository.UpdateAsync(stale); - _logger.LogInformation("作废原价待付订单,报名ID: {RegistrationId}, 订单号: {OrderNo}", - registration.Id, stale.OrderNo); + registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); } - // 强制重新生成订单号:保证 0 元成交单的单号唯一,与付费分支的策略保持一致 - registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); - // 2. 报名记录置为已支付(免费成交) registration.PaymentStatus = 1; registration.PaymentMethod = 2; // 免费赛事统一按线上支付方式,满足小程序列表放行条件 @@ -1093,35 +1081,6 @@ 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(); - } - /// /// 创建订单记录 /// @@ -3428,7 +3387,7 @@ public class UnifiedRegistrationService : ITransient var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId); // 查询订单获取实际支付金额(考虑优惠券抵扣) - var order = await GetSettledOrderAsync(input.RegistrationId); + var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == input.RegistrationId && x.OrderType == 1 && !x.DeleteMark); var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount; var originalAmount = registration.TotalAmount; @@ -3806,7 +3765,7 @@ public class UnifiedRegistrationService : ITransient // 计算实际退款金额 // 【重要】始终信任后端计算结果,前端计算只用于展示 // 不接受前端传递的退款金额,避免资金风险 - var order = await GetSettledOrderAsync(registration.Id); + var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registration.Id && o.OrderType == 1 && !o.DeleteMark); var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount; var originalAmount = registration.TotalAmount; From 03a3d175b94824f7063a85cb1308d4f36b768aa5 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:41:05 +0800 Subject: [PATCH 6/8] =?UTF-8?q?Revert=20"=E9=80=80=E6=AC=BE=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E7=8A=B6=E6=80=81"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 95325a7fa6ea2dc09242b6e61ce1ec856223c62a. --- YD_Event.Application/Event/UnifiedRegistrationService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index bd0e440..44a3258 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -3858,8 +3858,8 @@ public class UnifiedRegistrationService : ITransient OriginalAmount = order.ActualAmount, ActualAmount = actualRefundAmount, DiscountAmount = 0, - // 统一用 Refunded,实退 0 的留痕单也要能被列表查到: - OrderStatus = OrderStatus.Refunded, + // 实退 0 记为"已取消",与真实退款区分,避免对账时把 0 元单误读为已退款 + OrderStatus = actualRefundAmount > 0 ? OrderStatus.Refunded : OrderStatus.Cancelled, PaymentTime = now, Remark = input.CancelReason, CreatedTime = now, From 26750d9bf165cbadca8a2a3a6a2e46358e40f871 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:41:09 +0800 Subject: [PATCH 7/8] =?UTF-8?q?Revert=20"=E5=AE=9E=E4=BB=98=E9=87=91?= =?UTF-8?q?=E9=A2=9D0=E9=80=80=E6=AC=BE=E6=B5=81=E7=A8=8B=E4=BC=98?= =?UTF-8?q?=E5=8C=96"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8d812bf3df2862cc461d29db87399fc7250d54f4. --- .../Event/UnifiedRegistrationService.cs | 153 +++++++----------- 1 file changed, 61 insertions(+), 92 deletions(-) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 44a3258..cdc8fc2 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -997,9 +997,6 @@ public class UnifiedRegistrationService : ITransient var now = DateTime.Now; // 1. 确保有订单号(预报名创建时 OrderNo 为空字符串) - // 注意:0元成交不涉及外部支付接口,订单号首次生成后复用即可。 - // 与付费支付的"强制重新生成"策略不同,因为无需应对微信支付的参数一致性约束。 - // 幂等保护已在 ProcessPaymentAsync:L299 完成,此处实际每个报名订单只执行一次。 if (string.IsNullOrEmpty(registration.OrderNo)) { registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); @@ -3768,32 +3765,16 @@ public class UnifiedRegistrationService : ITransient var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registration.Id && o.OrderType == 1 && !o.DeleteMark); var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount; var originalAmount = registration.TotalAmount; - - decimal originalRefundAmount; - decimal actualRefundAmount; - - // 0 元成交(优惠券全额抵扣 / 免费赛事):实付为 0,无款可退,直接短路。 - // 不走按比例折算,避免 originalAmount>0 而 actualPaidAmount 取值异常时算出正的退款额造成资损。 - // 注意:优惠券已核销,按"使用后不予退还"规则不做回退。 - if (actualPaidAmount <= 0) - { - originalRefundAmount = 0; - actualRefundAmount = 0; - _logger.LogInformation("0元成交无需退款:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付=0", - registration.Id, originalAmount); - } - else - { - // 先计算按原价应该退多少 - originalRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons); - - // 按比例计算实际退款金额 - actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0; - actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount); - actualRefundAmount = Math.Max(0, Math.Min(actualRefundAmount, actualPaidAmount)); - } - - _logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}", + + // 先计算按原价应该退多少 + decimal originalRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons); + + // 按比例计算实际退款金额 + decimal actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0; + actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount); + actualRefundAmount = Math.Min(actualRefundAmount, actualPaidAmount); + + _logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}", registration.Id, originalAmount, actualPaidAmount, originalRefundAmount, actualRefundAmount); bool isPartialCancel = input.CancelType == 1; @@ -3837,56 +3818,49 @@ public class UnifiedRegistrationService : ITransient long? refundOrderId = null; string? refundOrderNo = null; - // 第一步:先创建退款订单 - // 只要是"有金额"的报名单(原价 > 0),取消时一律建退款订单(OrderType=2),即使实退为 0。 - // 原因:OrderType==2 是订单列表/详情展示退款轨迹的唯一依据,且软删除的人员、项目要挂到 - // refundOrderId 上才能追溯。券全额抵扣(实付 0、实退 0)若不建单,取消后无据可查 —— 与 - // 0 元成交不建订单是同一类状态孤岛。是否真正打款由第二步按 actualRefundAmount > 0 判断,与建单解耦。 - // 复用上面已按 OrderType==1 && !DeleteMark 过滤好的 order,避免按 OrderNo 反查捞到退款单。 - if (registration.TotalAmount > 0 && order != null) + // 第一步:先创建退款订单(如果有退款金额的话) + if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0) { - var newRefundOrder = new Core.Entities.Order + var originalOrder = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo); + if (originalOrder != null) { - Id = _idGenerator.NewLong(), - OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), - OrderType = 2, - MerchantId = order.MerchantId, - MerchantCertificationId = order.MerchantCertificationId, - EventId = order.EventId, - RegistrationId = order.RegistrationId, - RelatedOrderId = order.Id, - OriginalAmount = order.ActualAmount, - ActualAmount = actualRefundAmount, - DiscountAmount = 0, - // 实退 0 记为"已取消",与真实退款区分,避免对账时把 0 元单误读为已退款 - OrderStatus = actualRefundAmount > 0 ? OrderStatus.Refunded : OrderStatus.Cancelled, - PaymentTime = now, - Remark = input.CancelReason, - CreatedTime = now, - CreatedUserId = operatorUserId, - Status = AvailabilityStatus.Enable - }; - - // 线下支付时保存收款人信息 - if (registration.PaymentMethod == 1) - { - newRefundOrder.PayeeName = input.PayeeName; - newRefundOrder.PayeeBankName = input.PayeeBankName; - newRefundOrder.PayeeBankAccount = input.PayeeBankAccount; + var newRefundOrder = new Core.Entities.Order + { + Id = _idGenerator.NewLong(), + OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), + OrderType = 2, + MerchantId = originalOrder.MerchantId, + MerchantCertificationId = originalOrder.MerchantCertificationId, + EventId = originalOrder.EventId, + RegistrationId = originalOrder.RegistrationId, + RelatedOrderId = originalOrder.Id, + OriginalAmount = originalOrder.ActualAmount, + ActualAmount = actualRefundAmount, + DiscountAmount = 0, + OrderStatus = OrderStatus.Refunded, + PaymentTime = now, + Remark = input.CancelReason, + CreatedTime = now, + CreatedUserId = operatorUserId, + Status = AvailabilityStatus.Enable + }; + + // 线下支付时保存收款人信息 + if (registration.PaymentMethod == 1) + { + newRefundOrder.PayeeName = input.PayeeName; + newRefundOrder.PayeeBankName = input.PayeeBankName; + newRefundOrder.PayeeBankAccount = input.PayeeBankAccount; + } + + await _orderRepository.InsertAsync(newRefundOrder); + refundOrderId = newRefundOrder.Id; + refundOrderNo = newRefundOrder.OrderNo; + output.RefundNo = newRefundOrder.OrderNo; + + _logger.LogInformation("退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}", + registration.Id, originalOrder.OrderNo, newRefundOrder.OrderNo, actualRefundAmount); } - - await _orderRepository.InsertAsync(newRefundOrder); - refundOrderId = newRefundOrder.Id; - refundOrderNo = newRefundOrder.OrderNo; - output.RefundNo = newRefundOrder.OrderNo; - - _logger.LogInformation("退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}, 订单状态={OrderStatus}", - registration.Id, order.OrderNo, newRefundOrder.OrderNo, actualRefundAmount, newRefundOrder.OrderStatus); - } - else if (registration.TotalAmount > 0) - { - _logger.LogWarning("未找到原订单,无法创建退款订单:报名ID={RegistrationId}, 订单号={OrderNo}", - registration.Id, registration.OrderNo); } if (registration.PaymentMethod == 1) @@ -4046,21 +4020,22 @@ public class UnifiedRegistrationService : ITransient } } - // 第二步:执行实际打款并创建退款流水记录 - // 退款单据在第一步已经落库(含实退 0 的留痕单),这里只负责真正的资金动作: - // actualRefundAmount == 0 时不得调用微信退款接口(金额 0 会被微信拒绝),也无需退款流水。 - if (!string.IsNullOrEmpty(refundOrderNo) && actualRefundAmount > 0) + // 第二步:创建退款记录和执行微信退款(如果是线上支付) + if (!string.IsNullOrEmpty(refundOrderNo)) { if (registration.PaymentMethod == 1) { // 线下支付:创建退款记录 - _logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}", + _logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}", registration.Id, refundOrderNo, actualRefundAmount); - // 创建线下支付退款记录 - await CreateRefundRecordAsync(registration, eventInfo, null, refundOrderNo, - null, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId, - input.PayeeName, input.PayeeBankName, input.PayeeBankAccount); + if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0) + { + // 创建线下支付退款记录 + await CreateRefundRecordAsync(registration, eventInfo, null, refundOrderNo, + null, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId, + input.PayeeName, input.PayeeBankName, input.PayeeBankAccount); + } } else { @@ -4098,18 +4073,12 @@ public class UnifiedRegistrationService : ITransient await CreateRefundRecordAsync(registration, eventInfo, wechatConfig, refundOrderNo, refundResult.RefundId, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId); - _logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}", + _logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}", registration.Id, refundOrderNo, actualRefundAmount); } } } } - else if (!string.IsNullOrEmpty(refundOrderNo)) - { - // 实退为 0(优惠券全额抵扣、或按规则本次不退款):只保留退款单据留痕,不动资金 - _logger.LogInformation("退款金额为0,仅生成退款单据不调用支付渠道:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 实付={ActualPaidAmount}", - registration.Id, refundOrderNo, actualPaidAmount); - } // 不修改原订单状态,只创建退款订单记录 // if (!isPartialCancel || registration.PaymentStatus == 3 || registration.PaymentStatus == 4) From 3d41663213429526ca78c1f24f87d13a8b000d76 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:41:12 +0800 Subject: [PATCH 8/8] =?UTF-8?q?Revert=20"=E4=BF=AE=E5=A4=8D=E4=BC=98?= =?UTF-8?q?=E6=83=A0=E5=88=B8=E8=B6=B3=E9=A2=9D=E6=8A=B5=E6=89=A3=E6=8A=A5?= =?UTF-8?q?=E5=90=8D=E8=B4=B9=E5=90=8E=E5=87=BA=E7=8E=B0=E7=9A=84=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E9=97=AE=E9=A2=98"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2768c4ceefd9c6189b33e0ce28e4409db3446e6f. --- .../Event/UnifiedRegistrationService.cs | 132 ++---------------- 1 file changed, 14 insertions(+), 118 deletions(-) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index cdc8fc2..306f28c 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -156,17 +156,11 @@ public class UnifiedRegistrationService : ITransient registration.TotalPersonCount = totalPersonCount; await _registrationRepository.UpdateAsync(registration); - // 5. 如果是免费赛事,直接完成支付(无优惠券,原价即0) + // 5. 如果是免费赛事,直接完成支付 if (totalAmount <= 0) { _logger.LogInformation("免费赛事,直接完成支付,报名ID: {RegistrationId}", registration.Id); - var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId); - if (eventInfo == null) - { - throw Oops.Bah("赛事信息不存在"); - } - await HandleFreePaymentAsync(registration, eventInfo, totalAmount, 0, - null, null, null, input.CreatedUserId); + await HandleFreePaymentAsync(registration, null, input.CreatedUserId); } _logger.LogInformation("统一报名处理完成,报名ID: {RegistrationId}", registration.Id); @@ -292,28 +286,6 @@ public class UnifiedRegistrationService : ITransient throw Oops.Bah("赛事信息不存在"); } - // 2.1 幂等短路:已成交(含 0 元券全额抵扣)直接返回。 - // 关键:必须在重新计算金额之前短路。0 元成交时优惠券已被核销(Status=1), - // 若重入再走 CalculateFinalAmountAsync,会因查不到可用券而把 finalAmount 回退为原价, - // 从而落入付费分支:覆盖 PaymentStatus(1→0)、重复建单、重复调起微信支付 → 重复扣款/列表不显示。 - if (registration.PaymentStatus == 1) - { - _logger.LogInformation("报名已成交,幂等返回,报名ID: {RegistrationId}", registration.Id); - var paidOrder = await _orderRepository.GetFirstAsync(o => - o.RegistrationId == registration.Id && !o.DeleteMark); - var paidAmount = paidOrder?.ActualAmount ?? 0; - return new UnifiedPaymentOutput - { - RegistrationId = registration.Id, - OrderNo = registration.OrderNo, - OriginalAmount = paidOrder?.OriginalAmount ?? registration.TotalAmount, - DiscountAmount = paidOrder?.DiscountAmount ?? 0, - ActualAmount = paidAmount, - PaymentStatus = 1, - IsFree = paidAmount <= 0 - }; - } - // 3. 计算优惠后金额(每次都重新计算,确保金额正确) var calculateResult = await CalculateFinalAmountAsync( registration.TotalAmount, input.CouponId, input.OperatorUserId); @@ -336,15 +308,13 @@ public class UnifiedRegistrationService : ITransient CouponRuleNote = usedCoupon != null ? "优惠券使用后不予退还" : null }; - // 6. 处理免费情况(0 元券全额抵扣等):与付费成交对齐——生成订单+支付流水并完成核销,避免状态孤岛 + // 6. 处理免费情况 if (finalAmount <= 0) { if (registration.PaymentStatus != 1) { - await HandleFreePaymentAsync(registration, eventInfo, originalAmount, discountAmount, - input.CouponId, usedCoupon, usedUserCoupon, input.OperatorUserId); + await HandleFreePaymentAsync(registration, usedUserCoupon, input.OperatorUserId); } - output.OrderNo = registration.OrderNo; output.PaymentStatus = 1; return output; } @@ -984,98 +954,24 @@ public class UnifiedRegistrationService : ITransient } /// - /// 处理免费支付(0 元券全额抵扣 / 免费赛事成交) - /// 与线上支付回调 对齐: - /// 生成订单(已支付)+支付流水(金额0),并完整核销优惠券,避免“付了钱但查不到”的状态孤岛。 + /// 处理免费支付 /// - private async Task HandleFreePaymentAsync( - EventRegistration registration, EventInfo eventInfo, - decimal originalAmount, decimal discountAmount, - long? couponId, YD_Event.Core.Entities.Coupon? coupon, - YD_Event.Core.Entities.UserCoupon? userCoupon, long operatorUserId) + private async Task HandleFreePaymentAsync(EventRegistration registration, YD_Event.Core.Entities.UserCoupon? userCoupon, long operatorUserId) { - var now = DateTime.Now; - - // 1. 确保有订单号(预报名创建时 OrderNo 为空字符串) - if (string.IsNullOrEmpty(registration.OrderNo)) - { - registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); - } - - // 2. 报名记录置为已支付(免费成交) - registration.PaymentStatus = 1; - registration.PaymentMethod = 2; // 免费赛事统一按线上支付方式,满足小程序列表放行条件 - registration.PaymentMethodType = PaymentMethodType.WeChatPay; - registration.PaymentTime = now; - registration.UpdatedTime = now; - registration.UpdatedUserId = operatorUserId; - await _registrationRepository.UpdateAsync(registration); - - // 3. 生成订单并标记为已支付(实付 0),消除状态孤岛 - var order = await CreateOrderAsync(registration, eventInfo, originalAmount, discountAmount, - 0, couponId, operatorUserId); - order.OrderStatus = OrderStatus.Paid; - order.PaymentTime = now; - order.UpdatedTime = now; - await _orderRepository.UpdateAsync(order); - - // 4. 写支付流水(金额 0),与微信回调路径保持一致 - var payment = new EventRegistrationPayment - { - Id = _idGenerator.NewLong(), - RegistrationId = registration.Id, - TransactionNo = "", - PaymentChannel = 1, - PaymentAmount = 0, - PaymentTime = now, - PaymentStatus = 1, - RefundStatus = 0, - PayeeAccount = "", - CreatedUserId = registration.CreatedUserId, - CreatedTime = now - }; - await _paymentRepository.InsertAsync(payment); - - // 5. 完整核销优惠券(补齐 OrderId / UsedQuantity / 兑换码,与银行转账、微信回调路径一致) if (userCoupon != null) { userCoupon.Status = 1; - userCoupon.UseTime = now; - userCoupon.OrderId = order.Id; - userCoupon.UpdatedTime = now; - userCoupon.UpdatedUserId = operatorUserId; + userCoupon.UseTime = DateTime.Now; await _userCouponRepository.UpdateAsync(userCoupon); - - // 兑换码标记已使用 - if (userCoupon.RedemptionCodeId.HasValue) - { - var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc => - rc.Id == userCoupon.RedemptionCodeId.Value && rc.Status == 0 && !rc.DeleteMark); - if (redemptionCode != null) - { - redemptionCode.Status = 1; - redemptionCode.UsedTime = now; - redemptionCode.UpdatedTime = now; - redemptionCode.UpdatedUserId = operatorUserId; - await _redemptionCodeRepository.UpdateAsync(redemptionCode); - } - } - - // 优惠券已使用数量 +1 - var couponEntity = coupon ?? (couponId.HasValue - ? await _couponRepository.GetByIdAsync(couponId.Value) - : null); - if (couponEntity != null) - { - couponEntity.UsedQuantity++; - couponEntity.UpdatedTime = now; - couponEntity.UpdatedUserId = operatorUserId; - await _couponRepository.UpdateAsync(couponEntity); - } } - _logger.LogInformation("免费成交处理完成(0元/券全额抵扣),报名ID: {RegistrationId}, 订单号: {OrderNo}", - registration.Id, registration.OrderNo); + registration.PaymentStatus = 1; + registration.PaymentMethod = 2; // 免费赛事设置为线上支付方式 + registration.PaymentMethodType = PaymentMethodType.WeChatPay; // 默认微信支付 + registration.PaymentTime = DateTime.Now; + registration.UpdatedTime = DateTime.Now; + registration.UpdatedUserId = operatorUserId; + await _registrationRepository.UpdateAsync(registration); } ///