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] =?UTF-8?q?=E5=AE=9E=E4=BB=98=E9=87=91=E9=A2=9D0=E9=80=80?= =?UTF-8?q?=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)