微信小程序接口对接

This commit is contained in:
马梓彭 2026-04-07 23:52:35 +08:00
parent efabdebbd2
commit d4d5c13a13
8 changed files with 398 additions and 105 deletions

View File

@ -392,18 +392,16 @@ public class SubmitRegistrationInput
public string? TeamName { get; set; }
/// <summary>
/// 联系人姓名
/// 联系人姓名 (个人报名时可自动从用户信息获取)
/// </summary>
[Required(ErrorMessage = "联系人姓名不能为空")]
[MaxLength(50, ErrorMessage = "联系人姓名不能超过50个字符")]
public string ContactName { get; set; }
public string? ContactName { get; set; }
/// <summary>
/// 联系人电话
/// 联系人电话 (个人报名时可自动从用户信息获取)
/// </summary>
[Required(ErrorMessage = "联系人电话不能为空")]
[RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号格式")]
public string ContactPhone { get; set; }
public string? ContactPhone { get; set; }
/// <summary>
/// 报名项目列表
@ -436,11 +434,9 @@ public class SubmitRegistrationItemInput
public long? GroupId { get; set; }
/// <summary>
/// 参赛人员列表
/// 参赛人员列表 (个人报名时可自动从用户信息获取)
/// </summary>
[Required(ErrorMessage = "参赛人员列表不能为空")]
[MinLength(1, ErrorMessage = "请至少添加一名参赛人员")]
public List<PersonInput> Persons { get; set; }
public List<PersonInput>? Persons { get; set; }
}
/// <summary>

View File

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using YD_Event.Core.Infrastructure.WeChat;
namespace YD_Event.Application.Client.Dtos;
/// <summary>
/// 微信小程序 code2session 输入参数
/// </summary>
public class WeChatCode2SessionInput
{
/// <summary>
/// 登录时获取的 code
/// </summary>
[Required(ErrorMessage = "code 不能为空")]
public string Code { get; set; }
}
/// <summary>
/// 微信小程序 code2session 输出参数
/// </summary>
public class WeChatCode2SessionOutput
{
/// <summary>
/// 用户唯一标识
/// </summary>
public string OpenId { get; set; }
/// <summary>
/// 会话密钥
/// </summary>
public string SessionKey { get; set; }
/// <summary>
/// 用户在开放平台的唯一标识符(在满足 UnionID 下发条件的情况下会返回)
/// </summary>
public string? UnionId { get; set; }
}

View File

@ -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<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
private readonly ISqlSugarRepository<ProjectGroup> _groupRepository;
private readonly ISqlSugarRepository<EventProjectGroup> _eventProjectGroupRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventRegistrationItem> _registrationItemRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Banner> _bannerRepository;
@ -51,6 +53,7 @@ public class EventClientController : IDynamicApiController
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<ProjectGroup> groupRepository,
ISqlSugarRepository<EventProjectGroup> eventProjectGroupRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Banner> 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<PersonInput>
{
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<EventRegistrationItem>();
var persons = new List<EventRegistrationPerson>();
// 先获取所有需要的 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;
}
/// <summary>
/// 根据赛事ID精确获取对应的微信支付配置
/// </summary>
/// <remarks>
/// 1. 通过赛事信息中的 MerchantId 查找对应的微信支付配置
/// 2. 必须确保赛事已关联商户配置,不使用兜底策略
/// 3. 只返回启用状态且未删除的配置
/// </remarks>
/// <param name="eventInfo">赛事信息,必须包含有效的 MerchantId</param>
/// <returns>微信支付配置,如果未找到则返回 null</returns>
/// <exception cref="ArgumentException">当赛事没有关联商户配置时抛出</exception>
private async Task<WeChatPayConfig?> 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;
}
/// <summary>
/// 上传支付凭证(线下支付)
/// </summary>
@ -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
/// <summary>
/// 微信支付回调通知处理
/// </summary>
/// <remarks>
/// 处理流程:
/// 1. 接收微信支付平台回调通知
/// 2. 使用平台级别的支付配置验证签名和解密数据(微信平台证书是平台级别)
/// 3. 获取订单号后查找报名记录
/// 4. 根据报名记录获取赛事信息
/// 5. 根据赛事信息获取商户级别的支付配置(用于后续可能的退款等操作)
/// 6. 更新订单支付状态
/// </remarks>
/// <returns>微信支付平台要求的响应格式</returns>
[HttpPost("wechat-pay-notify")]
[AllowAnonymous]
[UnitOfWork]
[NonAction]
public async Task<object> WeChatPayNotify()
[DisplayName("微信支付回调通知")]
public async Task<IActionResult> 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 = "处理失败" });
}
}

View File

@ -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;
/// <summary>
/// 微信小程序接口
/// </summary>
[ApiDescriptionSettings("微信小程序接口")]
[AllowAnonymous]
public class WeChatController : IDynamicApiController
{
private readonly WeChatService _weChatService;
private readonly ILogger<WeChatController>_logger;
public WeChatController(
WeChatService weChatService,
ILogger<WeChatController>logger)
{
_weChatService = weChatService;
_logger = logger;
}
/// <summary>
/// 小程序 code 换取 openid 和 session_key
/// </summary>
/// <param name="input">包含 code 的输入参数</param>
/// <returns>openid、session_key 等信息</returns>
[HttpPost("jscode2session")]
[DisplayName("小程序 code 换取 openid")]
public async Task<WeChatCode2SessionOutput> 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
};
}
}

