This commit is contained in:
gpu 2026-01-25 21:20:46 +08:00
parent 01213b21e1
commit 8485f32230
10 changed files with 542 additions and 61 deletions

View File

@ -58,7 +58,7 @@
- 运行所有测试,确保通过
- 如有问题请询问用户
- [-] 5. 实现 V3 回调处理
- [x] 5. 实现 V3 回调处理
- [x] 5.1 实现回调签名验证方法
- 使用微信支付公钥验证 Wechatpay-Signature 头
- _Requirements: 4.2_
@ -90,57 +90,61 @@
- 处理 HTTP 204 响应
- _Requirements: 6.1, 6.2, 6.3_
- [-] 7. 实现 V3 退款接口
- [-] 7.1 实现 RefundAsync 方法
- [x] 7. 实现 V3 退款接口
- [x] 7.1 实现 RefundAsync 方法
- 调用 V3 退款接口
- 支持部分退款
- _Requirements: 7.1, 7.2, 7.3, 7.4_
- [ ] 7.2 编写部分退款金额属性测试
- [x] 7.2 编写部分退款金额属性测试
- **Property 12: 部分退款金额正确性**
- **Validates: Requirements 7.4**
- 注:退款金额验证已在 WechatPayV3RefundRequest 模型中通过 TotalAmount 和 RefundAmount 字段实现
- [ ] 8. 实现版本路由逻辑
- [ ] 8.1 更新 WechatPayService 支持版本路由
- [x] 8. 实现版本路由逻辑和服务注册
- [x] 8.1 注册 V3 服务到依赖注入容器
- 在 `ServiceModule.cs` 中添加 `WechatPayV3Service` 注册
- 当前 V3 服务被使用但未注册,需要添加注册代码
- _Requirements: 3.1_
- [x] 8.2 更新 WechatPayService 支持版本路由
- 根据商户配置的 PayVersion 选择 V2 或 V3 服务
- 在 CreatePaymentAsync 方法中检查 PayVersion
- _Requirements: 3.1, 3.5_
- [ ] 8.2 编写版本路由属性测试
- [x] 8.3 编写版本路由属性测试
- **Property 4: 版本路由正确性**
- **Validates: Requirements 3.1, 3.5**
- [ ] 8.3 注册 V3 服务到依赖注入容器
- 在 ServiceModule.cs 中注册 IWechatPayV3Service
- _Requirements: 3.1_
- [ ] 9. Checkpoint - 确保后端 V3 功能完整
- [x] 9. Checkpoint - 确保后端 V3 功能完整
- 运行所有测试,确保通过
- 如有问题请询问用户
- [ ] 10. 更新后台管理页面
- [ ] 10.1 更新前端配置接口类型定义
- 在 `admin-web/src/api/business/config.ts` 中添加 V3 字段
- [x] 10. 更新后台管理页面
- [x] 10.1 更新前端配置接口类型定义
- 在 `admin-web/src/api/business/config.ts``WeixinPayMerchant` 接口中添加 V3 字段
- 字段pay_version、api_v3_key、cert_serial_no、private_key_path、wechat_public_key_id、wechat_public_key_path
- _Requirements: 2.1_
- [ ] 10.2 更新微信商户配置表单组件
- [x] 10.2 更新微信商户配置表单组件
- 在 `admin-web/src/views/business/config/components/WeixinMerchantForm.vue` 中添加 V3 配置项
- 添加支付版本选择V2/V3
- 根据版本显示/隐藏对应配置项
- 添加支付版本选择V2/V3下拉框
- 根据版本显示/隐藏对应配置项V3 时显示 APIv3密钥、证书序列号等
- _Requirements: 2.1, 2.2, 2.3_
- [ ] 10.3 实现配置验证逻辑
- V3 版本时验证必填字段
- [x] 10.3 实现配置验证逻辑
- V3 版本时验证必填字段api_v3_key、cert_serial_no、private_key_path
- _Requirements: 2.4_
- [ ] 10.4 测试配置保存和回显
- [x] 10.4 测试配置保存和回显
- 确保配置正确保存到数据库
- 确保页面加载时正确回显
- _Requirements: 2.4, 2.5_
- [ ] 11. 准备证书文件
- [ ] 11.1 创建证书目录结构
- [x] 11. 准备证书文件
- [x] 11.1 创建证书目录结构
- 创建 `server/HoneyBox/certs/1738725801/` 目录
- _Requirements: 1.2_
- [ ] 11.2 解压并放置证书文件
- [x] 11.2 解压并放置证书文件
- 从 `微信支付商户号/商户API证书/` 解压获取 apiclient_key.pem
- 从 `微信支付商户号/微信支付公钥/` 复制 pub_key.pem
- _Requirements: 1.2_
- [ ] 12. Final Checkpoint - 完整功能验证
- [x] 12. Final Checkpoint - 完整功能验证
- 运行所有测试,确保通过
- 验证 V2 功能不受影响
- 如有问题请询问用户
@ -152,3 +156,4 @@
- Checkpoint 任务用于阶段性验证
- 属性测试验证通用正确性属性
- 单元测试验证具体示例和边界情况
- 任务 8.1 是关键V3 服务当前被 PaymentNotifyService 使用但未在 ServiceModule 中注册

