赛事退款

This commit is contained in:
马梓彭 2026-04-09 23:45:24 +08:00
parent 66d75e1497
commit f9b5e8e1ee
3 changed files with 314 additions and 0 deletions

View File

@ -994,3 +994,26 @@ public class SelectPaymentMethodInput
[RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "OpenId格式不正确,只能包含字母、数字、下划线和连字符")]
public string? OpenId { get; set; }
}
/// <summary>
/// 取消报名退款输入DTO
/// </summary>
public class CancelRegistrationRefundInput
{
/// <summary>
/// 报名记录ID
/// </summary>
[Required(ErrorMessage = "报名记录ID不能为空")]
public long RegistrationId { get; set; }
/// <summary>
/// 退款原因
/// </summary>
[MaxLength(200, ErrorMessage = "退款原因不能超过200个字符")]
public string? RefundReason { get; set; }
/// <summary>
/// 取消的项目ID列表(可选,用于部分取消)
/// </summary>
public List<long>? CancelItemIds { get; set; }
}

View File

@ -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<YD_Event.Core.Entities.Order> _orderRepository;
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
private readonly ISqlSugarRepository<RefundRecord> _refundRecordRepository;
private readonly ISqlSugarRepository<RefundConfig> _refundConfigRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IIdGenerator _idGenerator;
private readonly WeChatPayService _weChatPayService;
@ -82,6 +85,8 @@ public class EventClientController : IDynamicApiController
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
ISqlSugarRepository<RefundRecord> refundRecordRepository,
ISqlSugarRepository<RefundConfig> 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);
}
/// <summary>
/// 取消报名并申请退款
/// </summary>
[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);
}
/// <summary>
/// 计算退款金额
/// </summary>
private async Task<decimal> 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;
}
/// <summary>
/// 处理退款核心逻辑
/// </summary>
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);
}
}
/// <summary>
/// 调用微信退款接口
/// </summary>
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;
}
}
/// <summary>
/// 完成退款流程
/// </summary>
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);
}
}
}

View File

@ -2509,6 +2509,26 @@
- 允许的字符:字母、数字、下划线、连字符
</remarks>
</member>
<member name="T:YD_Event.Application.Client.Dtos.CancelRegistrationRefundInput">
<summary>
取消报名退款输入DTO
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.CancelRegistrationRefundInput.RegistrationId">
<summary>
报名记录ID
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.CancelRegistrationRefundInput.RefundReason">
<summary>
退款原因
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.CancelRegistrationRefundInput.CancelItemIds">
<summary>
取消的项目ID列表(可选,用于部分取消)
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.MiniProgramLoginInput">
<summary>
小程序登录输入参数
@ -3378,6 +3398,31 @@
<param name="input">上传素材输入参数</param>
<returns>操作是否成功</returns>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.CancelRegistrationRefund(YD_Event.Application.Client.Dtos.CancelRegistrationRefundInput)">
<summary>
取消报名并申请退款
</summary>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.CalculateRefundAmount(System.Decimal,System.DateTime)">
<summary>
计算退款金额
</summary>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.ProcessRefundAsync(YD_Event.Core.Entities.Order,YD_Event.Core.Entities.EventRegistration,System.Decimal,System.String,System.Int64)">
<summary>
处理退款核心逻辑
</summary>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.CreateWeChatRefundAsync(YD_Event.Core.Entities.RefundRecord,YD_Event.Core.Entities.Order,YD_Event.Core.Entities.WeChatPayConfig,System.Decimal,YD_Event.Core.Entities.EventRegistration,System.Int64)">
<summary>
调用微信退款接口
</summary>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.CompleteRefundAsync(YD_Event.Core.Entities.RefundRecord,YD_Event.Core.Entities.Order,YD_Event.Core.Entities.EventRegistration,System.Int64)">
<summary>
完成退款流程
</summary>
</member>
<member name="T:YD_Event.Application.Client.MyController">
<summary>
小程序-个人中心接口