This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
58
openspec/changes/add-payment-merchant-pools/design.md
Normal file
58
openspec/changes/add-payment-merchant-pools/design.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## Context
|
||||
|
||||
现有 `tb_wechat_config` 同时承担支付渠道配置;订单、充值和 `tb_payment` 通过 `payment_config_id` 供回调加载创建时配置。`tb_payment` 已有非敏感 `merchant_identity` 快照,但不足以区分商户池、服务商和完整路由。新模型必须让新单和旧单分流,不能把历史订单指向迁移后新商户。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 独立实体与不可变路由快照
|
||||
新增商户、商户池、池成员、微信授权配置及金额/笔数统计事实;支付单扩展实际商户和商户池 ID 及非敏感快照。商户凭证只留在商户表受控字段,支付单/审计不复制。被引用后锁定商户支付方式、服务商与身份,避免历史验签与退款语义漂移。
|
||||
|
||||
### 路由和统计在支付创建/成功边界闭合
|
||||
支付创建在事务内读取唯一启用池、按方式选择成员并冻结路由;无成员/池失败即返回。金额/笔数统计只在现有“支付成功首次生效”路径按实际商户条件递增,以支付 ID 唯一约束避免重复回调累计;不在预下单预占。时间方式由当前时间和受控起点计算,不写逐次路由日志。
|
||||
|
||||
### 新旧配置双读切换
|
||||
新支付单具有 merchant ID 时,支付加载、回调验签、查询和退款均从商户加载服务商凭证;merchant ID 为空的历史单保持现有 `payment_config_id` 路径。迁移先复制生效配置中完整凭证,再发布新支付路径;不可将现有记录批量回填为新商户,因为其实际历史身份无法保证一致。
|
||||
|
||||
### 敏感配置边界
|
||||
后台管理响应按 PRD 向两类已认证管理角色完整返回凭证;所有 logger、错误、审计 payload、支付单快照和导出只允许写 ID、名称和脱敏/非敏感身份。更新凭证后使现有配置加载缓存失效;历史回调读取最新有效凭证以支持轮换。
|
||||
|
||||
## 管理与支付动作契约
|
||||
|
||||
以下路径为本 Change 新增后台管理契约;所有管理写操作仅限超级管理员和平台用户,金额均为分,敏感凭证只在已认证管理请求的写入/详情响应中传输,绝不进入支付快照、审计、日志或导出。
|
||||
|
||||
### 商户
|
||||
|
||||
- `POST /payment-merchants`:请求 `name`、`payment_method`(`wechat`/`alipay`)、`provider_type`、`merchant_identity`、`credentials`、`enabled`、`remark`。同一支付方式下身份标识不得重复;凭证缺失或与服务商类型不匹配时拒绝。
|
||||
- `PUT /payment-merchants/:id`:未被任何支付单引用时可修改全部字段;被引用后仅可改名称、凭证、启停、备注,修改支付方式、服务商类型或商户身份返回“已被支付单引用,不能修改收款身份”。
|
||||
- `DELETE /payment-merchants/:id`:被引用或仍属于任一商户池时拒绝;删除前必须显式二次确认。停用不影响已冻结该商户的查单、验签和退款。
|
||||
|
||||
### 商户池
|
||||
|
||||
- `POST /payment-merchant-pools`:请求 `name`、`payment_method`、`enabled`、`strategy`(金额/笔数/时间)、策略参数和有序 `member_ids`。成员均须存在、启用、与池支付方式一致且不重复;同一支付方式最多一个启用池,冲突返回“该支付方式已有启用商户池”。
|
||||
- `PUT /payment-merchant-pools/:id`:修改阈值只保留当前统计;修改金额/笔数统计周期、时间周期、起始时间或每轮成员排序时开启新统计周期;自然周期仅调整排序时保留未移除成员累计。成员移除后不再选择,但历史支付快照不改写。
|
||||
- `POST /payment-merchant-pools/:id/enable` 与 `/disable`:启用时再次校验唯一启用池和可用成员;停用后新支付创建明确失败,不回退综合支付配置。
|
||||
|
||||
### 微信授权配置
|
||||
|
||||
- `GET /wechat-authorizations`:最多返回一个启用配置;仅管理角色可读取。`PUT /wechat-authorizations/current` 创建或更新唯一配置,写入公众号 H5/JSSDK、小程序登录及支付 AppID 所需字段。
|
||||
- 启用第二个配置返回“平台已有启用微信授权配置”;停用后 C 端微信登录/OpenID/微信支付 AppID 不得静默回退其他支付商户或旧配置。
|
||||
|
||||
### 新旧支付分流
|
||||
|
||||
- C 端套餐订单、资产钱包充值、代理在线预存款创建支付时,在同一事务内读取对应支付方式唯一启用池并冻结 `merchant_id`、`merchant_pool_id`、非敏感收款身份与轮询快照;无可用成员返回“暂无可用商户”。
|
||||
- `merchant_id` 非空的支付,在回调、查单、退款时加载该商户当前凭证;为空的历史支付仅按既有 `payment_config_id` 处理。不得按当前启用池为历史单推断商户。
|
||||
- 首次支付成功消费者以支付 ID 唯一记账金额/笔数统计;重复回调不重复累计。预下单、失败、关闭和退款均不变更统计。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 并发成功回调超过阈值:这是“只统计成功、不预占”的明确结果,下一次选路才跳过。
|
||||
- 商户停用后的退款:停用不阻断历史退款,实际调用仍由凭证/渠道结果决定。
|
||||
- 迁移缺失凭证:不造空商户池,受影响新支付明确失败。
|
||||
- 旧回调误入新路径:以支付单 merchant ID 为唯一分流条件,严禁按当前启用池推断。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移创建商户/池/成员/微信授权表、唯一启用约束、支付单路由列和成功统计索引。
|
||||
2. 在迁移事务中从唯一生效综合配置复制完整凭证并创建单成员池;全过程禁止日志输出密钥/证书。
|
||||
3. 隔离库验证微信直连、富友、支付宝、空配置、停用历史商户、回调兼容、三种轮询及 up/down/up。
|
||||
4. 回滚前停止新支付创建;有新路由单时仅回退应用流量,不执行会破坏新支付事实的 down。
|
||||
27
openspec/changes/add-payment-merchant-pools/proposal.md
Normal file
27
openspec/changes/add-payment-merchant-pools/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Why
|
||||
|
||||
当前所有线上支付依赖一份综合支付配置,无法按支付方式在多个实际收款商户之间受控轮询;创建支付后的商户事实也不足以让停用商户的历史回调、查单和原路退款继续安全执行。
|
||||
|
||||
本 Change 落实 AUG26-002:将收款商户、支付路由和 C 端微信授权分离,并以商户池快照取代新业务对旧综合支付配置的依赖。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增独立商户管理:微信商户与支付宝商户分别建档;商户保存支付能力和敏感凭证,但历史支付单仅保存非敏感身份快照。
|
||||
- 新增每种支付方式至多一个启用商户池,支持按成功收款金额、成功笔数或时间周期轮询;新支付仅从命中池选择实际商户。
|
||||
- 新增全局唯一启用的微信授权配置,专供 C 端公众号 H5/JSSDK、小程序登录、OpenID 和支付 AppID;它不是微信收款商户。
|
||||
- 新建 C 端套餐购买、资产钱包充值、代理在线预存款充值按商户池路由;后台线下/钱包支付不经过商户池。无可用商户时失败,不得回退旧配置或自动换商户重试。
|
||||
- 从当前生效综合支付配置一次性复制完整凭证形成新商户、单成员商户池及微信授权配置;新支付切换后,旧配置和历史订单仅继续处理其自身回调、查询和退款。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `merchant-payment-routing`: 商户、商户池、微信授权配置、轮询、历史商户快照和迁移切换行为。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。该能力向既有支付创建、回调和退款调用链提供已选商户事实,不改变其资金幂等不变量。
|
||||
|
||||
## Impact
|
||||
|
||||
影响支付配置模型和后台接口、`tb_payment`/订单/充值支付关联、微信/支付宝/富友支付加载和回调、支付与退款审计、敏感信息访问及新增成对迁移。
|
||||
@@ -0,0 +1,44 @@
|
||||
## Purpose
|
||||
|
||||
管理实际收款商户、商户池轮询和全局微信授权配置,使新线上支付的收款身份可冻结、历史支付可继续使用其原商户,并避免凭证泄露或无配置时静默回退。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 商户与微信授权配置管理
|
||||
系统 SHALL 将实际收款商户与微信授权配置分离。一个商户 MUST 仅对应 `wechat` 或 `alipay` 一种支付方式,并保存名称、支付方式、服务商类型、商户号或应用标识、敏感凭证、状态和备注;微信直连与富友均为微信支付商户。平台最多存在一个启用的微信授权配置,该配置保存 C 端公众号 H5/JSSDK、小程序登录所需参数,C 端微信登录、OpenID 和微信支付 AppID MUST 只读取该配置。
|
||||
|
||||
超级管理员和平台用户可创建、编辑、启用、停用商户、商户池和微信授权配置,其他角色无管理入口。被支付单引用的商户 MUST NOT 删除且其支付方式、服务商类型、商户号/应用标识不得修改;未被引用商户仅可移出所有商户池并经二次确认删除。停用只影响新支付单,历史支付的回调、查单和原路退款仍使用该商户当前凭证。管理 API 可向上述已认证管理角色返回完整凭证,但日志、审计快照、错误和普通业务响应 MUST NOT 保存或返回敏感凭证。
|
||||
|
||||
#### Scenario: 受引用商户停用
|
||||
- **WHEN** 管理员停用已被支付单命中的商户
|
||||
- **THEN** 新支付单不再选择该商户,已命中支付单的回调、查询和原路退款仍按该商户处理
|
||||
|
||||
#### Scenario: 非管理角色读取配置
|
||||
- **WHEN** 不具备超级管理员或平台用户身份的账号请求商户或微信授权配置
|
||||
- **THEN** 系统拒绝访问且不返回任何凭证或身份字段
|
||||
|
||||
### Requirement: 商户池唯一性与轮询配置
|
||||
系统 SHALL 为每种支付方式最多启用一个商户池;停用历史池可保留,但不得同时启用多个同支付方式池。商户池成员支付方式 MUST 与池一致,成员按明确顺序排列;金额/笔数方式必须配置 `每轮累计`、`自然日累计` 或 `自然月累计` 统计周期,时间方式必须配置最小为 1 分钟的数值、单位和起始时间。
|
||||
|
||||
金额和笔数轮询只统计已确认支付成功结果,不在预下单时预占,也不因退款回冲。达到阈值的成员在当前周期跳过;所有成员达到阈值时新支付失败。时间轮询自起始时间按固定时段和成员顺序选择,成员停用即时跳下一个可用成员但不重置时段。预下单失败不得自动切换或重试,失败单不计入统计;客户再次发起时重新选择。修改阈值保留当前统计,修改统计周期、金额/笔数方式、时间周期、起始时间或每轮排序按 PRD 规则开启新周期;自然周期排序调整保留未移除成员累计。
|
||||
|
||||
#### Scenario: 并发预下单未预占额度
|
||||
- **WHEN** 多个客户并发创建金额或笔数轮询支付单且当前成员尚未达到阈值
|
||||
- **THEN** 系统可使这些支付单均命中当前成员,只有后续确认成功的支付才计入累计,已创建支付单不因轮询切换改挂商户
|
||||
|
||||
#### Scenario: 当期没有可用商户
|
||||
- **WHEN** 启用商户池中不存在启用且未达阈值的成员,或商户池已停用
|
||||
- **THEN** 系统拒绝创建新支付单并提示暂无可用商户,不回退到旧综合支付配置
|
||||
|
||||
### Requirement: 新支付商户快照与历史兼容
|
||||
C 端套餐购买、C 端资产钱包充值及代理在线预存款充值 SHALL 按支付方式通过对应启用商户池选择实际商户;后台线下订单和钱包余额支付 MUST NOT 经过商户池。每笔通过商户池创建的支付单 MUST 保存商户 ID、商户名称/支付方式/服务商类型/商户号或应用标识快照、商户池 ID/名称快照及轮询方式快照,但不得复制敏感凭证。支付、回调验签、查单和原路退款读取该实际商户当前凭证;商户退款能力只由服务商类型和退款必需凭证完整性决定,不提供人工开关。
|
||||
|
||||
上线迁移 MUST 从当前生效综合支付配置复制完整凭证:创建全局微信授权配置、微信/支付宝商户和各自单成员启用池。凭证不完整的方式不建池;迁移仅在数据库复制敏感数据。新订单必须只走商户池;旧配置和其历史订单不改写,继续服务历史回调、查询和退款。
|
||||
|
||||
#### Scenario: 新支付冻结实际商户
|
||||
- **WHEN** 客户以微信或支付宝创建覆盖范围内的新线上支付单
|
||||
- **THEN** 系统选择并冻结一个实际商户和商户池路由快照,并使用该商户的服务商凭证发起支付
|
||||
|
||||
#### Scenario: 旧支付单回调
|
||||
- **WHEN** 商户池切换后收到未带新商户快照的历史支付单回调
|
||||
- **THEN** 系统按既有综合支付配置兼容处理该历史单,不将其改挂到任何新商户
|
||||
20
openspec/changes/add-payment-merchant-pools/tasks.md
Normal file
20
openspec/changes/add-payment-merchant-pools/tasks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## 1. 数据与配置管理
|
||||
|
||||
- [ ] 1.1 追踪 `tb_wechat_config`、订单/充值、`tb_payment`、支付加载器和三类回调的现有 `payment_config_id` 读写链路,列出新旧分流点和敏感字段清单。
|
||||
- [ ] 1.2 新增成对迁移:商户、商户池、成员、微信授权配置、成功累计/路由快照所需表列、唯一启用/成员支付方式/历史引用约束和查询索引;不得修改既有迁移。
|
||||
- [ ] 1.3 实现商户、商户池及微信授权配置模型、管理 Query/Handler/RouteSpec:管理角色权限、启停、成员排序、受引用字段锁定、移出后确认删除及无敏感审计。
|
||||
- [ ] 1.4 实现迁移时从当前生效综合支付配置复制完整微信授权、微信/支付宝商户和单成员池;不完整方式不创建池,迁移日志不得含凭证。
|
||||
|
||||
## 2. 商户池选择与支付链路
|
||||
|
||||
- [ ] 2.1 实现金额、笔数、时间轮询选择器及池配置变更重置规则;使用事务/受控查询保证唯一启用池和成员状态一致。
|
||||
- [ ] 2.2 在 C 端套餐支付、资产钱包充值、代理在线充值的创建路径接入选择器,保存商户/池/方式非敏感快照;无可用商户和预下单失败不回退、不换商户。
|
||||
- [ ] 2.3 在支付成功首次生效路径按支付 ID 幂等累计金额/笔数;退款不得回冲,失败/重复回调不得计入。
|
||||
- [ ] 2.4 改造支付加载、微信/支付宝/富友回调、查单和原路退款:新单读取实际商户,历史 merchant ID 为空的单继续读取旧 `payment_config_id`;停用商户仍可处理历史单。
|
||||
- [ ] 2.5 清理日志、错误、审计和普通 DTO 中的敏感凭证,管理 API 仅向超级管理员/平台用户按 PRD 返回完整配置并使凭证更新刷新加载缓存。
|
||||
|
||||
## 3. 文档与验证
|
||||
|
||||
- [ ] 3.1 更新支付、商户和微信授权管理接口 OpenAPI,并运行 `go run cmd/gendocs/main.go`。
|
||||
- [ ] 3.2 在隔离数据库验证迁移 up/down/up、配置迁移、三类新支付、缺失配置、三种轮询、并发成功累计、商户停用历史回调/退款及旧单兼容。
|
||||
- [ ] 3.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`openspec validate add-payment-merchant-pools --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
Reference in New Issue
Block a user