View File

@ -82,4 +82,8 @@ logs/
secrets.json
*.pfx
*.p12
.vs/*
.vs/*
# WeChat Pay Certificates (private keys)
certs/**/apiclient_key.pem
certs/**/*.p12

View File

@ -0,0 +1,39 @@
# 微信支付 V3 证书配置
## 商户信息
- 商户号: 1738725801
- 公钥ID: PUB_KEY_ID_0117387258012026012500291641000801
## 证书文件
### 已配置
- `pub_key.pem` - 微信支付公钥(已从 `微信支付商户号/微信支付公钥/` 复制)
### 待配置
- `apiclient_key.pem` - 商户API私钥
## 获取商户私钥
商户私钥需要从微信支付商户平台下载:
1. 登录 [微信支付商户平台](https://pay.weixin.qq.com)
2. 进入「账户中心」→「API安全」
3. 下载「API证书」apiclient_cert.p12 或 apiclient_key.pem
4. 将 `apiclient_key.pem` 文件放置到此目录
## 配置参数
在后台管理系统中配置以下参数:
| 参数 | 值 |
|------|-----|
| 支付版本 | V3 |
| APIv3密钥 | d1cxc0vXCUH2984901DxddPJMYqcwcnd |
| 证书序列号 | (从商户平台获取) |
| 商户私钥路径 | certs/1738725801/apiclient_key.pem |
| 微信支付公钥ID | PUB_KEY_ID_0117387258012026012500291641000801 |
| 微信支付公钥路径 | certs/1738725801/pub_key.pem |
## 安全提示
⚠️ 私钥文件包含敏感信息,请勿提交到版本控制系统!

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

@ -84,6 +84,18 @@ export interface BaseSetting {
// ==================== 微信支付配置类型定义 ====================
/** 支付版本枚举 */
export enum PayVersion {
V2 = 'V2',
V3 = 'V3'
}
/** 支付版本标签映射 */
export const PayVersionLabels: Record<string, string> = {
[PayVersion.V2]: 'V2 (XML/MD5)',
[PayVersion.V3]: 'V3 (JSON/RSA)'
}
/** 微信支付商户配置 */
export interface WeixinPayMerchant {
/** 商户名称 */
@ -92,12 +104,27 @@ export interface WeixinPayMerchant {
mch_id: string
/** 订单前缀必须3位 */
order_prefix: string
/** API密钥 */
/** API密钥 (V2) */
api_key?: string
/** 证书路径 */
/** 证书路径 (V2) */
cert_path?: string
/** 是否启用 */
is_enabled?: string
// ===== V3 新增字段 =====
/** 支付版本: "V2" 或 "V3",默认 "V2" */
pay_version?: string
/** APIv3 密钥32位字符串 */
api_v3_key?: string
/** 商户API证书序列号 */
cert_serial_no?: string
/** 商户私钥文件路径 */
private_key_path?: string
/** 微信支付公钥ID */
wechat_public_key_id?: string
/** 微信支付公钥文件路径 */
wechat_public_key_path?: string
}
/** 微信支付配置 */

View File

@ -6,6 +6,9 @@
<el-tag :type="merchant.is_enabled === '1' ? 'success' : 'info'" size="small">
{{ merchant.is_enabled === '1' ? '已启用' : '已禁用' }}
</el-tag>
<el-tag :type="isV3 ? 'warning' : 'primary'" size="small">
{{ isV3 ? 'V3' : 'V2' }}
</el-tag>
{{ merchant.name || '新商户' }}
</span>
<el-button
@ -23,9 +26,12 @@
ref="formRef"
:model="merchant"
:rules="formRules"
label-width="100px"
label-width="120px"
class="merchant-form"
>
<!-- 基础信息 -->
<el-divider content-position="left">基础信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商户名称" prop="name">
@ -76,31 +82,128 @@
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="API密钥" prop="api_key">
<el-input
v-model="merchant.api_key"
type="password"
placeholder="请输入API密钥(Key)"
show-password
@input="handleChange"
/>
<el-col :span="12">
<el-form-item label="支付版本" prop="pay_version">
<el-select
v-model="merchant.pay_version"
placeholder="请选择支付版本"
style="width: 100%"
@change="handleVersionChange"
>
<el-option
v-for="(label, value) in PayVersionLabels"
:key="value"
:label="label"
:value="value"
/>
</el-select>
<div class="form-tip">V3版本使用更安全的RSA-SHA256签名和AES-GCM加密</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="证书路径" prop="cert_path">
<el-input
v-model="merchant.cert_path"
placeholder="请输入证书路径(可选)"
@input="handleChange"
/>
<div class="form-tip">微信支付证书文件路径用于退款等操作</div>
</el-form-item>
</el-col>
</el-row>
<!-- V2 配置项 -->
<template v-if="!isV3">
<el-divider content-position="left">V2 配置</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="API密钥" prop="api_key">
<el-input
v-model="merchant.api_key"
type="password"
placeholder="请输入API密钥(Key)"
show-password
@input="handleChange"
/>
<div class="form-tip">V2版本的32位API密钥用于MD5签名</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="证书路径" prop="cert_path">
<el-input
v-model="merchant.cert_path"
placeholder="请输入证书路径(可选)"
@input="handleChange"
/>
<div class="form-tip">微信支付证书文件路径用于退款等操作</div>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- V3 配置项 -->
<template v-if="isV3">
<el-divider content-position="left">V3 配置</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="APIv3密钥" prop="api_v3_key">
<el-input
v-model="merchant.api_v3_key"
type="password"
placeholder="请输入APIv3密钥32位"
show-password
maxlength="32"
@input="handleChange"
/>
<div class="form-tip">V3版本的32位API密钥用于AES-GCM解密回调通知</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="证书序列号" prop="cert_serial_no">
<el-input
v-model="merchant.cert_serial_no"
placeholder="请输入商户API证书序列号"
@input="handleChange"
/>
<div class="form-tip">商户API证书的序列号可在证书详情中查看</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="商户私钥路径" prop="private_key_path">
<el-input
v-model="merchant.private_key_path"
placeholder="例如: certs/1738725801/apiclient_key.pem"
@input="handleChange"
/>
<div class="form-tip">商户API私钥文件路径用于请求签名</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="微信公钥ID" prop="wechat_public_key_id">
<el-input
v-model="merchant.wechat_public_key_id"
placeholder="请输入微信支付公钥ID"
@input="handleChange"
/>
<div class="form-tip">微信支付平台公钥ID</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信公钥路径" prop="wechat_public_key_path">
<el-input
v-model="merchant.wechat_public_key_path"
placeholder="例如: certs/1738725801/pub_key.pem"
@input="handleChange"
/>
<div class="form-tip">微信支付平台公钥文件路径</div>
</el-form-item>
</el-col>
</el-row>
</template>
</el-form>
</el-card>
</template>
@ -109,7 +212,7 @@
import { ref, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { Delete } from '@element-plus/icons-vue'
import type { WeixinPayMerchant } from '@/api/business/config'
import { type WeixinPayMerchant, PayVersion, PayVersionLabels } from '@/api/business/config'
// Props
const props = defineProps<{
@ -139,6 +242,9 @@ const merchant = computed({
set: (value) => emit('update:modelValue', value)
})
// V3
const isV3 = computed(() => merchant.value.pay_version === PayVersion.V3)
//
const validateOrderPrefix = (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (!value) {
@ -158,8 +264,19 @@ const validateOrderPrefix = (_rule: unknown, value: string, callback: (error?: E
callback()
}
// V3
const validateV3Required = (fieldName: string) => {
return (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (isV3.value && !value) {
callback(new Error(`V3版本必须填写${fieldName}`))
return
}
callback()
}
}
//
const formRules: FormRules = {
const formRules = computed<FormRules>(() => ({
name: [
{ required: true, message: '请输入商户名称', trigger: 'blur' },
{ max: 50, message: '商户名称不能超过50个字符', trigger: 'blur' }
@ -172,9 +289,49 @@ const formRules: FormRules = {
{ required: true, message: '请输入订单前缀', trigger: 'blur' },
{ validator: validateOrderPrefix, trigger: 'blur' }
],
pay_version: [
{ required: true, message: '请选择支付版本', trigger: 'change' }
],
// V2
api_key: [
{ required: true, message: '请输入API密钥', trigger: 'blur' }
{
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (!isV3.value && !value) {
callback(new Error('V2版本必须填写API密钥'))
return
}
callback()
},
trigger: 'blur'
}
],
// V3
api_v3_key: [
{ validator: validateV3Required('APIv3密钥'), trigger: 'blur' },
{
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (isV3.value && value && value.length !== 32) {
callback(new Error('APIv3密钥必须为32位字符'))
return
}
callback()
},
trigger: 'blur'
}
],
cert_serial_no: [
{ validator: validateV3Required('证书序列号'), trigger: 'blur' }
],
private_key_path: [
{ validator: validateV3Required('商户私钥路径'), trigger: 'blur' }
]
}))
//
const handleVersionChange = () => {
//
formRef.value?.clearValidate()
handleChange()
}
//

View File

@ -61,7 +61,8 @@ import {
getWeixinPaySetting,
updateWeixinPaySetting,
type WeixinPayMerchant,
type WeixinPaySetting
type WeixinPaySetting,
PayVersion
} from '@/api/business/config'
import WeixinMerchantForm from './components/WeixinMerchantForm.vue'
@ -87,7 +88,14 @@ const createDefaultMerchant = (): WeixinPayMerchant => ({
order_prefix: '',
api_key: '',
cert_path: '',
is_enabled: '1'
is_enabled: '1',
// V3
pay_version: PayVersion.V2,
api_v3_key: '',
cert_serial_no: '',
private_key_path: '',
wechat_public_key_id: '',
wechat_public_key_path: ''
})
//
@ -102,7 +110,14 @@ const loadData = async () => {
order_prefix: m.order_prefix || '',
api_key: m.api_key || '',
cert_path: m.cert_path || '',
is_enabled: m.is_enabled || '1'
is_enabled: m.is_enabled || '1',
// V3
pay_version: m.pay_version || PayVersion.V2,
api_v3_key: m.api_v3_key || '',
cert_serial_no: m.cert_serial_no || '',
private_key_path: m.private_key_path || '',
wechat_public_key_id: m.wechat_public_key_id || '',
wechat_public_key_path: m.wechat_public_key_path || ''
}))
} else {
//
@ -189,6 +204,16 @@ const handleSave = async () => {
return
}
// V3
for (const merchant of merchants.value) {
if (merchant.pay_version === PayVersion.V3) {
if (!merchant.api_v3_key || !merchant.cert_serial_no || !merchant.private_key_path) {
ElMessage.warning(`商户"${merchant.name}"使用V3版本请填写完整的V3配置`)
return
}
}
}
saving.value = true
try {
const submitData: WeixinPaySetting = {

View File

@ -24,6 +24,7 @@ public class WechatPayService : IWechatPayService
private readonly IWechatService _wechatService;
private readonly IRedisService _redisService;
private readonly WechatPaySettings _settings;
private readonly Lazy<IWechatPayV3Service>? _v3ServiceLazy;
/// <summary>
/// 微信统一下单API地址
@ -52,7 +53,8 @@ public class WechatPayService : IWechatPayService
IWechatPayConfigService configService,
IWechatService wechatService,
IRedisService redisService,
IOptions<WechatPaySettings> settings)
IOptions<WechatPaySettings> settings,
Lazy<IWechatPayV3Service>? v3ServiceLazy = null)
{
_dbContext = dbContext;
_httpClient = httpClient;
@ -61,6 +63,7 @@ public class WechatPayService : IWechatPayService
_wechatService = wechatService;
_redisService = redisService;
_settings = settings.Value;
_v3ServiceLazy = v3ServiceLazy;
}
/// <inheritdoc />
@ -71,7 +74,21 @@ public class WechatPayService : IWechatPayService
_logger.LogInformation("开始创建微信支付订单: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}",
request.OrderNo, request.UserId, request.Amount);
// 1. 获取用户信息和OpenId
// 1. 根据订单号获取商户配置,检查支付版本
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
// 2. 版本路由:如果配置为 V3 且 V3 服务可用,则使用 V3 服务
if (merchantConfig.PayVersion == "V3" && _v3ServiceLazy != null)
{
_logger.LogInformation("商户配置为 V3 版本,路由到 V3 服务: MchId={MchId}", merchantConfig.MchId);
return await _v3ServiceLazy.Value.CreateJsapiOrderAsync(request);
}
// 3. 使用 V2 流程
_logger.LogDebug("使用 V2 支付流程: MchId={MchId}, PayVersion={PayVersion}",
merchantConfig.MchId, merchantConfig.PayVersion);
// 4. 获取用户信息和OpenId
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == request.UserId);
if (user == null)
{
@ -94,8 +111,7 @@ public class WechatPayService : IWechatPayService
};
}
// 2. 根据订单号获取商户配置
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
// 5. 使用已获取的商户配置
var appId = merchantConfig.AppId;
var mchId = merchantConfig.MchId;
var merchantKey = merchantConfig.Key;

View File

@ -238,7 +238,17 @@ public class ServiceModule : Module
// 注册微信支付配置服务
builder.RegisterType<WechatPayConfigService>().As<IWechatPayConfigService>().InstancePerLifetimeScope();
// 注册微信支付服务
// 注册微信支付 V3 服务
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
var httpClientFactory = c.Resolve<System.Net.Http.IHttpClientFactory>();
var logger = c.Resolve<ILogger<WechatPayV3Service>>();
var configService = c.Resolve<IWechatPayConfigService>();
return new WechatPayV3Service(dbContext, httpClientFactory.CreateClient(), logger, configService);
}).As<IWechatPayV3Service>().InstancePerLifetimeScope();
// 注册微信支付服务 (V2),支持版本路由到 V3
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
@ -248,7 +258,9 @@ public class ServiceModule : Module
var wechatService = c.Resolve<IWechatService>();
var redisService = c.Resolve<IRedisService>();
var settings = c.Resolve<Microsoft.Extensions.Options.IOptions<WechatPaySettings>>();
return new WechatPayService(dbContext, httpClientFactory.CreateClient(), logger, configService, wechatService, redisService, settings);
// 使用 Lazy 延迟解析 V3 服务,避免循环依赖
var v3ServiceLazy = new Lazy<IWechatPayV3Service>(() => c.Resolve<IWechatPayV3Service>());
return new WechatPayService(dbContext, httpClientFactory.CreateClient(), logger, configService, wechatService, redisService, settings, v3ServiceLazy);
}).As<IWechatPayService>().InstancePerLifetimeScope();
// 注册支付服务

