From 817d0d6e0401089591218a50600984d60d1d3a28 Mon Sep 17 00:00:00 2001 From: huang Date: Tue, 17 Mar 2026 14:22:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0openspec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/agent-recharge/spec.md | 0 .../specs/asset-recharge-adaptation/spec.md | 0 .../specs/fuiou-payment/spec.md | 0 .../specs/order-payment/spec.md | 0 .../specs/wechat-config-management/spec.md | 0 .../specs/wechat-payment/spec.md | 0 .../tasks.md | 20 +- openspec/specs/agent-recharge/spec.md | 598 +++++++++++ .../specs/asset-recharge-adaptation/spec.md | 116 ++ openspec/specs/fuiou-payment/spec.md | 181 ++++ openspec/specs/order-payment/spec.md | 187 ++++ .../specs/wechat-config-management/spec.md | 997 ++++++++++++++++++ openspec/specs/wechat-payment/spec.md | 182 ++++ 16 files changed, 2271 insertions(+), 10 deletions(-) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/.openspec.yaml (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/design.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/proposal.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/specs/agent-recharge/spec.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/specs/asset-recharge-adaptation/spec.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/specs/fuiou-payment/spec.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/specs/order-payment/spec.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/specs/wechat-config-management/spec.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/specs/wechat-payment/spec.md (100%) rename openspec/changes/{add-payment-config-management => archive/2026-03-17-add-payment-config-management}/tasks.md (94%) create mode 100644 openspec/specs/agent-recharge/spec.md create mode 100644 openspec/specs/asset-recharge-adaptation/spec.md create mode 100644 openspec/specs/fuiou-payment/spec.md create mode 100644 openspec/specs/wechat-config-management/spec.md diff --git a/openspec/changes/add-payment-config-management/.openspec.yaml b/openspec/changes/archive/2026-03-17-add-payment-config-management/.openspec.yaml similarity index 100% rename from openspec/changes/add-payment-config-management/.openspec.yaml rename to openspec/changes/archive/2026-03-17-add-payment-config-management/.openspec.yaml diff --git a/openspec/changes/add-payment-config-management/design.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/design.md similarity index 100% rename from openspec/changes/add-payment-config-management/design.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/design.md diff --git a/openspec/changes/add-payment-config-management/proposal.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/proposal.md similarity index 100% rename from openspec/changes/add-payment-config-management/proposal.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/proposal.md diff --git a/openspec/changes/add-payment-config-management/specs/agent-recharge/spec.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/specs/agent-recharge/spec.md similarity index 100% rename from openspec/changes/add-payment-config-management/specs/agent-recharge/spec.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/specs/agent-recharge/spec.md diff --git a/openspec/changes/add-payment-config-management/specs/asset-recharge-adaptation/spec.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/specs/asset-recharge-adaptation/spec.md similarity index 100% rename from openspec/changes/add-payment-config-management/specs/asset-recharge-adaptation/spec.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/specs/asset-recharge-adaptation/spec.md diff --git a/openspec/changes/add-payment-config-management/specs/fuiou-payment/spec.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/specs/fuiou-payment/spec.md similarity index 100% rename from openspec/changes/add-payment-config-management/specs/fuiou-payment/spec.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/specs/fuiou-payment/spec.md diff --git a/openspec/changes/add-payment-config-management/specs/order-payment/spec.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/specs/order-payment/spec.md similarity index 100% rename from openspec/changes/add-payment-config-management/specs/order-payment/spec.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/specs/order-payment/spec.md diff --git a/openspec/changes/add-payment-config-management/specs/wechat-config-management/spec.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/specs/wechat-config-management/spec.md similarity index 100% rename from openspec/changes/add-payment-config-management/specs/wechat-config-management/spec.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/specs/wechat-config-management/spec.md diff --git a/openspec/changes/add-payment-config-management/specs/wechat-payment/spec.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/specs/wechat-payment/spec.md similarity index 100% rename from openspec/changes/add-payment-config-management/specs/wechat-payment/spec.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/specs/wechat-payment/spec.md diff --git a/openspec/changes/add-payment-config-management/tasks.md b/openspec/changes/archive/2026-03-17-add-payment-config-management/tasks.md similarity index 94% rename from openspec/changes/add-payment-config-management/tasks.md rename to openspec/changes/archive/2026-03-17-add-payment-config-management/tasks.md index 3fea067..82e6882 100644 --- a/openspec/changes/add-payment-config-management/tasks.md +++ b/openspec/changes/archive/2026-03-17-add-payment-config-management/tasks.md @@ -70,11 +70,11 @@ ### 1.8 集成验证与文档 -- [ ] 1.8.1 验证完整流程:创建微信支付配置 → 激活 → 确认缓存生效 → 停用 → 确认降级为钱包支付 -- [ ] 1.8.2 验证配置切换:激活配置 A → 创建订单(记录 payment_config_id=A)→ 切换到配置 B → 新订单使用 B -- [ ] 1.8.3 验证权限控制:代理/企业账号无法访问微信参数配置管理接口 -- [ ] 1.8.4 验证审计日志:CRUD 和激活/停用操作产生审计记录 -- [ ] 1.8.5 创建功能文档 `docs/wechat-config-management/功能总结.md` +- [x] 1.8.1 验证完整流程:创建微信支付配置 → 激活 → 确认缓存生效 → 停用 → 确认降级为钱包支付 +- [x] 1.8.2 验证配置切换:激活配置 A → 创建订单(记录 payment_config_id=A)→ 切换到配置 B → 新订单使用 B +- [x] 1.8.3 验证权限控制:代理/企业账号无法访问微信参数配置管理接口 +- [x] 1.8.4 验证审计日志:CRUD 和激活/停用操作产生审计记录 +- [x] 1.8.5 创建功能文档 `docs/wechat-config-management/功能总结.md` --- @@ -108,8 +108,8 @@ ### 2.5 集成验证与文档 -- [ ] 2.5.1 验证微信支付充值流程:创建充值订单(wechat)→ 确认 payment_config_id 记录 → 模拟回调 → 确认余额增加 -- [ ] 2.5.2 验证线下充值流程:平台创建充值订单(offline)→ 确认线下支付 → 验证操作密码 → 确认余额增加 -- [ ] 2.5.3 验证权限控制:代理只能充自己店铺、非平台不能线下充值、操作密码错误拒绝 -- [ ] 2.5.4 验证审计日志:线下充值操作产生审计记录 -- [ ] 2.5.5 创建功能文档 `docs/agent-recharge/功能总结.md` +- [x] 2.5.1 验证微信支付充值流程:创建充值订单(wechat)→ 确认 payment_config_id 记录 → 模拟回调 → 确认余额增加 +- [x] 2.5.2 验证线下充值流程:平台创建充值订单(offline)→ 确认线下支付 → 验证操作密码 → 确认余额增加 +- [x] 2.5.3 验证权限控制:代理只能充自己店铺、非平台不能线下充值、操作密码错误拒绝 +- [x] 2.5.4 验证审计日志:线下充值操作产生审计记录 +- [x] 2.5.5 创建功能文档 `docs/agent-recharge/功能总结.md` diff --git a/openspec/specs/agent-recharge/spec.md b/openspec/specs/agent-recharge/spec.md new file mode 100644 index 0000000..60ce498 --- /dev/null +++ b/openspec/specs/agent-recharge/spec.md @@ -0,0 +1,598 @@ +# 代理充值管理 API 规范 + +## ADDED Requirements + +--- + +### Requirement: 创建代理充值订单 + +**接口描述**:代理或平台账号发起代理余额钱包充值,创建充值订单。 + +**HTTP 方法与路径** + +``` +POST /api/admin/agent-recharges +``` + +**鉴权** + +- 需要登录态(Bearer Token) +- 代理账号:只能为自己所属店铺的主钱包(wallet_type=main)充值 +- 平台账号:可指定任意店铺 + +--- + +**请求体示例(在线充值 - 微信)** + +```json +{ + "shop_id": 101, + "amount": 50000, + "payment_method": "wechat" +} +``` + +**请求体示例(线下充值 - 仅平台)** + +```json +{ + "shop_id": 101, + "amount": 200000, + "payment_method": "offline" +} +``` + +**请求字段说明** + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| shop_id | integer | 是 | 目标店铺 ID。代理账号只能填写自己所属店铺 ID | +| amount | integer | 是 | 充值金额(单位:分)。范围:10000~100000000(即 100 元~100 万元) | +| payment_method | string | 是 | 支付方式。可选值:`wechat`(在线微信支付)、`offline`(线下转账,仅平台可用) | + +**业务规则** + +- `amount` 最小值为 `AgentRechargeMinAmount`(10000 分 = 100 元),最大值为 `AgentRechargeMaxAmount`(100000000 分 = 100 万元) +- `payment_method=wechat` 时,系统根据当前激活的支付配置自动路由至微信直连或富友通道,并记录 `payment_config_id`;客户端发起支付的具体流程本期暂不实现(Stub) +- `payment_method=offline` 仅平台账号可使用,代理账号调用此方式将返回 `1005 CodeForbidden` +- 订单创建后状态为 `1`(待支付) +- 充值单号前缀为 `ARCH`,全局唯一 + +--- + +**成功响应示例** + +```json +{ + "code": 0, + "msg": "success", + "data": { + "id": 88, + "recharge_no": "ARCH20260316100001", + "shop_id": 101, + "amount": 50000, + "payment_method": "wechat", + "payment_channel": "wechat_direct", + "payment_config_id": 3, + "status": 1, + "created_at": "2026-03-16T10:00:00+08:00" + }, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +**响应字段说明** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | integer | 充值记录 ID | +| recharge_no | string | 充值单号(ARCH 前缀) | +| shop_id | integer | 店铺 ID | +| amount | integer | 充值金额(分) | +| payment_method | string | 支付方式 | +| payment_channel | string | 实际支付通道(wechat_direct / fuyou / offline) | +| payment_config_id | integer\|null | 关联的支付配置 ID(线下充值为 null) | +| status | integer | 订单状态:1=待支付,2=已完成,3=已取消 | +| created_at | string | 创建时间(RFC3339) | + +--- + +**错误响应示例** + +金额超出范围: +```json +{ + "code": 1001, + "msg": "充值金额超出允许范围(100元~100万元)", + "data": null, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +代理账号使用线下充值: +```json +{ + "code": 1005, + "msg": "只有平台账号可以使用线下充值", + "data": null, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +钱包不存在: +```json +{ + "code": 1053, + "msg": "钱包不存在", + "data": null, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +无可用支付配置: +```json +{ + "code": 1175, + "msg": "当前无可用的支付配置,请联系管理员", + "data": null, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +越权访问(代理操作他人店铺): +```json +{ + "code": 1005, + "msg": "无权限操作该资源或资源不存在", + "data": null, + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +### Requirement: 线下充值确认 + +**接口描述**:平台账号确认线下转账已到账,完成充值并为代理钱包增加余额。 + +**HTTP 方法与路径** + +``` +POST /api/admin/agent-recharges/:id/offline-pay +``` + +**鉴权** + +- 需要登录态(Bearer Token) +- 仅平台账号可调用,其他账号类型返回 `1005 CodeForbidden` + +--- + +**请求体示例** + +```json +{ + "operation_password": "Abc123456" +} +``` + +**请求字段说明** + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| operation_password | string | 是 | 操作密码,用于二次身份验证 | + +**路径参数说明** + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| id | integer | 充值记录 ID | + +**业务规则** + +- 操作密码验证失败返回 `1043 CodeInvalidOldPassword` +- 充值记录必须存在且 `payment_method=offline`,否则返回 `1121 CodeRechargeNotFound` +- 充值记录状态必须为 `1`(待支付),否则返回 `1050 CodeInvalidStatus` +- 确认成功后: + 1. 充值记录状态更新为 `2`(已完成),记录 `paid_at` 和 `completed_at` + 2. 代理主钱包余额增加对应金额(使用乐观锁 version 字段防并发) + 3. 创建钱包流水记录 + 4. 记录审计日志(操作人、操作前后数据) + +--- + +**成功响应示例** + +```json +{ + "code": 0, + "msg": "success", + "data": { + "id": 88, + "recharge_no": "ARCH20260316100001", + "shop_id": 101, + "amount": 200000, + "payment_method": "offline", + "payment_channel": "offline", + "payment_config_id": null, + "status": 2, + "paid_at": "2026-03-16T11:00:00+08:00", + "completed_at": "2026-03-16T11:00:00+08:00", + "created_at": "2026-03-16T10:00:00+08:00" + }, + "timestamp": "2026-03-16T11:00:00+08:00" +} +``` + +--- + +**错误响应示例** + +操作密码错误: +```json +{ + "code": 1043, + "msg": "操作密码错误", + "data": null, + "timestamp": "2026-03-16T11:00:00+08:00" +} +``` + +充值记录不存在: +```json +{ + "code": 1121, + "msg": "充值记录不存在", + "data": null, + "timestamp": "2026-03-16T11:00:00+08:00" +} +``` + +充值记录状态不允许操作: +```json +{ + "code": 1050, + "msg": "当前充值记录状态不允许此操作", + "data": null, + "timestamp": "2026-03-16T11:00:00+08:00" +} +``` + +非平台账号调用: +```json +{ + "code": 1005, + "msg": "只有平台账号可以使用线下充值", + "data": null, + "timestamp": "2026-03-16T11:00:00+08:00" +} +``` + +--- + +### Requirement: 代理充值查询 + +#### 接口一:充值记录列表 + +**接口描述**:分页查询代理充值记录,支持按店铺、状态、日期范围过滤。 + +**HTTP 方法与路径** + +``` +GET /api/admin/agent-recharges +``` + +**鉴权** + +- 需要登录态(Bearer Token) +- 代理账号:只能查看自己所属店铺的充值记录 +- 平台账号:可查看所有店铺的充值记录 + +--- + +**请求参数(Query String)** + +``` +GET /api/admin/agent-recharges?page=1&page_size=20&shop_id=101&status=2&start_date=2026-03-01&end_date=2026-03-31 +``` + +**请求参数说明** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| page | integer | 否 | 页码,默认 1 | +| page_size | integer | 否 | 每页条数,默认 20,最大 100 | +| shop_id | integer | 否 | 按店铺 ID 过滤(平台账号可用) | +| status | integer | 否 | 按状态过滤:1=待支付,2=已完成,3=已取消 | +| start_date | string | 否 | 创建时间起始日期,格式 `YYYY-MM-DD` | +| end_date | string | 否 | 创建时间截止日期,格式 `YYYY-MM-DD` | + +--- + +**成功响应示例** + +```json +{ + "code": 0, + "msg": "success", + "data": { + "total": 56, + "page": 1, + "page_size": 20, + "list": [ + { + "id": 88, + "recharge_no": "ARCH20260316100001", + "shop_id": 101, + "shop_name": "测试店铺A", + "amount": 50000, + "payment_method": "wechat", + "payment_channel": "wechat_direct", + "payment_config_id": 3, + "status": 2, + "paid_at": "2026-03-16T10:05:00+08:00", + "completed_at": "2026-03-16T10:05:00+08:00", + "created_at": "2026-03-16T10:00:00+08:00" + }, + { + "id": 87, + "recharge_no": "ARCH20260315090001", + "shop_id": 101, + "shop_name": "测试店铺A", + "amount": 200000, + "payment_method": "offline", + "payment_channel": "offline", + "payment_config_id": null, + "status": 2, + "paid_at": "2026-03-15T11:00:00+08:00", + "completed_at": "2026-03-15T11:00:00+08:00", + "created_at": "2026-03-15T09:00:00+08:00" + } + ] + }, + "timestamp": "2026-03-16T12:00:00+08:00" +} +``` + +**列表项字段说明** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | integer | 充值记录 ID | +| recharge_no | string | 充值单号 | +| shop_id | integer | 店铺 ID | +| shop_name | string | 店铺名称 | +| amount | integer | 充值金额(分) | +| payment_method | string | 支付方式 | +| payment_channel | string | 实际支付通道 | +| payment_config_id | integer\|null | 关联支付配置 ID | +| status | integer | 状态:1=待支付,2=已完成,3=已取消 | +| paid_at | string\|null | 支付时间 | +| completed_at | string\|null | 完成时间 | +| created_at | string | 创建时间 | + +--- + +**错误响应示例** + +参数错误: +```json +{ + "code": 1001, + "msg": "参数验证失败", + "data": null, + "timestamp": "2026-03-16T12:00:00+08:00" +} +``` + +--- + +#### 接口二:充值记录详情 + +**接口描述**:查询单条充值记录的完整详情。 + +**HTTP 方法与路径** + +``` +GET /api/admin/agent-recharges/:id +``` + +**鉴权** + +- 需要登录态(Bearer Token) +- 代理账号:只能查看自己所属店铺的充值记录,否则返回 `1121 CodeRechargeNotFound` +- 平台账号:可查看任意充值记录 + +--- + +**路径参数说明** + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| id | integer | 充值记录 ID | + +--- + +**成功响应示例** + +```json +{ + "code": 0, + "msg": "success", + "data": { + "id": 88, + "recharge_no": "ARCH20260316100001", + "shop_id": 101, + "shop_name": "测试店铺A", + "agent_wallet_id": 55, + "amount": 50000, + "payment_method": "wechat", + "payment_channel": "wechat_direct", + "payment_config_id": 3, + "payment_transaction_id": "wx_txn_20260316_abc123", + "status": 2, + "paid_at": "2026-03-16T10:05:00+08:00", + "completed_at": "2026-03-16T10:05:00+08:00", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:05:00+08:00" + }, + "timestamp": "2026-03-16T12:00:00+08:00" +} +``` + +**详情字段说明** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | integer | 充值记录 ID | +| recharge_no | string | 充值单号 | +| shop_id | integer | 店铺 ID | +| shop_name | string | 店铺名称 | +| agent_wallet_id | integer | 代理钱包 ID | +| amount | integer | 充值金额(分) | +| payment_method | string | 支付方式 | +| payment_channel | string | 实际支付通道 | +| payment_config_id | integer\|null | 关联支付配置 ID | +| payment_transaction_id | string\|null | 第三方支付流水号 | +| status | integer | 状态:1=待支付,2=已完成,3=已取消 | +| paid_at | string\|null | 支付时间 | +| completed_at | string\|null | 完成时间 | +| created_at | string | 创建时间 | +| updated_at | string | 最后更新时间 | + +--- + +**错误响应示例** + +充值记录不存在或无权限: +```json +{ + "code": 1121, + "msg": "充值记录不存在", + "data": null, + "timestamp": "2026-03-16T12:00:00+08:00" +} +``` + +--- + +### Requirement: 代理充值回调处理 + +**接口描述**:接收第三方支付平台(微信直连 / 富友)的异步支付结果通知,完成充值订单状态更新和钱包余额增加。 + +**HTTP 方法与路径** + +回调地址由支付配置中的 `notify_url` 字段决定,格式示例: + +``` +POST /api/payment/callback/agent-recharge/{payment_channel} +``` + +其中 `payment_channel` 为 `wechat_direct` 或 `fuyou`。 + +**鉴权** + +- 无需登录态 +- 通过签名验证确认请求来源合法性 + +--- + +**处理流程** + +``` +1. 接收回调请求 +2. 根据 payment_channel 确定验签方式 +3. 通过 recharge_no(充值单号)查找充值记录 +4. 幂等性检查:若记录状态已为 2(已完成),直接返回成功 +5. 使用充值记录中的 payment_config_id 查找对应支付配置 +6. 使用支付配置的密钥验证签名 +7. 验签通过后,在事务中执行: + a. 更新充值记录状态为 2(已完成),记录 payment_transaction_id、paid_at、completed_at + b. 代理主钱包余额增加充值金额(乐观锁 version 字段防并发) + c. 创建钱包流水记录(类型:充值入账) +8. 返回支付平台要求的成功响应格式 +``` + +**幂等性保障** + +- 使用充值记录状态作为幂等判断依据(状态条件更新:`WHERE status = 1`) +- `RowsAffected == 0` 时说明已被处理,直接返回成功,不重复入账 + +**签名验证** + +- 根据充值记录的 `payment_config_id` 查找对应支付配置 +- 使用该配置的密钥(`api_key` / `app_secret`)按对应通道规则验签 +- 验签失败时记录错误日志,返回失败响应(不更新订单状态) + +**回调响应** + +- 微信直连:返回 `{"code": "SUCCESS", "message": "成功"}` +- 富友:按富友协议返回对应成功标识 +- 处理失败时返回对应通道的失败标识,触发第三方平台重试 + +**异常处理** + +- 充值记录不存在:记录警告日志,返回失败(触发重试,等待数据一致) +- 签名验证失败:记录错误日志(含完整请求体),返回失败 +- 钱包余额更新失败(乐观锁冲突):最多重试 3 次,仍失败则记录告警日志并返回失败 + +--- + +### Requirement: 权限控制 + +**账号类型与操作权限矩阵** + +| 操作 | 平台账号 | 代理账号 | 企业账号 | +|------|----------|----------|----------| +| 创建充值订单(在线) | ✅ 任意店铺 | ✅ 仅自己店铺 | ❌ | +| 创建充值订单(线下) | ✅ 任意店铺 | ❌ | ❌ | +| 线下充值确认 | ✅ | ❌ | ❌ | +| 查询充值列表 | ✅ 全部 | ✅ 仅自己店铺 | ❌ | +| 查询充值详情 | ✅ 全部 | ✅ 仅自己店铺 | ❌ | + +**越权防护规则** + +1. **路由层**:企业账号访问代理充值相关接口,统一返回 `1005 CodeForbidden` +2. **Service 层**: + - 代理账号创建充值时,验证 `shop_id` 必须属于自己所属店铺 + - 代理账号查询详情时,验证充值记录的 `shop_id` 必须属于自己所属店铺 +3. **越权统一响应**:不区分"不存在"和"无权限",统一返回 `1005` 或对应资源不存在错误,防止信息泄露 + +**线下充值操作密码** + +- 平台账号执行线下充值确认时,必须提供操作密码 +- 操作密码验证失败返回 `1043 CodeInvalidOldPassword` +- 操作密码不在响应中返回,不记录到日志明文中 + +--- + +## 数据模型补充说明 + +**tb_agent_recharge_record 新增字段** + +| 字段名 | 类型 | 可空 | 说明 | +|--------|------|------|------| +| payment_config_id | bigint | 是 | 关联支付配置 ID,线下充值为 NULL,在线充值记录实际使用的支付配置 | + +**充值状态枚举** + +| 值 | 含义 | +|----|------| +| 1 | 待支付(订单已创建,等待支付) | +| 2 | 已完成(支付成功,余额已到账) | +| 3 | 已取消(超时未支付或主动取消) | + +**支付方式枚举** + +| 值 | 含义 | +|----|------| +| wechat | 微信在线支付(自动路由至微信直连或富友) | +| offline | 线下转账(仅平台账号可用) | + +**支付通道枚举** + +| 值 | 含义 | +|----|------| +| wechat_direct | 微信直连通道 | +| fuyou | 富友通道 | +| offline | 线下转账 | diff --git a/openspec/specs/asset-recharge-adaptation/spec.md b/openspec/specs/asset-recharge-adaptation/spec.md new file mode 100644 index 0000000..39395da --- /dev/null +++ b/openspec/specs/asset-recharge-adaptation/spec.md @@ -0,0 +1,116 @@ +## MODIFIED Requirements + +### Requirement: 资产充值关联支付配置 + +系统 SHALL 在创建资产充值订单时记录当前生效的支付配置 ID,用于回调处理时加载正确的配置验签。 + +#### Scenario: 创建充值订单时记录支付配置 ID + +- **WHEN** 个人客户创建资产充值订单(IoT 卡钱包或设备钱包充值) + +``` +POST /api/h5/wallets/recharge +Authorization: Bearer {token} +Content-Type: application/json +``` + +**请求体(现有接口,字段不变)** + +```json +{ + "resource_type": "iot_card", + "resource_id": 101, + "amount": 10000, + "payment_method": "wechat" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `resource_type` | string | ✅ | 资源类型:`iot_card` / `device` | +| `resource_id` | uint | ✅ | 资源 ID(卡 ID 或设备 ID) | +| `amount` | int64 | ✅ | 充值金额(分),范围 100~10000000(1 元~10 万元) | +| `payment_method` | string | ✅ | 支付方式:`wechat` / `alipay`(支付宝保留但本次不改造) | + +- **THEN** 系统查询当前生效的微信参数配置 +- **THEN** 将 `payment_config_id` 写入充值记录 + +**成功响应 `200 OK`(新增 `payment_config_id` 字段)** + +```json +{ + "code": 0, + "data": { + "id": 1, + "recharge_no": "CRCH20260316100000654321", + "user_id": 100, + "wallet_id": 50, + "amount": 10000, + "payment_method": "wechat", + "payment_config_id": 1, + "status": 1, + "status_text": "待支付", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:00:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 无生效配置时拒绝第三方充值 + +- **WHEN** 个人客户创建充值订单(wechat/alipay),但当前无生效的微信参数配置 +- **THEN** 系统返回错误 + +```json +{ + "code": 1175, + "data": null, + "msg": "暂无可用的第三方支付渠道", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +### Requirement: 资产充值表结构变更 + +`tb_asset_recharge_record` 新增字段: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `payment_config_id` | bigint | ❌ | 创建充值订单时使用的微信参数配置 ID(支付宝支付时为 NULL) | + +--- + +### Requirement: 资产充值回调按配置验签 + +- **WHEN** 收到支付回调(微信或富友),订单号前缀为 `CRCH` +- **THEN** 系统查询 `tb_asset_recharge_record`,通过 `payment_config_id` 加载对应配置 +- **THEN** 使用该配置的凭证验签 +- **THEN** 验签通过后调用 `rechargeService.HandlePaymentCallback()` + +> **注意**:当前代码中 `callback/payment.go` 使用废弃的 `RechargeOrderPrefix = "RCH"` 进行前缀匹配,需修复为 `AssetRechargeOrderPrefix = "CRCH"`。 + +--- + +### Requirement: 常量重命名(Card → Asset) + +`pkg/constants/wallet.go` 中以下常量从 `Card` 前缀重命名为 `Asset` 前缀: + +| 旧名称 | 新名称 | +|--------|--------| +| `CardWalletResourceTypeIotCard` | `AssetWalletResourceTypeIotCard` | +| `CardWalletResourceTypeDevice` | `AssetWalletResourceTypeDevice` | +| `CardWalletStatusNormal` | `AssetWalletStatusNormal` | +| `CardWalletStatusFrozen` | `AssetWalletStatusFrozen` | +| `CardWalletStatusClosed` | `AssetWalletStatusClosed` | +| `CardTransactionTypeRecharge` | `AssetTransactionTypeRecharge` | +| `CardTransactionTypeDeduct` | `AssetTransactionTypeDeduct` | +| `CardTransactionTypeRefund` | `AssetTransactionTypeRefund` | +| `CardRechargeOrderPrefix` | `AssetRechargeOrderPrefix` | +| `CardRechargeMinAmount` | `AssetRechargeMinAmount` | +| `CardRechargeMaxAmount` | `AssetRechargeMaxAmount` | + +旧 `Card*` 常量保留为废弃别名,添加 `Deprecated` 注释。段落标题 `卡钱包常量` → `资产钱包常量`。 diff --git a/openspec/specs/fuiou-payment/spec.md b/openspec/specs/fuiou-payment/spec.md new file mode 100644 index 0000000..2c6d128 --- /dev/null +++ b/openspec/specs/fuiou-payment/spec.md @@ -0,0 +1,181 @@ +## ADDED Requirements + +### Requirement: 富友支付公众号 JSAPI 下单 + +系统 SHALL 支持通过富友支付 `wxPreCreate` 接口发起微信公众号 JSAPI 支付。 + +> **本次留桩**:`FuiouPayJSAPI` 方法在 Service 层定义,但实际调用第三方获取支付参数的逻辑暂不实现,返回"富友支付发起暂未实现"错误。`pkg/fuiou/` SDK 包完整实现。 + +#### Scenario: 公众号 JSAPI 下单成功 + +- **WHEN** 系统调用富友 `wxPreCreate` 接口 + - `trade_type=JSAPI` + - `sub_appid=公众号AppID`(从 `tb_wechat_config.oa_app_id` 读取) + - `sub_openid=用户公众号OpenID` + - 传入订单号、金额(分)、商品描述、终端 IP、回调地址 +- **THEN** 富友返回 `result_code=000000`,包含支付参数 + +**富友返回支付参数结构** + +```json +{ + "sdk_appid": "wx1234567890abcdef", + "sdk_timestamp": "1711411341", + "sdk_noncestr": "abc123def456", + "sdk_prepayid": "wx26112221580621e9b071c00d9e093b0000", + "sdk_package": "Sign=WXPay", + "sdk_signtype": "RSA", + "sdk_paysign": "..." +} +``` + +- **THEN** 系统将支付参数返回给前端,前端调用 `WeixinJSBridge.invoke('getBrandWCPayRequest', ...)` 拉起支付 + +#### Scenario: 公众号 JSAPI 下单失败 + +- **WHEN** 富友返回 `result_code` 非 `000000` +- **THEN** 系统记录 ERROR 日志(订单号、错误码、错误消息) +- **THEN** 系统返回错误 + +```json +{ + "code": 1173, + "data": null, + "msg": "支付发起失败,请重试", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +### Requirement: 富友支付小程序下单 + +系统 SHALL 支持通过富友支付 `wxPreCreate` 接口发起微信小程序支付。 + +#### Scenario: 小程序下单成功 + +- **WHEN** 系统调用富友 `wxPreCreate` 接口 + - `trade_type=LETPAY` + - `sub_appid=小程序AppID`(从 `tb_wechat_config.miniapp_app_id` 读取) + - `sub_openid=用户小程序OpenID` +- **THEN** 富友返回 `result_code=000000`,包含支付参数 +- **THEN** 系统将支付参数返回给前端,前端调用 `wx.requestPayment(...)` 拉起支付 + +#### Scenario: 小程序下单缺少 OpenID + +- **WHEN** 系统发起小程序支付但未传入 `sub_openid` +- **THEN** 系统返回错误 + +```json +{ + "code": 1001, + "data": null, + "msg": "小程序支付必须提供用户 OpenID", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +### Requirement: 富友支付回调处理 + +系统 SHALL 接收并处理富友支付成功回调通知,验证签名后更新订单/充值状态。 + +#### Scenario: 接收到合法的支付成功回调 + +``` +POST /api/callback/fuiou-pay +Content-Type: application/x-www-form-urlencoded +无需认证 +``` + +**请求体格式**:`req=<双重URL编码的GBK XML>` + +- **THEN** 系统将请求体从 GBK 转换为 UTF-8 +- **THEN** 系统解析 XML 格式的回调数据 +- **THEN** 系统根据 `mchnt_order_no` 判断订单类型: + - `ORD` 开头 → 套餐订单 → 查询 `tb_order` + - `CRCH` 开头 → 资产充值 → 查询 `tb_asset_recharge_record` + - `ARCH` 开头 → 代理充值 → 查询 `tb_agent_recharge_record` +- **THEN** 通过记录的 `payment_config_id` 加载对应的富友配置 +- **THEN** 使用该配置的富友公钥验证 RSA 签名 +- **THEN** 验证 `result_code=000000` 且金额匹配 +- **THEN** 调用对应 Service 的 HandlePaymentCallback +- **THEN** 返回成功 XML 响应(GBK 编码) + +**成功响应** + +```xml + + + 000000 + success + +``` + +#### Scenario: 回调签名验证失败 + +- **WHEN** 富友回调的 RSA 签名与本地计算不匹配 +- **THEN** 系统记录 ERROR 日志 +- **THEN** 返回失败 XML 响应 + +```xml + + + 999999 + signature verification failed + +``` + +#### Scenario: 回调订单号不存在 + +- **WHEN** `mchnt_order_no` 在系统中不存在 +- **THEN** 系统记录 ERROR 日志,返回失败 XML 响应 + +#### Scenario: 重复回调幂等处理 + +- **WHEN** 富友对同一订单多次发送支付成功回调 +- **THEN** 系统识别已支付,直接返回成功 XML 响应 + +--- + +### Requirement: 富友 XML 通信协议 + +系统 SHALL 正确处理富友支付的 XML + GBK 编码通信协议。 + +#### Scenario: 请求编码 + +- **WHEN** 系统向富友发送请求 +- **THEN** 请求体为 XML 格式,GBK 编码声明 +- **THEN** XML 内容经 GBK 编码后进行两次 URL 编码 +- **THEN** 以 `req=` 的 form 格式发送 + +#### Scenario: 响应解码 + +- **WHEN** 系统接收富友响应 +- **THEN** 先进行 URL 解码 +- **THEN** 将 GBK 内容转换为 UTF-8 +- **THEN** 替换 XML 声明中的 `encoding="GBK"` 为 `encoding="UTF-8"` +- **THEN** 解析 XML 到结构体 + +--- + +### Requirement: 富友 RSA 签名算法 + +系统 SHALL 实现富友支付的 RSA + MD5 签名验签算法。 + +#### Scenario: 生成请求签名 + +- **WHEN** 系统需要对富友请求签名 +- **THEN** 提取所有非空字段(排除 `sign` 和 `reserved_` 开头字段) +- **THEN** 按字典序排列为 `key=value&key=value` 格式 +- **THEN** 将签名原文转换为 GBK 编码 +- **THEN** 计算 MD5 哈希 +- **THEN** 使用商户私钥对 MD5 哈希进行 RSA PKCS1v15 签名 +- **THEN** 对签名结果进行 Base64 编码 + +#### Scenario: 验证回调签名 + +- **WHEN** 系统需要验证富友回调签名 +- **THEN** 使用相同算法计算签名原文的 MD5 哈希 +- **THEN** 使用富友公钥对回调中的 `sign` 字段进行 RSA PKCS1v15 验签 diff --git a/openspec/specs/order-payment/spec.md b/openspec/specs/order-payment/spec.md index c033789..83aab77 100644 --- a/openspec/specs/order-payment/spec.md +++ b/openspec/specs/order-payment/spec.md @@ -376,3 +376,190 @@ - **WHEN** 订单取消时,钱包解冻失败(如钱包不存在、冻结余额不足) - **THEN** 事务回滚,订单状态不变,返回错误信息"订单取消失败" + +--- + +## MODIFIED Requirements (from: add-payment-config-management) + +### Requirement: 订单关联支付配置 + +系统 SHALL 在创建订单时记录当前生效的支付配置 ID,用于回调处理时加载正确的配置验签。 + +#### Scenario: 创建订单时记录支付配置 ID + +- **WHEN** 用户创建订单(H5 或后台) +- **THEN** 系统查询当前生效的微信参数配置(`is_active=true`) +- **THEN** 将 `payment_config_id` 写入订单记录 + +**订单模型变更** + +`tb_order` 新增字段: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `payment_config_id` | bigint | ❌ | 下单时使用的微信参数配置 ID(钱包/线下支付时为 NULL) | + +**OrderResponse 新增返回字段** + +```json +{ + "code": 0, + "data": { + "id": 1, + "order_no": "ORD20260316100000123456", + "payment_config_id": 1, + "...": "(现有字段不变)" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 钱包/线下支付不记录配置 ID + +- **WHEN** 用户创建订单,支付方式为 `wallet` 或 `offline` +- **THEN** 订单的 `payment_config_id` 为 NULL + +#### Scenario: 无生效配置时拒绝第三方支付 + +- **WHEN** 用户创建订单时选择第三方支付(wechat/fuiou),但当前无生效的微信参数配置 +- **THEN** 系统返回错误 + +```json +{ + "code": 1175, + "data": null, + "msg": "暂无可用的第三方支付渠道,请使用钱包支付", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 无生效配置时允许钱包支付 + +- **WHEN** 当前无生效支付配置,用户选择钱包支付 +- **THEN** 系统正常创建订单,`payment_config_id` 为 NULL + +--- + +### Requirement: 第三方支付回调 + +系统 SHALL 处理微信支付和富友支付的支付回调。回调验签 MUST 使用订单关联的 `payment_config_id` 加载对应配置,而非当前生效配置。系统新增富友支付回调端点和代理充值回调分发。 + +#### Scenario: 微信支付成功回调(订单) + +``` +POST /api/callback/wechat-pay +Content-Type: 由微信服务器决定 +无需认证 +``` + +- **WHEN** 收到微信支付成功回调,订单号格式为 `ORD` 开头 +- **THEN** 系统查询订单,通过 `order.payment_config_id` 加载对应支付配置 +- **THEN** 系统使用该配置的凭证验证签名,更新订单状态,激活套餐 + +**成功响应** + +```json +{ + "code": 0, + "data": { + "return_code": "SUCCESS" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 微信支付成功回调(资产充值) + +- **WHEN** 收到微信支付成功回调,订单号格式为 `CRCH` 开头(修复:当前代码误用废弃的 `RCH` 前缀) +- **THEN** 系统查询 `tb_asset_recharge_record`,通过 `payment_config_id` 加载配置验签 +- **THEN** 系统更新充值订单状态,增加钱包余额,触发佣金判断 + +#### Scenario: 微信支付成功回调(代理充值) + +- **WHEN** 收到微信支付成功回调,订单号格式为 `ARCH` 开头(全新支持) +- **THEN** 系统查询 `tb_agent_recharge_record`,通过 `payment_config_id` 加载配置验签 +- **THEN** 系统更新充值订单状态,增加代理余额钱包余额 + +#### Scenario: 富友支付成功回调 + +``` +POST /api/callback/fuiou-pay +Content-Type: application/x-www-form-urlencoded +无需认证 +``` + +- **WHEN** 收到富友支付回调,`result_code=000000` +- **THEN** 系统解析 XML(GBK → UTF-8),通过 `mchnt_order_no` 判断订单类型(ORD/CRCH/ARCH) +- **THEN** 查询对应表,通过 `payment_config_id` 加载富友配置,使用富友公钥验签 +- **THEN** 验证金额匹配后,调用对应 Service 的 HandlePaymentCallback + +**成功响应(XML,GBK 编码)** + +```xml + + + 000000 + success + +``` + +#### Scenario: 重复回调 + +- **WHEN** 收到已处理订单/充值的重复回调(微信或富友) +- **THEN** 系统返回成功响应,不重复处理 + +#### Scenario: 签名验证失败 + +- **WHEN** 回调签名验证失败(微信或富友) +- **THEN** 系统拒绝处理,记录 ERROR 日志,返回失败响应 + +#### Scenario: 订单号不存在 + +- **WHEN** 回调中的订单号在系统中不存在 +- **THEN** 系统记录 ERROR 日志,返回失败响应 + +--- + +### Requirement: 钱包支付与第三方支付的区别 + +系统 SHALL 区分后台钱包支付和第三方支付的业务逻辑。第三方支付方式对前端统一显示为"微信支付",后端根据生效配置自动路由。 + +**后台支付方式限制**(`CreateAdminOrderRequest`): +- 允许:`wallet`、`offline` +- 拒绝:`wechat`、`alipay`、`fuiou`、其他任何值 + +**H5/小程序支付方式**(两步走): +- 步骤 1 创建订单:`payment_method` 为 `wallet` +- 步骤 2 发起第三方支付:通过独立端点 `/orders/:id/wechat-pay/jsapi` 等 + +#### Scenario: 后台参数验证拒绝第三方支付 + +- **WHEN** 代理在后台创建订单时 `payment_method` 为 wechat 或 fuiou +- **THEN** DTO 验证阶段拒绝请求 + +```json +{ + "code": 1001, + "data": null, + "msg": "请求参数解析失败", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: H5 两步走支付 + +- **WHEN** 个人客户在 H5 创建订单(步骤 1) +- **THEN** 订单创建为待支付状态,记录 `payment_config_id` +- **WHEN** 客户调用 `POST /orders/:id/wechat-pay/jsapi`(步骤 2) +- **THEN** 系统按 `payment_config_id` 加载配置,根据 `provider_type` 发起对应渠道支付(本次留桩) + +--- + +### Requirement: 配置切换不取消在途订单 + +- **WHEN** 管理员激活新配置时,系统中存在使用旧配置创建的待支付订单 +- **THEN** 系统不取消这些订单 +- **THEN** 旧订单若支付成功,回调按 `payment_config_id` 加载旧配置验签 +- **THEN** 旧订单若未支付,由 30 分钟超时机制自动取消 diff --git a/openspec/specs/wechat-config-management/spec.md b/openspec/specs/wechat-config-management/spec.md new file mode 100644 index 0000000..6c69113 --- /dev/null +++ b/openspec/specs/wechat-config-management/spec.md @@ -0,0 +1,997 @@ +## ADDED Requirements + +### Requirement: 微信参数配置 CRUD 管理 + +系统 SHALL 支持平台用户对微信支付参数配置的完整生命周期管理,包括创建、列表查询、详情查询、更新、删除。每个配置包含完整的支付身份信息(渠道凭证 + 公众号 OAuth 信息 + 小程序 OAuth 信息)。 + +--- + +#### Scenario: 创建微信直连支付配置 + +**WHEN** 平台用户调用 `POST /api/admin/wechat-configs`,`provider_type` 为 `wechat`,提供名称、公众号信息、小程序信息、商户号、API V3 密钥、证书内容(Base64)、私钥内容(Base64)、证书序列号、回调地址 + +**THEN** 系统创建配置记录,`is_active` 默认为 `false`,返回完整配置信息(敏感字段脱敏) + +##### 请求 + +``` +POST /api/admin/wechat-configs +Authorization: Bearer {token} +Content-Type: application/json +``` + +```json +{ + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcdef1234567890abcdef1234567890", + "oa_token": "mytoken123", + "oa_aes_key": "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedcba0987654321fedcba0987654321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your32charv3keyhere1234567890abc", + "wx_api_v2_key": "your32charv2keyhere1234567890abc", + "wx_cert_content": "BASE64_ENCODED_CERT_CONTENT_HERE", + "wx_key_content": "BASE64_ENCODED_KEY_CONTENT_HERE", + "wx_serial_no": "ABCDEF1234567890ABCDEF1234567890ABCDEF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify" +} +``` + +##### 请求字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | 是 | 配置名称,最长 100 字符 | +| description | string | 否 | 配置描述,最长 500 字符 | +| provider_type | string | 是 | 渠道类型,枚举值:`wechat`(微信直连)、`fuiou`(富友) | +| oa_app_id | string | 否 | 公众号 AppID | +| oa_app_secret | string | 否 | 公众号 AppSecret | +| oa_token | string | 否 | 公众号消息校验 Token | +| oa_aes_key | string | 否 | 公众号消息加解密 Key | +| oa_oauth_redirect_url | string | 否 | 公众号 OAuth 回调地址 | +| miniapp_app_id | string | 否 | 小程序 AppID | +| miniapp_app_secret | string | 否 | 小程序 AppSecret | +| wx_mch_id | string | provider_type=wechat 时必填 | 微信商户号 | +| wx_api_v3_key | string | provider_type=wechat 时必填 | API V3 密钥(32字符) | +| wx_api_v2_key | string | 否 | API V2 密钥(32字符) | +| wx_cert_content | string | provider_type=wechat 时必填 | 商户证书内容(Base64 编码) | +| wx_key_content | string | provider_type=wechat 时必填 | 商户私钥内容(Base64 编码) | +| wx_serial_no | string | provider_type=wechat 时必填 | 商户证书序列号 | +| wx_notify_url | string | provider_type=wechat 时必填 | 微信支付回调地址 | + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "is_active": false, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:00:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +##### 敏感字段脱敏规则 + +| 字段 | 脱敏规则 | 示例原值 | 脱敏后 | +|------|---------|---------|--------| +| oa_app_secret | 前4位 + `***` + 后4位 | `abcdef1234567890abcdef1234567890` | `abcd***7890` | +| oa_token | 前4位 + `***` + 后4位 | `mytoken123` | `myto***n123` | +| oa_aes_key | `[已配置]` / `[未配置]` | 任意值 | `[已配置]` | +| miniapp_app_secret | 前4位 + `***` + 后4位 | `fedcba0987654321fedcba0987654321` | `fedc***4321` | +| wx_api_v3_key | 前4位 + `***` + 后4位 | `your32charv3keyhere1234567890abc` | `your***0abc` | +| wx_api_v2_key | 前4位 + `***` + 后4位 | `your32charv2keyhere1234567890abc` | `your***0abc` | +| wx_cert_content | `[已配置]` / `[未配置]` | Base64 内容 | `[已配置]` | +| wx_key_content | `[已配置]` / `[未配置]` | Base64 内容 | `[已配置]` | +| wx_serial_no | 前4位 + `***` + 后4位 | `ABCDEF1234567890ABCDEF1234567890ABCDEF12` | `ABCD***EF12` | +| fy_private_key | `[已配置]` / `[未配置]` | Base64 内容 | `[已配置]` | +| fy_public_key | `[已配置]` / `[未配置]` | Base64 内容 | `[已配置]` | + +--- + +#### Scenario: 创建富友支付配置 + +**WHEN** 平台用户调用 `POST /api/admin/wechat-configs`,`provider_type` 为 `fuiou`,提供名称、公众号信息、小程序信息、机构号、商户号、终端号、商户私钥(Base64)、富友公钥(Base64)、API 地址、回调地址 + +**THEN** 系统创建配置记录,`is_active` 默认为 `false` + +##### 请求 + +``` +POST /api/admin/wechat-configs +Authorization: Bearer {token} +Content-Type: application/json +``` + +```json +{ + "name": "富友支付配置", + "description": "富友聚合支付渠道配置", + "provider_type": "fuiou", + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcdef1234567890abcdef1234567890", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedcba0987654321fedcba0987654321", + "fy_ins_cd": "0000100", + "fy_mchnt_cd": "0000100002000001", + "fy_term_id": "00000001", + "fy_private_key": "BASE64_ENCODED_MERCHANT_PRIVATE_KEY", + "fy_public_key": "BASE64_ENCODED_FUIOU_PUBLIC_KEY", + "fy_api_url": "https://spay.fuiou.com", + "fy_notify_url": "https://example.com/api/payment/fuiou/notify" +} +``` + +##### 请求字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | 是 | 配置名称,最长 100 字符 | +| description | string | 否 | 配置描述,最长 500 字符 | +| provider_type | string | 是 | 渠道类型,此处为 `fuiou` | +| oa_app_id | string | 否 | 公众号 AppID | +| oa_app_secret | string | 否 | 公众号 AppSecret | +| oa_token | string | 否 | 公众号消息校验 Token | +| oa_aes_key | string | 否 | 公众号消息加解密 Key | +| oa_oauth_redirect_url | string | 否 | 公众号 OAuth 回调地址 | +| miniapp_app_id | string | 否 | 小程序 AppID | +| miniapp_app_secret | string | 否 | 小程序 AppSecret | +| fy_ins_cd | string | provider_type=fuiou 时必填 | 富友机构号 | +| fy_mchnt_cd | string | provider_type=fuiou 时必填 | 富友商户号 | +| fy_term_id | string | provider_type=fuiou 时必填 | 富友终端号 | +| fy_private_key | string | provider_type=fuiou 时必填 | 商户私钥(Base64 编码) | +| fy_public_key | string | provider_type=fuiou 时必填 | 富友公钥(Base64 编码) | +| fy_api_url | string | provider_type=fuiou 时必填 | 富友 API 地址 | +| fy_notify_url | string | provider_type=fuiou 时必填 | 富友支付回调地址 | + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "id": 2, + "name": "富友支付配置", + "description": "富友聚合支付渠道配置", + "provider_type": "fuiou", + "is_active": false, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "", + "oa_aes_key": "[未配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "", + "wx_api_v3_key": "", + "wx_api_v2_key": "", + "wx_cert_content": "[未配置]", + "wx_key_content": "[未配置]", + "wx_serial_no": "", + "wx_notify_url": "", + "fy_ins_cd": "0000100", + "fy_mchnt_cd": "0000100002000001", + "fy_term_id": "00000001", + "fy_private_key": "[已配置]", + "fy_public_key": "[已配置]", + "fy_api_url": "https://spay.fuiou.com", + "fy_notify_url": "https://example.com/api/payment/fuiou/notify", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:00:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 创建配置参数校验失败 + +**WHEN** 平台用户创建配置时缺少必填字段(如 `provider_type` 为 `wechat` 但未提供 `wx_mch_id`) + +**THEN** 系统返回错误码 `1001`,拒绝创建 + +##### 请求示例(缺少 wx_mch_id) + +``` +POST /api/admin/wechat-configs +Authorization: Bearer {token} +Content-Type: application/json +``` + +```json +{ + "name": "微信直连配置", + "provider_type": "wechat", + "wx_api_v3_key": "your32charv3keyhere1234567890abc" +} +``` + +##### 错误响应 + +```json +{ + "code": 1001, + "data": null, + "msg": "参数错误", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 查询配置列表 + +**WHEN** 平台用户调用 `GET /api/admin/wechat-configs`,支持按 `provider_type` 和 `is_active` 筛选,支持分页 + +**THEN** 系统返回配置列表,敏感字段脱敏 + +##### 请求 + +``` +GET /api/admin/wechat-configs?provider_type=wechat&is_active=false&page=1&page_size=20 +Authorization: Bearer {token} +``` + +##### 查询参数说明 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| provider_type | string | 否 | 按渠道类型筛选,枚举值:`wechat`、`fuiou` | +| is_active | boolean | 否 | 按激活状态筛选,`true` 或 `false` | +| page | integer | 否 | 页码,默认 1 | +| page_size | integer | 否 | 每页条数,默认 20,最大 100 | + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "list": [ + { + "id": 1, + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "is_active": false, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:00:00+08:00" + } + ], + "total": 1, + "page": 1, + "page_size": 20 + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 查询配置详情 + +**WHEN** 平台用户调用 `GET /api/admin/wechat-configs/:id` + +**THEN** 系统返回配置详情,敏感字段脱敏 + +##### 请求 + +``` +GET /api/admin/wechat-configs/1 +Authorization: Bearer {token} +``` + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "is_active": false, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:00:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +##### 配置不存在时的错误响应 + +```json +{ + "code": 1170, + "data": null, + "msg": "微信支付配置不存在", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 更新配置(非敏感字段) + +**WHEN** 平台用户调用 `PUT /api/admin/wechat-configs/:id`,仅更新名称、描述、回调地址等非敏感字段 + +**THEN** 系统更新对应字段,敏感字段保持不变 + +##### 请求 + +``` +PUT /api/admin/wechat-configs/1 +Authorization: Bearer {token} +Content-Type: application/json +``` + +```json +{ + "name": "微信直连主配置(已更新)", + "description": "更新后的描述", + "wx_notify_url": "https://new.example.com/api/payment/wechat/notify" +} +``` + +##### 请求字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | 否 | 配置名称 | +| description | string | 否 | 配置描述 | +| oa_app_id | string | 否 | 公众号 AppID | +| oa_app_secret | string | 否 | 公众号 AppSecret;空字符串或不传 = 保留原值;传新值 = 替换 | +| oa_token | string | 否 | 公众号消息校验 Token;空字符串或不传 = 保留原值;传新值 = 替换 | +| oa_aes_key | string | 否 | 公众号消息加解密 Key;空字符串或不传 = 保留原值;传新值 = 替换 | +| oa_oauth_redirect_url | string | 否 | 公众号 OAuth 回调地址 | +| miniapp_app_id | string | 否 | 小程序 AppID | +| miniapp_app_secret | string | 否 | 小程序 AppSecret;空字符串或不传 = 保留原值;传新值 = 替换 | +| wx_mch_id | string | 否 | 微信商户号 | +| wx_api_v3_key | string | 否 | API V3 密钥;空字符串或不传 = 保留原值;传新值 = 替换 | +| wx_api_v2_key | string | 否 | API V2 密钥;空字符串或不传 = 保留原值;传新值 = 替换 | +| wx_cert_content | string | 否 | 商户证书内容(Base64);空字符串或不传 = 保留原值;传新值 = 替换 | +| wx_key_content | string | 否 | 商户私钥内容(Base64);空字符串或不传 = 保留原值;传新值 = 替换 | +| wx_serial_no | string | 否 | 商户证书序列号;空字符串或不传 = 保留原值;传新值 = 替换 | +| wx_notify_url | string | 否 | 微信支付回调地址 | +| fy_ins_cd | string | 否 | 富友机构号 | +| fy_mchnt_cd | string | 否 | 富友商户号 | +| fy_term_id | string | 否 | 富友终端号 | +| fy_private_key | string | 否 | 商户私钥(Base64);空字符串或不传 = 保留原值;传新值 = 替换 | +| fy_public_key | string | 否 | 富友公钥(Base64);空字符串或不传 = 保留原值;传新值 = 替换 | +| fy_api_url | string | 否 | 富友 API 地址 | +| fy_notify_url | string | 否 | 富友支付回调地址 | + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "微信直连主配置(已更新)", + "description": "更新后的描述", + "provider_type": "wechat", + "is_active": false, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://new.example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:30:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:30:00+08:00" +} +``` + +--- + +#### Scenario: 更新当前生效配置时清除 Redis 缓存 + +**WHEN** 平台用户更新的配置 `is_active=true`(当前生效配置) + +**THEN** 系统更新字段后,主动清除 Redis 缓存 `wechat:config:active`,使变更即时生效 + +**THEN** 响应格式与普通更新相同 + +--- + +#### Scenario: 更新配置时替换敏感字段 + +**WHEN** 平台用户更新配置时,对脱敏字段传入新的明文值(非空字符串、非脱敏格式) + +**THEN** 系统将该字段替换为新值 + +##### 请求示例(替换 API V3 密钥) + +```json +{ + "wx_api_v3_key": "newkey32charsnewkey32charsnewkey" +} +``` + +**THEN** 系统将 `wx_api_v3_key` 更新为新值,响应中该字段显示脱敏后的新值 `newk***ekey` + +--- + +#### Scenario: 删除未激活的配置 + +**WHEN** 平台用户调用 `DELETE /api/admin/wechat-configs/:id`,且该配置 `is_active=false` + +**THEN** 系统软删除该配置记录,返回成功 + +##### 请求 + +``` +DELETE /api/admin/wechat-configs/1 +Authorization: Bearer {token} +``` + +##### 成功响应 + +```json +{ + "code": 0, + "data": null, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 禁止删除激活中的配置 + +**WHEN** 平台用户尝试删除 `is_active=true` 的配置 + +**THEN** 系统返回错误码 `1171`,拒绝删除 + +##### 错误响应 + +```json +{ + "code": 1171, + "data": null, + "msg": "不能删除当前生效的支付配置,请先停用", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 禁止删除有在途订单的配置 + +**WHEN** 平台用户尝试删除某配置,且存在 `payment_config_id` 指向该配置的待支付订单 + +**THEN** 系统返回错误码 `1172`,拒绝删除 + +##### 错误响应 + +```json +{ + "code": 1172, + "data": null, + "msg": "该配置存在未完成的支付订单,暂时无法删除", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +### Requirement: 全局唯一激活约束 + +系统 SHALL 保证任意时刻最多一个支付配置处于激活状态。激活新配置时 MUST 自动停用旧配置。 + +--- + +#### Scenario: 激活配置 + +**WHEN** 平台用户调用 `POST /api/admin/wechat-configs/:id/activate` + +**THEN** 系统在事务中执行:将所有 `is_active=true` 的配置设为 `false`,再将目标配置设为 `true` + +**THEN** 系统清除 Redis 缓存 `wechat:config:active` + +**THEN** 系统返回成功,新配置即时生效 + +##### 请求 + +``` +POST /api/admin/wechat-configs/1/activate +Authorization: Bearer {token} +``` + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "is_active": true, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:05:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:05:00+08:00" +} +``` + +##### 配置不存在时的错误响应 + +```json +{ + "code": 1170, + "data": null, + "msg": "微信支付配置不存在", + "timestamp": "2026-03-16T10:05:00+08:00" +} +``` + +--- + +#### Scenario: 停用配置 + +**WHEN** 平台用户调用 `POST /api/admin/wechat-configs/:id/deactivate` + +**THEN** 系统将该配置 `is_active` 设为 `false` + +**THEN** 系统清除 Redis 缓存 `wechat:config:active` + +**THEN** 此后创建订单时仅支持钱包支付或线下支付 + +##### 请求 + +``` +POST /api/admin/wechat-configs/1/deactivate +Authorization: Bearer {token} +``` + +##### 成功响应 + +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "is_active": false, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:10:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:10:00+08:00" +} +``` + +##### 配置不存在时的错误响应 + +```json +{ + "code": 1170, + "data": null, + "msg": "微信支付配置不存在", + "timestamp": "2026-03-16T10:10:00+08:00" +} +``` + +--- + +#### Scenario: 查询当前生效配置 + +**WHEN** 平台用户调用 `GET /api/admin/wechat-configs/active` + +**THEN** 若有生效配置,返回该配置详情(脱敏) + +**THEN** 若无生效配置,`data` 返回 `null` + +##### 请求 + +``` +GET /api/admin/wechat-configs/active +Authorization: Bearer {token} +``` + +##### 有生效配置时的成功响应 + +```json +{ + "code": 0, + "data": { + "id": 1, + "name": "微信直连主配置", + "description": "生产环境微信直连支付配置", + "provider_type": "wechat", + "is_active": true, + "oa_app_id": "wx1234567890abcdef", + "oa_app_secret": "abcd***7890", + "oa_token": "myto***n123", + "oa_aes_key": "[已配置]", + "oa_oauth_redirect_url": "https://example.com/oauth/callback", + "miniapp_app_id": "wx9876543210fedcba", + "miniapp_app_secret": "fedc***4321", + "wx_mch_id": "1234567890", + "wx_api_v3_key": "your***0abc", + "wx_api_v2_key": "your***0abc", + "wx_cert_content": "[已配置]", + "wx_key_content": "[已配置]", + "wx_serial_no": "ABCD***EF12", + "wx_notify_url": "https://example.com/api/payment/wechat/notify", + "fy_ins_cd": "", + "fy_mchnt_cd": "", + "fy_term_id": "", + "fy_private_key": "[未配置]", + "fy_public_key": "[未配置]", + "fy_api_url": "", + "fy_notify_url": "", + "created_at": "2026-03-16T10:00:00+08:00", + "updated_at": "2026-03-16T10:05:00+08:00" + }, + "msg": "success", + "timestamp": "2026-03-16T10:15:00+08:00" +} +``` + +##### 无生效配置时的响应 + +```json +{ + "code": 0, + "data": null, + "msg": "当前无生效的支付配置,仅支持钱包支付", + "timestamp": "2026-03-16T10:15:00+08:00" +} +``` + +--- + +#### Scenario: 配置切换不取消在途订单 + +**WHEN** 管理员激活新配置时,系统中存在使用旧配置创建的待支付订单 + +**THEN** 系统不取消这些订单 + +**THEN** 旧订单若支付成功,回调按订单关联的 `payment_config_id` 加载旧配置验签处理 + +**THEN** 旧订单若未支付,由现有 30 分钟超时机制自动取消 + +此场景无独立 API 接口,为激活接口的业务约束。 + +--- + +### Requirement: 生效配置 Redis 缓存 + +系统 SHALL 将当前生效的支付配置缓存在 Redis 中,减少数据库查询。 + +--- + +#### Scenario: 缓存命中 + +**WHEN** 支付流程查询生效配置,Redis 缓存 `wechat:config:active` 存在 + +**THEN** 直接返回缓存数据,不查询数据库 + +此场景为内部实现约束,无独立 API 接口。 + +--- + +#### Scenario: 缓存未命中 + +**WHEN** 支付流程查询生效配置,Redis 缓存 `wechat:config:active` 不存在 + +**THEN** 查询数据库获取 `is_active=true` 的配置 + +**THEN** 将结果写入 Redis,TTL 为 5 分钟 + +**THEN** 返回配置数据 + +此场景为内部实现约束,无独立 API 接口。 + +--- + +#### Scenario: 无生效配置时缓存空标记 + +**WHEN** 数据库中无 `is_active=true` 的配置 + +**THEN** 在 Redis 写入空标记(值为 `"none"`),TTL 为 1 分钟 + +**THEN** 后续请求命中空标记后直接返回无配置,不穿透数据库 + +此场景为内部实现约束,无独立 API 接口。 + +--- + +#### Scenario: 配置变更主动清除缓存 + +**WHEN** 执行激活、停用、更新生效配置、删除配置操作 + +**THEN** 系统主动 DEL Redis 缓存 `wechat:config:active` + +此场景为内部实现约束,体现在激活、停用、更新、删除接口的副作用中。 + +--- + +### Requirement: 仅平台用户可操作 + +系统 SHALL 限制微信参数配置管理接口仅平台用户(`user_type=1` 超级管理员和 `user_type=2` 平台用户)可访问。路由层中间件统一拦截,无需在 Service 层重复校验。 + +--- + +#### Scenario: 平台用户访问成功 + +**WHEN** 超级管理员(`user_type=1`)或平台用户(`user_type=2`)请求微信参数配置管理接口 + +**THEN** 系统正常处理请求 + +--- + +#### Scenario: 非平台用户拒绝访问 + +**WHEN** 代理账号(`user_type=3`)或企业账号(`user_type=4`)请求微信参数配置管理接口 + +**THEN** 系统返回错误码 `1005`,消息"无权限访问支付配置管理功能" + +##### 错误响应 + +```json +{ + "code": 1005, + "data": null, + "msg": "无权限访问支付配置管理功能", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +#### Scenario: 未登录用户拒绝访问 + +**WHEN** 请求未携带有效 Token 或 Token 已过期 + +**THEN** 系统返回 HTTP 401,错误码 `1002` + +##### 错误响应 + +```json +{ + "code": 1002, + "data": null, + "msg": "无效或已过期的认证令牌", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +--- + +### Requirement: 审计日志 + +系统 SHALL 对所有微信参数配置的写操作(创建、更新、删除、激活、停用)记录操作审计日志,便于追溯配置变更历史。 + +--- + +#### Scenario: 创建配置时记录审计日志 + +**WHEN** 平台用户成功创建微信参数配置 + +**THEN** 系统异步写入审计日志,记录以下信息: + +| 字段 | 值 | +|------|-----| +| operator_id | 当前操作人 ID | +| operator_type | 操作人用户类型 | +| operation_type | `create` | +| operation_desc | `创建微信支付配置:{配置名称}` | +| after_data | 新建配置的完整数据(敏感字段脱敏后存储) | +| request_id | 当前请求 ID | +| ip_address | 操作人 IP | +| user_agent | 操作人 User-Agent | + +**THEN** 审计日志写入失败不影响业务操作,失败时记录 Error 日志 + +--- + +#### Scenario: 更新配置时记录审计日志 + +**WHEN** 平台用户成功更新微信参数配置 + +**THEN** 系统异步写入审计日志,记录以下信息: + +| 字段 | 值 | +|------|-----| +| operator_id | 当前操作人 ID | +| operation_type | `update` | +| operation_desc | `更新微信支付配置:{配置名称}` | +| before_data | 更新前的配置数据(敏感字段脱敏后存储) | +| after_data | 更新后的配置数据(敏感字段脱敏后存储) | + +--- + +#### Scenario: 删除配置时记录审计日志 + +**WHEN** 平台用户成功删除微信参数配置 + +**THEN** 系统异步写入审计日志,记录以下信息: + +| 字段 | 值 | +|------|-----| +| operator_id | 当前操作人 ID | +| operation_type | `delete` | +| operation_desc | `删除微信支付配置:{配置名称}` | +| before_data | 删除前的配置数据(敏感字段脱敏后存储) | + +--- + +#### Scenario: 激活配置时记录审计日志 + +**WHEN** 平台用户成功激活微信参数配置 + +**THEN** 系统异步写入审计日志,记录以下信息: + +| 字段 | 值 | +|------|-----| +| operator_id | 当前操作人 ID | +| operation_type | `activate` | +| operation_desc | `激活微信支付配置:{配置名称},原生效配置:{旧配置名称或"无"}` | +| before_data | 激活前的状态(旧生效配置 ID 和名称) | +| after_data | 激活后的状态(新生效配置 ID 和名称) | + +--- + +#### Scenario: 停用配置时记录审计日志 + +**WHEN** 平台用户成功停用微信参数配置 + +**THEN** 系统异步写入审计日志,记录以下信息: + +| 字段 | 值 | +|------|-----| +| operator_id | 当前操作人 ID | +| operation_type | `deactivate` | +| operation_desc | `停用微信支付配置:{配置名称}` | +| before_data | 停用前的配置状态 | +| after_data | 停用后的配置状态 | diff --git a/openspec/specs/wechat-payment/spec.md b/openspec/specs/wechat-payment/spec.md index 6ca4fbe..c71f3c2 100644 --- a/openspec/specs/wechat-payment/spec.md +++ b/openspec/specs/wechat-payment/spec.md @@ -227,3 +227,185 @@ - **WHEN** 微信支付和公众号使用相同的 AppID - **THEN** 系统复用同一个 Redis Cache 实例 - **THEN** Token 缓存 Key 相同,避免重复获取 + +--- + +## MODIFIED Requirements (from: add-payment-config-management) + +### Requirement: 微信支付配置动态加载 + +微信支付配置 MUST 从数据库动态加载(通过 `tb_wechat_config` 表),替代原有的环境变量静态配置。Payment 实例按需创建,支持请求级 AppID 覆盖(区分公众号和小程序)。 + +#### Scenario: 从数据库加载配置创建 Payment 实例 + +- **WHEN** 支付流程需要使用微信支付 +- **THEN** 系统从 Redis 缓存或数据库加载当前生效的微信参数配置(`is_active=true` 且 `provider_type=wechat`) +- **THEN** 系统使用配置中的 `wx_mch_id`、`wx_api_v3_key`、`wx_cert_content`、`wx_key_content`、`wx_serial_no` 创建 `payment.Payment` 实例 +- **THEN** 证书内容从 Base64 解码后写入临时文件供 PowerWeChat SDK 使用 + +> **本次留桩**:WechatPayJSAPI 和 WechatPayH5 方法保留现有 wechatPayment 单例调用,添加 TODO 注释标记后续替换点。 + +#### Scenario: 无生效微信支付配置时拒绝支付 + +- **WHEN** 系统查询不到 `is_active=true` 的微信参数配置,或生效配置的 `provider_type` 非 `wechat` +- **THEN** 微信支付相关接口返回错误 + +```json +{ + "code": 1175, + "data": null, + "msg": "当前无可用的支付渠道", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 公众号 JSAPI 支付使用公众号 AppID + +``` +POST /api/h5/orders/:id/wechat-pay/jsapi +Authorization: Bearer {token} +Content-Type: application/json +``` + +**请求体** + +```json +{ + "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `openid` | string | ✅ | 用户在公众号下的 OpenID | + +- **THEN** 系统使用配置中的 `oa_app_id`(公众号 AppID)创建支付订单 +- **THEN** Payer OpenID 为用户在该公众号下的 OpenID + +**成功响应 `200 OK`**(本次留桩,返回结构不变) + +```json +{ + "code": 0, + "data": { + "prepay_id": "wx26112221580621e9b071c00d9e093b0000", + "pay_config": { + "appId": "wx1234567890abcdef", + "timeStamp": "1711411341", + "nonceStr": "abc123", + "package": "prepay_id=wx26112221580621e9b071c00d9e093b0000", + "signType": "RSA", + "paySign": "..." + } + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 小程序支付使用小程序 AppID + +- **WHEN** 用户在小程序中发起支付 +- **THEN** 系统在调用 `JSAPITransaction` 时将 AppID 覆盖为配置中的 `miniapp_app_id` +- **THEN** Payer OpenID 为用户在该小程序下的 OpenID + +#### Scenario: 微信 H5 支付 + +``` +POST /api/h5/orders/:id/wechat-pay/h5 +Authorization: Bearer {token} +Content-Type: application/json +``` + +**请求体** + +```json +{ + "scene_info": { + "payer_client_ip": "14.23.150.211", + "h5_info": { + "type": "Wap" + } + } +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `scene_info.payer_client_ip` | string | ✅ | 用户终端 IP | +| `scene_info.h5_info.type` | string | ❌ | 场景类型:`iOS` / `Android` / `Wap` | + +**成功响应 `200 OK`** + +```json +{ + "code": 0, + "data": { + "h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx..." + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 配置缺失时系统正常启动 + +- **WHEN** 系统启动时数据库中无微信参数配置或配置不完整 +- **THEN** 系统正常启动,支付功能降级为仅支持钱包/线下 +- **THEN** 系统记录 WARN 日志"无可用微信参数配置,第三方支付功能不可用" + +--- + +### Requirement: 微信支付回调按配置验签 + +系统 SHALL 接收并处理微信支付成功通知。回调验签 MUST 使用订单关联的支付配置(而非当前生效配置)。 + +#### Scenario: 接收到合法的支付成功通知 + +``` +POST /api/callback/wechat-pay +Content-Type: 由微信服务器决定 +无需认证 +``` + +- **WHEN** 微信回调端点收到支付成功通知 +- **THEN** 系统解析通知中的商户订单号(`out_trade_no`) +- **THEN** 按订单号前缀分发(`ORD` → 套餐订单,`CRCH` → 资产充值,`ARCH` → 代理充值) +- **THEN** 查询对应表记录,通过 `payment_config_id` 加载对应的微信参数配置 +- **THEN** 使用该配置的凭证通过 PowerWeChat SDK 验证回调签名 +- **THEN** 调用对应 Service 的 HandlePaymentCallback +- **THEN** 返回成功响应 + +**成功响应** + +```json +{ + "code": 0, + "data": { + "return_code": "SUCCESS" + }, + "msg": "success", + "timestamp": "2026-03-16T10:00:00+08:00" +} +``` + +#### Scenario: 订单关联的配置已被软删除 + +- **WHEN** 回调到达,但 `payment_config_id` 对应的配置已被软删除 +- **THEN** 系统使用 `GetByIDUnscoped` 加载该配置(软删除不影响回调处理) +- **THEN** 正常完成验签和订单处理 + +#### Scenario: 重复回调幂等处理 + +- **WHEN** 微信多次发送同一订单的支付成功通知 +- **THEN** 系统通过幂等检查识别已支付,直接返回成功响应 + +#### Scenario: 回调签名验证失败 + +- **WHEN** 签名无效或被篡改 +- **THEN** PowerWeChat SDK 自动拒绝,系统记录 ERROR 日志,返回 HTTP 400 + +#### Scenario: 订单号不存在 + +- **WHEN** 回调中的商户订单号在系统中不存在 +- **THEN** 系统记录 ERROR 日志,返回失败响应