This commit is contained in:
zpc 2026-01-27 12:01:58 +08:00
parent 885f7f29e3
commit 53a61f6298
12 changed files with 459 additions and 362 deletions

View File

@ -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: ''

View File

@ -110,6 +110,8 @@ export interface WeixinPayMerchant {
cert_path?: string
/** 是否启用 */
is_enabled?: string
/** 支付回调地址 */
notify_url?: string
// ===== V3 新增字段 =====

View File

@ -100,6 +100,16 @@
<div class="form-tip">V3版本使用更安全的RSA-SHA256签名和AES-GCM加密</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="回调地址" prop="notify_url">
<el-input
v-model="merchant.notify_url"
placeholder="例如: https://api.example.com/api/notify/order_notify"
@input="handleChange"
/>
<div class="form-tip">支付成功后微信回调通知的地址留空使用默认值</div>
</el-form-item>
</el-col>
</el-row>
<!-- V2 配置项 -->

View File

@ -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 || '',

View File

@ -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
}
/// <summary>
/// 微信支付回调接口
/// 微信支付回调接口(支持 V2 XML 和 V3 JSON 格式)
/// POST /api/notify/order_notify
/// 接收微信支付结果通知,处理订单状态更新
/// Requirements: 2.1-2.9
/// </summary>
/// <remarks>
/// 微信支付回调流程:
/// 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: 发货运费
/// </remarks>
/// <returns>XML格式响应</returns>
[HttpPost("order_notify")]
[Consumes("application/xml", "text/xml")]
[Produces("application/xml")]
public async Task<IActionResult> 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 ?? "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>", "application/xml");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理微信支付回调异常");
// 即使发生异常,也返回成功响应,避免微信重复通知
// 后续通过其他机制(如定时任务)处理失败的订单
// 返回成功响应,避免微信重复通知
var successResponse = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
return Content(successResponse, "application/xml");
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@ -9,7 +9,7 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
@ -22,10 +22,27 @@
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HoneyBox.Model\HoneyBox.Model.csproj" />
<ProjectReference Include="..\HoneyBox.Core\HoneyBox.Core.csproj" />
<ProjectReference Include="..\HoneyBox.Infrastructure\HoneyBox.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="cert\apiclient_cert.p12">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="cert\apiclient_cert.pem">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="cert\apiclient_key.pem">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="cert\pub_key.pem">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

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

View File

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

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KeKMd6Yxovf4kPI0c1Q
Islyq9fi/Wg60dodzPNkRRoraqmqbbW7uQcKHkHvIZi5Z9fK8SGkezyhcjiR3o8z
uwnH5QiFuMw6P+1XB1koFfbxxCc6Eh0iuRI5BqNfyRwXwn9wIEUNwfF/SAPJGTkk
hCzViil3tOmnJDMxQUJitt4RsnL6BvQ3afWcm7oqt7MLlcIhIW8jAsSFeWPuZcW5
Hj+o2udrTUaTRkw7AEsHr9xyePhsqYjGxbi9fTlghkUYnRUNikSydtQoHbGHP70Q
tz4HbPqH4gpsCqabPVuANFGH5a8uidOH3XKq2iPLggbPci1nFI8xMmHMaT88u/o5
GQIDAQAB
-----END PUBLIC KEY-----

View File

@ -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<OrderService> _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<OrderService> logger, ILotteryEngine lotteryEngine)
public OrderService(
HoneyBoxDbContext dbContext,
ILogger<OrderService> 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
}
};
}

View File

