3821 lines
168 KiB
C#
3821 lines
168 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using YD_Event.Application.Event.Dtos;
|
||
using YD_Event.Application.Order.Dtos;
|
||
using YD_Event.Application.Refund;
|
||
using YD_Event.Core.Domain;
|
||
using YD_Event.Core.Domain.Pricing;
|
||
using YD_Event.Core.Domain.Pricing.Models;
|
||
using YD_Event.Core.Entities;
|
||
using YD_Event.Core.Enum;
|
||
using YD_Event.Core.Extensions;
|
||
using YD_Event.Core.Infrastructure.WeChat;
|
||
using YD_Event.Core.Infrastructure.WeChat.Dto;
|
||
using YD_Event.Core.Options;
|
||
|
||
namespace YD_Event.Application.Event;
|
||
|
||
/// <summary>
|
||
/// 统一报名处理服务
|
||
/// </summary>
|
||
public class UnifiedRegistrationService : ITransient
|
||
{
|
||
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
|
||
private readonly ISqlSugarRepository<EventUnit> _unitRepository;
|
||
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
|
||
private readonly ISqlSugarRepository<EventProjectGroup> _eventProjectGroupRepository;
|
||
private readonly ISqlSugarRepository<ProjectGroup> _projectGroupRepository;
|
||
private readonly ISqlSugarRepository<EventProjectRegistrationLimit> _projectLimitRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
|
||
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
|
||
private readonly ISqlSugarRepository<EventRegistrationItem> _registrationItemRepository;
|
||
private readonly ISqlSugarRepository<EventRegistrationPerson> _registrationPersonRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
|
||
private readonly ISqlSugarRepository<SysUser> _sysUserRepository;
|
||
private readonly ISqlSugarRepository<EventFeeConfig> _feeConfigRepository;
|
||
private readonly ISqlSugarRepository<EventRegistrationLimit> _limitRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
|
||
private readonly ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Coupon> _couponRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.UserCoupon> _userCouponRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.CouponRedemptionCode> _redemptionCodeRepository;
|
||
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
|
||
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
||
private readonly ISqlSugarClient _db;
|
||
private readonly IIdGenerator _idGenerator;
|
||
private readonly WeChatPayService _weChatPayService;
|
||
private readonly IOptionsMonitor<WechatTenpayClientOptions> _wechatTenpayClientOptionsMonitor;
|
||
private readonly ILogger<UnifiedRegistrationService> _logger;
|
||
private readonly OrderNoGeneratorService _orderNoGeneratorService;
|
||
private readonly RefundConfigService _refundConfigService;
|
||
private readonly ISqlSugarRepository<RefundRecord> _refundRecordRepository;
|
||
|
||
public UnifiedRegistrationService(
|
||
ISqlSugarRepository<EventInfo> eventRepository,
|
||
ISqlSugarRepository<EventUnit> unitRepository,
|
||
ISqlSugarRepository<EventProject> eventProjectRepository,
|
||
ISqlSugarRepository<EventProjectGroup> eventProjectGroupRepository,
|
||
ISqlSugarRepository<ProjectGroup> projectGroupRepository,
|
||
ISqlSugarRepository<EventProjectRegistrationLimit> projectLimitRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
|
||
ISqlSugarRepository<EventRegistration> registrationRepository,
|
||
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
|
||
ISqlSugarRepository<EventRegistrationPerson> registrationPersonRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
|
||
ISqlSugarRepository<SysUser> sysUserRepository,
|
||
ISqlSugarRepository<EventFeeConfig> feeConfigRepository,
|
||
ISqlSugarRepository<EventRegistrationLimit> limitRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
|
||
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Coupon> couponRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.UserCoupon> userCouponRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.CouponRedemptionCode> redemptionCodeRepository,
|
||
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
|
||
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
|
||
ISqlSugarClient db,
|
||
IIdGenerator idGenerator,
|
||
WeChatPayService weChatPayService,
|
||
IOptionsMonitor<WechatTenpayClientOptions> wechatTenpayClientOptionsMonitor,
|
||
ILogger<UnifiedRegistrationService> logger,
|
||
OrderNoGeneratorService orderNoGeneratorService,
|
||
RefundConfigService refundConfigService,
|
||
ISqlSugarRepository<RefundRecord> refundRecordRepository)
|
||
{
|
||
_eventRepository = eventRepository;
|
||
_unitRepository = unitRepository;
|
||
_eventProjectRepository = eventProjectRepository;
|
||
_eventProjectGroupRepository = eventProjectGroupRepository;
|
||
_projectGroupRepository = projectGroupRepository;
|
||
_projectLimitRepository = projectLimitRepository;
|
||
_projectRepository = projectRepository;
|
||
_registrationRepository = registrationRepository;
|
||
_registrationItemRepository = registrationItemRepository;
|
||
_registrationPersonRepository = registrationPersonRepository;
|
||
_userRepository = userRepository;
|
||
_sysUserRepository = sysUserRepository;
|
||
_feeConfigRepository = feeConfigRepository;
|
||
_limitRepository = limitRepository;
|
||
_orderRepository = orderRepository;
|
||
_weChatPayConfigRepository = weChatPayConfigRepository;
|
||
_merchantRepository = merchantRepository;
|
||
_couponRepository = couponRepository;
|
||
_userCouponRepository = userCouponRepository;
|
||
_redemptionCodeRepository = redemptionCodeRepository;
|
||
_paymentRepository = paymentRepository;
|
||
_merchantCertificationRepository = merchantCertificationRepository;
|
||
_db = db;
|
||
_idGenerator = idGenerator;
|
||
_weChatPayService = weChatPayService;
|
||
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
|
||
_logger = logger;
|
||
_orderNoGeneratorService = orderNoGeneratorService;
|
||
_refundConfigService = refundConfigService;
|
||
_refundRecordRepository = refundRecordRepository;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一报名方法A - 处理核心报名业务逻辑
|
||
/// </summary>
|
||
[UnitOfWork]
|
||
public async Task<UnifiedRegistrationOutput> ProcessRegistrationAsync(UnifiedRegistrationInput input)
|
||
{
|
||
_logger.LogInformation("开始处理统一报名,赛事ID: {EventId}, 模式: {Mode}", input.EventId, input.RegistrationMode);
|
||
|
||
// 1. 验证报名
|
||
var validationResult = await ValidateRegistrationAsync(input);
|
||
if (!validationResult.IsValid)
|
||
{
|
||
throw Oops.Bah(validationResult.ErrorMessage ?? "报名验证失败");
|
||
}
|
||
|
||
// 2. 获取或创建报名记录
|
||
var registration = await GetOrCreateRegistrationAsync(input);
|
||
|
||
// 3. 创建报名明细和参赛人员
|
||
var (totalAmount, totalPersonCount) = await CreateRegistrationDetailsAsync(registration, input);
|
||
|
||
// 4. 更新报名信息
|
||
registration.TotalAmount = totalAmount;
|
||
registration.TotalPersonCount = totalPersonCount;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
|
||
_logger.LogInformation("统一报名处理完成,报名ID: {RegistrationId}", registration.Id);
|
||
|
||
return new UnifiedRegistrationOutput
|
||
{
|
||
RegistrationId = registration.Id,
|
||
OrderNo = registration.OrderNo,
|
||
OriginalAmount = totalAmount,
|
||
DiscountAmount = 0,
|
||
ActualAmount = totalAmount,
|
||
PaymentStatus = registration.PaymentStatus
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或创建报名记录
|
||
/// </summary>
|
||
private async Task<EventRegistration> GetOrCreateRegistrationAsync(UnifiedRegistrationInput input)
|
||
{
|
||
var existingRegistration = await _registrationRepository.GetFirstAsync(
|
||
r => r.EventId == input.EventId && r.CreatedUserId == input.CreatedUserId && r.PaymentStatus != 4 && !r.DeleteMark);
|
||
|
||
if (existingRegistration != null)
|
||
{
|
||
return await UpdateExistingRegistrationAsync(existingRegistration, input);
|
||
}
|
||
|
||
return await CreateNewRegistrationAsync(input);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新现有报名记录
|
||
/// </summary>
|
||
private async Task<EventRegistration> UpdateExistingRegistrationAsync(EventRegistration registration, UnifiedRegistrationInput input)
|
||
{
|
||
registration.RegistrationMode = input.RegistrationMode;
|
||
registration.TeamName = input.RegistrationMode == 2 ? input.TeamName : null;
|
||
registration.ContactName = input.ContactName;
|
||
registration.ContactPhone = input.ContactPhone;
|
||
registration.PaymentStatus = 0;
|
||
registration.OrderNo = string.Empty; // 清空旧的订单号,确保重新生成新订单
|
||
registration.UpdatedTime = DateTime.Now;
|
||
registration.UpdatedUserId = input.CreatedUserId;
|
||
|
||
// 删除旧的明细和人员
|
||
await _registrationItemRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
|
||
await _registrationPersonRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
|
||
|
||
return registration;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建新的报名记录
|
||
/// </summary>
|
||
private async Task<EventRegistration> CreateNewRegistrationAsync(UnifiedRegistrationInput input)
|
||
{
|
||
// 查询报名用户的单位认证信息
|
||
long? merchantCertId = null;
|
||
var merchantCert = await _merchantCertificationRepository.GetFirstAsync(m =>
|
||
m.UserId == input.CreatedUserId &&
|
||
m.AuditStatus == AuditStatus.Pass &&
|
||
!m.DeleteMark);
|
||
if (merchantCert != null)
|
||
{
|
||
merchantCertId = merchantCert.Id;
|
||
}
|
||
|
||
var registration = new EventRegistration
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
EventId = input.EventId,
|
||
RegistrationMode = input.RegistrationMode,
|
||
TeamName = input.RegistrationMode == 2 ? input.TeamName : null,
|
||
ContactName = input.ContactName,
|
||
ContactPhone = input.ContactPhone,
|
||
PaymentStatus = 5, // 5=待支付确认,不占用正式名额
|
||
RegistrationTime = DateTime.Now,
|
||
CreatedUserId = input.CreatedUserId,
|
||
CreatedTime = DateTime.Now,
|
||
Status = AvailabilityStatus.Enable,
|
||
OrderNo = string.Empty,
|
||
MerchantCertificationId = merchantCertId
|
||
};
|
||
|
||
await _registrationRepository.InsertAsync(registration);
|
||
_logger.LogInformation("创建预报名记录(待支付确认),报名ID: {RegistrationId}", registration.Id);
|
||
return registration;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一支付处理方法 - 处理支付相关逻辑
|
||
/// </summary>
|
||
[UnitOfWork]
|
||
public async Task<UnifiedPaymentOutput> ProcessPaymentAsync(UnifiedPaymentInput input)
|
||
{
|
||
_logger.LogInformation("开始处理统一支付,报名ID: {RegistrationId}, 支付方式: {PaymentMethod}",
|
||
input.RegistrationId, input.PaymentMethod);
|
||
|
||
// 1. 获取报名信息
|
||
var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId);
|
||
if (registration == null)
|
||
{
|
||
throw Oops.Bah("报名记录不存在");
|
||
}
|
||
|
||
// 2. 获取赛事信息
|
||
var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
|
||
if (eventInfo == null)
|
||
{
|
||
throw Oops.Bah("赛事信息不存在");
|
||
}
|
||
|
||
// 3. 计算优惠后金额(每次都重新计算,确保金额正确)
|
||
var calculateResult = await CalculateFinalAmountAsync(
|
||
registration.TotalAmount, input.CouponId, input.OperatorUserId);
|
||
var originalAmount = calculateResult.originalAmount;
|
||
var discountAmount = calculateResult.discountAmount;
|
||
var finalAmount = calculateResult.finalAmount;
|
||
var usedCoupon = calculateResult.coupon;
|
||
var usedUserCoupon = calculateResult.userCoupon;
|
||
|
||
// 5. 构建支付输出对象
|
||
var output = new UnifiedPaymentOutput
|
||
{
|
||
RegistrationId = registration.Id,
|
||
OrderNo = registration.OrderNo,
|
||
OriginalAmount = originalAmount,
|
||
DiscountAmount = discountAmount,
|
||
ActualAmount = finalAmount,
|
||
PaymentStatus = registration.PaymentStatus,
|
||
IsFree = finalAmount <= 0,
|
||
CouponRuleNote = usedCoupon != null ? "优惠券使用后不予退还" : null
|
||
};
|
||
|
||
// 6. 处理免费情况
|
||
if (finalAmount <= 0)
|
||
{
|
||
if (registration.PaymentStatus != 1)
|
||
{
|
||
await HandleFreePaymentAsync(registration, usedUserCoupon, input.OperatorUserId);
|
||
}
|
||
output.PaymentStatus = 1;
|
||
return output;
|
||
}
|
||
|
||
// 7. 生成订单号(强制重新生成新的订单号,确保不会重复使用旧的订单号)
|
||
// 即使之前有订单号,也要重新生成,避免微信端的旧订单影响
|
||
var oldOrderNo = registration.OrderNo;
|
||
registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
|
||
output.OrderNo = registration.OrderNo;
|
||
|
||
_logger.LogInformation("生成新订单号,报名ID: {RegistrationId}, 旧订单号: {OldOrderNo}, 新订单号: {NewOrderNo}",
|
||
registration.Id, oldOrderNo, registration.OrderNo);
|
||
|
||
// 处理支付方式转换逻辑
|
||
registration.PaymentMethod = GetPaymentMethodByType(input.PaymentMethodType) ?? input.PaymentMethod;
|
||
registration.PaymentMethodType = input.PaymentMethodType;
|
||
|
||
// 设置支付状态
|
||
registration.PaymentStatus = 0;
|
||
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
|
||
// 8. 创建新的订单记录(强制创建新订单)
|
||
await CreateOrderAsync(registration, eventInfo, originalAmount, discountAmount, finalAmount,
|
||
input.CouponId, input.OperatorUserId);
|
||
|
||
// 9. 根据支付方式处理
|
||
if (input.PaymentMethod == 2) // 线上支付
|
||
{
|
||
// 检查是否已经有支付记录或已支付状态,避免重复调用微信支付
|
||
var existingPayment = await _paymentRepository.GetFirstAsync(p =>
|
||
p.RegistrationId == registration.Id && !p.DeleteMark && p.RefundStatus != 1);
|
||
|
||
if (registration.PaymentStatus == 1)
|
||
{
|
||
_logger.LogInformation("订单已支付,跳过微信统一下单,报名ID: {RegistrationId}",
|
||
registration.Id);
|
||
output.PaymentStatus = registration.PaymentStatus;
|
||
return output;
|
||
}
|
||
|
||
var onlinePaymentOutput = await HandleOnlinePaymentAsync(registration, eventInfo, finalAmount, input);
|
||
output.WechatPayParams = onlinePaymentOutput.WechatPayParams;
|
||
output.QrcodeUrl = onlinePaymentOutput.QrcodeUrl;
|
||
output.TradeType = onlinePaymentOutput.TradeType;
|
||
}
|
||
else // 线下支付
|
||
{
|
||
var offlinePaymentOutput = await HandleOfflinePaymentAsync(registration, input);
|
||
output.PaymentStatus = offlinePaymentOutput.PaymentStatus;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证报名
|
||
/// </summary>
|
||
private async Task<RegistrationValidationResult> ValidateRegistrationAsync(UnifiedRegistrationInput input)
|
||
{
|
||
var result = new RegistrationValidationResult
|
||
{
|
||
IsValid = true,
|
||
Details = new List<ValidationDetail>()
|
||
};
|
||
|
||
// 1. 验证赛事是否存在且可报名
|
||
var eventInfo = await _eventRepository.GetByIdAsync(input.EventId);
|
||
if (eventInfo == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "赛事不存在";
|
||
result.Details.Add(CreateValidationDetail("赛事验证", false, "赛事不存在"));
|
||
return result;
|
||
}
|
||
|
||
// 1.5 验证报名相关配置参数
|
||
var configValidationResult = await ValidateRegistrationConfigAsync(eventInfo);
|
||
if (!configValidationResult.IsValid)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = configValidationResult.ErrorMessage;
|
||
result.Details.AddRange(configValidationResult.Details);
|
||
return result;
|
||
}
|
||
result.Details.AddRange(configValidationResult.Details);
|
||
|
||
// 2. 验证用户单位认证状态:仅在满足以下特定条件时执行单位认证状态验证
|
||
// 条件1:报名类型为个人报名(非团队/机构报名)
|
||
// 条件2:用户通过小程序渠道进行报名
|
||
// 当同时满足上述两个条件时,执行以下单位认证验证逻辑
|
||
var isIndividualRegistration = input.RegistrationMode == 1; // 1-个人报名
|
||
var isMiniAppChannel = input.RegistrationChannel == 1; // 1-小程序渠道
|
||
|
||
if (isIndividualRegistration && isMiniAppChannel)
|
||
{
|
||
var certValidationResult = await ValidateUserCertificationAsync(input.CreatedUserId);
|
||
if (!certValidationResult.IsValid)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = certValidationResult.ErrorMessage;
|
||
result.Details.AddRange(certValidationResult.Details);
|
||
return result;
|
||
}
|
||
result.Details.AddRange(certValidationResult.Details);
|
||
}
|
||
|
||
// 3. 验证报名时间
|
||
var now = DateTime.Now;
|
||
if (now < eventInfo.RegistrationStartTime || now > eventInfo.RegistrationEndTime)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "不在报名时间内";
|
||
result.Details.Add(CreateValidationDetail("时间验证", false, "不在报名时间内"));
|
||
return result;
|
||
}
|
||
|
||
// 4. 验证项目选择(一阶段:单选机制,每个项目只能选择一个组别)
|
||
var projectSelectionResult = await ValidateProjectSelectionAsync(input);
|
||
if (!projectSelectionResult.IsValid)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = projectSelectionResult.ErrorMessage;
|
||
result.Details.AddRange(projectSelectionResult.Details);
|
||
return result;
|
||
}
|
||
result.Details.AddRange(projectSelectionResult.Details);
|
||
|
||
// 5. 验证队员分配(二阶段:人数校验、资质验证、年龄跨组等)
|
||
var teamAssignmentResult = await ValidateTeamAssignmentAsync(input);
|
||
if (!teamAssignmentResult.IsValid)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = teamAssignmentResult.ErrorMessage;
|
||
result.Details.AddRange(teamAssignmentResult.Details);
|
||
return result;
|
||
}
|
||
result.Details.AddRange(teamAssignmentResult.Details);
|
||
|
||
// 6. 验证报名限制
|
||
var limitCheckResult = await CheckRegistrationLimitAsync(input);
|
||
if (limitCheckResult.HasLimit)
|
||
{
|
||
result.IsValid = false;
|
||
var firstLimit = limitCheckResult.LimitDetails.FirstOrDefault();
|
||
result.ErrorMessage = firstLimit?.LimitMessage ?? "报名限制检查失败";
|
||
result.Details.AddRange(limitCheckResult.LimitDetails.Select(l => CreateValidationDetail(l.LimitType, false, l.LimitMessage)));
|
||
return result;
|
||
}
|
||
|
||
result.Details.Add(CreateValidationDetail("整体验证", true, "验证通过"));
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证项目选择(一阶段:单选机制)
|
||
/// </summary>
|
||
private async Task<RegistrationValidationResult> ValidateProjectSelectionAsync(UnifiedRegistrationInput input)
|
||
{
|
||
var result = new RegistrationValidationResult
|
||
{
|
||
IsValid = true,
|
||
Details = new List<ValidationDetail>()
|
||
};
|
||
|
||
// 1. 检查是否有重复选择的项目(每个项目只能选择一个组别)
|
||
var duplicateProjects = input.Items
|
||
.GroupBy(i => i.ProjectId)
|
||
.Where(g => g.Count() > 1)
|
||
.Select(g => g.Key)
|
||
.ToList();
|
||
|
||
if (duplicateProjects.Any())
|
||
{
|
||
var eventProjects = await _eventProjectRepository.GetListAsync(x => duplicateProjects.Contains(x.Id));
|
||
var projectNames = eventProjects.Select(ep => $"{ep.ProjectId}").ToList();
|
||
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"以下项目被重复选择,请确保每个项目只选择一个组别:{string.Join(", ", projectNames)}";
|
||
result.Details.Add(CreateValidationDetail("项目选择", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
result.Details.Add(CreateValidationDetail("项目单选机制", true, "每个项目仅选择一个组别,符合要求"));
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证队员分配(二阶段)
|
||
/// </summary>
|
||
public async Task<RegistrationValidationResult> ValidateTeamAssignmentAsync(UnifiedRegistrationInput input)
|
||
{
|
||
var result = new RegistrationValidationResult
|
||
{
|
||
IsValid = true,
|
||
Details = new List<ValidationDetail>()
|
||
};
|
||
|
||
var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList();
|
||
var groupIds = input.Items.Where(i => i.GroupId.HasValue).Select(i => i.GroupId.Value).Distinct().ToList();
|
||
|
||
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
|
||
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
|
||
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
|
||
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x =>
|
||
eventProjectIds.Contains(x.EventProjectId) || groupIds.Contains(x.Id));
|
||
|
||
foreach (var item in input.Items)
|
||
{
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId);
|
||
if (eventProject == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"项目不存在: {item.ProjectId}";
|
||
result.Details.Add(CreateValidationDetail("项目验证", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
var isTeamProject = baseProject?.IsTeamProject() ?? false;
|
||
|
||
if (!item.GroupId.HasValue)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"项目组别不能为空";
|
||
result.Details.Add(CreateValidationDetail("组别验证", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
||
if (eventProjectGroup == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"项目组别不存在: {item.GroupId.Value}";
|
||
result.Details.Add(CreateValidationDetail("组别验证", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
var teamsToValidate = GetTeamsFromItem(item);
|
||
if (!teamsToValidate.Any())
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"请为项目添加至少一个团队和队员";
|
||
result.Details.Add(CreateValidationDetail("团队验证", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
foreach (var team in teamsToValidate)
|
||
{
|
||
if (team.Persons == null || !team.Persons.Any())
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"团队{team.TeamIndex}请至少添加一个队员";
|
||
result.Details.Add(CreateValidationDetail("团队验证", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
var qualificationResult = ValidatePersonQualifications(team.Persons, eventProjectGroup, isTeamProject);
|
||
if (!qualificationResult.IsValid)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = qualificationResult.ErrorMessage;
|
||
result.Details.AddRange(qualificationResult.Details);
|
||
return result;
|
||
}
|
||
result.Details.AddRange(qualificationResult.Details);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从报名项中获取团队列表(支持新旧数据结构)
|
||
/// </summary>
|
||
private static List<UnifiedTeamInput> GetTeamsFromItem(UnifiedRegistrationItemInput item)
|
||
{
|
||
if (item.Teams != null && item.Teams.Any())
|
||
{
|
||
return item.Teams;
|
||
}
|
||
|
||
if (item.Persons != null && item.Persons.Any())
|
||
{
|
||
return new List<UnifiedTeamInput>
|
||
{
|
||
new()
|
||
{
|
||
TeamIndex = 1,
|
||
Persons = item.Persons
|
||
}
|
||
};
|
||
}
|
||
|
||
return new List<UnifiedTeamInput>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从报名项中获取所有人员列表(支持新旧数据结构)
|
||
/// </summary>
|
||
private static List<UnifiedPersonInput> GetAllPersonsFromItem(UnifiedRegistrationItemInput item)
|
||
{
|
||
var persons = new List<UnifiedPersonInput>();
|
||
var teams = GetTeamsFromItem(item);
|
||
|
||
foreach (var team in teams)
|
||
{
|
||
if (team.Persons != null)
|
||
{
|
||
persons.AddRange(team.Persons);
|
||
}
|
||
}
|
||
|
||
return persons;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证队员资质(年龄和性别)
|
||
/// </summary>
|
||
public RegistrationValidationResult ValidatePersonQualifications(List<UnifiedPersonInput> persons, EventProjectGroup group, bool isTeamProject)
|
||
{
|
||
var result = new RegistrationValidationResult
|
||
{
|
||
IsValid = true,
|
||
Details = new List<ValidationDetail>()
|
||
};
|
||
|
||
// 首先验证性别,发现问题立即返回
|
||
var genderError = ValidatePersonGenders(persons, group);
|
||
if (genderError != null)
|
||
{
|
||
return genderError;
|
||
}
|
||
|
||
// 验证年龄限制和跨组规则
|
||
var ageError = ValidatePersonAges(persons, group, isTeamProject, result);
|
||
if (ageError != null)
|
||
{
|
||
return ageError;
|
||
}
|
||
|
||
result.Details.Add(CreateValidationDetail("队员资质", true, "所有队员的性别符合组别要求,年龄符合规则"));
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证队员性别
|
||
/// </summary>
|
||
private static RegistrationValidationResult? ValidatePersonGenders(List<UnifiedPersonInput> persons, EventProjectGroup group)
|
||
{
|
||
if (group.Gender == Gender.All)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
foreach (var person in persons)
|
||
{
|
||
if (person.Gender != group.Gender)
|
||
{
|
||
var errorMessage = $"队员{person.Name}的性别不符合组别要求。要求:{group.Gender.Description()},实际:{person.Gender.Description()}";
|
||
return new RegistrationValidationResult
|
||
{
|
||
IsValid = false,
|
||
ErrorMessage = errorMessage,
|
||
Details = new List<ValidationDetail> { CreateValidationDetail("性别验证", false, errorMessage) }
|
||
};
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证队员年龄
|
||
/// </summary>
|
||
private static RegistrationValidationResult? ValidatePersonAges(List<UnifiedPersonInput> persons, EventProjectGroup group, bool isTeamProject, RegistrationValidationResult result)
|
||
{
|
||
// 检查是否有超过最大年龄限制的队员
|
||
var aboveAgePerson = persons.FirstOrDefault(p => p.BirthDate > group.BirthDateEnd);
|
||
if (aboveAgePerson != null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = $"队员{aboveAgePerson.Name}的年龄超过组别最大限制。要求出生日期不晚于:{group.BirthDateEnd:yyyy-MM-dd},实际:{aboveAgePerson.BirthDate:yyyy-MM-dd}";
|
||
result.Details.Add(CreateValidationDetail("年龄验证", false, result.ErrorMessage));
|
||
return result;
|
||
}
|
||
|
||
// 统计年龄状态
|
||
var inTargetCount = persons.Count(p => p.BirthDate >= group.BirthDateStart && p.BirthDate <= group.BirthDateEnd);
|
||
var belowTargetCount = persons.Count(p => p.BirthDate < group.BirthDateStart);
|
||
bool hasBelow = belowTargetCount > 0;
|
||
bool allBelow = belowTargetCount == persons.Count;
|
||
|
||
// 团队项目跨组验证
|
||
if (isTeamProject && hasBelow && inTargetCount == 0)
|
||
{
|
||
string errorMessage = allBelow
|
||
? "跨组报名时,团队中所有成员年龄均小于目标年龄段,团队中至少需要有一名成员属于目标年龄段"
|
||
: "跨组报名时,团队中至少需要有一名成员属于目标年龄段";
|
||
|
||
string itemName = allBelow
|
||
? "年龄跨组验证(全部小于目标年龄)"
|
||
: "年龄跨组验证";
|
||
|
||
result.IsValid = false;
|
||
result.ErrorMessage = errorMessage;
|
||
result.Details.Add(CreateValidationDetail(itemName, false, errorMessage));
|
||
return result;
|
||
}
|
||
|
||
// 添加验证通过的详情
|
||
result.Details.Add(hasBelow
|
||
? CreateValidationDetail("年龄跨组", true, "符合年龄跨组报名要求")
|
||
: CreateValidationDetail("年龄验证", true, "所有队员均在目标年龄段内"));
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建报名明细和参赛人员
|
||
/// </summary>
|
||
private async Task<(decimal totalAmount, int totalPersonCount)> CreateRegistrationDetailsAsync(
|
||
EventRegistration registration, UnifiedRegistrationInput input)
|
||
{
|
||
// 获取相关数据
|
||
var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList();
|
||
var groupIds = input.Items.Where(i => i.GroupId.HasValue).Select(i => i.GroupId.Value).Distinct().ToList();
|
||
|
||
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
|
||
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
|
||
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
|
||
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x =>
|
||
eventProjectIds.Contains(x.EventProjectId) || groupIds.Contains(x.Id));
|
||
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
||
|
||
var regItems = new List<EventRegistrationItem>();
|
||
var persons = new List<EventRegistrationPerson>();
|
||
var uniqueUserIds = new HashSet<long>(); // 用于去重的用户ID集合(仅用于单价赛事模式)
|
||
|
||
foreach (var item in input.Items)
|
||
{
|
||
var eventProject = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
var groupName = GetGroupName(item, eventProjectGroups);
|
||
var teamsToProcess = GetTeamsFromItem(item);
|
||
|
||
foreach (var team in teamsToProcess)
|
||
{
|
||
var (unitPrice, totalPrice) = CalculateItemPrice(feeConfig, eventProject, team.Persons.Count);
|
||
|
||
var regItemId = _idGenerator.NewLong();
|
||
var regItem = new EventRegistrationItem
|
||
{
|
||
Id = regItemId,
|
||
RegistrationId = registration.Id,
|
||
ProjectId = item.ProjectId,
|
||
EventProjectGroupId = item.GroupId,
|
||
ProjectName = baseProject?.Name ?? "",
|
||
GroupName = groupName,
|
||
ProjectTypeName = baseProject?.ProjectType.Description() ?? "",
|
||
UnitPrice = unitPrice,
|
||
PersonCount = team.Persons.Count,
|
||
TotalPrice = totalPrice,
|
||
CreatedUserId = input.CreatedUserId,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
regItems.Add(regItem);
|
||
|
||
foreach (var pInput in team.Persons)
|
||
{
|
||
persons.Add(new EventRegistrationPerson
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
RegistrationId = registration.Id,
|
||
RegistrationItemId = regItemId,
|
||
ProjectId = item.ProjectId,
|
||
EventProjectGroupId = item.GroupId,
|
||
GroupName = groupName,
|
||
ProjectName = baseProject?.Name ?? "",
|
||
Name = pInput.Name,
|
||
Gender = pInput.Gender,
|
||
IdCard = pInput.IdCardNumber,
|
||
BirthDate = pInput.BirthDate,
|
||
Phone = pInput.Mobile,
|
||
Age = CalculateAge(pInput.BirthDate),
|
||
PersonType = PersonType.Athlete,
|
||
UserId = pInput.UserId ?? 0,
|
||
CreatedUserId = input.CreatedUserId,
|
||
CreatedTime = DateTime.Now
|
||
});
|
||
|
||
// 将用户ID添加到去重集合中
|
||
if (pInput.UserId.HasValue)
|
||
{
|
||
uniqueUserIds.Add(pInput.UserId.Value);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
await _registrationItemRepository.InsertRangeAsync(regItems);
|
||
await _registrationPersonRepository.InsertRangeAsync(persons);
|
||
|
||
// 根据收费模式决定使用哪种人数计算金额
|
||
int countForCalculation;
|
||
if (feeConfig != null && feeConfig.ChargeMode == ChargeMode.UnitPriceEvent)
|
||
{
|
||
// 单价(赛事)模式:使用去重后的人数
|
||
countForCalculation = uniqueUserIds.Count;
|
||
}
|
||
else
|
||
{
|
||
// 其他模式(包括单价项目):使用实际人数
|
||
countForCalculation = persons.Count;
|
||
}
|
||
|
||
var totalAmount = CalculateTotalAmount(feeConfig, eventProjects, input.Items, countForCalculation);
|
||
|
||
return (totalAmount, countForCalculation);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算最终金额(含优惠券)
|
||
/// </summary>
|
||
private async Task<(decimal originalAmount, decimal discountAmount, decimal finalAmount, YD_Event.Core.Entities.Coupon? coupon, YD_Event.Core.Entities.UserCoupon? userCoupon)>
|
||
CalculateFinalAmountAsync(decimal originalAmount, long? couponId, long userId)
|
||
{
|
||
decimal discountAmount = 0;
|
||
decimal finalAmount = originalAmount;
|
||
YD_Event.Core.Entities.Coupon? coupon = null;
|
||
YD_Event.Core.Entities.UserCoupon? userCoupon = null;
|
||
|
||
if (couponId.HasValue)
|
||
{
|
||
coupon = await _couponRepository.GetFirstAsync(c =>
|
||
c.Id == couponId.Value && c.Status == AvailabilityStatus.Enable && !c.DeleteMark);
|
||
|
||
if (coupon != null)
|
||
{
|
||
userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
|
||
uc.CouponId == coupon.Id && uc.UserId == userId && uc.Status == 0 && !uc.DeleteMark);
|
||
|
||
if (userCoupon != null)
|
||
{
|
||
(discountAmount, finalAmount) = CalculateCouponDiscount(coupon, originalAmount);
|
||
}
|
||
}
|
||
}
|
||
|
||
return (originalAmount, discountAmount, finalAmount, coupon, userCoupon);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算优惠券折扣
|
||
/// </summary>
|
||
private static (decimal discountAmount, decimal finalAmount) CalculateCouponDiscount(
|
||
YD_Event.Core.Entities.Coupon coupon, decimal originalAmount)
|
||
{
|
||
decimal discountAmount = 0;
|
||
|
||
if (coupon.Type == 0) // 折扣
|
||
{
|
||
discountAmount = originalAmount * (1 - coupon.Value / 100);
|
||
}
|
||
else if (coupon.Type == 1) // 定额
|
||
{
|
||
discountAmount = Math.Min(coupon.Value, originalAmount);
|
||
}
|
||
|
||
var finalAmount = Math.Max(0, originalAmount - discountAmount);
|
||
return (discountAmount, finalAmount);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据支付方式类型获取对应的支付方式
|
||
/// </summary>
|
||
private static int? GetPaymentMethodByType(PaymentMethodType type)
|
||
{
|
||
return type switch
|
||
{
|
||
PaymentMethodType.WeChatPay or PaymentMethodType.AliPay => 2, // 线上支付
|
||
PaymentMethodType.BankTransfer => 1, // 线下支付
|
||
_ => null
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理免费支付
|
||
/// </summary>
|
||
private async Task HandleFreePaymentAsync(EventRegistration registration, YD_Event.Core.Entities.UserCoupon? userCoupon, long operatorUserId)
|
||
{
|
||
if (userCoupon != null)
|
||
{
|
||
userCoupon.Status = 1;
|
||
userCoupon.UseTime = DateTime.Now;
|
||
await _userCouponRepository.UpdateAsync(userCoupon);
|
||
}
|
||
|
||
registration.PaymentStatus = 1;
|
||
registration.PaymentMethod = 1; // 免费赛事设置为线下支付方式(但实际不产生费用)
|
||
registration.PaymentTime = DateTime.Now;
|
||
registration.UpdatedTime = DateTime.Now;
|
||
registration.UpdatedUserId = operatorUserId;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建订单记录
|
||
/// </summary>
|
||
private async Task<YD_Event.Core.Entities.Order> CreateOrderAsync(EventRegistration registration, EventInfo eventInfo,
|
||
decimal originalAmount, decimal discountAmount, decimal finalAmount, long? couponId, long operatorUserId)
|
||
{
|
||
var certification = await _merchantCertificationRepository.GetFirstAsync(c =>
|
||
c.UserId == registration.CreatedUserId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
|
||
|
||
var order = new YD_Event.Core.Entities.Order
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
OrderNo = registration.OrderNo,
|
||
OrderType = 1,
|
||
MerchantId = eventInfo.MerchantId,
|
||
MerchantCertificationId = certification?.Id,
|
||
EventId = registration.EventId,
|
||
RegistrationId = registration.Id,
|
||
OriginalAmount = originalAmount,
|
||
ActualAmount = finalAmount,
|
||
DiscountAmount = discountAmount,
|
||
CouponId = couponId,
|
||
OrderStatus = OrderStatus.Pending,
|
||
CreatedTime = DateTime.Now,
|
||
CreatedUserId = operatorUserId,
|
||
Status = AvailabilityStatus.Enable
|
||
};
|
||
await _orderRepository.InsertAsync(order);
|
||
return order;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理线上支付
|
||
/// </summary>
|
||
private async Task<UnifiedPaymentOutput> HandleOnlinePaymentAsync(EventRegistration registration,
|
||
EventInfo eventInfo, decimal finalAmount, UnifiedPaymentInput input)
|
||
{
|
||
var payConfig = await GetWeChatPayConfigAsync(eventInfo);
|
||
if (payConfig == null)
|
||
{
|
||
throw Oops.Bah("商户微信支付配置不存在");
|
||
}
|
||
|
||
var output = new UnifiedPaymentOutput
|
||
{
|
||
RegistrationId = registration.Id,
|
||
OrderNo = registration.OrderNo,
|
||
PaymentStatus = 0,
|
||
IsFree = false
|
||
};
|
||
|
||
return !string.IsNullOrEmpty(input.OpenId)
|
||
? await HandleJsApiPaymentAsync(registration, eventInfo, finalAmount, input, payConfig, output)
|
||
: await HandleNativePaymentAsync(registration, eventInfo, finalAmount, payConfig, output);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取微信支付配置
|
||
/// </summary>
|
||
private async Task<WeChatPayConfig?> GetWeChatPayConfigAsync(EventInfo eventInfo)
|
||
{
|
||
if (eventInfo.MerchantId.HasValue)
|
||
{
|
||
return await _weChatPayConfigRepository.GetFirstAsync(x =>
|
||
x.MerchantId == eventInfo.MerchantId.Value && x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理 JSAPI 支付
|
||
/// </summary>
|
||
private async Task<UnifiedPaymentOutput> HandleJsApiPaymentAsync(
|
||
EventRegistration registration, EventInfo eventInfo, decimal finalAmount,
|
||
UnifiedPaymentInput input, WeChatPayConfig payConfig, UnifiedPaymentOutput output)
|
||
{
|
||
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
|
||
|
||
// 1. 先查询微信侧是否已有预支付单,避免重复下单导致参数不一致
|
||
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, registration.OrderNo);
|
||
if (queryResult.Success)
|
||
{
|
||
_logger.LogInformation("查询到微信侧已有预支付单,订单号:{OrderNo},交易状态:{TradeState}",
|
||
registration.OrderNo, queryResult.TradeState);
|
||
|
||
// 如果订单已经成功支付,直接返回
|
||
if (queryResult.TradeState == "SUCCESS")
|
||
{
|
||
_logger.LogInformation("订单已在微信侧支付成功,更新本地状态,订单号:{OrderNo}", registration.OrderNo);
|
||
await HandlePaymentSuccessAsync(
|
||
registration.OrderNo,
|
||
queryResult.TransactionId,
|
||
queryResult.SuccessTime?.DateTime,
|
||
input.OperatorUserId);
|
||
output.PaymentStatus = 1;
|
||
return output;
|
||
}
|
||
|
||
// 如果订单已存在但未支付,我们需要确认是否可以重试
|
||
// 微信支付允许使用相同订单号重试,但参数必须完全一致
|
||
// 为了确保参数一致性,我们使用查询到的信息来验证
|
||
if (queryResult.TradeState == "NOTPAY" || queryResult.TradeState == "CLOSED")
|
||
{
|
||
_logger.LogInformation("订单状态为 {TradeState},将重新调用统一下单,订单号:{OrderNo}",
|
||
queryResult.TradeState, registration.OrderNo);
|
||
}
|
||
}
|
||
|
||
// 2. 构建统一下单请求
|
||
var jsapiRequest = new CreateJsApiPayRequest
|
||
{
|
||
OutTradeNo = registration.OrderNo,
|
||
AppId = configOptions.AppId,
|
||
Description = $"{eventInfo.EventName} - 报名费",
|
||
NotifyUrl = configOptions.NotifyUrl,
|
||
TotalAmount = AmountHelper.YuanToFen(finalAmount),
|
||
// 固定过期时间计算方式,确保重试时参数一致
|
||
// 从订单创建时间开始计算30分钟,而不是当前时间
|
||
ExpireTime = registration.CreatedTime.AddMinutes(30),
|
||
OpenId = input.OpenId
|
||
};
|
||
|
||
// 3. 调用微信统一下单
|
||
var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest);
|
||
if (!payResult.Success || payResult.JsApiParameters == null)
|
||
{
|
||
_logger.LogError("微信JSAPI支付下单失败,订单号:{OrderNo},错误:{Error}", registration.OrderNo, payResult.Message);
|
||
throw Oops.Bah($"微信统一下单失败:{payResult.Message}");
|
||
}
|
||
|
||
output.WechatPayParams = new
|
||
{
|
||
appId = payResult.JsApiParameters.AppId,
|
||
timeStamp = payResult.JsApiParameters.TimeStamp,
|
||
nonceStr = payResult.JsApiParameters.NonceStr,
|
||
package = payResult.JsApiParameters.Package,
|
||
signType = payResult.JsApiParameters.SignType,
|
||
paySign = payResult.JsApiParameters.PaySign
|
||
};
|
||
output.TradeType = payResult.TradeType;
|
||
_logger.LogInformation("微信JSAPI支付下单成功,订单号:{OrderNo}", registration.OrderNo);
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理 Native 支付
|
||
/// </summary>
|
||
private async Task<UnifiedPaymentOutput> HandleNativePaymentAsync(
|
||
EventRegistration registration, EventInfo eventInfo, decimal finalAmount,
|
||
WeChatPayConfig payConfig, UnifiedPaymentOutput output)
|
||
{
|
||
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
|
||
|
||
// 1. 先查询微信侧是否已有预支付单,避免重复下单导致参数不一致
|
||
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, registration.OrderNo);
|
||
if (queryResult.Success)
|
||
{
|
||
_logger.LogInformation("查询到微信侧已有预支付单,订单号:{OrderNo},交易状态:{TradeState}",
|
||
registration.OrderNo, queryResult.TradeState);
|
||
|
||
// 如果订单已经成功支付,直接返回
|
||
if (queryResult.TradeState == "SUCCESS")
|
||
{
|
||
_logger.LogInformation("订单已在微信侧支付成功,更新本地状态,订单号:{OrderNo}", registration.OrderNo);
|
||
await HandlePaymentSuccessAsync(
|
||
registration.OrderNo,
|
||
queryResult.TransactionId,
|
||
queryResult.SuccessTime?.DateTime,
|
||
registration.CreatedUserId);
|
||
output.PaymentStatus = 1;
|
||
return output;
|
||
}
|
||
|
||
// 如果订单已存在但未支付,我们需要确认是否可以重试
|
||
if (queryResult.TradeState == "NOTPAY" || queryResult.TradeState == "CLOSED")
|
||
{
|
||
_logger.LogInformation("订单状态为 {TradeState},将重新调用统一下单,订单号:{OrderNo}",
|
||
queryResult.TradeState, registration.OrderNo);
|
||
}
|
||
}
|
||
|
||
// 2. 构建统一下单请求
|
||
var nativeRequest = new CreateNativePayRequest
|
||
{
|
||
OutTradeNo = registration.OrderNo,
|
||
AppId = configOptions.AppId,
|
||
Description = $"{eventInfo.EventName} - 报名费",
|
||
NotifyUrl = configOptions.NotifyUrl,
|
||
TotalAmount = AmountHelper.YuanToFen(finalAmount),
|
||
// 固定过期时间计算方式,确保重试时参数一致
|
||
ExpireTime = registration.CreatedTime.AddMinutes(5)
|
||
};
|
||
|
||
// 3. 调用微信统一下单
|
||
var payResult = await _weChatPayService.CreateNativePayAsync(payConfig, nativeRequest);
|
||
if (!payResult.Success)
|
||
{
|
||
_logger.LogError("微信Native支付下单失败,订单号:{OrderNo},错误:{Error}", registration.OrderNo, payResult.Message);
|
||
throw Oops.Bah($"微信统一下单失败:{payResult.Message}");
|
||
}
|
||
|
||
output.QrcodeUrl = payResult.QrcodeUrl;
|
||
output.TradeType = payResult.TradeType;
|
||
_logger.LogInformation("微信Native支付下单成功,订单号:{OrderNo}", registration.OrderNo);
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理线下支付
|
||
/// </summary>
|
||
private async Task<UnifiedPaymentOutput> HandleOfflinePaymentAsync(EventRegistration registration, UnifiedPaymentInput input)
|
||
{
|
||
// 如果是银行转账,保持状态为5(待支付确认),等用户确认转账后再处理
|
||
if (input.PaymentMethodType == PaymentMethodType.BankTransfer)
|
||
{
|
||
registration.PaymentProofUrl = input.PaymentProofUrl;
|
||
// 保持支付状态为5(待支付确认),不占用正式名额
|
||
registration.PaymentStatus = 5;
|
||
registration.UpdatedTime = DateTime.Now;
|
||
registration.UpdatedUserId = input.OperatorUserId;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
|
||
_logger.LogInformation("银行转账方式,等待用户确认转账,报名ID:{RegistrationId},凭证URL:{ProofUrl}",
|
||
registration.Id, input.PaymentProofUrl);
|
||
|
||
return new UnifiedPaymentOutput
|
||
{
|
||
RegistrationId = registration.Id,
|
||
OrderNo = registration.OrderNo,
|
||
PaymentStatus = 5, // 待支付确认
|
||
IsFree = false
|
||
};
|
||
}
|
||
|
||
// 其他线下支付方式,立即升级为正式报名(状态0)
|
||
registration.PaymentProofUrl = input.PaymentProofUrl;
|
||
registration.PaymentStatus = 0; // 待审核
|
||
registration.UpdatedTime = DateTime.Now;
|
||
registration.UpdatedUserId = input.OperatorUserId;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
|
||
_logger.LogInformation("线下支付提交审核,报名ID:{RegistrationId},凭证URL:{ProofUrl}",
|
||
registration.Id, input.PaymentProofUrl);
|
||
|
||
return new UnifiedPaymentOutput
|
||
{
|
||
RegistrationId = registration.Id,
|
||
OrderNo = registration.OrderNo,
|
||
PaymentStatus = 0, // 待审核
|
||
IsFree = false
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确认银行转账
|
||
/// </summary>
|
||
public async Task<ConfirmBankTransferOutput> ConfirmBankTransferAsync(ConfirmBankTransferInput input, long operatorUserId)
|
||
{
|
||
_logger.LogInformation("开始确认银行转账,报名ID:{RegistrationId}", input.RegistrationId);
|
||
|
||
var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId);
|
||
if (registration == null)
|
||
{
|
||
_logger.LogWarning("未找到报名记录,报名ID:{RegistrationId}", input.RegistrationId);
|
||
return new ConfirmBankTransferOutput
|
||
{
|
||
RegistrationId = input.RegistrationId,
|
||
PaymentStatus = -1,
|
||
Success = false,
|
||
Message = "报名记录不存在"
|
||
};
|
||
}
|
||
|
||
// 检查是否为银行转账方式
|
||
if (registration.PaymentMethodType != PaymentMethodType.BankTransfer)
|
||
{
|
||
_logger.LogWarning("该报名不是银行转账方式,报名ID:{RegistrationId},支付方式:{PaymentMethodType}",
|
||
input.RegistrationId, registration.PaymentMethodType);
|
||
return new ConfirmBankTransferOutput
|
||
{
|
||
RegistrationId = input.RegistrationId,
|
||
PaymentStatus = registration.PaymentStatus,
|
||
Success = false,
|
||
Message = "该报名不是银行转账方式"
|
||
};
|
||
}
|
||
|
||
// 重新验证报名限制,防止在等待转账期间名额已满
|
||
var unifiedInput = new UnifiedRegistrationInput
|
||
{
|
||
EventId = registration.EventId,
|
||
RegistrationMode = registration.RegistrationMode,
|
||
CreatedUserId = registration.CreatedUserId
|
||
};
|
||
|
||
// 获取报名明细来重建完整的输入
|
||
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registration.Id);
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id);
|
||
|
||
unifiedInput.Items = items.Select(item =>
|
||
{
|
||
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList();
|
||
return new UnifiedRegistrationItemInput
|
||
{
|
||
ProjectId = item.ProjectId,
|
||
GroupId = item.EventProjectGroupId,
|
||
Persons = itemPersons.Select(p => new UnifiedPersonInput
|
||
{
|
||
UserId = p.UserId,
|
||
Name = p.Name,
|
||
Gender = p.Gender,
|
||
IdCardNumber = p.IdCard,
|
||
BirthDate = p.BirthDate,
|
||
Mobile = p.Phone
|
||
}).ToList()
|
||
};
|
||
}).ToList();
|
||
|
||
var limitCheck = await CheckRegistrationLimitAsync(unifiedInput, registration.Id);
|
||
if (limitCheck.HasLimit)
|
||
{
|
||
_logger.LogWarning("银行转账确认时发现报名限制,报名ID:{RegistrationId},原因:{Reason}",
|
||
input.RegistrationId, limitCheck.LimitDetails.FirstOrDefault()?.LimitMessage);
|
||
|
||
// 删除预报名记录,释放名额
|
||
await DeleteRegistrationAsync(registration);
|
||
|
||
return new ConfirmBankTransferOutput
|
||
{
|
||
RegistrationId = input.RegistrationId,
|
||
PaymentStatus = -2,
|
||
Success = false,
|
||
Message = limitCheck.LimitDetails.FirstOrDefault()?.LimitMessage ?? "报名名额已满,请重新报名"
|
||
};
|
||
}
|
||
|
||
// 更新报名记录状态为待审核 - 正式创建报名记录
|
||
registration.PaymentProofUrl = input.PaymentProofUrl ?? registration.PaymentProofUrl;
|
||
registration.PaymentStatus = 0; // 待审核 - 正式报名记录
|
||
registration.UpdatedTime = DateTime.Now;
|
||
registration.UpdatedUserId = operatorUserId;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
|
||
_logger.LogInformation("银行转账确认成功,报名ID:{RegistrationId},正式创建报名记录,状态已更新为待审核", input.RegistrationId);
|
||
|
||
return new ConfirmBankTransferOutput
|
||
{
|
||
RegistrationId = input.RegistrationId,
|
||
PaymentStatus = 0,
|
||
Success = true,
|
||
Message = "转账确认成功,订单已提交审核"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除报名记录(用于删除预报名记录)
|
||
/// </summary>
|
||
private async Task DeleteRegistrationAsync(EventRegistration registration)
|
||
{
|
||
// 删除报名明细
|
||
await _registrationItemRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
|
||
|
||
// 删除报名人员
|
||
await _registrationPersonRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
|
||
|
||
// 删除报名记录
|
||
await _registrationRepository.DeleteAsync(registration);
|
||
|
||
_logger.LogInformation("已删除预报名记录,报名ID:{RegistrationId}", registration.Id);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查报名限制
|
||
/// </summary>
|
||
public async Task<RegistrationLimitCheckOutput> CheckRegistrationLimitAsync(UnifiedRegistrationInput input, long? excludeRegistrationId = null)
|
||
{
|
||
var result = new RegistrationLimitCheckOutput
|
||
{
|
||
HasLimit = false,
|
||
LimitDetails = new List<RegistrationLimitDetail>()
|
||
};
|
||
|
||
_logger.LogInformation("开始检查报名限制,赛事ID: {EventId}", input.EventId);
|
||
|
||
var limit = await _limitRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
||
var projectLimits = await _projectLimitRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
||
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId);
|
||
var projects = await _projectRepository.GetListAsync(x => eventProjects.Select(ep => ep.ProjectId).Contains(x.Id) && !x.DeleteMark);
|
||
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
||
|
||
// 先获取所有报名信息,供后续检查使用
|
||
// 线上支付类型的报名记录,仅统计PaymentStatus == 1(已支付)的记录
|
||
// 线下支付类型的报名记录:
|
||
// - 银行转账方式:仅统计PaymentStatus == 1(已支付)和PaymentStatus == 0(待审核)的记录,
|
||
// 不统计PaymentStatus == 4(待确认转账)和PaymentStatus == 5(待支付确认)的记录
|
||
// - 其他线下方式:统计PaymentStatus == 1(已支付)和PaymentStatus == 0(待审核)的记录
|
||
var eventRegistrationsQuery = _registrationRepository.AsQueryable().Where(x =>
|
||
x.EventId == input.EventId && !x.DeleteMark &&
|
||
(
|
||
// 线上支付:仅已支付
|
||
(x.PaymentMethod == 2 && x.PaymentStatus == 1) ||
|
||
// 线下支付:
|
||
(x.PaymentMethod == 1 && (
|
||
// 已支付的都统计
|
||
x.PaymentStatus == 1 ||
|
||
// 待审核的都统计
|
||
x.PaymentStatus == 0
|
||
))
|
||
) &&
|
||
// 排除待支付确认的记录(状态5)、待确认转账的记录(状态4)、支付成功但名额已满的记录(状态6)
|
||
x.PaymentStatus != 4 && x.PaymentStatus != 5 && x.PaymentStatus != 6
|
||
);
|
||
|
||
// 如果指定了要排除的报名ID,则排除该记录
|
||
if (excludeRegistrationId.HasValue)
|
||
{
|
||
eventRegistrationsQuery = eventRegistrationsQuery.Where(x => x.Id != excludeRegistrationId.Value);
|
||
}
|
||
|
||
var eventRegistrations = await eventRegistrationsQuery.ToListAsync();
|
||
var eventRegistrationIds = eventRegistrations.Select(r => r.Id).ToList();
|
||
var registrationPersons = await _registrationPersonRepository.GetListAsync(x =>
|
||
eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark);
|
||
var allRegistrationItems = await _registrationItemRepository.GetListAsync(x =>
|
||
eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark);
|
||
|
||
_logger.LogInformation("获取到赛事报名记录: {RegistrationCount},报名人员: {PersonCount},报名项目: {ItemCount}",
|
||
eventRegistrations.Count, registrationPersons.Count, allRegistrationItems.Count);
|
||
|
||
// ==================== 报名限制检查流程 ====================
|
||
// 检查顺序说明:
|
||
// 1. 先检查团队组别容量限制 - 检查是否超过组别限报的团队数量
|
||
// 2. 再检查成员项目限报 - 检查队员是否超过个人/团队项目的限报数量
|
||
// 3. 然后检查人员项目组别重复 - 检查同一人员是否在同一项目的多个组别中重复报名
|
||
// 4. 再检查人员是否已报名 - 检查人员是否已在其他报名记录中报过同一项目组别
|
||
// 5. 再检查团队成员冲突 - 检查同一人员是否在同一项目的多个团队中
|
||
// 6. 最后检查项目完成人数 - 确保人数与项目的完成人数完全一致
|
||
|
||
// 检查1:团队组别容量限制
|
||
// 验证是否有组别超过了限报的团队数量(例如:某组别限报2队,但本次报了3队)
|
||
CheckTeamGroupCapacityLimitAsync(input, result, projectLimits, eventProjects, projects, eventProjectGroups);
|
||
|
||
// 检查2:成员项目限报
|
||
// 验证队员是否超过了个人项目或团队项目的限报数量(例如:某队员已报2个个人项目,限报2个,又报第3个)
|
||
await CheckMemberProjectLimitAsync(input, result, limit, eventProjects, projects, registrationPersons, allRegistrationItems);
|
||
|
||
// 检查3:人员项目组别重复
|
||
// 验证同一人员是否在本次报名中重复选择了同一项目的多个组别(例如:同一队员在同一项目的男子组和女子组都报了名)
|
||
CheckPersonProjectDuplicateAsync(input, result, eventProjects, projects, eventProjectGroups);
|
||
|
||
// 检查4:人员是否已在其他报名中报名
|
||
// 验证人员是否已在其他已支付的报名记录中报过同一项目组别(避免重复占用名额)
|
||
CheckPersonAlreadyRegisteredAsync(input, result, eventProjects, projects, eventProjectGroups, registrationPersons, allRegistrationItems, eventRegistrationIds);
|
||
|
||
// 检查5:团队成员冲突检测
|
||
// 验证同一人员是否在同一项目的多个团队中(例如:同一队员既在1队又在2队)
|
||
CheckTeamMemberConflictAsync(input, result, eventProjects, projects, eventProjectGroups);
|
||
|
||
// 检查6:项目完成人数验证
|
||
// 验证用户选择的参与人数必须与项目的"完成人数"完全一致(例如:项目要求完成3人,但只选了2人或选了4人)
|
||
CheckProjectCompletionAsync(input, result, eventProjects, projects, eventProjectGroups);
|
||
|
||
_logger.LogInformation("报名限制检查完成,是否存在限制: {HasLimit}, 限制详情数: {LimitCount}",
|
||
result.HasLimit, result.LimitDetails.Count);
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建报名限制详情
|
||
/// </summary>
|
||
private static RegistrationLimitDetail CreateLimitDetail(long memberId, string memberName, string limitType, string limitMessage)
|
||
{
|
||
return new RegistrationLimitDetail
|
||
{
|
||
MemberId = memberId,
|
||
MemberName = memberName,
|
||
LimitType = limitType,
|
||
LimitMessage = limitMessage
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建验证详情
|
||
/// </summary>
|
||
private static ValidationDetail CreateValidationDetail(string itemName, bool isPassed, string message)
|
||
{
|
||
return new ValidationDetail
|
||
{
|
||
ItemName = itemName,
|
||
IsPassed = isPassed,
|
||
Message = message
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查团队组别容量限制(项目组别限报设置)
|
||
/// </summary>
|
||
private void CheckTeamGroupCapacityLimitAsync(
|
||
UnifiedRegistrationInput input,
|
||
RegistrationLimitCheckOutput result,
|
||
List<EventProjectRegistrationLimit> projectLimits,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventProjectGroup> eventProjectGroups)
|
||
{
|
||
foreach (var item in input.Items)
|
||
{
|
||
if (!item.GroupId.HasValue) continue;
|
||
|
||
// 获取组别信息
|
||
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
||
if (eventProjectGroup == null) continue;
|
||
|
||
// 获取赛事项目信息
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
// 获取基础项目信息
|
||
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
if (project == null) continue;
|
||
|
||
// 判断是否为团队项目
|
||
var isTeamProject = project.IsTeamProject();
|
||
|
||
// 获取项目限报配置
|
||
var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject.Id);
|
||
if (projectLimit == null) continue;
|
||
|
||
if (isTeamProject)
|
||
{
|
||
// ==================== 团队项目验证 ====================
|
||
// 检查该组别申报的团队数量是否超过限报配置
|
||
var teams = GetTeamsFromItem(item);
|
||
var submittedTeamCount = teams.Count;
|
||
|
||
if (submittedTeamCount > projectLimit.LimitCount)
|
||
{
|
||
_logger.LogWarning("团队项目组别容量超限,项目: {ProjectName},组别: {GroupName},限报: {LimitCount}队,提交: {SubmittedCount}队",
|
||
project.Name, eventProjectGroup.GroupName, projectLimit.LimitCount, submittedTeamCount);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
0,
|
||
eventProjectGroup.GroupName ?? "未知组别",
|
||
"ProjectGroupCapacity",
|
||
$"{project.Name}项目已达到报名限制"));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ==================== 个人项目验证 ====================
|
||
// 检查该组别申报的人员数量是否超过限报配置
|
||
var persons = GetAllPersonsFromItem(item);
|
||
var submittedPersonCount = persons.Count;
|
||
|
||
if (submittedPersonCount > projectLimit.LimitCount)
|
||
{
|
||
_logger.LogWarning("个人项目组别容量超限,项目: {ProjectName},组别: {GroupName},限报: {LimitCount}人,提交: {SubmittedCount}人",
|
||
project.Name, eventProjectGroup.GroupName, projectLimit.LimitCount, submittedPersonCount);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
0,
|
||
eventProjectGroup.GroupName ?? "未知组别",
|
||
"ProjectGroupCapacity",
|
||
$"{project.Name}项目已达到报名限制"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查成员项目限报
|
||
/// </summary>
|
||
private async Task CheckMemberProjectLimitAsync(
|
||
UnifiedRegistrationInput input,
|
||
RegistrationLimitCheckOutput result,
|
||
EventRegistrationLimit? limit,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventRegistrationPerson> registrationPersons,
|
||
List<EventRegistrationItem> allRegistrationItems)
|
||
{
|
||
var memberIdsList = new List<long>();
|
||
foreach (var item in input.Items)
|
||
{
|
||
var persons = GetAllPersonsFromItem(item);
|
||
memberIdsList.AddRange(persons.Where(p => p.UserId.HasValue).Select(p => p.UserId.Value));
|
||
}
|
||
var memberIds = memberIdsList.Distinct().ToList();
|
||
|
||
if (limit == null || !memberIds.Any())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var members = await _userRepository.GetListAsync(x => memberIds.Contains(x.Id) && !x.DeleteMark);
|
||
_logger.LogInformation("检查报名限制的成员数量: {MemberCount}", members.Count);
|
||
|
||
foreach (var member in members)
|
||
{
|
||
_logger.LogDebug("检查成员报名限制,成员ID: {MemberId}, 姓名: {MemberName}", member.Id, member.Name);
|
||
|
||
var (individualProjectCount, teamProjectCount) = CalculateMemberProjectCounts(
|
||
member.Id, eventProjects, projects, registrationPersons, allRegistrationItems, input);
|
||
|
||
_logger.LogDebug("成员 {MemberName} 个人项目报名数: {IndividualCount}, 团队项目报名数: {TeamCount}",
|
||
member.Name, individualProjectCount, teamProjectCount);
|
||
|
||
CheckIndividualProjectLimit(result, limit, member, individualProjectCount);
|
||
CheckTeamProjectLimit(result, limit, member, teamProjectCount);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算成员项目报名数量(包含本次提交的项目)
|
||
/// </summary>
|
||
private (int individualCount, int teamCount) CalculateMemberProjectCounts(
|
||
long memberId,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventRegistrationPerson> registrationPersons,
|
||
List<EventRegistrationItem> allRegistrationItems,
|
||
UnifiedRegistrationInput input)
|
||
{
|
||
var matchedPersons = registrationPersons.Where(x => x.UserId == memberId).ToList();
|
||
var personRegistrationIds = matchedPersons.Select(x => x.RegistrationId).Distinct().ToList();
|
||
var personRegistrationItems = allRegistrationItems.Where(ri => personRegistrationIds.Contains(ri.RegistrationId)).ToList();
|
||
|
||
int individualProjectCount = 0;
|
||
int teamProjectCount = 0;
|
||
|
||
// 统计已存在的报名项目
|
||
foreach (var item in personRegistrationItems)
|
||
{
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
var baseProject = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
if (baseProject == null) continue;
|
||
|
||
if (baseProject.IsIndividualProject())
|
||
{
|
||
individualProjectCount++;
|
||
}
|
||
else if (baseProject.IsTeamProject())
|
||
{
|
||
teamProjectCount++;
|
||
}
|
||
}
|
||
|
||
// 统计本次提交的项目,确保不重复统计
|
||
var existingProjectIds = new HashSet<long>(personRegistrationItems.Select(x => x.ProjectId));
|
||
|
||
foreach (var item in input.Items)
|
||
{
|
||
// 检查该成员是否在本次提交的项目中
|
||
var allPersonsInItem = GetAllPersonsFromItem(item);
|
||
if (!allPersonsInItem.Any(p => p.UserId.HasValue && p.UserId.Value == memberId))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 检查是否已经统计过该项目
|
||
if (existingProjectIds.Contains(item.ProjectId))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
var baseProject = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
if (baseProject == null) continue;
|
||
|
||
if (baseProject.IsIndividualProject())
|
||
{
|
||
individualProjectCount++;
|
||
}
|
||
else if (baseProject.IsTeamProject())
|
||
{
|
||
teamProjectCount++;
|
||
}
|
||
|
||
// 标记该项目已统计
|
||
existingProjectIds.Add(item.ProjectId);
|
||
}
|
||
|
||
return (individualProjectCount, teamProjectCount);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查个人项目限报
|
||
/// </summary>
|
||
private void CheckIndividualProjectLimit(
|
||
RegistrationLimitCheckOutput result,
|
||
EventRegistrationLimit limit,
|
||
YD_Event.Core.Entities.AppUser member,
|
||
int individualProjectCount)
|
||
{
|
||
if (individualProjectCount <= limit.IndividualProjectLimit)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_logger.LogWarning("个人项目限报超限,成员: {MemberName}, 已报名: {Count}, 限制: {Limit}",
|
||
member.Name, individualProjectCount, limit.IndividualProjectLimit);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
member.Id,
|
||
member.Name ?? member.NickName ?? "未知",
|
||
"IndividualProjectLimit",
|
||
$"{member.Name}队员已达到报名限制"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查团队项目限报
|
||
/// </summary>
|
||
private void CheckTeamProjectLimit(
|
||
RegistrationLimitCheckOutput result,
|
||
EventRegistrationLimit limit,
|
||
YD_Event.Core.Entities.AppUser member,
|
||
int teamProjectCount)
|
||
{
|
||
if (teamProjectCount <= limit.TeamProjectLimit)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_logger.LogWarning("团队项目限报超限,成员: {MemberName}, 已报名: {Count}, 限制: {Limit}",
|
||
member.Name, teamProjectCount, limit.TeamProjectLimit);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
member.Id,
|
||
member.Name ?? member.NickName ?? "未知",
|
||
"TeamProjectLimit",
|
||
$"{member.Name}队员已达到报名限制"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查人员项目组别重复
|
||
/// </summary>
|
||
private void CheckPersonProjectDuplicateAsync(
|
||
UnifiedRegistrationInput input,
|
||
RegistrationLimitCheckOutput result,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventProjectGroup> eventProjectGroups)
|
||
{
|
||
var personProjectGroupMap = new Dictionary<string, List<(long GroupId, string GroupName, string ProjectName)>>();
|
||
|
||
foreach (var item in input.Items)
|
||
{
|
||
if (!item.GroupId.HasValue) continue;
|
||
|
||
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
||
if (eventProjectGroup == null) continue;
|
||
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
var projectName = project?.Name ?? "未知项目";
|
||
|
||
var allPersonsInItem = GetAllPersonsFromItem(item);
|
||
|
||
foreach (var person in allPersonsInItem)
|
||
{
|
||
var key = person.UserId.HasValue ? person.UserId.Value.ToString() : person.IdCardNumber;
|
||
if (string.IsNullOrEmpty(key)) continue;
|
||
|
||
if (!personProjectGroupMap.ContainsKey(key))
|
||
{
|
||
personProjectGroupMap[key] = new List<(long GroupId, string GroupName, string ProjectName)>();
|
||
}
|
||
|
||
personProjectGroupMap[key].Add((item.GroupId.Value, eventProjectGroup.GroupName ?? "未知组别", projectName));
|
||
}
|
||
}
|
||
|
||
foreach (var kvp in personProjectGroupMap)
|
||
{
|
||
var key = kvp.Key;
|
||
var groupInfos = kvp.Value;
|
||
|
||
var groupDuplicates = groupInfos
|
||
.GroupBy(g => g.GroupId)
|
||
.Where(g => g.Count() > 1)
|
||
.ToList();
|
||
|
||
if (!groupDuplicates.Any()) continue;
|
||
|
||
foreach (var duplicate in groupDuplicates)
|
||
{
|
||
var groupInfo = duplicate.First();
|
||
var person = input.Items.SelectMany(i => i.Persons).FirstOrDefault(p =>
|
||
(p.UserId.HasValue && p.UserId.Value.ToString() == key) ||
|
||
(!p.UserId.HasValue && p.IdCardNumber == key));
|
||
var personName = person?.Name ?? "未知人员";
|
||
|
||
_logger.LogWarning("人员项目组别重复,人员: {PersonName}, 项目: {ProjectName}, 组别: {GroupName}",
|
||
personName, groupInfo.ProjectName, groupInfo.GroupName);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
0,
|
||
personName,
|
||
"PersonProjectDuplicate",
|
||
$"人员项目重复检测:人员\"{personName}\"在项目\"{groupInfo.ProjectName}\"的组别\"{groupInfo.GroupName}\"中重复出现,同一人员只能在一个团队中报名同一项目组别"));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查人员是否已在其他报名中报过同一项目组别
|
||
/// </summary>
|
||
private void CheckPersonAlreadyRegisteredAsync(
|
||
UnifiedRegistrationInput input,
|
||
RegistrationLimitCheckOutput result,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventProjectGroup> eventProjectGroups,
|
||
List<EventRegistrationPerson> registrationPersons,
|
||
List<EventRegistrationItem> allRegistrationItems,
|
||
List<long> eventRegistrationIds)
|
||
{
|
||
if (!eventRegistrationIds.Any())
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var item in input.Items)
|
||
{
|
||
if (!item.GroupId.HasValue) continue;
|
||
|
||
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
||
if (eventProjectGroup == null) continue;
|
||
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
var projectName = project?.Name ?? "未知项目";
|
||
|
||
var existingItems = allRegistrationItems.Where(ri => ri.EventProjectGroupId == item.GroupId.Value).ToList();
|
||
if (!existingItems.Any()) continue;
|
||
|
||
var existingItemIds = existingItems.Select(ri => ri.Id).ToList();
|
||
var existingPersons = registrationPersons.Where(rp => existingItemIds.Contains(rp.RegistrationItemId)).ToList();
|
||
|
||
var allPersonsInItem = GetAllPersonsFromItem(item);
|
||
|
||
foreach (var person in allPersonsInItem)
|
||
{
|
||
if (!person.UserId.HasValue) continue;
|
||
|
||
var existingPerson = existingPersons.FirstOrDefault(ep => ep.UserId == person.UserId.Value);
|
||
if (existingPerson == null) continue;
|
||
|
||
_logger.LogWarning("人员已报名该项目组别,人员: {PersonName}, 项目: {ProjectName}, 组别: {GroupName}",
|
||
person.Name, projectName, eventProjectGroup.GroupName);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
0,
|
||
person.Name,
|
||
"PersonAlreadyRegistered",
|
||
$"人员已报名检测:人员\"{person.Name}\"已在项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"中报名过,不能重复报名,避免占用多个名额"));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查团队成员冲突
|
||
/// 验证同一人员是否在同一项目的多个团队中(例如:同一队员既在1队又在2队)
|
||
/// </summary>
|
||
private void CheckTeamMemberConflictAsync(
|
||
UnifiedRegistrationInput input,
|
||
RegistrationLimitCheckOutput result,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventProjectGroup> eventProjectGroups)
|
||
{
|
||
foreach (var item in input.Items)
|
||
{
|
||
if (!item.GroupId.HasValue) continue;
|
||
|
||
// 获取组别信息
|
||
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
||
if (eventProjectGroup == null) continue;
|
||
|
||
// 获取赛事项目信息
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
// 获取基础项目信息
|
||
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
if (project == null) continue;
|
||
|
||
// 判断是否为团队项目
|
||
var isTeamProject = project.IsTeamProject();
|
||
if (!isTeamProject) continue;
|
||
|
||
// 获取所有团队
|
||
var teams = GetTeamsFromItem(item);
|
||
if (teams.Count <= 1) continue;
|
||
|
||
// 建立用户到团队的映射,用于检测冲突
|
||
var userTeamMap = new Dictionary<long, List<(int TeamIndex, string MemberName)>>();
|
||
|
||
foreach (var team in teams)
|
||
{
|
||
if (team.Persons == null || !team.Persons.Any()) continue;
|
||
|
||
foreach (var person in team.Persons)
|
||
{
|
||
if (!person.UserId.HasValue) continue;
|
||
|
||
var userId = person.UserId.Value;
|
||
var memberName = person.Name ?? "未知人员";
|
||
|
||
if (!userTeamMap.TryGetValue(userId, out var teamsList))
|
||
{
|
||
teamsList = new List<(int, string)>();
|
||
userTeamMap[userId] = teamsList;
|
||
}
|
||
|
||
teamsList.Add((team.TeamIndex, memberName));
|
||
}
|
||
}
|
||
|
||
// 检测冲突:同一用户在多个团队中
|
||
foreach (var kvp in userTeamMap)
|
||
{
|
||
if (kvp.Value.Count > 1)
|
||
{
|
||
var memberName = kvp.Value.First().MemberName;
|
||
var firstTeamIndex = kvp.Value.First().TeamIndex;
|
||
var projectName = project.Name ?? "未知项目";
|
||
|
||
// 提示信息格式:"XXX已在xx项目x队"
|
||
var errorMessage = $"{memberName}已在{projectName}{GetTeamNumber(firstTeamIndex)}";
|
||
|
||
_logger.LogWarning("团队成员冲突检测:人员{MemberName}已在项目{ProjectName}的多个团队中",
|
||
memberName, projectName);
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
kvp.Key,
|
||
memberName,
|
||
"TeamMemberConflict",
|
||
errorMessage));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取团队序号的中文表示
|
||
/// </summary>
|
||
private static string GetTeamNumber(int teamIndex)
|
||
{
|
||
return teamIndex switch
|
||
{
|
||
1 => "1队",
|
||
2 => "2队",
|
||
3 => "3队",
|
||
4 => "4队",
|
||
5 => "5队",
|
||
6 => "6队",
|
||
7 => "7队",
|
||
8 => "8队",
|
||
9 => "9队",
|
||
10 => "10队",
|
||
_ => $"{teamIndex}队"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查项目完成人数
|
||
/// </summary>
|
||
private void CheckProjectCompletionAsync(
|
||
UnifiedRegistrationInput input,
|
||
RegistrationLimitCheckOutput result,
|
||
List<EventProject> eventProjects,
|
||
List<YD_Event.Core.Entities.Project> projects,
|
||
List<EventProjectGroup> eventProjectGroups)
|
||
{
|
||
foreach (var item in input.Items)
|
||
{
|
||
if (!item.GroupId.HasValue) continue;
|
||
|
||
// 获取组别信息
|
||
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
||
if (eventProjectGroup == null) continue;
|
||
|
||
// 获取赛事项目信息
|
||
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
||
if (eventProject == null) continue;
|
||
|
||
// 获取基础项目信息
|
||
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
||
if (project == null) continue;
|
||
|
||
// 判断是否为团队项目
|
||
var isTeamProject = project.IsTeamProject();
|
||
|
||
// ==================== 项目组别条件判断逻辑 ====================
|
||
// 当且仅当某个项目组别中已选择至少一名人员时,才对该项目组别执行后续条件判断流程
|
||
// 个人项目不进行完成人数限制,仅适用项目报名限制规则
|
||
if (!isTeamProject)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// ==================== 团队项目完成人数验证 ====================
|
||
if (project.CompletionCount <= 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var teams = GetTeamsFromItem(item);
|
||
if (teams.Count > 0)
|
||
{
|
||
// ========== 多团队模式 ==========
|
||
// 对每个团队单独验证人数是否等于 CompletionCount
|
||
foreach (var team in teams)
|
||
{
|
||
var teamMemberCount = team.Persons?.Count ?? 0;
|
||
if (teamMemberCount != project.CompletionCount)
|
||
{
|
||
string errorMessage;
|
||
if (teamMemberCount < project.CompletionCount)
|
||
{
|
||
errorMessage = $"{project.Name}{GetTeamNumber(team.TeamIndex)}参赛人数错误(少{project.CompletionCount - teamMemberCount}人)";
|
||
}
|
||
else
|
||
{
|
||
errorMessage = $"{project.Name}{GetTeamNumber(team.TeamIndex)}参赛人数错误(多{teamMemberCount - project.CompletionCount}人)";
|
||
}
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
0,
|
||
eventProjectGroup.GroupName ?? "未知组别",
|
||
"ProjectCompletion",
|
||
errorMessage));
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ========== 单团队模式(向后兼容) ==========
|
||
var assignedPersons = GetAllPersonsFromItem(item);
|
||
var totalAssignedMembers = assignedPersons
|
||
.Where(p => p.UserId.HasValue)
|
||
.Select(p => p.UserId.Value)
|
||
.Distinct()
|
||
.Count();
|
||
|
||
if (totalAssignedMembers != project.CompletionCount)
|
||
{
|
||
string errorMessage;
|
||
if (totalAssignedMembers < project.CompletionCount)
|
||
{
|
||
errorMessage = $"{project.Name}参赛人数错误(少{project.CompletionCount - totalAssignedMembers}人)";
|
||
}
|
||
else
|
||
{
|
||
errorMessage = $"{project.Name}参赛人数错误(多{totalAssignedMembers - project.CompletionCount}人)";
|
||
}
|
||
|
||
result.HasLimit = true;
|
||
result.LimitDetails.Add(CreateLimitDetail(
|
||
0,
|
||
eventProjectGroup.GroupName ?? "未知组别",
|
||
"ProjectCompletion",
|
||
errorMessage));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取组别名称
|
||
/// </summary>
|
||
private static string GetGroupName(UnifiedRegistrationItemInput item, List<EventProjectGroup> eventProjectGroups)
|
||
{
|
||
if (!string.IsNullOrEmpty(item.GroupName))
|
||
{
|
||
return item.GroupName;
|
||
}
|
||
|
||
if (item.GroupId.HasValue)
|
||
{
|
||
var selectedGroup = eventProjectGroups.FirstOrDefault(eg => eg.Id == item.GroupId.Value);
|
||
if (selectedGroup != null)
|
||
{
|
||
return selectedGroup.GroupName ?? "";
|
||
}
|
||
}
|
||
|
||
var firstGroup = eventProjectGroups.FirstOrDefault(eg => eg.EventProjectId == item.ProjectId);
|
||
return firstGroup?.GroupName ?? "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算单个项目费用
|
||
/// </summary>
|
||
private (decimal unitPrice, decimal totalPrice) CalculateItemPrice(EventFeeConfig? feeConfig, EventProject eventProject, int personCount)
|
||
{
|
||
if (feeConfig == null)
|
||
{
|
||
return (0, 0);
|
||
}
|
||
|
||
decimal unitPrice = feeConfig.ChargeMode switch
|
||
{
|
||
ChargeMode.Free => 0,
|
||
ChargeMode.FixedPrice or ChargeMode.UnitPriceEvent => feeConfig.Price,
|
||
ChargeMode.UnitPriceProject => eventProject.Price,
|
||
_ => 0
|
||
};
|
||
|
||
var totalPrice = unitPrice * personCount;
|
||
return (unitPrice, totalPrice);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算总费用
|
||
/// </summary>
|
||
private decimal CalculateTotalAmount(EventFeeConfig? feeConfig, List<EventProject> eventProjects,
|
||
List<UnifiedRegistrationItemInput> items, int totalPersonCount)
|
||
{
|
||
if (feeConfig == null)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
var pricingInput = new PricingInput
|
||
{
|
||
ChargeMode = feeConfig.ChargeMode,
|
||
ParticipantCount = totalPersonCount,
|
||
EventPrice = feeConfig.Price
|
||
};
|
||
|
||
var eventProjectsDict = eventProjects.ToDictionary(p => p.Id);
|
||
|
||
foreach (var item in items)
|
||
{
|
||
if (eventProjectsDict.TryGetValue(item.ProjectId, out var ep))
|
||
{
|
||
var projectInfo = new ProjectPricingInfo
|
||
{
|
||
ProjectId = ep.Id,
|
||
UnitPrice = ep.Price
|
||
};
|
||
|
||
// 仅在单价项目模式时设置项目人数
|
||
if (feeConfig.ChargeMode == ChargeMode.UnitPriceProject)
|
||
{
|
||
if (item.Teams != null && item.Teams.Any())
|
||
{
|
||
projectInfo.Quantity = item.Teams.Sum(t => t.Persons?.Count ?? 0);
|
||
}
|
||
else if (item.Persons != null && item.Persons.Any())
|
||
{
|
||
projectInfo.Quantity = item.Persons.Count;
|
||
}
|
||
}
|
||
|
||
pricingInput.Projects.Add(projectInfo);
|
||
}
|
||
}
|
||
|
||
var pricingResult = EventPricingService.Calculate(pricingInput);
|
||
return pricingResult.TotalAmount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算报名金额(对外公开方法)
|
||
/// </summary>
|
||
/// <param name="eventId">赛事ID</param>
|
||
/// <param name="items">报名项目列表(包含人员数量信息)</param>
|
||
/// <returns>计算结果</returns>
|
||
public async Task<(decimal totalAmount, ChargeMode chargeMode, List<(long projectId, string projectName, decimal unitPrice, int personCount, decimal subtotal)> itemDetails)> CalculateRegistrationAmountAsync(
|
||
long eventId, List<CalculateAmountInputItem> items)
|
||
{
|
||
var evt = await _eventRepository.GetFirstAsync(e => e.Id == eventId && !e.DeleteMark);
|
||
if (evt == null) throw Oops.Bah("赛事不存在");
|
||
|
||
var result = new List<(long projectId, string projectName, decimal unitPrice, int personCount, decimal subtotal)>();
|
||
|
||
if (evt.ChargeMode == ChargeMode.Free)
|
||
{
|
||
return (0, ChargeMode.Free, result);
|
||
}
|
||
|
||
var feeConfig = await _feeConfigRepository.GetFirstAsync(f => f.EventId == eventId && !f.DeleteMark);
|
||
var eventProjectIds = items.Select(i => i.ProjectId).Distinct().ToList();
|
||
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
|
||
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
|
||
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
|
||
|
||
// 根据收费模式决定使用哪种人数计算金额(和 CreateRegistrationDetailsAsync 保持一致)
|
||
int countForCalculation;
|
||
if (feeConfig != null && feeConfig.ChargeMode == ChargeMode.UnitPriceEvent)
|
||
{
|
||
// 单价(赛事)模式:预览时默认按1个人计算(因为是同一个用户报名多个项目)
|
||
countForCalculation = 1;
|
||
}
|
||
else
|
||
{
|
||
// 其他模式(包括单价项目):使用实际人数
|
||
countForCalculation = items.Sum(i => i.PersonCount);
|
||
}
|
||
|
||
// 构建专门用于金额计算的 PricingInput,直接使用 CalculateAmountInputItem 的信息
|
||
var pricingInput = new PricingInput
|
||
{
|
||
ChargeMode = feeConfig.ChargeMode,
|
||
ParticipantCount = countForCalculation,
|
||
EventPrice = feeConfig.Price
|
||
};
|
||
|
||
var eventProjectsDict = eventProjects.ToDictionary(p => p.Id);
|
||
|
||
foreach (var item in items)
|
||
{
|
||
if (eventProjectsDict.TryGetValue(item.ProjectId, out var ep))
|
||
{
|
||
var projectInfo = new ProjectPricingInfo
|
||
{
|
||
ProjectId = ep.Id,
|
||
UnitPrice = ep.Price
|
||
};
|
||
|
||
// 仅在单价项目模式时设置项目人数
|
||
if (feeConfig.ChargeMode == ChargeMode.UnitPriceProject)
|
||
{
|
||
projectInfo.Quantity = item.PersonCount;
|
||
}
|
||
|
||
pricingInput.Projects.Add(projectInfo);
|
||
}
|
||
}
|
||
|
||
var pricingResult = EventPricingService.Calculate(pricingInput);
|
||
var totalAmount = pricingResult.TotalAmount;
|
||
|
||
var baseProjectsDict = baseProjects.ToDictionary(p => p.Id);
|
||
|
||
foreach (var item in items)
|
||
{
|
||
if (!eventProjectsDict.TryGetValue(item.ProjectId, out var ep)) continue;
|
||
|
||
var projectName = baseProjectsDict.TryGetValue(ep.ProjectId, out var bp) ? bp.Name ?? "" : "";
|
||
var (unitPrice, subtotal) = CalculateItemPrice(feeConfig, ep, item.PersonCount);
|
||
result.Add((item.ProjectId, projectName, unitPrice, item.PersonCount, subtotal));
|
||
}
|
||
|
||
return (totalAmount, evt.ChargeMode, result);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算金额输入项(简化版)
|
||
/// </summary>
|
||
public class CalculateAmountInputItem
|
||
{
|
||
public long ProjectId { get; set; }
|
||
public int PersonCount { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据出生日期计算年龄
|
||
/// </summary>
|
||
private static int CalculateAge(DateTime birthDate)
|
||
{
|
||
var today = DateTime.Today;
|
||
var age = today.Year - birthDate.Year;
|
||
if (birthDate.Date > today.AddYears(-age)) age--;
|
||
return age;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理支付成功
|
||
/// </summary>
|
||
public async Task HandlePaymentSuccessAsync(string orderNo, string transactionId, DateTime? successTime, long operatorUserId)
|
||
{
|
||
var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == orderNo);
|
||
if (reg == null)
|
||
{
|
||
_logger.LogWarning($"未找到订单:{orderNo}");
|
||
return;
|
||
}
|
||
|
||
if (reg.PaymentStatus == 1)
|
||
{
|
||
_logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付");
|
||
return;
|
||
}
|
||
|
||
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
|
||
if (evt == null)
|
||
{
|
||
_logger.LogError($"未找到赛事信息,订单号:{reg.OrderNo},赛事ID:{reg.EventId}");
|
||
return;
|
||
}
|
||
|
||
// 如果当前状态是待支付确认,需要先重新验证报名限制
|
||
if (reg.PaymentStatus == 5)
|
||
{
|
||
// 重新验证报名限制,防止在支付期间名额已满
|
||
var unifiedInput = new UnifiedRegistrationInput
|
||
{
|
||
EventId = reg.EventId,
|
||
RegistrationMode = reg.RegistrationMode,
|
||
CreatedUserId = reg.CreatedUserId
|
||
};
|
||
|
||
// 获取报名明细来重建完整的输入
|
||
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == reg.Id);
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == reg.Id);
|
||
|
||
unifiedInput.Items = items.Select(item =>
|
||
{
|
||
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList();
|
||
return new UnifiedRegistrationItemInput
|
||
{
|
||
ProjectId = item.ProjectId,
|
||
GroupId = item.EventProjectGroupId,
|
||
Persons = itemPersons.Select(p => new UnifiedPersonInput
|
||
{
|
||
UserId = p.UserId,
|
||
Name = p.Name,
|
||
Gender = p.Gender,
|
||
IdCardNumber = p.IdCard,
|
||
BirthDate = p.BirthDate,
|
||
Mobile = p.Phone
|
||
}).ToList()
|
||
};
|
||
}).ToList();
|
||
|
||
var limitCheck = await CheckRegistrationLimitAsync(unifiedInput, reg.Id);
|
||
if (limitCheck.HasLimit)
|
||
{
|
||
_logger.LogWarning("微信支付成功时发现报名限制,报名ID:{RegistrationId},原因:{Reason}",
|
||
reg.Id, limitCheck.LimitDetails.FirstOrDefault()?.LimitMessage);
|
||
|
||
// 虽然支付成功了,但名额已满,这里我们需要记录日志,后续人工处理
|
||
// 不删除报名记录,保留支付信息,方便后续退款处理
|
||
reg.PaymentStatus = 6; // 支付成功但名额已满的状态
|
||
reg.RegistrationRemark = $"支付成功但名额已满: {limitCheck.LimitDetails.FirstOrDefault()?.LimitMessage}";
|
||
reg.UpdatedTime = DateTime.Now;
|
||
reg.UpdatedUserId = operatorUserId;
|
||
await _registrationRepository.UpdateAsync(reg);
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("订单支付成功,订单号={OrderNo},赛事ID={EventId},商户ID={MerchantId}",
|
||
reg.OrderNo, evt.Id, evt.MerchantId);
|
||
|
||
// 正式创建报名记录,更新状态为已支付
|
||
reg.PaymentStatus = 1;
|
||
reg.PaymentTime = successTime;
|
||
reg.RegistrationRemark = transactionId;
|
||
reg.UpdatedTime = DateTime.Now;
|
||
reg.UpdatedUserId = operatorUserId;
|
||
|
||
await _registrationRepository.UpdateAsync(reg);
|
||
_logger.LogInformation($"订单 {reg.OrderNo} 支付成功,正式创建报名记录,微信交易号:{transactionId}");
|
||
|
||
var payment = new EventRegistrationPayment
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
RegistrationId = reg.Id,
|
||
TransactionNo = transactionId ?? "",
|
||
PaymentChannel = 1,
|
||
PaymentAmount = reg.TotalAmount,
|
||
PaymentTime = successTime ?? DateTime.Now,
|
||
PaymentStatus = 1,
|
||
RefundStatus = 0,
|
||
PayeeAccount = "",
|
||
CreatedUserId = reg.CreatedUserId,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
await _paymentRepository.InsertAsync(payment);
|
||
_logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={transactionId}");
|
||
|
||
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
|
||
if (order != null)
|
||
{
|
||
order.OrderStatus = OrderStatus.Paid;
|
||
order.PaymentTime = successTime;
|
||
order.UpdatedTime = DateTime.Now;
|
||
await _orderRepository.UpdateAsync(order);
|
||
_logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付");
|
||
|
||
// 支付成功后,标记优惠券为已使用
|
||
if (order.CouponId.HasValue)
|
||
{
|
||
var userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
|
||
uc.CouponId == order.CouponId.Value &&
|
||
uc.UserId == order.CreatedUserId &&
|
||
uc.Status == 0 &&
|
||
!uc.DeleteMark);
|
||
|
||
if (userCoupon != null)
|
||
{
|
||
userCoupon.Status = 1;
|
||
userCoupon.UseTime = successTime ?? DateTime.Now;
|
||
userCoupon.OrderId = order.Id;
|
||
await _userCouponRepository.UpdateAsync(userCoupon);
|
||
|
||
// 更新优惠券的已使用数量
|
||
var coupon = await _couponRepository.GetByIdAsync(order.CouponId.Value);
|
||
if (coupon != null)
|
||
{
|
||
coupon.UsedQuantity++;
|
||
await _couponRepository.UpdateAsync(coupon);
|
||
}
|
||
|
||
_logger.LogInformation($"优惠券已标记为已使用,用户优惠券ID={userCoupon.Id},订单ID={order.Id}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消报名
|
||
/// </summary>
|
||
public async Task<bool> CancelRegistrationAsync(long registrationId, long operatorUserId)
|
||
{
|
||
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == operatorUserId);
|
||
if (reg == null) throw Oops.Bah("报名记录不存在");
|
||
|
||
if (reg.PaymentStatus == 1) throw Oops.Bah("已支付订单请联系客服退款");
|
||
if (reg.PaymentStatus == 4) throw Oops.Bah("订单已取消,无需重复操作");
|
||
|
||
_logger.LogInformation("开始取消报名:报名ID={RegistrationId}, 操作人={UserId}, 当前状态={PaymentStatus}",
|
||
registrationId, operatorUserId, reg.PaymentStatus);
|
||
|
||
// 如果是预报名记录(状态5),直接删除
|
||
if (reg.PaymentStatus == 5)
|
||
{
|
||
await DeleteRegistrationAsync(reg);
|
||
_logger.LogInformation("已删除预报名记录:报名ID={RegistrationId}", registrationId);
|
||
return true;
|
||
}
|
||
|
||
// 其他状态的记录,更新状态为已取消
|
||
reg.PaymentStatus = 4;
|
||
reg.UpdatedTime = DateTime.Now;
|
||
reg.UpdatedUserId = operatorUserId;
|
||
await _registrationRepository.UpdateAsync(reg);
|
||
|
||
if (!string.IsNullOrEmpty(reg.OrderNo))
|
||
{
|
||
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
|
||
if (order != null)
|
||
{
|
||
order.OrderStatus = OrderStatus.Cancelled;
|
||
order.UpdatedTime = DateTime.Now;
|
||
order.UpdatedUserId = operatorUserId;
|
||
await _orderRepository.UpdateAsync(order);
|
||
_logger.LogInformation("订单状态已同步更新:订单号={OrderNo}, 状态={OrderStatus}",
|
||
reg.OrderNo, OrderStatus.Cancelled);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("未找到对应的订单记录:报名ID={RegistrationId}, 订单号={OrderNo}",
|
||
registrationId, reg.OrderNo);
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("取消报名成功:报名ID={RegistrationId}", registrationId);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询并同步微信支付状态
|
||
/// </summary>
|
||
public async Task<bool> QueryAndSyncWeChatPaymentStatusAsync(long registrationId, long operatorUserId)
|
||
{
|
||
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == operatorUserId);
|
||
if (reg == null) throw Oops.Bah("报名记录不存在");
|
||
|
||
if (reg.PaymentStatus == 1) return true;
|
||
|
||
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
|
||
if (evt == null) throw Oops.Bah("赛事不存在");
|
||
|
||
WeChatPayConfig? payConfig = null;
|
||
if (evt.MerchantId.HasValue)
|
||
{
|
||
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
||
x.MerchantId == evt.MerchantId.Value &&
|
||
x.Status == AvailabilityStatus.Enable &&
|
||
!x.DeleteMark);
|
||
}
|
||
|
||
if (payConfig == null)
|
||
{
|
||
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
||
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
||
}
|
||
|
||
if (payConfig == null)
|
||
{
|
||
_logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId);
|
||
throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员");
|
||
}
|
||
|
||
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, reg.OrderNo);
|
||
if (queryResult.Success && queryResult.TradeState == "SUCCESS")
|
||
{
|
||
await HandlePaymentSuccessAsync(reg.OrderNo, queryResult.TransactionId, queryResult.SuccessTime?.DateTime, operatorUserId);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取平台级别的微信支付配置
|
||
/// </summary>
|
||
public async Task<WeChatPayConfig?> GetPlatformWeChatPayConfigAsync()
|
||
{
|
||
return await _weChatPayConfigRepository.GetFirstAsync(x =>
|
||
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据赛事获取对应的微信支付配置
|
||
/// </summary>
|
||
public async Task<WeChatPayConfig?> GetWeChatPayConfigByEventAsync(EventInfo evt)
|
||
{
|
||
WeChatPayConfig? payConfig = null;
|
||
if (evt.MerchantId.HasValue)
|
||
{
|
||
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
||
x.MerchantId == evt.MerchantId.Value &&
|
||
x.Status == AvailabilityStatus.Enable &&
|
||
!x.DeleteMark);
|
||
}
|
||
|
||
if (payConfig == null)
|
||
{
|
||
payConfig = await GetPlatformWeChatPayConfigAsync();
|
||
}
|
||
|
||
return payConfig;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一优惠券验证和使用方法
|
||
/// </summary>
|
||
[UnitOfWork]
|
||
public async Task<ValidateCouponOutput> ValidateAndUseCouponAsync(ValidateCouponInput input, long userId)
|
||
{
|
||
var result = new ValidateCouponOutput
|
||
{
|
||
IsValid = false,
|
||
InvalidReason = ""
|
||
};
|
||
|
||
// 查找兑换码
|
||
var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc =>
|
||
rc.Code == input.CouponCode &&
|
||
rc.Status == 0 &&
|
||
!rc.DeleteMark);
|
||
|
||
if (redemptionCode == null)
|
||
{
|
||
result.InvalidReason = "兑换码不存在或已使用";
|
||
return result;
|
||
}
|
||
|
||
// 查找优惠券
|
||
var coupon = await _couponRepository.GetFirstAsync(c =>
|
||
c.Id == redemptionCode.CouponId &&
|
||
c.Status == AvailabilityStatus.Enable &&
|
||
!c.DeleteMark);
|
||
|
||
if (coupon == null)
|
||
{
|
||
result.InvalidReason = "优惠券不存在或已失效";
|
||
return result;
|
||
}
|
||
|
||
// 检查优惠券是否已过期
|
||
if (coupon.EndTime.HasValue && coupon.EndTime.Value < DateTime.Now)
|
||
{
|
||
result.InvalidReason = "优惠券已过期";
|
||
return result;
|
||
}
|
||
|
||
// 检查优惠券是否已开始
|
||
if (coupon.StartTime.HasValue && coupon.StartTime.Value > DateTime.Now)
|
||
{
|
||
result.InvalidReason = "优惠券尚未生效";
|
||
return result;
|
||
}
|
||
|
||
// 检查库存
|
||
if (coupon.TotalQuantity > 0 && coupon.ClaimedQuantity >= coupon.TotalQuantity)
|
||
{
|
||
result.InvalidReason = "优惠券已被领完";
|
||
return result;
|
||
}
|
||
|
||
// 检查优惠券是否适用于该赛事
|
||
if (!string.IsNullOrEmpty(coupon.EventIds))
|
||
{
|
||
var eventIds = coupon.EventIds.Split(',').Select(long.Parse).ToList();
|
||
if (!eventIds.Contains(input.EventId))
|
||
{
|
||
result.InvalidReason = "该优惠券不适用于当前赛事";
|
||
return result;
|
||
}
|
||
}
|
||
|
||
// 检查当前用户是否拥有该优惠券且未使用,或者可以直接领取
|
||
var userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
|
||
uc.CouponId == coupon.Id &&
|
||
uc.UserId == userId &&
|
||
uc.Status == 0 &&
|
||
!uc.DeleteMark);
|
||
|
||
// 如果用户没有该优惠券,检查是否可以直接领取(假设可以直接通过兑换码领取)
|
||
if (userCoupon == null)
|
||
{
|
||
// 创建用户优惠券记录
|
||
userCoupon = new YD_Event.Core.Entities.UserCoupon
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
UserId = userId,
|
||
CouponId = coupon.Id,
|
||
ClaimTime = DateTime.Now,
|
||
Status = 0,
|
||
RedemptionCodeId = redemptionCode.Id,
|
||
RedemptionCode = redemptionCode.Code,
|
||
CreatedUserId = userId,
|
||
CreatedTime = DateTime.Now,
|
||
DeleteMark = false
|
||
};
|
||
await _userCouponRepository.InsertAsync(userCoupon);
|
||
|
||
// 更新兑换码状态和关联
|
||
redemptionCode.Status = 1;
|
||
redemptionCode.UsedTime = DateTime.Now;
|
||
redemptionCode.UserCouponId = userCoupon.Id;
|
||
await _redemptionCodeRepository.UpdateAsync(redemptionCode);
|
||
|
||
// 更新优惠券已领取数量
|
||
coupon.ClaimedQuantity += 1;
|
||
await _couponRepository.UpdateAsync(coupon);
|
||
}
|
||
|
||
// 计算优惠金额
|
||
var (discountAmount, finalAmount) = CalculateCouponDiscount(coupon, input.OriginalAmount);
|
||
|
||
result.IsValid = true;
|
||
result.CouponId = coupon.Id;
|
||
result.CouponName = coupon.Name ?? "优惠券";
|
||
result.DiscountType = coupon.Type;
|
||
result.DiscountValue = coupon.Value;
|
||
result.DiscountAmount = discountAmount;
|
||
result.FinalAmount = finalAmount;
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证报名相关配置参数
|
||
/// </summary>
|
||
private async Task<RegistrationValidationResult> ValidateRegistrationConfigAsync(EventInfo eventInfo)
|
||
{
|
||
var result = new RegistrationValidationResult
|
||
{
|
||
IsValid = true,
|
||
Details = new List<ValidationDetail>()
|
||
};
|
||
|
||
// 1. 验证费用配置是否存在(除非是免费赛事)
|
||
if (eventInfo.ChargeMode != ChargeMode.Free)
|
||
{
|
||
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == eventInfo.Id && !x.DeleteMark);
|
||
if (feeConfig == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "赛事费用配置不存在,请联系管理员";
|
||
result.Details.Add(CreateValidationDetail("费用配置验证", false, "赛事费用配置不存在"));
|
||
return result;
|
||
}
|
||
|
||
// 验证费用配置值是否有效
|
||
if (eventInfo.ChargeMode == ChargeMode.FixedPrice && feeConfig.Price <= 0)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "赛事费用配置不正确,请联系管理员";
|
||
result.Details.Add(CreateValidationDetail("费用配置验证", false, "定价模式费用配置不正确"));
|
||
return result;
|
||
}
|
||
|
||
result.Details.Add(CreateValidationDetail("费用配置验证", true, "费用配置验证通过"));
|
||
}
|
||
else
|
||
{
|
||
result.Details.Add(CreateValidationDetail("费用配置验证", true, "免费赛事,无需费用配置"));
|
||
}
|
||
|
||
// 2. 验证赛事项目是否存在
|
||
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventInfo.Id);
|
||
if (!eventProjects.Any())
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "赛事未配置任何项目,请联系管理员";
|
||
result.Details.Add(CreateValidationDetail("项目配置验证", false, "赛事项目配置不存在"));
|
||
return result;
|
||
}
|
||
result.Details.Add(CreateValidationDetail("项目配置验证", true, $"赛事配置了{eventProjects.Count}个项目"));
|
||
|
||
// 3. 验证商户配置是否存在(如果需要支付)
|
||
if (eventInfo.ChargeMode != ChargeMode.Free)
|
||
{
|
||
if (eventInfo.MerchantId.HasValue)
|
||
{
|
||
var merchant = await _merchantRepository.GetFirstAsync(x => x.Id == eventInfo.MerchantId.Value && !x.DeleteMark);
|
||
if (merchant == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "赛事关联的商户不存在,请联系管理员";
|
||
result.Details.Add(CreateValidationDetail("商户配置验证", false, "赛事关联的商户不存在"));
|
||
return result;
|
||
}
|
||
result.Details.Add(CreateValidationDetail("商户配置验证", true, "商户配置验证通过"));
|
||
}
|
||
else
|
||
{
|
||
// 如果赛事没有关联商户,检查是否有平台级别的支付配置
|
||
var platformPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x => x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
||
if (platformPayConfig == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "支付配置不存在,请联系管理员";
|
||
result.Details.Add(CreateValidationDetail("支付配置验证", false, "平台支付配置不存在"));
|
||
return result;
|
||
}
|
||
result.Details.Add(CreateValidationDetail("支付配置验证", true, "平台支付配置验证通过"));
|
||
}
|
||
}
|
||
|
||
result.Details.Add(CreateValidationDetail("配置验证", true, "所有报名相关配置验证通过"));
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证用户单位认证状态
|
||
/// </summary>
|
||
private async Task<RegistrationValidationResult> ValidateUserCertificationAsync(long userId)
|
||
{
|
||
var result = new RegistrationValidationResult
|
||
{
|
||
IsValid = true,
|
||
Details = new List<ValidationDetail>()
|
||
};
|
||
|
||
// 验证用户是否通过了单位认证
|
||
var certification = await _userRepository.GetFirstAsync(c =>
|
||
c.Id == userId &&
|
||
c.AuditStatus == AuditStatus.Pass &&
|
||
!c.DeleteMark);
|
||
|
||
if (certification == null)
|
||
{
|
||
result.IsValid = false;
|
||
result.ErrorMessage = "您尚未通过单位认证,请先完成单位认证";
|
||
result.Details.Add(CreateValidationDetail("单位认证验证", false, "用户未通过单位认证"));
|
||
return result;
|
||
}
|
||
|
||
result.Details.Add(CreateValidationDetail("单位认证验证", true, "用户单位认证验证通过"));
|
||
return result;
|
||
}
|
||
|
||
#region 统一退款相关方法
|
||
|
||
/// <summary>
|
||
/// 计算退款金额(统一方法)
|
||
/// </summary>
|
||
/// <param name="totalAmount">总金额</param>
|
||
/// <param name="eventStartTime">赛事开始时间</param>
|
||
/// <returns>应退金额</returns>
|
||
[NonAction]
|
||
public async Task<decimal> CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime)
|
||
{
|
||
var refundAmount = await _refundConfigService.CalculateRefundAmount(totalAmount, eventStartTime);
|
||
_logger.LogInformation("统一退款金额计算:总金额={TotalAmount}, 赛事开始时间={EventStartTime}, 计算后退款金额={RefundAmount}",
|
||
totalAmount, eventStartTime, refundAmount);
|
||
return refundAmount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一取消报名预览方法
|
||
/// </summary>
|
||
/// <param name="registrationId">报名ID</param>
|
||
/// <param name="operatorUserId">操作人用户ID</param>
|
||
/// <returns>取消预览输出</returns>
|
||
[NonAction]
|
||
public async Task<CancelPreviewOutput> GetCancelPreviewAsync(long registrationId, long operatorUserId)
|
||
{
|
||
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == operatorUserId);
|
||
if (registration == null) throw Oops.Bah("报名记录不存在");
|
||
|
||
var eventInfo = await _db.Queryable<EventInfo>().InSingleAsync(registration.EventId);
|
||
var items = await _db.Queryable<EventRegistrationItem>().Where(x => x.RegistrationId == registrationId).ToListAsync();
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId);
|
||
|
||
// 查询历史已退款金额
|
||
var historyRefunds = await _refundRecordRepository.GetListAsync(x => x.RegistrationId == registrationId && x.RefundStatus == 1);
|
||
var refundedAmount = historyRefunds.Sum(x => x.RefundAmount);
|
||
|
||
// 获取尚未被软删除的报名项
|
||
var remainingItems = items.Where(x => !x.DeleteMark).ToList();
|
||
|
||
// 计算剩余可退款金额:应该基于每个未退款的报名项分别计算,然后求和
|
||
// 这样确保每个团队/个人的退款是独立的,不受其他团队退款影响
|
||
decimal refundableAmount = 0;
|
||
|
||
// 区分不同的收费模式来计算
|
||
if (eventInfo.ChargeMode == ChargeMode.UnitPriceProject)
|
||
{
|
||
// 单价(项目)模式:每个未删除的项目独立计算退款
|
||
foreach (var item in remainingItems)
|
||
{
|
||
var itemRefund = await CalculateRefundAmountAsync(item.TotalPrice, eventInfo.MatchStartTime);
|
||
refundableAmount += itemRefund;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 其他模式:使用传统方式计算
|
||
// 计算剩余未退款的原始金额
|
||
var remainingOriginalAmount = registration.TotalAmount - refundedAmount;
|
||
|
||
// 基于剩余金额计算可退款金额
|
||
refundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime);
|
||
}
|
||
|
||
// 确保可退款金额不小于0
|
||
refundableAmount = Math.Max(0, refundableAmount);
|
||
|
||
_logger.LogInformation("统一取消报名预览:报名ID={RegistrationId}, 赛事名称={EventName}, 实际支付={ActualAmount}, 已退款={RefundedAmount}, 剩余可退款={RefundAmount}, 支付方式={PaymentMethod}",
|
||
registrationId, eventInfo?.EventName, registration.TotalAmount, refundedAmount, refundableAmount, registration.PaymentMethod);
|
||
|
||
// 获取组别信息(年龄和性别限制)
|
||
var groupIds = items.Where(x => x.EventProjectGroupId.HasValue).Select(x => x.EventProjectGroupId.Value).Distinct().ToList();
|
||
var groups = new Dictionary<long, EventProjectGroup>();
|
||
if (groupIds.Any())
|
||
{
|
||
var groupList = await _db.Queryable<EventProjectGroup>()
|
||
.Where(x => groupIds.Contains(x.Id))
|
||
.ToListAsync();
|
||
groups = groupList.ToDictionary(x => x.Id);
|
||
}
|
||
|
||
// 获取项目信息(从Project表查询项目名称)
|
||
var projectIds = items.Select(x => x.ProjectId).Distinct().ToList();
|
||
var projects = new Dictionary<long, YD_Event.Core.Entities.Project>();
|
||
if (projectIds.Any())
|
||
{
|
||
var projectList = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
|
||
projects = projectList.ToDictionary(x => x.Id);
|
||
}
|
||
|
||
var output = new CancelPreviewOutput
|
||
{
|
||
RegistrationId = registration.Id,
|
||
EventName = eventInfo?.EventName ?? "",
|
||
TeamName = registration.TeamName ?? "",
|
||
ChargeMode = (int)eventInfo.ChargeMode,
|
||
ActualPaidAmount = registration.TotalAmount,
|
||
RefundableAmount = refundableAmount,
|
||
RefundedAmount = refundedAmount,
|
||
CurrentRefundAmount = refundableAmount,
|
||
PaymentMethod = registration.PaymentMethod,
|
||
PaymentStatus = registration.PaymentStatus,
|
||
NeedPayeeInfo = registration.PaymentMethod == 1 && registration.PaymentStatus == 1
|
||
};
|
||
|
||
foreach (var item in items)
|
||
{
|
||
output.Projects.Add(new CancelPreviewProjectDto
|
||
{
|
||
ProjectId = item.ProjectId,
|
||
ProjectName = item.ProjectName ?? "",
|
||
ProjectType = item.ProjectTypeName.Contains("团队")? "团队" : (item.ProjectTypeName.Contains("个人") ? "个人" : string.Empty),
|
||
PersonCount = item.PersonCount,
|
||
UnitPrice = item.UnitPrice
|
||
});
|
||
}
|
||
|
||
foreach (var person in persons)
|
||
{
|
||
output.Members.Add(new CancelPreviewMemberDto
|
||
{
|
||
MemberId = person.Id,
|
||
Name = person.Name ?? "",
|
||
IsLeader = person.PersonType == PersonType.Leader
|
||
});
|
||
}
|
||
|
||
// 构建新版详细数据结构(按项目 -> 组别 -> 团队 -> 队员分类)
|
||
var itemGroups = items.GroupBy(x => x.ProjectId);
|
||
foreach (var itemGroup in itemGroups)
|
||
{
|
||
var firstItem = itemGroup.First();
|
||
// 获取项目信息
|
||
YD_Event.Core.Entities.Project? project = null;
|
||
if (projects.TryGetValue(firstItem.ProjectId, out var foundProject))
|
||
{
|
||
project = foundProject;
|
||
}
|
||
|
||
var projectDetail = new CancelPreviewProjectDetailDto
|
||
{
|
||
ProjectId = firstItem.ProjectId,
|
||
ProjectName = project?.Name ?? firstItem.ProjectName ?? "",
|
||
ProjectType = firstItem.ProjectTypeName.Contains("团队") ? "团队" : (firstItem.ProjectTypeName.Contains("个人") ? "个人" : string.Empty),
|
||
PersonCount = itemGroup.Sum(x => x.PersonCount),
|
||
UnitPrice = firstItem.UnitPrice
|
||
};
|
||
|
||
// 按项目和组别唯一组合分组(使用ProjectId + EventProjectGroupId作为唯一标识)
|
||
var groupGroups = itemGroup.GroupBy(x => new { x.ProjectId, GroupKey = x.EventProjectGroupId.HasValue ? x.EventProjectGroupId.Value.ToString() : (x.GroupName ?? "") });
|
||
foreach (var groupGroup in groupGroups)
|
||
{
|
||
var firstGroupItem = groupGroup.First();
|
||
|
||
// 获取组别信息
|
||
EventProjectGroup? group = null;
|
||
if (firstGroupItem.EventProjectGroupId.HasValue && groups.TryGetValue(firstGroupItem.EventProjectGroupId.Value, out var foundGroup))
|
||
{
|
||
group = foundGroup;
|
||
}
|
||
|
||
var groupDto = new CancelPreviewGroupDto
|
||
{
|
||
GroupName = firstGroupItem.GroupName ?? "",
|
||
RegistrationItemId = firstGroupItem.Id,
|
||
ProjectTypeName = firstGroupItem.ProjectTypeName ?? "",
|
||
PersonCount = groupGroup.Sum(x => x.PersonCount),
|
||
UnitPrice = firstGroupItem.UnitPrice,
|
||
BirthDateStart = group?.BirthDateStart,
|
||
BirthDateEnd = group?.BirthDateEnd,
|
||
GenderRequirement = group?.Gender
|
||
};
|
||
|
||
// 处理该项目组别下的所有报名项,每个报名项作为一个独立的团队
|
||
int teamIndex = 0;
|
||
foreach (var item in groupGroup)
|
||
{
|
||
// 获取该报名项目下的所有队员
|
||
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList();
|
||
|
||
// 每个报名项作为一个独立的团队条目
|
||
var teamDto = new CancelPreviewTeamDto
|
||
{
|
||
TeamName = "",
|
||
TeamIndex = teamIndex++,
|
||
RegistrationItemId = item.Id
|
||
};
|
||
|
||
foreach (var person in itemPersons)
|
||
{
|
||
teamDto.Members.Add(new CancelPreviewMemberDto
|
||
{
|
||
MemberId = person.Id,
|
||
Name = person.Name ?? "",
|
||
IsLeader = person.PersonType == PersonType.Leader
|
||
});
|
||
}
|
||
|
||
groupDto.Teams.Add(teamDto);
|
||
}
|
||
|
||
projectDetail.Groups.Add(groupDto);
|
||
}
|
||
|
||
output.ProjectDetails.Add(projectDetail);
|
||
}
|
||
|
||
var refundConfigs = await _refundConfigService.GetList();
|
||
refundConfigs = refundConfigs.Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable).OrderBy(x => x.DayStart).ToList();
|
||
|
||
foreach (var config in refundConfigs)
|
||
{
|
||
output.RefundRules.Add(new RefundRuleDto
|
||
{
|
||
DayStart = config.DayStart,
|
||
DayEnd = config.DayEnd,
|
||
DeductPercentage = config.DeductPercentage,
|
||
Remark = config.Remark
|
||
});
|
||
}
|
||
|
||
var daysRemaining = (eventInfo.MatchStartTime - DateTime.Now).TotalDays;
|
||
if (daysRemaining < 0) daysRemaining = 0;
|
||
|
||
if (registration.PaymentMethod == 1)
|
||
{
|
||
output.ApplicableRefundRule = "线下支付订单取消报名将不执行微信退款,仅更新报名状态为已取消。";
|
||
}
|
||
else
|
||
{
|
||
var matchConfig = refundConfigs.FirstOrDefault(c =>
|
||
daysRemaining >= c.DayStart &&
|
||
(c.DayEnd == int.MaxValue ? true : daysRemaining < c.DayEnd)
|
||
);
|
||
|
||
if (matchConfig != null)
|
||
{
|
||
var refundPercentage = 100 - matchConfig.DeductPercentage;
|
||
output.ApplicableRefundRule = $"距离比赛开始还有 {Math.Round(daysRemaining, 1)} 天,退款比例为 {refundPercentage}%,扣除 {matchConfig.DeductPercentage}%。";
|
||
}
|
||
else
|
||
{
|
||
output.ApplicableRefundRule = "距离比赛开始还有 " + Math.Round(daysRemaining, 1) + " 天,全额退款。";
|
||
}
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算退款金额(前端选择时调用)
|
||
/// </summary>
|
||
/// <param name="input">计算输入</param>
|
||
/// <param name="operatorUserId">操作人用户ID</param>
|
||
/// <returns>计算输出</returns>
|
||
public async Task<CalculateRefundAmountOutput> CalculateRefundAmountAsync(CalculateRefundAmountInput input, long operatorUserId)
|
||
{
|
||
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == input.RegistrationId && x.CreatedUserId == operatorUserId);
|
||
if (registration == null) throw Oops.Bah("报名记录不存在");
|
||
|
||
var eventInfo = await _db.Queryable<EventInfo>().InSingleAsync(registration.EventId);
|
||
var items = await _db.Queryable<EventRegistrationItem>().Where(x => x.RegistrationId == input.RegistrationId).ToListAsync();
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId);
|
||
|
||
// 构建一个临时的CancelExecuteInput来复用计算逻辑
|
||
var tempInput = new CancelExecuteInput
|
||
{
|
||
RegistrationId = input.RegistrationId,
|
||
CancelType = input.CancelType,
|
||
PersonIds = input.PersonIds,
|
||
CancelTeams = input.CancelTeams
|
||
};
|
||
|
||
// 计算退款金额
|
||
var refundAmount = await CalculateActualRefundAmountAsync(tempInput, registration, eventInfo, items, persons);
|
||
|
||
// 查询历史已退款金额
|
||
var historyRefunds = await _refundRecordRepository.GetListAsync(x => x.RegistrationId == registration.Id && x.RefundStatus == 1);
|
||
var refundedAmount = historyRefunds.Sum(x => x.RefundAmount);
|
||
|
||
// 计算剩余可退款金额:应该基于每个未退款的报名项分别计算,然后求和
|
||
var remainingItems = items.Where(x => !x.DeleteMark).ToList();
|
||
decimal remainingRefundableAmount = 0;
|
||
|
||
// 区分不同的收费模式来计算
|
||
if (eventInfo.ChargeMode == ChargeMode.UnitPriceProject)
|
||
{
|
||
// 单价(项目)模式:每个未删除的项目独立计算退款
|
||
foreach (var item in remainingItems)
|
||
{
|
||
var itemRefund = await CalculateRefundAmountAsync(item.TotalPrice, eventInfo.MatchStartTime);
|
||
remainingRefundableAmount += itemRefund;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 其他模式:使用传统方式计算
|
||
// 计算剩余未退款的原始金额
|
||
var remainingOriginalAmount = registration.TotalAmount - refundedAmount;
|
||
remainingRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime);
|
||
}
|
||
|
||
return new CalculateRefundAmountOutput
|
||
{
|
||
refundAmount = refundAmount,
|
||
refundableAmount = remainingRefundableAmount,
|
||
refundedAmount = refundedAmount,
|
||
actualPaidAmount = registration.TotalAmount
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一取消报名执行方法
|
||
/// </summary>
|
||
/// <param name="input">取消执行输入</param>
|
||
/// <param name="operatorUserId">操作人用户ID</param>
|
||
/// <returns>取消执行输出</returns>
|
||
/// <summary>
|
||
/// 根据取消规则计算退款金额
|
||
/// </summary>
|
||
private async Task<decimal> CalculateActualRefundAmountAsync(CancelExecuteInput input, Core.Entities.EventRegistration registration,
|
||
Core.Entities.EventInfo eventInfo, List<EventRegistrationItem> items, List<EventRegistrationPerson> persons)
|
||
{
|
||
_logger.LogInformation("CalculateActualRefundAmountAsync:报名ID={RegistrationId}, 收费模式={ChargeMode}, 取消类型={CancelType}",
|
||
registration.Id, eventInfo.ChargeMode, input.CancelType);
|
||
|
||
// 如果是免费项目,不退款
|
||
if (eventInfo.ChargeMode == ChargeMode.Free || registration.TotalAmount <= 0)
|
||
{
|
||
_logger.LogInformation("免费项目或总金额为0,不退款");
|
||
return 0;
|
||
}
|
||
|
||
// 查询历史已退款金额
|
||
var historyRefunds = await _refundRecordRepository.GetListAsync(x => x.RegistrationId == registration.Id && x.RefundStatus == 1);
|
||
var refundedAmount = historyRefunds.Sum(x => x.RefundAmount);
|
||
|
||
// 计算剩余的原始未退款金额
|
||
var remainingOriginalAmount = registration.TotalAmount - refundedAmount;
|
||
|
||
// 如果没有剩余可退,直接返回0
|
||
if (remainingOriginalAmount <= 0)
|
||
{
|
||
_logger.LogInformation("没有剩余可退款金额,remainingOriginalAmount={Amount}", remainingOriginalAmount);
|
||
return 0;
|
||
}
|
||
|
||
// 全部取消
|
||
if (input.CancelType == 0)
|
||
{
|
||
var fullRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime);
|
||
_logger.LogInformation("全部取消,退款金额={Amount}", fullRefundAmount);
|
||
return fullRefundAmount;
|
||
}
|
||
|
||
// 部分取消
|
||
// 收集所有取消的队员ID
|
||
var cancelMemberIds = input.PersonIds ?? new List<long>();
|
||
var cancelTeams = input.CancelTeams ?? new List<string>();
|
||
|
||
var allCancelIds = new HashSet<long>(cancelMemberIds);
|
||
foreach (var teamKey in cancelTeams)
|
||
{
|
||
var parts = teamKey.Split('_');
|
||
if (parts.Length == 2 && long.TryParse(parts[0], out var registrationItemId))
|
||
{
|
||
var teamPersons = persons.Where(p => p.RegistrationItemId == registrationItemId);
|
||
foreach (var person in teamPersons)
|
||
{
|
||
allCancelIds.Add(person.Id);
|
||
}
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("本次取消的队员数量:{Count}", allCancelIds.Count);
|
||
|
||
switch (eventInfo.ChargeMode)
|
||
{
|
||
case ChargeMode.FixedPrice:
|
||
// 【定价(赛事)退款规则】
|
||
// - 全队取消:严格按照预设退款规则执行退款
|
||
// - 个人取消:不退款
|
||
// - 仅当所有项目取消时才启动退款,单个项目取消不退款
|
||
|
||
_logger.LogInformation("定价(赛事)退款:开始计算");
|
||
|
||
// 检查是否取消了所有队员(所有项目取消)
|
||
var allPersonsCanceled = allCancelIds.Count == persons.Count;
|
||
|
||
if (!allPersonsCanceled)
|
||
{
|
||
// 没有取消所有队员,个人取消不退款
|
||
_logger.LogInformation("定价赛事部分取消(未取消所有人员),个人取消不退款");
|
||
return 0;
|
||
}
|
||
|
||
// 取消了所有队员,按退款规则退款
|
||
var fixedPriceRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime);
|
||
_logger.LogInformation("定价赛事全部取消,按退款规则退款,金额={Amount}", fixedPriceRefundAmount);
|
||
return fixedPriceRefundAmount;
|
||
|
||
case ChargeMode.UnitPriceEvent:
|
||
// 【单价(赛事)退款规则】
|
||
// - 全队取消:严格按照退款规则执行退款
|
||
// - 个人取消:计算公式为【单价(赛事)× 取消人数】,并正确应用退款规则
|
||
// - 全额退款:用户取消全部报名项目时按退款规则全额退还费用
|
||
// - 部分退款:用户取消部分项目时按取消人数对应费用进行退款
|
||
|
||
_logger.LogInformation("单价(赛事)退款:开始计算");
|
||
|
||
if (allCancelIds.Count == 0)
|
||
{
|
||
_logger.LogInformation("单价赛事退款:没有取消任何人,不退款");
|
||
return 0;
|
||
}
|
||
|
||
// 获取赛事单价(从费用配置中获取)
|
||
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == eventInfo.Id && !x.DeleteMark);
|
||
var eventUnitPrice = feeConfig?.Price ?? 0;
|
||
|
||
_logger.LogInformation("单价(赛事)信息:赛事单价={EventUnitPrice}", eventUnitPrice);
|
||
|
||
// 计算本次取消的原始金额
|
||
var unitPriceEventCancelAmount = eventUnitPrice * allCancelIds.Count;
|
||
// 直接对本次取消的原始金额应用退款规则
|
||
var unitPriceEventFinalAmount = await CalculateRefundAmountAsync(unitPriceEventCancelAmount, eventInfo.MatchStartTime);
|
||
|
||
// 移除了错误的限制:每个团队/个人的退款应该独立计算,不受其他团队已退款金额影响
|
||
// 直接返回基于本次取消金额计算的退款金额
|
||
|
||
_logger.LogInformation("单价(赛事)退款:取消人数={CancelCount}, 赛事单价={UnitPrice}, 本次取消原始金额={CancelAmount}, 按规则退款金额={RefundAmount}",
|
||
allCancelIds.Count, eventUnitPrice, unitPriceEventCancelAmount, unitPriceEventFinalAmount);
|
||
|
||
return unitPriceEventFinalAmount;
|
||
|
||
case ChargeMode.UnitPriceProject:
|
||
// 【单价(项目)退款规则】
|
||
// - 全队取消:严格按照退款规则执行退款
|
||
// - 个人取消:计算公式为【单价(项目A)× 项目A取消人数 + 单价(项目B)× 项目B取消人数】,并正确应用退款规则
|
||
|
||
_logger.LogInformation("单价(项目)退款:开始计算");
|
||
|
||
if (allCancelIds.Count == 0)
|
||
{
|
||
_logger.LogInformation("单价项目退款:没有取消任何人,不退款");
|
||
return 0;
|
||
}
|
||
|
||
decimal totalProjectCancelAmount = 0;
|
||
|
||
// 按项目分组计算取消的人数和金额
|
||
foreach (var item in items)
|
||
{
|
||
// 只处理未被软删除的项目中要取消的人员
|
||
if (item.DeleteMark) continue;
|
||
|
||
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id && !p.DeleteMark).ToList();
|
||
var canceledInItem = itemPersons.Count(p => allCancelIds.Contains(p.Id));
|
||
|
||
if (canceledInItem > 0)
|
||
{
|
||
var projectCancelAmount = item.UnitPrice * canceledInItem;
|
||
totalProjectCancelAmount += projectCancelAmount;
|
||
_logger.LogInformation("单价(项目)退款:项目={ProjectName}, 单价={UnitPrice}, 取消人数={CancelCount}, 本项原始金额={Amount}",
|
||
item.ProjectName, item.UnitPrice, canceledInItem, projectCancelAmount);
|
||
}
|
||
}
|
||
|
||
// 直接对本次取消的原始总金额应用退款规则
|
||
var projectRefundAmount = await CalculateRefundAmountAsync(totalProjectCancelAmount, eventInfo.MatchStartTime);
|
||
|
||
// 移除了错误的限制:每个团队/个人的退款应该独立计算,不受其他团队已退款金额影响
|
||
// 直接返回基于本次取消金额计算的退款金额
|
||
|
||
_logger.LogInformation("单价(项目)退款:本次取消总原始金额={TotalCancelAmount}, 按规则退款金额={RefundAmount}",
|
||
totalProjectCancelAmount, projectRefundAmount);
|
||
|
||
return projectRefundAmount;
|
||
|
||
default:
|
||
_logger.LogInformation("未知收费模式,不退款");
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
[UnitOfWork]
|
||
[NonAction]
|
||
public async Task<CancelExecuteOutput> ExecuteCancelRegistrationAsync(CancelExecuteInput input, long operatorUserId)
|
||
{
|
||
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == input.RegistrationId && x.CreatedUserId == operatorUserId);
|
||
if (registration == null) throw Oops.Bah("报名记录不存在");
|
||
|
||
if (registration.PaymentStatus == 4) throw Oops.Bah("订单已取消");
|
||
if (registration.PaymentMethod == 1 && registration.PaymentStatus != 0 && registration.PaymentStatus != 1)
|
||
{
|
||
throw Oops.Bah("线下支付订单仅当状态为未支付或已支付时允许取消,当前状态不允许取消");
|
||
}
|
||
if (registration.PaymentMethod != 1 && registration.PaymentStatus != 1) throw Oops.Bah("未支付的订单无法退款");
|
||
|
||
// 验证线下支付且已支付时,是否填写了收款人信息
|
||
if (registration.PaymentMethod == 1 && registration.PaymentStatus == 1)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.PayeeName))
|
||
{
|
||
throw Oops.Bah("请填写收款人姓名");
|
||
}
|
||
if (string.IsNullOrWhiteSpace(input.PayeeBankName))
|
||
{
|
||
throw Oops.Bah("请填写收款人开户行");
|
||
}
|
||
if (string.IsNullOrWhiteSpace(input.PayeeBankAccount))
|
||
{
|
||
throw Oops.Bah("请填写收款人银行卡号");
|
||
}
|
||
}
|
||
|
||
var now = DateTime.Now;
|
||
var eventInfo = await _eventRepository.GetFirstAsync(e => e.Id == registration.EventId);
|
||
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registration.Id);
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id);
|
||
|
||
// 计算实际退款金额
|
||
// 【重要】始终信任后端计算结果,前端计算只用于展示
|
||
// 不接受前端传递的退款金额,避免资金风险
|
||
decimal actualRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons);
|
||
_logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 总金额={TotalAmount}, 退款金额={ActualRefundAmount}",
|
||
registration.Id, registration.TotalAmount, actualRefundAmount);
|
||
|
||
bool isPartialCancel = input.CancelType == 1;
|
||
|
||
// 收集所有要取消的队员ID
|
||
var allCancelMemberIds = new HashSet<long>();
|
||
if (input.PersonIds != null)
|
||
{
|
||
foreach (var id in input.PersonIds)
|
||
{
|
||
allCancelMemberIds.Add(id);
|
||
}
|
||
}
|
||
|
||
// 收集被取消团队的所有队员
|
||
if (input.CancelTeams != null)
|
||
{
|
||
foreach (var teamKey in input.CancelTeams)
|
||
{
|
||
var parts = teamKey.Split('_');
|
||
if (parts.Length == 2 && long.TryParse(parts[0], out var registrationItemId))
|
||
{
|
||
var teamPersons = persons.Where(p => p.RegistrationItemId == registrationItemId);
|
||
foreach (var person in teamPersons)
|
||
{
|
||
allCancelMemberIds.Add(person.Id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (registration.PaymentMethod == 1)
|
||
{
|
||
// 线下支付取消报名
|
||
if (isPartialCancel)
|
||
{
|
||
var totalPersons = persons.Count;
|
||
if (totalPersons == 0)
|
||
{
|
||
throw Oops.Bah("报名记录中没有队员信息");
|
||
}
|
||
|
||
var selectedPersons = persons.Where(p => allCancelMemberIds.Contains(p.Id)).ToList();
|
||
if (selectedPersons.Count == 0)
|
||
{
|
||
throw Oops.Bah("请选择要取消的队员或团队");
|
||
}
|
||
|
||
_logger.LogInformation("线下支付部分取消:报名ID={RegistrationId}, 选择人数={SelectedCount}, 退款金额={RefundAmount}",
|
||
registration.Id, selectedPersons.Count, actualRefundAmount);
|
||
|
||
// 软删除报名人员
|
||
await SoftDeletePersonsAsync(allCancelMemberIds.ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}",
|
||
registration.Id, string.Join(", ", allCancelMemberIds));
|
||
|
||
// 检查并软删除空的 RegistrationItem(所有成员都被软删除的项)
|
||
var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id && !x.DeleteMark);
|
||
var remainingRegistrationItemIds = new HashSet<long>(remainingPersons.Select(p => p.RegistrationItemId));
|
||
var itemsToDelete = items.Where(item => !remainingRegistrationItemIds.Contains(item.Id)).ToList();
|
||
if (itemsToDelete.Count > 0)
|
||
{
|
||
await SoftDeleteItemsAsync(itemsToDelete.Select(i => i.Id).ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除空团队:报名ID={RegistrationId}, 团队ID列表={ItemIds}",
|
||
registration.Id, string.Join(", ", itemsToDelete.Select(i => i.Id)));
|
||
}
|
||
|
||
var remainingPersonsCount = remainingPersons.Count;
|
||
registration.TotalPersonCount = remainingPersonsCount;
|
||
|
||
if (remainingPersonsCount > 0)
|
||
{
|
||
registration.UpdatedUserId = operatorUserId;
|
||
registration.UpdatedTime = now;
|
||
registration.RegistrationRemark = input.CancelReason;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
_logger.LogInformation("线下支付部分取消完成,仍有剩余人员:报名ID={RegistrationId}, 剩余人数={RemainingCount}",
|
||
registration.Id, remainingPersonsCount);
|
||
}
|
||
else
|
||
{
|
||
registration.PaymentStatus = 4; // 已取消
|
||
registration.UpdatedUserId = operatorUserId;
|
||
registration.UpdatedTime = now;
|
||
registration.RegistrationRemark = input.CancelReason;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
_logger.LogInformation("线下支付所有人员已取消,更新报名状态为已取消:报名ID={RegistrationId}", registration.Id);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogInformation("线下支付全额取消:报名ID={RegistrationId}, 退款金额={RefundAmount}",
|
||
registration.Id, actualRefundAmount);
|
||
|
||
// 软删除所有报名人员
|
||
await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除所有队员:报名ID={RegistrationId}", registration.Id);
|
||
|
||
// 软删除所有报名项目
|
||
await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除所有项目:报名ID={RegistrationId}", registration.Id);
|
||
|
||
registration.PaymentStatus = 4; // 已取消
|
||
registration.UpdatedUserId = operatorUserId;
|
||
registration.UpdatedTime = now;
|
||
registration.RegistrationRemark = input.CancelReason;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 线上支付:原有退款逻辑
|
||
if (isPartialCancel)
|
||
{
|
||
var totalPersons = persons.Count;
|
||
if (totalPersons == 0)
|
||
{
|
||
throw Oops.Bah("报名记录中没有队员信息");
|
||
}
|
||
|
||
var selectedPersons = persons.Where(p => allCancelMemberIds.Contains(p.Id)).ToList();
|
||
if (selectedPersons.Count == 0)
|
||
{
|
||
throw Oops.Bah("请选择要取消的队员或团队");
|
||
}
|
||
|
||
_logger.LogInformation("统一部分退款:报名ID={RegistrationId}, 总人数={TotalPersons}, 选择人数={SelectedCount}, 实际退款金额={ActualRefundAmount}",
|
||
registration.Id, totalPersons, selectedPersons.Count, actualRefundAmount);
|
||
|
||
// 软删除报名人员
|
||
await SoftDeletePersonsAsync(allCancelMemberIds.ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}",
|
||
registration.Id, string.Join(", ", allCancelMemberIds));
|
||
|
||
// 检查并软删除空的 RegistrationItem(所有成员都被软删除的项)
|
||
var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id && !x.DeleteMark);
|
||
var remainingRegistrationItemIds = new HashSet<long>(remainingPersons.Select(p => p.RegistrationItemId));
|
||
var itemsToDelete = items.Where(item => !remainingRegistrationItemIds.Contains(item.Id)).ToList();
|
||
if (itemsToDelete.Count > 0)
|
||
{
|
||
await SoftDeleteItemsAsync(itemsToDelete.Select(i => i.Id).ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除空团队:报名ID={RegistrationId}, 团队ID列表={ItemIds}",
|
||
registration.Id, string.Join(", ", itemsToDelete.Select(i => i.Id)));
|
||
}
|
||
|
||
var remainingPersonsCount = remainingPersons.Count;
|
||
registration.TotalPersonCount = remainingPersonsCount;
|
||
|
||
if (remainingPersonsCount > 0)
|
||
{
|
||
registration.UpdatedUserId = operatorUserId;
|
||
registration.UpdatedTime = now;
|
||
registration.RegistrationRemark = input.CancelReason;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
_logger.LogInformation("统一部分退款完成,仍有剩余人员:报名ID={RegistrationId}, 剩余人数={RemainingCount}",
|
||
registration.Id, remainingPersonsCount);
|
||
}
|
||
else
|
||
{
|
||
registration.PaymentStatus = 3;
|
||
registration.UpdatedUserId = operatorUserId;
|
||
registration.UpdatedTime = now;
|
||
registration.RegistrationRemark = input.CancelReason;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
_logger.LogInformation("统一所有人员已退款,更新报名状态为已退款:报名ID={RegistrationId}", registration.Id);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogInformation("统一全额退款:报名ID={RegistrationId}, 退款金额={ActualRefundAmount}",
|
||
registration.Id, actualRefundAmount);
|
||
|
||
// 软删除所有报名人员
|
||
await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除所有队员:报名ID={RegistrationId}", registration.Id);
|
||
|
||
// 软删除所有报名项目
|
||
await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now);
|
||
_logger.LogInformation("已软删除所有项目:报名ID={RegistrationId}", registration.Id);
|
||
|
||
registration.PaymentStatus = 3;
|
||
registration.UpdatedUserId = operatorUserId;
|
||
registration.UpdatedTime = now;
|
||
registration.RegistrationRemark = input.CancelReason;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
}
|
||
}
|
||
|
||
var output = new CancelExecuteOutput
|
||
{
|
||
Success = true,
|
||
Message = isPartialCancel ? "部分取消报名成功" : "取消报名成功",
|
||
CancelTime = now,
|
||
RefundAmount = actualRefundAmount
|
||
};
|
||
|
||
if (registration.PaymentMethod == 1)
|
||
{
|
||
// 线下支付:创建退款订单和退款记录,保存收款人信息,不执行微信退款
|
||
_logger.LogInformation("线下支付取消报名:报名ID={RegistrationId}, 退款金额={RefundAmount}",
|
||
registration.Id, actualRefundAmount);
|
||
|
||
if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0)
|
||
{
|
||
var originalOrder = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo);
|
||
if (originalOrder != null)
|
||
{
|
||
var refundOrder = new YD_Event.Core.Entities.Order
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(),
|
||
OrderType = 2,
|
||
MerchantId = originalOrder.MerchantId,
|
||
MerchantCertificationId = originalOrder.MerchantCertificationId,
|
||
EventId = originalOrder.EventId,
|
||
RegistrationId = originalOrder.RegistrationId,
|
||
RelatedOrderId = originalOrder.Id,
|
||
OriginalAmount = originalOrder.ActualAmount,
|
||
ActualAmount = actualRefundAmount,
|
||
DiscountAmount = 0,
|
||
OrderStatus = OrderStatus.Refunded,
|
||
PaymentTime = now,
|
||
Remark = input.CancelReason,
|
||
PayeeName = input.PayeeName,
|
||
PayeeBankName = input.PayeeBankName,
|
||
PayeeBankAccount = input.PayeeBankAccount,
|
||
CreatedTime = now,
|
||
CreatedUserId = operatorUserId,
|
||
Status = AvailabilityStatus.Enable
|
||
};
|
||
await _orderRepository.InsertAsync(refundOrder);
|
||
output.RefundNo = refundOrder.OrderNo;
|
||
|
||
_logger.LogInformation("线下支付退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}",
|
||
registration.Id, originalOrder.OrderNo, refundOrder.OrderNo, actualRefundAmount);
|
||
|
||
// 创建线下支付退款记录
|
||
await CreateRefundRecordAsync(registration, eventInfo, null, refundOrder.OrderNo,
|
||
null, actualRefundAmount, registration.TotalAmount, input.CancelReason, operatorUserId,
|
||
input.PayeeName, input.PayeeBankName, input.PayeeBankAccount);
|
||
}
|
||
}
|
||
}
|
||
else if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0)
|
||
{
|
||
var originalOrder = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo);
|
||
if (originalOrder != null)
|
||
{
|
||
var refundOrder = new YD_Event.Core.Entities.Order
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(),
|
||
OrderType = 2,
|
||
MerchantId = originalOrder.MerchantId,
|
||
MerchantCertificationId = originalOrder.MerchantCertificationId,
|
||
EventId = originalOrder.EventId,
|
||
RegistrationId = originalOrder.RegistrationId,
|
||
RelatedOrderId = originalOrder.Id,
|
||
OriginalAmount = originalOrder.ActualAmount,
|
||
ActualAmount = actualRefundAmount,
|
||
DiscountAmount = 0,
|
||
OrderStatus = OrderStatus.Refunded,
|
||
PaymentTime = now,
|
||
Remark = input.CancelReason,
|
||
CreatedTime = now,
|
||
CreatedUserId = operatorUserId,
|
||
Status = AvailabilityStatus.Enable
|
||
};
|
||
await _orderRepository.InsertAsync(refundOrder);
|
||
output.RefundNo = refundOrder.OrderNo;
|
||
|
||
_logger.LogInformation("退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}",
|
||
registration.Id, originalOrder.OrderNo, refundOrder.OrderNo, actualRefundAmount);
|
||
}
|
||
|
||
var merchant = await _merchantRepository.GetFirstAsync(x => x.Id == eventInfo.MerchantId);
|
||
|
||
if (merchant != null)
|
||
{
|
||
var wechatConfig = await _weChatPayConfigRepository.GetFirstAsync(x => x.MerchantId == merchant.Id);
|
||
|
||
if (wechatConfig != null)
|
||
{
|
||
var outRefundNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync();
|
||
if (string.IsNullOrEmpty(output.RefundNo))
|
||
{
|
||
output.RefundNo = outRefundNo;
|
||
}
|
||
|
||
var refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest
|
||
{
|
||
OutTradeNo = registration.OrderNo,
|
||
OutRefundNo = outRefundNo,
|
||
RefundAmount = AmountHelper.YuanToFen(actualRefundAmount),
|
||
TotalAmount = AmountHelper.YuanToFen(registration.TotalAmount),
|
||
Reason = input.CancelReason
|
||
};
|
||
|
||
_logger.LogInformation("开始执行统一退款:报名ID={RegistrationId}, 订单号={OrderNo}, 退款单号={RefundNo}, 退款金额={Amount}, 退款类型={CancelType}",
|
||
registration.Id, registration.OrderNo, outRefundNo, actualRefundAmount, isPartialCancel ? "部分退款" : "全额退款");
|
||
|
||
var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest);
|
||
if (!refundResult.Success)
|
||
{
|
||
_logger.LogError("统一退款失败:报名ID={RegistrationId}, 错误信息={ErrorMessage}",
|
||
registration.Id, refundResult.Message);
|
||
output.Success = false;
|
||
output.Message = refundResult.Message;
|
||
return output;
|
||
}
|
||
|
||
await CreateRefundRecordAsync(registration, eventInfo, wechatConfig, outRefundNo,
|
||
refundResult.RefundId, actualRefundAmount, registration.TotalAmount, input.CancelReason, operatorUserId);
|
||
|
||
_logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}",
|
||
registration.Id, outRefundNo, actualRefundAmount);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!isPartialCancel || registration.PaymentStatus == 3 || registration.PaymentStatus == 4)
|
||
{
|
||
var orderStatus = registration.PaymentMethod == 1 ? OrderStatus.Cancelled : OrderStatus.Refunded;
|
||
await UpdateOrderStatusAsync(registration.OrderNo, orderStatus, operatorUserId);
|
||
}
|
||
|
||
_logger.LogInformation("统一取消报名完成:报名ID={RegistrationId}, 操作人={UserId}, 退款金额={RefundAmount}, 成功={Success}",
|
||
registration.Id, operatorUserId, actualRefundAmount, output.Success);
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建退款记录
|
||
/// </summary>
|
||
private async Task CreateRefundRecordAsync(EventRegistration registration, EventInfo eventInfo,
|
||
WeChatPayConfig? wechatConfig, string outRefundNo, string? refundId,
|
||
decimal refundAmount, decimal totalAmount, string? reason, long operatorUserId,
|
||
string? payeeName = null, string? payeeBankName = null, string? payeeBankAccount = null)
|
||
{
|
||
// 获取 TransactionId(微信支付交易号)
|
||
string? transactionId = null;
|
||
if (registration.PaymentMethod == 2) // 线上支付
|
||
{
|
||
var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == registration.Id);
|
||
transactionId = payment?.TransactionNo;
|
||
}
|
||
|
||
var refundRecord = new RefundRecord
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
OutRefundNo = outRefundNo,
|
||
RefundId = refundId,
|
||
RegistrationId = registration.Id,
|
||
OutTradeNo = registration.OrderNo,
|
||
TransactionId = transactionId, // 正确的微信支付交易号
|
||
RefundAmount = refundAmount,
|
||
TotalAmount = totalAmount,
|
||
RefundReason = reason,
|
||
RefundStatus = 1,
|
||
ApplyTime = DateTime.Now,
|
||
SuccessTime = DateTime.Now,
|
||
MerchantId = eventInfo.MerchantId,
|
||
WeChatPayConfigId = wechatConfig?.Id,
|
||
PayeeName = payeeName,
|
||
PayeeBankName = payeeBankName,
|
||
PayeeBankAccount = payeeBankAccount,
|
||
Status = AvailabilityStatus.Enable,
|
||
CreatedUserId = operatorUserId,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
await _refundRecordRepository.InsertAsync(refundRecord);
|
||
_logger.LogInformation("退款记录创建成功:退款记录ID={RefundRecordId}, 退款单号={OutRefundNo}",
|
||
refundRecord.Id, outRefundNo);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 软删除报名人员
|
||
/// </summary>
|
||
private async Task SoftDeletePersonsAsync(List<long> personIds, long operatorUserId, DateTime now)
|
||
{
|
||
var personsToDelete = await _registrationPersonRepository.GetListAsync(x => personIds.Contains(x.Id));
|
||
foreach (var person in personsToDelete)
|
||
{
|
||
person.DeleteMark = true;
|
||
person.UpdatedUserId = operatorUserId;
|
||
person.UpdatedTime = now;
|
||
}
|
||
await _registrationPersonRepository.UpdateRangeAsync(personsToDelete);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 软删除报名项目
|
||
/// </summary>
|
||
private async Task SoftDeleteItemsAsync(List<long> itemIds, long operatorUserId, DateTime now)
|
||
{
|
||
var itemsToDelete = await _registrationItemRepository.GetListAsync(x => itemIds.Contains(x.Id));
|
||
foreach (var item in itemsToDelete)
|
||
{
|
||
item.DeleteMark = true;
|
||
item.UpdatedUserId = operatorUserId;
|
||
item.UpdatedTime = now;
|
||
}
|
||
await _registrationItemRepository.UpdateRangeAsync(itemsToDelete);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新订单状态
|
||
/// </summary>
|
||
private async Task UpdateOrderStatusAsync(string orderNo, OrderStatus orderStatus, long operatorUserId)
|
||
{
|
||
if (!string.IsNullOrEmpty(orderNo))
|
||
{
|
||
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == orderNo);
|
||
if (order != null)
|
||
{
|
||
order.OrderStatus = orderStatus;
|
||
order.UpdatedTime = DateTime.Now;
|
||
order.UpdatedUserId = operatorUserId;
|
||
await _orderRepository.UpdateAsync(order);
|
||
_logger.LogInformation("订单状态已同步更新:订单号={OrderNo}, 状态={OrderStatus}",
|
||
orderNo, orderStatus);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("未找到对应的订单记录:订单号={OrderNo}", orderNo);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一后台管理退款方法
|
||
/// </summary>
|
||
/// <param name="input">退款订单输入</param>
|
||
/// <param name="operatorUserId">操作人用户ID</param>
|
||
[UnitOfWork]
|
||
[NonAction]
|
||
public async Task ExecuteAdminRefundAsync(RefundOrderInput input, long operatorUserId)
|
||
{
|
||
var order = await _orderRepository.GetByIdAsync(input.Id);
|
||
if (order == null) throw Oops.Bah("订单不存在");
|
||
|
||
if (order.OrderStatus != OrderStatus.Paid) throw Oops.Bah("订单未支付,无法退款");
|
||
|
||
if (!AmountHelper.IsValidPositiveAmount(input.RefundAmount))
|
||
{
|
||
throw Oops.Bah("退款金额必须大于0且最多保留两位小数");
|
||
}
|
||
|
||
if (input.RefundAmount > order.ActualAmount)
|
||
{
|
||
_logger.LogWarning("统一后台超额退款尝试:订单ID={OrderId}, 实付金额={ActualAmount}, 申请退款金额={RefundAmount}",
|
||
order.Id, order.ActualAmount, input.RefundAmount);
|
||
throw Oops.Bah($"退款金额({input.RefundAmount})不能超过实付金额({order.ActualAmount})");
|
||
}
|
||
|
||
var currentUserId = operatorUserId;
|
||
var now = DateTime.Now;
|
||
bool isPartialCancel = input.CancelType == 1 && input.PersonIds != null && input.PersonIds.Any();
|
||
|
||
if (isPartialCancel)
|
||
{
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId);
|
||
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId);
|
||
var totalPersons = persons.Count;
|
||
if (totalPersons == 0)
|
||
{
|
||
throw Oops.Bah("报名记录中没有队员信息");
|
||
}
|
||
|
||
var selectedPersons = persons.Where(p => input.PersonIds.Contains(p.Id)).ToList();
|
||
if (selectedPersons.Count != input.PersonIds.Count)
|
||
{
|
||
var invalidIds = input.PersonIds.Except(selectedPersons.Select(p => p.Id)).ToList();
|
||
throw Oops.Bah($"选择的人员ID无效:{string.Join(", ", invalidIds)}");
|
||
}
|
||
|
||
_logger.LogInformation("统一后台部分退款:订单ID={OrderId}, 总人数={TotalPersons}, 选择人数={SelectedCount}, 退款金额={RefundAmount}",
|
||
order.Id, totalPersons, selectedPersons.Count, input.RefundAmount);
|
||
|
||
// 软删除报名人员
|
||
await SoftDeletePersonsAsync(input.PersonIds, currentUserId, now);
|
||
_logger.LogInformation("已软删除队员:订单ID={OrderId}, 队员ID列表={PersonIds}",
|
||
order.Id, string.Join(", ", input.PersonIds));
|
||
|
||
// 检查并软删除空的 RegistrationItem(所有成员都被软删除的项)
|
||
var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId && !x.DeleteMark);
|
||
var remainingRegistrationItemIds = new HashSet<long>(remainingPersons.Select(p => p.RegistrationItemId));
|
||
var itemsToDelete = items.Where(item => !remainingRegistrationItemIds.Contains(item.Id)).ToList();
|
||
if (itemsToDelete.Count > 0)
|
||
{
|
||
await SoftDeleteItemsAsync(itemsToDelete.Select(i => i.Id).ToList(), currentUserId, now);
|
||
_logger.LogInformation("已软删除空团队:订单ID={OrderId}, 团队ID列表={ItemIds}",
|
||
order.Id, string.Join(", ", itemsToDelete.Select(i => i.Id)));
|
||
}
|
||
|
||
var remainingPersonsCount = remainingPersons.Count;
|
||
var reg = await _registrationRepository.GetByIdAsync(order.RegistrationId);
|
||
if (reg != null)
|
||
{
|
||
reg.TotalPersonCount = remainingPersonsCount;
|
||
reg.UpdatedTime = now;
|
||
reg.UpdatedUserId = currentUserId;
|
||
await _registrationRepository.UpdateAsync(reg);
|
||
}
|
||
|
||
if (remainingPersonsCount > 0)
|
||
{
|
||
var refundOrder = new YD_Event.Core.Entities.Order
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(),
|
||
OrderType = 2,
|
||
MerchantId = order.MerchantId,
|
||
MerchantCertificationId = order.MerchantCertificationId,
|
||
EventId = order.EventId,
|
||
RegistrationId = order.RegistrationId,
|
||
RelatedOrderId = order.Id,
|
||
OriginalAmount = order.ActualAmount,
|
||
ActualAmount = input.RefundAmount,
|
||
OrderStatus = OrderStatus.Refunded,
|
||
PaymentTime = now,
|
||
Remark = input.Reason,
|
||
CreatedTime = now,
|
||
CreatedUserId = currentUserId
|
||
};
|
||
await _orderRepository.InsertAsync(refundOrder);
|
||
|
||
_logger.LogInformation("统一后台部分退款成功:订单ID={OrderId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}, 剩余人数={RemainingCount}, 操作人={UserId}",
|
||
order.Id, refundOrder.OrderNo, input.RefundAmount, remainingPersonsCount, currentUserId);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!isPartialCancel)
|
||
{
|
||
var existingRefund = await _orderRepository.IsAnyAsync(o => o.RelatedOrderId == order.Id && o.OrderType == 2);
|
||
if (existingRefund)
|
||
{
|
||
throw Oops.Bah("该订单已存在退款记录");
|
||
}
|
||
|
||
// 软删除所有报名人员
|
||
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId);
|
||
if (persons.Count > 0)
|
||
{
|
||
await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), currentUserId, now);
|
||
_logger.LogInformation("已软删除所有队员:订单ID={OrderId}", order.Id);
|
||
}
|
||
|
||
// 软删除所有报名项目
|
||
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId);
|
||
if (items.Count > 0)
|
||
{
|
||
await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), currentUserId, now);
|
||
_logger.LogInformation("已软删除所有项目:订单ID={OrderId}", order.Id);
|
||
}
|
||
}
|
||
|
||
var fullRefundOrder = new YD_Event.Core.Entities.Order
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(),
|
||
OrderType = 2,
|
||
MerchantId = order.MerchantId,
|
||
MerchantCertificationId = order.MerchantCertificationId,
|
||
EventId = order.EventId,
|
||
RegistrationId = order.RegistrationId,
|
||
RelatedOrderId = order.Id,
|
||
OriginalAmount = order.ActualAmount,
|
||
ActualAmount = input.RefundAmount,
|
||
OrderStatus = OrderStatus.Refunded,
|
||
PaymentTime = now,
|
||
Remark = input.Reason,
|
||
CreatedTime = now,
|
||
CreatedUserId = currentUserId
|
||
};
|
||
await _orderRepository.InsertAsync(fullRefundOrder);
|
||
|
||
_logger.LogInformation("统一后台退款成功:订单ID={OrderId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}, 退款类型={RefundType}, 操作人={UserId}",
|
||
order.Id, fullRefundOrder.OrderNo, input.RefundAmount, isPartialCancel ? "部分退款" : "全额退款", currentUserId);
|
||
|
||
order.OrderStatus = OrderStatus.Refunded;
|
||
order.UpdatedTime = now;
|
||
order.UpdatedUserId = currentUserId;
|
||
await _orderRepository.UpdateAsync(order);
|
||
|
||
var registration = await _registrationRepository.GetByIdAsync(order.RegistrationId);
|
||
if (registration != null)
|
||
{
|
||
registration.PaymentStatus = 3;
|
||
registration.UpdatedTime = now;
|
||
registration.UpdatedUserId = currentUserId;
|
||
await _registrationRepository.UpdateAsync(registration);
|
||
}
|
||
|
||
var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == order.RegistrationId);
|
||
if (payment != null)
|
||
{
|
||
payment.RefundStatus = 1;
|
||
payment.UpdatedTime = now;
|
||
payment.UpdatedUserId = currentUserId;
|
||
await _paymentRepository.UpdateAsync(payment);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|