微信小程序接口对接
This commit is contained in:
parent
efabdebbd2
commit
d4d5c13a13
@ -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>
|
||||
|
||||
37
YD_Event.Application/Client/Dtos/WeChatDtos.cs
Normal file
37
YD_Event.Application/Client/Dtos/WeChatDtos.cs
Normal 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; }
|
||||
}
|
||||
@ -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 = "处理失败" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
48
YD_Event.Application/Client/WeChatController.cs
Normal file
48
YD_Event.Application/Client/WeChatController.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
更新赛事输入
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user