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;
using EasyCaching.Core;
namespace YD_Event.Application.Event;
///
/// 统一报名处理服务
///
public class UnifiedRegistrationService : ITransient
{
private readonly ISqlSugarRepository _eventRepository;
private readonly ISqlSugarRepository _unitRepository;
private readonly ISqlSugarRepository _eventProjectRepository;
private readonly ISqlSugarRepository _eventProjectGroupRepository;
private readonly ISqlSugarRepository _projectGroupRepository;
private readonly ISqlSugarRepository _projectLimitRepository;
private readonly ISqlSugarRepository _projectRepository;
private readonly ISqlSugarRepository _registrationRepository;
private readonly ISqlSugarRepository _registrationItemRepository;
private readonly ISqlSugarRepository _registrationPersonRepository;
private readonly ISqlSugarRepository _userRepository;
private readonly ISqlSugarRepository _sysUserRepository;
private readonly ISqlSugarRepository _feeConfigRepository;
private readonly ISqlSugarRepository _limitRepository;
private readonly ISqlSugarRepository _orderRepository;
private readonly ISqlSugarRepository _weChatPayConfigRepository;
private readonly ISqlSugarRepository _merchantRepository;
private readonly ISqlSugarRepository _couponRepository;
private readonly ISqlSugarRepository _userCouponRepository;
private readonly ISqlSugarRepository _redemptionCodeRepository;
private readonly ISqlSugarRepository _paymentRepository;
private readonly ISqlSugarRepository _merchantCertificationRepository;
private readonly ISqlSugarClient _db;
private readonly IIdGenerator _idGenerator;
private readonly WeChatPayService _weChatPayService;
private readonly IOptionsMonitor _wechatTenpayClientOptionsMonitor;
private readonly ILogger _logger;
private readonly OrderNoGeneratorService _orderNoGeneratorService;
private readonly RefundConfigService _refundConfigService;
private readonly ISqlSugarRepository _refundRecordRepository;
private readonly EventRuleSnapshotService _ruleSnapshotService;
private readonly IEasyCachingProvider _easyCachingProvider;
// 缓存键前缀
private const string CacheKey_EventConfig = "EventConfig:";
private const string CacheKey_EventLimit = "EventLimit:";
private const string CacheKey_ProjectLimit = "EventProjectLimit:";
private const string CacheKey_EventProjects = "EventProjects:";
private const string CacheKey_ProjectGroups = "EventProjectGroups:";
private const string CacheKey_BaseProjects = "BaseProjects:";
public UnifiedRegistrationService(
ISqlSugarRepository eventRepository,
ISqlSugarRepository unitRepository,
ISqlSugarRepository eventProjectRepository,
ISqlSugarRepository eventProjectGroupRepository,
ISqlSugarRepository projectGroupRepository,
ISqlSugarRepository projectLimitRepository,
ISqlSugarRepository projectRepository,
ISqlSugarRepository registrationRepository,
ISqlSugarRepository registrationItemRepository,
ISqlSugarRepository registrationPersonRepository,
ISqlSugarRepository userRepository,
ISqlSugarRepository sysUserRepository,
ISqlSugarRepository feeConfigRepository,
ISqlSugarRepository limitRepository,
ISqlSugarRepository orderRepository,
ISqlSugarRepository weChatPayConfigRepository,
ISqlSugarRepository merchantRepository,
ISqlSugarRepository couponRepository,
ISqlSugarRepository userCouponRepository,
ISqlSugarRepository redemptionCodeRepository,
ISqlSugarRepository paymentRepository,
ISqlSugarRepository merchantCertificationRepository,
ISqlSugarClient db,
IIdGenerator idGenerator,
WeChatPayService weChatPayService,
IOptionsMonitor wechatTenpayClientOptionsMonitor,
ILogger logger,
OrderNoGeneratorService orderNoGeneratorService,
RefundConfigService refundConfigService,
ISqlSugarRepository refundRecordRepository,
EventRuleSnapshotService ruleSnapshotService,
IEasyCachingProvider easyCachingProvider)
{
_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;
_ruleSnapshotService = ruleSnapshotService;
_easyCachingProvider = easyCachingProvider;
}
///
/// 统一报名方法A - 处理核心报名业务逻辑
///
[UnitOfWork]
public async Task 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);
// 5. 如果是免费赛事,直接完成支付
if (totalAmount <= 0)
{
_logger.LogInformation("免费赛事,直接完成支付,报名ID: {RegistrationId}", registration.Id);
await HandleFreePaymentAsync(registration, null, input.CreatedUserId);
}
_logger.LogInformation("统一报名处理完成,报名ID: {RegistrationId}", registration.Id);
return new UnifiedRegistrationOutput
{
RegistrationId = registration.Id,
OrderNo = registration.OrderNo,
OriginalAmount = totalAmount,
DiscountAmount = 0,
ActualAmount = totalAmount,
PaymentStatus = registration.PaymentStatus
};
}
///
/// 获取或创建报名记录
///
private async Task 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);
}
///
/// 更新现有报名记录
///
private async Task 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 = 5; // 5=待支付确认,不占用正式名额
registration.OrderNo = string.Empty; // 清空旧的订单号,确保重新生成新订单
registration.UpdatedTime = DateTime.Now;
registration.UpdatedUserId = input.CreatedUserId;
// 软删除旧的明细和人员(保留历史记录,与取消报名逻辑一致)
var oldItems = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registration.Id && !x.DeleteMark);
var oldPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id && !x.DeleteMark);
if (oldItems.Any())
{
await _registrationItemRepository.SoftDeleteAsync(x => x.RegistrationId == registration.Id);
_logger.LogInformation("重新报名,软删除旧项目:报名ID={RegistrationId}, 项目数={Count}", registration.Id, oldItems.Count);
}
if (oldPersons.Any())
{
await _registrationPersonRepository.SoftDeleteAsync(x => x.RegistrationId == registration.Id);
_logger.LogInformation("重新报名,软删除旧人员:报名ID={RegistrationId}, 人数={Count}", registration.Id, oldPersons.Count);
}
return registration;
}
///
/// 创建新的报名记录
///
private async Task 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;
}
///
/// 统一支付处理方法 - 处理支付相关逻辑
///
[UnitOfWork]
public async Task 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;
}
///
/// 验证报名
///
private async Task ValidateRegistrationAsync(UnifiedRegistrationInput input)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List()
};
// 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;
}
///
/// 验证项目选择(一阶段:支持同一项目选择多个组别)
///
private async Task ValidateProjectSelectionAsync(UnifiedRegistrationInput input)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List()
};
// 1. 检查是否存在重复选择的同一项目下的同一组别(同一项目下的不同组别是允许的)
var duplicateGroupIds = input.Items
.Where(i => i.GroupId.HasValue)
.GroupBy(i => i.GroupId!.Value)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
if (duplicateGroupIds.Any())
{
var duplicateGroups = await _eventProjectGroupRepository.GetListAsync(x => duplicateGroupIds.Contains(x.Id));
var groupNames = duplicateGroups.Select(g => $"{g.GroupName ?? g.Id.ToString()}").ToList();
result.IsValid = false;
result.ErrorMessage = $"同一组别被重复选择:{string.Join(", ", groupNames)},同一项目下的不同组别可以重复选择,但同一组别不能选择两次";
result.Details.Add(CreateValidationDetail("组别选择", false, result.ErrorMessage));
return result;
}
// 2. 验证每个 item 的 ProjectId 与 GroupId 的对应关系
var groupIds = input.Items.Where(i => i.GroupId.HasValue).Select(i => i.GroupId!.Value).Distinct().ToList();
if (groupIds.Any())
{
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => groupIds.Contains(x.Id));
foreach (var item in input.Items)
{
if (!item.GroupId.HasValue) continue;
var epg = eventProjectGroups.FirstOrDefault(g => g.Id == item.GroupId.Value);
if (epg == null) continue;
if (item.ProjectId != epg.EventProjectId)
{
result.IsValid = false;
result.ErrorMessage = $"组别 {item.GroupId.Value} 不属于项目 {item.ProjectId},请检查选择";
result.Details.Add(CreateValidationDetail("组别-项目匹配", false, result.ErrorMessage));
return result;
}
}
}
result.Details.Add(CreateValidationDetail("项目组别选择", true, "同一项目可以选择多个组别,符合要求"));
return result;
}
///
/// 验证队员分配(二阶段)
///
public async Task ValidateTeamAssignmentAsync(UnifiedRegistrationInput input)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List()
};
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;
}
///
/// 从报名项中获取团队列表(支持新旧数据结构)
///
private static List GetTeamsFromItem(UnifiedRegistrationItemInput item)
{
if (item.Teams != null && item.Teams.Any())
{
return item.Teams;
}
if (item.Persons != null && item.Persons.Any())
{
return new List
{
new()
{
TeamIndex = 1,
Persons = item.Persons
}
};
}
return new List();
}
///
/// 从报名项中获取所有人员列表(支持新旧数据结构)
///
private static List GetAllPersonsFromItem(UnifiedRegistrationItemInput item)
{
var persons = new List();
var teams = GetTeamsFromItem(item);
foreach (var team in teams)
{
if (team.Persons != null)
{
persons.AddRange(team.Persons);
}
}
return persons;
}
///
/// 获取人员证件号码(根据认证类型返回身份证号或护照号)
///
private static string GetPersonCertNumber(UnifiedPersonInput person)
{
return person.AuthType == 2 ? (person.PassportNumber ?? "") : (person.IdCardNumber ?? "");
}
///
/// 验证队员资质(年龄和性别)
///
public RegistrationValidationResult ValidatePersonQualifications(List persons, EventProjectGroup group, bool isTeamProject)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List()
};
// 首先验证性别,发现问题立即返回
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;
}
///
/// 验证队员性别
///
private static RegistrationValidationResult? ValidatePersonGenders(List 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 { CreateValidationDetail("性别验证", false, errorMessage) }
};
}
}
return null;
}
///
/// 验证队员年龄
///
private static RegistrationValidationResult? ValidatePersonAges(List persons, EventProjectGroup group, bool isTeamProject, RegistrationValidationResult result)
{
// 组别未配置年龄限制时,跳过年龄检查(避免 DateTime.MinValue 造成的误判)
bool hasEndLimit = group.BirthDateEnd != DateTime.MinValue;
bool hasStartLimit = group.BirthDateStart != DateTime.MinValue;
// ① 超龄检查(大年龄段报小年龄段 → 拒绝)
// BirthDate < BirthDateStart → 出生太早 → 年龄偏大 → 不允许报小年龄组
if (hasStartLimit)
{
var tooOldPerson = persons.FirstOrDefault(p => p.BirthDate < group.BirthDateStart);
if (tooOldPerson != null)
{
result.IsValid = false;
result.ErrorMessage = $"队员{tooOldPerson.Name}的年龄超过组别最大年龄限制。要求出生日期不早于:{group.BirthDateStart:yyyy-MM-dd},实际:{tooOldPerson.BirthDate:yyyy-MM-dd}";
result.Details.Add(CreateValidationDetail("年龄验证", false, result.ErrorMessage));
return result;
}
}
// ② 统计年龄状态
// inTargetCount:在目标年龄段内(BirthDateStart ≤ BirthDate ≤ BirthDateEnd)
// aboveTargetCount:小年龄段报大年龄段(BirthDate > BirthDateEnd,年龄偏小)→ 允许跨组
int inTargetCount = 0;
int aboveTargetCount = 0;
if (hasStartLimit || hasEndLimit)
{
inTargetCount = persons.Count(p =>
(!hasStartLimit || p.BirthDate >= group.BirthDateStart) &&
(!hasEndLimit || p.BirthDate <= group.BirthDateEnd));
aboveTargetCount = persons.Count(p => hasEndLimit && p.BirthDate > group.BirthDateEnd);
}
// ③ 团队项目跨组验证:允许小年龄段报大年龄段,但团队中至少需要有一人属于目标年龄段
if (hasEndLimit && isTeamProject && aboveTargetCount > 0 && inTargetCount == 0)
{
bool allAbove = aboveTargetCount == persons.Count;
string errorMessage = allAbove
? "跨组报名时,团队中所有成员年龄均小于目标年龄段,团队中至少需要有一名成员属于目标年龄段"
: "跨组报名时,团队中至少需要有一名成员属于目标年龄段";
string itemName = allAbove
? "年龄跨组验证(全部小于目标年龄)"
: "年龄跨组验证";
result.IsValid = false;
result.ErrorMessage = errorMessage;
result.Details.Add(CreateValidationDetail(itemName, false, errorMessage));
return result;
}
// 添加验证通过的详情
if (hasStartLimit || hasEndLimit)
{
result.Details.Add(aboveTargetCount > 0
? CreateValidationDetail("年龄跨组", true, "符合年龄跨组报名要求")
: CreateValidationDetail("年龄验证", true, "所有队员均在目标年龄段内"));
}
return null;
}
///
/// 创建报名明细和参赛人员
///
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();
var persons = new List();
var uniqueUserIds = new HashSet(); // 用于去重的用户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.AuthType == 2 ? pInput.PassportNumber : 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);
}
///
/// 计算最终金额(含优惠券)
///
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);
}
///
/// 计算优惠券折扣
///
private static (decimal discountAmount, decimal finalAmount) CalculateCouponDiscount(
YD_Event.Core.Entities.Coupon coupon, decimal originalAmount)
{
decimal discountAmount = 0;
if (coupon.Type == 0) // 折扣
{
discountAmount = originalAmount * coupon.Value / 100;
}
else if (coupon.Type == 1) // 定额
{
discountAmount = Math.Min(coupon.Value, originalAmount);
}
var finalAmount = Math.Max(0, originalAmount - discountAmount);
return (discountAmount, finalAmount);
}
///
/// 根据支付方式类型获取对应的支付方式
///
private static int? GetPaymentMethodByType(PaymentMethodType type)
{
return type switch
{
PaymentMethodType.WeChatPay or PaymentMethodType.AliPay => 2, // 线上支付
PaymentMethodType.BankTransfer => 1, // 线下支付
_ => null
};
}
///
/// 处理免费支付
///
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 = 2; // 免费赛事设置为线上支付方式
registration.PaymentMethodType = PaymentMethodType.WeChatPay; // 默认微信支付
registration.PaymentTime = DateTime.Now;
registration.UpdatedTime = DateTime.Now;
registration.UpdatedUserId = operatorUserId;
await _registrationRepository.UpdateAsync(registration);
}
///
/// 创建订单记录
///
private async Task 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;
}
///
/// 处理线上支付
///
private async Task 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);
}
///
/// 获取微信支付配置
///
private async Task 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;
}
///
/// 处理 JSAPI 支付
///
private async Task 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;
}
///
/// 处理 Native 支付
///
private async Task 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;
}
///
/// 处理线下支付
///
private async Task 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
};
}
///
/// 确认银行转账
///
public async Task 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 ?? "报名名额已满,请重新报名"
};
}
// 如果订单使用了优惠券,标记为已使用
if (!string.IsNullOrEmpty(registration.OrderNo))
{
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo && !o.DeleteMark);
if (order != null && order.CouponId.HasValue)
{
var userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
uc.CouponId == order.CouponId.Value &&
uc.UserId == registration.CreatedUserId &&
uc.Status == 0 &&
!uc.DeleteMark);
if (userCoupon != null)
{
userCoupon.Status = 1;
userCoupon.UseTime = DateTime.Now;
userCoupon.OrderId = order.Id;
userCoupon.UpdatedTime = DateTime.Now;
userCoupon.UpdatedUserId = operatorUserId;
await _userCouponRepository.UpdateAsync(userCoupon);
// 如果有兑换码,标记兑换码为已使用
if (userCoupon.RedemptionCodeId.HasValue)
{
var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc =>
rc.Id == userCoupon.RedemptionCodeId.Value &&
rc.Status == 0 &&
!rc.DeleteMark);
if (redemptionCode != null)
{
redemptionCode.Status = 1;
redemptionCode.UsedTime = DateTime.Now;
redemptionCode.UpdatedTime = DateTime.Now;
redemptionCode.UpdatedUserId = operatorUserId;
await _redemptionCodeRepository.UpdateAsync(redemptionCode);
}
}
// 更新优惠券的已使用数量
var coupon = await _couponRepository.GetFirstAsync(c => c.Id == order.CouponId.Value && !c.DeleteMark);
if (coupon != null)
{
coupon.UsedQuantity++;
coupon.UpdatedTime = DateTime.Now;
coupon.UpdatedUserId = operatorUserId;
await _couponRepository.UpdateAsync(coupon);
}
}
}
}
// 更新报名记录状态为待审核 - 正式创建报名记录
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 = "转账确认成功,订单已提交审核"
};
}
///
/// 删除报名记录(用于删除预报名记录)
///
private async Task DeleteRegistrationAsync(EventRegistration registration)
{
// 软删报名明细
await _registrationItemRepository.SoftDeleteAsync(x => x.RegistrationId == registration.Id);
// 软删报名人员
await _registrationPersonRepository.SoftDeleteAsync(x => x.RegistrationId == registration.Id);
// 软删报名记录
await _registrationRepository.SoftDeleteByIdAsync(registration.Id);
_logger.LogInformation("已删除预报名记录,报名ID:{RegistrationId}", registration.Id);
}
///
/// 获取赛事报名限制配置(带缓存,3分钟过期)
///
private async Task GetEventLimitCacheAsync(long eventId)
{
var cacheKey = $"{CacheKey_EventLimit}{eventId}";
var cacheValue = await _easyCachingProvider.GetAsync(cacheKey);
if (cacheValue.HasValue) return cacheValue.Value;
var limit = await _limitRepository.GetFirstAsync(x => x.EventId == eventId && !x.DeleteMark);
await _easyCachingProvider.SetAsync(cacheKey, limit, TimeSpan.FromMinutes(3));
return limit;
}
///
/// 获取赛事项目限制配置列表(带缓存,3分钟过期)
///
private async Task> GetProjectLimitsCacheAsync(long eventId)
{
var cacheKey = $"{CacheKey_ProjectLimit}{eventId}";
var cacheValue = await _easyCachingProvider.GetAsync>(cacheKey);
if (cacheValue.HasValue) return cacheValue.Value;
var projectLimits = await _projectLimitRepository.GetListAsync(x => x.EventId == eventId && !x.DeleteMark);
await _easyCachingProvider.SetAsync(cacheKey, projectLimits, TimeSpan.FromMinutes(3));
return projectLimits;
}
///
/// 获取赛事项目列表(带缓存,3分钟过期)
///
private async Task> GetEventProjectsCacheAsync(long eventId)
{
var cacheKey = $"{CacheKey_EventProjects}{eventId}";
var cacheValue = await _easyCachingProvider.GetAsync>(cacheKey);
if (cacheValue.HasValue) return cacheValue.Value;
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId);
await _easyCachingProvider.SetAsync(cacheKey, eventProjects, TimeSpan.FromMinutes(3));
return eventProjects;
}
///
/// 获取赛事项目组别列表(带缓存,3分钟过期)
///
private async Task> GetEventProjectGroupsCacheAsync(long eventId)
{
var cacheKey = $"{CacheKey_ProjectGroups}{eventId}";
var cacheValue = await _easyCachingProvider.GetAsync>(cacheKey);
if (cacheValue.HasValue) return cacheValue.Value;
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == eventId && !x.DeleteMark);
await _easyCachingProvider.SetAsync(cacheKey, eventProjectGroups, TimeSpan.FromMinutes(3));
return eventProjectGroups;
}
///
/// 获取基础项目列表(带缓存,3分钟过期)
///
private async Task> GetBaseProjectsCacheAsync(List projectIds, long eventId)
{
var cacheKey = $"{CacheKey_BaseProjects}{eventId}";
var cacheValue = await _easyCachingProvider.GetAsync>(cacheKey);
if (cacheValue.HasValue) return cacheValue.Value;
var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id) && !x.DeleteMark);
await _easyCachingProvider.SetAsync(cacheKey, projects, TimeSpan.FromMinutes(3));
return projects;
}
///
/// 检查报名限制
///
public async Task CheckRegistrationLimitAsync(
UnifiedRegistrationInput input,
long? excludeRegistrationId = null,
List? eventProjects = null,
List? eventProjectGroups = null)
{
var result = new RegistrationLimitCheckOutput
{
HasLimit = false,
LimitDetails = new List()
};
_logger.LogInformation("开始检查报名限制,赛事ID: {EventId}", input.EventId);
var limit = await GetEventLimitCacheAsync(input.EventId);
var projectLimits = await GetProjectLimitsCacheAsync(input.EventId);
eventProjects ??= await GetEventProjectsCacheAsync(input.EventId);
var projects = await GetBaseProjectsCacheAsync(eventProjects.Select(ep => ep.ProjectId).Distinct().ToList(), input.EventId);
eventProjectGroups ??= await GetEventProjectGroupsCacheAsync(input.EventId);
_logger.LogInformation("获取赛事配置完成,项目数: {ProjectCount}, 组別数: {GroupCount}",
eventProjects.Count, eventProjectGroups.Count);
// ==================== 报名限制检查流程 ====================
// 检查顺序说明:
// 1. 先检查团队组别容量限制 - 检查是否超过组别限报的团队数量
// 2. 再检查成员项目限报 - 检查队员是否超过个人/团队项目的限报数量
// 3. 然后检查人员项目组别重复 - 检查同一人员是否在同一项目的多个组别中重复报名
// 4. 再检查人员是否已报名 - 检查人员是否已在其他报名记录中报过同一项目组别
// 5. 再检查团队成员冲突 - 检查同一人员是否在同一项目的多个团队中
// 6. 最后检查项目完成人数 - 确保人数与项目的完成人数完全一致
// 检查1:团队组别容量限制
CheckTeamGroupCapacityLimitAsync(input, result, projectLimits, eventProjects, projects, eventProjectGroups);
if (result.HasLimit) return result;
// 检查2:成员项目限报(优化:使用数据库聚合查询,避免全量加载)
await CheckMemberProjectLimitAsync(input, result, limit, eventProjects, projects, excludeRegistrationId);
if (result.HasLimit) return result;
// 检查3:人员项目组别重复(纯内存操作,输入数据验证)
CheckPersonProjectDuplicateAsync(input, result, eventProjects, projects, eventProjectGroups);
if (result.HasLimit) return result;
// 检查4:人员是否已在其他报名中报名(优化:使用数据库查询,避免全量加载)
await CheckPersonAlreadyRegisteredAsync(input, result, eventProjects, projects, eventProjectGroups, excludeRegistrationId);
if (result.HasLimit) return result;
// 检查5:团队成员冲突检测(纯内存操作,输入数据验证)
CheckTeamMemberConflictAsync(input, result, eventProjects, projects, eventProjectGroups);
if (result.HasLimit) return result;
// 检查6:项目完成人数验证(纯内存操作,输入数据验证)
CheckProjectCompletionAsync(input, result, eventProjects, projects, eventProjectGroups);
_logger.LogInformation("报名限制检查完成,是否存在限制: {HasLimit}, 限制详情数: {LimitCount}",
result.HasLimit, result.LimitDetails.Count);
return result;
}
///
/// 创建报名限制详情
///
private static RegistrationLimitDetail CreateLimitDetail(long memberId, string memberName, string limitType, string limitMessage)
{
return new RegistrationLimitDetail
{
MemberId = memberId,
MemberName = memberName,
LimitType = limitType,
LimitMessage = limitMessage
};
}
///
/// 创建验证详情
///
private static ValidationDetail CreateValidationDetail(string itemName, bool isPassed, string message)
{
return new ValidationDetail
{
ItemName = itemName,
IsPassed = isPassed,
Message = message
};
}
///
/// 检查团队组别容量限制(项目组别限报设置)
///
private void CheckTeamGroupCapacityLimitAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List projectLimits,
List eventProjects,
List projects,
List 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);
// 限报数量 <= 0 表示不限制,跳过验证
if (projectLimit == null || projectLimit.LimitCount <= 0) 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}项目已达到报名限制"));
}
}
}
}
///
/// 检查成员项目限报(优化版:使用数据库查询,仅查询涉及的成员数据,避免全量加载)
///
private async Task CheckMemberProjectLimitAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
EventRegistrationLimit? limit,
List eventProjects,
List projects,
long? excludeRegistrationId = null)
{
var memberIdsList = new List();
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);
// 构建有效报名记录的子查询(与原逻辑一致的过滤条件)
var validRegistrationQuery = _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))
) &&
x.PaymentStatus != 4 && x.PaymentStatus != 5 && x.PaymentStatus != 6);
if (excludeRegistrationId.HasValue)
{
validRegistrationQuery = validRegistrationQuery.Where(x => x.Id != excludeRegistrationId.Value);
}
// 查询这些成员在该赛事中已报名的项目(按用户+项目去重)
var existingMemberProjects = await _db.Queryable()
.InnerJoin((p, i) => p.RegistrationItemId == i.Id && !i.DeleteMark)
.InnerJoin(validRegistrationQuery, (p, i, r) => p.RegistrationId == r.Id)
.Where((p, i, r) => memberIds.Contains(p.UserId) && !p.DeleteMark)
.Select((p, i, r) => new
{
UserId = p.UserId,
ProjectId = i.ProjectId
})
.Distinct()
.ToListAsync();
// 按用户分组统计已报名项目
var memberProjectMap = existingMemberProjects
.GroupBy(x => x.UserId)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.ProjectId).ToHashSet());
// 构建项目类型映射(赛事项目ID -> 基础项目类型)
var projectTypeMap = new Dictionary(); // key: eventProjectId, value: isIndividual
foreach (var ep in eventProjects)
{
var baseProject = projects.FirstOrDefault(p => p.Id == ep.ProjectId);
if (baseProject != null)
{
projectTypeMap[ep.Id] = baseProject.IsIndividualProject();
}
}
// 统计每个成员的个人/团队项目数
foreach (var member in members)
{
var countedProjectIds = new HashSet();
int individualProjectCount = 0;
int teamProjectCount = 0;
// 统计已存在的报名项目
if (memberProjectMap.TryGetValue(member.Id, out var existingProjectIds))
{
foreach (var projectId in existingProjectIds)
{
if (!countedProjectIds.Add(projectId)) continue;
if (projectTypeMap.TryGetValue(projectId, out var isIndividual))
{
if (isIndividual)
individualProjectCount++;
else
teamProjectCount++;
}
}
}
// 统计本次提交的项目,确保不重复统计
foreach (var item in input.Items)
{
var allPersonsInItem = GetAllPersonsFromItem(item);
if (!allPersonsInItem.Any(p => p.UserId.HasValue && p.UserId.Value == member.Id))
continue;
if (!countedProjectIds.Add(item.ProjectId)) continue;
if (projectTypeMap.TryGetValue(item.ProjectId, out var isIndividual))
{
if (isIndividual)
individualProjectCount++;
else
teamProjectCount++;
}
}
_logger.LogDebug("成员 {MemberName} 个人项目报名数: {IndividualCount}, 团队项目报名数: {TeamCount}",
member.Name, individualProjectCount, teamProjectCount);
CheckIndividualProjectLimit(result, limit, member, individualProjectCount);
if (result.HasLimit) return;
CheckTeamProjectLimit(result, limit, member, teamProjectCount);
if (result.HasLimit) return;
}
}
///
/// 检查个人项目限报
///
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}队员已达到报名限制"));
}
///
/// 检查团队项目限报
///
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}队员已达到报名限制"));
}
///
/// 检查人员项目组别重复
///
private void CheckPersonProjectDuplicateAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List eventProjects,
List projects,
List eventProjectGroups)
{
var personProjectGroupMap = new Dictionary>();
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 certNumber = GetPersonCertNumber(person);
var key = person.UserId.HasValue ? person.UserId.Value.ToString() : certNumber;
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 && GetPersonCertNumber(p) == 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}\"中重复出现,同一人员只能在一个团队中报名同一项目组别"));
}
}
}
///
/// 检查人员是否已在其他报名中报过同一项目组别(优化版:使用数据库查询,避免全量加载)
///
private async Task CheckPersonAlreadyRegisteredAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List eventProjects,
List projects,
List eventProjectGroups,
long? excludeRegistrationId = null)
{
// 收集所有待检查的 (groupId, userId) 组合
var checkPairs = new List<(long GroupId, long UserId, string UserName)>();
foreach (var item in input.Items)
{
if (!item.GroupId.HasValue) continue;
var allPersonsInItem = GetAllPersonsFromItem(item);
foreach (var person in allPersonsInItem)
{
if (!person.UserId.HasValue) continue;
checkPairs.Add((item.GroupId.Value, person.UserId.Value, person.Name ?? "未知人员"));
}
}
if (!checkPairs.Any()) return;
var groupIds = checkPairs.Select(p => p.GroupId).Distinct().ToList();
var userIds = checkPairs.Select(p => p.UserId).Distinct().ToList();
// 构建有效报名记录的子查询
var validRegistrationQuery = _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))
) &&
x.PaymentStatus != 4 && x.PaymentStatus != 5 && x.PaymentStatus != 6);
if (excludeRegistrationId.HasValue)
{
validRegistrationQuery = validRegistrationQuery.Where(x => x.Id != excludeRegistrationId.Value);
}
// 查询这些用户在这些组别中是否已有报名记录
var existingRegistrations = await _db.Queryable()
.InnerJoin((p, i) => p.RegistrationItemId == i.Id && !i.DeleteMark)
.InnerJoin(validRegistrationQuery, (p, i, r) => p.RegistrationId == r.Id)
.Where((p, i, r) =>
userIds.Contains(p.UserId) &&
groupIds.Contains(i.EventProjectGroupId.Value) &&
!p.DeleteMark)
.Select((p, i, r) => new
{
UserId = p.UserId,
GroupId = i.EventProjectGroupId
})
.Distinct()
.ToListAsync();
// 构建已报名的哈希集合,用于快速查找
var registeredSet = new HashSet<(long UserId, long GroupId)>();
foreach (var reg in existingRegistrations)
{
if (reg.GroupId.HasValue)
{
registeredSet.Add((reg.UserId, reg.GroupId.Value));
}
}
// 检查是否有重复
foreach (var pair in checkPairs)
{
if (registeredSet.Contains((pair.UserId, pair.GroupId)))
{
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == pair.GroupId);
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup?.EventProjectId);
var project = projects.FirstOrDefault(p => p.Id == eventProject?.ProjectId);
var projectName = project?.Name ?? "未知项目";
var groupName = eventProjectGroup?.GroupName ?? "未知组别";
_logger.LogWarning("人员已报名该项目组别,人员: {PersonName}, 项目: {ProjectName}, 组别: {GroupName}",
pair.UserName, projectName, groupName);
result.HasLimit = true;
result.LimitDetails.Add(CreateLimitDetail(
0,
pair.UserName,
"PersonAlreadyRegistered",
$"人员已报名检测:人员\"{pair.UserName}\"已在项目\"{projectName}\"的组别\"{groupName}\"中报名过,不能重复报名,避免占用多个名额"));
return;
}
}
}
///
/// 检查团队成员冲突
/// 验证同一人员是否在同一项目的多个团队中(例如:同一队员既在1队又在2队)
///
private void CheckTeamMemberConflictAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List eventProjects,
List projects,
List 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>();
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));
}
}
}
}
///
/// 获取团队序号的中文表示
///
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}队"
};
}
///
/// 检查项目完成人数
///
private void CheckProjectCompletionAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List eventProjects,
List projects,
List 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;
}
// ==================== 完成人数配置解析 ====================
// 解析完成人数范围(单一值模式:min==max;范围值模式:min 0)
{
completionMin = project.CompletionCount;
completionMax = project.CompletionCount;
hasValidCompletionConfig = true;
}
}
else if (project.CompletionCountType == 1)
{
// 范围值模式 — 必须同时配置 Min/Max 且 Min <= Max
if (project.CompletionCountMin.HasValue && project.CompletionCountMax.HasValue &&
project.CompletionCountMin.Value > 0 && project.CompletionCountMax.Value > 0 &&
project.CompletionCountMin.Value <= project.CompletionCountMax.Value)
{
completionMin = project.CompletionCountMin.Value;
completionMax = project.CompletionCountMax.Value;
hasValidCompletionConfig = true;
}
}
// 未配置有效完成人数时跳过
if (!hasValidCompletionConfig)
{
continue;
}
var teams = GetTeamsFromItem(item);
if (teams.Count > 0)
{
// ========== 多团队模式 ==========
// 对每个团队单独验证人数是否在范围内
foreach (var team in teams)
{
var teamMemberCount = team.Persons?.Count ?? 0;
if (teamMemberCount < completionMin || teamMemberCount > completionMax)
{
string errorMessage;
if (completionMin == completionMax)
{
// 单一值模式
if (teamMemberCount < completionMin)
{
errorMessage = $"{project.Name}{GetTeamNumber(team.TeamIndex)}参赛人数错误(少{completionMin - teamMemberCount}人)";
}
else
{
errorMessage = $"{project.Name}{GetTeamNumber(team.TeamIndex)}参赛人数错误(多{teamMemberCount - completionMax}人)";
}
}
else
{
// 范围值模式
if (teamMemberCount < completionMin)
{
errorMessage = $"{project.Name}{GetTeamNumber(team.TeamIndex)}参赛人数错误(最少{completionMin}人,当前{teamMemberCount}人)";
}
else
{
errorMessage = $"{project.Name}{GetTeamNumber(team.TeamIndex)}参赛人数错误(最多{completionMax}人,当前{teamMemberCount}人)";
}
}
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 < completionMin || totalAssignedMembers > completionMax)
{
string errorMessage;
if (completionMin == completionMax)
{
// 单一值模式
if (totalAssignedMembers < completionMin)
{
errorMessage = $"{project.Name}参赛人数错误(少{completionMin - totalAssignedMembers}人)";
}
else
{
errorMessage = $"{project.Name}参赛人数错误(多{totalAssignedMembers - completionMax}人)";
}
}
else
{
// 范围值模式
if (totalAssignedMembers < completionMin)
{
errorMessage = $"{project.Name}参赛人数错误(最少{completionMin}人,当前{totalAssignedMembers}人)";
}
else
{
errorMessage = $"{project.Name}参赛人数错误(最多{completionMax}人,当前{totalAssignedMembers}人)";
}
}
result.HasLimit = true;
result.LimitDetails.Add(CreateLimitDetail(
0,
eventProjectGroup.GroupName ?? "未知组别",
"ProjectCompletion",
errorMessage));
}
}
}
}
///
/// 获取组别名称
///
private static string GetGroupName(UnifiedRegistrationItemInput item, List 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 ?? "";
}
///
/// 计算单个项目费用
///
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);
}
///
/// 计算总费用
///
private decimal CalculateTotalAmount(EventFeeConfig? feeConfig, List eventProjects,
List 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;
}
///
/// 计算报名金额(对外公开方法)
///
/// 赛事ID
/// 报名项目列表(包含人员数量信息)
/// 计算结果
public async Task<(decimal totalAmount, ChargeMode chargeMode, List<(long projectId, string projectName, decimal unitPrice, int personCount, decimal subtotal)> itemDetails)> CalculateRegistrationAmountAsync(
long eventId, List 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);
}
///
/// 计算金额输入项(简化版)
///
public class CalculateAmountInputItem
{
public long ProjectId { get; set; }
public int PersonCount { get; set; }
}
///
/// 根据出生日期计算年龄
///
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;
}
///
/// 处理支付成功
///
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;
userCoupon.UpdatedTime = DateTime.Now;
userCoupon.UpdatedUserId = operatorUserId;
await _userCouponRepository.UpdateAsync(userCoupon);
// 如果有兑换码,标记兑换码为已使用
if (userCoupon.RedemptionCodeId.HasValue)
{
var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc =>
rc.Id == userCoupon.RedemptionCodeId.Value &&
rc.Status == 0 &&
!rc.DeleteMark);
if (redemptionCode != null)
{
redemptionCode.Status = 1;
redemptionCode.UsedTime = DateTime.Now;
redemptionCode.UpdatedTime = DateTime.Now;
redemptionCode.UpdatedUserId = operatorUserId;
await _redemptionCodeRepository.UpdateAsync(redemptionCode);
_logger.LogInformation($"兑换码已标记为已使用,兑换码ID={redemptionCode.Id}");
}
}
// 更新优惠券的已使用数量
var coupon = await _couponRepository.GetByIdAsync(order.CouponId.Value);
if (coupon != null)
{
coupon.UsedQuantity++;
coupon.UpdatedTime = DateTime.Now;
coupon.UpdatedUserId = operatorUserId;
await _couponRepository.UpdateAsync(coupon);
}
_logger.LogInformation($"优惠券已标记为已使用,用户优惠券ID={userCoupon.Id},订单ID={order.Id}");
}
}
}
}
///
/// 取消报名
///
[UnitOfWork]
public async Task 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;
}
///
/// 查询并同步微信支付状态
///
public async Task 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;
}
///
/// 获取平台级别的微信支付配置
///
public async Task GetPlatformWeChatPayConfigAsync()
{
return await _weChatPayConfigRepository.GetFirstAsync(x =>
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
}
///
/// 根据赛事获取对应的微信支付配置
///
public async Task 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;
}
///
/// 统一优惠券验证和使用方法
///
[UnitOfWork]
public async Task 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);
// 注意:不立即更新兑换码状态,也不增加已领取数量,等支付完成后再做
}
// 计算优惠金额
var (discountAmount, finalAmount) = CalculateCouponDiscount(coupon, input.OriginalAmount);
result.IsValid = true;
result.CouponId = coupon.Id;
result.UserCouponId = userCoupon.Id;
result.CouponName = coupon.Name ?? "优惠券";
result.DiscountType = coupon.Type;
result.DiscountValue = coupon.Value;
result.DiscountAmount = discountAmount;
result.FinalAmount = finalAmount;
return result;
}
///
/// 验证报名相关配置参数
///
private async Task ValidateRegistrationConfigAsync(EventInfo eventInfo)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List()
};
// 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;
}
///
/// 验证用户单位认证状态
///
private async Task ValidateUserCertificationAsync(long userId)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List()
};
// 验证用户是否通过了单位认证
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 统一退款相关方法
///
/// 计算退款金额(统一方法)
///
/// 总金额
/// 赛事开始时间
/// 应退金额
[NonAction]
private decimal RoundUpToTwoDecimalPlaces(decimal amount)
{
// 原理:将金额乘100转成整数,用Math.Ceiling向上取整,再除100
// 例如:0.125 * 100 = 12.5 → Ceiling后=13 → 13/100=0.13
// 例如:0.124 * 100 = 12.4 → Ceiling后=13 → 13/100=0.13
var roundedAmount = Math.Ceiling(amount * 100) / 100;
return roundedAmount;
}
public async Task CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime)
{
var refundAmount = await _refundConfigService.CalculateRefundAmount(totalAmount, eventStartTime);
var roundedRefundAmount = RoundUpToTwoDecimalPlaces(refundAmount);
return roundedRefundAmount;
}
public async Task CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime, long eventId)
{
var refundAmount = await _refundConfigService.CalculateRefundAmountByEvent(totalAmount, eventStartTime, eventId);
var roundedRefundAmount = RoundUpToTwoDecimalPlaces(refundAmount);
return roundedRefundAmount;
}
///
/// 统一取消报名预览方法
///
/// 报名ID
/// 操作人用户ID
/// 取消预览输出
[NonAction]
public async Task 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().InSingleAsync(registration.EventId);
var items = await _db.Queryable().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();
// 查询订单获取实际支付金额(考虑优惠券抵扣) - 先获取,因为后面需要用来计算
var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.OrderType == 1 && !x.DeleteMark);
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
var originalAmount = registration.TotalAmount;
// 区分不同的收费模式来计算(基于原价)
decimal originalRefundableAmount = 0;
if (eventInfo.ChargeMode == ChargeMode.UnitPriceProject)
{
// 单价(项目)模式:每个未删除的项目独立计算退款
foreach (var item in remainingItems)
{
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id && !p.DeleteMark).ToList();
var itemOriginalAmount = item.UnitPrice * itemPersons.Count;
var itemRefund = await CalculateRefundAmountAsync(itemOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
originalRefundableAmount += itemRefund;
}
}
else if (eventInfo.ChargeMode == ChargeMode.UnitPriceEvent)
{
// 单价(赛事)模式:计算剩余可完全取消所有项目的人数
var remainingPersons = persons.Where(p => !p.DeleteMark).ToList();
// 按 UserId 或身份证号分组,找出每个人的所有报名记录
var personGroups = remainingPersons
.GroupBy(p => p.UserId > 0 ? p.UserId.ToString() : p.IdCard)
.Where(g => !string.IsNullOrEmpty(g.Key))
.ToList();
// 获取赛事单价
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == eventInfo.Id && !x.DeleteMark);
var eventUnitPrice = feeConfig?.Price ?? 0;
// 剩余可退款的人数
var remainingPersonCount = personGroups.Count;
var remainingOriginalAmount = eventUnitPrice * remainingPersonCount;
originalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
}
else
{
// 定价(赛事)模式:使用传统方式计算
var remainingOriginalAmount = registration.TotalAmount - refundedAmount;
originalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
}
// 确保可退款金额不小于0
originalRefundableAmount = Math.Max(0, originalRefundableAmount);
// 按比例转换为基于实际支付的可退款金额
decimal refundableAmount = originalAmount > 0 ? originalRefundableAmount * (actualPaidAmount / originalAmount) : 0;
refundableAmount = RoundUpToTwoDecimalPlaces(refundableAmount);
refundableAmount = Math.Min(refundableAmount, actualPaidAmount - refundedAmount);
_logger.LogInformation("统一取消报名预览:报名ID={RegistrationId}, 赛事名称={EventName}, 原价={OriginalAmount}, 实际支付={ActualAmount}, 已退款={RefundedAmount}, 原价可退款={OriginalRefundableAmount}, 实际可退款={RefundableAmount}, 支付方式={PaymentMethod}",
registrationId, eventInfo?.EventName, originalAmount, actualPaidAmount, refundedAmount, originalRefundableAmount, refundableAmount, registration.PaymentMethod);
// 获取组别信息(年龄和性别限制)
var groupIds = items.Where(x => x.EventProjectGroupId.HasValue).Select(x => x.EventProjectGroupId.Value).Distinct().ToList();
var groups = new Dictionary();
if (groupIds.Any())
{
var groupList = await _db.Queryable()
.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();
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 = actualPaidAmount,
RefundableAmount = refundableAmount,
RefundedAmount = refundedAmount,
CurrentRefundAmount = refundableAmount,
PaymentMethod = registration.PaymentMethod,
PaymentStatus = registration.PaymentStatus,
NeedPayeeInfo = registration.PaymentMethod == 1 && registration.PaymentStatus == 1
};
foreach (var item in items.Where(x => !x.DeleteMark))
{
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.Where(x => !x.DeleteMark))
{
output.Members.Add(new CancelPreviewMemberDto
{
MemberId = person.Id,
Name = person.Name ?? "",
IsLeader = person.PersonType == PersonType.Leader
});
}
// 构建新版详细数据结构(按项目 -> 组别 -> 团队 -> 队员分类)
var itemGroups = items.Where(x => !x.DeleteMark).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 && !p.DeleteMark).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 refundSnapshots = await _ruleSnapshotService.GetRefundRuleSnapshot(eventInfo.Id);
foreach (var snapshot in refundSnapshots)
{
output.RefundRules.Add(new RefundRuleDto
{
DayStart = snapshot.DayStart,
DayEnd = snapshot.DayEnd,
DeductPercentage = snapshot.DeductPercentage,
Remark = snapshot.Remark
});
}
var daysRemaining = (eventInfo.MatchStartTime - DateTime.Now).TotalDays;
if (daysRemaining < 0) daysRemaining = 0;
if (registration.PaymentMethod == 1)
{
output.ApplicableRefundRule = "线下支付订单取消报名将不执行微信退款,仅更新报名状态为已取消。";
}
else
{
var matchConfig = refundSnapshots.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;
}
///
/// 计算退款金额(前端选择时调用)
///
/// 计算输入
/// 操作人用户ID
/// 计算输出
public async Task 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().InSingleAsync(registration.EventId);
var items = await _db.Queryable().Where(x => x.RegistrationId == input.RegistrationId).ToListAsync();
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId);
// 查询订单获取实际支付金额(考虑优惠券抵扣)
var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == input.RegistrationId && x.OrderType == 1 && !x.DeleteMark);
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
var originalAmount = registration.TotalAmount;
// 构建一个临时的CancelExecuteInput来复用计算逻辑
var tempInput = new CancelExecuteInput
{
RegistrationId = input.RegistrationId,
CancelType = input.CancelType,
PersonIds = input.PersonIds,
CancelTeams = input.CancelTeams
};
// 先计算按原价应该退多少
decimal originalRefundAmount = await CalculateActualRefundAmountAsync(tempInput, registration, eventInfo, items, persons);
// 按比例计算实际退款金额(基于实际支付金额)
decimal actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0;
actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount);
actualRefundAmount = Math.Min(actualRefundAmount, actualPaidAmount);
// 查询历史已退款金额
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 remainingOriginalRefundableAmount = 0;
// 区分不同的收费模式来计算
if (eventInfo.ChargeMode == ChargeMode.UnitPriceProject)
{
// 单价(项目)模式:每个未删除的项目独立计算退款
foreach (var item in remainingItems)
{
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id && !p.DeleteMark).ToList();
var itemOriginalAmount = item.UnitPrice * itemPersons.Count;
var itemRefund = await CalculateRefundAmountAsync(itemOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
remainingOriginalRefundableAmount += itemRefund;
}
}
else if (eventInfo.ChargeMode == ChargeMode.UnitPriceEvent)
{
// 单价(赛事)模式:计算剩余可完全取消所有项目的人数
var remainingPersons = persons.Where(p => !p.DeleteMark).ToList();
// 按 UserId 或身份证号分组,找出每个人的所有报名记录
var personGroups = remainingPersons
.GroupBy(p => p.UserId > 0 ? p.UserId.ToString() : p.IdCard)
.Where(g => !string.IsNullOrEmpty(g.Key))
.ToList();
// 获取赛事单价
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == eventInfo.Id && !x.DeleteMark);
var eventUnitPrice = feeConfig?.Price ?? 0;
// 剩余可退款的人数
var remainingPersonCount = personGroups.Count;
var remainingOriginalAmount = eventUnitPrice * remainingPersonCount;
remainingOriginalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
}
else
{
// 定价(赛事)模式:使用传统方式计算
var remainingOriginalAmount = registration.TotalAmount - refundedAmount;
remainingOriginalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
}
// 按比例计算剩余可退款金额(基于实际支付金额)
decimal remainingActualRefundableAmount = originalAmount > 0 ? remainingOriginalRefundableAmount * (actualPaidAmount / originalAmount) : 0;
remainingActualRefundableAmount = RoundUpToTwoDecimalPlaces(remainingActualRefundableAmount);
remainingActualRefundableAmount = Math.Min(remainingActualRefundableAmount, actualPaidAmount - refundedAmount);
_logger.LogInformation("计算退款金额:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}, 剩余原价可退={RemainingOriginalRefundableAmount}, 剩余实际可退={RemainingActualRefundableAmount}, 已退={RefundedAmount}",
registration.Id, originalAmount, actualPaidAmount, originalRefundAmount, actualRefundAmount, remainingOriginalRefundableAmount, remainingActualRefundableAmount, refundedAmount);
return new CalculateRefundAmountOutput
{
refundAmount = actualRefundAmount,
refundableAmount = remainingActualRefundableAmount,
refundedAmount = refundedAmount,
actualPaidAmount = actualPaidAmount
};
}
///
/// 统一取消报名执行方法
///
/// 取消执行输入
/// 操作人用户ID
/// 取消执行输出
///
/// 根据取消规则计算退款金额
///
private async Task CalculateActualRefundAmountAsync(CancelExecuteInput input, Core.Entities.EventRegistration registration,
Core.Entities.EventInfo eventInfo, List items, List 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 remainingItems = items.Where(x => !x.DeleteMark).ToList();
decimal remainingOriginalTotal = 0;
if (eventInfo.ChargeMode == ChargeMode.UnitPriceProject)
{
// 单价项目模式:计算所有未删除项目的总金额
foreach (var item in remainingItems)
{
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id && !p.DeleteMark).ToList();
remainingOriginalTotal += item.UnitPrice * itemPersons.Count;
}
}
else if (eventInfo.ChargeMode == ChargeMode.UnitPriceEvent)
{
// 单价赛事模式:计算剩余可完全取消所有项目的人数
var remainingPersons = persons.Where(p => !p.DeleteMark).ToList();
// 按 UserId 或身份证号分组,找出每个人的所有报名记录
var personGroups = remainingPersons
.GroupBy(p => p.UserId > 0 ? p.UserId.ToString() : p.IdCard)
.Where(g => !string.IsNullOrEmpty(g.Key))
.ToList();
// 获取赛事单价
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == eventInfo.Id && !x.DeleteMark);
var eventUnitPrice = feeConfig?.Price ?? 0;
// 剩余可退款的人数
var remainingPersonCount = personGroups.Count;
remainingOriginalTotal = eventUnitPrice * remainingPersonCount;
}
else
{
// 定价赛事模式:全部取消,直接使用剩余原始金额
// 注意:这里不需要检查是否所有人取消,因为CancelType=0本身就表示全部取消
_logger.LogInformation("定价赛事全部取消,使用剩余原始金额计算退款");
remainingOriginalTotal = remainingOriginalAmount;
}
var fullRefundAmount = await CalculateRefundAmountAsync(remainingOriginalTotal, eventInfo.MatchStartTime, eventInfo.Id);
_logger.LogInformation("全部取消,退款金额={Amount}", fullRefundAmount);
return fullRefundAmount;
}
// 部分取消
// 收集所有取消的队员ID
var cancelMemberIds = input.PersonIds ?? new List();
var cancelTeams = input.CancelTeams ?? new List();
var allCancelIds = new HashSet(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 activePersons = persons.Where(p => !p.DeleteMark).ToList();
// 检查是否取消了所有有效队员
var allActivePersonsCanceled = allCancelIds.Count == activePersons.Count;
if (!allActivePersonsCanceled)
{
// 没有取消所有队员,个人取消不退款
_logger.LogInformation("定价赛事部分取消(未取消所有人员),个人取消不退款");
return 0;
}
// 取消了所有队员,按退款规则退款
var fixedPriceRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id);
_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 remainingPersons = persons.Where(p => !p.DeleteMark).ToList();
// 找出完全取消所有项目的人员
// 在单价赛事模式下,需要判断一个人员是否取消了其所有报名的项目
// 先按 UserId 或身份证号分组,找出每个人的所有报名记录
var personGroups = remainingPersons
.GroupBy(p => p.UserId > 0 ? p.UserId.ToString() : p.IdCard)
.Where(g => !string.IsNullOrEmpty(g.Key))
.ToList();
var fullyCanceledPersonCount = 0;
foreach (var group in personGroups)
{
var personAllItems = group.ToList();
// 检查这个人是否所有报名的记录都在本次取消中
var allCanceledForThisPerson = personAllItems.All(p => allCancelIds.Contains(p.Id));
if (allCanceledForThisPerson)
{
fullyCanceledPersonCount++;
_logger.LogInformation("单价(赛事)退款:人员完全取消所有项目,人员标识={PersonKey}, 报名记录数={ItemCount}",
group.Key, personAllItems.Count);
}
}
if (fullyCanceledPersonCount == 0)
{
_logger.LogInformation("单价(赛事)退款:没有人员完全取消所有项目,不退款");
return 0;
}
// 计算本次取消的原始金额
var unitPriceEventCancelAmount = eventUnitPrice * fullyCanceledPersonCount;
// 直接对本次取消的原始金额应用退款规则
var unitPriceEventFinalAmount = await CalculateRefundAmountAsync(unitPriceEventCancelAmount, eventInfo.MatchStartTime, eventInfo.Id);
_logger.LogInformation("单价(赛事)退款:完全取消所有项目的人数={CancelCount}, 赛事单价={UnitPrice}, 本次取消原始金额={CancelAmount}, 按规则退款金额={RefundAmount}",
fullyCanceledPersonCount, 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, eventInfo.Id);
_logger.LogInformation("单价(项目)退款:本次取消总原始金额={TotalCancelAmount}, 按规则退款金额={RefundAmount}",
totalProjectCancelAmount, projectRefundAmount);
return projectRefundAmount;
default:
_logger.LogInformation("未知收费模式,不退款");
return 0;
}
}
[UnitOfWork]
[NonAction]
public async Task 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)
{
// 线下支付只有已支付/审核通过状态(PaymentStatus == 1)才允许退款
if (registration.PaymentStatus != 1)
{
throw Oops.Bah("订单还在审核中,请联系管理员审核之后进行退款");
}
}
else
{
// 线上支付场景保持原有逻辑
if (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);
// 计算实际退款金额
// 【重要】始终信任后端计算结果,前端计算只用于展示
// 不接受前端传递的退款金额,避免资金风险
var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registration.Id && o.OrderType == 1 && !o.DeleteMark);
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
var originalAmount = registration.TotalAmount;
// 先计算按原价应该退多少
decimal originalRefundAmount = await CalculateActualRefundAmountAsync(input, registration, eventInfo, items, persons);
// 按比例计算实际退款金额
decimal actualRefundAmount = originalAmount > 0 ? originalRefundAmount * (actualPaidAmount / originalAmount) : 0;
actualRefundAmount = RoundUpToTwoDecimalPlaces(actualRefundAmount);
actualRefundAmount = Math.Min(actualRefundAmount, actualPaidAmount);
_logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 原价={OriginalAmount}, 实付={ActualPaidAmount}, 按原价应退={OriginalRefundAmount}, 实际退款={ActualRefundAmount}",
registration.Id, originalAmount, actualPaidAmount, originalRefundAmount, actualRefundAmount);
bool isPartialCancel = input.CancelType == 1;
// 提前初始化输出
var output = new CancelExecuteOutput
{
Success = true,
Message = isPartialCancel ? "部分取消报名成功" : "取消报名成功",
CancelTime = now,
RefundAmount = actualRefundAmount
};
// 收集所有要取消的队员ID
var allCancelMemberIds = new HashSet();
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);
}
}
}
}
long? refundOrderId = null;
string? refundOrderNo = null;
// 第一步:先创建退款订单(如果有退款金额的话)
if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0)
{
var originalOrder = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo);
if (originalOrder != null)
{
var newRefundOrder = new 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
};
// 线下支付时保存收款人信息
if (registration.PaymentMethod == 1)
{
newRefundOrder.PayeeName = input.PayeeName;
newRefundOrder.PayeeBankName = input.PayeeBankName;
newRefundOrder.PayeeBankAccount = input.PayeeBankAccount;
}
await _orderRepository.InsertAsync(newRefundOrder);
refundOrderId = newRefundOrder.Id;
refundOrderNo = newRefundOrder.OrderNo;
output.RefundNo = newRefundOrder.OrderNo;
_logger.LogInformation("退款订单创建成功:报名ID={RegistrationId}, 原订单号={OriginalOrderNo}, 退款订单号={RefundOrderNo}, 退款金额={Amount}",
registration.Id, originalOrder.OrderNo, newRefundOrder.OrderNo, actualRefundAmount);
}
}
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);
// 软删除报名人员(保存退款订单ID)
await SoftDeletePersonsAsync(allCancelMemberIds.ToList(), operatorUserId, now, refundOrderId);
_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(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, refundOrderId);
_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);
// 软删除所有报名人员(保存退款订单ID)
await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now, refundOrderId);
_logger.LogInformation("已软删除所有队员:报名ID={RegistrationId}", registration.Id);
// 软删除所有报名项目(保存退款订单ID)
await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now, refundOrderId);
_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);
// 软删除报名人员(保存退款订单ID)
await SoftDeletePersonsAsync(allCancelMemberIds.ToList(), operatorUserId, now, refundOrderId);
_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(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, refundOrderId);
_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}, 退款金额={ActualRefundAmount}",
registration.Id, actualRefundAmount);
// 软删除所有报名人员(保存退款订单ID)
await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now, refundOrderId);
_logger.LogInformation("已软删除所有队员:报名ID={RegistrationId}", registration.Id);
// 软删除所有报名项目(保存退款订单ID)
await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now, refundOrderId);
_logger.LogInformation("已软删除所有项目:报名ID={RegistrationId}", registration.Id);
registration.PaymentStatus = 4; // 已取消(与线下支付保持一致)
registration.UpdatedUserId = operatorUserId;
registration.UpdatedTime = now;
registration.RegistrationRemark = input.CancelReason;
await _registrationRepository.UpdateAsync(registration);
}
}
// 第二步:创建退款记录和执行微信退款(如果是线上支付)
if (!string.IsNullOrEmpty(refundOrderNo))
{
if (registration.PaymentMethod == 1)
{
// 线下支付:创建退款记录
_logger.LogInformation("线下支付创建退款记录:报名ID={RegistrationId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}",
registration.Id, refundOrderNo, actualRefundAmount);
if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0)
{
// 创建线下支付退款记录
await CreateRefundRecordAsync(registration, eventInfo, null, refundOrderNo,
null, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId,
input.PayeeName, input.PayeeBankName, input.PayeeBankAccount);
}
}
else
{
// 线上支付:执行微信退款
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 refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest
{
OutTradeNo = registration.OrderNo,
OutRefundNo = refundOrderNo,
RefundAmount = AmountHelper.YuanToFen(actualRefundAmount),
TotalAmount = AmountHelper.YuanToFen(actualPaidAmount), // 使用实际支付金额作为总金额
Reason = input.CancelReason
};
_logger.LogInformation("开始执行统一退款:报名ID={RegistrationId}, 订单号={OrderNo}, 退款单号={RefundNo}, 退款金额={Amount}, 退款类型={CancelType}",
registration.Id, registration.OrderNo, refundOrderNo, 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, refundOrderNo,
refundResult.RefundId, actualRefundAmount, actualPaidAmount, input.CancelReason, operatorUserId);
_logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}",
registration.Id, refundOrderNo, 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;
}
///
/// 创建退款记录
///
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);
}
///
/// 软删除报名人员
///
private async Task SoftDeletePersonsAsync(List personIds, long operatorUserId, DateTime now, long? refundOrderId = null)
{
var personsToDelete = await _registrationPersonRepository.GetListAsync(x => personIds.Contains(x.Id));
foreach (var person in personsToDelete)
{
person.DeleteMark = true;
person.UpdatedUserId = operatorUserId;
person.UpdatedTime = now;
if (refundOrderId.HasValue)
{
person.RefundOrderId = refundOrderId.Value;
}
}
await _registrationPersonRepository.UpdateRangeAsync(personsToDelete);
}
///
/// 软删除报名项目
///
private async Task SoftDeleteItemsAsync(List itemIds, long operatorUserId, DateTime now, long? refundOrderId = null)
{
var itemsToDelete = await _registrationItemRepository.GetListAsync(x => itemIds.Contains(x.Id));
foreach (var item in itemsToDelete)
{
item.DeleteMark = true;
item.UpdatedUserId = operatorUserId;
item.UpdatedTime = now;
if (refundOrderId.HasValue)
{
item.RefundOrderId = refundOrderId.Value;
}
}
await _registrationItemRepository.UpdateRangeAsync(itemsToDelete);
}
///
/// 更新订单状态
///
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);
}
}
}
///
/// 统一后台管理退款方法
///
/// 退款订单输入
/// 操作人用户ID
[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(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
///
/// 处理已取消但实际支付成功的退款(报名失败退款)
///
/// 报名ID
/// 操作人用户ID
/// 是否成功
[UnitOfWork]
public async Task ProcessFailedRegistrationRefundAsync(long registrationId, long operatorUserId)
{
try
{
_logger.LogInformation("开始处理报名失败退款,报名ID={RegistrationId}", registrationId);
var registration = await _registrationRepository.GetByIdAsync(registrationId);
if (registration == null)
{
_logger.LogWarning("报名记录不存在,报名ID={RegistrationId}", registrationId);
return false;
}
// 检查是否已经是已取消状态
if (registration.PaymentStatus != 4)
{
_logger.LogWarning("报名状态不是已取消,报名ID={RegistrationId}, 当前状态={Status}", registrationId, registration.PaymentStatus);
return false;
}
// 获取订单信息
var order = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registrationId && o.OrderType == 1);
if (order == null)
{
_logger.LogWarning("找不到订单记录,报名ID={RegistrationId}", registrationId);
return false;
}
// 获取赛事信息
var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
if (eventInfo == null)
{
_logger.LogWarning("找不到赛事信息,报名ID={RegistrationId}, 赛事ID={EventId}", registrationId, registration.EventId);
return false;
}
var now = DateTime.Now;
// 获取微信支付配置
var wechatConfig = await GetWeChatPayConfigByEventAsync(eventInfo);
if (wechatConfig == null)
{
_logger.LogError("找不到微信支付配置,报名ID={RegistrationId}", registrationId);
return false;
}
// 先从微信查询订单状态,确认是否真的支付成功
var wechatQueryResult = await _weChatPayService.QueryOrderAsync(wechatConfig, registration.OrderNo);
if (!wechatQueryResult.Success)
{
_logger.LogWarning("查询微信订单失败,报名ID={RegistrationId}, 订单号={OrderNo}, 错误={Error}",
registrationId, registration.OrderNo, wechatQueryResult.Message);
return false;
}
if (wechatQueryResult.TradeState != "SUCCESS")
{
_logger.LogInformation("微信订单不是支付成功状态,报名ID={RegistrationId}, 订单号={OrderNo}, 状态={TradeState}",
registrationId, registration.OrderNo, wechatQueryResult.TradeState);
return false;
}
_logger.LogInformation("确认微信订单已支付成功,报名ID={RegistrationId}, 订单号={OrderNo}",
registrationId, registration.OrderNo);
// 1. 保持原订单状态不变,仅添加备注
registration.RegistrationRemark = "报名失败,已全额退款";
registration.UpdatedTime = now;
registration.UpdatedUserId = operatorUserId;
await _registrationRepository.UpdateAsync(registration);
// 2. 更新原订单状态为"报名失败"
order.Remark = $"{eventInfo.EventName} - 报名失败";
order.UpdatedTime = now;
order.UpdatedUserId = operatorUserId;
await _orderRepository.UpdateAsync(order);
// 3. 生成退款单号
var refundOrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync();
// 4. 构建退款通知文案
var refundReason = $"{eventInfo.EventName} - 报名失败";
// 5. 创建退款订单记录
var refundOrder = new YD_Event.Core.Entities.Order
{
Id = _idGenerator.NewLong(),
OrderNo = refundOrderNo,
OrderType = 2,
MerchantId = order.MerchantId,
MerchantCertificationId = order.MerchantCertificationId,
EventId = order.EventId,
RegistrationId = order.RegistrationId,
RelatedOrderId = order.Id,
OriginalAmount = order.ActualAmount,
ActualAmount = order.ActualAmount,
DiscountAmount = 0,
OrderStatus = OrderStatus.RegistrationFailed,
PaymentTime = now,
Remark = refundReason,
CreatedTime = now,
CreatedUserId = operatorUserId,
Status = AvailabilityStatus.Enable
};
await _orderRepository.InsertAsync(refundOrder);
// 6. 调用微信退款接口
var refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest
{
OutTradeNo = registration.OrderNo,
OutRefundNo = refundOrderNo,
RefundAmount = AmountHelper.YuanToFen(order.ActualAmount),
TotalAmount = AmountHelper.YuanToFen(order.ActualAmount),
Reason = refundReason
};
var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest);
if (!refundResult.Success)
{
_logger.LogError("微信退款申请失败,报名ID={RegistrationId}, 错误={Error}", registrationId, refundResult.Message);
return false;
}
// 7. 获取或创建支付记录
var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == registrationId && !p.DeleteMark);
if (payment == null)
{
payment = new EventRegistrationPayment
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
TransactionNo = wechatQueryResult.TransactionId ?? "",
PaymentChannel = 1,
PaymentAmount = order.ActualAmount,
PaymentTime = wechatQueryResult.SuccessTime?.DateTime ?? now,
PaymentStatus = 1,
RefundStatus = 0,
RefundAmount = 0,
PayeeAccount = "",
CreatedTime = now,
CreatedUserId = operatorUserId
};
await _paymentRepository.InsertAsync(payment);
}
// 8. 创建退款记录
await CreateRefundRecordAsync(
registration,
eventInfo,
wechatConfig,
refundOrderNo,
refundResult.RefundId,
order.ActualAmount,
order.ActualAmount,
refundReason,
operatorUserId
);
// 9. 更新支付记录的退款状态
payment.RefundStatus = 2;
payment.RefundAmount = order.ActualAmount;
payment.RefundTime = now;
payment.UpdatedTime = now;
payment.UpdatedUserId = operatorUserId;
await _paymentRepository.UpdateAsync(payment);
// 10. 软删除报名人员和项目
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId);
if (persons.Count > 0)
{
await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now, refundOrder.Id);
}
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registrationId);
if (items.Count > 0)
{
await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now, refundOrder.Id);
}
_logger.LogInformation("开始执行优惠券和兑换码回滚:报名ID={RegistrationId}, 订单号={OrderNo}",
registrationId, registration.OrderNo);
if (order.CouponId.HasValue)
{
var userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
uc.CouponId == order.CouponId.Value &&
uc.OrderId == order.Id &&
uc.Status == 1 &&
!uc.DeleteMark);
if (userCoupon != null)
{
// 回滚用户优惠券状态
userCoupon.Status = 0;
userCoupon.UseTime = null;
userCoupon.OrderId = null;
userCoupon.UpdatedTime = now;
userCoupon.UpdatedUserId = operatorUserId;
await _userCouponRepository.UpdateAsync(userCoupon);
_logger.LogInformation("用户优惠券回滚成功:用户优惠券ID={UserCouponId}", userCoupon.Id);
// 如果有兑换码,回滚兑换码
if (userCoupon.RedemptionCodeId.HasValue)
{
var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc =>
rc.Id == userCoupon.RedemptionCodeId.Value &&
rc.Status == 1 &&
!rc.DeleteMark);
if (redemptionCode != null)
{
redemptionCode.Status = 0;
redemptionCode.UsedTime = null;
redemptionCode.UpdatedTime = now;
redemptionCode.UpdatedUserId = operatorUserId;
await _redemptionCodeRepository.UpdateAsync(redemptionCode);
_logger.LogInformation("兑换码回滚成功:兑换码ID={RedemptionCodeId}", redemptionCode.Id);
}
}
// 更新优惠券的已使用数量
var coupon = await _couponRepository.GetFirstAsync(c => c.Id == order.CouponId.Value && !c.DeleteMark);
if (coupon != null)
{
coupon.UsedQuantity = Math.Max(0, coupon.UsedQuantity - 1);
coupon.UpdatedTime = now;
coupon.UpdatedUserId = operatorUserId;
await _couponRepository.UpdateAsync(coupon);
_logger.LogInformation("优惠券已使用数量更新成功:优惠券ID={CouponId}, 原使用数量={OldUsed}, 新使用数量={NewUsed}",
coupon.Id, coupon.UsedQuantity + 1, coupon.UsedQuantity);
}
}
}
_logger.LogInformation("报名失败退款处理成功,报名ID={RegistrationId}, 退款金额={Amount}, 退款单号={RefundNo}",
registrationId, order.ActualAmount, refundOrderNo);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理报名失败退款时发生异常,报名ID={RegistrationId}", registrationId);
return false;
}
}
}