Files
junhong_cmp_fiber/openspec/specs/merchant-payment-routing/spec.md
break 09abee9778 docs(归档): 归档退款方式与原路退款变更并同步主规格
- 将 add-refund-methods-and-original-route-refunds 归档为
  2026-09-14-add-refund-methods-and-original-route-refunds。
- 合并两份 delta 到主规格:
  * order-refund-exchange:改写「订单、退款与换货状态门禁」,新增「退款实收金额与方式矩阵」
    「企业微信唯一终审与审批尝试重提」「原路退款渠道能力与执行」「退款权益与订单状态时点」
    「退款终态事实与失败分类」五项行为要求。
  * merchant-payment-routing:改写「商户与微信授权配置管理」与「新支付商户快照与历史兼容」
    (删除「不得新增渠道退款能力」与「不新增富友退款」,改由退款能力按商户凭证执行;
    微信 v2 客户端证书改为可选凭证键)。
- 同步上下文健康检查证据链与入口矩阵:为新要求登记证据行,并把退款创建、重提、
  企微审批回调、退款详情与 refund:channel:recovery 任务与对应要求双向关联。
2026-09-14 12:00:35 +08:00

127 lines
12 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.
# 商户支付路由当前行为
## Purpose
管理实际收款商户、商户池轮询和全局微信授权配置,使新线上支付的收款身份可冻结、历史支付可继续使用其原商户,并避免凭证泄露或无配置时静默回退;本能力不新增任何支付渠道退款能力。
## Requirements
### Requirement: 商户与微信授权配置管理
系统 SHALL 将实际收款商户与微信授权配置分离。一个商户 MUST 仅对应 `wechat``alipay` 一种支付方式,并保存名称、支付方式、服务商类型、商户号或应用标识、敏感凭证、状态和备注;微信直连与富友均为微信支付商户。创建或更新商户时 `credentials` MUST 为扁平 JSON 对象,必填键由支付方式与服务商类型组合确定:`wechat/wechat``wx_mch_id``wx_api_v3_key``wx_cert_content``wx_key_content``wx_serial_no``wx_notify_url``wechat/wechat_v2``wx_mch_id``wx_api_v2_key``wx_notify_url`,并可选 `wx_client_cert_content``wx_client_key_content`API 客户端证书与私钥,为 v2 原路退款的双向证书所需);`wechat/fuiou``fy_mchnt_cd``fy_ins_cd``fy_term_id``fy_private_key``fy_public_key``fy_api_url``fy_notify_url``alipay/alipay``ali_app_id``ali_private_key``ali_public_key``ali_notify_url``ali_return_url`。凭证值 MUST 为字符串,仅可选的 `ali_production`(布尔)与 `ali_pay_expire_minutes`(整数)例外;`merchant_identity` MUST 分别等于 `wx_mch_id``fy_mchnt_cd``ali_app_id`。平台最多存在一个启用的微信授权配置,该配置保存 C 端公众号 H5/JSSDK、小程序登录所需参数C 端微信登录、OpenID 和微信支付 AppID MUST 只读取该配置。
超级管理员和平台用户可创建、编辑、启用、停用商户、商户池和微信授权配置,其他角色无管理入口。仅上述角色的专用管理列表和详情响应可返回完整凭证;日志、审计快照、错误、导出、支付快照和其他业务响应 MUST NOT 保存或返回敏感凭证。被支付单引用的商户 MUST NOT 删除且其支付方式、服务商类型、商户号/应用标识不得修改;未被引用商户仅可移出所有商户池并经二次确认删除。停用只影响新支付单,历史支付的回调、查单和原路退款仍使用该商户当前凭证;原路退款的实际渠道调用由 `order-refund-exchange` 能力按该商户当前凭证执行,本能力 MUST NOT 保存渠道退款流水或推进退款状态。可退性只依据服务商类型与该服务商类型退款必需凭证的完整性判定,本能力 MUST NOT 提供人工退款能力开关。`wechat/wechat_v2``wx_client_cert_content``wx_client_key_content` 为可选键:不填不影响该商户的支付、查单与回调,只使原路退款按其凭证完整性判定为不可用。
#### Scenario: 受引用商户停用
- **WHEN** 管理员停用已被支付单命中的商户
- **THEN** 新支付单不再选择该商户,已命中支付单的回调、查询和原路退款仍按该商户处理
#### Scenario: 非管理角色读取配置
- **WHEN** 不具备超级管理员或平台用户身份的账号请求商户或微信授权配置
- **THEN** 系统拒绝访问且不返回任何凭证或身份字段
#### Scenario: 凭证轮换后处理历史支付
- **GIVEN** 已被支付单引用的商户或当前微信授权配置完成凭证更新
- **WHEN** 更新提交后创建支付、处理回调、查单或原路退款
- **THEN** 系统只使用更新后的当前有效凭证,不得继续使用更新前的缓存凭证
#### Scenario: 凭证键不完整或身份不一致被拒
- **WHEN** 管理员创建或更新商户时提交缺少该支付方式与服务商类型必填键的 `credentials`,或 `merchant_identity` 与凭证中的商户号或应用标识不一致
- **THEN** 系统拒绝写入并返回参数错误,不创建或修改商户记录
#### Scenario: 商户凭证不足以执行退款
- **GIVEN** 已被支付单命中的商户缺少其服务商类型执行原路退款所需的凭证
- **WHEN** 系统判定该支付单原路退款的可退性
- **THEN** 判定结果为不可用且只允许客户收款信息退款,系统不因该结果新增商户凭证键或人工退款开关
### Requirement: 商户池唯一性与轮询配置
系统 SHALL 为每种支付方式最多启用一个商户池;停用历史池可保留,但不得同时启用多个同支付方式池。商户池成员支付方式 MUST 与池一致,成员按明确顺序排列;金额/笔数方式必须配置 `每轮累计``自然日累计``自然月累计` 统计周期,时间方式必须配置最小为 1 分钟的数值、单位和起始时间。
金额和笔数轮询只统计已确认支付成功结果,不在预下单时预占,也不因退款回冲。支付创建时 MUST 冻结本次路由所属的统计世代;首次成功只按支付 ID 一次性计入冻结商户和冻结统计世代,当前选路只读取当前统计世代。达到阈值的成员在当前周期跳过;所有成员达到阈值时新支付失败。时间轮询自起始时间按固定时段和成员顺序选择,成员停用即时跳下一个可用成员但不重置时段。预下单失败不得自动切换或重试,失败单不计入统计;客户再次发起时重新选择。修改阈值保留当前统计,修改统计周期、金额/笔数方式、时间周期、起始时间或每轮排序按 PRD 规则开启新周期;自然周期排序调整保留未移除成员累计。
#### Scenario: 并发预下单未预占额度
- **WHEN** 多个客户并发创建金额或笔数轮询支付单且当前成员尚未达到阈值
- **THEN** 系统可使这些支付单均命中当前成员,只有后续确认成功的支付才计入累计,已创建支付单不因轮询切换改挂商户
#### Scenario: 迟到首次成功归属冻结统计世代
- **GIVEN** 支付已创建但尚未成功,之后商户池切换统计周期、成员或排序
- **WHEN** 该支付首次确认成功
- **THEN** 系统仅将金额或笔数写入该支付创建时冻结的商户和统计世代,不得改写当前选路统计或重复累计
#### Scenario: 当期没有可用商户
- **WHEN** 启用商户池中不存在启用且未达阈值的成员,或商户池已停用
- **THEN** 系统拒绝创建新支付单并提示暂无可用商户,不回退到旧综合支付配置
### Requirement: 新支付商户快照与历史兼容
C 端套餐购买、C 端资产钱包充值及代理在线预存款充值 SHALL 按支付方式通过对应启用商户池选择实际商户;后台线下订单和钱包余额支付 MUST NOT 经过商户池。代理在线充值的全局允许范围、其与商户池方式的交集及对外方式查询由对应代理自充能力定义;本能力在方式已获准后负责实际商户选择,并在无可用商户时拒绝创建。每笔通过商户池创建的支付单 MUST 保存商户 ID、商户名称/支付方式/服务商类型/商户号或应用标识快照、商户池 ID/名称快照、轮询方式快照及统计世代快照,但不得复制敏感凭证。支付、回调验签、查单和原路退款读取该实际商户当前凭证;服务商类型和退款必需凭证完整性只用于原路退款路径的可退性判定,不提供人工开关;实际渠道退款调用与退款终态由 `order-refund-exchange` 能力实现,本能力 MUST NOT 发起渠道退款请求。
上线迁移在存在唯一当前生效综合支付配置时复制完整凭证:具备完整凭证的微信/支付宝方式创建商户和各自单成员启用池,微信授权字段完整时创建全局微信授权配置;不完整的支付方式不建池,授权字段不完整时不建授权配置并使相关 C 端微信功能明确失败。没有当前生效综合支付配置时迁移仅创建 Schema 和管理入口,不创建商户、商户池或微信授权配置数据;新订单按暂无可用商户失败,微信授权相关功能明确返回未配置。存在多条当前生效综合支付配置时迁移 MUST 失败。迁移完成后部署支持按 merchant ID 或旧 `payment_config_id` 双读的 API/Worker三类后续新支付 MUST 立即只走商户池并冻结路由无可用池、成员或停用池时明确失败且不回退旧综合支付配置。merchant ID 为空仅表示历史订单,继续按 `payment_config_id` 服务历史回调、查询和原路退款,直到独立 Change 按数据留存期删除旧读取路径。本 Change MUST NOT 自动删除旧配置或旧读取路径。
#### Scenario: 新支付冻结实际商户
- **WHEN** 客户以微信或支付宝创建覆盖范围内的新线上支付单
- **THEN** 系统选择并冻结一个实际商户和商户池路由快照,并使用该商户的服务商凭证发起支付
#### Scenario: 迁移后的新支付只走商户池
- **GIVEN** 迁移完成且已部署支持 merchant ID 或旧 `payment_config_id` 双读的 API/Worker
- **WHEN** 客户创建覆盖范围内的后续新线上支付
- **THEN** 系统立即经启用商户池选择并冻结实际商户;无可用池、成员或池已停用时明确失败,不得走旧综合支付配置创建
#### Scenario: 历史支付保留旧读取路径
- **GIVEN** 支付单 merchant ID 为空
- **WHEN** 该支付经过回调、查单或原路退款路径
- **THEN** 系统仅按其 `payment_config_id` 处理,不因当前商户池推断或改写其商户,直到独立 Change 按数据留存期删除旧读取路径
#### Scenario: 旧支付单回调
- **WHEN** 商户池切换后收到未带新商户快照的历史支付单回调
- **THEN** 系统按既有综合支付配置兼容处理该历史单,不将其改挂到任何新商户
#### Scenario: 微信授权配置迁移缺失
- **GIVEN** 当前生效综合支付配置的微信授权字段不完整
- **WHEN** 执行商户池配置迁移后访问 C 端微信登录、OpenID、JSSDK 或微信支付 AppID 功能
- **THEN** 系统明确返回微信授权未配置,不得回退旧综合支付配置
#### Scenario: 多个当前生效综合支付配置
- **GIVEN** 存在多条当前生效综合支付配置
- **WHEN** 执行商户池配置迁移
- **THEN** 迁移失败且不选择任一配置作为复制来源
#### Scenario: 富友双读兼容基线
- **GIVEN** 富友 `CommonQuery` 未经第三方实测
- **WHEN** 系统以商户当前凭证完成富友支付的本地双读配置来源和商户加载接线,且不改变现有 `CommonQuery` 请求格式、签名算法、验签、状态映射或恢复语义
- **THEN** 系统保留该兼容基线;富友原路退款由 `order-refund-exchange` 能力按 `/commonRefund``/refundQuery` 契约单独实现,未第三方实测可以记录,但不得作为实施、验证或归档阻塞
#### Scenario: 维护者指定测试环境的配置迁移验证
- **GIVEN** 维护者指定的 `junhong_cmp_test` PostgreSQL、Redis DB6 与当前 Change fixture
- **WHEN** 本地工作区以明确 `DB_*` 完成 migration up/down/up 和数据行为验证后提交推送 Iteration/8-11
- **THEN** Gitea 只构建/部署 `cmp-test` 测试镜像并检查迁移版本;测试日志保存在 `/opt/junhong_cmp/logs`,不得自动执行迁移或重置整库
#### Scenario: 历史支付路径保留
- **GIVEN** 存在 merchant ID 为空的历史支付
- **WHEN** 商户池功能已上线且独立 Change 尚未按数据留存期删除旧读取路径
- **THEN** 系统仍按该支付的 `payment_config_id` 处理回调、查询和退款,不自动删除旧配置或将其改挂商户
## 可达操作索引
本节只用于入口导航,不是行为 Requirement业务义务以上述 Requirements 为准。
### 商户与微信授权配置
`GET /api/admin/payment-merchants`(查询支付商户);`POST /api/admin/payment-merchants`(创建支付商户);`GET /api/admin/payment-merchants/{id}`(查询支付商户详情);`PUT /api/admin/payment-merchants/{id}`(更新支付商户);`DELETE /api/admin/payment-merchants/{id}`(删除支付商户);`GET /api/admin/wechat-authorizations`(获取微信授权配置);`PUT /api/admin/wechat-authorizations/current`(保存微信授权配置)。
### 商户池
`GET /api/admin/payment-merchant-pools`(查询商户池);`POST /api/admin/payment-merchant-pools`(创建商户池);`GET /api/admin/payment-merchant-pools/{id}`(查询商户池详情);`PUT /api/admin/payment-merchant-pools/{id}`(更新商户池);`POST /api/admin/payment-merchant-pools/{id}/enable`(启用商户池);`POST /api/admin/payment-merchant-pools/{id}/disable`(停用商户池)。