diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index 0f3db7d..559b2e6 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using YD_Event.Application.Client.Dtos; +using YD_Event.Application.Event; +using YD_Event.Application.Event.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using Easy.Core; @@ -61,6 +63,7 @@ public class EventClientController : IDynamicApiController private readonly ILogger _logger; private readonly OrderNoGeneratorService _orderNoGeneratorService; private readonly YD_Event.Application.Banner.BannerService _bannerService; + private readonly UnifiedRegistrationService _unifiedRegistrationService; public EventClientController( ISqlSugarRepository eventRepository, @@ -95,7 +98,8 @@ public class EventClientController : IDynamicApiController IOptionsMonitor wechatTenpayClientOptionsMonitor, ILogger logger, OrderNoGeneratorService orderNoGeneratorService, - YD_Event.Application.Banner.BannerService bannerService) + YD_Event.Application.Banner.BannerService bannerService, + UnifiedRegistrationService unifiedRegistrationService) { _eventRepository = eventRepository; _projectRepository = projectRepository; @@ -130,6 +134,7 @@ public class EventClientController : IDynamicApiController _logger = logger; _orderNoGeneratorService = orderNoGeneratorService; _bannerService = bannerService; + _unifiedRegistrationService = unifiedRegistrationService; } private long GetCurrentUserId() @@ -452,7 +457,7 @@ public class EventClientController : IDynamicApiController IndividualProjectLimit = limit?.IndividualProjectLimit ?? 0, TeamProjectLimit = limit?.TeamProjectLimit ?? 0, - Units = units.Select(u => new EventUnitDto + Units = units.Select(u => new YD_Event.Application.Client.Dtos.EventUnitDto { UnitType = u.UnitType, UnitName = u.UnitName, @@ -608,17 +613,10 @@ public class EventClientController : IDynamicApiController var userId = GetCurrentUserId(); // 0. 校验报名模式 - if (input.RegistrationMode == 2) // 假设2为团队 + if (input.RegistrationMode == 2) throw Oops.Bah("团队报名请前往网页端进行操作"); - var evt = await _eventRepository.GetFirstAsync(e => e.Id == input.EventId); - if (evt == null) throw Oops.Bah("赛事不存在"); - - // 1. 校验时间 - if (DateTime.Now < evt.RegistrationStartTime || DateTime.Now > evt.RegistrationEndTime) - throw Oops.Bah("不在报名时间内"); - - // 2. 校验重复报名 - 如果已有未取消的报名记录,直接返回现有记录ID + // 校验重复报名 - 如果已有未取消的报名记录,直接返回现有记录ID var existingRegistration = await _registrationRepository.GetFirstAsync( r => r.EventId == input.EventId && r.CreatedUserId == userId && r.PaymentStatus != 4); // 4=已取消 @@ -628,12 +626,12 @@ public class EventClientController : IDynamicApiController return existingRegistration.Id; } - // 3. 获取当前用户信息(用于个人报名时自动填充) + // 获取当前用户信息(用于个人报名时自动填充) var appUser = await _appUserRepository.GetFirstAsync(u => u.CreatedUserId == userId && !u.DeleteMark); if (appUser == null && input.RegistrationMode == 1) throw Oops.Bah("请先完成身份认证"); - // 3.5 个人报名模式下自动填充联系人信息 + // 个人报名模式下自动填充联系人信息 if (input.RegistrationMode == 1) { if (string.IsNullOrWhiteSpace(input.ContactName)) @@ -650,269 +648,61 @@ public class EventClientController : IDynamicApiController throw Oops.Bah("联系人电话不能为空,请先完善身份认证信息"); } - // 4. 校验项目有效性 & 限制 - if (input.Items == null || !input.Items.Any()) throw Oops.Bah("请至少选择一个项目"); - - var limit = await _limitRepository.GetFirstAsync(l => l.EventId == input.EventId && !l.DeleteMark); - if (limit != null) + // 个人报名模式下自动填充参赛人员信息 + if (input.RegistrationMode == 1 && input.Items != null) { - if (input.Items.Count > limit.IndividualProjectLimit) - throw Oops.Bah($"每人最多可报{limit.IndividualProjectLimit}个项目"); - } - - var projectLimits = await _projectLimitRepository.AsQueryable() - .Where(pl => pl.EventId == input.EventId && !pl.DeleteMark) - .ToListAsync(); - - foreach (var item in input.Items) - { - var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == item.ProjectId); - if (projectLimit != null) + foreach (var item in input.Items) { - var currentCount = await _registrationItemRepository.AsQueryable() - .InnerJoin((ri, r) => ri.RegistrationId == r.Id) - .Where((ri, r) => ri.ProjectId == item.ProjectId && r.PaymentStatus != 4) - .CountAsync(); - - if (currentCount >= projectLimit.LimitCount) + if (item.Persons == null || !item.Persons.Any()) { - var eventProject = await _eventProjectRepository.GetFirstAsync(ep => ep.Id == item.ProjectId); - var project = await _projectRepository.GetFirstAsync(p => p.Id == eventProject.ProjectId); - throw Oops.Bah($"项目[{project?.Name}]已达到报名上限,无法继续报名"); + if (appUser == null) + throw Oops.Bah("请先完成身份认证"); + + item.Persons = new List + { + new PersonInput + { + Name = appUser.Name ?? "", + Gender = appUser.Gender, + IdCardNumber = appUser.IdCardNumber ?? "", + BirthDate = appUser.BirthDate ?? DateTime.Now, + Mobile = appUser.Mobile + } + }; } } - - // 个人报名模式下自动填充参赛人员信息 - if (input.RegistrationMode == 1 && (item.Persons == null || !item.Persons.Any())) - { - if (appUser == null) - throw Oops.Bah("请先完成身份认证"); - - item.Persons = new List - { - new PersonInput - { - Name = appUser.Name ?? "", - Gender = appUser.Gender, - IdCardNumber = appUser.IdCardNumber ?? "", - BirthDate = appUser.BirthDate ?? DateTime.Now, - Mobile = appUser.Mobile - } - }; - } - - // 最终验证参赛人员信息 - if (item.Persons == null || !item.Persons.Any()) - throw Oops.Bah($"项目未填写参赛人信息"); } - - // 5. 创建报名主表 - var registrationId = _idGenerator.NewLong(); - var registration = new EventRegistration + + // 转换为统一输入模型 + var unifiedInput = new UnifiedRegistrationInput { - Id = registrationId, EventId = input.EventId, RegistrationMode = input.RegistrationMode, - TeamName = input.RegistrationMode == 2 ? input.TeamName : null, - ContactName = input.ContactName, - ContactPhone = input.ContactPhone, - PaymentStatus = 0, // 未支付 - RegistrationTime = DateTime.Now, + TeamName = input.TeamName, + ContactName = input.ContactName ?? "", + ContactPhone = input.ContactPhone ?? "", CreatedUserId = userId, - CreatedTime = DateTime.Now, - Status = AvailabilityStatus.Enable, - TotalPersonCount = 0 // 稍后计算 - }; - - // 5. 获取费用配置并使用统一的定价服务计算费用 - var feeConfig = await _feeConfigRepository.GetFirstAsync(f => f.EventId == input.EventId && !f.DeleteMark); - - // 先获取所有需要的 eventProjectGroup 信息和 eventProject 信息 - var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList(); - var allEventProjectGroups = await _eventProjectGroupRepository.GetListAsync( - eg => eventProjectIds.Contains(eg.EventProjectId) && !eg.DeleteMark); - var eventProjects = await _eventProjectRepository.AsQueryable() - .Where(ep => eventProjectIds.Contains(ep.Id)) - .ToListAsync(); - var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList(); - var baseProjects = await _projectRepository.AsQueryable() - .Where(p => projectIds.Contains(p.Id)) - .ToListAsync(); - - // 计算总人数 - int totalPerson = input.Items.Sum(i => i.Persons!.Count); - - // 构建定价服务输入 - var pricingInput = new PricingInput - { - ChargeMode = evt.ChargeMode, - ParticipantCount = totalPerson, - EventPrice = feeConfig?.Price ?? 0m - }; - - // 添加项目信息(用于单价项目模式) - foreach (var item in input.Items) - { - var ep = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId); - var bp = baseProjects.FirstOrDefault(p => p.Id == ep?.ProjectId); - - if (ep != null) + IsDraft = false, + Items = input.Items?.Select(item => new UnifiedRegistrationItemInput { - pricingInput.Projects.Add(new ProjectPricingInfo - { - ProjectId = ep.Id, - ProjectName = bp?.Name ?? "", - UnitPrice = ep.Price / 100m - }); - } - } - - // 使用统一的定价服务计算费用 - var pricingResult = YD_Event.Core.Domain.Pricing.EventPricingService.Calculate(pricingInput); - - // 6. 创建报名明细 & 参赛人员 - var regItems = new List(); - var persons = new List(); - - foreach (var item in input.Items) - { - var eventProject = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId); - if (eventProject == null) throw Oops.Bah($"项目不存在: {item.ProjectId}"); - - var project = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId); - if (project == null) throw Oops.Bah($"关联基础项目不存在: {item.ProjectId}"); - - // 获取项目类型名称 - var projectTypeName = project.ProjectType.Description(); - - // 获取组别名称 - 优先从前端传的获取,否则从数据库获取默认组别 - var groupName = item.GroupName; - if (string.IsNullOrWhiteSpace(groupName)) - { - var eventProjectGroups = allEventProjectGroups.Where(eg => eg.EventProjectId == item.ProjectId).ToList(); - if (item.GroupId.HasValue) - { - // 如果传了 groupId,优先使用 - var selectedGroup = eventProjectGroups.FirstOrDefault(eg => eg.Id == item.GroupId.Value); - groupName = selectedGroup?.GroupName ?? ""; - } - - // 如果还是没有,使用第一个组别 - if (string.IsNullOrWhiteSpace(groupName)) - { - var firstGroup = eventProjectGroups.FirstOrDefault(); - groupName = firstGroup?.GroupName ?? ""; - } - - // 如果还是没有,设置默认值 - if (string.IsNullOrWhiteSpace(groupName)) - { - groupName = "默认组别"; - } - } - - // 根据收费模式计算该项目的单价和总价 - decimal unitPrice = 0; - decimal totalPrice = 0; - - switch (evt.ChargeMode) - { - case ChargeMode.Free: - unitPrice = 0; - totalPrice = 0; - break; - - case ChargeMode.FixedPrice: - // 定价(赛事):使用赛事费用配置的单价 - unitPrice = feeConfig?.Price ?? 0; - totalPrice = unitPrice * item.Persons!.Count; - break; - - case ChargeMode.UnitPriceEvent: - // 单价(赛事):使用赛事单价 - unitPrice = feeConfig?.Price ?? 0; - totalPrice = unitPrice * item.Persons!.Count; - break; - - case ChargeMode.UnitPriceProject: - // 单价(项目):使用项目单价 - unitPrice = eventProject.Price / 100m; - totalPrice = unitPrice * item.Persons!.Count; - break; - } - - var regItemId = _idGenerator.NewLong(); - var regItem = new EventRegistrationItem - { - Id = regItemId, - RegistrationId = registrationId, ProjectId = item.ProjectId, - ProjectName = project.Name, - ProjectTypeName = projectTypeName, - GroupName = groupName, - UnitPrice = unitPrice, - PersonCount = item.Persons!.Count, - TotalPrice = totalPrice, - CreatedUserId = userId, - CreatedTime = DateTime.Now - }; - regItems.Add(regItem); - - // 添加参赛人员 - foreach (var pInput in item.Persons) - { - persons.Add(new EventRegistrationPerson + GroupId = item.GroupId, + GroupName = item.GroupName, + Persons = item.Persons?.Select(p => new UnifiedPersonInput { - Id = _idGenerator.NewLong(), - RegistrationId = registrationId, - RegistrationItemId = regItemId, - ProjectId = item.ProjectId, - PersonType = PersonType.Athlete, // 运动员 - Name = pInput.Name, - Gender = pInput.Gender, - IdCard = pInput.IdCardNumber, - BirthDate = pInput.BirthDate, - Phone = pInput.Mobile, - GroupName = groupName, - ProjectName = project.Name, - CreatedUserId = userId, - CreatedTime = DateTime.Now - }); - } - } - - registration.TotalAmount = pricingResult.TotalAmount; - registration.TotalPersonCount = totalPerson; - registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); // 使用统一的订单号生成器 - - await _registrationRepository.InsertAsync(registration); - await _registrationItemRepository.InsertRangeAsync(regItems); - await _personRepository.InsertRangeAsync(persons); - - // 创建订单记录 - var certification = await _merchantCertificationRepository.GetFirstAsync(c => - c.UserId == userId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark); - - var order = new YD_Event.Core.Entities.Order - { - Id = _idGenerator.NewLong(), - OrderNo = registration.OrderNo, - OrderType = 1, // 收入 - MerchantId = evt.MerchantId, - MerchantCertificationId = certification?.Id, - EventId = input.EventId, - RegistrationId = registrationId, - OriginalAmount = pricingResult.TotalAmount, - DiscountAmount = 0, - ActualAmount = pricingResult.TotalAmount, - OrderStatus = OrderStatus.Pending, // 待支付 - CreatedTime = DateTime.Now, - CreatedUserId = userId, - Status = AvailabilityStatus.Enable + Name = p.Name, + Gender = p.Gender, + IdCardNumber = p.IdCardNumber, + BirthDate = p.BirthDate, + Mobile = p.Mobile + }).ToList() ?? new List() + }).ToList() ?? new List() }; - await _orderRepository.InsertAsync(order); - return registrationId; + // 调用统一报名服务 + var result = await _unifiedRegistrationService.ProcessRegistrationAsync(unifiedInput); + + return result.RegistrationId; } /// @@ -929,68 +719,44 @@ public class EventClientController : IDynamicApiController public async Task GetPaymentInfo([FromBody] GetPaymentInfoInput input) { var userId = GetCurrentUserId(); + + // 验证报名记录归属权 var reg = await _registrationRepository.GetFirstAsync(r => r.Id == input.RegistrationId && r.CreatedUserId == userId); if (reg == null) throw Oops.Bah("报名记录不存在"); var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId); if (evt == null) throw Oops.Bah("赛事不存在"); - var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue; var merchant = await _merchantRepository.GetFirstAsync(m => m.Id == evt.MerchantId && !m.DeleteMark); - var paymentInfo = new PaymentInfoDto + + // 如果支付方式已经设置为微信支付或免费,直接调用统一支付服务 + int paymentMethod = reg.PaymentMethod == 0 ? 2 : reg.PaymentMethod; + + // 构建统一支付输入 + var unifiedInput = new UnifiedPaymentInput { - RegistrationId = reg.Id, - Amount = reg.TotalAmount, - PaymentMethod = reg.PaymentMethod, - BankAccountName = merchant?.MerchantName ?? "上海跃动文化科技有限公司", - BankAccountNo = merchant?.AccountNumber ?? "456498465198456261", - BankName = merchant?.BankName ?? "招商银行" + RegistrationId = input.RegistrationId, + PaymentMethod = paymentMethod, + CouponId = null, + OpenId = input.OpenId, + PaymentProofUrl = null, + OperatorUserId = userId }; - if (reg.PaymentMethod == 2 && reg.TotalAmount > 0) + // 调用统一支付服务 + var unifiedResult = await _unifiedRegistrationService.ProcessPaymentAsync(unifiedInput); + + // 构建返回结果 + var paymentInfo = new PaymentInfoDto { - // 根据赛事信息获取对应的商户支付配置 - var payConfig = await GetWeChatPayConfigByEventAsync(evt); - if (payConfig == null) - { - _logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId); - throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员"); - } - - _logger.LogInformation("开始创建微信 JSAPI 支付订单,订单号={OrderNo},OpenId={OpenId},商户ID={MerchantId}", - reg.OrderNo, input.OpenId, payConfig.MerchantId); - - var jsapiRequest = new CreateJsApiPayRequest - { - OutTradeNo = reg.OrderNo, - AppId = configOptions.AppId, - Description = $"{evt.EventName} - 报名费", - NotifyUrl = configOptions.NotifyUrl, - TotalAmount = AmountHelper.YuanToFen(reg.TotalAmount), - ExpireTime = DateTimeOffset.Now.AddMinutes(30), - OpenId = input.OpenId - }; - - var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest); - if (payResult.Success && payResult.JsApiParameters != null) - { - _logger.LogInformation("微信 JSAPI 支付下单成功,订单号={OrderNo}", reg.OrderNo); - paymentInfo.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 - }; - } - else - { - _logger.LogError("微信 JSAPI 支付下单失败:订单号={OrderNo},错误信息={Message}", reg.OrderNo, payResult.Message); - throw Oops.Bah($"微信支付下单失败:{payResult.Message}"); - } - } + RegistrationId = unifiedResult.RegistrationId, + Amount = unifiedResult.ActualAmount, + PaymentMethod = unifiedResult.PaymentStatus == 1 ? 0 : paymentMethod, // 如果已支付,不需要显示支付方式 + BankAccountName = merchant?.MerchantName, + BankAccountNo = merchant?.AccountNumber, + BankName = merchant?.BankName, + WechatPayParams = unifiedResult.WechatPayParams + }; return paymentInfo; } @@ -1034,19 +800,30 @@ public class EventClientController : IDynamicApiController /// [HttpPost("upload-voucher/{registrationId}")] [DisplayName("上传支付凭证")] - public async Task UploadPaymentVoucher(long registrationId, [FromBody] UploadVoucherInput input) + [UnitOfWork] + public async Task UploadPaymentVoucher(long registrationId, [FromBody] YD_Event.Application.Client.Dtos.UploadVoucherInput input) { var userId = GetCurrentUserId(); + + // 验证报名记录归属权 var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); if (reg == null) throw Oops.Bah("报名记录不存在"); - reg.PaymentMethod = 1; // 线下 - reg.PaymentProofUrl = input.VoucherUrl; - reg.PaymentStatus = 0; // 待审核 - reg.UpdatedTime = DateTime.Now; - reg.UpdatedUserId = userId; + // 构建统一支付输入 + var unifiedInput = new UnifiedPaymentInput + { + RegistrationId = registrationId, + PaymentMethod = 1, // 线下支付 + CouponId = null, + OpenId = null, + PaymentProofUrl = input.VoucherUrl, + OperatorUserId = userId + }; + + // 调用统一支付服务 + await _unifiedRegistrationService.ProcessPaymentAsync(unifiedInput); - return await _registrationRepository.UpdateAsync(reg); + return true; } /// @@ -1217,45 +994,7 @@ public class EventClientController : IDynamicApiController public async Task CancelRegistration(long registrationId) { var userId = GetCurrentUserId(); - var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); - 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, userId, reg.PaymentStatus); - - // 更新报名状态 - reg.PaymentStatus = 4; // 4=已取消 - reg.UpdatedTime = DateTime.Now; - reg.UpdatedUserId = userId; - 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 = userId; - 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; + return await _unifiedRegistrationService.CancelRegistrationAsync(registrationId, userId); } /// @@ -1290,6 +1029,18 @@ public class EventClientController : IDynamicApiController } } + // 构建统一支付输入 + var unifiedInput = new UnifiedPaymentInput + { + RegistrationId = input.RegistrationId, + PaymentMethod = input.PaymentMethod, + CouponId = null, + OpenId = input.OpenId, + PaymentProofUrl = null, + OperatorUserId = userId + }; + + // 调用统一支付服务(仅设置支付方式,不处理实际支付) reg.PaymentMethod = input.PaymentMethod; reg.UpdatedTime = DateTime.Now; reg.UpdatedUserId = userId; @@ -1309,68 +1060,7 @@ public class EventClientController : IDynamicApiController public async Task SyncPaymentStatus(long registrationId) { var userId = GetCurrentUserId(); - var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); - 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("赛事不存在"); - - // 根据赛事信息获取对应的商户支付配置 - var payConfig = await GetWeChatPayConfigByEventAsync(evt); - 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") - { - // 更新支付状态 - reg.PaymentStatus = 1; - reg.PaymentTime = queryResult.SuccessTime?.DateTime; - reg.RegistrationRemark = queryResult.TransactionId; // 将第三方交易号存储在备注字段 - reg.UpdatedTime = DateTime.Now; - reg.UpdatedUserId = userId; - - await _registrationRepository.UpdateAsync(reg); - _logger.LogInformation($"订单 {reg.OrderNo} 支付状态同步成功,微信交易号:{queryResult.TransactionId}"); - - // 创建支付记录 - var payment = new EventRegistrationPayment - { - Id = _idGenerator.NewLong(), - RegistrationId = reg.Id, - TransactionNo = queryResult.TransactionId ?? "", - PaymentChannel = 1, // 微信支付 - PaymentAmount = reg.TotalAmount, - PaymentTime = queryResult.SuccessTime?.DateTime ?? DateTime.Now, - PaymentStatus = 1, // 已支付 - RefundStatus = 0, // 未退款 - PayeeAccount = "", // 收款账户 - CreatedUserId = userId, - CreatedTime = DateTime.Now - }; - await _paymentRepository.InsertAsync(payment); - _logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={queryResult.TransactionId}"); - - // 更新 Order 记录状态 - var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo); - if (order != null) - { - order.OrderStatus = OrderStatus.Paid; - order.PaymentTime = queryResult.SuccessTime?.DateTime; - order.UpdatedTime = DateTime.Now; - await _orderRepository.UpdateAsync(order); - _logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付"); - } - - return true; - } - - return false; + return await _unifiedRegistrationService.QueryAndSyncWeChatPaymentStatusAsync(registrationId, userId); } /// @@ -1653,7 +1343,7 @@ public class EventClientController : IDynamicApiController /// 操作是否成功 [HttpPost("upload-material")] [DisplayName("上传赛事素材")] - public async Task UploadMaterial([FromBody] UploadMaterialInput input) + public async Task UploadMaterial([FromBody] YD_Event.Application.Client.Dtos.UploadMaterialInput input) { var userId = GetCurrentUserId(); diff --git a/YD_Event.Application/Event/Dtos/UnifiedRegistrationDtos.cs b/YD_Event.Application/Event/Dtos/UnifiedRegistrationDtos.cs new file mode 100644 index 0000000..a459339 --- /dev/null +++ b/YD_Event.Application/Event/Dtos/UnifiedRegistrationDtos.cs @@ -0,0 +1,315 @@ +using System.ComponentModel.DataAnnotations; +using YD_Event.Core.Enum; + +namespace YD_Event.Application.Event.Dtos; + +/// +/// 统一报名输入DTO +/// +public class UnifiedRegistrationInput +{ + /// + /// 赛事ID + /// + [Required(ErrorMessage = "赛事ID不能为空")] + public long EventId { get; set; } + + /// + /// 报名模式 (1-个人 2-团队) + /// + [Required(ErrorMessage = "报名模式不能为空")] + [Range(1, 2, ErrorMessage = "报名模式只能是1(个人)或2(团队)")] + public int RegistrationMode { get; set; } + + /// + /// 团队名称 (团队报名必填) + /// + [MaxLength(100, ErrorMessage = "团队名称不能超过100个字符")] + public string? TeamName { get; set; } + + /// + /// 联系人姓名 + /// + [Required(ErrorMessage = "联系人姓名不能为空")] + [MaxLength(50, ErrorMessage = "联系人姓名不能超过50个字符")] + public string ContactName { get; set; } = string.Empty; + + /// + /// 联系人电话 + /// + [Required(ErrorMessage = "联系人电话不能为空")] + [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号格式")] + public string ContactPhone { get; set; } = string.Empty; + + /// + /// 报名项目列表 + /// + [Required(ErrorMessage = "报名项目列表不能为空")] + [MinLength(1, ErrorMessage = "请至少选择一个项目")] + public List Items { get; set; } = new(); + + /// + /// 创建用户ID + /// + public long CreatedUserId { get; set; } + + /// + /// 是否为草稿 + /// + public bool IsDraft { get; set; } +} + +/// +/// 统一报名项目输入DTO +/// +public class UnifiedRegistrationItemInput +{ + /// + /// 项目ID + /// + [Required(ErrorMessage = "项目ID不能为空")] + public long ProjectId { get; set; } + + /// + /// 组别ID + /// + public long? GroupId { get; set; } + + /// + /// 组别名称 + /// + public string? GroupName { get; set; } + + /// + /// 参赛人员列表 + /// + [Required(ErrorMessage = "参赛人员列表不能为空")] + [MinLength(1, ErrorMessage = "请至少填写一个参赛人员")] + public List Persons { get; set; } = new(); +} + +/// +/// 统一参赛人员输入DTO +/// +public class UnifiedPersonInput +{ + /// + /// 用户ID (可选,已有用户时使用) + /// + public long? UserId { get; set; } + + /// + /// 姓名 + /// + [Required(ErrorMessage = "姓名不能为空")] + [MaxLength(50, ErrorMessage = "姓名不能超过50个字符")] + public string Name { get; set; } = string.Empty; + + /// + /// 身份证号 + /// + [Required(ErrorMessage = "身份证号不能为空")] + [RegularExpression(@"^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$", ErrorMessage = "请输入正确的身份证号格式")] + public string IdCardNumber { get; set; } = string.Empty; + + /// + /// 性别 + /// + [Required(ErrorMessage = "性别不能为空")] + public Gender Gender { get; set; } + + /// + /// 电话 + /// + [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号格式")] + public string? Mobile { get; set; } + + /// + /// 出生日期 + /// + [Required(ErrorMessage = "出生日期不能为空")] + public DateTime BirthDate { get; set; } +} + +/// +/// 统一报名结果输出DTO +/// +public class UnifiedRegistrationOutput +{ + /// + /// 报名ID + /// + public long RegistrationId { get; set; } + + /// + /// 订单号 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 总金额(优惠前) + /// + public decimal OriginalAmount { get; set; } + + /// + /// 优惠金额 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 实际支付金额 + /// + public decimal ActualAmount { get; set; } + + /// + /// 支付状态 + /// + public int PaymentStatus { get; set; } + + /// + /// 是否免费 + /// + public bool IsFree => ActualAmount <= 0; +} + +/// +/// 统一支付处理输入DTO +/// +public class UnifiedPaymentInput +{ + /// + /// 报名ID + /// + [Required(ErrorMessage = "报名ID不能为空")] + public long RegistrationId { get; set; } + + /// + /// 支付方式 (1-线下 2-线上) + /// + [Required(ErrorMessage = "支付方式不能为空")] + [Range(1, 2, ErrorMessage = "支付方式只能是1(线下)或2(线上)")] + public int PaymentMethod { get; set; } + + /// + /// 优惠券ID (可选) + /// + public long? CouponId { get; set; } + + /// + /// 微信OpenId (微信支付时必填) + /// + public string? OpenId { get; set; } + + /// + /// 支付凭证URL (线下支付时必填) + /// + public string? PaymentProofUrl { get; set; } + + /// + /// 操作用户ID + /// + public long OperatorUserId { get; set; } +} + +/// +/// 统一支付处理输出DTO +/// +public class UnifiedPaymentOutput +{ + /// + /// 报名ID + /// + public long RegistrationId { get; set; } + + /// + /// 订单号 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 支付状态 + /// + public int PaymentStatus { get; set; } + + /// + /// 是否免费 + /// + public bool IsFree { get; set; } + + /// + /// 优惠前金额 + /// + public decimal OriginalAmount { get; set; } + + /// + /// 优惠金额 + /// + public decimal DiscountAmount { get; set; } + + /// + /// 实际支付金额 + /// + public decimal ActualAmount { get; set; } + + /// + /// 优惠券使用规则说明 + /// + public string? CouponRuleNote { get; set; } + + /// + /// 微信支付参数 (微信支付时返回) + /// + public object? WechatPayParams { get; set; } + + /// + /// 支付二维码URL (微信Native支付时返回) + /// + public string? QrcodeUrl { get; set; } + + /// + /// 交易类型 + /// + public string? TradeType { get; set; } +} + +/// +/// 报名验证结果DTO +/// +public class RegistrationValidationResult +{ + /// + /// 是否通过验证 + /// + public bool IsValid { get; set; } + + /// + /// 错误信息 + /// + public string? ErrorMessage { get; set; } + + /// + /// 验证详情 + /// + public List? Details { get; set; } +} + +/// +/// 验证详情DTO +/// +public class ValidationDetail +{ + /// + /// 验证项名称 + /// + public string ItemName { get; set; } = string.Empty; + + /// + /// 是否通过 + /// + public bool IsPassed { get; set; } + + /// + /// 消息 + /// + public string Message { get; set; } = string.Empty; +} diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs new file mode 100644 index 0000000..92a60e4 --- /dev/null +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -0,0 +1,1283 @@ +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; +using YD_Event.Core.Infrastructure.WeChat; +using YD_Event.Core.Infrastructure.WeChat.Dto; +using YD_Event.Core.Options; + +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 _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; + + 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 paymentRepository, + ISqlSugarRepository merchantCertificationRepository, + ISqlSugarClient db, + IIdGenerator idGenerator, + WeChatPayService weChatPayService, + IOptionsMonitor wechatTenpayClientOptionsMonitor, + ILogger 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; + } + + /// + /// 统一报名方法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 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 + }; + } + + /// + /// 统一支付处理方法 - 处理支付相关逻辑 + /// + [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 (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; + } + + /// + /// 验证报名 + /// + 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(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; + } + + /// + /// 验证项目选择(一阶段:单选机制) + /// + private async Task ValidateProjectSelectionAsync(UnifiedRegistrationInput input) + { + var result = new RegistrationValidationResult + { + IsValid = true, + Details = new List() + }; + + // 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; + } + + /// + /// 验证队员分配(二阶段) + /// + private 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 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; + } + + // 2.1 验证队员资质(年龄和性别) + var qualificationResult = ValidatePersonQualifications(item.Persons, eventProjectGroup); + if (!qualificationResult.IsValid) + { + result.IsValid = false; + result.ErrorMessage = qualificationResult.ErrorMessage; + result.Details.AddRange(qualificationResult.Details); + return result; + } + result.Details.AddRange(qualificationResult.Details); + + // 2.2 验证年龄跨组 + var ageGroupResult = ValidateAgeCrossGroup(item.Persons, eventProjectGroup); + if (!ageGroupResult.IsValid) + { + result.IsValid = false; + result.ErrorMessage = ageGroupResult.ErrorMessage; + result.Details.AddRange(ageGroupResult.Details); + return result; + } + result.Details.AddRange(ageGroupResult.Details); + } + + return result; + } + + /// + /// 验证队员资质(年龄和性别) + /// + private RegistrationValidationResult ValidatePersonQualifications(List persons, EventProjectGroup group) + { + var result = new RegistrationValidationResult + { + IsValid = true, + Details = new List() + }; + + foreach (var person in persons) + { + // 验证性别 + if (person.Gender != group.Gender) + { + 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; + } + + // 验证年龄 + var age = CalculateAge(person.BirthDate); + if (person.BirthDate < group.BirthDateStart || person.BirthDate > group.BirthDateEnd) + { + result.IsValid = false; + result.ErrorMessage = $"队员{person.Name}的年龄不符合组别要求。要求出生日期范围:{group.BirthDateStart:yyyy-MM-dd} 至 {group.BirthDateEnd:yyyy-MM-dd},实际:{person.BirthDate:yyyy-MM-dd}"; + result.Details.Add(new ValidationDetail + { + ItemName = "年龄验证", + IsPassed = false, + Message = result.ErrorMessage + }); + return result; + } + } + + result.Details.Add(new ValidationDetail + { + ItemName = "队员资质", + IsPassed = true, + Message = "所有队员的年龄和性别均符合组别要求" + }); + + return result; + } + + /// + /// 验证年龄跨组 + /// 支持小年龄段向大年龄段跨组报名,需满足两个条件: + /// ①团队中至少有一名成员属于目标年龄段; + /// ②仅允许跨一个年龄段 + /// + private RegistrationValidationResult ValidateAgeCrossGroup(List persons, EventProjectGroup targetGroup) + { + var result = new RegistrationValidationResult + { + IsValid = true, + Details = new List() + }; + + bool hasAtLeastOneInTargetAge = false; + bool hasAnyBelowTargetAge = false; + + foreach (var person in persons) + { + if (person.BirthDate >= targetGroup.BirthDateStart && person.BirthDate <= targetGroup.BirthDateEnd) + { + hasAtLeastOneInTargetAge = true; + } + else if (person.BirthDate < targetGroup.BirthDateStart) + { + hasAnyBelowTargetAge = true; + } + } + + // 如果有队员年龄低于目标年龄段(小年龄跨大年龄) + 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 + { + ItemName = "年龄跨组", + IsPassed = true, + Message = "无需跨组,所有队员均在目标年龄段内" + }); + } + + return result; + } + + /// + /// 创建报名明细和参赛人员 + /// + 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(); + var persons = new List(); + int totalPersonCount = 0; + + foreach (var item in input.Items) + { + var eventProject = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId); + if (eventProject == null) continue; + + var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId); + var groupName = GetGroupName(item, eventProjectGroups); + + // 计算费用 + var (unitPrice, totalPrice) = CalculateItemPrice(feeConfig, eventProject, item.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 = item.Persons.Count, + TotalPrice = totalPrice, + CreatedUserId = input.CreatedUserId, + CreatedTime = DateTime.Now + }; + regItems.Add(regItem); + + totalPersonCount += item.Persons.Count; + + // 添加参赛人员 + foreach (var pInput in item.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, + CreatedUserId = input.CreatedUserId, + CreatedTime = DateTime.Now + }); + } + } + + await _registrationItemRepository.InsertRangeAsync(regItems); + await _registrationPersonRepository.InsertRangeAsync(persons); + + // 计算总费用 + var totalAmount = CalculateTotalAmount(feeConfig, eventProjects, input.Items, totalPersonCount); + + return (totalAmount, totalPersonCount); + } + + /// + /// 计算最终金额(含优惠券) + /// + 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); + } + + /// + /// 处理免费支付 + /// + 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); + } + + /// + /// 创建订单记录 + /// + 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) + { + 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; + } + + /// + /// 处理线下支付 + /// + private async Task 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 + }; + } + + /// + /// 检查报名限制 + /// + private async Task CheckRegistrationLimitAsync(UnifiedRegistrationInput input) + { + var result = new RegistrationLimitCheckOutput + { + HasLimit = false, + LimitDetails = new List() + }; + + 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); + + // 1. 检查项目组别容量控制(团队报名数量限制) + var groupIds = input.Items.Where(i => i.GroupId.HasValue).Select(i => i.GroupId.Value).ToList(); + if (groupIds.Any()) + { + foreach (var groupId in groupIds) + { + 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; + + var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject.ProjectId); + if (projectLimit == null) continue; + + // 统计当前组别已报名的团队数量(排除草稿和已取消的) + var registrationIds = await _registrationRepository.AsQueryable() + .Where(r => r.EventId == input.EventId && !r.DeleteMark && r.PaymentStatus != 4) + .Select(r => r.Id) + .ToListAsync(); + + var currentTeams = await _registrationItemRepository.AsQueryable() + .Where(ri => ri.EventProjectGroupId == eventProjectGroup.Id + && !ri.DeleteMark + && registrationIds.Contains(ri.RegistrationId)) + .Select(ri => ri.RegistrationId) + .Distinct() + .CountAsync(); + + if (currentTeams >= projectLimit.LimitCount) + { + 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", + LimitMessage = $"项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"已报名{currentTeams}队,达到限报{projectLimit.LimitCount}队" + }); + } + } + } + + // 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); + 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); + + 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; + + // 根据项目类型判断是个人项目还是团队项目 + // 这里假设ProjectType中有个人和团队的区分,您可以根据实际情况调整 + // 暂时根据报名模式统计 + var registration = eventRegistrations.FirstOrDefault(r => r.Id == item.RegistrationId); + if (registration != null) + { + if (registration.RegistrationMode == 1) // 个人报名 + { + individualProjectCount++; + } + else // 团队报名 + { + teamProjectCount++; + } + } + } + + // 检查个人项目限报 + 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}个" + }); + } + } + } + + return result; + } + + /// + /// 获取组别名称 + /// + private 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) + { + 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); + } + + /// + /// 计算总费用 + /// + 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 + }; + + 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; + } + + /// + /// 根据出生日期计算年龄 + /// + 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; + } + + _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} 状态已更新为已支付"); + } + } + + /// + /// 取消报名 + /// + 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); + + 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; + } +} diff --git a/YD_Event.Application/Event/UserEventRecordService.cs b/YD_Event.Application/Event/UserEventRecordService.cs index 092558d..047af56 100644 --- a/YD_Event.Application/Event/UserEventRecordService.cs +++ b/YD_Event.Application/Event/UserEventRecordService.cs @@ -368,7 +368,7 @@ public class UserEventRecordService : IDynamicApiController, ITransient Code = 200, Success = true, Data = new CertificateDTO { - CertUrl = cert.CertificateUrl + "?sas_token=mock_10min_token", // 模拟带签名 URL + CertUrl = cert.CertificateUrl, CertNumber = cert.CertificateNo, IssueTime = cert.IssuedTime ?? DateTime.MinValue, ExpireTime = null diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index af7fdbd..a05a9bb 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -65,6 +65,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient private readonly ISqlSugarRepository _paymentRepository; private readonly ILogger _logger; private readonly OrderNoGeneratorService _orderNoGeneratorService; + private readonly UnifiedRegistrationService _unifiedRegistrationService; public UserEventRegistrationService( ISqlSugarRepository eventRepository, @@ -97,7 +98,8 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient ISqlSugarRepository redemptionCodeRepository, ISqlSugarRepository paymentRepository, ILogger logger, - OrderNoGeneratorService orderNoGeneratorService) + OrderNoGeneratorService orderNoGeneratorService, + UnifiedRegistrationService unifiedRegistrationService) { _eventRepository = eventRepository; _unitRepository = unitRepository; @@ -130,6 +132,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient _paymentRepository = paymentRepository; _logger = logger; _orderNoGeneratorService = orderNoGeneratorService; + _unifiedRegistrationService = unifiedRegistrationService; } /// @@ -405,11 +408,14 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient var eventInfo = await _eventRepository.GetByIdAsync(input.EventId); if (eventInfo == null) throw Oops.Bah("赛事不存在"); - // 验证2:一次报名只能选择单个团队内的运动员 + // 获取团队信息 + var team = await _db.Queryable().FirstAsync(t => t.Id == input.TeamId && !t.DeleteMark); + if (team == null) throw Oops.Bah("团队不存在"); + + // 验证:一次报名只能选择单个团队内的运动员 if (input.MemberIds != null && input.MemberIds.Count > 0) { - // 获取所有队员的团队关系 - var teamMembers = await _db.Queryable() + var teamMembers = await _db.Queryable() .Where(tm => input.MemberIds.Contains(tm.UserId)) .Where(tm => !tm.DeleteMark) .ToListAsync(); @@ -419,19 +425,16 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient throw Oops.Bah("未找到队员信息"); } - // 按用户ID分组,获取每个用户所属的所有团队 var userTeamMap = teamMembers .GroupBy(tm => tm.UserId) .ToDictionary(g => g.Key, g => g.Select(tm => tm.TeamId).ToList()); - // 检查是否所有用户都有团队信息 var missingUsers = input.MemberIds.Except(userTeamMap.Keys).ToList(); if (missingUsers.Any()) { throw Oops.Bah("部分队员未找到团队信息"); } - // 验证所有队员是否都属于指定的 TeamId var allBelongToTeam = userTeamMap.All(ut => ut.Value.Contains(input.TeamId)); if (!allBelongToTeam) { @@ -439,73 +442,27 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient } } - // 验证3:调用检查报名限制API,确保所有限制都满足 - var checkInput = new RegistrationLimitCheckInput - { - EventId = input.EventId, - MemberIds = input.MemberIds ?? new List(), - GroupIds = input.GroupMembers?.Select(g => g.GroupId).Distinct().ToList() ?? new List() - }; - var checkResult = await CheckRegistrationLimit(checkInput); - if (checkResult.HasLimit) - { - var firstLimit = checkResult.LimitDetails.FirstOrDefault(); - throw Oops.Bah(firstLimit?.LimitMessage ?? "报名限制检查失败"); - } - - var registration = await _registrationRepository.GetFirstAsync(x => x.EventId == input.EventId && x.CreatedUserId == userId && !x.DeleteMark); - - if (registration == null) - { - registration = new EventRegistration - { - Id = _idGenerator.NewLong(), - EventId = input.EventId, - CreatedUserId = userId, - CreatedTime = DateTime.Now, - Status = AvailabilityStatus.Enable, - PaymentStatus = input.IsDraft ? 0 : 0, // 0-未支付/待审核 - ContactName = userName, - ContactPhone = userMobile, - OrderNo = "", - RegistrationMode = 2 // PC端只做团队报名 - }; - await _registrationRepository.InsertAsync(registration); - } - else - { - registration.PaymentStatus = input.IsDraft ? 0 : 0; - registration.ContactName = userName; - registration.ContactPhone = userMobile; - registration.UpdatedTime = DateTime.Now; - registration.UpdatedUserId = userId; - await _registrationRepository.UpdateAsync(registration); - } - - await _registrationItemRepository.DeleteAsync(x => x.RegistrationId == registration.Id); - await _registrationPersonRepository.DeleteAsync(x => x.RegistrationId == registration.Id); - + // 获取成员信息 var members = await _userRepository.GetListAsync(x => input.MemberIds.Contains(x.Id)); + // 获取所有组别信息 var groupIds = input.GroupMembers?.Select(g => g.GroupId).Distinct().ToList() ?? new List(); var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => groupIds.Contains(x.Id)); - // 通过EventProjectGroup获取对应的EventProject - var eventProjectIds = eventProjectGroups.Select(x => x.EventProjectId).Distinct().ToList(); - var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id)); - - var baseProjectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); - var baseProjects = await _projectRepository.GetListAsync(x => baseProjectIds.Contains(x.Id)); - - var baseProjectGroupIds = eventProjectGroups.Select(x => x.ProjectGroupId).Distinct().ToList(); - var baseProjectGroups = await _projectGroupRepository.GetListAsync(x => baseProjectGroupIds.Contains(x.Id)); - - // 获取赛事费用配置 - var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark); - - int totalPersonCount = 0; - FeeCalculationResult? feeCalculationResult = null; + // 构建统一报名输入 + var unifiedInput = new UnifiedRegistrationInput + { + EventId = input.EventId, + RegistrationMode = 2, // PC端只做团队报名 + TeamName = team.Name, + ContactName = userName, + ContactPhone = userMobile, + Items = new List(), + CreatedUserId = userId, + IsDraft = input.IsDraft + }; + // 填充项目和人员信息 if (input.GroupMembers != null && input.GroupMembers.Any()) { foreach (var groupMember in input.GroupMembers) @@ -513,186 +470,30 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient var eventProjectGroup = eventProjectGroups.FirstOrDefault(g => g.Id == groupMember.GroupId); if (eventProjectGroup == null) continue; - var eventProject = eventProjects.FirstOrDefault(p => p.Id == eventProjectGroup.EventProjectId); - if (eventProject == null) continue; + var groupMembersList = members.Where(m => groupMember.MemberIds.Contains(m.Id)).ToList(); + if (!groupMembersList.Any()) continue; - var baseProject = baseProjects.FirstOrDefault(x => x.Id == eventProject.ProjectId); - var baseProjectGroup = baseProjectGroups.FirstOrDefault(bg => bg.Id == eventProjectGroup.ProjectGroupId); - - var groupMembers = members.Where(m => groupMember.MemberIds.Contains(m.Id)).ToList(); - if (!groupMembers.Any()) continue; - - // 根据收费模式设置单价和总价 - decimal unitPrice = 0; - decimal totalPrice = 0; - - switch (feeConfig.ChargeMode) + unifiedInput.Items.Add(new UnifiedRegistrationItemInput { - case ChargeMode.Free: - unitPrice = 0; - totalPrice = 0; - break; - - case ChargeMode.FixedPrice: - // 定价(赛事)模式:使用赛事费用配置的单价 - unitPrice = feeConfig.Price; - totalPrice = unitPrice * groupMembers.Count(); - break; - - case ChargeMode.UnitPriceEvent: - // 单价(赛事)模式:使用赛事单价 - unitPrice = feeConfig.Price; - totalPrice = unitPrice * groupMembers.Count(); - break; - - case ChargeMode.UnitPriceProject: - // 单价(项目)模式:使用项目单价 - unitPrice = eventProject.Price; - totalPrice = unitPrice * groupMembers.Count(); - break; - } - - var item = new EventRegistrationItem - { - Id = _idGenerator.NewLong(), - RegistrationId = registration.Id, - ProjectId = eventProject.Id, - EventProjectGroupId = eventProjectGroup.Id, - ProjectName = baseProject?.Name ?? "", - GroupName = baseProjectGroup?.GroupName ?? "", - ProjectTypeName = baseProject?.ProjectType.Description() ?? "", - UnitPrice = unitPrice, - PersonCount = groupMembers.Count(), - TotalPrice = totalPrice, - CreatedUserId = userId, - CreatedTime = DateTime.Now - }; - await _registrationItemRepository.InsertAsync(item); - - totalPersonCount += item.PersonCount; - - foreach (var member in groupMembers) - { - var person = new EventRegistrationPerson + ProjectId = eventProjectGroup.EventProjectId, + GroupId = groupMember.GroupId, + GroupName = eventProjectGroup.GroupName, + Persons = groupMembersList.Select(m => new UnifiedPersonInput { - Id = _idGenerator.NewLong(), - RegistrationId = registration.Id, - RegistrationItemId = item.Id, - ProjectId = eventProject.Id, - EventProjectGroupId = eventProjectGroup.Id, - GroupName = baseProjectGroup?.GroupName ?? "", - ProjectName = baseProject?.Name ?? "", - Name = member.Name, - Gender = member.Gender, - IdCard = member.IdCardNumber, - Phone = member.Mobile, - Age = CalculateAge(member.BirthDate), - BirthDate = member.BirthDate ?? DateTime.MinValue, - PersonType = PersonType.Athlete, - CreatedUserId = userId, - CreatedTime = DateTime.Now - }; - await _registrationPersonRepository.InsertAsync(person); - } - } - } - else - { - foreach (var eventProject in eventProjects) - { - var baseProject = baseProjects.FirstOrDefault(x => x.Id == eventProject.ProjectId); - - // 根据收费模式设置单价和总价 - decimal unitPrice = 0; - decimal totalPrice = 0; - - if (feeConfig != null) - { - switch (feeConfig.ChargeMode) - { - case ChargeMode.Free: - unitPrice = 0; - totalPrice = 0; - break; - - case ChargeMode.FixedPrice: - // 定价(赛事)模式:使用赛事费用配置的单价 - unitPrice = feeConfig.Price; - totalPrice = unitPrice * members.Count(); - break; - - case ChargeMode.UnitPriceEvent: - // 单价(赛事)模式:使用赛事单价 - unitPrice = feeConfig.Price; - totalPrice = unitPrice * members.Count(); - break; - - case ChargeMode.UnitPriceProject: - // 单价(项目)模式:使用项目单价 - unitPrice = eventProject.Price; - totalPrice = unitPrice * members.Count(); - break; - } - } - - var item = new EventRegistrationItem - { - Id = _idGenerator.NewLong(), - RegistrationId = registration.Id, - ProjectId = eventProject.Id, - ProjectName = baseProject?.Name ?? "", - ProjectTypeName = baseProject?.ProjectType.Description() ?? "", - UnitPrice = unitPrice, - PersonCount = members.Count(), - TotalPrice = totalPrice, - CreatedUserId = userId, - CreatedTime = DateTime.Now - }; - await _registrationItemRepository.InsertAsync(item); - - totalPersonCount += item.PersonCount; - - foreach (var member in members) - { - var person = new EventRegistrationPerson - { - Id = _idGenerator.NewLong(), - RegistrationId = registration.Id, - RegistrationItemId = item.Id, - ProjectId = eventProject.Id, - GroupName = baseProject?.Name ?? "", - ProjectName = baseProject?.Name ?? "", - Name = member.Name, - Gender = member.Gender, - IdCard = member.IdCardNumber, - Phone = member.Mobile, - Age = CalculateAge(member.BirthDate), - BirthDate = member.BirthDate ?? DateTime.MinValue, - PersonType = PersonType.Athlete, - CreatedUserId = userId, - CreatedTime = DateTime.Now - }; - await _registrationPersonRepository.InsertAsync(person); - } + UserId = m.Id, + Name = m.Name ?? "", + IdCardNumber = m.IdCardNumber ?? "", + Gender = m.Gender, + Mobile = m.Mobile, + BirthDate = m.BirthDate ?? DateTime.MinValue + }).ToList() + }); } } - if (totalPersonCount > 0) - { - var feeInput = new FeeCalculationInput - { - EventId = input.EventId, - EventProjectIds = eventProjectIds, - ParticipantCount = totalPersonCount - }; - feeCalculationResult = await CalculateFeeAsync(feeInput); - registration.TotalAmount = feeCalculationResult.TotalAmount; - } - - registration.TotalPersonCount = totalPersonCount; - await _registrationRepository.UpdateAsync(registration); - - return registration.Id; + // 调用统一报名服务 + var result = await _unifiedRegistrationService.ProcessRegistrationAsync(unifiedInput); + return result.RegistrationId; } /// @@ -769,155 +570,30 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient [UnitOfWork] public async Task CreateOrder([FromBody] UserCreateOrderInput input) { - var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId); - if (registration == null) throw Oops.Bah("报名信息不存在"); + var userId = _authManager.UserId; - var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId); - if (eventInfo == null) throw Oops.Bah("赛事信息不存在"); - - var result = new UserPaymentResultOutput(); - - // 计算最终支付金额 - decimal originalAmount = registration.TotalAmount; - decimal finalAmount = originalAmount; - decimal discountAmount = 0; - YD_Event.Core.Entities.Coupon? coupon = null; - YD_Event.Core.Entities.UserCoupon? userCoupon = null; - - // 处理优惠券 - if (input.CouponId.HasValue) + // 构建统一支付输入 + var unifiedInput = new UnifiedPaymentInput { - var userId = _authManager.UserId; - coupon = await _couponRepository.GetFirstAsync(c => - c.Id == input.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 && // 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); - } - } - } - - // 如果最终金额为0,直接完成报名,无需支付 - if (finalAmount <= 0) - { - // 如果使用了优惠券,标记为已使用 - if (userCoupon != null) - { - userCoupon.Status = 1; // 1-已使用 - userCoupon.UseTime = DateTime.Now; - await _userCouponRepository.UpdateAsync(userCoupon); - } - - registration.PaymentStatus = 1; // 已支付 - registration.PaymentTime = DateTime.Now; - await _registrationRepository.UpdateAsync(registration); - - result.Status = 1; - result.IsFree = true; - return result; - } - - // 获取当前用户的单位认证ID - var certification = await _db.Queryable() - .FirstAsync(c => c.UserId == registration.CreatedUserId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark); - - var orderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); - registration.OrderNo = orderNo; - registration.PaymentMethod = input.PaymentMethod; - registration.PaymentStatus = 0; // 未支付 - await _registrationRepository.UpdateAsync(registration); - - // 创建内部订单记录 - var order = new YD_Event.Core.Entities.Order - { - Id = _idGenerator.NewLong(), - OrderNo = orderNo, - OrderType = 1, // Income - MerchantId = eventInfo.MerchantId, - MerchantCertificationId = certification?.Id, // 设置单位认证ID - EventId = registration.EventId, - RegistrationId = registration.Id, - OriginalAmount = originalAmount, - ActualAmount = finalAmount, - DiscountAmount = discountAmount, + RegistrationId = input.RegistrationId, + PaymentMethod = input.PaymentMethod, CouponId = input.CouponId, - OrderStatus = OrderStatus.Pending, - CreatedTime = DateTime.Now, - CreatedUserId = registration.CreatedUserId + OperatorUserId = userId }; - await _orderRepository.InsertAsync(order); - result.OrderNo = orderNo; - result.Status = 0; - result.IsFree = false; + // 调用统一支付处理服务 + var unifiedResult = await _unifiedRegistrationService.ProcessPaymentAsync(unifiedInput); - // 如果是微信支付 (假设 1 是微信支付) - if (input.PaymentMethod == 1) + // 转换结果 + var result = new UserPaymentResultOutput { - 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 nativeRequest = new CreateNativePayRequest - { - OutTradeNo = 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) - { - result.PayUrl = payResult.QrcodeUrl ?? string.Empty; - result.QrcodeUrl = payResult.QrcodeUrl; - result.TradeType = payResult.TradeType; - } - else - { - throw Oops.Bah($"微信统一下单失败:{payResult.Message}"); - } - } - else - { - result.PayUrl = "http://mock-pay-url.com/" + orderNo; - result.TradeType = "MOCK"; - } + OrderNo = unifiedResult.OrderNo, + Status = unifiedResult.PaymentStatus, + IsFree = unifiedResult.IsFree, + PayUrl = unifiedResult.QrcodeUrl ?? string.Empty, + QrcodeUrl = unifiedResult.QrcodeUrl, + TradeType = unifiedResult.TradeType + }; return result; } @@ -950,8 +626,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient return new JsonResult(new { code = "FAIL", message = "参数不完整" }); } - var platformPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x => - x.Status == AvailabilityStatus.Enable && !x.DeleteMark); + var platformPayConfig = await _unifiedRegistrationService.GetPlatformWeChatPayConfigAsync(); if (platformPayConfig == null) { _logger.LogError("未找到平台级别的微信支付配置"); @@ -981,63 +656,12 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient return new JsonResult(new { code = "SUCCESS", message = "成功" }); } - var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == notifyData.OutTradeNo); - if (reg == null) - { - _logger.LogWarning($"未找到订单:{notifyData.OutTradeNo}"); - return new JsonResult(new { code = "SUCCESS", message = "成功" }); - } - - if (reg.PaymentStatus == 1) - { - _logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付回调"); - return new JsonResult(new { code = "SUCCESS", message = "成功" }); - } - - var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId); - if (evt == null) - { - _logger.LogError($"未找到赛事信息,订单号:{reg.OrderNo},赛事ID:{reg.EventId}"); - return new JsonResult(new { code = "FAIL", message = "赛事信息不存在" }); - } - - _logger.LogInformation("订单支付成功,订单号={OrderNo},赛事ID={EventId},商户ID={MerchantId}", - reg.OrderNo, evt.Id, evt.MerchantId); - - reg.PaymentStatus = 1; - reg.PaymentTime = notifyData.SuccessTime?.DateTime; - reg.RegistrationRemark = notifyData.TransactionId; - reg.UpdatedTime = DateTime.Now; - - await _registrationRepository.UpdateAsync(reg); - _logger.LogInformation($"订单 {reg.OrderNo} 支付成功,微信交易号:{notifyData.TransactionId}"); - - var payment = new EventRegistrationPayment - { - Id = _idGenerator.NewLong(), - RegistrationId = reg.Id, - TransactionNo = notifyData.TransactionId ?? "", - PaymentChannel = 1, - PaymentAmount = reg.TotalAmount, - PaymentTime = notifyData.SuccessTime?.DateTime ?? DateTime.Now, - PaymentStatus = 1, - RefundStatus = 0, - PayeeAccount = "", - CreatedUserId = reg.CreatedUserId, - CreatedTime = DateTime.Now - }; - await _paymentRepository.InsertAsync(payment); - _logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={notifyData.TransactionId}"); - - var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo); - if (order != null) - { - order.OrderStatus = OrderStatus.Paid; - order.PaymentTime = notifyData.SuccessTime?.DateTime; - order.UpdatedTime = DateTime.Now; - await _orderRepository.UpdateAsync(order); - _logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付"); - } + // 调用统一支付成功处理方法 + await _unifiedRegistrationService.HandlePaymentSuccessAsync( + notifyData.OutTradeNo, + notifyData.TransactionId, + notifyData.SuccessTime?.DateTime, + 0); return new JsonResult(new { code = "SUCCESS", message = "成功" }); } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index bc63740..552d682 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -738,7 +738,7 @@ - 模式(0-手动配置 1-自动配置) + 模式(0-自动配置 1-手动配置) @@ -748,7 +748,7 @@ - 模式(0-手动配置 1-自动配置) + 模式(0-自动配置 1-手动配置) @@ -5554,6 +5554,286 @@ 发放时间 + + + 统一报名输入DTO + + + + + 赛事ID + + + + + 报名模式 (1-个人 2-团队) + + + + + 团队名称 (团队报名必填) + + + + + 联系人姓名 + + + + + 联系人电话 + + + + + 报名项目列表 + + + + + 创建用户ID + + + + + 是否为草稿 + + + + + 统一报名项目输入DTO + + + + + 项目ID + + + + + 组别ID + + + + + 组别名称 + + + + + 参赛人员列表 + + + + + 统一参赛人员输入DTO + + + + + 用户ID (可选,已有用户时使用) + + + + + 姓名 + + + + + 身份证号 + + + + + 性别 + + + + + 电话 + + + + + 出生日期 + + + + + 统一报名结果输出DTO + + + + + 报名ID + + + + + 订单号 + + + + + 总金额(优惠前) + + + + + 优惠金额 + + + + + 实际支付金额 + + + + + 支付状态 + + + + + 是否免费 + + + + + 统一支付处理输入DTO + + + + + 报名ID + + + + + 支付方式 (1-线下 2-线上) + + + + + 优惠券ID (可选) + + + + + 微信OpenId (微信支付时必填) + + + + + 支付凭证URL (线下支付时必填) + + + + + 操作用户ID + + + + + 统一支付处理输出DTO + + + + + 报名ID + + + + + 订单号 + + + + + 支付状态 + + + + + 是否免费 + + + + + 优惠前金额 + + + + + 优惠金额 + + + + + 实际支付金额 + + + + + 优惠券使用规则说明 + + + + + 微信支付参数 (微信支付时返回) + + + + + 支付二维码URL (微信Native支付时返回) + + + + + 交易类型 + + + + + 报名验证结果DTO + + + + + 是否通过验证 + + + + + 错误信息 + + + + + 验证详情 + + + + + 验证详情DTO + + + + + 验证项名称 + + + + + 是否通过 + + + + + 消息 + + 用户端赛事查询输入 @@ -6956,6 +7236,129 @@ 当前时间 计算后的赛事状态 + + + 统一报名处理服务 + + + + + 统一报名方法A - 处理核心报名业务逻辑 + + + + + 统一支付处理方法 - 处理支付相关逻辑 + + + + + 验证报名 + + + + + 验证项目选择(一阶段:单选机制) + + + + + 验证队员分配(二阶段) + + + + + 验证队员资质(年龄和性别) + + + + + 验证年龄跨组 + 支持小年龄段向大年龄段跨组报名,需满足两个条件: + ①团队中至少有一名成员属于目标年龄段; + ②仅允许跨一个年龄段 + + + + + 创建报名明细和参赛人员 + + + + + 计算最终金额(含优惠券) + + + + + 处理免费支付 + + + + + 创建订单记录 + + + + + 处理线上支付 + + + + + 处理线下支付 + + + + + 检查报名限制 + + + + + 获取组别名称 + + + + + 计算单个项目费用 + + + + + 计算总费用 + + + + + 根据出生日期计算年龄 + + + + + 处理支付成功 + + + + + 取消报名 + + + + + 查询并同步微信支付状态 + + + + + 获取平台级别的微信支付配置 + + + + + 根据赛事获取对应的微信支付配置 + + 用户端参赛记录管理 diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index e5042f0..370aad9 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -726,7 +726,7 @@ - 模式(0-手动配置 1-自动配置) + 模式(0-自动配置 1-手动配置)