2026-04-13 00:40:44 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
using YD_Event.Application.Event.Dtos;
|
|
|
|
|
|
using YD_Event.Core.Domain;
|
|
|
|
|
|
using YD_Event.Core.Domain.Pricing;
|
|
|
|
|
|
using YD_Event.Core.Domain.Pricing.Models;
|
|
|
|
|
|
using YD_Event.Core.Entities;
|
|
|
|
|
|
using YD_Event.Core.Enum;
|
2026-04-13 03:44:49 +08:00
|
|
|
|
using YD_Event.Core.Extensions;
|
2026-04-13 00:40:44 +08:00
|
|
|
|
using YD_Event.Core.Infrastructure.WeChat;
|
|
|
|
|
|
using YD_Event.Core.Infrastructure.WeChat.Dto;
|
|
|
|
|
|
using YD_Event.Core.Options;
|
|
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Event;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 统一报名处理服务
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class UnifiedRegistrationService : ITransient
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventUnit> _unitRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventProjectGroup> _eventProjectGroupRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<ProjectGroup> _projectGroupRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventProjectRegistrationLimit> _projectLimitRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventRegistrationItem> _registrationItemRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventRegistrationPerson> _registrationPersonRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<SysUser> _sysUserRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventFeeConfig> _feeConfigRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventRegistrationLimit> _limitRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Coupon> _couponRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.UserCoupon> _userCouponRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
|
|
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
|
|
|
private readonly IIdGenerator _idGenerator;
|
|
|
|
|
|
private readonly WeChatPayService _weChatPayService;
|
|
|
|
|
|
private readonly IOptionsMonitor<WechatTenpayClientOptions> _wechatTenpayClientOptionsMonitor;
|
|
|
|
|
|
private readonly ILogger<UnifiedRegistrationService> _logger;
|
|
|
|
|
|
private readonly OrderNoGeneratorService _orderNoGeneratorService;
|
|
|
|
|
|
|
|
|
|
|
|
public UnifiedRegistrationService(
|
|
|
|
|
|
ISqlSugarRepository<EventInfo> eventRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventUnit> unitRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventProject> eventProjectRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventProjectGroup> eventProjectGroupRepository,
|
|
|
|
|
|
ISqlSugarRepository<ProjectGroup> projectGroupRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventProjectRegistrationLimit> projectLimitRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistration> registrationRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistrationPerson> registrationPersonRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
|
|
|
|
|
|
ISqlSugarRepository<SysUser> sysUserRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventFeeConfig> feeConfigRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistrationLimit> limitRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
|
|
|
|
|
|
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Coupon> couponRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.UserCoupon> userCouponRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
|
|
|
|
|
|
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
|
|
|
|
|
|
ISqlSugarClient db,
|
|
|
|
|
|
IIdGenerator idGenerator,
|
|
|
|
|
|
WeChatPayService weChatPayService,
|
|
|
|
|
|
IOptionsMonitor<WechatTenpayClientOptions> wechatTenpayClientOptionsMonitor,
|
|
|
|
|
|
ILogger<UnifiedRegistrationService> logger,
|
|
|
|
|
|
OrderNoGeneratorService orderNoGeneratorService)
|
|
|
|
|
|
{
|
|
|
|
|
|
_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;
|
|
|
|
|
|
_paymentRepository = paymentRepository;
|
|
|
|
|
|
_merchantCertificationRepository = merchantCertificationRepository;
|
|
|
|
|
|
_db = db;
|
|
|
|
|
|
_idGenerator = idGenerator;
|
|
|
|
|
|
_weChatPayService = weChatPayService;
|
|
|
|
|
|
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
|
|
|
|
|
|
_logger = logger;
|
|
|
|
|
|
_orderNoGeneratorService = orderNoGeneratorService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 统一报名方法A - 处理核心报名业务逻辑
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<UnifiedRegistrationOutput> ProcessRegistrationAsync(UnifiedRegistrationInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation("开始处理统一报名,赛事ID: {EventId}, 模式: {Mode}", input.EventId, input.RegistrationMode);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 验证报名
|
|
|
|
|
|
var validationResult = await ValidateRegistrationAsync(input);
|
|
|
|
|
|
if (!validationResult.IsValid)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw Oops.Bah(validationResult.ErrorMessage ?? "报名验证失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 检查是否已有报名记录
|
|
|
|
|
|
var existingRegistration = await _registrationRepository.GetFirstAsync(
|
|
|
|
|
|
r => r.EventId == input.EventId && r.CreatedUserId == input.CreatedUserId && r.PaymentStatus != 4);
|
|
|
|
|
|
|
|
|
|
|
|
EventRegistration registration;
|
|
|
|
|
|
if (existingRegistration != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 更新现有报名
|
|
|
|
|
|
registration = existingRegistration;
|
|
|
|
|
|
registration.RegistrationMode = input.RegistrationMode;
|
|
|
|
|
|
registration.TeamName = input.RegistrationMode == 2 ? input.TeamName : null;
|
|
|
|
|
|
registration.ContactName = input.ContactName;
|
|
|
|
|
|
registration.ContactPhone = input.ContactPhone;
|
|
|
|
|
|
registration.PaymentStatus = 0;
|
|
|
|
|
|
registration.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
registration.UpdatedUserId = input.CreatedUserId;
|
|
|
|
|
|
|
|
|
|
|
|
// 删除旧的明细和人员
|
|
|
|
|
|
await _registrationItemRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
|
|
|
|
|
|
await _registrationPersonRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 创建新报名
|
|
|
|
|
|
registration = new EventRegistration
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NewLong(),
|
|
|
|
|
|
EventId = input.EventId,
|
|
|
|
|
|
RegistrationMode = input.RegistrationMode,
|
|
|
|
|
|
TeamName = input.RegistrationMode == 2 ? input.TeamName : null,
|
|
|
|
|
|
ContactName = input.ContactName,
|
|
|
|
|
|
ContactPhone = input.ContactPhone,
|
|
|
|
|
|
PaymentStatus = 0,
|
|
|
|
|
|
RegistrationTime = DateTime.Now,
|
|
|
|
|
|
CreatedUserId = input.CreatedUserId,
|
|
|
|
|
|
CreatedTime = DateTime.Now,
|
|
|
|
|
|
Status = AvailabilityStatus.Enable,
|
|
|
|
|
|
OrderNo = string.Empty
|
|
|
|
|
|
};
|
|
|
|
|
|
await _registrationRepository.InsertAsync(registration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 创建报名明细和参赛人员
|
|
|
|
|
|
var (totalAmount, totalPersonCount) = await CreateRegistrationDetailsAsync(registration, input);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 更新报名信息
|
|
|
|
|
|
registration.TotalAmount = totalAmount;
|
|
|
|
|
|
registration.TotalPersonCount = totalPersonCount;
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("统一报名处理完成,报名ID: {RegistrationId}", registration.Id);
|
|
|
|
|
|
|
|
|
|
|
|
return new UnifiedRegistrationOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
OrderNo = registration.OrderNo,
|
|
|
|
|
|
OriginalAmount = totalAmount,
|
|
|
|
|
|
DiscountAmount = 0,
|
|
|
|
|
|
ActualAmount = totalAmount,
|
|
|
|
|
|
PaymentStatus = registration.PaymentStatus
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 统一支付处理方法 - 处理支付相关逻辑
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<UnifiedPaymentOutput> ProcessPaymentAsync(UnifiedPaymentInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation("开始处理统一支付,报名ID: {RegistrationId}, 支付方式: {PaymentMethod}",
|
|
|
|
|
|
input.RegistrationId, input.PaymentMethod);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 获取报名信息
|
|
|
|
|
|
var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId);
|
|
|
|
|
|
if (registration == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw Oops.Bah("报名记录不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 获取赛事信息
|
|
|
|
|
|
var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
|
|
|
|
|
|
if (eventInfo == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw Oops.Bah("赛事信息不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 计算优惠后金额
|
|
|
|
|
|
var (originalAmount, discountAmount, finalAmount, usedCoupon, usedUserCoupon) =
|
|
|
|
|
|
await CalculateFinalAmountAsync(registration.TotalAmount, input.CouponId, input.OperatorUserId);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 构建支付输出对象
|
|
|
|
|
|
var output = new UnifiedPaymentOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
OrderNo = registration.OrderNo,
|
|
|
|
|
|
OriginalAmount = originalAmount,
|
|
|
|
|
|
DiscountAmount = discountAmount,
|
|
|
|
|
|
ActualAmount = finalAmount,
|
|
|
|
|
|
PaymentStatus = 0,
|
|
|
|
|
|
IsFree = finalAmount <= 0,
|
|
|
|
|
|
CouponRuleNote = usedCoupon != null ? "优惠券使用后不予退还" : null
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 处理免费情况
|
|
|
|
|
|
if (finalAmount <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
await HandleFreePaymentAsync(registration, usedUserCoupon, input.OperatorUserId);
|
|
|
|
|
|
output.PaymentStatus = 1;
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 生成订单号
|
|
|
|
|
|
if (string.IsNullOrEmpty(registration.OrderNo))
|
|
|
|
|
|
{
|
|
|
|
|
|
registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
|
|
|
|
|
|
output.OrderNo = registration.OrderNo;
|
|
|
|
|
|
}
|
|
|
|
|
|
registration.PaymentMethod = input.PaymentMethod;
|
|
|
|
|
|
registration.PaymentStatus = 0;
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 创建订单记录
|
|
|
|
|
|
await CreateOrderAsync(registration, eventInfo, originalAmount, discountAmount, finalAmount,
|
|
|
|
|
|
input.CouponId, input.OperatorUserId);
|
|
|
|
|
|
|
|
|
|
|
|
// 8. 根据支付方式处理
|
|
|
|
|
|
if (input.PaymentMethod == 2) // 线上支付
|
|
|
|
|
|
{
|
|
|
|
|
|
var onlinePaymentOutput = await HandleOnlinePaymentAsync(registration, eventInfo, finalAmount, input);
|
|
|
|
|
|
output.WechatPayParams = onlinePaymentOutput.WechatPayParams;
|
|
|
|
|
|
output.QrcodeUrl = onlinePaymentOutput.QrcodeUrl;
|
|
|
|
|
|
output.TradeType = onlinePaymentOutput.TradeType;
|
|
|
|
|
|
}
|
|
|
|
|
|
else // 线下支付
|
|
|
|
|
|
{
|
|
|
|
|
|
var offlinePaymentOutput = await HandleOfflinePaymentAsync(registration, input);
|
|
|
|
|
|
output.PaymentStatus = offlinePaymentOutput.PaymentStatus;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 验证报名
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<RegistrationValidationResult> ValidateRegistrationAsync(UnifiedRegistrationInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new RegistrationValidationResult
|
|
|
|
|
|
{
|
|
|
|
|
|
IsValid = true,
|
|
|
|
|
|
Details = new List<ValidationDetail>()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 验证赛事是否存在且可报名
|
|
|
|
|
|
var eventInfo = await _eventRepository.GetByIdAsync(input.EventId);
|
|
|
|
|
|
if (eventInfo == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = "赛事不存在";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail { ItemName = "赛事验证", IsPassed = false, Message = "赛事不存在" });
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 验证报名时间
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
if (now < eventInfo.RegistrationStartTime || now > eventInfo.RegistrationEndTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = "不在报名时间内";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail { ItemName = "时间验证", IsPassed = false, Message = "不在报名时间内" });
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 验证项目选择(一阶段:单选机制,每个项目只能选择一个组别)
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 验证队员分配(二阶段:人数校验、资质验证、年龄跨组等)
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 验证报名限制
|
|
|
|
|
|
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 => new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = l.LimitType,
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = l.LimitMessage
|
|
|
|
|
|
}));
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.Details.Add(new ValidationDetail { ItemName = "整体验证", IsPassed = true, Message = "验证通过" });
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 验证项目选择(一阶段:单选机制)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<RegistrationValidationResult> ValidateProjectSelectionAsync(UnifiedRegistrationInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new RegistrationValidationResult
|
|
|
|
|
|
{
|
|
|
|
|
|
IsValid = true,
|
|
|
|
|
|
Details = new List<ValidationDetail>()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 检查是否有重复选择的项目(每个项目只能选择一个组别)
|
|
|
|
|
|
var duplicateProjects = input.Items
|
|
|
|
|
|
.GroupBy(i => i.ProjectId)
|
|
|
|
|
|
.Where(g => g.Count() > 1)
|
|
|
|
|
|
.Select(g => g.Key)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
if (duplicateProjects.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
var eventProjects = await _eventProjectRepository.GetListAsync(x => duplicateProjects.Contains(x.Id));
|
|
|
|
|
|
var projectNames = eventProjects.Select(ep => $"{ep.ProjectId}").ToList();
|
|
|
|
|
|
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = $"以下项目被重复选择,请确保每个项目只选择一个组别:{string.Join(", ", projectNames)}";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "项目选择",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "项目单选机制",
|
|
|
|
|
|
IsPassed = true,
|
|
|
|
|
|
Message = "每个项目仅选择一个组别,符合要求"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 验证队员分配(二阶段)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<RegistrationValidationResult> ValidateTeamAssignmentAsync(UnifiedRegistrationInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new RegistrationValidationResult
|
|
|
|
|
|
{
|
|
|
|
|
|
IsValid = true,
|
|
|
|
|
|
Details = new List<ValidationDetail>()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList();
|
|
|
|
|
|
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
|
|
|
|
|
|
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => eventProjectIds.Contains(x.EventProjectId));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in input.Items)
|
|
|
|
|
|
{
|
|
|
|
|
|
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId);
|
|
|
|
|
|
if (eventProject == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId);
|
|
|
|
|
|
if (eventProjectGroup == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = $"项目组别不存在";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "组别验证",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 05:20:09 +08:00
|
|
|
|
// 处理团队列表(支持多团队报名)
|
|
|
|
|
|
var teamsToValidate = new List<UnifiedTeamInput>();
|
|
|
|
|
|
|
|
|
|
|
|
if (item.Teams != null && item.Teams.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
// 使用新的团队列表格式
|
|
|
|
|
|
teamsToValidate.AddRange(item.Teams);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (item.Persons != null && item.Persons.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
// 向后兼容:使用旧的单个团队格式
|
|
|
|
|
|
teamsToValidate.Add(new UnifiedTeamInput
|
|
|
|
|
|
{
|
|
|
|
|
|
TeamIndex = 1,
|
|
|
|
|
|
Persons = item.Persons
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
2026-04-13 05:20:09 +08:00
|
|
|
|
result.ErrorMessage = $"请为项目添加至少一个团队和队员";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "团队验证",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
2026-04-13 00:40:44 +08:00
|
|
|
|
return result;
|
|
|
|
|
|
}
|
2026-04-13 05:20:09 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证每个团队
|
|
|
|
|
|
foreach (var team in teamsToValidate)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (team.Persons == null || !team.Persons.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = $"团队{team.TeamIndex}请至少添加一个队员";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "团队验证",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证队员资质(年龄和性别)
|
|
|
|
|
|
var qualificationResult = ValidatePersonQualifications(team.Persons, eventProjectGroup);
|
|
|
|
|
|
if (!qualificationResult.IsValid)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = qualificationResult.ErrorMessage;
|
|
|
|
|
|
result.Details.AddRange(qualificationResult.Details);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
result.Details.AddRange(qualificationResult.Details);
|
|
|
|
|
|
}
|
2026-04-13 00:40:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 验证队员资质(年龄和性别)
|
|
|
|
|
|
/// </summary>
|
2026-04-13 05:20:09 +08:00
|
|
|
|
public RegistrationValidationResult ValidatePersonQualifications(List<UnifiedPersonInput> persons, EventProjectGroup group)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
var result = new RegistrationValidationResult
|
|
|
|
|
|
{
|
|
|
|
|
|
IsValid = true,
|
|
|
|
|
|
Details = new List<ValidationDetail>()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-13 03:44:49 +08:00
|
|
|
|
bool hasAtLeastOneInTargetAge = false;
|
|
|
|
|
|
bool hasAnyBelowTargetAge = false;
|
|
|
|
|
|
bool hasAnyAboveTargetAge = false;
|
|
|
|
|
|
|
2026-04-13 00:40:44 +08:00
|
|
|
|
foreach (var person in persons)
|
|
|
|
|
|
{
|
2026-04-13 03:44:49 +08:00
|
|
|
|
// 验证性别 - 如果组别是"全部",则允许任何性别
|
|
|
|
|
|
if (group.Gender != Gender.All && person.Gender != group.Gender)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = $"队员{person.Name}的性别不符合组别要求。要求:{group.Gender.Description()},实际:{person.Gender.Description()}";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "性别验证",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 03:44:49 +08:00
|
|
|
|
// 检查年龄状态
|
|
|
|
|
|
if (person.BirthDate >= group.BirthDateStart && person.BirthDate <= group.BirthDateEnd)
|
|
|
|
|
|
{
|
|
|
|
|
|
hasAtLeastOneInTargetAge = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (person.BirthDate < group.BirthDateStart)
|
|
|
|
|
|
{
|
|
|
|
|
|
hasAnyBelowTargetAge = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (person.BirthDate > group.BirthDateEnd)
|
|
|
|
|
|
{
|
|
|
|
|
|
hasAnyAboveTargetAge = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证年龄
|
|
|
|
|
|
// 规则:
|
|
|
|
|
|
// 1. 所有队员年龄都在目标年龄段内 → 允许
|
|
|
|
|
|
// 2. 有队员年龄在目标年龄段内,且有队员年龄小于目标年龄段(跨组)→ 允许
|
|
|
|
|
|
// 3. 有队员年龄大于目标年龄段 → 不允许
|
|
|
|
|
|
if (hasAnyAboveTargetAge)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 找到年龄大于目标年龄段的队员
|
|
|
|
|
|
var aboveAgePerson = persons.FirstOrDefault(p => p.BirthDate > group.BirthDateEnd);
|
|
|
|
|
|
if (aboveAgePerson != null)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
2026-04-13 03:44:49 +08:00
|
|
|
|
result.ErrorMessage = $"队员{aboveAgePerson.Name}的年龄超过组别最大限制。要求出生日期不晚于:{group.BirthDateEnd:yyyy-MM-dd},实际:{aboveAgePerson.BirthDate:yyyy-MM-dd}";
|
2026-04-13 00:40:44 +08:00
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "年龄验证",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 03:44:49 +08:00
|
|
|
|
// 如果有队员年龄小于目标年龄段,需要确保至少有一名队员在目标年龄段内
|
2026-04-13 00:40:44 +08:00
|
|
|
|
if (hasAnyBelowTargetAge)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!hasAtLeastOneInTargetAge)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.IsValid = false;
|
|
|
|
|
|
result.ErrorMessage = "跨组报名时,团队中至少需要有一名成员属于目标年龄段";
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "年龄跨组验证",
|
|
|
|
|
|
IsPassed = false,
|
|
|
|
|
|
Message = result.ErrorMessage
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "年龄跨组",
|
|
|
|
|
|
IsPassed = true,
|
|
|
|
|
|
Message = "符合年龄跨组报名要求"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
2026-04-13 03:44:49 +08:00
|
|
|
|
ItemName = "年龄验证",
|
2026-04-13 00:40:44 +08:00
|
|
|
|
IsPassed = true,
|
2026-04-13 03:44:49 +08:00
|
|
|
|
Message = "所有队员均在目标年龄段内"
|
2026-04-13 00:40:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 03:44:49 +08:00
|
|
|
|
result.Details.Add(new ValidationDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemName = "队员资质",
|
|
|
|
|
|
IsPassed = true,
|
|
|
|
|
|
Message = "所有队员的性别符合组别要求,年龄符合规则"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-13 00:40:44 +08:00
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建报名明细和参赛人员
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<(decimal totalAmount, int totalPersonCount)> CreateRegistrationDetailsAsync(
|
|
|
|
|
|
EventRegistration registration, UnifiedRegistrationInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取相关数据
|
|
|
|
|
|
var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList();
|
|
|
|
|
|
var 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));
|
|
|
|
|
|
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
|
|
|
|
|
|
|
|
|
|
|
var regItems = new List<EventRegistrationItem>();
|
|
|
|
|
|
var persons = new List<EventRegistrationPerson>();
|
|
|
|
|
|
int totalPersonCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in input.Items)
|
|
|
|
|
|
{
|
|
|
|
|
|
var eventProject = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
|
|
|
|
|
|
if (eventProject == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
|
|
|
|
|
var groupName = GetGroupName(item, eventProjectGroups);
|
|
|
|
|
|
|
2026-04-13 05:20:09 +08:00
|
|
|
|
// 处理团队列表(支持多团队报名)
|
|
|
|
|
|
var teamsToProcess = new List<UnifiedTeamInput>();
|
|
|
|
|
|
|
|
|
|
|
|
if (item.Teams != null && item.Teams.Any())
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
2026-04-13 05:20:09 +08:00
|
|
|
|
// 使用新的团队列表格式
|
|
|
|
|
|
teamsToProcess.AddRange(item.Teams);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (item.Persons != null && item.Persons.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
// 向后兼容:使用旧的单个团队格式
|
|
|
|
|
|
teamsToProcess.Add(new UnifiedTeamInput
|
|
|
|
|
|
{
|
|
|
|
|
|
TeamIndex = 1,
|
|
|
|
|
|
Persons = item.Persons
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-13 00:40:44 +08:00
|
|
|
|
|
2026-04-13 05:20:09 +08:00
|
|
|
|
// 为每个团队创建报名明细
|
|
|
|
|
|
foreach (var team in teamsToProcess)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
2026-04-13 05:20:09 +08:00
|
|
|
|
// 计算费用
|
|
|
|
|
|
var (unitPrice, totalPrice) = CalculateItemPrice(feeConfig, eventProject, team.Persons.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var regItemId = _idGenerator.NewLong();
|
|
|
|
|
|
var regItem = new EventRegistrationItem
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
2026-04-13 05:20:09 +08:00
|
|
|
|
Id = regItemId,
|
2026-04-13 00:40:44 +08:00
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
ProjectId = item.ProjectId,
|
|
|
|
|
|
EventProjectGroupId = item.GroupId,
|
|
|
|
|
|
ProjectName = baseProject?.Name ?? "",
|
2026-04-13 05:20:09 +08:00
|
|
|
|
GroupName = groupName,
|
|
|
|
|
|
ProjectTypeName = baseProject?.ProjectType.Description() ?? "",
|
|
|
|
|
|
UnitPrice = unitPrice,
|
|
|
|
|
|
PersonCount = team.Persons.Count,
|
|
|
|
|
|
TotalPrice = totalPrice,
|
2026-04-13 00:40:44 +08:00
|
|
|
|
CreatedUserId = input.CreatedUserId,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
2026-04-13 05:20:09 +08:00
|
|
|
|
};
|
|
|
|
|
|
regItems.Add(regItem);
|
|
|
|
|
|
|
|
|
|
|
|
totalPersonCount += team.Persons.Count;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加参赛人员
|
|
|
|
|
|
foreach (var pInput in team.Persons)
|
|
|
|
|
|
{
|
|
|
|
|
|
persons.Add(new EventRegistrationPerson
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NewLong(),
|
|
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
RegistrationItemId = regItemId,
|
|
|
|
|
|
ProjectId = item.ProjectId,
|
|
|
|
|
|
EventProjectGroupId = item.GroupId,
|
|
|
|
|
|
GroupName = groupName,
|
|
|
|
|
|
ProjectName = baseProject?.Name ?? "",
|
|
|
|
|
|
Name = pInput.Name,
|
|
|
|
|
|
Gender = pInput.Gender,
|
|
|
|
|
|
IdCard = pInput.IdCardNumber,
|
|
|
|
|
|
BirthDate = pInput.BirthDate,
|
|
|
|
|
|
Phone = pInput.Mobile,
|
|
|
|
|
|
Age = CalculateAge(pInput.BirthDate),
|
|
|
|
|
|
PersonType = PersonType.Athlete,
|
|
|
|
|
|
UserId = pInput.UserId,
|
|
|
|
|
|
CreatedUserId = input.CreatedUserId,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-13 00:40:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await _registrationItemRepository.InsertRangeAsync(regItems);
|
|
|
|
|
|
await _registrationPersonRepository.InsertRangeAsync(persons);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算总费用
|
|
|
|
|
|
var totalAmount = CalculateTotalAmount(feeConfig, eventProjects, input.Items, totalPersonCount);
|
|
|
|
|
|
|
|
|
|
|
|
return (totalAmount, totalPersonCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 计算最终金额(含优惠券)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<(decimal originalAmount, decimal discountAmount, decimal finalAmount, YD_Event.Core.Entities.Coupon? coupon, YD_Event.Core.Entities.UserCoupon? userCoupon)>
|
|
|
|
|
|
CalculateFinalAmountAsync(decimal originalAmount, long? couponId, long userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
decimal discountAmount = 0;
|
|
|
|
|
|
decimal finalAmount = originalAmount;
|
|
|
|
|
|
YD_Event.Core.Entities.Coupon? coupon = null;
|
|
|
|
|
|
YD_Event.Core.Entities.UserCoupon? userCoupon = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (couponId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
coupon = await _couponRepository.GetFirstAsync(c =>
|
|
|
|
|
|
c.Id == couponId.Value && c.Status == AvailabilityStatus.Enable && !c.DeleteMark);
|
|
|
|
|
|
|
|
|
|
|
|
if (coupon != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
|
|
|
|
|
|
uc.CouponId == coupon.Id && uc.UserId == userId && uc.Status == 0 && !uc.DeleteMark);
|
|
|
|
|
|
|
|
|
|
|
|
if (userCoupon != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (coupon.Type == 0) // 折扣
|
|
|
|
|
|
{
|
|
|
|
|
|
discountAmount = originalAmount * (1 - coupon.Value / 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (coupon.Type == 1) // 定额
|
|
|
|
|
|
{
|
|
|
|
|
|
discountAmount = Math.Min(coupon.Value, originalAmount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
finalAmount = originalAmount - discountAmount;
|
|
|
|
|
|
finalAmount = Math.Max(0, finalAmount);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (originalAmount, discountAmount, finalAmount, coupon, userCoupon);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理免费支付
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task HandleFreePaymentAsync(EventRegistration registration, YD_Event.Core.Entities.UserCoupon? userCoupon, long operatorUserId)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (userCoupon != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
userCoupon.Status = 1;
|
|
|
|
|
|
userCoupon.UseTime = DateTime.Now;
|
|
|
|
|
|
await _userCouponRepository.UpdateAsync(userCoupon);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
registration.PaymentStatus = 1;
|
|
|
|
|
|
registration.PaymentTime = DateTime.Now;
|
|
|
|
|
|
registration.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
registration.UpdatedUserId = operatorUserId;
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建订单记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<YD_Event.Core.Entities.Order> CreateOrderAsync(EventRegistration registration, EventInfo eventInfo,
|
|
|
|
|
|
decimal originalAmount, decimal discountAmount, decimal finalAmount, long? couponId, long operatorUserId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var certification = await _merchantCertificationRepository.GetFirstAsync(c =>
|
|
|
|
|
|
c.UserId == registration.CreatedUserId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
|
|
|
|
|
|
|
|
|
|
|
|
var order = new YD_Event.Core.Entities.Order
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NewLong(),
|
|
|
|
|
|
OrderNo = registration.OrderNo,
|
|
|
|
|
|
OrderType = 1,
|
|
|
|
|
|
MerchantId = eventInfo.MerchantId,
|
|
|
|
|
|
MerchantCertificationId = certification?.Id,
|
|
|
|
|
|
EventId = registration.EventId,
|
|
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
OriginalAmount = originalAmount,
|
|
|
|
|
|
ActualAmount = finalAmount,
|
|
|
|
|
|
DiscountAmount = discountAmount,
|
|
|
|
|
|
CouponId = couponId,
|
|
|
|
|
|
OrderStatus = OrderStatus.Pending,
|
|
|
|
|
|
CreatedTime = DateTime.Now,
|
|
|
|
|
|
CreatedUserId = operatorUserId,
|
|
|
|
|
|
Status = AvailabilityStatus.Enable
|
|
|
|
|
|
};
|
|
|
|
|
|
await _orderRepository.InsertAsync(order);
|
|
|
|
|
|
return order;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理线上支付
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<UnifiedPaymentOutput> HandleOnlinePaymentAsync(EventRegistration registration,
|
|
|
|
|
|
EventInfo eventInfo, decimal finalAmount, UnifiedPaymentInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
WeChatPayConfig? payConfig = null;
|
|
|
|
|
|
if (eventInfo.MerchantId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.MerchantId == eventInfo.MerchantId.Value && x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw Oops.Bah("商户微信支付配置不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
|
|
|
|
|
|
var output = new UnifiedPaymentOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
OrderNo = registration.OrderNo,
|
|
|
|
|
|
PaymentStatus = 0,
|
|
|
|
|
|
IsFree = false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(input.OpenId))
|
|
|
|
|
|
{
|
|
|
|
|
|
// JSAPI支付
|
|
|
|
|
|
var jsapiRequest = new CreateJsApiPayRequest
|
|
|
|
|
|
{
|
|
|
|
|
|
OutTradeNo = registration.OrderNo,
|
|
|
|
|
|
AppId = configOptions.AppId,
|
|
|
|
|
|
Description = $"{eventInfo.EventName} - 报名费",
|
|
|
|
|
|
NotifyUrl = configOptions.NotifyUrl,
|
|
|
|
|
|
TotalAmount = AmountHelper.YuanToFen(finalAmount),
|
|
|
|
|
|
ExpireTime = DateTimeOffset.Now.AddMinutes(30),
|
|
|
|
|
|
OpenId = input.OpenId
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest);
|
|
|
|
|
|
if (payResult.Success && payResult.JsApiParameters != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError("微信JSAPI支付下单失败,订单号:{OrderNo},错误:{Error}", registration.OrderNo, payResult.Message);
|
|
|
|
|
|
throw Oops.Bah($"微信统一下单失败:{payResult.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Native支付
|
|
|
|
|
|
var nativeRequest = new CreateNativePayRequest
|
|
|
|
|
|
{
|
|
|
|
|
|
OutTradeNo = registration.OrderNo,
|
|
|
|
|
|
AppId = configOptions.AppId,
|
|
|
|
|
|
Description = $"{eventInfo.EventName} - 报名费",
|
|
|
|
|
|
NotifyUrl = configOptions.NotifyUrl,
|
|
|
|
|
|
TotalAmount = AmountHelper.YuanToFen(finalAmount),
|
|
|
|
|
|
ExpireTime = DateTimeOffset.Now.AddMinutes(5)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var payResult = await _weChatPayService.CreateNativePayAsync(payConfig, nativeRequest);
|
|
|
|
|
|
if (payResult.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
output.QrcodeUrl = payResult.QrcodeUrl;
|
|
|
|
|
|
output.TradeType = payResult.TradeType;
|
|
|
|
|
|
_logger.LogInformation("微信Native支付下单成功,订单号:{OrderNo}", registration.OrderNo);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError("微信Native支付下单失败,订单号:{OrderNo},错误:{Error}", registration.OrderNo, payResult.Message);
|
|
|
|
|
|
throw Oops.Bah($"微信统一下单失败:{payResult.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理线下支付
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<UnifiedPaymentOutput> HandleOfflinePaymentAsync(EventRegistration registration, UnifiedPaymentInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(input.PaymentProofUrl))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw Oops.Bah("线下支付需要上传付款凭证");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
registration.PaymentProofUrl = input.PaymentProofUrl;
|
|
|
|
|
|
registration.PaymentStatus = 0; // 待审核
|
|
|
|
|
|
registration.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
registration.UpdatedUserId = input.OperatorUserId;
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("线下支付提交审核,报名ID:{RegistrationId},凭证URL:{ProofUrl}",
|
|
|
|
|
|
registration.Id, input.PaymentProofUrl);
|
|
|
|
|
|
|
|
|
|
|
|
return new UnifiedPaymentOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RegistrationId = registration.Id,
|
|
|
|
|
|
OrderNo = registration.OrderNo,
|
|
|
|
|
|
PaymentStatus = 0, // 待审核
|
|
|
|
|
|
IsFree = false
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 检查报名限制
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<RegistrationLimitCheckOutput> CheckRegistrationLimitAsync(UnifiedRegistrationInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new RegistrationLimitCheckOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
HasLimit = false,
|
|
|
|
|
|
LimitDetails = new List<RegistrationLimitDetail>()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var limit = await _limitRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
|
|
|
|
|
var projectLimits = await _projectLimitRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
|
|
|
|
|
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId);
|
|
|
|
|
|
var projects = await _projectRepository.GetListAsync(x => eventProjects.Select(ep => ep.ProjectId).Contains(x.Id) && !x.DeleteMark);
|
|
|
|
|
|
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
|
2026-04-13 03:44:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 先获取所有报名信息,供后续检查使用
|
|
|
|
|
|
var eventRegistrations = await _registrationRepository.GetListAsync(x =>
|
|
|
|
|
|
x.EventId == input.EventId && x.PaymentStatus != 4 && !x.DeleteMark);
|
|
|
|
|
|
var eventRegistrationIds = eventRegistrations.Select(r => r.Id).ToList();
|
|
|
|
|
|
var registrationPersons = await _registrationPersonRepository.GetListAsync(x =>
|
|
|
|
|
|
eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark);
|
|
|
|
|
|
var allRegistrationItems = await _registrationItemRepository.GetListAsync(x =>
|
|
|
|
|
|
eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 检查本次提交的团队数量是否超过项目限报配置(仅检查本次提交,不统计已报名数量)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
var groupIds = input.Items.Where(i => i.GroupId.HasValue).Select(i => i.GroupId.Value).ToList();
|
|
|
|
|
|
if (groupIds.Any())
|
|
|
|
|
|
{
|
2026-04-13 03:44:49 +08:00
|
|
|
|
// 按组别分组统计本次提交的团队数量
|
|
|
|
|
|
var groupTeamCount = groupIds.GroupBy(g => g).ToDictionary(g => g.Key, g => g.Count());
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var groupId in groupTeamCount.Keys)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == groupId);
|
|
|
|
|
|
if (eventProjectGroup == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
|
|
|
|
|
if (eventProject == null) continue;
|
|
|
|
|
|
|
2026-04-13 05:20:09 +08:00
|
|
|
|
var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject.Id);
|
2026-04-13 00:40:44 +08:00
|
|
|
|
if (projectLimit == null) continue;
|
|
|
|
|
|
|
2026-04-13 03:44:49 +08:00
|
|
|
|
var submittedTeamCount = groupTeamCount[groupId];
|
|
|
|
|
|
if (submittedTeamCount > projectLimit.LimitCount)
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
|
|
|
|
|
var projectName = project?.Name ?? "未知项目";
|
|
|
|
|
|
|
|
|
|
|
|
result.HasLimit = true;
|
|
|
|
|
|
result.LimitDetails.Add(new RegistrationLimitDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
MemberId = 0,
|
|
|
|
|
|
MemberName = eventProjectGroup.GroupName ?? "未知组别",
|
|
|
|
|
|
LimitType = "ProjectGroupCapacity",
|
2026-04-13 03:44:49 +08:00
|
|
|
|
LimitMessage = $"项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"限报{projectLimit.LimitCount}队,本次提交{submittedTeamCount}队,超出限报配置"
|
2026-04-13 00:40:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 检查个人/团队项目限报
|
|
|
|
|
|
var memberIds = input.Items.SelectMany(i => i.Persons.Where(p => p.UserId.HasValue).Select(p => p.UserId.Value)).Distinct().ToList();
|
|
|
|
|
|
if (limit != null && memberIds.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
var members = await _userRepository.GetListAsync(x => memberIds.Contains(x.Id) && !x.DeleteMark);
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var member in members)
|
|
|
|
|
|
{
|
|
|
|
|
|
var matchedPersons = registrationPersons.Where(x => x.IdCard == member.IdCardNumber && !string.IsNullOrEmpty(x.IdCard)).ToList();
|
|
|
|
|
|
var personRegistrationIds = matchedPersons.Select(x => x.RegistrationId).Distinct().ToList();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取该队员已报名的所有项目项
|
|
|
|
|
|
var personRegistrationItems = allRegistrationItems.Where(ri => personRegistrationIds.Contains(ri.RegistrationId)).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
// 统计个人项目和团队项目数量
|
|
|
|
|
|
int individualProjectCount = 0;
|
|
|
|
|
|
int teamProjectCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in personRegistrationItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId);
|
|
|
|
|
|
if (eventProject == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var baseProject = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
|
|
|
|
|
if (baseProject == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据项目类型判断是个人项目还是团队项目
|
2026-04-13 03:44:49 +08:00
|
|
|
|
if (baseProject.IsIndividualProject())
|
2026-04-13 00:40:44 +08:00
|
|
|
|
{
|
2026-04-13 03:44:49 +08:00
|
|
|
|
individualProjectCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (baseProject.IsTeamProject())
|
|
|
|
|
|
{
|
|
|
|
|
|
teamProjectCount++;
|
2026-04-13 00:40:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查个人项目限报
|
|
|
|
|
|
if (individualProjectCount >= limit.IndividualProjectLimit)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.HasLimit = true;
|
|
|
|
|
|
result.LimitDetails.Add(new RegistrationLimitDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
MemberId = member.Id,
|
|
|
|
|
|
MemberName = member.Name ?? member.NickName ?? "未知",
|
|
|
|
|
|
LimitType = "IndividualProjectLimit",
|
|
|
|
|
|
LimitMessage = $"队员{member.Name}已报名{individualProjectCount}个个人项目,达到个人项目限报{limit.IndividualProjectLimit}个"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查团队项目限报
|
|
|
|
|
|
if (teamProjectCount >= limit.TeamProjectLimit)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.HasLimit = true;
|
|
|
|
|
|
result.LimitDetails.Add(new RegistrationLimitDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
MemberId = member.Id,
|
|
|
|
|
|
MemberName = member.Name ?? member.NickName ?? "未知",
|
|
|
|
|
|
LimitType = "TeamProjectLimit",
|
|
|
|
|
|
LimitMessage = $"队员{member.Name}已参与{teamProjectCount}个团队项目,达到团队项目限报{limit.TeamProjectLimit}个"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 03:44:49 +08:00
|
|
|
|
// 3. 强化人员-项目唯一性校验:同一人员不得同时在多个团队中报名同一个项目组别
|
|
|
|
|
|
var personProjectGroupMap = new Dictionary<string, List<(long GroupId, string GroupName, string ProjectName)>>();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in input.Items)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!item.GroupId.HasValue) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
|
|
|
|
|
if (eventProjectGroup == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
|
|
|
|
|
if (eventProject == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
|
|
|
|
|
var projectName = project?.Name ?? "未知项目";
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var person in item.Persons)
|
|
|
|
|
|
{
|
|
|
|
|
|
var idCard = person.IdCardNumber;
|
|
|
|
|
|
if (string.IsNullOrEmpty(idCard)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (!personProjectGroupMap.ContainsKey(idCard))
|
|
|
|
|
|
{
|
|
|
|
|
|
personProjectGroupMap[idCard] = new List<(long GroupId, string GroupName, string ProjectName)>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
personProjectGroupMap[idCard].Add((item.GroupId.Value, eventProjectGroup.GroupName ?? "未知组别", projectName));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查同一人员在同一项目组别中是否出现多次
|
|
|
|
|
|
foreach (var kvp in personProjectGroupMap)
|
|
|
|
|
|
{
|
|
|
|
|
|
var idCard = kvp.Key;
|
|
|
|
|
|
var groupInfos = kvp.Value;
|
|
|
|
|
|
|
|
|
|
|
|
// 按组别分组
|
|
|
|
|
|
var groupDuplicates = groupInfos
|
|
|
|
|
|
.GroupBy(g => g.GroupId)
|
|
|
|
|
|
.Where(g => g.Count() > 1)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
if (groupDuplicates.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var duplicate in groupDuplicates)
|
|
|
|
|
|
{
|
|
|
|
|
|
var groupInfo = duplicate.First();
|
|
|
|
|
|
var person = input.Items.SelectMany(i => i.Persons).FirstOrDefault(p => p.IdCardNumber == idCard);
|
|
|
|
|
|
var personName = person?.Name ?? "未知人员";
|
|
|
|
|
|
|
|
|
|
|
|
result.HasLimit = true;
|
|
|
|
|
|
result.LimitDetails.Add(new RegistrationLimitDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
MemberId = 0,
|
|
|
|
|
|
MemberName = personName,
|
|
|
|
|
|
LimitType = "PersonProjectDuplicate",
|
|
|
|
|
|
LimitMessage = $"人员\"{personName}\"(身份证:{idCard})在项目\"{groupInfo.ProjectName}\"的组别\"{groupInfo.GroupName}\"中重复出现,同一人员只能在一个团队中报名同一项目组别"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 检查人员是否已在其他报名中报过同一项目组别
|
|
|
|
|
|
if (eventRegistrationIds.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var item in input.Items)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!item.GroupId.HasValue) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value);
|
|
|
|
|
|
if (eventProjectGroup == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
|
|
|
|
|
|
if (eventProject == null) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
|
|
|
|
|
|
var projectName = project?.Name ?? "未知项目";
|
|
|
|
|
|
|
|
|
|
|
|
// 查找该组别已有的报名项
|
|
|
|
|
|
var existingItems = allRegistrationItems.Where(ri => ri.EventProjectGroupId == item.GroupId.Value).ToList();
|
|
|
|
|
|
if (!existingItems.Any()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var existingItemIds = existingItems.Select(ri => ri.Id).ToList();
|
|
|
|
|
|
var existingPersons = registrationPersons.Where(rp => existingItemIds.Contains(rp.RegistrationItemId)).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var person in item.Persons)
|
|
|
|
|
|
{
|
|
|
|
|
|
var idCard = person.IdCardNumber;
|
|
|
|
|
|
if (string.IsNullOrEmpty(idCard)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
var existingPerson = existingPersons.FirstOrDefault(ep => ep.IdCard == idCard);
|
|
|
|
|
|
if (existingPerson != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.HasLimit = true;
|
|
|
|
|
|
result.LimitDetails.Add(new RegistrationLimitDetail
|
|
|
|
|
|
{
|
|
|
|
|
|
MemberId = 0,
|
|
|
|
|
|
MemberName = person.Name,
|
|
|
|
|
|
LimitType = "PersonAlreadyRegistered",
|
|
|
|
|
|
LimitMessage = $"人员\"{person.Name}\"(身份证:{idCard})已在项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"中报名过,不能重复报名"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 00:40:44 +08:00
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取组别名称
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private string GetGroupName(UnifiedRegistrationItemInput item, List<EventProjectGroup> eventProjectGroups)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!string.IsNullOrEmpty(item.GroupName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return item.GroupName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (item.GroupId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var selectedGroup = eventProjectGroups.FirstOrDefault(eg => eg.Id == item.GroupId.Value);
|
|
|
|
|
|
if (selectedGroup != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return selectedGroup.GroupName ?? "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var firstGroup = eventProjectGroups.FirstOrDefault(eg => eg.EventProjectId == item.ProjectId);
|
|
|
|
|
|
return firstGroup?.GroupName ?? "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 计算单个项目费用
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private (decimal unitPrice, decimal totalPrice) CalculateItemPrice(EventFeeConfig? feeConfig, EventProject eventProject, int personCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
decimal unitPrice = 0;
|
|
|
|
|
|
decimal totalPrice = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (feeConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return (0, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (feeConfig.ChargeMode)
|
|
|
|
|
|
{
|
|
|
|
|
|
case ChargeMode.Free:
|
|
|
|
|
|
unitPrice = 0;
|
|
|
|
|
|
totalPrice = 0;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case ChargeMode.FixedPrice:
|
|
|
|
|
|
unitPrice = feeConfig.Price;
|
|
|
|
|
|
totalPrice = unitPrice * personCount;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case ChargeMode.UnitPriceEvent:
|
|
|
|
|
|
unitPrice = feeConfig.Price;
|
|
|
|
|
|
totalPrice = unitPrice * personCount;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case ChargeMode.UnitPriceProject:
|
|
|
|
|
|
unitPrice = eventProject.Price / 100m;
|
|
|
|
|
|
totalPrice = unitPrice * personCount;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (unitPrice, totalPrice);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 计算总费用
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private decimal CalculateTotalAmount(EventFeeConfig? feeConfig, List<EventProject> eventProjects,
|
|
|
|
|
|
List<UnifiedRegistrationItemInput> items, int totalPersonCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (feeConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pricingInput = new PricingInput
|
|
|
|
|
|
{
|
|
|
|
|
|
ChargeMode = feeConfig.ChargeMode,
|
|
|
|
|
|
ParticipantCount = totalPersonCount,
|
|
|
|
|
|
EventPrice = feeConfig.Price
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in items)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ep = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
|
|
|
|
|
|
if (ep != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
pricingInput.Projects.Add(new ProjectPricingInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
ProjectId = ep.Id,
|
|
|
|
|
|
UnitPrice = ep.Price
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var pricingResult = EventPricingService.Calculate(pricingInput);
|
|
|
|
|
|
return pricingResult.TotalAmount;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据出生日期计算年龄
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static int CalculateAge(DateTime birthDate)
|
|
|
|
|
|
{
|
|
|
|
|
|
var today = DateTime.Today;
|
|
|
|
|
|
var age = today.Year - birthDate.Year;
|
|
|
|
|
|
if (birthDate.Date > today.AddYears(-age)) age--;
|
|
|
|
|
|
return age;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理支付成功
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task HandlePaymentSuccessAsync(string orderNo, string transactionId, DateTime? successTime, long operatorUserId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == orderNo);
|
|
|
|
|
|
if (reg == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning($"未找到订单:{orderNo}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (reg.PaymentStatus == 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
|
|
|
|
|
|
if (evt == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError($"未找到赛事信息,订单号:{reg.OrderNo},赛事ID:{reg.EventId}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("订单支付成功,订单号={OrderNo},赛事ID={EventId},商户ID={MerchantId}",
|
|
|
|
|
|
reg.OrderNo, evt.Id, evt.MerchantId);
|
|
|
|
|
|
|
|
|
|
|
|
reg.PaymentStatus = 1;
|
|
|
|
|
|
reg.PaymentTime = successTime;
|
|
|
|
|
|
reg.RegistrationRemark = transactionId;
|
|
|
|
|
|
reg.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
reg.UpdatedUserId = operatorUserId;
|
|
|
|
|
|
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(reg);
|
|
|
|
|
|
_logger.LogInformation($"订单 {reg.OrderNo} 支付成功,微信交易号:{transactionId}");
|
|
|
|
|
|
|
|
|
|
|
|
var payment = new EventRegistrationPayment
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NewLong(),
|
|
|
|
|
|
RegistrationId = reg.Id,
|
|
|
|
|
|
TransactionNo = transactionId ?? "",
|
|
|
|
|
|
PaymentChannel = 1,
|
|
|
|
|
|
PaymentAmount = reg.TotalAmount,
|
|
|
|
|
|
PaymentTime = successTime ?? DateTime.Now,
|
|
|
|
|
|
PaymentStatus = 1,
|
|
|
|
|
|
RefundStatus = 0,
|
|
|
|
|
|
PayeeAccount = "",
|
|
|
|
|
|
CreatedUserId = reg.CreatedUserId,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
await _paymentRepository.InsertAsync(payment);
|
|
|
|
|
|
_logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={transactionId}");
|
|
|
|
|
|
|
|
|
|
|
|
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
|
|
|
|
|
|
if (order != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
order.OrderStatus = OrderStatus.Paid;
|
|
|
|
|
|
order.PaymentTime = successTime;
|
|
|
|
|
|
order.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
await _orderRepository.UpdateAsync(order);
|
|
|
|
|
|
_logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 取消报名
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<bool> CancelRegistrationAsync(long registrationId, long operatorUserId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == operatorUserId);
|
|
|
|
|
|
if (reg == null) throw Oops.Bah("报名记录不存在");
|
|
|
|
|
|
|
|
|
|
|
|
if (reg.PaymentStatus == 1) throw Oops.Bah("已支付订单请联系客服退款");
|
|
|
|
|
|
if (reg.PaymentStatus == 4) throw Oops.Bah("订单已取消,无需重复操作");
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("开始取消报名:报名ID={RegistrationId}, 操作人={UserId}, 当前状态={PaymentStatus}",
|
|
|
|
|
|
registrationId, operatorUserId, reg.PaymentStatus);
|
|
|
|
|
|
|
|
|
|
|
|
reg.PaymentStatus = 4;
|
|
|
|
|
|
reg.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
reg.UpdatedUserId = operatorUserId;
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(reg);
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(reg.OrderNo))
|
|
|
|
|
|
{
|
|
|
|
|
|
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
|
|
|
|
|
|
if (order != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
order.OrderStatus = OrderStatus.Cancelled;
|
|
|
|
|
|
order.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
order.UpdatedUserId = operatorUserId;
|
|
|
|
|
|
await _orderRepository.UpdateAsync(order);
|
|
|
|
|
|
_logger.LogInformation("订单状态已同步更新:订单号={OrderNo}, 状态={OrderStatus}",
|
|
|
|
|
|
reg.OrderNo, OrderStatus.Cancelled);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning("未找到对应的订单记录:报名ID={RegistrationId}, 订单号={OrderNo}",
|
|
|
|
|
|
registrationId, reg.OrderNo);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("取消报名成功:报名ID={RegistrationId}", registrationId);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 查询并同步微信支付状态
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<bool> QueryAndSyncWeChatPaymentStatusAsync(long registrationId, long operatorUserId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == operatorUserId);
|
|
|
|
|
|
if (reg == null) throw Oops.Bah("报名记录不存在");
|
|
|
|
|
|
|
|
|
|
|
|
if (reg.PaymentStatus == 1) return true;
|
|
|
|
|
|
|
|
|
|
|
|
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
|
|
|
|
|
|
if (evt == null) throw Oops.Bah("赛事不存在");
|
|
|
|
|
|
|
|
|
|
|
|
WeChatPayConfig? payConfig = null;
|
|
|
|
|
|
if (evt.MerchantId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.MerchantId == evt.MerchantId.Value &&
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable &&
|
|
|
|
|
|
!x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId);
|
|
|
|
|
|
throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, reg.OrderNo);
|
|
|
|
|
|
if (queryResult.Success && queryResult.TradeState == "SUCCESS")
|
|
|
|
|
|
{
|
|
|
|
|
|
await HandlePaymentSuccessAsync(reg.OrderNo, queryResult.TransactionId, queryResult.SuccessTime?.DateTime, operatorUserId);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取平台级别的微信支付配置
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<WeChatPayConfig?> GetPlatformWeChatPayConfigAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
return await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据赛事获取对应的微信支付配置
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<WeChatPayConfig?> GetWeChatPayConfigByEventAsync(EventInfo evt)
|
|
|
|
|
|
{
|
|
|
|
|
|
WeChatPayConfig? payConfig = null;
|
|
|
|
|
|
if (evt.MerchantId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.MerchantId == evt.MerchantId.Value &&
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable &&
|
|
|
|
|
|
!x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await GetPlatformWeChatPayConfigAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return payConfig;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|