From 6e576690d274f47a8ac906532eaad1ba30047fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 14 May 2026 09:31:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E5=90=8D=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PaymentExpirationBackgroundService.cs | 87 +++++++- .../Event/UnifiedRegistrationService.cs | 201 ++++++++++++++++++ YD_Event.Core/Enum/OrderStatus.cs | 8 +- 3 files changed, 294 insertions(+), 2 deletions(-) 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 }