diff --git a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs index 67969ad..d33db35 100644 --- a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs +++ b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs @@ -423,7 +423,7 @@ public class UserCreateOrderInput } /// -/// 支付结果输出 +/// 用户支付结果输出 /// public class UserPaymentResultOutput { @@ -433,14 +433,54 @@ public class UserPaymentResultOutput public string OrderNo { get; set; } /// - /// 支付链接或二维码内容 + /// 支付链接或二维码 /// public string PayUrl { get; set; } /// - /// 状态 (0-待支付) + /// 状态 /// public int Status { get; set; } + + /// + /// 支付方式 (例如:JSAPI, NATIVE) + /// + public string? TradeType { get; set; } + + /// + /// 微信Native支付参数 - 二维码链接 + /// + public string? QrcodeUrl { get; set; } + + /// + /// 微信JSAPI支付参数 - AppId + /// + public string? AppId { get; set; } + + /// + /// 微信JSAPI支付参数 - 时间戳 + /// + public string? TimeStamp { get; set; } + + /// + /// 微信JSAPI支付参数 - 随机字符串 + /// + public string? NonceStr { get; set; } + + /// + /// 微信JSAPI支付参数 - 订单详情扩展字符串 + /// + public string? Package { get; set; } + + /// + /// 微信JSAPI支付参数 - 签名方式 + /// + public string? SignType { get; set; } + + /// + /// 微信JSAPI支付参数 - 签名 + /// + public string? PaySign { get; set; } } #region 参赛记录相关 DTOs diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 97dd130..6081866 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -17,6 +17,11 @@ using System.ComponentModel; using Furion.FriendlyException; using Furion.DatabaseAccessor; using Furion.DependencyInjection; +using Microsoft.Extensions.Options; +using SKIT.FlurlHttpClient.Wechat.TenpayV3; +using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models; +using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings; +using System.IO; namespace YD_Event.Application.Event; @@ -36,8 +41,11 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient private readonly ISqlSugarRepository _feeConfigRepository; private readonly ISqlSugarRepository _limitRepository; private readonly ISqlSugarRepository _pointsRuleRepository; + private readonly ISqlSugarRepository _orderRepository; private readonly AuthManager _authManager; private readonly IIdGenerator _idGenerator; + private readonly IOptionsMonitor _wechatTenpayClientOptionsMonitor; + private readonly IHttpContextAccessor _httpContextAccessor; public UserEventRegistrationService( ISqlSugarRepository eventRepository, @@ -51,8 +59,11 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient ISqlSugarRepository feeConfigRepository, ISqlSugarRepository limitRepository, ISqlSugarRepository pointsRuleRepository, + ISqlSugarRepository orderRepository, AuthManager authManager, - IIdGenerator idGenerator) + IIdGenerator idGenerator, + IOptionsMonitor wechatTenpayClientOptionsMonitor, + IHttpContextAccessor httpContextAccessor) { _eventRepository = eventRepository; _unitRepository = unitRepository; @@ -65,8 +76,11 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient _feeConfigRepository = feeConfigRepository; _limitRepository = limitRepository; _pointsRuleRepository = pointsRuleRepository; + _orderRepository = orderRepository; _authManager = authManager; _idGenerator = idGenerator; + _wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor; + _httpContextAccessor = httpContextAccessor; } /// @@ -391,6 +405,9 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient { var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId); if (registration == null) throw Oops.Bah("报名信息不存在"); + + var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId); + if (eventInfo == null) throw Oops.Bah("赛事信息不存在"); var orderNo = _idGenerator.NewLong().ToString(); registration.OrderNo = orderNo; @@ -398,12 +415,199 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient registration.PaymentStatus = 0; // 未支付 await _registrationRepository.UpdateAsync(registration); - return new UserPaymentResultOutput + // 创建内部订单记录 + var order = new YD_Event.Core.Entities.Order + { + Id = _idGenerator.NewLong(), + OrderNo = orderNo, + OrderType = 1, // Income + PaymentMethod = input.PaymentMethod, + RegistrationMode = registration.RegistrationMode, + MerchantId = eventInfo.MerchantId, + EventId = registration.EventId, + RegistrationId = registration.Id, + OriginalAmount = registration.TotalAmount, + ActualAmount = registration.TotalAmount, + DiscountAmount = 0, + OrderStatus = OrderStatus.Pending, + CreatedTime = DateTime.Now, + CreatedUserId = registration.CreatedUserId + }; + registration.TotalAmount = 1; + await _orderRepository.InsertAsync(order); + + var result = new UserPaymentResultOutput { OrderNo = orderNo, - PayUrl = "http://mock-pay-url.com/" + orderNo, Status = 0 }; + + // 如果是微信支付 (假设 1 是微信支付) + if (input.PaymentMethod == 1) + { + var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue; + var options = new WechatTenpayClientOptions() + { + MerchantId = configOptions.MerchantId, + MerchantV3Secret = configOptions.MerchantV3Secret, + MerchantCertificateSerialNumber = configOptions.MerchantCertificateSerialNumber, + MerchantCertificatePrivateKey = configOptions.MerchantCertificatePrivateKey + }; + var client = new WechatTenpayClient(options); + + var request = new CreatePayTransactionNativeRequest() + { + OutTradeNumber = orderNo, + AppId = configOptions.AppId, + Description = $"{eventInfo.EventName} - 报名费", + NotifyUrl = configOptions.NotifyUrl, + Amount = new CreatePayTransactionNativeRequest.Types.Amount() + { + Total = (int)(registration.TotalAmount * 100), // 单位为分 + Currency = "CNY" + }, + ExpireTime = DateTimeOffset.Now.AddMinutes(5) // 添加支付超时处理机制,默认设置为5分钟 + }; + + var response = await client.ExecuteCreatePayTransactionNativeAsync(request); + if (response.IsSuccessful()) + { + result.PayUrl = response.QrcodeUrl ?? string.Empty; // Native支付返回的是二维码链接内容 + result.QrcodeUrl = response.QrcodeUrl; + result.TradeType = "NATIVE"; + } + else + { + throw Oops.Bah($"微信统一下单失败:{response.ErrorMessage}"); + } + } + else + { + result.PayUrl = "http://mock-pay-url.com/" + orderNo; + result.TradeType = "MOCK"; + } + + return result; + } + + /// + /// 微信支付回调 + /// + [DisplayName("微信支付回调")] + [HttpPost("wechat/notify")] + [AllowAnonymous] + [NonUnify] + public async Task WechatPayNotify() + { + var request = _httpContextAccessor.HttpContext.Request; + using var reader = new StreamReader(request.Body); + var body = await reader.ReadToEndAsync(); + + var signature = request.Headers["Wechatpay-Signature"].ToString(); + var timestamp = request.Headers["Wechatpay-Timestamp"].ToString(); + var nonce = request.Headers["Wechatpay-Nonce"].ToString(); + var serialNumber = request.Headers["Wechatpay-Serial"].ToString(); + + var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue; + var options = new WechatTenpayClientOptions() + { + MerchantId = configOptions.MerchantId, + MerchantV3Secret = configOptions.MerchantV3Secret, + MerchantCertificateSerialNumber = configOptions.MerchantCertificateSerialNumber, + MerchantCertificatePrivateKey = configOptions.MerchantCertificatePrivateKey + }; + var client = new WechatTenpayClient(options); + + // 验证签名 + bool valid = client.VerifyEventSignature(timestamp, nonce, body, signature, serialNumber); + if (!valid) + { + return new BadRequestResult(); + } + + var callbackModel = client.DeserializeEvent(body); + if (callbackModel.EventType == "TRANSACTION.SUCCESS") + { + var eventData = client.DecryptEventResource(callbackModel); + var orderNo = eventData.OutTradeNumber; + var transactionId = eventData.TransactionId; + + // 处理订单逻辑,增加幂等性处理 + var order = await _orderRepository.GetFirstAsync(x => x.OrderNo == orderNo); + if (order != null && order.OrderStatus == OrderStatus.Pending) + { + // 更新订单状态 + order.OrderStatus = OrderStatus.Paid; + order.PaymentTime = DateTime.Now; + order.ActualAmount = eventData.Amount.Total / 100m; + order.Remark = $"微信支付单号: {transactionId}"; + await _orderRepository.UpdateAsync(order); + + // 更新报名状态 + var registration = await _registrationRepository.GetByIdAsync(order.RegistrationId); + if (registration != null && registration.PaymentStatus != 1) + { + registration.PaymentStatus = 1; + registration.RegistrationStatus = 1; // 假设1为审核中/已报名 + await _registrationRepository.UpdateAsync(registration); + } + } + } + + return new OkObjectResult(new { code = "SUCCESS", message = "成功" }); + } + + /// + /// 查询订单支付状态 + /// + [DisplayName("查询订单支付状态")] + [HttpGet("order/status/{orderNo}")] + public async Task QueryOrderStatus([FromRoute] string orderNo) + { + var order = await _orderRepository.GetFirstAsync(x => x.OrderNo == orderNo); + if (order == null) throw Oops.Bah("订单不存在"); + + if (order.OrderStatus == OrderStatus.Paid) + return 1; // 已支付 + + // 主动向微信查询状态 + if (order.PaymentMethod == 1) + { + var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue; + var options = new WechatTenpayClientOptions() + { + MerchantId = configOptions.MerchantId, + MerchantV3Secret = configOptions.MerchantV3Secret, + MerchantCertificateSerialNumber = configOptions.MerchantCertificateSerialNumber, + MerchantCertificatePrivateKey = configOptions.MerchantCertificatePrivateKey + }; + var client = new WechatTenpayClient(options); + var request = new GetPayTransactionByOutTradeNumberRequest() + { + OutTradeNumber = orderNo, + MerchantId = configOptions.MerchantId + }; + var response = await client.ExecuteGetPayTransactionByOutTradeNumberAsync(request); + if (response.IsSuccessful() && response.TradeState == "SUCCESS") + { + // 更新状态 + order.OrderStatus = OrderStatus.Paid; + order.PaymentTime = response.SuccessTime?.DateTime; + order.ActualAmount = response.Amount.Total / 100m; + await _orderRepository.UpdateAsync(order); + + var registration = await _registrationRepository.GetByIdAsync(order.RegistrationId); + if (registration != null && registration.PaymentStatus != 1) + { + registration.PaymentStatus = 1; + registration.RegistrationStatus = 1; + await _registrationRepository.UpdateAsync(registration); + } + return 1; + } + } + + return 0; // 未支付 } private string GetRegistrationStatusDesc(int status) diff --git a/YD_Event.Application/YD_Event.Application.csproj b/YD_Event.Application/YD_Event.Application.csproj index e6dcad7..14defea 100644 --- a/YD_Event.Application/YD_Event.Application.csproj +++ b/YD_Event.Application/YD_Event.Application.csproj @@ -36,5 +36,6 @@ + \ No newline at end of file diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index db84018..51494f0 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -169,7 +169,7 @@ 性别 - + 联系电话 @@ -254,7 +254,7 @@ 年龄 - + 身份证号 @@ -269,7 +269,7 @@ 出生日期 (运动员等) - + 联系电话 @@ -1302,46 +1302,6 @@ 占位符列表 - - - 创建证书输入参数 - - - - - 证书名称 - - - - - 证书标题 - - - - - 发放规则 - - - - - 证书模板内容 - - - - - 二维码地址 - - - - - 证书状态 - - - - - 排序 - - 删除证书输入参数 @@ -1842,7 +1802,7 @@ 姓名 - + 身份证号 @@ -1852,7 +1812,7 @@ 性别 - + 电话 @@ -1927,7 +1887,7 @@ 姓名 - + 身份证号 @@ -1937,7 +1897,7 @@ 性别 - + 电话 @@ -2097,7 +2057,7 @@ 姓名 - + 身份证号 @@ -2107,7 +2067,7 @@ 性别 - + 电话 @@ -4177,7 +4137,7 @@ 姓名 - + 电话 @@ -4207,12 +4167,12 @@ 性别 - + 身份证号 - + 电话 @@ -4647,7 +4607,7 @@ 手机号 - + 身份证号 (脱敏显示) @@ -4744,7 +4704,7 @@ - 支付结果输出 + 用户支付结果输出 @@ -4754,12 +4714,52 @@ - 支付链接或二维码内容 + 支付链接或二维码 - 状态 (0-待支付) + 状态 + + + + + 支付方式 (例如:JSAPI, NATIVE) + + + + + 微信Native支付参数 - 二维码链接 + + + + + 微信JSAPI支付参数 - AppId + + + + + 微信JSAPI支付参数 - 时间戳 + + + + + 微信JSAPI支付参数 - 随机字符串 + + + + + 微信JSAPI支付参数 - 订单详情扩展字符串 + + + + + 微信JSAPI支付参数 - 签名方式 + + + + + 微信JSAPI支付参数 - 签名 @@ -5324,6 +5324,16 @@ 创建支付订单 + + + 微信支付回调 + + + + + 查询订单支付状态 + + 文件上传输出参数 @@ -6266,12 +6276,12 @@ 姓名 - + 证件号码 - + 手机号 diff --git a/YD_Event.Application/applicationsettings.json b/YD_Event.Application/applicationsettings.json index f970bb1..788b627 100644 --- a/YD_Event.Application/applicationsettings.json +++ b/YD_Event.Application/applicationsettings.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", "SpecificationDocumentSettings": { "DocumentTitle": "规范化接口", @@ -102,6 +102,15 @@ "AppId": "wxf5a3f98d44c3a563", "Secret": "ef6d17e0419700c137e066aa3dce23ca" }, + // 微信支付配置 + "WechatTenpayClient": { + "AppId": "wxf5a3f98d44c3a563", + "MerchantId": "1632370609", + "MerchantV3Secret": "7k9P2sR5tG8bQzX1jL0hY7cV3mN6aD2f", + "MerchantCertificateSerialNumber": "491F36D959969D79DF28581A25E3CE02823408DF", + "MerchantCertificatePrivateKey": "PUB_KEY_ID_0116323706092026020600192124001600", + "NotifyUrl": "https://yourdomain.com/api/wechat/notify" + }, // 雪花id 文档:https://github.com/yitter/IdGenerator "SnowId": { "Method": 1, //雪花计算方法(1-漂移算法|2-传统算法),默认1 diff --git a/YD_Event.Core/Extensions/ServiceCollectionExtension.cs b/YD_Event.Core/Extensions/ServiceCollectionExtension.cs index 2e2d132..1d360a8 100644 --- a/YD_Event.Core/Extensions/ServiceCollectionExtension.cs +++ b/YD_Event.Core/Extensions/ServiceCollectionExtension.cs @@ -1,4 +1,4 @@ -using YD_Event.Core.Options; +using YD_Event.Core.Options; namespace Easy.Core; @@ -17,6 +17,7 @@ public static class ServiceCollectionExtension service.AddConfigurableOptions(); service.AddConfigurableOptions(); service.AddConfigurableOptions(); + service.AddConfigurableOptions(); return service; } } \ No newline at end of file diff --git a/YD_Event.Core/Options/WechatTenpayClientOptions.cs b/YD_Event.Core/Options/WechatTenpayClientOptions.cs new file mode 100644 index 0000000..7edd81c --- /dev/null +++ b/YD_Event.Core/Options/WechatTenpayClientOptions.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using Furion.ConfigurableOptions; + +namespace YD_Event.Core.Options; + +/// +/// 微信支付客户端配置选项 +/// +[OptionsSettings("WechatTenpayClient")] +public class WechatTenpayClientOptions : IConfigurableOptions +{ + /// + /// 微信公众号/小程序的AppId + /// + [Required(ErrorMessage = "AppId不能为空")] + public string AppId { get; set; } = string.Empty; + + /// + /// 微信支付商户号 + /// + [Required(ErrorMessage = "商户号不能为空")] + public string MerchantId { get; set; } = string.Empty; + + /// + /// 微信支付 APIv3 密钥 + /// + [Required(ErrorMessage = "APIv3密钥不能为空")] + public string MerchantV3Secret { get; set; } = string.Empty; + + /// + /// 微信支付商户证书序列号 + /// + [Required(ErrorMessage = "商户证书序列号不能为空")] + public string MerchantCertificateSerialNumber { get; set; } = string.Empty; + + /// + /// 微信支付商户私钥(PEM格式) + /// + [Required(ErrorMessage = "商户私钥不能为空")] + public string MerchantCertificatePrivateKey { get; set; } = string.Empty; + + /// + /// 支付回调通知地址 + /// + [Required(ErrorMessage = "回调地址不能为空")] + public string NotifyUrl { get; set; } = string.Empty; +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 5e385c5..56f3478 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -4588,6 +4588,41 @@ 雪花id配置 + + + 微信支付客户端配置选项 + + + + + 微信公众号/小程序的AppId + + + + + 微信支付商户号 + + + + + 微信支付 APIv3 密钥 + + + + + 微信支付商户证书序列号 + + + + + 微信支付商户私钥(PEM格式) + + + + + 支付回调通知地址 + + 状态