赛事退款
This commit is contained in:
parent
66d75e1497
commit
f9b5e8e1ee
@ -994,3 +994,26 @@ public class SelectPaymentMethodInput
|
|||||||
[RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "OpenId格式不正确,只能包含字母、数字、下划线和连字符")]
|
[RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "OpenId格式不正确,只能包含字母、数字、下划线和连字符")]
|
||||||
public string? OpenId { get; set; }
|
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; }
|
||||||
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ using Furion.FriendlyException;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using YD_Event.Core.Entities;
|
||||||
|
|
||||||
namespace YD_Event.Application.Client;
|
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<YD_Event.Core.Entities.Order> _orderRepository;
|
||||||
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
||||||
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
|
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
|
||||||
|
private readonly ISqlSugarRepository<RefundRecord> _refundRecordRepository;
|
||||||
|
private readonly ISqlSugarRepository<RefundConfig> _refundConfigRepository;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly IIdGenerator _idGenerator;
|
private readonly IIdGenerator _idGenerator;
|
||||||
private readonly WeChatPayService _weChatPayService;
|
private readonly WeChatPayService _weChatPayService;
|
||||||
@ -82,6 +85,8 @@ public class EventClientController : IDynamicApiController
|
|||||||
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
|
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
|
||||||
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
|
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
|
||||||
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
|
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
|
||||||
|
ISqlSugarRepository<RefundRecord> refundRecordRepository,
|
||||||
|
ISqlSugarRepository<RefundConfig> refundConfigRepository,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
IIdGenerator idGenerator,
|
IIdGenerator idGenerator,
|
||||||
WeChatPayService weChatPayService,
|
WeChatPayService weChatPayService,
|
||||||
@ -113,6 +118,8 @@ public class EventClientController : IDynamicApiController
|
|||||||
_orderRepository = orderRepository;
|
_orderRepository = orderRepository;
|
||||||
_merchantCertificationRepository = merchantCertificationRepository;
|
_merchantCertificationRepository = merchantCertificationRepository;
|
||||||
_paymentRepository = paymentRepository;
|
_paymentRepository = paymentRepository;
|
||||||
|
_refundRecordRepository = refundRecordRepository;
|
||||||
|
_refundConfigRepository = refundConfigRepository;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_idGenerator = idGenerator;
|
_idGenerator = idGenerator;
|
||||||
_weChatPayService = weChatPayService;
|
_weChatPayService = weChatPayService;
|
||||||
@ -1624,4 +1631,243 @@ public class EventClientController : IDynamicApiController
|
|||||||
|
|
||||||
return await _registrationRepository.UpdateAsync(registration);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2509,6 +2509,26 @@
|
|||||||
- 允许的字符:字母、数字、下划线、连字符
|
- 允许的字符:字母、数字、下划线、连字符
|
||||||
</remarks>
|
</remarks>
|
||||||
</member>
|
</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">
|
<member name="T:YD_Event.Application.Client.Dtos.MiniProgramLoginInput">
|
||||||
<summary>
|
<summary>
|
||||||
小程序登录输入参数
|
小程序登录输入参数
|
||||||
@ -3378,6 +3398,31 @@
|
|||||||
<param name="input">上传素材输入参数</param>
|
<param name="input">上传素材输入参数</param>
|
||||||
<returns>操作是否成功</returns>
|
<returns>操作是否成功</returns>
|
||||||
</member>
|
</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">
|
<member name="T:YD_Event.Application.Client.MyController">
|
||||||
<summary>
|
<summary>
|
||||||
小程序-个人中心接口
|
小程序-个人中心接口
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user