@ -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;
/// <summary>
/// 微信支付配置服务实现
/// 负责处理多商户配置、订单前缀匹配等逻辑
/// 从数据库读取配置,支持多商户、多小程序
/// </summary>
public class WechatPayConfigService : IWechatPayConfigService
{
private readonly WechatPaySettings _settings;
private readonly HoneyBoxDbContext _dbContext;
private readonly IRedisService _redisService;
private readonly ILogger<WechatPayConfigService> _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<WechatPaySettings> settings,
HoneyBoxDbContext dbContext,
IRedisService redisService,
ILogger<WechatPayConfigService> logger)
{
_settings = settings.Value;
_dbContext = dbContext;
_redisService = redisService;
_logger = logger;
}
/// <inheritdoc />
public WechatPayMerchantConfig GetDefaultConfig()
private async Task<List<WechatPayMerchantConfig>> LoadMerchantsAsync()
{
return _settings.DefaultMerchant;
}
/// <inheritdoc />
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<List<WechatPayMerchantConfig>>(cachedJson);
if (cached != null && cached.Count > 0) return cached;
}
return _settings.DefaultMerchant;
catch { }
}
// 提取订单前缀信息
var prefixInfo = ExtractOrderPrefix(orderNo);
if (prefixInfo == null)
var merchants = new List<WechatPayMerchantConfig>();
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<DbWeixinPaySetting>(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;
}
/// <inheritdoc />
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));
}
/// <inheritdoc />
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));
}
/// <inheritdoc />
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<DbWeixinPayConfig>(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();
}
/// <inheritdoc />
public MiniprogramConfig? GetDefaultMiniprogram()
{
// 查找默认小程序配置
var defaultMiniprogram = _settings.Miniprograms.FirstOrDefault(m => m.IsDefault);
// 如果没有设置默认配置,返回第一个配置
if (defaultMiniprogram == null && _settings.Miniprograms.Count > 0)
{
return _settings.Miniprograms[0];
}
return defaultMiniprogram;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public WechatPayMerchantConfig? GetRandomMerchant(IEnumerable<WechatPayMerchantConfig> 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;
}
/// <inheritdoc />
public (WechatPayMerchantConfig Merchant, string AppId) GetWxPayConfig()
private async Task<List<MiniprogramConfig>> LoadMiniprogramsAsync()
{
// 获取当前域名对应的小程序配置
var miniprogram = GetDefaultMiniprogram();
WechatPayMerchantConfig? merchant = null;
var cachedJson = await _redisService.GetStringAsync(MINIPROGRAMS_CACHE_KEY);
if (!string.IsNullOrEmpty(cachedJson))
{
try
{
var cached = JsonSerializer.Deserialize<List<MiniprogramConfig>>(cachedJson);
if (cached != null && cached.Count > 0) return cached;
}
catch { }
}
var miniprograms = new List<MiniprogramConfig>();
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<DbMiniprogramSetting>(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<string>()
});
}
}
}
_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<WechatPayMerchantConfig> 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);
}
/// <inheritdoc />
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);
}
/// <summary>
/// 检查域名是否匹配
/// </summary>
/// <param name="pattern">配置中的域名模式</param>
/// <param name="domain">当前请求的域名</param>
/// <returns>是否匹配</returns>
private static bool DomainMatch(string pattern, string domain)
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
private class DbWeixinPaySetting { [JsonPropertyName("merchants")] public List<DbMerchantConfig>? 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<DbMiniprogramConfig>? 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<string>? Merchants { get; set; }
}
}

View File

@ -220,7 +220,8 @@ public class ServiceModule : Module
var dbContext = c.Resolve<HoneyBoxDbContext>();
var logger = c.Resolve<ILogger<OrderService>>();
var lotteryEngine = c.Resolve<ILotteryEngine>();
return new OrderService(dbContext, logger, lotteryEngine);
var wechatPayService = c.Resolve<IWechatPayService>();
return new OrderService(dbContext, logger, lotteryEngine, wechatPayService);
}).As<IOrderService>().InstancePerLifetimeScope();
// 注册仓库服务
@ -235,8 +236,14 @@ public class ServiceModule : Module
// ========== 支付系统服务注册 ==========
// 注册微信支付配置服务
builder.RegisterType<WechatPayConfigService>().As<IWechatPayConfigService>().InstancePerLifetimeScope();
// 注册微信支付配置服务(从数据库读取配置)
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
var redisService = c.Resolve<IRedisService>();
var logger = c.Resolve<ILogger<WechatPayConfigService>>();
return new WechatPayConfigService(dbContext, redisService, logger);
}).As<IWechatPayConfigService>().InstancePerLifetimeScope();
// 注册微信支付 V3 服务
builder.Register(c =>
@ -258,8 +265,8 @@ public class ServiceModule : Module
var wechatService = c.Resolve<IWechatService>();
var redisService = c.Resolve<IRedisService>();
var settings = c.Resolve<Microsoft.Extensions.Options.IOptions<WechatPaySettings>>();
// 使用 Lazy 延迟解析 V3 服务,避免循环依赖
var v3ServiceLazy = new Lazy<IWechatPayV3Service>(() => c.Resolve<IWechatPayV3Service>());
// Autofac 原生支持 Lazy<T>,直接解析即可
var v3ServiceLazy = c.Resolve<Lazy<IWechatPayV3Service>>();
return new WechatPayService(dbContext, httpClientFactory.CreateClient(), logger, configService, wechatService, redisService, settings, v3ServiceLazy);
}).As<IWechatPayService>().InstancePerLifetimeScope();