实付金额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;
// 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)