diff --git a/YD_Event.Application/Client/Dtos/EventClientDtos.cs b/YD_Event.Application/Client/Dtos/EventClientDtos.cs index 0c886f8..c0709fa 100644 --- a/YD_Event.Application/Client/Dtos/EventClientDtos.cs +++ b/YD_Event.Application/Client/Dtos/EventClientDtos.cs @@ -392,18 +392,16 @@ public class SubmitRegistrationInput public string? TeamName { get; set; } /// - /// 联系人姓名 + /// 联系人姓名 (个人报名时可自动从用户信息获取) /// - [Required(ErrorMessage = "联系人姓名不能为空")] [MaxLength(50, ErrorMessage = "联系人姓名不能超过50个字符")] - public string ContactName { get; set; } + public string? ContactName { get; set; } /// - /// 联系人电话 + /// 联系人电话 (个人报名时可自动从用户信息获取) /// - [Required(ErrorMessage = "联系人电话不能为空")] [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号格式")] - public string ContactPhone { get; set; } + public string? ContactPhone { get; set; } /// /// 报名项目列表 @@ -436,11 +434,9 @@ public class SubmitRegistrationItemInput public long? GroupId { get; set; } /// - /// 参赛人员列表 + /// 参赛人员列表 (个人报名时可自动从用户信息获取) /// - [Required(ErrorMessage = "参赛人员列表不能为空")] - [MinLength(1, ErrorMessage = "请至少添加一名参赛人员")] - public List Persons { get; set; } + public List? Persons { get; set; } } /// diff --git a/YD_Event.Application/Client/Dtos/WeChatDtos.cs b/YD_Event.Application/Client/Dtos/WeChatDtos.cs new file mode 100644 index 0000000..f54628e --- /dev/null +++ b/YD_Event.Application/Client/Dtos/WeChatDtos.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using YD_Event.Core.Infrastructure.WeChat; + +namespace YD_Event.Application.Client.Dtos; + +/// +/// 微信小程序 code2session 输入参数 +/// +public class WeChatCode2SessionInput +{ + /// + /// 登录时获取的 code + /// + [Required(ErrorMessage = "code 不能为空")] + public string Code { get; set; } +} + +/// +/// 微信小程序 code2session 输出参数 +/// +public class WeChatCode2SessionOutput +{ + /// + /// 用户唯一标识 + /// + public string OpenId { get; set; } + + /// + /// 会话密钥 + /// + public string SessionKey { get; set; } + + /// + /// 用户在开放平台的唯一标识符(在满足 UnionID 下发条件的情况下会返回) + /// + public string? UnionId { get; set; } +} \ No newline at end of file diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index db23657..686af44 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization; using Furion.FriendlyException; using System.Security.Claims; using System.Text.Json; +using System.IO; namespace YD_Event.Application.Client; @@ -27,6 +28,7 @@ public class EventClientController : IDynamicApiController private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _eventProjectRepository; private readonly ISqlSugarRepository _groupRepository; + private readonly ISqlSugarRepository _eventProjectGroupRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _registrationItemRepository; private readonly ISqlSugarRepository _bannerRepository; @@ -51,6 +53,7 @@ public class EventClientController : IDynamicApiController ISqlSugarRepository projectRepository, ISqlSugarRepository eventProjectRepository, ISqlSugarRepository groupRepository, + ISqlSugarRepository eventProjectGroupRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository registrationItemRepository, ISqlSugarRepository bannerRepository, @@ -74,6 +77,7 @@ public class EventClientController : IDynamicApiController _projectRepository = projectRepository; _eventProjectRepository = eventProjectRepository; _groupRepository = groupRepository; + _eventProjectGroupRepository = eventProjectGroupRepository; _registrationRepository = registrationRepository; _registrationItemRepository = registrationItemRepository; _bannerRepository = bannerRepository; @@ -444,12 +448,39 @@ public class EventClientController : IDynamicApiController if (DateTime.Now < evt.RegistrationStartTime || DateTime.Now > evt.RegistrationEndTime) throw Oops.Bah("不在报名时间内"); - // 2. 校验重复报名 (同一赛事同一人只能报一次?还是可以多次?通常一次报名包含多个项目) - // 假设同一用户对同一赛事只能有一个“未取消”的报名记录 - var exist = await _registrationRepository.IsAnyAsync(r => r.EventId == input.EventId && r.CreatedUserId == userId && r.RegistrationStatus != 4); // 4=已取消 - if (exist) throw Oops.Bah("您已报名该赛事,请勿重复提交"); + // 2. 校验重复报名 - 如果已有未取消的报名记录,直接返回现有记录ID + var existingRegistration = await _registrationRepository.GetFirstAsync( + r => r.EventId == input.EventId && r.CreatedUserId == userId && r.RegistrationStatus != 4); // 4=已取消 + + if (existingRegistration != null) + { + // 已有未取消的报名记录,直接返回现有记录ID,让前端跳转到支付页面 + return existingRegistration.Id; + } - // 3. 校验项目有效性 & 限制 + // 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)) + input.ContactName = appUser?.Name; + + if (string.IsNullOrWhiteSpace(input.ContactPhone)) + input.ContactPhone = appUser?.Mobile; + + // 验证必填联系人信息 + if (string.IsNullOrWhiteSpace(input.ContactName)) + throw Oops.Bah("联系人姓名不能为空,请先完善身份认证信息"); + + if (string.IsNullOrWhiteSpace(input.ContactPhone)) + 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); @@ -480,9 +511,32 @@ public class EventClientController : IDynamicApiController throw Oops.Bah($"项目[{project?.Name}]已达到报名上限,无法继续报名"); } } + + // 个人报名模式下自动填充参赛人员信息 + 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($"项目未填写参赛人信息"); } - // 4. 创建报名主表 + // 5. 创建报名主表 var registrationId = _idGenerator.NewLong(); var registration = new EventRegistration { @@ -507,6 +561,11 @@ public class EventClientController : IDynamicApiController var regItems = new List(); var persons = new List(); + // 先获取所有需要的 eventProjectGroup 信息 + var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList(); + var allEventProjectGroups = await _eventProjectGroupRepository.GetListAsync( + eg => eventProjectIds.Contains(eg.EventProjectId) && !eg.DeleteMark); + foreach (var item in input.Items) { var eventProject = await _eventProjectRepository.GetFirstAsync(p => p.Id == item.ProjectId); @@ -515,11 +574,37 @@ public class EventClientController : IDynamicApiController var project = await _projectRepository.GetFirstAsync(p => p.Id == eventProject.ProjectId); if (project == null) throw Oops.Bah($"关联基础项目不存在: {item.ProjectId}"); - // 校验组别 - // 略,假设前端传参正确,或需根据 GroupId 查 + // 获取项目类型名称 + var projectType = await _projectTypeRepository.GetFirstAsync(t => t.Id == project.ProjectTypeId); + var projectTypeName = projectType?.Name ?? ""; - if (item.Persons == null || !item.Persons.Any()) - throw Oops.Bah($"项目[{project.Name}]未填写参赛人信息"); + // 获取组别名称 - 优先从前端传的获取,否则从数据库获取默认组别 + 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 = "默认组别"; + } + } + + // 参赛人员已经在前面确保了存在,这里不再重复验证 var regItemId = _idGenerator.NewLong(); var regItem = new EventRegistrationItem @@ -528,9 +613,10 @@ public class EventClientController : IDynamicApiController RegistrationId = registrationId, ProjectId = item.ProjectId, ProjectName = project.Name, - GroupName = item.GroupName, + ProjectTypeName = projectTypeName, + GroupName = groupName, UnitPrice = eventProject.Price / 100m, - PersonCount = item.Persons.Count, + PersonCount = item.Persons!.Count, TotalPrice = (eventProject.Price / 100m) * item.Persons.Count, CreatedUserId = userId, CreatedTime = DateTime.Now @@ -554,7 +640,7 @@ public class EventClientController : IDynamicApiController IdCard = pInput.IdCardNumber, BirthDate = pInput.BirthDate, Phone = pInput.Mobile, - GroupName = item.GroupName, + GroupName = groupName, ProjectName = project.Name, CreatedUserId = userId, CreatedTime = DateTime.Now @@ -607,66 +693,86 @@ public class EventClientController : IDynamicApiController if (reg.PaymentMethod == 2 && reg.TotalAmount > 0) { - WeChatPayConfig? payConfig = null; - if (evt.MerchantId.HasValue) - { - payConfig = await _weChatPayConfigRepository.GetFirstAsync(x => - x.MerchantId == evt.MerchantId.Value && - x.Status == AvailabilityStatus.Enable && - !x.DeleteMark); - } - + // 根据赛事信息获取对应的商户支付配置 + var payConfig = await GetWeChatPayConfigByEventAsync(evt); if (payConfig == null) { - payConfig = await _weChatPayConfigRepository.GetFirstAsync(x => - x.Status == AvailabilityStatus.Enable && !x.DeleteMark); + _logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId); + throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员"); } - if (payConfig != null) - { - _logger.LogInformation("开始创建微信 JSAPI 支付订单,订单号={OrderNo},OpenId={OpenId}", reg.OrderNo, input.OpenId); + _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 = (int)(reg.TotalAmount * 100), - ExpireTime = DateTimeOffset.Now.AddMinutes(30), - OpenId = input.OpenId - }; + var jsapiRequest = new CreateJsApiPayRequest + { + OutTradeNo = reg.OrderNo, + AppId = configOptions.AppId, + Description = $"{evt.EventName} - 报名费", + NotifyUrl = configOptions.NotifyUrl, + TotalAmount = (int)(reg.TotalAmount * 100), + ExpireTime = DateTimeOffset.Now.AddMinutes(30), + OpenId = input.OpenId + }; - var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest); - if (payResult.Success && payResult.JsApiParameters != null) + var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest); + if (payResult.Success && payResult.JsApiParameters != null) + { + _logger.LogInformation("微信 JSAPI 支付下单成功,订单号={OrderNo}", reg.OrderNo); + paymentInfo.WechatPayParams = new { - _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}"); - } + 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("未找到可用的微信支付配置,赛事ID={EventId}", evt.Id); - throw Oops.Bah("未找到可用的微信支付配置"); + _logger.LogError("微信 JSAPI 支付下单失败:订单号={OrderNo},错误信息={Message}", reg.OrderNo, payResult.Message); + throw Oops.Bah($"微信支付下单失败:{payResult.Message}"); } } return paymentInfo; } + /// + /// 根据赛事ID精确获取对应的微信支付配置 + /// + /// + /// 1. 通过赛事信息中的 MerchantId 查找对应的微信支付配置 + /// 2. 必须确保赛事已关联商户配置,不使用兜底策略 + /// 3. 只返回启用状态且未删除的配置 + /// + /// 赛事信息,必须包含有效的 MerchantId + /// 微信支付配置,如果未找到则返回 null + /// 当赛事没有关联商户配置时抛出 + private async Task GetWeChatPayConfigByEventAsync(EventInfo eventInfo) + { + if (!eventInfo.MerchantId.HasValue) + { + _logger.LogError("赛事未关联商户配置,赛事ID={EventId},赛事名称={EventName}", + eventInfo.Id, eventInfo.EventName); + throw new ArgumentException($"赛事[{eventInfo.EventName}]未关联商户配置,请联系管理员配置", nameof(eventInfo)); + } + + var payConfig = await _weChatPayConfigRepository.GetFirstAsync(x => + x.MerchantId == eventInfo.MerchantId.Value && + x.Status == AvailabilityStatus.Enable && + !x.DeleteMark); + + if (payConfig == null) + { + _logger.LogError("未找到赛事关联的商户支付配置,赛事ID={EventId},商户ID={MerchantId}", + eventInfo.Id, eventInfo.MerchantId); + } + + return payConfig; + } + /// /// 上传支付凭证(线下支付) /// @@ -927,23 +1033,14 @@ public class EventClientController : IDynamicApiController 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); - } - + // 根据赛事信息获取对应的商户支付配置 + var payConfig = await GetWeChatPayConfigByEventAsync(evt); if (payConfig == null) { - payConfig = await _weChatPayConfigRepository.GetFirstAsync(x => - x.Status == AvailabilityStatus.Enable && !x.DeleteMark); + _logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId); + throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员"); } - if (payConfig == null) throw Oops.Bah("微信支付配置未设置"); - var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, reg.OrderNo); if (queryResult.Success && queryResult.TradeState == "SUCCESS") { @@ -966,11 +1063,21 @@ public class EventClientController : IDynamicApiController /// /// 微信支付回调通知处理 /// + /// + /// 处理流程: + /// 1. 接收微信支付平台回调通知 + /// 2. 使用平台级别的支付配置验证签名和解密数据(微信平台证书是平台级别) + /// 3. 获取订单号后查找报名记录 + /// 4. 根据报名记录获取赛事信息 + /// 5. 根据赛事信息获取商户级别的支付配置(用于后续可能的退款等操作) + /// 6. 更新订单支付状态 + /// + /// 微信支付平台要求的响应格式 [HttpPost("wechat-pay-notify")] [AllowAnonymous] [UnitOfWork] - [NonAction] - public async Task WeChatPayNotify() + [DisplayName("微信支付回调通知")] + public async Task WeChatPayNotify() { try { @@ -987,16 +1094,17 @@ public class EventClientController : IDynamicApiController string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(serialNumber)) { _logger.LogWarning("微信支付回调通知参数不完整"); - return new { code = "FAIL", message = "参数不完整" }; + return new JsonResult(new { code = "FAIL", message = "参数不完整" }); } - // 获取默认支付配置(后续可根据订单号找到对应的商户配置) - var payConfig = await _weChatPayConfigRepository.GetFirstAsync(x => + // 第一步:使用平台级别的支付配置验证签名和解密数据 + // 注意:微信支付签名验证使用的是微信平台证书(平台级别),所有商户共用 + var platformPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x => x.Status == AvailabilityStatus.Enable && !x.DeleteMark); - if (payConfig == null) + if (platformPayConfig == null) { - _logger.LogError("微信支付配置未找到"); - return new { code = "FAIL", message = "支付配置未找到" }; + _logger.LogError("未找到平台级别的微信支付配置"); + return new JsonResult(new { code = "FAIL", message = "支付配置未找到" }); } var notifyRequest = new WeChatPayNotifyRequest @@ -1009,19 +1117,19 @@ public class EventClientController : IDynamicApiController }; // 验证签名 - var isValid = await _weChatPayService.VerifyPayNotifyAsync(payConfig, notifyRequest); + var isValid = await _weChatPayService.VerifyPayNotifyAsync(platformPayConfig, notifyRequest); if (!isValid) { _logger.LogWarning("微信支付回调签名验证失败"); - return new { code = "FAIL", message = "签名验证失败" }; + return new JsonResult(new { code = "FAIL", message = "签名验证失败" }); } // 解密通知数据 - var notifyData = await _weChatPayService.DecryptPayNotifyDataAsync(payConfig, notifyRequest); + var notifyData = await _weChatPayService.DecryptPayNotifyDataAsync(platformPayConfig, notifyRequest); if (notifyData == null || notifyData.TradeState != "SUCCESS") { _logger.LogWarning("微信支付回调数据无效或交易未成功"); - return new { code = "SUCCESS", message = "成功" }; + return new JsonResult(new { code = "SUCCESS", message = "成功" }); } // 根据商户订单号查找报名记录 @@ -1029,15 +1137,35 @@ public class EventClientController : IDynamicApiController if (reg == null) { _logger.LogWarning($"未找到订单:{notifyData.OutTradeNo}"); - return new { code = "SUCCESS", message = "成功" }; + return new JsonResult(new { code = "SUCCESS", message = "成功" }); } if (reg.PaymentStatus == 1) { _logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付回调"); - return new { code = "SUCCESS", message = "成功" }; + 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 = "赛事信息不存在" }); + } + + // 验证赛事是否关联了有效的商户支付配置 + var merchantPayConfig = await GetWeChatPayConfigByEventAsync(evt); + if (merchantPayConfig == null) + { + _logger.LogError("赛事未配置有效的商户支付配置,订单号={OrderNo},赛事ID={EventId}", + reg.OrderNo, evt.Id); + 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; @@ -1048,12 +1176,17 @@ public class EventClientController : IDynamicApiController await _registrationRepository.UpdateAsync(reg); _logger.LogInformation($"订单 {reg.OrderNo} 支付成功,微信交易号:{notifyData.TransactionId}"); - return new { code = "SUCCESS", message = "成功" }; + return new JsonResult(new { code = "SUCCESS", message = "成功" }); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "处理微信支付回调通知时参数验证失败"); + return new JsonResult(new { code = "FAIL", message = "业务验证失败" }); } catch (Exception ex) { _logger.LogError(ex, "处理微信支付回调通知异常"); - return new { code = "FAIL", message = "处理失败" }; + return new JsonResult(new { code = "FAIL", message = "处理失败" }); } } diff --git a/YD_Event.Application/Client/WeChatController.cs b/YD_Event.Application/Client/WeChatController.cs new file mode 100644 index 0000000..4ea6144 --- /dev/null +++ b/YD_Event.Application/Client/WeChatController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using YD_Event.Application.Client.Dtos; +using YD_Event.Core.Infrastructure.WeChat; + +namespace YD_Event.Application.Client; + +/// +/// 微信小程序接口 +/// +[ApiDescriptionSettings("微信小程序接口")] +[AllowAnonymous] +public class WeChatController : IDynamicApiController +{ + private readonly WeChatService _weChatService; + private readonly ILogger_logger; + + public WeChatController( + WeChatService weChatService, + ILoggerlogger) + { + _weChatService = weChatService; + _logger = logger; + } + + /// + /// 小程序 code 换取 openid 和 session_key + /// + /// 包含 code 的输入参数 + /// openid、session_key 等信息 + [HttpPost("jscode2session")] + [DisplayName("小程序 code 换取 openid")] + public async Task Code2Session([FromBody] WeChatCode2SessionInput input) + { + _logger.LogInformation("开始调用微信 code2session 接口"); + + var result = await _weChatService.Code2Session(input.Code); + + _logger.LogInformation("微信 code2session 接口调用成功,OpenId={OpenId}", result.OpenId); + + return new WeChatCode2SessionOutput + { + OpenId = result.OpenId, + SessionKey = result.SessionKey, + UnionId = result.UnionId + }; + } +} \ No newline at end of file diff --git a/YD_Event.Application/Event/Dtos/EventDtos.cs b/YD_Event.Application/Event/Dtos/EventDtos.cs index 5fc8c93..8878287 100644 --- a/YD_Event.Application/Event/Dtos/EventDtos.cs +++ b/YD_Event.Application/Event/Dtos/EventDtos.cs @@ -473,6 +473,11 @@ public class EventFeeConfigInput /// 营业执照图片URL (显示字段) /// public string? BusinessLicenseUrl { get; set; } + + /// + /// 项目价格字典 (key: 项目ID, value: 价格) + /// + public Dictionary? ProjectPrices { get; set; } } /// diff --git a/YD_Event.Application/Event/EventService.cs b/YD_Event.Application/Event/EventService.cs index aba3f6b..97391d9 100644 --- a/YD_Event.Application/Event/EventService.cs +++ b/YD_Event.Application/Event/EventService.cs @@ -264,6 +264,16 @@ public class EventService : BaseService, ITransient output.FeeConfig.BusinessLicenseUrl = merchant.BusinessLicenseUrl; } } + + // 从 EventProject 表中读取项目价格并填充到 ProjectPrices + if (eventProjects.Any()) + { + output.FeeConfig.ProjectPrices = new Dictionary(); + foreach (var ep in eventProjects) + { + output.FeeConfig.ProjectPrices[ep.Id] = ep.Price / 100m; + } + } } return output; diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 4d609d1..0122a47 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -437,12 +437,6 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient var eventInfo = await _eventRepository.GetByIdAsync(input.EventId); if (eventInfo == null) throw Oops.Bah("赛事不存在"); - - // 验证1:一次报名只能选择一个项目 - if (input.ProjectIds != null && input.ProjectIds.Count > 1) - { - throw Oops.Bah("一次报名只能选择一个项目"); - } // 验证2:一次报名只能选择单个团队内的运动员 if (input.MemberIds != null && input.MemberIds.Count > 0) diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index bd97233..351ce95 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -1958,12 +1958,12 @@ - 联系人姓名 + 联系人姓名 (个人报名时可自动从用户信息获取) - 联系人电话 + 联系人电话 (个人报名时可自动从用户信息获取) @@ -1993,7 +1993,7 @@ - 参赛人员列表 + 参赛人员列表 (个人报名时可自动从用户信息获取) @@ -3029,6 +3029,36 @@ 报名项目集合 + + + 微信小程序 code2session 输入参数 + + + + + 登录时获取的 code + + + + + 微信小程序 code2session 输出参数 + + + + + 用户唯一标识 + + + + + 会话密钥 + + + + + 用户在开放平台的唯一标识符(在满足 UnionID 下发条件的情况下会返回) + + 小程序-赛事接口 @@ -3079,6 +3109,19 @@ 支付信息查询输入参数,包含报名记录ID和OpenId 支付信息,包括微信支付参数(如果是微信支付) + + + 根据赛事ID精确获取对应的微信支付配置 + + + 1. 通过赛事信息中的 MerchantId 查找对应的微信支付配置 + 2. 必须确保赛事已关联商户配置,不使用兜底策略 + 3. 只返回启用状态且未删除的配置 + + 赛事信息,必须包含有效的 MerchantId + 微信支付配置,如果未找到则返回 null + 当赛事没有关联商户配置时抛出 + 上传支付凭证(线下支付) @@ -3135,6 +3178,16 @@ 微信支付回调通知处理 + + 处理流程: + 1. 接收微信支付平台回调通知 + 2. 使用平台级别的支付配置验证签名和解密数据(微信平台证书是平台级别) + 3. 获取订单号后查找报名记录 + 4. 根据报名记录获取赛事信息 + 5. 根据赛事信息获取商户级别的支付配置(用于后续可能的退款等操作) + 6. 更新订单支付状态 + + 微信支付平台要求的响应格式 @@ -3274,6 +3327,18 @@ 生成自定义 Token + + + 微信小程序接口 + + + + + 小程序 code 换取 openid 和 session_key + + 包含 code 的输入参数 + openid、session_key 等信息 + OCR识别输入 @@ -4039,6 +4104,11 @@ 营业执照图片URL (显示字段) + + + 项目价格字典 (key: 项目ID, value: 价格) + + 更新赛事输入