Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
182 lines
16 KiB
Markdown
182 lines
16 KiB
Markdown
# 商户支付路由当前行为
|
||
|
||
## 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 一次性计入冻结商户和冻结统计世代,当前选路只读取当前统计世代;金额与笔数累计 MUST 只统计与当前统计世代匹配的成功事实(自然日与自然月方式另加支付时间不早于当期窗口起点)。达到阈值的成员在当前周期跳过;全部成员均达到阈值时的行为按统计周期区分:`每轮累计` MUST 视为当轮结束,以保存成功时间为新起点开启新一轮并从零重新累计,MUST NOT 因当轮全部达标而拒绝创建,开启新一轮 MUST 以条件更新递增统计世代,仅在当前世代未被并发改变时成功,更新冲突 MUST 按并发冲突拒绝创建本次支付且 MUST NOT 在旧世代上重复累计;`自然日累计` 与 `自然月累计` MUST 拒绝创建新支付单并提示当前周期暂无可用商户。池内只有一个启用成员时该成员 MUST 被固定选中:MUST NOT 因达到阈值而在当前周期被跳过,也 MUST NOT 因「全部成员达标」而拒绝创建,该规则优先于上句的自然周期拒绝语义。时间轮询自起始时间按固定时段和成员顺序选择,成员停用即时跳下一个可用成员但不重置时段;修改时间周期数值、单位、起始时间或成员顺序并保存成功后,系统 MUST 以保存成功时间为新起点并从前述成员列表第一项重新计算时段,MUST NOT 沿用请求提交前的起点。预下单失败不得自动切换或重试,失败单不计入统计;客户再次发起时重新选择。修改阈值保留当前统计,修改统计周期、金额/笔数方式、时间周期、起始时间或每轮排序按 PRD 规则开启新周期;自然周期排序调整保留未移除成员累计。
|
||
|
||
#### Scenario: 并发预下单未预占额度
|
||
|
||
- **WHEN** 多个客户并发创建金额或笔数轮询支付单且当前成员尚未达到阈值
|
||
- **THEN** 系统可使这些支付单均命中当前成员,只有后续确认成功的支付才计入累计,已创建支付单不因轮询切换改挂商户
|
||
|
||
#### Scenario: 迟到首次成功归属冻结统计世代
|
||
|
||
- **GIVEN** 支付已创建但尚未成功,之后商户池切换统计周期、成员或排序
|
||
- **WHEN** 该支付首次确认成功
|
||
- **THEN** 系统仅将金额或笔数写入该支付创建时冻结的商户和统计世代,不得改写当前选路统计或重复累计
|
||
|
||
#### Scenario: 每轮累计全部达标开启新一轮
|
||
|
||
- **GIVEN** 启用商户池统计周期为每轮累计,且本轮全部成员均已达到阈值
|
||
- **WHEN** 客户创建新的支付单
|
||
- **THEN** 系统开启新一轮、各成员从零累计,并选中成员顺序第一项,不返回暂无可用商户
|
||
|
||
#### Scenario: 当期没有可用商户
|
||
|
||
- **WHEN** 启用商户池中不存在启用且未达阈值的成员,或商户池已停用
|
||
- **THEN** 系统拒绝创建新支付单并提示暂无可用商户,不回退到旧综合支付配置
|
||
|
||
#### Scenario: 自然周期全部达标拒绝创建
|
||
|
||
- **GIVEN** 启用商户池统计周期为自然日累计或自然月累计,且当前周期全部成员均已达到阈值
|
||
- **WHEN** 客户创建新的支付单
|
||
- **THEN** 系统拒绝创建并提示当前周期暂无可用商户,不回退到旧综合支付配置
|
||
|
||
#### Scenario: 单成员池达标后仍可收款
|
||
|
||
- **GIVEN** 启用商户池只有一个启用成员,且该成员在当前自然日或自然月周期内已达到阈值
|
||
- **WHEN** 客户创建新的支付单
|
||
- **THEN** 系统仍选中该成员,不返回暂无可用商户
|
||
|
||
#### Scenario: 修改时间轮询配置后起点重算
|
||
|
||
- **GIVEN** 时间轮询商户池已存在起始时间与成员顺序
|
||
- **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: 商户池列表投影
|
||
|
||
商户池列表与详情 SHALL 在既有配置字段之外返回:启用成员数量与成员总数量、当前命中成员、最近配置更新时间。当前命中成员 MUST 以与支付创建相同的选择规则只读计算:金额/笔数方式取成员顺序中第一个未达阈值的成员,「每轮累计」全部达标时取成员顺序第一项;时间方式按当前时段计算结果。该计算 MUST 为只读,MUST NOT 创建支付单、MUST NOT 推进统计世代、MUST NOT 计入成功累计。商户池停用或没有可用成员时当前命中成员 SHALL 返回空值 MUST NOT 阻断列表响应。成员数量与当前命中成员 MUST 按该页池集合批量计算,MUST NOT 逐池放大查询次数。
|
||
|
||
#### Scenario: 列表展示商户数量与更新时间
|
||
|
||
- **WHEN** 授权账号查询商户池列表
|
||
- **THEN** 每行返回启用成员数、成员总数与最近配置更新时间
|
||
|
||
#### Scenario: 当前命中成员按顺序计算
|
||
|
||
- **GIVEN** 金额轮询商户池的成员顺序为 A、B,且 A 尚未达到阈值
|
||
- **WHEN** 查询商户池列表
|
||
- **THEN** 当前命中成员返回 A,且查询本身不产生支付单、不推进统计世代
|
||
|
||
#### Scenario: 停用池无当前成员
|
||
|
||
- **WHEN** 查询一个已停用且无可用成员的商户池
|
||
- **THEN** 当前命中成员为空值,其余字段正常返回
|
||
|
||
#### Scenario: 批量计算不放大查询
|
||
|
||
- **GIVEN** 一页返回多个商户池
|
||
- **WHEN** 查询该页列表
|
||
- **THEN** 系统以批量方式读取成员与成功累计,查询次数不随池数量线性增长
|
||
|
||
## 可达操作索引
|
||
|
||
本节只用于入口导航,不是行为 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`(停用商户池)。
|