YD_Event.Admin/YD_Event.Application/Event/UnifiedRegistrationService.cs

2589 lines
108 KiB
C#
Raw Normal View History

2026-04-13 00:40:44 +08:00
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using YD_Event.Application.Event.Dtos;
2026-04-15 09:58:05 +08:00
using YD_Event.Application.Order.Dtos;
using YD_Event.Application.Refund;
2026-04-13 00:40:44 +08:00
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;
2026-04-13 03:44:49 +08:00
using YD_Event.Core.Extensions;
2026-04-13 00:40:44 +08:00
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;
2026-04-13 15:28:25 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.CouponRedemptionCode> _redemptionCodeRepository;
2026-04-13 00:40:44 +08:00
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;
2026-04-15 09:58:05 +08:00
private readonly RefundConfigService _refundConfigService;
private readonly ISqlSugarRepository<RefundRecord> _refundRecordRepository;
2026-04-13 00:40:44 +08:00
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,
2026-04-13 15:28:25 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.CouponRedemptionCode> redemptionCodeRepository,
2026-04-13 00:40:44 +08:00
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
ISqlSugarClient db,
IIdGenerator idGenerator,
WeChatPayService weChatPayService,
IOptionsMonitor<WechatTenpayClientOptions> wechatTenpayClientOptionsMonitor,
ILogger<UnifiedRegistrationService> logger,
2026-04-15 09:58:05 +08:00
OrderNoGeneratorService orderNoGeneratorService,
RefundConfigService refundConfigService,
ISqlSugarRepository<RefundRecord> refundRecordRepository)
2026-04-13 00:40:44 +08:00
{
_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;
2026-04-13 15:28:25 +08:00
_redemptionCodeRepository = redemptionCodeRepository;
2026-04-13 00:40:44 +08:00
_paymentRepository = paymentRepository;
_merchantCertificationRepository = merchantCertificationRepository;
_db = db;
_idGenerator = idGenerator;
_weChatPayService = weChatPayService;
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
_logger = logger;
_orderNoGeneratorService = orderNoGeneratorService;
2026-04-15 09:58:05 +08:00
_refundConfigService = refundConfigService;
_refundRecordRepository = refundRecordRepository;
2026-04-13 00:40:44 +08:00
}
/// <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);
2026-04-13 00:40:44 +08:00
// 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);
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.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)
{
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 = 0,
RegistrationTime = DateTime.Now,
CreatedUserId = input.CreatedUserId,
CreatedTime = DateTime.Now,
Status = AvailabilityStatus.Enable,
OrderNo = string.Empty
};
await _registrationRepository.InsertAsync(registration);
return registration;
}
2026-04-13 00:40:44 +08:00
/// <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("赛事信息不存在");
}
2026-04-13 18:14:32 +08:00
// 3. 检查是否已存在订单(幂等性处理)
var existingOrder = await _orderRepository.GetFirstAsync(o =>
o.RegistrationId == registration.Id && !o.DeleteMark);
decimal originalAmount, discountAmount, finalAmount;
YD_Event.Core.Entities.Coupon? usedCoupon = null;
YD_Event.Core.Entities.UserCoupon? usedUserCoupon = null;
// 如果订单已存在,使用订单中保存的金额,确保参数一致性
if (existingOrder != null)
{
_logger.LogInformation("找到已存在订单,报名ID: {RegistrationId}, 订单号: {OrderNo}",
registration.Id, existingOrder.OrderNo);
originalAmount = existingOrder.OriginalAmount;
discountAmount = existingOrder.DiscountAmount;
finalAmount = existingOrder.ActualAmount;
// 确保报名记录的订单号与订单一致
if (string.IsNullOrEmpty(registration.OrderNo))
{
registration.OrderNo = existingOrder.OrderNo;
await _registrationRepository.UpdateAsync(registration);
}
}
else
{
// 订单不存在,计算优惠后金额
var calculateResult = await CalculateFinalAmountAsync(
registration.TotalAmount, input.CouponId, input.OperatorUserId);
originalAmount = calculateResult.originalAmount;
discountAmount = calculateResult.discountAmount;
finalAmount = calculateResult.finalAmount;
usedCoupon = calculateResult.coupon;
usedUserCoupon = calculateResult.userCoupon;
}
2026-04-13 00:40:44 +08:00
// 4. 构建支付输出对象
var output = new UnifiedPaymentOutput
{
RegistrationId = registration.Id,
OrderNo = registration.OrderNo,
OriginalAmount = originalAmount,
DiscountAmount = discountAmount,
ActualAmount = finalAmount,
2026-04-13 18:14:32 +08:00
PaymentStatus = registration.PaymentStatus,
2026-04-13 00:40:44 +08:00
IsFree = finalAmount <= 0,
CouponRuleNote = usedCoupon != null ? "优惠券使用后不予退还" : null
};
// 5. 处理免费情况
if (finalAmount <= 0)
{
2026-04-13 18:14:32 +08:00
if (registration.PaymentStatus != 1)
{
await HandleFreePaymentAsync(registration, usedUserCoupon, input.OperatorUserId);
}
2026-04-13 00:40:44 +08:00
output.PaymentStatus = 1;
return output;
}
2026-04-13 18:14:32 +08:00
// 6. 生成订单号(仅在不存在时生成)
2026-04-13 00:40:44 +08:00
if (string.IsNullOrEmpty(registration.OrderNo))
{
registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
output.OrderNo = registration.OrderNo;
}
2026-04-13 18:14:32 +08:00
else
{
output.OrderNo = registration.OrderNo;
}
2026-04-13 06:58:21 +08:00
// 处理支付方式转换逻辑
registration.PaymentMethod = GetPaymentMethodByType(input.PaymentMethodType) ?? input.PaymentMethod;
2026-04-13 06:58:21 +08:00
registration.PaymentMethodType = input.PaymentMethodType;
// 设置支付状态
registration.PaymentStatus = registration.PaymentStatus == 1 ? 1 : 0;
2026-04-13 06:58:21 +08:00
2026-04-13 00:40:44 +08:00
await _registrationRepository.UpdateAsync(registration);
2026-04-13 18:14:32 +08:00
// 7. 创建订单记录(仅在不存在时创建)
if (existingOrder == null)
{
await CreateOrderAsync(registration, eventInfo, originalAmount, discountAmount, finalAmount,
input.CouponId, input.OperatorUserId);
}
2026-04-13 00:40:44 +08:00
// 8. 根据支付方式处理
if (input.PaymentMethod == 2) // 线上支付
{
2026-04-13 18:14:32 +08:00
// 检查是否已经有支付记录或已支付状态,避免重复调用微信支付
var existingPayment = await _paymentRepository.GetFirstAsync(p =>
p.RegistrationId == registration.Id && !p.DeleteMark);
if (registration.PaymentStatus == 1 || existingPayment != null)
{
_logger.LogInformation("订单已支付或已有支付记录,跳过微信统一下单,报名ID: {RegistrationId}",
registration.Id);
output.PaymentStatus = registration.PaymentStatus;
return output;
}
2026-04-13 00:40:44 +08:00
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, "赛事不存在"));
2026-04-13 00:40:44 +08:00
return result;
}
2026-04-13 18:14:32 +08:00
// 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. 验证报名时间
2026-04-13 00:40:44 +08:00
var now = DateTime.Now;
if (now < eventInfo.RegistrationStartTime || now > eventInfo.RegistrationEndTime)
{
result.IsValid = false;
result.ErrorMessage = "不在报名时间内";
result.Details.Add(CreateValidationDetail("时间验证", false, "不在报名时间内"));
2026-04-13 00:40:44 +08:00
return result;
}
// 4. 验证项目选择(一阶段:单选机制,每个项目只能选择一个组别)
2026-04-13 00:40:44 +08:00
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. 验证队员分配(二阶段:人数校验、资质验证、年龄跨组等)
2026-04-13 00:40:44 +08:00
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. 验证报名限制
2026-04-13 00:40:44 +08:00
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)));
2026-04-13 00:40:44 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("整体验证", true, "验证通过"));
2026-04-13 00:40:44 +08:00
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));
2026-04-13 00:40:44 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("项目单选机制", true, "每个项目仅选择一个组别,符合要求"));
2026-04-13 00:40:44 +08:00
return result;
}
/// <summary>
/// 验证队员分配(二阶段)
/// </summary>
private 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();
2026-04-13 00:40:44 +08:00
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
2026-04-13 13:23:00 +08:00
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));
2026-04-13 00:40:44 +08:00
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;
}
2026-04-13 00:40:44 +08:00
2026-04-13 13:23:00 +08:00
var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
var isTeamProject = baseProject?.IsTeamProject() ?? false;
if (!item.GroupId.HasValue)
2026-04-13 00:40:44 +08:00
{
result.IsValid = false;
result.ErrorMessage = $"项目组别不能为空";
result.Details.Add(CreateValidationDetail("组别验证", false, result.ErrorMessage));
2026-04-13 00:40:44 +08:00
return result;
}
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
if (eventProjectGroup == null)
2026-04-13 05:20:09 +08:00
{
result.IsValid = false;
result.ErrorMessage = $"项目组别不存在: {item.GroupId.Value}";
result.Details.Add(CreateValidationDetail("组别验证", false, result.ErrorMessage));
return result;
2026-04-13 05:20:09 +08:00
}
var teamsToValidate = GetTeamsFromItem(item);
if (!teamsToValidate.Any())
2026-04-13 00:40:44 +08:00
{
result.IsValid = false;
2026-04-13 05:20:09 +08:00
result.ErrorMessage = $"请为项目添加至少一个团队和队员";
result.Details.Add(CreateValidationDetail("团队验证", false, result.ErrorMessage));
2026-04-13 00:40:44 +08:00
return result;
}
2026-04-13 05:20:09 +08:00
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));
2026-04-13 05:20:09 +08:00
return result;
}
2026-04-13 13:23:00 +08:00
var qualificationResult = ValidatePersonQualifications(team.Persons, eventProjectGroup, isTeamProject);
2026-04-13 05:20:09 +08:00
if (!qualificationResult.IsValid)
{
result.IsValid = false;
result.ErrorMessage = qualificationResult.ErrorMessage;
result.Details.AddRange(qualificationResult.Details);
return result;
}
result.Details.AddRange(qualificationResult.Details);
}
2026-04-13 00:40:44 +08:00
}
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;
}
2026-04-13 00:40:44 +08:00
/// <summary>
/// 验证队员资质(年龄和性别)
/// </summary>
2026-04-13 13:23:00 +08:00
public RegistrationValidationResult ValidatePersonQualifications(List<UnifiedPersonInput> persons, EventProjectGroup group, bool isTeamProject)
2026-04-13 00:40:44 +08:00
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List<ValidationDetail>()
};
// 首先验证性别,发现问题立即返回
var genderError = ValidatePersonGenders(persons, group);
if (genderError != null)
2026-04-13 00:40:44 +08:00
{
return genderError;
}
2026-04-13 00:40:44 +08:00
// 验证年龄限制和跨组规则
var ageError = ValidatePersonAges(persons, group, isTeamProject, result);
if (ageError != null)
{
return ageError;
2026-04-13 03:44:49 +08:00
}
result.Details.Add(CreateValidationDetail("队员资质", true, "所有队员的性别符合组别要求,年龄符合规则"));
return result;
}
/// <summary>
/// 验证队员性别
/// </summary>
private static RegistrationValidationResult? ValidatePersonGenders(List<UnifiedPersonInput> persons, EventProjectGroup group)
{
if (group.Gender == Gender.All)
2026-04-13 03:44:49 +08:00
{
return null;
2026-04-13 00:40:44 +08:00
}
foreach (var person in persons)
2026-04-13 00:40:44 +08:00
{
if (person.Gender != group.Gender)
2026-04-13 00:40:44 +08:00
{
var errorMessage = $"队员{person.Name}的性别不符合组别要求。要求:{group.Gender.Description()},实际:{person.Gender.Description()}";
return new RegistrationValidationResult
2026-04-13 13:23:00 +08:00
{
IsValid = false,
ErrorMessage = errorMessage,
Details = new List<ValidationDetail> { CreateValidationDetail("性别验证", false, errorMessage) }
};
2026-04-13 00:40:44 +08:00
}
}
return null;
}
2026-04-13 00:40:44 +08:00
/// <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;
2026-04-13 00:40:44 +08:00
}
// 统计年龄状态
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)
2026-04-13 00:40:44 +08:00
{
string errorMessage = allBelow
? "跨组报名时,团队中所有成员年龄均小于目标年龄段,团队中至少需要有一名成员属于目标年龄段"
: "跨组报名时,团队中至少需要有一名成员属于目标年龄段";
string itemName = allBelow
? "年龄跨组验证(全部小于目标年龄)"
: "年龄跨组验证";
result.IsValid = false;
result.ErrorMessage = errorMessage;
result.Details.Add(CreateValidationDetail(itemName, false, errorMessage));
return result;
2026-04-13 00:40:44 +08:00
}
// 添加验证通过的详情
result.Details.Add(hasBelow
? CreateValidationDetail("年龄跨组", true, "符合年龄跨组报名要求")
: CreateValidationDetail("年龄验证", true, "所有队员均在目标年龄段内"));
2026-04-13 03:44:49 +08:00
return null;
2026-04-13 00:40:44 +08:00
}
/// <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();
2026-04-13 00:40:44 +08:00
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));
2026-04-13 00:40:44 +08:00
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark);
var regItems = new List<EventRegistrationItem>();
var persons = new List<EventRegistrationPerson>();
int totalPersonCount = 0;
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);
2026-04-13 00:40:44 +08:00
2026-04-13 05:20:09 +08:00
foreach (var team in teamsToProcess)
2026-04-13 00:40:44 +08:00
{
2026-04-13 05:20:09 +08:00
var (unitPrice, totalPrice) = CalculateItemPrice(feeConfig, eventProject, team.Persons.Count);
var regItemId = _idGenerator.NewLong();
var regItem = new EventRegistrationItem
2026-04-13 00:40:44 +08:00
{
2026-04-13 05:20:09 +08:00
Id = regItemId,
2026-04-13 00:40:44 +08:00
RegistrationId = registration.Id,
ProjectId = item.ProjectId,
EventProjectGroupId = item.GroupId,
ProjectName = baseProject?.Name ?? "",
2026-04-13 05:20:09 +08:00
GroupName = groupName,
ProjectTypeName = baseProject?.ProjectType.Description() ?? "",
UnitPrice = unitPrice,
PersonCount = team.Persons.Count,
TotalPrice = totalPrice,
2026-04-13 00:40:44 +08:00
CreatedUserId = input.CreatedUserId,
CreatedTime = DateTime.Now
2026-04-13 05:20:09 +08:00
};
regItems.Add(regItem);
totalPersonCount += team.Persons.Count;
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,
2026-04-13 06:58:21 +08:00
UserId = pInput.UserId ?? 0,
2026-04-13 05:20:09 +08:00
CreatedUserId = input.CreatedUserId,
CreatedTime = DateTime.Now
});
}
2026-04-13 00:40:44 +08:00
}
}
await _registrationItemRepository.InsertRangeAsync(regItems);
await _registrationPersonRepository.InsertRangeAsync(persons);
var totalAmount = CalculateTotalAmount(feeConfig, eventProjects, input.Items, totalPersonCount);
return (totalAmount, totalPersonCount);
}
/// <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);
2026-04-13 00:40:44 +08:00
}
}
}
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
};
}
2026-04-13 00:40:44 +08:00
/// <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.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);
2026-04-13 00:40:44 +08:00
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)
2026-04-13 00:40:44 +08:00
{
return await _weChatPayConfigRepository.GetFirstAsync(x =>
x.MerchantId == eventInfo.MerchantId.Value && x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
}
return null;
}
2026-04-13 00:40:44 +08:00
/// <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")
2026-04-13 00:40:44 +08:00
{
_logger.LogInformation("订单已在微信侧支付成功,更新本地状态,订单号:{OrderNo}", registration.OrderNo);
await HandlePaymentSuccessAsync(
registration.OrderNo,
queryResult.TransactionId,
queryResult.SuccessTime?.DateTime,
input.OperatorUserId);
output.PaymentStatus = 1;
return output;
2026-04-13 00:40:44 +08:00
}
// 如果订单已存在但未支付,我们需要确认是否可以重试
// 微信支付允许使用相同订单号重试,但参数必须完全一致
// 为了确保参数一致性,我们使用查询到的信息来验证
if (queryResult.TradeState == "NOTPAY" || queryResult.TradeState == "CLOSED")
2026-04-13 00:40:44 +08:00
{
_logger.LogInformation("订单状态为 {TradeState},将重新调用统一下单,订单号:{OrderNo}",
queryResult.TradeState, registration.OrderNo);
2026-04-13 00:40:44 +08:00
}
}
// 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)
2026-04-13 00:40:44 +08:00
{
_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);
2026-04-13 00:40:44 +08:00
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")
2026-04-13 00:40:44 +08:00
{
_logger.LogInformation("订单已在微信侧支付成功,更新本地状态,订单号:{OrderNo}", registration.OrderNo);
await HandlePaymentSuccessAsync(
registration.OrderNo,
queryResult.TransactionId,
queryResult.SuccessTime?.DateTime,
registration.CreatedUserId);
output.PaymentStatus = 1;
return output;
2026-04-13 00:40:44 +08:00
}
// 如果订单已存在但未支付,我们需要确认是否可以重试
if (queryResult.TradeState == "NOTPAY" || queryResult.TradeState == "CLOSED")
2026-04-13 00:40:44 +08:00
{
_logger.LogInformation("订单状态为 {TradeState},将重新调用统一下单,订单号:{OrderNo}",
queryResult.TradeState, registration.OrderNo);
2026-04-13 00:40:44 +08:00
}
}
// 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);
2026-04-13 00:40:44 +08:00
return output;
}
/// <summary>
/// 处理线下支付
/// </summary>
private async Task<UnifiedPaymentOutput> HandleOfflinePaymentAsync(EventRegistration registration, UnifiedPaymentInput input)
{
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>
2026-04-13 13:23:00 +08:00
public async Task<RegistrationLimitCheckOutput> CheckRegistrationLimitAsync(UnifiedRegistrationInput input)
2026-04-13 00:40:44 +08:00
{
var result = new RegistrationLimitCheckOutput
{
HasLimit = false,
LimitDetails = new List<RegistrationLimitDetail>()
};
2026-04-13 13:23:00 +08:00
_logger.LogInformation("开始检查报名限制,赛事ID: {EventId}", input.EventId);
2026-04-13 00:40:44 +08:00
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);
2026-04-13 03:44:49 +08:00
// 先获取所有报名信息,供后续检查使用
2026-04-14 23:34:16 +08:00
// 只统计已支付的报名记录(PaymentStatus == 1),避免未支付/已取消的报名占用名额
2026-04-13 03:44:49 +08:00
var eventRegistrations = await _registrationRepository.GetListAsync(x =>
2026-04-14 23:34:16 +08:00
x.EventId == input.EventId && x.PaymentStatus == 1 && !x.DeleteMark);
2026-04-13 03:44:49 +08:00
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);
2026-04-13 13:23:00 +08:00
_logger.LogInformation("获取到赛事报名记录: {RegistrationCount},报名人员: {PersonCount},报名项目: {ItemCount}",
eventRegistrations.Count, registrationPersons.Count, allRegistrationItems.Count);
// 检查各类型限制
CheckTeamGroupCapacityLimitAsync(input, result, projectLimits, eventProjects, projects, eventProjectGroups);
await CheckMemberProjectLimitAsync(input, result, limit, eventProjects, projects, registrationPersons, allRegistrationItems);
CheckPersonProjectDuplicateAsync(input, result, eventProjects, projects, eventProjectGroups);
CheckPersonAlreadyRegisteredAsync(input, result, eventProjects, projects, eventProjectGroups, registrationPersons, allRegistrationItems, eventRegistrationIds);
2026-04-13 00:40:44 +08:00
_logger.LogInformation("报名限制检查完成,是否存在限制: {HasLimit}, 限制详情数: {LimitCount}",
result.HasLimit, result.LimitDetails.Count);
2026-04-13 00:40:44 +08:00
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
};
}
2026-04-13 00:40:44 +08:00
/// <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)
{
var teamGroupCountDict = new Dictionary<long, int>();
foreach (var item in input.Items)
{
if (!item.GroupId.HasValue) continue;
var teams = GetTeamsFromItem(item);
var teamCountForItem = teams.Count;
if (teamCountForItem > 0)
{
if (teamGroupCountDict.ContainsKey(item.GroupId.Value))
2026-04-13 00:40:44 +08:00
{
teamGroupCountDict[item.GroupId.Value] += teamCountForItem;
}
else
{
teamGroupCountDict[item.GroupId.Value] = teamCountForItem;
2026-04-13 00:40:44 +08:00
}
}
}
if (!teamGroupCountDict.Any())
{
return;
}
2026-04-13 00:40:44 +08:00
foreach (var groupId in teamGroupCountDict.Keys)
2026-04-13 00:40:44 +08:00
{
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == groupId);
if (eventProjectGroup == null) continue;
2026-04-13 00:40:44 +08:00
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
if (eventProject == null) continue;
2026-04-13 00:40:44 +08:00
var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject.Id);
if (projectLimit == null) continue;
2026-04-13 00:40:44 +08:00
var submittedTeamCount = teamGroupCountDict[groupId];
if (submittedTeamCount <= projectLimit.LimitCount) continue;
2026-04-13 00:40:44 +08:00
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
var projectName = project?.Name ?? "未知项目";
_logger.LogWarning("组别容量超限,项目: {ProjectName},组别: {GroupName},限报: {LimitCount},提交: {SubmittedCount}",
projectName, eventProjectGroup.GroupName, projectLimit.LimitCount, submittedTeamCount);
result.HasLimit = true;
result.LimitDetails.Add(CreateLimitDetail(
0,
eventProjectGroup.GroupName ?? "未知组别",
"ProjectGroupCapacity",
$"项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"限报{projectLimit.LimitCount}队,本次提交{submittedTeamCount}队,超出限报配置"));
}
}
2026-04-13 00:40:44 +08:00
/// <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;
}
2026-04-13 13:23:00 +08:00
var members = await _userRepository.GetListAsync(x => memberIds.Contains(x.Id) && !x.DeleteMark);
_logger.LogInformation("检查报名限制的成员数量: {MemberCount}", members.Count);
2026-04-13 00:40:44 +08:00
foreach (var member in members)
{
_logger.LogDebug("检查成员报名限制,成员ID: {MemberId}, 姓名: {MemberName}", member.Id, member.Name);
var (individualProjectCount, teamProjectCount) = CalculateMemberProjectCounts(
2026-04-15 09:58:05 +08:00
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>
2026-04-15 09:58:05 +08:00
/// 计算成员项目报名数量(包含本次提交的项目)
/// </summary>
private (int individualCount, int teamCount) CalculateMemberProjectCounts(
long memberId,
List<EventProject> eventProjects,
List<YD_Event.Core.Entities.Project> projects,
List<EventRegistrationPerson> registrationPersons,
2026-04-15 09:58:05 +08:00
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;
2026-04-15 09:58:05 +08:00
// 统计已存在的报名项目
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++;
2026-04-13 00:40:44 +08:00
}
}
2026-04-15 09:58:05 +08:00
// 统计本次提交的项目,确保不重复统计
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)
{
2026-04-15 09:58:05 +08:00
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}已报名{individualProjectCount}个个人项目,达到个人项目限报{limit.IndividualProjectLimit}个"));
}
/// <summary>
/// 检查团队项目限报
/// </summary>
private void CheckTeamProjectLimit(
RegistrationLimitCheckOutput result,
EventRegistrationLimit limit,
YD_Event.Core.Entities.AppUser member,
int teamProjectCount)
{
2026-04-15 09:58:05 +08:00
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}已参与{teamProjectCount}个团队项目,达到团队项目限报{limit.TeamProjectLimit}个"));
}
/// <summary>
/// 检查人员项目组别重复
/// </summary>
private void CheckPersonProjectDuplicateAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List<EventProject> eventProjects,
List<YD_Event.Core.Entities.Project> projects,
List<EventProjectGroup> eventProjectGroups)
{
2026-04-13 03:44:49 +08:00
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)
2026-04-13 03:44:49 +08:00
{
2026-04-13 13:23:00 +08:00
var key = person.UserId.HasValue ? person.UserId.Value.ToString() : person.IdCardNumber;
if (string.IsNullOrEmpty(key)) continue;
2026-04-13 03:44:49 +08:00
2026-04-13 13:23:00 +08:00
if (!personProjectGroupMap.ContainsKey(key))
2026-04-13 03:44:49 +08:00
{
2026-04-13 13:23:00 +08:00
personProjectGroupMap[key] = new List<(long GroupId, string GroupName, string ProjectName)>();
2026-04-13 03:44:49 +08:00
}
2026-04-13 13:23:00 +08:00
personProjectGroupMap[key].Add((item.GroupId.Value, eventProjectGroup.GroupName ?? "未知组别", projectName));
2026-04-13 03:44:49 +08:00
}
}
foreach (var kvp in personProjectGroupMap)
{
2026-04-13 13:23:00 +08:00
var key = kvp.Key;
2026-04-13 03:44:49 +08:00
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)
2026-04-13 03:44:49 +08:00
{
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}\"中重复出现,同一人员只能在一个团队中报名同一项目组别"));
2026-04-13 03:44:49 +08:00
}
}
}
/// <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;
}
2026-04-13 03:44:49 +08:00
foreach (var item in input.Items)
2026-04-13 03:44:49 +08:00
{
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)
2026-04-13 03:44:49 +08:00
{
if (!person.UserId.HasValue) continue;
2026-04-13 03:44:49 +08:00
var existingPerson = existingPersons.FirstOrDefault(ep => ep.UserId == person.UserId.Value);
if (existingPerson == null) continue;
_logger.LogWarning("人员已报名该项目组别,人员: {PersonName}, 项目: {ProjectName}, 组别: {GroupName}",
person.Name, projectName, eventProjectGroup.GroupName);
2026-04-13 03:44:49 +08:00
result.HasLimit = true;
result.LimitDetails.Add(CreateLimitDetail(
0,
person.Name,
"PersonAlreadyRegistered",
$"人员\"{person.Name}\"已在项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"中报名过,不能重复报名"));
2026-04-13 03:44:49 +08:00
}
}
2026-04-13 00:40:44 +08:00
}
/// <summary>
/// 获取组别名称
/// </summary>
private static string GetGroupName(UnifiedRegistrationItemInput item, List<EventProjectGroup> eventProjectGroups)
2026-04-13 00:40:44 +08:00
{
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
2026-04-13 00:40:44 +08:00
{
ChargeMode.Free => 0,
ChargeMode.FixedPrice or ChargeMode.UnitPriceEvent => feeConfig.Price,
ChargeMode.UnitPriceProject => eventProject.Price,
_ => 0
};
2026-04-13 00:40:44 +08:00
var totalPrice = unitPrice * personCount;
2026-04-13 00:40:44 +08:00
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);
2026-04-13 00:40:44 +08:00
foreach (var item in items)
{
if (eventProjectsDict.TryGetValue(item.ProjectId, out var ep))
2026-04-13 00:40:44 +08:00
{
pricingInput.Projects.Add(new ProjectPricingInfo
{
ProjectId = ep.Id,
UnitPrice = ep.Price
});
}
}
var pricingResult = EventPricingService.Calculate(pricingInput);
return pricingResult.TotalAmount;
}
2026-04-13 18:14:32 +08:00
/// <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));
int totalParticipantCount = items.Sum(i => i.PersonCount);
var unifiedItems = items.Select(i => new UnifiedRegistrationItemInput
{
ProjectId = i.ProjectId,
Persons = new List<UnifiedPersonInput>(new UnifiedPersonInput[i.PersonCount])
}).ToList();
var totalAmount = CalculateTotalAmount(feeConfig, eventProjects, unifiedItems, totalParticipantCount);
var eventProjectsDict = eventProjects.ToDictionary(p => p.Id);
var baseProjectsDict = baseProjects.ToDictionary(p => p.Id);
2026-04-13 18:14:32 +08:00
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));
2026-04-13 18:14:32 +08:00
}
return (totalAmount, evt.ChargeMode, result);
}
/// <summary>
/// 计算金额输入项(简化版)
/// </summary>
public class CalculateAmountInputItem
{
public long ProjectId { get; set; }
public int PersonCount { get; set; }
}
2026-04-13 00:40:44 +08:00
/// <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;
}
_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} 状态已更新为已支付");
2026-04-14 17:30:20 +08:00
// 支付成功后,标记优惠券为已使用
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}");
}
}
2026-04-13 00:40:44 +08:00
}
}
/// <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);
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;
}
2026-04-13 15:28:25 +08:00
/// <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);
2026-04-13 15:28:25 +08:00
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;
}
2026-04-13 18:14:32 +08:00
/// <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, "赛事费用配置不存在"));
2026-04-13 18:14:32 +08:00
return result;
}
// 验证费用配置值是否有效
if (eventInfo.ChargeMode == ChargeMode.FixedPrice && feeConfig.Price <= 0)
{
result.IsValid = false;
result.ErrorMessage = "赛事费用配置不正确,请联系管理员";
result.Details.Add(CreateValidationDetail("费用配置验证", false, "定价模式费用配置不正确"));
2026-04-13 18:14:32 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("费用配置验证", true, "费用配置验证通过"));
2026-04-13 18:14:32 +08:00
}
else
{
result.Details.Add(CreateValidationDetail("费用配置验证", true, "免费赛事,无需费用配置"));
2026-04-13 18:14:32 +08:00
}
// 2. 验证赛事项目是否存在
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventInfo.Id);
if (!eventProjects.Any())
{
result.IsValid = false;
result.ErrorMessage = "赛事未配置任何项目,请联系管理员";
result.Details.Add(CreateValidationDetail("项目配置验证", false, "赛事项目配置不存在"));
2026-04-13 18:14:32 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("项目配置验证", true, $"赛事配置了{eventProjects.Count}个项目"));
2026-04-13 18:14:32 +08:00
// 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, "赛事关联的商户不存在"));
2026-04-13 18:14:32 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("商户配置验证", true, "商户配置验证通过"));
2026-04-13 18:14:32 +08:00
}
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, "平台支付配置不存在"));
2026-04-13 18:14:32 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("支付配置验证", true, "平台支付配置验证通过"));
2026-04-13 18:14:32 +08:00
}
}
result.Details.Add(CreateValidationDetail("配置验证", true, "所有报名相关配置验证通过"));
2026-04-13 18:14:32 +08:00
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, "用户未通过单位认证"));
2026-04-13 18:14:32 +08:00
return result;
}
result.Details.Add(CreateValidationDetail("单位认证验证", true, "用户单位认证验证通过"));
2026-04-13 18:14:32 +08:00
return result;
}
2026-04-15 09:58:05 +08:00
#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 refundableAmount = await CalculateRefundAmountAsync(registration.TotalAmount, eventInfo.MatchStartTime);
_logger.LogInformation("统一取消报名预览:报名ID={RegistrationId}, 赛事名称={EventName}, 实际支付={ActualAmount}, 可退款={RefundAmount}",
registrationId, eventInfo?.EventName, registration.TotalAmount, refundableAmount);
var output = new CancelPreviewOutput
{
RegistrationId = registration.Id,
EventName = eventInfo?.EventName ?? "",
TeamName = registration.TeamName ?? "",
ChargeMode = registration.TotalAmount > 0 ? 1 : 0,
ActualPaidAmount = registration.TotalAmount,
RefundableAmount = refundableAmount
};
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
});
}
return output;
}
/// <summary>
/// 统一取消报名执行方法
/// </summary>
/// <param name="input">取消执行输入</param>
/// <param name="operatorUserId">操作人用户ID</param>
/// <returns>取消执行输出</returns>
[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.PaymentStatus != 1) throw Oops.Bah("未支付的订单无法退款");
var now = DateTime.Now;
var eventInfo = await _eventRepository.GetFirstAsync(e => e.Id == registration.EventId);
var baseRefundAmount = await CalculateRefundAmountAsync(registration.TotalAmount, eventInfo.MatchStartTime);
_logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 总金额={TotalAmount}, 按规则计算后金额={BaseRefundAmount}",
registration.Id, registration.TotalAmount, baseRefundAmount);
decimal actualRefundAmount = baseRefundAmount;
bool isPartialCancel = input.CancelType == 1 && input.PersonIds != null && input.PersonIds.Any();
if (isPartialCancel)
{
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id);
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)}");
}
var perPersonRefund = baseRefundAmount / totalPersons;
actualRefundAmount = perPersonRefund * selectedPersons.Count;
_logger.LogInformation("统一部分退款:报名ID={RegistrationId}, 总人数={TotalPersons}, 选择人数={SelectedCount}, 单人退款金额={PerPersonRefund}, 实际退款金额={ActualRefundAmount}",
registration.Id, totalPersons, selectedPersons.Count, perPersonRefund, actualRefundAmount);
await _registrationPersonRepository.DeleteAsync(x => input.PersonIds.Contains(x.Id));
_logger.LogInformation("已删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}",
registration.Id, string.Join(", ", input.PersonIds));
var remainingPersonsCount = await _registrationPersonRepository.CountAsync(x => x.RegistrationId == registration.Id);
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);
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.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0)
{
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();
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;
output.RefundAmount = 0;
output.RefundNo = null;
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)
{
await UpdateOrderStatusAsync(registration.OrderNo, OrderStatus.Refunded, 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)
{
var refundRecord = new RefundRecord
{
Id = _idGenerator.NewLong(),
OutRefundNo = outRefundNo,
RefundId = refundId,
RegistrationId = registration.Id,
OutTradeNo = registration.OrderNo,
TransactionId = registration.PaymentProofUrl,
RefundAmount = refundAmount,
TotalAmount = totalAmount,
RefundReason = reason,
RefundStatus = 1,
ApplyTime = DateTime.Now,
SuccessTime = DateTime.Now,
MerchantId = eventInfo.MerchantId,
WeChatPayConfigId = wechatConfig.Id,
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 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 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 _registrationPersonRepository.DeleteAsync(x => input.PersonIds.Contains(x.Id));
_logger.LogInformation("已删除队员:订单ID={OrderId}, 队员ID列表={PersonIds}",
order.Id, string.Join(", ", input.PersonIds));
var remainingPersonsCount = await _registrationPersonRepository.CountAsync(x => x.RegistrationId == order.RegistrationId);
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 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
2026-04-13 00:40:44 +08:00
}