diff --git a/honey_box/common/env.js b/honey_box/common/env.js index f192d080..13f1ca9d 100644 --- a/honey_box/common/env.js +++ b/honey_box/common/env.js @@ -11,8 +11,8 @@ // 测试环境配置 - .NET 10 后端 const testing = { - baseUrl: 'https://app.zpc-xy.com/honey/api', - // baseUrl: 'http://192.168.1.24:5238', + // baseUrl: 'https://app.zpc-xy.com/honey/api', + baseUrl: 'http://192.168.1.24:5238', imageUrl: 'https://youdas-1308826010.cos.ap-shanghai.myqcloud.com', loginPage: '', wxAppId: '' diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts index 2508c82b..472321d8 100644 --- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts @@ -110,6 +110,8 @@ export interface WeixinPayMerchant { cert_path?: string /** 是否启用 */ is_enabled?: string + /** 支付回调地址 */ + notify_url?: string // ===== V3 新增字段 ===== diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/components/WeixinMerchantForm.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/components/WeixinMerchantForm.vue index 15d0d529..377f9cfa 100644 --- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/components/WeixinMerchantForm.vue +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/components/WeixinMerchantForm.vue @@ -100,6 +100,16 @@
V3版本使用更安全的RSA-SHA256签名和AES-GCM加密
+ + + +
支付成功后微信回调通知的地址,留空使用默认值
+
+
diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/weixinpay.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/weixinpay.vue index 70342bf3..131efb30 100644 --- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/weixinpay.vue +++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/weixinpay.vue @@ -89,6 +89,7 @@ const createDefaultMerchant = (): WeixinPayMerchant => ({ api_key: '', cert_path: '', is_enabled: '1', + notify_url: '', // V3 字段默认值 pay_version: PayVersion.V2, api_v3_key: '', @@ -111,6 +112,7 @@ const loadData = async () => { api_key: m.api_key || '', cert_path: m.cert_path || '', is_enabled: m.is_enabled || '1', + notify_url: m.notify_url || '', // V3 字段回显 pay_version: m.pay_version || PayVersion.V2, api_v3_key: m.api_v3_key || '', diff --git a/server/HoneyBox/src/HoneyBox.Api/Controllers/NotifyController.cs b/server/HoneyBox/src/HoneyBox.Api/Controllers/NotifyController.cs index 61753857..f6240b4c 100644 --- a/server/HoneyBox/src/HoneyBox.Api/Controllers/NotifyController.cs +++ b/server/HoneyBox/src/HoneyBox.Api/Controllers/NotifyController.cs @@ -1,4 +1,5 @@ using HoneyBox.Core.Interfaces; +using HoneyBox.Model.Models.Payment; using Microsoft.AspNetCore.Mvc; namespace HoneyBox.Api.Controllers; @@ -23,60 +24,63 @@ public class NotifyController : ControllerBase } /// - /// 微信支付回调接口 + /// 微信支付回调接口(支持 V2 XML 和 V3 JSON 格式) /// POST /api/notify/order_notify /// 接收微信支付结果通知,处理订单状态更新 - /// Requirements: 2.1-2.9 /// - /// - /// 微信支付回调流程: - /// 1. 接收微信发送的XML格式回调数据 - /// 2. 验证签名确保数据安全 - /// 3. 根据订单类型(attach字段)路由到对应处理方法 - /// 4. 更新订单状态、扣减用户资产、触发抽奖等 - /// 5. 返回XML格式响应给微信 - /// - /// 支持的订单类型(attach值): - /// - user_recharge: 余额充值 - /// - order_yfs: 一番赏订单 - /// - order_lts: 擂台赏订单 - /// - order_zzs: 转转赏订单 - /// - order_flw: 福利屋订单 - /// - order_scs: 商城赏订单 - /// - order_wxs: 无限赏订单 - /// - order_fbs: 翻倍赏订单 - /// - order_ckj: 抽卡机订单 - /// - order_list_send: 发货运费 - /// - /// XML格式响应 [HttpPost("order_notify")] - [Consumes("application/xml", "text/xml")] - [Produces("application/xml")] public async Task OrderNotify() { try { - // 读取请求体中的XML数据 + // 读取请求体 using var reader = new StreamReader(Request.Body); - var xmlData = await reader.ReadToEndAsync(); + var notifyBody = await reader.ReadToEndAsync(); - _logger.LogInformation("收到微信支付回调请求,数据长度: {Length}", xmlData?.Length ?? 0); + _logger.LogInformation("收到微信支付回调请求,数据长度: {Length}, ContentType: {ContentType}", + notifyBody?.Length ?? 0, Request.ContentType); - // 调用服务处理回调 - var result = await _paymentNotifyService.HandleWechatNotifyAsync(xmlData ?? string.Empty); + // 提取 V3 回调请求头(如果存在) + WechatPayNotifyHeaders? headers = null; + if (Request.Headers.TryGetValue("Wechatpay-Timestamp", out var timestamp) && + Request.Headers.TryGetValue("Wechatpay-Nonce", out var nonce) && + Request.Headers.TryGetValue("Wechatpay-Signature", out var signature) && + Request.Headers.TryGetValue("Wechatpay-Serial", out var serial)) + { + headers = new WechatPayNotifyHeaders + { + Timestamp = timestamp.ToString(), + Nonce = nonce.ToString(), + Signature = signature.ToString(), + Serial = serial.ToString() + }; + _logger.LogDebug("检测到 V3 回调请求头: Timestamp={Timestamp}, Serial={Serial}", + headers.Timestamp, headers.Serial); + } + + // 调用服务处理回调(自动识别 V2/V3 格式) + var result = await _paymentNotifyService.HandleWechatNotifyAsync(notifyBody ?? string.Empty, headers); _logger.LogInformation("微信支付回调处理完成: Success={Success}, Message={Message}", result.Success, result.Message); - // 返回XML响应给微信 - return Content(result.XmlResponse, "application/xml"); + // 根据回调版本返回对应格式的响应 + if (!string.IsNullOrEmpty(result.JsonResponse)) + { + // V3 返回 JSON + return Content(result.JsonResponse, "application/json"); + } + else + { + // V2 返回 XML + return Content(result.XmlResponse ?? "", "application/xml"); + } } catch (Exception ex) { _logger.LogError(ex, "处理微信支付回调异常"); - // 即使发生异常,也返回成功响应,避免微信重复通知 - // 后续通过其他机制(如定时任务)处理失败的订单 + // 返回成功响应,避免微信重复通知 var successResponse = ""; return Content(successResponse, "application/xml"); } diff --git a/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj b/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj index 9d149890..157d3a9c 100644 --- a/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj +++ b/server/HoneyBox/src/HoneyBox.Api/HoneyBox.Api.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -9,7 +9,7 @@ Linux ..\.. - + @@ -22,10 +22,27 @@ + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + diff --git a/server/HoneyBox/src/HoneyBox.Api/cert/apiclient_cert.pem b/server/HoneyBox/src/HoneyBox.Api/cert/apiclient_cert.pem new file mode 100644 index 00000000..477e14fc --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Api/cert/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPTCCAyWgAwIBAgIUcxO5U5v6gITLJb2T8GKiVbfA+AswDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjYwMTI1MDg1NjAxWhcNMzEwMTI0MDg1NjAxWjCBljETMBEGA1UEAwwK +MTczODcyNTgwMTEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMUIwQAYDVQQL +DDnmoZPlj7Dljr/lk4jlsLznlLXlrZDllYbliqHlt6XkvZzlrqTvvIjkuKrkvZPl +t6XllYbmiLfvvIkxCzAJBgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANm51GTtTzcs/3m4WuCTppqv+QGk +fYXaMzQakc1ZpG1gdmOjEYTZmzUNz6vnbrCbp+T5mow45o/c6/x2ChhZvQXj/ud+ +RGPKpySuT1hdQoq+l6OfNbS/u35iDGgjD1A1gbRNCG+cNaGpedruvvHMMdrBVCL2 +nvtprj5s5Vc+72nYtjLVCrELOzHNN8DaoJ3PkCSKGNLG2OwDXWe0wP+0KJ4GFPpN +0OKEAY2vvEzOo1ENkBOn16mGBLwXnkn13J8hdih7KPcgmBeMHceDjCGfVo6Z+fES +C7SIL8obtt9HMXRqkVuWPcl+y3UmAsujWIjIHxEQDUlyj2TB5s2CefVb330CAwEA +AaOBuTCBtjAJBgNVHRMEAjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGN +oIGKoIGHhoGEaHR0cDovL2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2Ny +bD9DQT0xQkQ0MjIwRTUwREJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNn +PUhBQ0M0NzFCNjU0MjJFMTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0G +CSqGSIb3DQEBCwUAA4IBAQCDOzEAS8OtTib+gRbYRDMw3mZ/dRR7RYuE8d1Rxf2y +Xgv+C7NoHAFHxhoKmWGw9ImOMXM4YViHAWlkEZHqndF5ETNne2wl6X8wYpQIr1a1 +U0BlyxKOvgRquikPZp6mE2tOIxj6P2tngu2o9wljt7kuzDHsjdr1to4Omom1i514 +EgU2GoI37YgGIEeSy5c3h0j1vSQKy+fuKZKFWxPX1oOMTwVJFqtS/nrPBPftNMsf +fIXBXjbKaLrjyBJiV/fD84nPENgOgkdnGp6/WaVy3kNydosTNINL4Es+0pUTTm9z +EXRNzOxfvpYxGFGJyVEZmGwAOw/IVePN+J38FbSVyHyC +-----END CERTIFICATE----- diff --git a/server/HoneyBox/src/HoneyBox.Api/cert/apiclient_key.pem b/server/HoneyBox/src/HoneyBox.Api/cert/apiclient_key.pem new file mode 100644 index 00000000..43d3e85b --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Api/cert/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZudRk7U83LP95 +uFrgk6aar/kBpH2F2jM0GpHNWaRtYHZjoxGE2Zs1Dc+r526wm6fk+ZqMOOaP3Ov8 +dgoYWb0F4/7nfkRjyqckrk9YXUKKvpejnzW0v7t+YgxoIw9QNYG0TQhvnDWhqXna +7r7xzDHawVQi9p77aa4+bOVXPu9p2LYy1QqxCzsxzTfA2qCdz5AkihjSxtjsA11n +tMD/tCieBhT6TdDihAGNr7xMzqNRDZATp9ephgS8F55J9dyfIXYoeyj3IJgXjB3H +g4whn1aOmfnxEgu0iC/KG7bfRzF0apFblj3Jfst1JgLLo1iIyB8REA1Jco9kwebN +gnn1W999AgMBAAECggEATpKsnruxgcUAcYnhafh/AIYPA9O75OlI3z3TblsyZrKQ +Jwb7VIk/ZNcWIgCERsH1xkF5z67dLf/ZPiPPItiHya9tF1fPEIBa73bkdYw6bl23 +1bmoJRGodUSnG5HDffvBUjMWn0itZikGK8dLK3G4cCyi03dTCoIp+qdL4L96oSSE +uHChH9jSq2+LiR4P32GrSWO6z8dwS+2vonaepQoHbfEFbuSjNrdv78kt1DhJeY6u +ZkRqDR/T10+BLZX2gxuQ0ddH/YgeO2E6K99a7YWCGGVH7C0U4T2ZR5HJjAgxGsQ6 +8KQvzXwhHYNyxkpBaRCO6dogofVe1PXxW/Xi3rlJFQKBgQDvpCpDB8P5RUvq2Us/ +7GnmeuwWuO8T5byNpTSBHNwVH/vCQqvorFPYJXicvk/1yjriXwiPnfw0j11uaZsd +1ZJxQeXiS0ASsrihu5m6AxisFOU0cJNpl6njW5Y2JQAZdgg4APDzXfSLp2Ev5h6K +vuRMfmmse0gWeWDFUZEamInyVwKBgQDolq8XtfyDAZPEPldJmbVMiuBu7nJLDhHz +mL0tU5dPiKSduqFEbcuOSPREZb2wIw5MR1gsuCPk5rwx69DNY2Oztz+VcK3Vo1oN +4MufsPXKOTOSbdV8JjcVLxHxn+b8QIbDribncsg15I7n8P3wcZ640WWGom/H9spZ +HC3//DEgSwKBgHkRwWA4DiRjhCVUPpY/BImy1I/uQqsUyBvvuQT55Z6ul+ze7icQ +2RM8ayEVbSRKVVGEnbihIogTXiqoI/wAqImbt16KkgZgULM1Kkc1xUM7E0lZDsCs +JOJ+pPcZ3mD+psxUfWcWsrPTjmA6rHeAVarnus+vQQ5JqEBIIz0Cj77lAoGBAJPj +lAuIjLmkHBfw58GFubCksVX3ybaNiL6SRN94QkKxCLK+A2KmSYL8QkznQDip4aKA +zsEIiNI4IDvBzK973d5cy1IzJmUsC8u9PtwYQgDGZFNcAR2Ckw2mM0umt9F3Gfl8 +V4JdCo6x+GfkZSMoq5qqklqMGHVWJ42HjHwzF+2HAoGAE3FjmHDw5eGqu2aFNDUD +ulh+ikSkjH+1hLKR6amDqssCackET1gYIRnUXAQO7FKg/W96enffg7jZF+7E3OOT +TQo/obfpQPVaGKJ0CmqlSNyarZD6BFpBJKEiT8mlgkZ6XoyXZaAR2FjieoGd8xxi +1mIIXb9fbcQO1lIhXnHlZUQ= +-----END PRIVATE KEY----- diff --git a/server/HoneyBox/src/HoneyBox.Api/cert/pub_key.pem b/server/HoneyBox/src/HoneyBox.Api/cert/pub_key.pem new file mode 100644 index 00000000..d32530b0 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Api/cert/pub_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KeKMd6Yxovf4kPI0c1Q +Islyq9fi/Wg60dodzPNkRRoraqmqbbW7uQcKHkHvIZi5Z9fK8SGkezyhcjiR3o8z +uwnH5QiFuMw6P+1XB1koFfbxxCc6Eh0iuRI5BqNfyRwXwn9wIEUNwfF/SAPJGTkk +hCzViil3tOmnJDMxQUJitt4RsnL6BvQ3afWcm7oqt7MLlcIhIW8jAsSFeWPuZcW5 +Hj+o2udrTUaTRkw7AEsHr9xyePhsqYjGxbi9fTlghkUYnRUNikSydtQoHbGHP70Q +tz4HbPqH4gpsCqabPVuANFGH5a8uidOH3XKq2iPLggbPci1nFI8xMmHMaT88u/o5 +GQIDAQAB +-----END PUBLIC KEY----- diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/OrderService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/OrderService.cs index 1f497c49..1344fb04 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/OrderService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/OrderService.cs @@ -5,6 +5,7 @@ using HoneyBox.Model.Models; using HoneyBox.Model.Models.Goods; using HoneyBox.Model.Models.Lottery; using HoneyBox.Model.Models.Order; +using HoneyBox.Model.Models.Payment; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -18,6 +19,7 @@ public class OrderService : IOrderService private readonly HoneyBoxDbContext _dbContext; private readonly ILogger _logger; private readonly ILotteryEngine _lotteryEngine; + private readonly IWechatPayService _wechatPayService; // 抽奖赏品ID范围 [10, 33] private static readonly int[] ShangPrizeIdRange = { 10, 33 }; @@ -28,11 +30,16 @@ public class OrderService : IOrderService // 无限赏商品类型 private static readonly int[] InfiniteGoodsTypes = { 2, 8, 9, 10, 16, 17 }; - public OrderService(HoneyBoxDbContext dbContext, ILogger logger, ILotteryEngine lotteryEngine) + public OrderService( + HoneyBoxDbContext dbContext, + ILogger logger, + ILotteryEngine lotteryEngine, + IWechatPayService wechatPayService) { _dbContext = dbContext; _logger = logger; _lotteryEngine = lotteryEngine; + _wechatPayService = wechatPayService; } #region 订单金额计算 @@ -1034,21 +1041,37 @@ public class OrderService : IOrderService if (paymentResult.Price > 0) { - // 需要微信支付 - // 注意:实际的微信支付参数生成需要调用微信支付API - // 这里返回占位数据,实际实现需要集成微信支付SDK + // 需要微信支付 - 调用微信支付服务 + var payRequest = new WechatPayRequest + { + UserId = userId, + OrderNo = orderNum, + Amount = paymentResult.Price, + Body = goods.Title, + Attach = $"order_{goods.Type}" + }; + + var payResult = await _wechatPayService.CreatePaymentAsync(payRequest); + + if (payResult.Status != 1 || payResult.Data == null) + { + // 支付创建失败,回滚事务 + await transaction.RollbackAsync(); + throw new InvalidOperationException(payResult.Msg ?? "创建支付订单失败"); + } + response = new OrderBuyResponseDto { Status = 1, // 需要支付 OrderNum = orderNum, Res = new WechatPayParamsDto { - AppId = "", // 从配置获取 - TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), - NonceStr = Guid.NewGuid().ToString("N"), - Package = $"prepay_id=placeholder_{orderNum}", - SignType = "RSA", - PaySign = "" // 需要实际签名 + AppId = payResult.Data.AppId, + TimeStamp = payResult.Data.TimeStamp, + NonceStr = payResult.Data.NonceStr, + Package = payResult.Data.Package, + SignType = payResult.Data.SignType, + PaySign = payResult.Data.PaySign } }; } @@ -1453,21 +1476,37 @@ public class OrderService : IOrderService if (paymentResult.Price > 0) { - // 需要微信支付 - // 注意:实际的微信支付参数生成需要调用微信支付API - // 这里返回占位数据,实际实现需要集成微信支付SDK + // 需要微信支付 - 调用微信支付服务 + var payRequest = new WechatPayRequest + { + UserId = userId, + OrderNo = orderNum, + Amount = paymentResult.Price, + Body = goods.Title, + Attach = $"infinite_{goods.Type}" + }; + + var payResult = await _wechatPayService.CreatePaymentAsync(payRequest); + + if (payResult.Status != 1 || payResult.Data == null) + { + // 支付创建失败,回滚事务 + await transaction.RollbackAsync(); + throw new InvalidOperationException(payResult.Msg ?? "创建支付订单失败"); + } + response = new OrderBuyResponseDto { Status = 1, // 需要支付 OrderNum = orderNum, Res = new WechatPayParamsDto { - AppId = "", // 从配置获取 - TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), - NonceStr = Guid.NewGuid().ToString("N"), - Package = $"prepay_id=placeholder_{orderNum}", - SignType = "RSA", - PaySign = "" // 需要实际签名 + AppId = payResult.Data.AppId, + TimeStamp = payResult.Data.TimeStamp, + NonceStr = payResult.Data.NonceStr, + Package = payResult.Data.Package, + SignType = payResult.Data.SignType, + PaySign = payResult.Data.PaySign } }; } diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/WechatPayConfigService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/WechatPayConfigService.cs index 49e913dd..ee7854e5 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/WechatPayConfigService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/WechatPayConfigService.cs @@ -1,373 +1,327 @@ +using System.Text.Json; +using System.Text.Json.Serialization; using HoneyBox.Core.Interfaces; +using HoneyBox.Model.Data; using HoneyBox.Model.Models.Payment; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace HoneyBox.Core.Services; /// /// 微信支付配置服务实现 -/// 负责处理多商户配置、订单前缀匹配等逻辑 +/// 从数据库读取配置,支持多商户、多小程序 /// public class WechatPayConfigService : IWechatPayConfigService { - private readonly WechatPaySettings _settings; + private readonly HoneyBoxDbContext _dbContext; + private readonly IRedisService _redisService; private readonly ILogger _logger; private readonly Random _random = new(); - // 订单前缀常量 - private const string MH_PREFIX = "MH_"; - private const string FH_PREFIX = "FH_"; - private const int MERCHANT_PREFIX_LENGTH = 3; - private const int MINIPROGRAM_PREFIX_LENGTH = 2; - private const int TOTAL_PREFIX_LENGTH = MERCHANT_PREFIX_LENGTH + MINIPROGRAM_PREFIX_LENGTH; + private const string MERCHANTS_CACHE_KEY = "wechatpay:merchants"; + private const string MINIPROGRAMS_CACHE_KEY = "wechatpay:miniprograms"; + private static readonly TimeSpan CACHE_DURATION = TimeSpan.FromMinutes(5); public WechatPayConfigService( - IOptions settings, + HoneyBoxDbContext dbContext, + IRedisService redisService, ILogger logger) { - _settings = settings.Value; + _dbContext = dbContext; + _redisService = redisService; _logger = logger; } - /// - public WechatPayMerchantConfig GetDefaultConfig() + private async Task> LoadMerchantsAsync() { - return _settings.DefaultMerchant; - } - - /// - public WechatPayMerchantConfig GetMerchantByOrderNo(string orderNo) - { - if (string.IsNullOrEmpty(orderNo)) + var cachedJson = await _redisService.GetStringAsync(MERCHANTS_CACHE_KEY); + if (!string.IsNullOrEmpty(cachedJson)) { - return _settings.DefaultMerchant; - } - - // 检查是否是MH_或FH_开头的订单号 - if (!orderNo.StartsWith(MH_PREFIX) && !orderNo.StartsWith(FH_PREFIX)) - { - // 尝试直接匹配订单前缀 - foreach (var m in _settings.Merchants) + try { - if (!string.IsNullOrEmpty(m.OrderPrefix) && - orderNo.StartsWith(m.OrderPrefix)) - { - return m; - } + var cached = JsonSerializer.Deserialize>(cachedJson); + if (cached != null && cached.Count > 0) return cached; } - return _settings.DefaultMerchant; + catch { } } - // 提取订单前缀信息 - var prefixInfo = ExtractOrderPrefix(orderNo); - if (prefixInfo == null) + var merchants = new List(); + try { - return _settings.DefaultMerchant; - } + var settingConfig = await _dbContext.Configs + .Where(c => c.ConfigKey == "weixinpay_setting") + .Select(c => c.ConfigValue) + .FirstOrDefaultAsync(); - WechatPayMerchantConfig? merchant = null; - string appId = string.Empty; - - // 优先根据小程序前缀获取配置 - if (!string.IsNullOrEmpty(prefixInfo.MiniprogramPrefix)) - { - var miniprogramConfig = GetMiniprogramByPrefix(prefixInfo.MiniprogramPrefix); - if (miniprogramConfig != null) + if (!string.IsNullOrEmpty(settingConfig)) { - appId = miniprogramConfig.AppId; - - // 如果有商户前缀,使用指定商户 - if (!string.IsNullOrEmpty(prefixInfo.MerchantPrefix)) + var setting = JsonSerializer.Deserialize(settingConfig, JsonOptions); + if (setting?.Merchants != null) { - merchant = GetMerchantByPrefix(prefixInfo.MerchantPrefix); - } - - // 如果没有找到商户,从小程序关联的商户中选择 - if (merchant == null && miniprogramConfig.Merchants.Count > 0) - { - var associatedMerchants = _settings.Merchants - .Where(m => miniprogramConfig.Merchants.Contains(m.MchId)) - .ToList(); - - if (associatedMerchants.Count > 0) + foreach (var m in setting.Merchants.Where(x => x.IsEnabled == "1")) { - merchant = GetRandomMerchant(associatedMerchants); + merchants.Add(new WechatPayMerchantConfig + { + Name = m.Name ?? "", + MchId = m.MchId ?? "", + Key = m.ApiKey ?? "", + OrderPrefix = m.OrderPrefix ?? "", + PayVersion = m.PayVersion ?? "V2", + ApiV3Key = m.ApiV3Key, + CertSerialNo = m.CertSerialNo, + PrivateKeyPath = m.PrivateKeyPath, + WechatPublicKeyId = m.WechatPublicKeyId, + WechatPublicKeyPath = m.WechatPublicKeyPath, + NotifyUrl = !string.IsNullOrEmpty(m.NotifyUrl) ? m.NotifyUrl : "https://api.zfunbox.cn/api/notify" + }); } } } - } - // 如果没有通过小程序前缀获取到配置,则回退到商户前缀 - if (merchant == null && !string.IsNullOrEmpty(prefixInfo.MerchantPrefix)) - { - merchant = GetMerchantByPrefix(prefixInfo.MerchantPrefix); - } + var weixinpayConfig = await _dbContext.Configs + .Where(c => c.ConfigKey == "weixinpay") + .Select(c => c.ConfigValue) + .FirstOrDefaultAsync(); - // 如果还是没有找到,返回默认配置 - if (merchant == null) - { - return _settings.DefaultMerchant; - } - - // 如果有小程序AppId,覆盖商户的AppId - if (!string.IsNullOrEmpty(appId)) - { - // 创建一个新的配置对象,避免修改原始配置 - return new WechatPayMerchantConfig + if (!string.IsNullOrEmpty(weixinpayConfig)) { - Name = merchant.Name, - MchId = merchant.MchId, - AppId = appId, - Key = merchant.Key, - OrderPrefix = merchant.OrderPrefix, - Weight = merchant.Weight, - NotifyUrl = merchant.NotifyUrl, - // V3 字段映射 - PayVersion = merchant.PayVersion, - ApiV3Key = merchant.ApiV3Key, - CertSerialNo = merchant.CertSerialNo, - PrivateKeyPath = merchant.PrivateKeyPath, - WechatPublicKeyId = merchant.WechatPublicKeyId, - WechatPublicKeyPath = merchant.WechatPublicKeyPath - }; - } - - return merchant; - } - - - /// - public WechatPayMerchantConfig? GetMerchantByPrefix(string merchantPrefix) - { - if (string.IsNullOrEmpty(merchantPrefix)) - { - return null; - } - - return _settings.Merchants.FirstOrDefault(m => - !string.IsNullOrEmpty(m.OrderPrefix) && - m.OrderPrefix.Equals(merchantPrefix, StringComparison.OrdinalIgnoreCase)); - } - - /// - public MiniprogramConfig? GetMiniprogramByPrefix(string miniprogramPrefix) - { - if (string.IsNullOrEmpty(miniprogramPrefix)) - { - return null; - } - - return _settings.Miniprograms.FirstOrDefault(m => - !string.IsNullOrEmpty(m.OrderPrefix) && - m.OrderPrefix.Equals(miniprogramPrefix, StringComparison.OrdinalIgnoreCase)); - } - - /// - public MiniprogramConfig? GetMiniprogramByDomain(string domain) - { - if (string.IsNullOrEmpty(domain)) - { - return GetDefaultMiniprogram(); - } - - foreach (var miniprogram in _settings.Miniprograms) - { - if (string.IsNullOrEmpty(miniprogram.Domain)) - { - continue; - } - - // 分割多个域名 - var domains = miniprogram.Domain.Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var d in domains) - { - var trimmedDomain = d.Trim(); - if (DomainMatch(trimmedDomain, domain)) + var config = JsonSerializer.Deserialize(weixinpayConfig, JsonOptions); + if (config != null && !string.IsNullOrEmpty(config.MchId) && !merchants.Any(m => m.MchId == config.MchId)) { - return miniprogram; + merchants.Add(new WechatPayMerchantConfig + { + Name = "默认商户", + MchId = config.MchId, + AppId = config.AppId ?? "", + Key = config.Keys ?? "", + OrderPrefix = "MYH", + PayVersion = "V2", + NotifyUrl = "https://api.zfunbox.cn/api/notify" + }); } } - } - return GetDefaultMiniprogram(); - } - - /// - public MiniprogramConfig? GetDefaultMiniprogram() - { - // 查找默认小程序配置 - var defaultMiniprogram = _settings.Miniprograms.FirstOrDefault(m => m.IsDefault); - - // 如果没有设置默认配置,返回第一个配置 - if (defaultMiniprogram == null && _settings.Miniprograms.Count > 0) - { - return _settings.Miniprograms[0]; - } - - return defaultMiniprogram; - } - - /// - public OrderPrefixInfo? ExtractOrderPrefix(string orderNo) - { - if (string.IsNullOrEmpty(orderNo)) - { - return null; - } - - // 检查是否是MH_或FH_开头 - if (!orderNo.StartsWith(MH_PREFIX) && !orderNo.StartsWith(FH_PREFIX)) - { - return null; - } - - // 提取MH_或FH_后的字符 - // 订单格式: MH_ABC12... 或 FH_ABC12... - // 其中ABC是商户前缀(3位),12是小程序前缀(2位,可选) - var prefixStart = 3; // "MH_" 或 "FH_" 的长度 - - if (orderNo.Length < prefixStart + MERCHANT_PREFIX_LENGTH) - { - return null; - } - - var result = new OrderPrefixInfo(); - - // 提取商户前缀(前3位) - result.MerchantPrefix = orderNo.Substring(prefixStart, MERCHANT_PREFIX_LENGTH); - - // 如果有足够长度,提取小程序前缀(后2位) - if (orderNo.Length >= prefixStart + TOTAL_PREFIX_LENGTH) - { - result.MiniprogramPrefix = orderNo.Substring( - prefixStart + MERCHANT_PREFIX_LENGTH, - MINIPROGRAM_PREFIX_LENGTH); - } - - return result; - } - - /// - public WechatPayMerchantConfig? GetRandomMerchant(IEnumerable merchants) - { - var merchantList = merchants.ToList(); - - if (merchantList.Count == 0) - { - return null; - } - - // 只有一个商户,直接返回 - if (merchantList.Count == 1) - { - return merchantList[0]; - } - - // 计算总权重 - var totalWeight = merchantList.Sum(m => m.Weight > 0 ? m.Weight : 1); - - // 生成随机数 - var randomWeight = _random.Next(1, totalWeight + 1); - - // 根据权重选择商户 - var currentWeight = 0; - foreach (var merchant in merchantList) - { - var weight = merchant.Weight > 0 ? merchant.Weight : 1; - currentWeight += weight; - - if (randomWeight <= currentWeight) + _logger.LogInformation("从数据库加载了 {Count} 个商户配置", merchants.Count); + if (merchants.Count > 0) { - return merchant; + await _redisService.SetStringAsync(MERCHANTS_CACHE_KEY, JsonSerializer.Serialize(merchants), CACHE_DURATION); } } + catch (Exception ex) + { + _logger.LogError(ex, "加载商户配置失败"); + } - // 默认返回第一个商户 - return merchantList[0]; + return merchants; } - /// - public (WechatPayMerchantConfig Merchant, string AppId) GetWxPayConfig() + private async Task> LoadMiniprogramsAsync() { - // 获取当前域名对应的小程序配置 - var miniprogram = GetDefaultMiniprogram(); - WechatPayMerchantConfig? merchant = null; + var cachedJson = await _redisService.GetStringAsync(MINIPROGRAMS_CACHE_KEY); + if (!string.IsNullOrEmpty(cachedJson)) + { + try + { + var cached = JsonSerializer.Deserialize>(cachedJson); + if (cached != null && cached.Count > 0) return cached; + } + catch { } + } + + var miniprograms = new List(); + try + { + var settingConfig = await _dbContext.Configs + .Where(c => c.ConfigKey == "miniprogram_setting") + .Select(c => c.ConfigValue) + .FirstOrDefaultAsync(); + + if (!string.IsNullOrEmpty(settingConfig)) + { + var setting = JsonSerializer.Deserialize(settingConfig, JsonOptions); + if (setting?.Miniprograms != null) + { + foreach (var m in setting.Miniprograms) + { + miniprograms.Add(new MiniprogramConfig + { + Name = m.Name ?? "", + AppId = m.AppId ?? "", + AppSecret = m.AppSecret ?? "", + OrderPrefix = m.OrderPrefix ?? "", + IsDefault = m.IsDefault == 1, + Merchants = m.Merchants ?? new List() + }); + } + } + } + + _logger.LogInformation("从数据库加载了 {Count} 个小程序配置", miniprograms.Count); + if (miniprograms.Count > 0) + { + await _redisService.SetStringAsync(MINIPROGRAMS_CACHE_KEY, JsonSerializer.Serialize(miniprograms), CACHE_DURATION); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "加载小程序配置失败"); + } + + return miniprograms; + } + + public WechatPayMerchantConfig GetDefaultConfig() + { + var merchants = LoadMerchantsAsync().GetAwaiter().GetResult(); + return merchants.FirstOrDefault() ?? new WechatPayMerchantConfig(); + } + + public WechatPayMerchantConfig GetMerchantByOrderNo(string orderNo) + { + var merchants = LoadMerchantsAsync().GetAwaiter().GetResult(); + var miniprograms = LoadMiniprogramsAsync().GetAwaiter().GetResult(); + + if (string.IsNullOrEmpty(orderNo) || merchants.Count == 0) + return GetDefaultConfig(); + + var miniprogram = miniprograms.FirstOrDefault(m => m.IsDefault) ?? miniprograms.FirstOrDefault(); + WechatPayMerchantConfig? selectedMerchant = null; + string appId = miniprogram?.AppId ?? ""; - // 如果小程序配置了关联商户,从关联商户中随机选择一个 if (miniprogram != null && miniprogram.Merchants.Count > 0) { - var associatedMerchants = _settings.Merchants - .Where(m => miniprogram.Merchants.Contains(m.MchId)) - .ToList(); - + var associatedMerchants = merchants.Where(m => miniprogram.Merchants.Contains(m.MchId)).ToList(); if (associatedMerchants.Count > 0) { - merchant = GetRandomMerchant(associatedMerchants); + selectedMerchant = GetRandomMerchant(associatedMerchants); + _logger.LogDebug("从小程序关联商户中选择: MchId={MchId}", selectedMerchant?.MchId); } } - // 如果没有关联商户,则从所有商户中随机选择 - if (merchant == null && _settings.Merchants.Count > 0) + selectedMerchant ??= merchants.FirstOrDefault(); + if (selectedMerchant == null) return new WechatPayMerchantConfig(); + + return new WechatPayMerchantConfig { - merchant = GetRandomMerchant(_settings.Merchants); - } - - // 如果还是没有商户,使用默认商户 - merchant ??= _settings.DefaultMerchant; - - // 获取AppId - 优先使用小程序配置中的AppId - var appId = miniprogram?.AppId ?? merchant.AppId; - - return (merchant, appId); + Name = selectedMerchant.Name, + MchId = selectedMerchant.MchId, + AppId = appId, + Key = selectedMerchant.Key, + OrderPrefix = selectedMerchant.OrderPrefix, + Weight = selectedMerchant.Weight, + NotifyUrl = selectedMerchant.NotifyUrl, + PayVersion = selectedMerchant.PayVersion, + ApiV3Key = selectedMerchant.ApiV3Key, + CertSerialNo = selectedMerchant.CertSerialNo, + PrivateKeyPath = selectedMerchant.PrivateKeyPath, + WechatPublicKeyId = selectedMerchant.WechatPublicKeyId, + WechatPublicKeyPath = selectedMerchant.WechatPublicKeyPath + }; + } + + public WechatPayMerchantConfig? GetMerchantByPrefix(string merchantPrefix) + { + if (string.IsNullOrEmpty(merchantPrefix)) return null; + var merchants = LoadMerchantsAsync().GetAwaiter().GetResult(); + return merchants.FirstOrDefault(m => !string.IsNullOrEmpty(m.OrderPrefix) && m.OrderPrefix.Equals(merchantPrefix, StringComparison.OrdinalIgnoreCase)); + } + + public MiniprogramConfig? GetMiniprogramByPrefix(string miniprogramPrefix) + { + if (string.IsNullOrEmpty(miniprogramPrefix)) return null; + var miniprograms = LoadMiniprogramsAsync().GetAwaiter().GetResult(); + return miniprograms.FirstOrDefault(m => !string.IsNullOrEmpty(m.OrderPrefix) && m.OrderPrefix.Equals(miniprogramPrefix, StringComparison.OrdinalIgnoreCase)); + } + + public MiniprogramConfig? GetMiniprogramByDomain(string domain) => GetDefaultMiniprogram(); + + public MiniprogramConfig? GetDefaultMiniprogram() + { + var miniprograms = LoadMiniprogramsAsync().GetAwaiter().GetResult(); + return miniprograms.FirstOrDefault(m => m.IsDefault) ?? miniprograms.FirstOrDefault(); + } + + public OrderPrefixInfo? ExtractOrderPrefix(string orderNo) + { + if (string.IsNullOrEmpty(orderNo) || (!orderNo.StartsWith("MH_") && !orderNo.StartsWith("FH_"))) + return null; + if (orderNo.Length < 6) return null; + return new OrderPrefixInfo + { + MerchantPrefix = orderNo.Substring(3, 3), + MiniprogramPrefix = orderNo.Length >= 8 ? orderNo.Substring(6, 2) : null + }; + } + + public WechatPayMerchantConfig? GetRandomMerchant(IEnumerable merchants) + { + var list = merchants.ToList(); + if (list.Count == 0) return null; + if (list.Count == 1) return list[0]; + + var totalWeight = list.Sum(m => m.Weight > 0 ? m.Weight : 1); + var randomWeight = _random.Next(1, totalWeight + 1); + var currentWeight = 0; + + foreach (var merchant in list) + { + currentWeight += merchant.Weight > 0 ? merchant.Weight : 1; + if (randomWeight <= currentWeight) return merchant; + } + return list[0]; + } + + public (WechatPayMerchantConfig Merchant, string AppId) GetWxPayConfig() + { + var merchant = GetMerchantByOrderNo(""); + return (merchant, merchant.AppId); } - /// public (WechatPayMerchantConfig? Merchant, string AppId) GetFixedWxPayConfig(string orderPrefix) { - // 获取当前域名对应的小程序配置 - var miniprogram = GetDefaultMiniprogram(); - - // 尝试查找与订单前缀匹配的商户 var merchant = GetMerchantByPrefix(orderPrefix); - - // 如果没有找到匹配的商户,则使用随机选择 if (merchant == null) { var config = GetWxPayConfig(); return (config.Merchant, config.AppId); } - - // 获取AppId - 优先使用小程序配置中的AppId - var appId = miniprogram?.AppId ?? merchant.AppId; - - return (merchant, appId); + var miniprogram = GetDefaultMiniprogram(); + return (merchant, miniprogram?.AppId ?? merchant.AppId); } - /// - /// 检查域名是否匹配 - /// - /// 配置中的域名模式 - /// 当前请求的域名 - /// 是否匹配 - private static bool DomainMatch(string pattern, string domain) + private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; + + private class DbWeixinPaySetting { [JsonPropertyName("merchants")] public List? Merchants { get; set; } } + private class DbMerchantConfig { - if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(domain)) - { - return false; - } - - // 简单的域名匹配,支持通配符 * (例如: *.example.com) - if (pattern.Contains('*')) - { - var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern) - .Replace("\\*", ".*") + "$"; - return System.Text.RegularExpressions.Regex.IsMatch( - domain, - regexPattern, - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - } - - return string.Equals(pattern, domain, StringComparison.OrdinalIgnoreCase); + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("mch_id")] public string? MchId { get; set; } + [JsonPropertyName("order_prefix")] public string? OrderPrefix { get; set; } + [JsonPropertyName("api_key")] public string? ApiKey { get; set; } + [JsonPropertyName("is_enabled")] public string? IsEnabled { get; set; } + [JsonPropertyName("pay_version")] public string? PayVersion { get; set; } + [JsonPropertyName("api_v3_key")] public string? ApiV3Key { get; set; } + [JsonPropertyName("cert_serial_no")] public string? CertSerialNo { get; set; } + [JsonPropertyName("private_key_path")] public string? PrivateKeyPath { get; set; } + [JsonPropertyName("wechat_public_key_id")] public string? WechatPublicKeyId { get; set; } + [JsonPropertyName("wechat_public_key_path")] public string? WechatPublicKeyPath { get; set; } + [JsonPropertyName("notify_url")] public string? NotifyUrl { get; set; } + } + private class DbWeixinPayConfig + { + [JsonPropertyName("appid")] public string? AppId { get; set; } + [JsonPropertyName("mch_id")] public string? MchId { get; set; } + [JsonPropertyName("keys")] public string? Keys { get; set; } + } + private class DbMiniprogramSetting { [JsonPropertyName("miniprograms")] public List? Miniprograms { get; set; } } + private class DbMiniprogramConfig + { + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("appid")] public string? AppId { get; set; } + [JsonPropertyName("appsecret")] public string? AppSecret { get; set; } + [JsonPropertyName("order_prefix")] public string? OrderPrefix { get; set; } + [JsonPropertyName("is_default")] public int IsDefault { get; set; } + [JsonPropertyName("merchants")] public List? Merchants { get; set; } } } diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs index b8bdfecf..f1718a0d 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs @@ -220,7 +220,8 @@ public class ServiceModule : Module var dbContext = c.Resolve(); var logger = c.Resolve>(); var lotteryEngine = c.Resolve(); - return new OrderService(dbContext, logger, lotteryEngine); + var wechatPayService = c.Resolve(); + return new OrderService(dbContext, logger, lotteryEngine, wechatPayService); }).As().InstancePerLifetimeScope(); // 注册仓库服务 @@ -235,8 +236,14 @@ public class ServiceModule : Module // ========== 支付系统服务注册 ========== - // 注册微信支付配置服务 - builder.RegisterType().As().InstancePerLifetimeScope(); + // 注册微信支付配置服务(从数据库读取配置) + builder.Register(c => + { + var dbContext = c.Resolve(); + var redisService = c.Resolve(); + var logger = c.Resolve>(); + return new WechatPayConfigService(dbContext, redisService, logger); + }).As().InstancePerLifetimeScope(); // 注册微信支付 V3 服务 builder.Register(c => @@ -258,8 +265,8 @@ public class ServiceModule : Module var wechatService = c.Resolve(); var redisService = c.Resolve(); var settings = c.Resolve>(); - // 使用 Lazy 延迟解析 V3 服务,避免循环依赖 - var v3ServiceLazy = new Lazy(() => c.Resolve()); + // Autofac 原生支持 Lazy,直接解析即可 + var v3ServiceLazy = c.Resolve>(); return new WechatPayService(dbContext, httpClientFactory.CreateClient(), logger, configService, wechatService, redisService, settings, v3ServiceLazy); }).As().InstancePerLifetimeScope();