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 @@
上传素材输入参数
操作是否成功
+
+
+ 取消报名并申请退款
+
+
+
+
+ 计算退款金额
+
+
+
+
+ 处理退款核心逻辑
+
+
+
+
+ 调用微信退款接口
+
+
+
+
+ 完成退款流程
+
+
小程序-个人中心接口