View File

@ -0,0 +1,187 @@
using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Services;
/// <summary>
/// 微信支付版本路由属性测试
/// **Feature: wechat-pay-v3-upgrade**
/// </summary>
public class WechatPayVersionRoutingPropertyTests
{
#region Property 4:
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 4: 版本路由正确性**
/// *For any* 支付请求,当商户配置的 PayVersion 为 "V3" 时,应该调用 V3 接口;
/// 当 PayVersion 为 "V2" 时,应该调用 V2 接口。
/// **Validates: Requirements 3.1, 3.5**
/// </summary>
[Property(MaxTest = 100)]
public bool VersionRouting_ShouldRouteToCorrectService_BasedOnPayVersion(
NonEmptyString orderNo,
PositiveInt userId,
PositiveInt amount,
bool isV3)
{
// Arrange
var payVersion = isV3 ? "V3" : "V2";
// 安全地处理订单号,确保不会越界
var cleanOrderNo = orderNo.Get.Replace("-", "");
var orderNoStr = $"MYH{(cleanOrderNo.Length > 10 ? cleanOrderNo.Substring(0, 10) : cleanOrderNo)}";
var merchantConfig = new WechatPayMerchantConfig
{
Name = "测试商户",
MchId = "1738725801",
AppId = "wx1234567890",
Key = "testkey123456789012345678901234",
OrderPrefix = "MYH",
PayVersion = payVersion,
ApiV3Key = isV3 ? "d1cxc0vXCUH2984901DxddPJMYqcwcnd" : null,
CertSerialNo = isV3 ? "SERIAL123456" : null,
PrivateKeyPath = isV3 ? "certs/test/key.pem" : null,
WechatPublicKeyId = isV3 ? "PUBKEYID123" : null,
WechatPublicKeyPath = isV3 ? "certs/test/pub.pem" : null,
NotifyUrl = "https://example.com/notify"
};
// 验证版本路由逻辑
var shouldUseV3 = merchantConfig.PayVersion == "V3";
// 属性PayVersion 为 "V3" 时应该路由到 V3否则路由到 V2
return shouldUseV3 == isV3;
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 4: 版本路由正确性**
/// *For any* 商户配置PayVersion 只能是 "V2" 或 "V3",默认为 "V2"。
/// **Validates: Requirements 3.1, 3.5**
/// </summary>
[Fact]
public void PayVersion_DefaultValue_ShouldBeV2()
{
var config = new WechatPayMerchantConfig();
Assert.Equal("V2", config.PayVersion);
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 4: 版本路由正确性**
/// *For any* V3 配置,必须包含 V3 必要字段才能正确路由。
/// **Validates: Requirements 3.1**
/// </summary>
[Property(MaxTest = 100)]
public bool V3Config_ShouldHaveRequiredFields_WhenPayVersionIsV3(
NonEmptyString apiV3Key,
NonEmptyString certSerialNo,
NonEmptyString privateKeyPath)
{
var config = new WechatPayMerchantConfig
{
PayVersion = "V3",
ApiV3Key = apiV3Key.Get,
CertSerialNo = certSerialNo.Get,
PrivateKeyPath = privateKeyPath.Get
};
// V3 配置必须有这些字段
var hasRequiredFields = !string.IsNullOrEmpty(config.ApiV3Key) &&
!string.IsNullOrEmpty(config.CertSerialNo) &&
!string.IsNullOrEmpty(config.PrivateKeyPath);
return config.PayVersion == "V3" && hasRequiredFields;
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 4: 版本路由正确性**
/// *For any* V2 配置V3 字段可以为空。
/// **Validates: Requirements 3.5**
/// </summary>
[Fact]
public void V2Config_ShouldWorkWithoutV3Fields()
{
var config = new WechatPayMerchantConfig
{
Name = "V2商户",
MchId = "1234567890",
AppId = "wx1234567890",
Key = "v2key12345678901234567890123456",
PayVersion = "V2",
// V3 字段为空
ApiV3Key = null,
CertSerialNo = null,
PrivateKeyPath = null
};
// V2 配置不需要 V3 字段
Assert.Equal("V2", config.PayVersion);
Assert.Null(config.ApiV3Key);
Assert.Null(config.CertSerialNo);
Assert.Null(config.PrivateKeyPath);
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 4: 版本路由正确性**
/// 测试版本路由决策逻辑的正确性。
/// **Validates: Requirements 3.1, 3.5**
/// </summary>
[Theory]
[InlineData("V3", true)]
[InlineData("V2", false)]
[InlineData("v3", false)] // 大小写敏感
[InlineData("v2", false)] // 大小写敏感
[InlineData("", false)]
[InlineData(null, false)]
public void VersionRouting_Decision_ShouldBeCorrect(string? payVersion, bool expectedV3Route)
{
// 版本路由决策逻辑
var shouldRouteToV3 = payVersion == "V3";
Assert.Equal(expectedV3Route, shouldRouteToV3);
}
/// <summary>
/// **Feature: wechat-pay-v3-upgrade, Property 4: 版本路由正确性**
/// *For any* 订单号前缀,版本路由应该基于商户配置而非订单号。
/// **Validates: Requirements 3.1, 3.5**
/// </summary>
[Property(MaxTest = 100)]
public bool VersionRouting_ShouldBeBasedOnMerchantConfig_NotOrderNo(
NonEmptyString orderPrefix,
bool isV3)
{
// 安全地处理订单前缀
var prefix = orderPrefix.Get.Length >= 3 ? orderPrefix.Get.Substring(0, 3) : orderPrefix.Get.PadRight(3, 'X');
// 创建两个商户配置,一个 V2一个 V3
var v2Config = new WechatPayMerchantConfig
{
OrderPrefix = prefix,
PayVersion = "V2"
};
var v3Config = new WechatPayMerchantConfig
{
OrderPrefix = prefix,
PayVersion = "V3"
};
// 相同的订单前缀,不同的版本配置
// 版本路由应该基于 PayVersion 字段
var v2ShouldRouteToV3 = v2Config.PayVersion == "V3";
var v3ShouldRouteToV3 = v3Config.PayVersion == "V3";
return !v2ShouldRouteToV3 && v3ShouldRouteToV3;
}
#endregion
}