Files
junhong_cmp_fiber/openspec/changes/add-payment-merchant-pools/design.md
break 98c145fe70
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 3m55s
实现支付商户池与微信授权配置
新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付
(C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件
经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空
历史支付继续按 payment_config_id 双读。凭证版本化加载与
ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部
引用。
2026-09-09 18:13:04 +08:00

67 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
现有 `tb_wechat_config` 同时承担支付渠道配置;订单、充值和 `tb_payment` 通过 `payment_config_id` 供回调加载创建时配置。`tb_payment` 已有非敏感 `merchant_identity` 快照,但不足以区分商户池、服务商和完整路由。新模型必须让新单和旧单分流,不能把历史订单指向迁移后新商户。
## Decisions
### 独立实体与不可变路由快照
新增商户、商户池、池成员、微信授权配置及金额/笔数统计事实;支付单扩展实际商户和商户池 ID、非敏感快照及创建时的 `routing_epoch`。商户凭证只留在商户表受控字段,支付单/审计不复制。被引用后锁定商户支付方式、服务商与身份,避免历史验签与退款语义漂移。
### 路由和统计在支付创建/成功边界闭合
支付创建在事务内读取唯一启用池、按方式选择成员并冻结路由和 `routing_epoch`;无成员/池失败即返回。`routing_epoch` 是该次创建命中的池策略、统计周期与成员排序世代。金额/笔数统计只在现有“支付成功首次生效”路径以支付 ID 唯一事实按冻结商户和 epoch 递增,不在预下单预占。迟到的首次成功仍记入冻结 epoch当前选择器只读取当前 epoch因此周期切换、成员调整或排序变化不改写已创建支付的归属。时间方式由当前时间和受控起点计算不写逐次路由日志。
### 凭证版本与读取一致性
商户和微信授权配置各保存递增的凭证版本。凭证更新在事务提交时递增版本;支付创建、回调验签、查单和退款先从主库取得当前版本,再使用“配置 ID + 版本”读取缓存。旧版本缓存提交后可删除,但即使并发残留也不得再被新读取命中;缓存不可用时读取当前数据库记录。历史支付仍读取命中商户的当前有效凭证,以支持轮换。
### 新旧配置双读切换
迁移完成后部署支持 merchant ID 或旧 `payment_config_id` 双读的 API/Worker。三类后续新支付立即只走商户池并在创建事务冻结路由无可用池、成员或池停用时明确失败绝不回退旧综合支付配置创建。merchant ID 为空仅表示历史支付,回调验签、查询和已有可达的原路退款继续按 `payment_config_id` 处理,直到独立 Change 根据数据留存期删除旧读取路径。故障只允许在仍支持双读的版本上前向修复;一旦存在 merchant ID 非空支付、成功事实或新配置,禁止部署不识别新路由的旧二进制和执行破坏性 down。不可将现有记录批量回填为新商户因为其实际历史身份无法保证一致。
### 敏感配置边界
仅超级管理员和平台用户的专用管理列表、详情响应可完整返回凭证,管理写入请求可传递凭证;所有其他 DTO、logger、错误、审计 payload、支付单快照和导出只允许写 ID、名称和脱敏/非敏感身份。
### 已确认范围
本 Change 只为已有且可达的原路退款路径提供冻结商户的凭证加载与能力判定,不新增微信、支付宝、富友或其他渠道退款调用;服务商没有既有退款能力时保持不支持,且不提供人工开关。富友 `CommonQuery` 保持现有请求格式、签名算法、验签、状态映射和恢复语义的兼容基线,只实现本地双读配置来源和商户加载,不改变协议或业务能力,富友退款不新增;未第三方实测可记录但不得作为实施、验证或归档阻塞。零条 active 综合配置时仅创建 Schema 和管理入口,不插入商户、商户池或微信授权配置数据。代理在线充值的全局允许范围、交集计算和对外方式查询由对应代理自充 Change 负责;本 Change 在方式已获准后负责商户池可用性与实际商户冻结。
## 管理与支付动作契约
以下路径为本 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
- 并发成功回调超过阈值:这是“只统计成功、不预占”的明确结果,下一次选路才跳过。
- 商户停用后的退款:停用不阻断已有历史退款路径,实际调用仍由当前凭证、服务商已有能力和渠道结果决定;本 Change 不新增退款能力。
- 零条 active 综合配置:仅创建 Schema 和管理入口,不创建业务配置数据;所有新线上支付明确失败,微信授权相关功能明确返回未配置。一条 active 综合配置中支付凭证不完整的方式不建池,微信授权字段不完整时不建授权配置且相关 C 端微信功能明确失败;多条 active 综合配置无法安全选择来源,迁移必须失败。
- 旧回调误入新路径:以支付单 merchant ID 为唯一分流条件,严禁按当前启用池推断。
- 富友查单:`CommonQuery` 保持现有请求格式、签名算法、验签、状态映射和恢复语义,只调整本地双读配置来源和商户加载;不改变协议、状态解释、恢复规则或业务能力,不增加富友退款。未第三方实测仅作为记录,不阻塞本 Change。
## Migration Plan
1. 新增成对迁移创建商户/池/成员/微信授权表、唯一启用约束、支付单路由与 `routing_epoch` 列、凭证版本及成功统计唯一索引;迁移编号在实施开始前按当时迁移目录的最大编号确定,本规划不预占编号。
2. 迁移遇到一条 active 综合配置时只复制完整支付凭证形成对应单成员池;微信授权字段完整时创建授权配置。零条 active 配置时仅创建 Schema 和管理入口,不插入商户、商户池或微信授权配置数据;多条 active 配置时中止迁移。全过程禁止日志输出密钥/证书。
3. 在本地工作区以明确 `DB_*` 指向维护者提供的 `junhong_cmp_test` PostgreSQL 与 Redis DB6执行 migration up/down/up 和数据行为验证;允许本 Change fixture 创建/删除,禁止重置整个库。完成验证后才提交推送 Iteration/8-11仅连接、迁移或实际行为失败时阻塞对应验证。
4. Iteration/8-11 分支 Gitea 仅以本次提交 SHA 构建/部署 `cmp-test` 测试镜像并检查 migration version不自动执行 migration up/down 或重置。记录 API/Worker 容器状态、健康检查和 `/opt/junhong_cmp/logs` 的有限日志;不建运行时开关,这不是生产发布。
5. 迁移完成后的部署版本支持 merchant ID 或旧 `payment_config_id` 双读;三类后续新支付立即只走商户池并冻结路由,无池、成员或停用池时明确失败且无旧创建回退。验证新支付始终走池、历史 merchant ID 为空支付继续旧读取路径。故障只允许在仍支持双读的版本上前向修复;存在 merchant ID 非空支付、成功事实或新配置时,禁止部署不识别新路由的旧二进制和执行破坏性 down。
6. 富友 `CommonQuery` 仅保持现有请求格式、签名算法、验签、状态映射和恢复语义并完成本地双读商户加载,不改变协议或增加退款;未第三方实测仅记录,不阻塞验证或归档。验证覆盖微信直连、富友、支付宝、零/一/多 active 配置、授权或支付凭证缺失、停用历史商户、回调兼容、三种轮询、迟到成功归属、凭证轮换及 up/down/up。