diff --git a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs
index 3ff8ca2..25131e0 100644
--- a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs
+++ b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs
@@ -23,7 +23,7 @@ public class PaymentExpirationBackgroundService : BackgroundService
{
_serviceProvider = serviceProvider;
_logger = logger;
- _timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
+ _timer = new PeriodicTimer(TimeSpan.FromMinutes(0.2));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -37,6 +37,7 @@ public class PaymentExpirationBackgroundService : BackgroundService
await ProcessExpiredPaymentsAsync(stoppingToken);
await ProcessUnpaidWeChatOrdersAsync(stoppingToken);
await ProcessRefundingWeChatOrdersAsync(stoppingToken);
+ await ProcessFailedRegistrationsAsync(stoppingToken);
}
}
catch (OperationCanceledException)
@@ -540,6 +541,90 @@ public class PaymentExpirationBackgroundService : BackgroundService
_logger.LogInformation("退款 {OutRefundNo} 状态更新为失败成功", refundRecord.OutRefundNo);
}
+ ///
+ /// 处理已取消但实际支付成功的报名(报名失败退款)
+ ///
+ private async Task ProcessFailedRegistrationsAsync(CancellationToken stoppingToken)
+ {
+ try
+ {
+ _logger.LogInformation("开始检查已取消的报名,将查询微信订单状态");
+
+ using var scope = _serviceProvider.CreateScope();
+ var registrationRepository = scope.ServiceProvider.GetRequiredService>();
+ var unifiedRegistrationService = scope.ServiceProvider.GetRequiredService();
+
+ var oneDayAgo = DateTime.Now.AddDays(-1);
+
+ // 只查询一天内已取消状态的报名
+ var cancelledRegistrations = await registrationRepository.AsQueryable()
+ .Where(x => x.PaymentStatus == 4 && !x.DeleteMark && x.CreatedTime >= oneDayAgo)
+ .OrderBy(x => x.CreatedTime)
+ .Take(50)
+ .ToListAsync(stoppingToken);
+
+ if (!cancelledRegistrations.Any())
+ {
+ _logger.LogInformation("没有需要检查的已取消报名");
+ return;
+ }
+
+ _logger.LogInformation("发现 {Count} 个已取消的报名需要检查", cancelledRegistrations.Count);
+
+ foreach (var registration in cancelledRegistrations)
+ {
+ try
+ {
+ stoppingToken.ThrowIfCancellationRequested();
+
+ // 检查是否已经有退款记录,避免重复处理
+ var refundRecordRepository = scope.ServiceProvider.GetRequiredService>();
+ var existingRefund = await refundRecordRepository.GetFirstAsync(
+ r => r.RegistrationId == registration.Id && !r.DeleteMark,
+ stoppingToken);
+
+ if (existingRefund != null)
+ {
+ _logger.LogInformation("报名 {RegistrationId} 已有退款记录,跳过", registration.Id);
+ continue;
+ }
+
+ _logger.LogInformation("检查已取消的报名,报名ID={RegistrationId}", registration.Id);
+
+ // 调用统一报名服务处理退款
+ var success = await unifiedRegistrationService.ProcessFailedRegistrationRefundAsync(registration.Id, 0);
+
+ if (success)
+ {
+ _logger.LogInformation("报名失败退款处理成功,报名ID={RegistrationId}", registration.Id);
+ }
+ else
+ {
+ _logger.LogWarning("报名失败退款处理失败,报名ID={RegistrationId}", registration.Id);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "处理报名 {RegistrationId} 时发生异常", registration.Id);
+ }
+ }
+
+ _logger.LogInformation("已取消报名检查完成");
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "检查已取消但支付成功的报名时发生异常");
+ }
+ }
+
public override void Dispose()
{
_timer.Dispose();
diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs
index 489e02b..4637858 100644
--- a/YD_Event.Application/Event/UnifiedRegistrationService.cs
+++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs
@@ -4010,4 +4010,205 @@ public class UnifiedRegistrationService : ITransient
}
#endregion
+
+ ///
+ /// 处理已取消但实际支付成功的退款(报名失败退款)
+ ///
+ /// 报名ID
+ /// 操作人用户ID
+ /// 是否成功
+ [UnitOfWork]
+ public async Task ProcessFailedRegistrationRefundAsync(long registrationId, long operatorUserId)
+ {
+ try
+ {
+ _logger.LogInformation("开始处理报名失败退款,报名ID={RegistrationId}", registrationId);
+
+ var registration = await _registrationRepository.GetByIdAsync(registrationId);
+ if (registration == null)
+ {
+ _logger.LogWarning("报名记录不存在,报名ID={RegistrationId}", registrationId);
+ return false;
+ }
+
+ // 检查是否已经是已取消状态
+ if (registration.PaymentStatus != 4)
+ {
+ _logger.LogWarning("报名状态不是已取消,报名ID={RegistrationId}, 当前状态={Status}", registrationId, registration.PaymentStatus);
+ return false;
+ }
+
+
+
+ // 获取订单信息
+ var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registrationId && o.OrderType == 1);
+ if (order == null)
+ {
+ _logger.LogWarning("找不到订单记录,报名ID={RegistrationId}", registrationId);
+ return false;
+ }
+
+ // 获取赛事信息
+ var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
+ if (eventInfo == null)
+ {
+ _logger.LogWarning("找不到赛事信息,报名ID={RegistrationId}, 赛事ID={EventId}", registrationId, registration.EventId);
+ return false;
+ }
+
+ var now = DateTime.Now;
+
+ // 获取微信支付配置
+ var wechatConfig = await GetWeChatPayConfigByEventAsync(eventInfo);
+ if (wechatConfig == null)
+ {
+ _logger.LogError("找不到微信支付配置,报名ID={RegistrationId}", registrationId);
+ return false;
+ }
+
+ // 先从微信查询订单状态,确认是否真的支付成功
+ var wechatQueryResult = await _weChatPayService.QueryOrderAsync(wechatConfig, registration.OrderNo);
+ if (!wechatQueryResult.Success)
+ {
+ _logger.LogWarning("查询微信订单失败,报名ID={RegistrationId}, 订单号={OrderNo}, 错误={Error}",
+ registrationId, registration.OrderNo, wechatQueryResult.Message);
+ return false;
+ }
+
+ if (wechatQueryResult.TradeState != "SUCCESS")
+ {
+ _logger.LogInformation("微信订单不是支付成功状态,报名ID={RegistrationId}, 订单号={OrderNo}, 状态={TradeState}",
+ registrationId, registration.OrderNo, wechatQueryResult.TradeState);
+ return false;
+ }
+
+ _logger.LogInformation("确认微信订单已支付成功,报名ID={RegistrationId}, 订单号={OrderNo}",
+ registrationId, registration.OrderNo);
+
+ // 1. 更新报名状态为"已退款"(状态3)
+ registration.PaymentStatus = 3;
+ registration.UpdatedTime = now;
+ registration.UpdatedUserId = operatorUserId;
+ registration.RegistrationRemark = "报名失败,已全额退款";
+ await _registrationRepository.UpdateAsync(registration);
+
+ // 2. 更新订单状态为"报名失败"
+ order.OrderStatus = OrderStatus.RegistrationFailed;
+ order.UpdatedTime = now;
+ order.UpdatedUserId = operatorUserId;
+ await _orderRepository.UpdateAsync(order);
+
+ // 3. 生成退款单号
+ var refundOrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync();
+
+ // 4. 构建退款通知文案
+ var refundReason = $"{eventInfo.EventName} - 报名失败";
+
+ // 5. 创建退款订单记录
+ var refundOrder = new YD_Event.Core.Entities.Order
+ {
+ Id = _idGenerator.NewLong(),
+ OrderNo = refundOrderNo,
+ OrderType = 2,
+ MerchantId = order.MerchantId,
+ MerchantCertificationId = order.MerchantCertificationId,
+ EventId = order.EventId,
+ RegistrationId = order.RegistrationId,
+ RelatedOrderId = order.Id,
+ OriginalAmount = order.ActualAmount,
+ ActualAmount = order.ActualAmount,
+ DiscountAmount = 0,
+ OrderStatus = OrderStatus.Refunded,
+ PaymentTime = now,
+ Remark = refundReason,
+ CreatedTime = now,
+ CreatedUserId = operatorUserId,
+ Status = AvailabilityStatus.Enable
+ };
+ await _orderRepository.InsertAsync(refundOrder);
+
+ // 6. 调用微信退款接口
+ var refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest
+ {
+ OutTradeNo = registration.OrderNo,
+ OutRefundNo = refundOrderNo,
+ RefundAmount = AmountHelper.YuanToFen(order.ActualAmount),
+ TotalAmount = AmountHelper.YuanToFen(order.ActualAmount),
+ Reason = refundReason
+ };
+
+ var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest);
+ if (!refundResult.Success)
+ {
+ _logger.LogError("微信退款申请失败,报名ID={RegistrationId}, 错误={Error}", registrationId, refundResult.Message);
+ return false;
+ }
+
+ // 7. 获取或创建支付记录
+ var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == registrationId && !p.DeleteMark);
+ if (payment == null)
+ {
+ payment = new EventRegistrationPayment
+ {
+ Id = _idGenerator.NewLong(),
+ RegistrationId = registration.Id,
+ TransactionNo = wechatQueryResult.TransactionId ?? "",
+ PaymentChannel = 1,
+ PaymentAmount = order.ActualAmount,
+ PaymentTime = wechatQueryResult.SuccessTime?.DateTime ?? now,
+ PaymentStatus = 1,
+ RefundStatus = 0,
+ RefundAmount = 0,
+ PayeeAccount = "",
+ CreatedTime = now,
+ CreatedUserId = operatorUserId
+ };
+ await _paymentRepository.InsertAsync(payment);
+ }
+
+ // 8. 创建退款记录
+ await CreateRefundRecordAsync(
+ registration,
+ eventInfo,
+ wechatConfig,
+ refundOrderNo,
+ refundResult.RefundId,
+ order.ActualAmount,
+ order.ActualAmount,
+ refundReason,
+ operatorUserId
+ );
+
+ // 9. 更新支付记录的退款状态
+ payment.RefundStatus = 2;
+ payment.RefundAmount = order.ActualAmount;
+ payment.RefundTime = now;
+ payment.UpdatedTime = now;
+ payment.UpdatedUserId = operatorUserId;
+ await _paymentRepository.UpdateAsync(payment);
+
+ // 10. 软删除报名人员和项目
+ var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId);
+ if (persons.Count > 0)
+ {
+ await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now, refundOrder.Id);
+ }
+
+ var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registrationId);
+ if (items.Count > 0)
+ {
+ await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now, refundOrder.Id);
+ }
+
+ _logger.LogInformation("报名失败退款处理成功,报名ID={RegistrationId}, 退款金额={Amount}, 退款单号={RefundNo}",
+ registrationId, order.ActualAmount, refundOrderNo);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "处理报名失败退款时发生异常,报名ID={RegistrationId}", registrationId);
+ return false;
+ }
+ }
}
diff --git a/YD_Event.Core/Enum/OrderStatus.cs b/YD_Event.Core/Enum/OrderStatus.cs
index ca01f73..1bd7fa1 100644
--- a/YD_Event.Core/Enum/OrderStatus.cs
+++ b/YD_Event.Core/Enum/OrderStatus.cs
@@ -41,5 +41,11 @@ public enum OrderStatus
/// 已完成
///
[Description("已完成")]
- Completed = 5
+ Completed = 5,
+
+ ///
+ /// 报名失败
+ ///
+ [Description("报名失败")]
+ RegistrationFailed = 6
}