实付金额0退款流程优化

This commit is contained in:
马梓彭 2026-08-06 22:58:21 +08:00
parent 2768c4ceef
commit 8d812bf3df

View File

@ -997,6 +997,9 @@ public class UnifiedRegistrationService : ITransient
var now = DateTime.Now; var now = DateTime.Now;
// 1. 确保有订单号(预报名创建时 OrderNo 为空字符串) // 1. 确保有订单号(预报名创建时 OrderNo 为空字符串)
// 注意:0元成交不涉及外部支付接口,订单号首次生成后复用即可。
// 与付费支付的"强制重新生成"策略不同,因为无需应对微信支付的参数一致性约束。
// 幂等保护已在 ProcessPaymentAsync:L299 完成,此处实际每个报名订单只执行一次。
if (string.IsNullOrEmpty(registration.OrderNo)) if (string.IsNullOrEmpty(registration.OrderNo))
{ {
registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
@ -3766,13 +3769,29 @@ public class UnifiedRegistrationService : ITransient
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount; var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
var originalAmount = registration.TotalAmount; var originalAmount = registration.TotalAmount;
// 先计算按原价应该退多少 decimal originalRefundAmount;
decimal originalRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons); decimal actualRefundAmount;
// 按比例计算实际退款金额 // 0 元成交(优惠券全额抵扣 / 免费赛事):实付为 0,无款可退,直接短路。
decimal actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0; // 不走按比例折算,避免 originalAmount>0 而 actualPaidAmount 取值异常时算出正的退款额造成资损。
actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount); // 注意:优惠券已核销,按"使用后不予退还"规则不做回退。
actualRefundAmount = Math.Min(actualRefundAmount, 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}", _logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}",
registration.Id, originalAmount, actualPaidAmount, originalRefundAmount, actualRefundAmount); registration.Id, originalAmount, actualPaidAmount, originalRefundAmount, actualRefundAmount);
@ -3818,49 +3837,56 @@ public class UnifiedRegistrationService : ITransient
long? refundOrderId = null; long? refundOrderId = null;
string? refundOrderNo = 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); var newRefundOrder = new Core.Entities.Order
if (originalOrder != null)
{ {
var newRefundOrder = new Core.Entities.Order Id = _idGenerator.NewLong(),
{ OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(),
Id = _idGenerator.NewLong(), OrderType = 2,
OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), MerchantId = order.MerchantId,
OrderType = 2, MerchantCertificationId = order.MerchantCertificationId,
MerchantId = originalOrder.MerchantId, EventId = order.EventId,
MerchantCertificationId = originalOrder.MerchantCertificationId, RegistrationId = order.RegistrationId,
EventId = originalOrder.EventId, RelatedOrderId = order.Id,
RegistrationId = originalOrder.RegistrationId, OriginalAmount = order.ActualAmount,
RelatedOrderId = originalOrder.Id, ActualAmount = actualRefundAmount,
OriginalAmount = originalOrder.ActualAmount, DiscountAmount = 0,
ActualAmount = actualRefundAmount, // 实退 0 记为"已取消",与真实退款区分,避免对账时把 0 元单误读为已退款
DiscountAmount = 0, OrderStatus = actualRefundAmount > 0 ? OrderStatus.Refunded : OrderStatus.Cancelled,
OrderStatus = OrderStatus.Refunded, PaymentTime = now,
PaymentTime = now, Remark = input.CancelReason,
Remark = input.CancelReason, CreatedTime = now,
CreatedTime = now, CreatedUserId = operatorUserId,
CreatedUserId = operatorUserId, Status = AvailabilityStatus.Enable
Status = AvailabilityStatus.Enable };
};
// 线下支付时保存收款人信息 // 线下支付时保存收款人信息
if (registration.PaymentMethod == 1) if (registration.PaymentMethod == 1)
{ {
newRefundOrder.PayeeName = input.PayeeName; newRefundOrder.PayeeName = input.PayeeName;
newRefundOrder.PayeeBankName = input.PayeeBankName; newRefundOrder.PayeeBankName = input.PayeeBankName;
newRefundOrder.PayeeBankAccount = input.PayeeBankAccount; 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) if (registration.PaymentMethod == 1)
@ -4020,8 +4046,10 @@ public class UnifiedRegistrationService : ITransient
} }
} }
// 第二步:创建退款记录和执行微信退款(如果是线上支付) // 第二步:执行实际打款并创建退款流水记录
if (!string.IsNullOrEmpty(refundOrderNo)) // 退款单据在第一步已经落库(含实退 0 的留痕单),这里只负责真正的资金动作:
// actualRefundAmount == 0 时不得调用微信退款接口(金额 0 会被微信拒绝),也无需退款流水。
if (!string.IsNullOrEmpty(refundOrderNo) && actualRefundAmount > 0)
{ {
if (registration.PaymentMethod == 1) if (registration.PaymentMethod == 1)
{ {
@ -4029,13 +4057,10 @@ public class UnifiedRegistrationService : ITransient
_logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}", _logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}",
registration.Id, refundOrderNo, actualRefundAmount); 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,
await CreateRefundRecordAsync(registration, eventInfo, null, refundOrderNo, input.PayeeName, input.PayeeBankName, input.PayeeBankAccount);
null, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId,
input.PayeeName, input.PayeeBankName, input.PayeeBankAccount);
}
} }
else else
{ {
@ -4079,6 +4104,12 @@ public class UnifiedRegistrationService : ITransient
} }
} }
} }
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) // if (!isPartialCancel || registration.PaymentStatus == 3 || registration.PaymentStatus == 4)