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
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: 价格)
+
+
更新赛事输入