diff --git a/YD_Event.Application/Client/Dtos/EventClientDtos.cs b/YD_Event.Application/Client/Dtos/EventClientDtos.cs index b0aa3e6..a90aeb4 100644 --- a/YD_Event.Application/Client/Dtos/EventClientDtos.cs +++ b/YD_Event.Application/Client/Dtos/EventClientDtos.cs @@ -994,3 +994,26 @@ public class SelectPaymentMethodInput [RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "OpenId格式不正确,只能包含字母、数字、下划线和连字符")] public string? OpenId { get; set; } } + +/// +/// 取消报名退款输入DTO +/// +public class CancelRegistrationRefundInput +{ + /// + /// 报名记录ID + /// + [Required(ErrorMessage = "报名记录ID不能为空")] + public long RegistrationId { get; set; } + + /// + /// 退款原因 + /// + [MaxLength(200, ErrorMessage = "退款原因不能超过200个字符")] + public string? RefundReason { get; set; } + + /// + /// 取消的项目ID列表(可选,用于部分取消) + /// + public List? CancelItemIds { get; set; } +} diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index cfe4c4b..39fd551 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -15,6 +15,7 @@ using Furion.FriendlyException; using System.Security.Claims; using System.Text.Json; using System.IO; +using YD_Event.Core.Entities; namespace YD_Event.Application.Client; @@ -50,6 +51,8 @@ public class EventClientController : IDynamicApiController private readonly ISqlSugarRepository _orderRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; private readonly ISqlSugarRepository _paymentRepository; + private readonly ISqlSugarRepository _refundRecordRepository; + private readonly ISqlSugarRepository _refundConfigRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IIdGenerator _idGenerator; private readonly WeChatPayService _weChatPayService; @@ -82,6 +85,8 @@ public class EventClientController : IDynamicApiController ISqlSugarRepository orderRepository, ISqlSugarRepository merchantCertificationRepository, ISqlSugarRepository paymentRepository, + ISqlSugarRepository refundRecordRepository, + ISqlSugarRepository refundConfigRepository, IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, WeChatPayService weChatPayService, @@ -113,6 +118,8 @@ public class EventClientController : IDynamicApiController _orderRepository = orderRepository; _merchantCertificationRepository = merchantCertificationRepository; _paymentRepository = paymentRepository; + _refundRecordRepository = refundRecordRepository; + _refundConfigRepository = refundConfigRepository; _httpContextAccessor = httpContextAccessor; _idGenerator = idGenerator; _weChatPayService = weChatPayService; @@ -1624,4 +1631,243 @@ public class EventClientController : IDynamicApiController return await _registrationRepository.UpdateAsync(registration); } + + /// + /// 取消报名并申请退款 + /// + [HttpPost("cancel-registration-refund")] + [UnitOfWork] + [DisplayName("取消报名退款")] + public async Task CancelRegistrationRefund([FromBody] CancelRegistrationRefundInput input) + { + var userId = GetCurrentUserId(); + + // 验证报名记录 + var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId); + if (registration == null) throw Oops.Bah("报名记录不存在"); + if (registration.CreatedUserId != userId) throw Oops.Bah("无权操作此报名记录"); + if (registration.PaymentStatus != 1) throw Oops.Bah("只有已支付的报名才能申请退款"); + + // 获取订单信息 + var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == input.RegistrationId && x.OrderType == 1); + if (order == null) throw Oops.Bah("订单不存在"); + if (order.OrderStatus != OrderStatus.Paid) throw Oops.Bah("订单未支付,无法退款"); + + // 获取赛事信息 + var eventInfo = await _eventRepository.GetByIdAsync(order.EventId); + if (eventInfo == null) throw Oops.Bah("赛事信息不存在"); + + // 计算退款金额 + var refundAmount = await CalculateRefundAmount(order.ActualAmount, eventInfo.MatchStartTime); + + // 处理退款 + await ProcessRefundAsync(order, registration, refundAmount, input.RefundReason ?? "用户取消报名", userId); + } + + /// + /// 计算退款金额 + /// + private async Task CalculateRefundAmount(decimal totalAmount, DateTime eventStartTime) + { + var configs = await _refundConfigRepository.AsQueryable() + .Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark) + .OrderBy(x => x.DayStart) + .ToListAsync(); + + if (!configs.Any()) return totalAmount; + + var daysRemaining = (eventStartTime - DateTime.Now).TotalDays; + if (daysRemaining < 0) daysRemaining = 0; + + var matchConfig = configs.FirstOrDefault(c => + daysRemaining >= c.DayStart && + (c.DayEnd == int.MaxValue ? true : daysRemaining < c.DayEnd) + ); + + if (matchConfig != null) + { + var deductAmount = totalAmount * (matchConfig.DeductPercentage / 100m); + return totalAmount - deductAmount; + } + + return totalAmount; + } + + /// + /// 处理退款核心逻辑 + /// + private async Task ProcessRefundAsync( + YD_Event.Core.Entities.Order order, + EventRegistration registration, + decimal refundAmount, + string refundReason, + long userId) + { + var now = DateTime.Now; + + // 获取微信支付配置 + var weChatPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x => !x.DeleteMark); + + // 生成退款单号 + var outRefundNo = "RF" + DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(1000, 9999); + + // 创建退款记录 + var refundRecord = new RefundRecord + { + Id = _idGenerator.NewLong(), + OutRefundNo = outRefundNo, + RegistrationId = registration.Id, + OutTradeNo = order.OrderNo, + RefundAmount = refundAmount, + TotalAmount = order.ActualAmount, + RefundReason = refundReason, + RefundStatus = 0, + ApplyTime = now, + WeChatPayConfigId = weChatPayConfig?.Id, + CreatedTime = now, + CreatedUserId = userId, + Status = Core.Enum.AvailabilityStatus.Enable + }; + + // 获取支付记录中的微信交易号 + var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == registration.Id); + if (payment != null && !string.IsNullOrEmpty(payment.TransactionNo)) + { + refundRecord.TransactionId = payment.TransactionNo; + } + + await _refundRecordRepository.InsertAsync(refundRecord); + + // 如果是线上支付,调用微信退款接口 + if (order.PaymentMethod == 2 && weChatPayConfig != null) + { + await CreateWeChatRefundAsync(refundRecord, order, weChatPayConfig, refundAmount, registration, userId); + } + else + { + // 线下支付直接标记退款成功 + await CompleteRefundAsync(refundRecord, order, registration, userId); + } + } + + /// + /// 调用微信退款接口 + /// + private async Task CreateWeChatRefundAsync( + RefundRecord refundRecord, + YD_Event.Core.Entities.Order order, + WeChatPayConfig config, + decimal refundAmount, + EventRegistration registration, + long userId) + { + try + { + var request = new YD_Event.Core.Infrastructure.WeChat.Dto.CreateRefundRequest + { + OutTradeNo = order.OrderNo, + OutRefundNo = refundRecord.OutRefundNo, + RefundAmount = (int)(refundAmount * 100), + TotalAmount = (int)(order.ActualAmount * 100), + Reason = refundRecord.RefundReason + //NotifyUrl = config.RefundNotifyUrl + }; + + var result = await _weChatPayService.CreateRefundAsync(config, request); + + if (result.Success) + { + refundRecord.RefundId = result.RefundId; + refundRecord.WeChatResponse = result.WeChatResponse; + + if (result.RefundStatus == "SUCCESS") + { + await CompleteRefundAsync(refundRecord, order, registration, userId); + } + else + { + refundRecord.RefundStatus = 0; + await _refundRecordRepository.UpdateAsync(refundRecord); + } + } + else + { + refundRecord.RefundStatus = 2; + refundRecord.ErrorMessage = result.Message; + refundRecord.WeChatResponse = result.WeChatResponse; + await _refundRecordRepository.UpdateAsync(refundRecord); + throw Oops.Bah($"微信退款申请失败: {result.Message}"); + } + } + catch (Exception ex) + { + refundRecord.RefundStatus = 2; + refundRecord.ErrorMessage = ex.Message; + await _refundRecordRepository.UpdateAsync(refundRecord); + throw; + } + } + + /// + /// 完成退款流程 + /// + private async Task CompleteRefundAsync( + RefundRecord refundRecord, + YD_Event.Core.Entities.Order originalOrder, + EventRegistration registration, + long userId) + { + var now = DateTime.Now; + + // 更新退款记录状态 + refundRecord.RefundStatus = 1; + refundRecord.SuccessTime = now; + await _refundRecordRepository.UpdateAsync(refundRecord); + + // 创建退款订单 + var refundOrder = new YD_Event.Core.Entities.Order + { + Id = _idGenerator.NewLong(), + OrderNo = refundRecord.OutRefundNo, + OrderType = 2, + PaymentMethod = originalOrder.PaymentMethod, + RegistrationMode = originalOrder.RegistrationMode, + MerchantId = originalOrder.MerchantId, + MerchantCertificationId = originalOrder.MerchantCertificationId, + EventId = originalOrder.EventId, + RegistrationId = originalOrder.RegistrationId, + RelatedOrderId = originalOrder.Id, + OriginalAmount = originalOrder.ActualAmount, + ActualAmount = refundRecord.RefundAmount, + OrderStatus = OrderStatus.Refunded, + PaymentTime = now, + Remark = refundRecord.RefundReason, + CreatedTime = now, + CreatedUserId = userId, + Status = Core.Enum.AvailabilityStatus.Enable + }; + await _orderRepository.InsertAsync(refundOrder); + + // 更新原订单状态 + originalOrder.OrderStatus = OrderStatus.Refunded; + originalOrder.UpdatedTime = now; + originalOrder.UpdatedUserId = userId; + await _orderRepository.UpdateAsync(originalOrder); + + // 更新报名状态 + registration.PaymentStatus = 3; + registration.UpdatedTime = now; + registration.UpdatedUserId = userId; + await _registrationRepository.UpdateAsync(registration); + + // 更新支付记录 + var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == originalOrder.RegistrationId); + if (payment != null) + { + payment.RefundStatus = 1; + payment.UpdatedTime = now; + payment.UpdatedUserId = userId; + await _paymentRepository.UpdateAsync(payment); + } + } } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 42e519f..45c0a80 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -2509,6 +2509,26 @@ - 允许的字符:字母、数字、下划线、连字符 + + + 取消报名退款输入DTO + + + + + 报名记录ID + + + + + 退款原因 + + + + + 取消的项目ID列表(可选,用于部分取消) + + 小程序登录输入参数 @@ -3378,6 +3398,31 @@ 上传素材输入参数 操作是否成功 + + + 取消报名并申请退款 + + + + + 计算退款金额 + + + + + 处理退款核心逻辑 + + + + + 调用微信退款接口 + + + + + 完成退款流程 + + 小程序-个人中心接口