docs: 新增 OpenSpec 提案 add-payment-config-management
包含 proposal.md、design.md、tasks.md 及各模块 spec 文件(微信配置管理、富友支付、代理充值、订单支付、资产充值适配、微信支付留桩) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-16
|
||||
364
openspec/changes/add-payment-config-management/design.md
Normal file
364
openspec/changes/add-payment-config-management/design.md
Normal file
@@ -0,0 +1,364 @@
|
||||
## Context
|
||||
|
||||
当前系统通过环境变量配置微信参数(`pkg/config/config.go` 中的 `WechatConfig` 结构体,包含 `OfficialAccountConfig` 和 `PaymentConfig`),在启动时创建全局单例 `PaymentService`,注入到 `order.Service` 中使用。这种模式无法动态切换支付凭证,也不支持富友支付渠道。
|
||||
|
||||
公众号 OAuth 配置同样硬编码在环境变量中,与支付配置相互独立。业务上,一套"微信身份"包含公众号 AppID、小程序 AppID 和支付凭证,三者必须原子性切换,否则会出现 OpenID 与 AppID 不匹配的问题。
|
||||
|
||||
代理充值模块目前只有 Store 层(`internal/store/postgres/agent_recharge_store.go`),缺少 Service 层和 Handler 层,无法通过 API 发起充值。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 在管理后台 CRUD 管理多套微信配置(OAuth + 支付),支持微信直连和富友两种渠道
|
||||
- 全局唯一激活约束:任意时刻最多一个配置生效
|
||||
- 配置切换秒级生效,不影响在途支付
|
||||
- 无生效配置时系统降级为仅支持钱包/线下支付
|
||||
- 接入富友支付的公众号 JSAPI + 小程序支付能力
|
||||
- 支付回调按订单关联的 `payment_config_id` 验签,解决切换期间的竞态问题
|
||||
- 代理充值完整实现 Service/Handler/回调
|
||||
- 所有配置操作记录审计日志
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不支持同时激活多个配置进行负载均衡或路由
|
||||
- 不支持支付宝直连(现有支付宝代码保留不改造)
|
||||
- 不加密存储敏感字段(明文存储,接口返回时脱敏)
|
||||
- 不自动检测配置有效性(如证书过期、商户号封禁)
|
||||
- 不实现富友退款功能(后续按需扩展)
|
||||
- 不动态加载 OAuth 配置(本次只存不用,`OfficialAccountService` 暂时继续从环境变量读取)
|
||||
- 不实现客户端支付发起(留桩,另一个会话讨论)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: 表名和提案名称
|
||||
|
||||
**选择:`tb_wechat_config`(非 `tb_payment_config`)**
|
||||
|
||||
OAuth 配置纳入管理后,每套配置代表一套完整的微信身份(公众号 + 小程序 + 支付),切换配置等于原子性切换一切。用 `payment_config` 命名会遗漏 OAuth 语义,用 `wechat_config` 更准确地反映其职责范围。
|
||||
|
||||
### Decision 2: 扁平字段按前缀分组
|
||||
|
||||
**选择:扁平字段,按前缀分组(`oa_` / `miniapp_` / `wx_` / `fy_`)**
|
||||
|
||||
替代方案为 JSONB 字段存储不同渠道的参数。选择扁平字段的理由:系统使用者为非技术人员,前端表单可直接映射字段,无需 JSON 编辑器;字段级别的 NOT NULL 约束和验证更明确;不适用的字段留空即可(`provider_type=wechat` 时 `fy_*` 字段全部为空)。
|
||||
|
||||
### Decision 3: Payment 实例生命周期管理
|
||||
|
||||
**选择:按需创建 + Service 层缓存,配置变更时清除**
|
||||
|
||||
- 替代方案 A:启动时创建单例(当前方案)—— 无法动态切换
|
||||
- 替代方案 B:每次支付请求都创建新实例 —— 性能浪费
|
||||
- 选择方案:`PaymentConfigService` 维护当前生效配置的 Payment 实例内存缓存,配置切换时清除缓存,下次请求时按新配置重建实例
|
||||
|
||||
**「留桩」期间的过渡说明:**
|
||||
本次实现后,`WechatPayJSAPI`/`WechatPayH5` 方法加 TODO 注释但保留对 `s.wechatPayment` 单例的调用。这意味着在「留桩」期间:
|
||||
- `internal/bootstrap/dependencies.go` 的 `WechatPayment` 字段**暂时保留**
|
||||
- `internal/bootstrap/services.go` 和 `handlers.go` 的 `deps.WechatPayment` 注入**暂时保留**
|
||||
- 等 WechatPayJSAPI/WechatPayH5 完成动态加载改造后,再统一删除上述字段和注入点
|
||||
|
||||
**回调(callback)不属于留桩范围**:任务 1.6.1 的「留桩」仅指验签逻辑,回调的订单分发(按 payment_config_id 路由)必须完整实现,详见 Decision 5。
|
||||
|
||||
### Decision 4: 生效配置缓存策略
|
||||
|
||||
**选择:Redis 缓存 + 主动失效**
|
||||
|
||||
- 缓存 Key:**`wechat:config:active`**(全项目统一使用此命名,spec 文档中任何地方写 `payment:config:active` 均为笔误,以本文档为准),存储完整配置 JSON,TTL 5 分钟
|
||||
- 主动失效:激活/停用/更新/删除配置时主动 DEL 缓存
|
||||
- 读取流程:Redis GET → 命中返回 → MISS 则查 DB → SET 缓存
|
||||
- 空标记:无配置时缓存 `"none"` TTL 1 分钟,防止缓存穿透
|
||||
- Redis Key 定义在 `pkg/constants/redis.go`:`RedisWechatConfigActiveKey()`
|
||||
|
||||
### Decision 5: 配置切换时在途订单处理
|
||||
|
||||
**选择:不取消在途订单,自然完成或过期**
|
||||
|
||||
替代方案为切换时批量取消所有待支付的第三方订单,但用户可能已拉起支付正在输密码,取消订单会导致钱扣了但订单已取消,需要人工退款。
|
||||
|
||||
实现方式:
|
||||
1. `tb_order` 新增 `payment_config_id` 字段(nullable,钱包/线下支付不需要)
|
||||
2. `tb_asset_recharge_record` 新增 `payment_config_id` 字段
|
||||
3. `tb_agent_recharge_record` 新增 `payment_config_id` 字段
|
||||
4. 下单时记录当前使用的配置 ID
|
||||
5. 回调处理时,按 `payment_config_id` 加载对应配置进行验签
|
||||
6. 未支付的旧订单靠现有的 30 分钟超时自动取消机制清理
|
||||
7. 有待支付订单引用的配置不允许删除(软删除后仍可用于回调验签)
|
||||
|
||||
### Decision 6: 富友支付接入方案
|
||||
|
||||
**选择:基于 `wxPreCreate` 接口,支持公众号 JSAPI 和小程序支付**
|
||||
|
||||
- 接口地址:`POST /wxPreCreate`(生产地址从配置读取)
|
||||
- 公众号 JSAPI:`trade_type=JSAPI`,`sub_appid=公众号AppID`,`sub_openid=用户公众号OpenID`
|
||||
- 小程序支付:`trade_type=LETPAY`,`sub_appid=小程序AppID`,`sub_openid=用户小程序OpenID`
|
||||
- 签名算法:RSA + MD5(字典序排列参数 → GBK 编码 → MD5 哈希 → RSA 签名 → Base64)
|
||||
- 通信协议:XML + GBK 编码 + 双重 URL 编码
|
||||
- 回调处理:GBK → UTF-8 转换 → XML 解析 → RSA 验签
|
||||
- 代码组织:`pkg/fuiou/` 包,从 cc-coding 项目移植核心逻辑,适配本项目的日志和错误处理规范
|
||||
|
||||
### Decision 7: 模块分层设计
|
||||
|
||||
**遵循 Handler → Service → Store → Model 分层:**
|
||||
|
||||
```
|
||||
internal/
|
||||
├── model/
|
||||
│ ├── wechat_config.go # WechatConfig 模型 + 常量
|
||||
│ └── dto/
|
||||
│ ├── wechat_config_dto.go # 配置管理 DTO
|
||||
│ └── agent_recharge_dto.go # 代理充值 DTO
|
||||
├── store/postgres/
|
||||
│ ├── wechat_config_store.go # CRUD + 激活/停用
|
||||
│ └── agent_recharge_store.go # 已有,需扩展
|
||||
├── service/
|
||||
│ ├── wechat_config/service.go # 配置管理业务逻辑
|
||||
│ └── agent_recharge/service.go # 代理充值业务逻辑(新建)
|
||||
├── handler/
|
||||
│ ├── admin/wechat_config.go # 配置管理 Handler
|
||||
│ ├── admin/agent_recharge.go # 代理充值 Handler(新建)
|
||||
│ └── callback/payment.go # 改造:支持富友回调 + 按配置验签
|
||||
├── routes/
|
||||
│ ├── wechat_config.go # 配置管理路由
|
||||
│ └── agent_recharge.go # 代理充值路由(新建)
|
||||
└── bootstrap/ # 注册新模块
|
||||
|
||||
pkg/
|
||||
└── fuiou/
|
||||
├── client.go # 富友 HTTP 客户端
|
||||
└── types.go # 请求/响应结构体
|
||||
```
|
||||
|
||||
### Decision 8: 接口脱敏策略
|
||||
|
||||
**敏感字段在 API 响应中脱敏,数据库明文存储:**
|
||||
|
||||
| 字段类型 | 脱敏规则 | 示例 |
|
||||
|---------|---------|------|
|
||||
| Secret/Key(短) | 显示前4后4,中间 `***` | `secr****7890` |
|
||||
| 证书/私钥(长) | 仅显示状态 | `[已配置]` / `[未配置]` |
|
||||
|
||||
更新时:不传或传空字符串 = 不修改,传新值 = 替换。
|
||||
|
||||
### Decision 9: 前端支付方式展示
|
||||
|
||||
富友支付对用户透明,前端统一显示"微信支付"。后端根据生效配置的 `provider_type` 自动路由到微信直连或富友,前端不感知具体支付渠道。
|
||||
|
||||
### Decision 10: OAuth 配置管理策略
|
||||
|
||||
OAuth 字段(公众号 AppID/AppSecret、小程序 AppID/AppSecret)存入 `tb_wechat_config`。本次只存不用——`OfficialAccountService` 暂时继续从环境变量读取。H5/小程序重构时切换为从数据库动态加载。保证切换配置时 OAuth 和支付 AppID 原子性同步。
|
||||
|
||||
**因此,以下代码本次不删除:**
|
||||
- `pkg/config/config.go` 中的 `OfficialAccountConfig` 结构体(`WechatConfig.OfficialAccount` 字段仍需使用)
|
||||
- `pkg/config/defaults/config.yaml` 中的 `wechat.official_account` 配置节(`OfficialAccountService` 仍从中读取)
|
||||
- `cmd/api/main.go` 中 `validateWechatConfig` 里对公众号配置的校验逻辑(仅删除支付相关校验)
|
||||
|
||||
**以下代码本次必须删除(Payment 相关的 YAML 方案完全废弃):**
|
||||
- `pkg/config/config.go` 的 `PaymentConfig` 结构体 + `WechatConfig.Payment` 字段
|
||||
- `pkg/config/defaults/config.yaml` 的 `wechat.payment` 配置节
|
||||
- `pkg/wechat/config.go` 的 `NewPaymentApp(cfg *config.Config, ...)` 函数(从 YAML/CertPath 创建 Payment 实例,被 DB 方案彻底取代)
|
||||
- `cmd/api/main.go` `validateWechatConfig` 中所有 `wechatCfg.Payment.*` 相关校验
|
||||
|
||||
### Decision 11: 代理充值设计
|
||||
|
||||
- 仅充到余额钱包(`wallet_type=main`)
|
||||
- 代理只能充自己店铺,平台可指定任意店铺
|
||||
- 支付方式:`wechat`(在线)/ `offline`(线下,仅平台,需操作密码)
|
||||
- 回调处理完整实现,客户端支付发起留桩
|
||||
|
||||
### Decision 12: Card → Asset 常量重命名
|
||||
|
||||
`pkg/constants/wallet.go` 中 `Card*` 前缀统一改为 `Asset*`,原 `Card*` 常量保留为废弃别名(向后兼容)。
|
||||
|
||||
影响范围:`CardWalletResourceType*`、`CardWalletStatus*`、`CardTransactionType*`、`CardRechargeOrderPrefix`、`CardRechargeMinAmount`、`CardRechargeMaxAmount`。
|
||||
|
||||
## tb_wechat_config 表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE tb_wechat_config (
|
||||
-- 基础字段
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL, -- 配置名称
|
||||
description TEXT, -- 配置描述
|
||||
provider_type VARCHAR(20) NOT NULL, -- 支付渠道: wechat / fuiou
|
||||
is_active BOOLEAN NOT NULL DEFAULT FALSE, -- 是否激活
|
||||
|
||||
-- OAuth 公众号
|
||||
oa_app_id VARCHAR(100) NOT NULL, -- 公众号 AppID
|
||||
oa_app_secret VARCHAR(200) NOT NULL, -- 公众号 AppSecret
|
||||
oa_token VARCHAR(200), -- 消息验证 Token
|
||||
oa_aes_key VARCHAR(200), -- 消息加密 AESKey
|
||||
oa_oauth_redirect_url VARCHAR(500), -- OAuth 回调地址
|
||||
|
||||
-- OAuth 小程序
|
||||
miniapp_app_id VARCHAR(100), -- 小程序 AppID
|
||||
miniapp_app_secret VARCHAR(200), -- 小程序 AppSecret
|
||||
|
||||
-- 支付-微信直连 (provider_type=wechat 时使用)
|
||||
wx_mch_id VARCHAR(100), -- 商户号
|
||||
wx_api_v3_key VARCHAR(200), -- API V3 密钥
|
||||
wx_api_v2_key VARCHAR(200), -- API V2 密钥
|
||||
wx_cert_content TEXT, -- 证书内容 (Base64)
|
||||
wx_key_content TEXT, -- 私钥内容 (Base64)
|
||||
wx_serial_no VARCHAR(200), -- 证书序列号
|
||||
wx_notify_url VARCHAR(500), -- 支付回调地址
|
||||
|
||||
-- 支付-富友 (provider_type=fuiou 时使用)
|
||||
fy_ins_cd VARCHAR(50), -- 机构号
|
||||
fy_mchnt_cd VARCHAR(50), -- 商户号
|
||||
fy_term_id VARCHAR(50), -- 终端号
|
||||
fy_private_key TEXT, -- 商户 RSA 私钥 (Base64)
|
||||
fy_public_key TEXT, -- 富友 RSA 公钥 (Base64)
|
||||
fy_api_url VARCHAR(500), -- API 地址
|
||||
fy_notify_url VARCHAR(500), -- 回调地址
|
||||
|
||||
-- 审计字段
|
||||
creator BIGINT NOT NULL DEFAULT 0, -- 创建人 ID
|
||||
updater BIGINT NOT NULL DEFAULT 0, -- 更新人 ID
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ -- 软删除
|
||||
);
|
||||
|
||||
CREATE INDEX idx_wechat_config_is_active ON tb_wechat_config(is_active) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_wechat_config_provider_type ON tb_wechat_config(provider_type) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
## 流程图
|
||||
|
||||
### 配置切换流程
|
||||
|
||||
```
|
||||
管理员调用 PUT /api/admin/wechat-configs/:id/activate
|
||||
|
|
||||
+--① BEGIN 事务
|
||||
| +-- UPDATE tb_wechat_config SET is_active=false WHERE is_active=true
|
||||
| +-- UPDATE tb_wechat_config SET is_active=true WHERE id=:id
|
||||
+--② COMMIT
|
||||
+--③ DEL Redis "wechat:config:active"
|
||||
+--④ 清除内存中的 Payment 实例缓存
|
||||
+--⑤ 记录审计日志
|
||||
|
|
||||
+-- 新订单 --> 使用新配置
|
||||
+-- 旧订单(待支付) --> 回调时按 payment_config_id 加载旧配置验签
|
||||
+-- 30分钟超时自动取消
|
||||
```
|
||||
|
||||
### 改造后的第三方支付流程(两步走)
|
||||
|
||||
```
|
||||
步骤1: H5 创建订单
|
||||
POST /api/h5/orders
|
||||
+-- payment_method = "wechat"
|
||||
+-- 系统查询 active 配置
|
||||
+-- IF 有配置 --> 记录 payment_config_id 到订单
|
||||
+-- 返回 order (payment_status=1 待支付)
|
||||
|
||||
步骤2: 发起微信支付(留桩)
|
||||
POST /api/h5/orders/:id/wechat-pay/jsapi
|
||||
+-- 加载 order.payment_config_id 对应配置
|
||||
+-- 按 provider_type 分发:
|
||||
| +-- wechat --> PaymentService.CreateJSAPIOrder()
|
||||
| +-- fuiou --> FuiouClient.WxPreCreate()
|
||||
+-- 返回支付参数给前端(本次留桩)
|
||||
|
||||
步骤3: 支付回调
|
||||
POST /api/callback/wechat-pay 或 /api/callback/fuiou-pay
|
||||
+-- 解析订单号
|
||||
+-- 按订单号前缀分发:
|
||||
| +-- "ORD" --> 套餐订单 --> 查 tb_order
|
||||
| +-- "CRCH" --> 资产充值 --> 查 tb_asset_recharge_record
|
||||
| +-- "ARCH" --> 代理充值 --> 查 tb_agent_recharge_record
|
||||
+-- 按 payment_config_id 加载配置
|
||||
+-- 用对应凭证验签
|
||||
+-- 调用对应 Service.HandlePaymentCallback()
|
||||
```
|
||||
|
||||
### 代理预充值流程
|
||||
|
||||
```
|
||||
代理/平台 --> POST /api/admin/agent-recharges
|
||||
|
|
||||
+-- 验证权限: 代理只能充自己店铺,平台可指定店铺
|
||||
+-- 验证金额范围 (100元~100万元)
|
||||
+-- 查找目标店铺的 main 钱包
|
||||
|
|
||||
+-- IF payment_method = "wechat"
|
||||
| +-- 查询 active 配置 --> 无配置则拒绝
|
||||
| +-- 记录 payment_config_id
|
||||
| +-- 创建充值订单 (status=1 待支付)
|
||||
| +-- 返回订单信息(客户端支付发起留桩)
|
||||
|
|
||||
+-- IF payment_method = "offline"
|
||||
+-- 验证是否平台账号 --> 非平台拒绝
|
||||
+-- 返回订单信息(status=1 待支付,等待线下确认)
|
||||
|
||||
平台确认线下充值 --> POST /api/admin/agent-recharges/:id/offline-pay
|
||||
+-- 验证操作密码
|
||||
+-- 事务内:
|
||||
| +-- 更新充值订单状态 (status=2 已支付)
|
||||
| +-- 增加余额钱包余额(乐观锁)
|
||||
| +-- 创建钱包交易记录
|
||||
+-- 记录审计日志
|
||||
```
|
||||
|
||||
### 回调统一分发流程
|
||||
|
||||
```
|
||||
回调到达
|
||||
|
|
||||
+-- 微信回调 POST /api/callback/wechat-pay
|
||||
| +-- PowerWeChat SDK 解析 + 取 out_trade_no
|
||||
|
|
||||
+-- 富友回调 POST /api/callback/fuiou-pay
|
||||
| +-- GBK->UTF-8 --> XML解析 --> 取 mchnt_order_no
|
||||
|
|
||||
+-- 按订单号前缀分发
|
||||
|
|
||||
+-- "ORD" --> 套餐订单
|
||||
| +-- 查询 tb_order --> 取 payment_config_id
|
||||
| +-- 加载配置验签
|
||||
| +-- orderService.HandlePaymentCallback()
|
||||
|
|
||||
+-- "CRCH" --> 资产充值(修复:当前代码用废弃的 "RCH")
|
||||
| +-- 查询 tb_asset_recharge_record --> 取 payment_config_id
|
||||
| +-- 加载配置验签
|
||||
| +-- rechargeService.HandlePaymentCallback()
|
||||
|
|
||||
+-- "ARCH" --> 代理充值(全新)
|
||||
+-- 查询 tb_agent_recharge_record --> 取 payment_config_id
|
||||
+-- 加载配置验签
|
||||
+-- agentRechargeService.HandlePaymentCallback()
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 配置切换后旧回调验签失败 | 用户付了钱但订单未处理 | 订单记录 `payment_config_id`,回调按该字段加载配置验签 |
|
||||
| Redis 缓存与 DB 不一致 | 短暂使用旧配置 | TTL 5 分钟兜底 + 切换时主动失效 |
|
||||
| 富友 SDK 是自研非开源 | 维护成本 | 从已验证的 cc-coding 项目移植,核心逻辑已线上运行 |
|
||||
| 证书 Base64 存 DB 体量大 | 单行数据量增大 | 证书文件通常 2-4KB,Base64 后约 3-6KB,可接受 |
|
||||
| 多实例部署缓存一致性 | 某实例使用旧缓存 | Redis 是共享的,DEL 操作对所有实例生效;内存缓存通过 Redis MISS 时重建 |
|
||||
| Card→Asset 常量重命名 | 引用处需要更新 | 旧常量保留为废弃别名,渐进迁移 |
|
||||
| OAuth 只存不用 | 数据在但不生效 | 明确标注为预留,H5 重构时切换 |
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. **数据库迁移**:新建 `tb_wechat_config` 表 + `tb_order`、`tb_asset_recharge_record`、`tb_agent_recharge_record` 各新增 `payment_config_id` 列(nullable,不影响存量数据)
|
||||
2. **常量重命名**:`Card*` → `Asset*`,旧名保留为废弃别名
|
||||
3. **删除 YAML 支付配置基础设施**(以下操作必须作为独立任务明确执行,不可遗漏):
|
||||
- `pkg/config/config.go`:删除 `PaymentConfig` 结构体、删除 `WechatConfig.Payment` 字段
|
||||
- `pkg/config/defaults/config.yaml`:删除 `wechat.payment:` 整个配置节(保留 `wechat.official_account:` 节,OAuth 继续使用)
|
||||
- `pkg/wechat/config.go`:删除 `NewPaymentApp(cfg *config.Config, ...)` 函数(从文件路径创建 Payment 的方式已被 DB Base64 方案取代)
|
||||
- `cmd/api/main.go`:删除 `validateWechatConfig` 中所有 `wechatCfg.Payment.*` 相关的校验代码(保留公众号校验部分)
|
||||
4. **代码部署**:新代码兼容无配置场景(无生效配置 = 降级为纯钱包支付)
|
||||
5. **配置迁移**:将当前环境变量中的微信参数手动录入为第一个配置并激活
|
||||
6. **回滚策略**:回滚代码后,支付流程回退到读取环境变量的单例模式(若 `deps.WechatPayment` 仍存在),新表数据不影响旧逻辑
|
||||
|
||||
## Closed Questions
|
||||
|
||||
- ~~富友支付是否需要退款?~~ → 暂不包含(Non-Goals 已声明)
|
||||
- ~~是否需要配置变更审计日志?~~ → 必须纳入,复用 `AuditServiceInterface`
|
||||
- ~~充值模块是否需要改造?~~ → 全部纳入,资产充值 + 代理充值都加 `payment_config_id`
|
||||
- ~~支付宝如何处理?~~ → 保留不改造
|
||||
- ~~OpenID 与 AppID 一致性?~~ → OAuth 字段纳入同一配置,切换配置时原子性同步
|
||||
61
openspec/changes/add-payment-config-management/proposal.md
Normal file
61
openspec/changes/add-payment-config-management/proposal.md
Normal file
@@ -0,0 +1,61 @@
|
||||
## Why
|
||||
|
||||
当前微信相关参数(公众号 OAuth、小程序、支付凭证)硬编码在环境变量中,只有一套配置,无法动态切换。业务上,微信公众号/小程序随时可能被封禁,需要在管理后台**秒级切换**到备用配置恢复 OAuth 登录和支付能力。同时需要接入富友支付作为备选通道,降低对微信直连的单一依赖。此外,代理预充值(余额钱包在线充值)当前只有 Store 层骨架,缺少完整的 Service/Handler/回调处理,需要补齐。
|
||||
|
||||
## What Changes
|
||||
|
||||
本提案包含**两个目标**,按顺序实施:
|
||||
|
||||
### Goal 1:微信参数配置管理 + 支付流程改造
|
||||
|
||||
- **新增微信参数配置管理模块**:支持在管理后台 CRUD 管理多套微信参数配置,每套配置 = 完整的"微信身份"(OAuth 公众号 + OAuth 小程序 + 支付凭证),支持全局唯一激活
|
||||
- **新建 `tb_wechat_config` 表**:扁平字段存储 OAuth 参数(公众号 AppID/AppSecret、小程序 AppID/AppSecret)、微信直连支付参数、富友支付参数,支持多配置共存
|
||||
- **新增富友支付 SDK**:基于富友 `wxPreCreate` 接口实现公众号 JSAPI 和小程序支付,移植 cc-coding 项目的 RSA 签名、XML 编解码、GBK 转换等核心逻辑
|
||||
- **改造订单支付流程**:订单创建时从数据库/Redis 动态加载当前生效配置记录 `payment_config_id`;支付发起(WechatPayJSAPI/WechatPayH5)本次**留桩**,添加 TODO 标记,保留单例调用——等下一阶段实现动态加载;同时删除启动时基于 YAML 创建 PaymentService 单例的相关代码
|
||||
- **订单关联配置**:`tb_order` 新增 `payment_config_id` 字段,回调按该字段加载对应配置验签,解决配置切换期间在途支付的竞态问题
|
||||
- **资产充值模块适配**:`tb_asset_recharge_record` 新增 `payment_config_id` 字段,充值回调按配置验签
|
||||
- **常量重命名**:`pkg/constants/wallet.go` 中 `Card*` 前缀常量统一重命名为 `Asset*`,与模型层命名一致
|
||||
- **配置切换安全策略**:切换配置时不取消在途订单,旧订单按原配置自然完成或超时过期;无生效配置时只允许钱包/线下支付
|
||||
- **仅平台用户可操作**:通过路由层中间件限制仅平台用户访问
|
||||
- **审计日志**:所有 CRUD 和激活/停用操作记录审计日志,复用现有 AuditServiceInterface
|
||||
|
||||
> **注意**:OAuth 配置字段本次只**存储和管理**(数据库 + 管理界面),OfficialAccountService 的动态加载改造留待 H5/小程序重构时实施。客户端支付发起(调用第三方获取拉起支付参数)本次**留桩不实现**,在另一个 session 讨论。
|
||||
|
||||
### Goal 2:代理预充值系统
|
||||
|
||||
- **新增代理余额钱包在线充值**:完整的 Service + Handler + 回调处理,支持微信在线支付和线下充值
|
||||
- **代理只能选择微信支付**:后端根据当前生效配置自动路由到微信直连或富友,对代理透明
|
||||
- **线下充值仅平台操作**:需要操作密码二次验证
|
||||
- **`tb_agent_recharge_record` 新增 `payment_config_id` 字段**
|
||||
- **充值目标**:仅充到余额钱包(`wallet_type=main`),佣金钱包通过分佣自动入账
|
||||
|
||||
> **注意**:代理充值的客户端支付发起(调用第三方获取拉起参数)同样**留桩不实现**。回调处理完整实现(解析第三方支付成功通知)。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `wechat-config-management`:微信参数配置的 CRUD 管理、激活/停用、全局唯一激活约束、Redis 缓存、接口脱敏、审计日志
|
||||
- `fuiou-payment`:富友支付集成,包括 wxPreCreate 下单(公众号 JSAPI + 小程序)、支付回调验签处理、RSA 签名/XML 编解码
|
||||
- `agent-recharge`:代理余额钱包在线充值,创建充值订单、线下充值确认、回调处理、钱包余额更新
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `wechat-payment`:配置来源从环境变量改为数据库动态加载;Payment 实例从启动时单例改为按需创建(留桩)
|
||||
- `order-payment`:订单新增 `payment_config_id` 字段;下单时无生效配置则拒绝第三方支付;回调处理按 `payment_config_id` 加载对应配置验签
|
||||
- `asset-recharge`:充值表新增 `payment_config_id` 字段;回调处理按配置验签;修复 `RechargeOrderPrefix` 废弃问题(当前用 `"RCH"`,实际前缀是 `"CRCH"`)
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增文件**:Model(`wechat_config.go`)、DTO、Store、Service、Handler、迁移文件、富友 SDK 包(`pkg/fuiou/`)、常量、错误码、代理充值 Service/Handler
|
||||
- **修改文件**:`internal/model/order.go`(新增字段)、`internal/model/asset_wallet.go`(新增字段)、`internal/model/agent_wallet.go`(新增字段)、`internal/service/order/service.go`(动态加载配置 + 注入 wechatConfigService)、`internal/handler/callback/payment.go`(支持富友回调 + 按配置验签 + 修复前缀匹配)、`pkg/constants/wallet.go`(Card→Asset 重命名)、`internal/bootstrap/`(注册新模块)、`internal/routes/`(注册新路由)、`cmd/api/docs.go` + `cmd/gendocs/main.go`(文档生成器)
|
||||
- **删除/精简文件**(YAML 支付方案遗留代码,必须清理):
|
||||
- `pkg/config/config.go`:删除 `PaymentConfig` 结构体 + `WechatConfig.Payment` 字段(约 15 行),保留 `OfficialAccountConfig` 结构体
|
||||
- `pkg/config/defaults/config.yaml`:删除 `wechat.payment:` 配置节(约 10 行),保留 `wechat.official_account:` 节
|
||||
- `pkg/wechat/config.go`:删除 `NewPaymentApp(cfg *config.Config, ...)` 函数(整个函数,约 30 行),保留 `NewOfficialAccountApp`
|
||||
- `cmd/api/main.go`:从 `validateWechatConfig` 中删除所有 `wechatCfg.Payment.*` 校验代码(约 40 行),保留公众号校验部分
|
||||
- **新增 API 路由**:`/api/admin/wechat-configs/*`(8 个端点)、`/api/admin/agent-recharges/*`(4 个端点)、`/api/callback/fuiou-pay`(富友回调)
|
||||
- **数据库变更**:新建 `tb_wechat_config` 表、`tb_order` 新增 `payment_config_id` 列、`tb_asset_recharge_record` 新增 `payment_config_id` 列、`tb_agent_recharge_record` 新增 `payment_config_id` 列
|
||||
- **新增依赖**:`golang.org/x/text`(GBK 编解码,富友支付需要)
|
||||
- **保留不改造**:支付宝支付(AlipayCallback、PaymentMethodAlipay 常量保持原样不动);`OfficialAccountService` 及其 YAML 配置(OAuth 本次只存不用);`WechatPayment` 单例注入(留桩期间保留,等支付发起动态化后再移除)
|
||||
- **性能考虑**:生效配置使用 Redis 缓存(TTL 5 分钟,Key:`wechat:config:active`),避免每次支付查 DB;配置切换时主动清缓存保证即时生效
|
||||
@@ -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 | 线下转账 |
|
||||
@@ -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` 注释。段落标题 `卡钱包常量` → `资产钱包常量`。
|
||||
@@ -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
|
||||
<?xml version="1.0" encoding="GBK"?>
|
||||
<xml>
|
||||
<result_code>000000</result_code>
|
||||
<result_msg>success</result_msg>
|
||||
</xml>
|
||||
```
|
||||
|
||||
#### Scenario: 回调签名验证失败
|
||||
|
||||
- **WHEN** 富友回调的 RSA 签名与本地计算不匹配
|
||||
- **THEN** 系统记录 ERROR 日志
|
||||
- **THEN** 返回失败 XML 响应
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="GBK"?>
|
||||
<xml>
|
||||
<result_code>999999</result_code>
|
||||
<result_msg>signature verification failed</result_msg>
|
||||
</xml>
|
||||
```
|
||||
|
||||
#### 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=<encoded_xml>` 的 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 验签
|
||||
@@ -0,0 +1,184 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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
|
||||
<?xml version="1.0" encoding="GBK"?>
|
||||
<xml>
|
||||
<result_code>000000</result_code>
|
||||
<result_msg>success</result_msg>
|
||||
</xml>
|
||||
```
|
||||
|
||||
#### 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 分钟超时机制自动取消
|
||||
@@ -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 | 停用后的配置状态 |
|
||||
@@ -0,0 +1,179 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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 日志,返回失败响应
|
||||
115
openspec/changes/add-payment-config-management/tasks.md
Normal file
115
openspec/changes/add-payment-config-management/tasks.md
Normal file
@@ -0,0 +1,115 @@
|
||||
## Goal 1:微信参数配置管理 + 支付流程改造
|
||||
|
||||
### 1.1 前置准备:常量重命名
|
||||
|
||||
- [x] 1.1.1 `pkg/constants/wallet.go`:将 `Card*` 前缀常量重命名为 `Asset*`(CardWalletResourceType* → AssetWalletResourceType*、CardWalletStatus* → AssetWalletStatus*、CardTransactionType* → AssetTransactionType*、CardRechargeOrderPrefix → AssetRechargeOrderPrefix、CardRechargeMinAmount → AssetRechargeMinAmount、CardRechargeMaxAmount → AssetRechargeMaxAmount),段落标题 `卡钱包常量` → `资产钱包常量`
|
||||
- [x] 1.1.2 旧 `Card*` 常量保留为废弃别名(`const CardRechargeOrderPrefix = AssetRechargeOrderPrefix`),更新废弃注释中的引用
|
||||
- [x] 1.1.3 全局替换引用:将所有使用 `Card*` 常量的代码替换为 `Asset*`
|
||||
|
||||
### 1.1b 删除 YAML 支付配置遗留代码
|
||||
|
||||
> **必须在 1.3 之前完成**:这些是原有方案的残留,新方案部署后若不删除,配置和行为会产生歧义。
|
||||
|
||||
- [x] 1.1b.1 修改 `pkg/config/config.go`:删除 `PaymentConfig` 结构体(整体删除,含 `AppID`/`MchID`/`APIV3Key`/`APIV2Key`/`CertPath`/`KeyPath`/`SerialNo`/`NotifyURL`/`HttpDebug`/`Timeout` 所有字段);删除 `WechatConfig.Payment PaymentConfig` 字段;删除对应注释
|
||||
- [x] 1.1b.2 修改 `pkg/config/defaults/config.yaml`:删除 `wechat.payment:` 整个配置节(约 10 行),**保留** `wechat.official_account:` 节不变(OAuth 仍使用 YAML 配置)
|
||||
- [x] 1.1b.3 修改 `pkg/wechat/config.go`:删除 `NewPaymentApp(cfg *config.Config, ...)` 函数(从 YAML CertPath/KeyPath 文件路径创建 Payment 实例的方式已被 DB Base64 方案完全取代)
|
||||
- [x] 1.1b.4 修改 `cmd/api/main.go`:从 `validateWechatConfig` 中删除所有 `wechatCfg.Payment.*` 相关校验代码(包括对 `CertPath`/`KeyPath` 的 `os.Stat` 检查、缺失字段的 `appLogger.Fatal`),**保留**对 `wechatCfg.OfficialAccount` 的校验不变
|
||||
- [x] 1.1b.5 确认编译通过:删除后运行 `go build ./...` 确保无编译错误(此时 `wechatPayment` 相关代码仍保留,因为留桩期间仍在用单例)
|
||||
|
||||
### 1.2 数据库与基础模型
|
||||
|
||||
- [x] 1.2.1 创建数据库迁移文件:新建 `tb_wechat_config` 表(基础字段、OAuth 公众号字段、OAuth 小程序字段、微信直连支付字段、富友支付字段),`is_active` 默认 `false`
|
||||
- [x] 1.2.2 创建数据库迁移文件:`tb_order` 新增 `payment_config_id` 列(bigint, nullable, 带索引)
|
||||
- [x] 1.2.3 创建数据库迁移文件:`tb_asset_recharge_record` 新增 `payment_config_id` 列(bigint, nullable, 带索引)
|
||||
- [ ] 1.2.4 执行迁移,确认表结构正确
|
||||
- [x] 1.2.5 创建 `internal/model/wechat_config.go`:WechatConfig 模型(GORM 标签、TableName、渠道类型常量 `ProviderTypeWechat` / `ProviderTypeFuiou`)
|
||||
- [x] 1.2.6 修改 `internal/model/order.go`:Order 模型新增 `PaymentConfigID *uint` 字段
|
||||
- [x] 1.2.7 修改 `internal/model/asset_wallet.go`:AssetRechargeRecord 模型新增 `PaymentConfigID *uint` 字段
|
||||
- [x] 1.2.8 创建 `internal/model/dto/wechat_config_dto.go`:请求 DTO(Create/Update/List)、响应 DTO(含脱敏逻辑方法),详细字段定义见 spec
|
||||
- [x] 1.2.9 在 `pkg/constants/redis.go` 新增 `RedisWechatConfigActiveKey()` 函数
|
||||
- [x] 1.2.10 在 `pkg/errors/codes.go` 新增错误码:`CodeWechatConfigNotFound=1170`、`CodeWechatConfigActive=1171`、`CodeWechatConfigHasPendingOrders=1172`、`CodeFuiouPayFailed=1173`、`CodeFuiouCallbackInvalid=1174`、`CodeNoPaymentConfig=1175`
|
||||
|
||||
### 1.3 微信参数配置 CRUD(Store + Service + Handler)
|
||||
|
||||
- [x] 1.3.1 创建 `internal/store/postgres/wechat_config_store.go`:实现 Create、GetByID、GetByIDUnscoped(含软删除)、List(分页+筛选)、Update、SoftDelete、GetActive、ActivateInTx(事务内停用所有+激活指定)、Deactivate、CountPendingOrdersByConfigID、CountPendingRechargesByConfigID
|
||||
- [x] 1.3.2 创建 `internal/service/wechat_config/service.go`:实现 CRUD 业务逻辑,包含按 provider_type 校验必填字段、激活/停用(含 Redis 缓存清除)、删除保护(检查激活状态 + 在途订单 + 在途充值)、GetActiveConfig(Redis 缓存 + DB 回源 + 空标记)、更新脱敏字段处理、审计日志记录
|
||||
- [x] 1.3.3 创建 `internal/handler/admin/wechat_config.go`:实现 Create、List、Get、Update、Delete、Activate、Deactivate、GetActive 共 8 个 Handler 方法
|
||||
- [x] 1.3.4 创建 `internal/routes/wechat_config.go`:注册路由到 `/api/admin/wechat-configs/*`,包含平台用户权限中间件
|
||||
- [x] 1.3.5 更新 `internal/bootstrap/`(types.go、stores.go、services.go、handlers.go):注册 WechatConfigStore、WechatConfigService、WechatConfigHandler
|
||||
- [x] 1.3.6 更新 `cmd/api/docs.go` 和 `cmd/gendocs/main.go`:注册 WechatConfigHandler 到文档生成器
|
||||
|
||||
### 1.4 富友支付 SDK
|
||||
|
||||
- [x] 1.4.1 创建 `pkg/fuiou/types.go`:定义 WxPreCreateRequest/Response、NotifyRequest 等 XML 结构体
|
||||
- [x] 1.4.2 创建 `pkg/fuiou/client.go`:实现 Client 结构体(持有配置和 RSA 密钥对)、NewClient(从 WechatConfig 模型构造)、签名算法(字典序 → GBK → MD5 → RSA → Base64)、验签算法、HTTP 请求(XML + GBK + 双 URL 编码)、响应解码(URL 解码 → GBK→UTF-8 → XML 解析)
|
||||
- [x] 1.4.3 创建 `pkg/fuiou/wxprecreate.go`:实现 WxPreCreate 方法(公众号 JSAPI + 小程序支付下单),支持 trade_type(JSAPI / LETPAY)和 sub_appid / sub_openid 参数
|
||||
- [x] 1.4.4 创建 `pkg/fuiou/notify.go`:实现 VerifyNotify 方法(GBK→UTF-8 + XML 解析 + RSA 验签),BuildNotifyResponse 成功/失败响应构建
|
||||
- [x] 1.4.5 在 `go.mod` 添加 `golang.org/x/text` 依赖(GBK 编解码)
|
||||
|
||||
### 1.5 订单支付流程改造
|
||||
|
||||
- [x] 1.5.1 改造 `internal/service/order/service.go` 的 `CreateH5Order` 和 `CreateAdminOrder`:注入 `wechatConfigService`(新增字段),下单时查询 active 配置 → 无配置则拒绝第三方支付 → 有配置则记录 `payment_config_id` 到订单
|
||||
- [x] 1.5.2 改造 `WechatPayJSAPI` 方法(**留桩**):添加 TODO 注释 `// TODO: 从 payment_config_id 加载配置动态创建 Payment 实例`,本次保留现有 `s.wechatPayment` 单例调用不变;同时在构造函数中保留 `wechatPayment` 参数(留桩期间仍需注入)
|
||||
- [x] 1.5.3 改造 `WechatPayH5` 方法(**留桩**):同 1.5.2,添加 TODO 注释,保留 `s.wechatPayment` 调用
|
||||
- [x] 1.5.4 新增富友支付发起方法桩:`FuiouPayJSAPI`(返回 "富友支付发起暂未实现" 错误)和 `FuiouPayMiniApp`(同上),标记 TODO
|
||||
|
||||
> **留桩期间的 Bootstrap 注入**:任务 1.5.2/1.5.3 的留桩意味着 `internal/bootstrap/dependencies.go` 的 `WechatPayment` 字段、`services.go` 和 `handlers.go` 的 `deps.WechatPayment` 注入**暂时不改动**。当 WechatPayJSAPI/WechatPayH5 完成动态加载改造(留桩解除)后,再删除 `WechatPayment` 字段和所有注入点。
|
||||
|
||||
### 1.6 回调处理改造
|
||||
|
||||
- [x] 1.6.1 改造 `internal/handler/callback/payment.go` 的 `WechatPayCallback`:解析订单号 → 按前缀分发(`ORD`/`CRCH`/`ARCH`)→ 查询对应表取 `payment_config_id` → 按配置加载验签(本次留桩:验签仍用现有 `wechatPayment` 单例,添加 TODO `// TODO: 按 payment_config_id 加载配置验签`);**订单分发逻辑必须完整实现**(不是留桩),三种订单类型必须全部支持
|
||||
- [x] 1.6.2 修复回调中的前缀匹配:将 `constants.RechargeOrderPrefix("RCH")` 替换为分别匹配 `constants.AssetRechargeOrderPrefix("CRCH")` 和 `constants.AgentRechargeOrderPrefix("ARCH")`;同时修复 `AlipayCallback` 中同样使用 `RechargeOrderPrefix` 的问题(第 84 行)
|
||||
- [x] 1.6.3 新增 `FuiouPayCallback` Handler:接收富友回调 → 解析 XML(GBK→UTF-8)→ 按订单号前缀分发 → 查询对应记录 → 加载配置 → 验签 → 调用对应 Service.HandlePaymentCallback → 返回 XML 响应
|
||||
- [x] 1.6.4 在 `internal/routes/order.go` 注册富友回调路由 `POST /api/callback/fuiou-pay`(无需认证)
|
||||
- [x] 1.6.5 更新 `internal/bootstrap/handlers.go`:`NewPaymentHandler` 新增 `agentRechargeService` 参数(用于 `ARCH` 前缀分发);`WechatPayment` 参数留桩期间保留
|
||||
|
||||
### 1.7 资产充值模块适配
|
||||
|
||||
- [x] 1.7.1 改造 `internal/service/recharge/service.go` 的 `Create` 方法:创建充值订单时查询 active 配置,记录 `payment_config_id`
|
||||
- [x] 1.7.2 改造 `internal/service/recharge/service.go` 的 `HandlePaymentCallback` 方法:回调时按 `payment_config_id` 加载配置验签(留桩)
|
||||
|
||||
### 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`
|
||||
|
||||
---
|
||||
|
||||
## Goal 2:代理预充值系统
|
||||
|
||||
### 2.1 数据库与模型
|
||||
|
||||
- [x] 2.1.1 创建数据库迁移文件:`tb_agent_recharge_record` 新增 `payment_config_id` 列(bigint, nullable, 带索引)
|
||||
- [x] 2.1.2 修改 `internal/model/agent_wallet.go`:AgentRechargeRecord 模型新增 `PaymentConfigID *uint` 字段
|
||||
- [x] 2.1.3 创建 `internal/model/dto/agent_recharge_dto.go`:CreateAgentRechargeRequest、AgentOfflinePayRequest、AgentRechargeResponse、AgentRechargeListRequest、AgentRechargeListResponse,详细字段定义见 spec
|
||||
|
||||
### 2.2 代理充值 Service
|
||||
|
||||
- [x] 2.2.1 创建 `internal/service/agent_recharge/service.go`:
|
||||
- `Create`:验证权限(代理只能充自己店铺,平台可指定)→ 验证金额范围 → 查找 main 钱包 → 查询 active 配置(wechat 时必须有)→ 创建充值订单 → 记录 payment_config_id
|
||||
- `OfflinePay`:验证平台权限 → 验证操作密码 → 事务内更新订单状态 + 增加钱包余额(乐观锁)+ 创建交易记录 → 审计日志
|
||||
- `HandlePaymentCallback`:幂等检查 → 按 payment_config_id 验签 → 事务内更新订单状态 + 增加余额 + 创建交易记录
|
||||
- `GetByID`、`List`:查询充值订单
|
||||
|
||||
### 2.3 代理充值 Handler + 路由
|
||||
|
||||
- [x] 2.3.1 创建 `internal/handler/admin/agent_recharge.go`:实现 Create、List、Get、OfflinePay 共 4 个 Handler 方法
|
||||
- [x] 2.3.2 创建 `internal/routes/agent_recharge.go`:注册路由到 `/api/admin/agent-recharges/*`
|
||||
- [x] 2.3.3 更新 `internal/bootstrap/`(types.go、stores.go、services.go、handlers.go):注册 AgentRechargeService、AgentRechargeHandler
|
||||
- [x] 2.3.4 更新 `cmd/api/docs.go` 和 `cmd/gendocs/main.go`:注册 AgentRechargeHandler 到文档生成器
|
||||
|
||||
### 2.4 代理充值回调集成
|
||||
|
||||
- [x] 2.4.1 在回调 Handler(1.6.2 已完成的前缀分发逻辑)中接入代理充值回调:`"ARCH"` 前缀 → `agentRechargeService.HandlePaymentCallback()`
|
||||
- [x] 2.4.2 确认富友回调 Handler 也支持 `"ARCH"` 前缀分发
|
||||
|
||||
### 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`
|
||||
Reference in New Issue
Block a user