View File

@ -473,6 +473,11 @@ public class EventFeeConfigInput
/// 营业执照图片URL (显示字段)
/// </summary>
public string? BusinessLicenseUrl { get; set; }
/// <summary>
/// 项目价格字典 (key: 项目ID, value: 价格)
/// </summary>
public Dictionary<long, decimal>? ProjectPrices { get; set; }
}
/// <summary>

View File

@ -264,6 +264,16 @@ public class EventService : BaseService<EventInfo>, ITransient
output.FeeConfig.BusinessLicenseUrl = merchant.BusinessLicenseUrl;
}
}
// 从 EventProject 表中读取项目价格并填充到 ProjectPrices
if (eventProjects.Any())
{
output.FeeConfig.ProjectPrices = new Dictionary<long, decimal>();
foreach (var ep in eventProjects)
{
output.FeeConfig.ProjectPrices[ep.Id] = ep.Price / 100m;
}
}
}
return output;

View File

@ -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)

View File

@ -1958,12 +1958,12 @@
</member>
<member name="P:YD_Event.Application.Client.Dtos.SubmitRegistrationInput.ContactName">
<summary>
联系人姓名
联系人姓名 (个人报名时可自动从用户信息获取)
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.SubmitRegistrationInput.ContactPhone">
<summary>
联系人电话
联系人电话 (个人报名时可自动从用户信息获取)
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.SubmitRegistrationInput.Items">
@ -1993,7 +1993,7 @@
</member>
<member name="P:YD_Event.Application.Client.Dtos.SubmitRegistrationItemInput.Persons">
<summary>
参赛人员列表
参赛人员列表 (个人报名时可自动从用户信息获取)
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.PersonInput">
@ -3029,6 +3029,36 @@
报名项目集合
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.WeChatCode2SessionInput">
<summary>
微信小程序 code2session 输入参数
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatCode2SessionInput.Code">
<summary>
登录时获取的 code
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.WeChatCode2SessionOutput">
<summary>
微信小程序 code2session 输出参数
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatCode2SessionOutput.OpenId">
<summary>
用户唯一标识
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatCode2SessionOutput.SessionKey">
<summary>
会话密钥
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatCode2SessionOutput.UnionId">
<summary>
用户在开放平台的唯一标识符(在满足 UnionID 下发条件的情况下会返回)
</summary>
</member>
<member name="T:YD_Event.Application.Client.EventClientController">
<summary>
小程序-赛事接口
@ -3079,6 +3109,19 @@
<param name="input">支付信息查询输入参数,包含报名记录ID和OpenId</param>
<returns>支付信息,包括微信支付参数(如果是微信支付)</returns>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.GetWeChatPayConfigByEventAsync(YD_Event.Core.Entities.EventInfo)">
<summary>
根据赛事ID精确获取对应的微信支付配置
</summary>
<remarks>
1. 通过赛事信息中的 MerchantId 查找对应的微信支付配置
2. 必须确保赛事已关联商户配置,不使用兜底策略
3. 只返回启用状态且未删除的配置
</remarks>
<param name="eventInfo">赛事信息,必须包含有效的 MerchantId</param>
<returns>微信支付配置,如果未找到则返回 null</returns>
<exception cref="T:System.ArgumentException">当赛事没有关联商户配置时抛出</exception>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.UploadPaymentVoucher(System.Int64,YD_Event.Application.Client.Dtos.UploadVoucherInput)">
<summary>
上传支付凭证(线下支付)
@ -3135,6 +3178,16 @@
<summary>
微信支付回调通知处理
</summary>
<remarks>
处理流程:
1. 接收微信支付平台回调通知
2. 使用平台级别的支付配置验证签名和解密数据(微信平台证书是平台级别)
3. 获取订单号后查找报名记录
4. 根据报名记录获取赛事信息
5. 根据赛事信息获取商户级别的支付配置(用于后续可能的退款等操作)
6. 更新订单支付状态
</remarks>
<returns>微信支付平台要求的响应格式</returns>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.GetOpenIdFromRequest">
<summary>
@ -3274,6 +3327,18 @@
生成自定义 Token
</summary>
</member>
<member name="T:YD_Event.Application.Client.WeChatController">
<summary>
微信小程序接口
</summary>
</member>
<member name="M:YD_Event.Application.Client.WeChatController.Code2Session(YD_Event.Application.Client.Dtos.WeChatCode2SessionInput)">
<summary>
小程序 code 换取 openid 和 session_key
</summary>
<param name="input">包含 code 的输入参数</param>
<returns>openid、session_key 等信息</returns>
</member>
<member name="T:YD_Event.Application.Common.Dtos.AthleteOcrInput">
<summary>
OCR识别输入
@ -4039,6 +4104,11 @@
营业执照图片URL (显示字段)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.EventFeeConfigInput.ProjectPrices">
<summary>
项目价格字典 (key: 项目ID, value: 价格)
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.UpdateEventInput">
<summary>
更新赛事输入