From dc4e0d41031433c8ea6deaf9a3684af2b89d1da8 Mon Sep 17 00:00:00 2001 From: break Date: Thu, 10 Sep 2026 12:03:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=92=E6=A1=A3=E6=94=AF=E4=BB=98=E5=95=86?= =?UTF-8?q?=E6=88=B7=E6=B1=A0=E5=8F=98=E6=9B=B4=E5=B9=B6=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=B8=BB=E8=A7=84=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entry-capability-requirement-matrix.json | 126 +++++++++++++++++- .../context-reset/requirement-evidence.json | 100 ++++++++++++++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/merchant-payment-routing/spec.md | 0 .../tasks.md | 0 .../specs/merchant-payment-routing/spec.md | 103 ++++++++++++++ 8 files changed, 326 insertions(+), 3 deletions(-) rename openspec/changes/{add-payment-merchant-pools => archive/2026-09-10-add-payment-merchant-pools}/.openspec.yaml (100%) rename openspec/changes/{add-payment-merchant-pools => archive/2026-09-10-add-payment-merchant-pools}/design.md (100%) rename openspec/changes/{add-payment-merchant-pools => archive/2026-09-10-add-payment-merchant-pools}/proposal.md (100%) rename openspec/changes/{add-payment-merchant-pools => archive/2026-09-10-add-payment-merchant-pools}/specs/merchant-payment-routing/spec.md (100%) rename openspec/changes/{add-payment-merchant-pools => archive/2026-09-10-add-payment-merchant-pools}/tasks.md (100%) create mode 100644 openspec/specs/merchant-payment-routing/spec.md diff --git a/docs/verification/context-reset/entry-capability-requirement-matrix.json b/docs/verification/context-reset/entry-capability-requirement-matrix.json index 24adce2..d12691a 100644 --- a/docs/verification/context-reset/entry-capability-requirement-matrix.json +++ b/docs/verification/context-reset/entry-capability-requirement-matrix.json @@ -1769,7 +1769,8 @@ "external-integration::富友调用超时兼容行为", "identity-access::数据范围拒绝", "order-payment-wallet::金额单位", - "external-integration::富友主扫统一下单与订单查询" + "external-integration::富友主扫统一下单与订单查询", + "merchant-payment-routing::新支付商户快照与历史兼容" ], "classification": "behavior" }, @@ -2696,7 +2697,8 @@ "capability": "order-payment-wallet", "requirements": [ "external-integration::富友调用超时兼容行为", - "order-payment-wallet::钱包并发" + "order-payment-wallet::钱包并发", + "merchant-payment-routing::新支付商户快照与历史兼容" ], "classification": "behavior" }, @@ -2708,7 +2710,8 @@ "agent-funds-commission::充值边界", "external-integration::富友调用超时兼容行为", "order-payment-wallet::金额单位", - "order-payment-wallet::钱包并发" + "order-payment-wallet::钱包并发", + "merchant-payment-routing::新支付商户快照与历史兼容" ], "classification": "behavior" }, @@ -3642,5 +3645,122 @@ "system-operations::上下文健康检查证据链完整" ], "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "GET /api/admin/payment-merchants", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "POST /api/admin/payment-merchants", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "GET /api/admin/payment-merchants/{id}", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "PUT /api/admin/payment-merchants/{id}", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "DELETE /api/admin/payment-merchants/{id}", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "GET /api/admin/wechat-authorizations", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "PUT /api/admin/wechat-authorizations/current", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户与微信授权配置管理" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "GET /api/admin/payment-merchant-pools", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户池唯一性与轮询配置" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "POST /api/admin/payment-merchant-pools", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户池唯一性与轮询配置" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "GET /api/admin/payment-merchant-pools/{id}", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户池唯一性与轮询配置" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "PUT /api/admin/payment-merchant-pools/{id}", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户池唯一性与轮询配置" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "POST /api/admin/payment-merchant-pools/{id}/enable", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户池唯一性与轮询配置" + ], + "classification": "behavior" + }, + { + "entry_type": "http", + "entry": "POST /api/admin/payment-merchant-pools/{id}/disable", + "capability": "merchant-payment-routing", + "requirements": [ + "merchant-payment-routing::商户池唯一性与轮询配置" + ], + "classification": "behavior" } ] diff --git a/docs/verification/context-reset/requirement-evidence.json b/docs/verification/context-reset/requirement-evidence.json index 2a1dc9b..a414c14 100644 --- a/docs/verification/context-reset/requirement-evidence.json +++ b/docs/verification/context-reset/requirement-evidence.json @@ -975,6 +975,106 @@ "exit_status": 0 } }, + { + "capability": "merchant-payment-routing", + "requirement": "商户与微信授权配置管理", + "spec": "openspec/specs/merchant-payment-routing/spec.md", + "entries": [ + "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" + ], + "handler_consumer_job": [ + "internal/handler/admin/payment_merchant.go" + ], + "application_service_query": [ + "internal/application/merchantpayment/management.go" + ], + "domain_state_amount": [ + "internal/model/payment_merchant.go" + ], + "store_migration_config": [ + "migrations/000210_add_payment_merchant_pools.up.sql" + ], + "verification": { + "command": "代码中检索 requirePaymentMerchantAccess 与 无权限访问支付商户配置", + "literal_output": [ + "internal/routes/payment_merchant.go:16:func requirePaymentMerchantAccess(handler fiber.Handler) fiber.Handler {", + "internal/routes/payment_merchant.go:20:return errors.New(errors.CodeForbidden, \"无权限访问支付商户配置\")" + ], + "exit_status": 0 + } + }, + { + "capability": "merchant-payment-routing", + "requirement": "商户池唯一性与轮询配置", + "spec": "openspec/specs/merchant-payment-routing/spec.md", + "entries": [ + "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" + ], + "handler_consumer_job": [ + "internal/handler/admin/payment_merchant.go" + ], + "application_service_query": [ + "internal/application/merchantpayment/management.go" + ], + "domain_state_amount": [ + "internal/application/merchantpayment/routing.go" + ], + "store_migration_config": [ + "migrations/000210_add_payment_merchant_pools.up.sql" + ], + "verification": { + "command": "代码中检索 暂无可用商户", + "literal_output": [ + "internal/application/merchantpayment/routing.go:210:return nil, errors.New(errors.CodeNoPaymentConfig, \"暂无可用商户\")", + "internal/application/merchantpayment/routing.go:219:return nil, errors.New(errors.CodeNoPaymentConfig, \"暂无可用商户\")", + "internal/application/merchantpayment/routing.go:254:return nil, errors.New(errors.CodeNoPaymentConfig, \"暂无可用商户\")" + ], + "exit_status": 0 + } + }, + { + "capability": "merchant-payment-routing", + "requirement": "新支付商户快照与历史兼容", + "spec": "openspec/specs/merchant-payment-routing/spec.md", + "entries": [ + "POST /api/c/v1/orders/{id}/pay", + "POST /api/c/v1/wallet/recharge", + "POST /api/admin/agent-recharges" + ], + "handler_consumer_job": [ + "internal/service/client_order/service.go", + "internal/service/recharge_order/service.go", + "internal/application/agentrecharge/online_creation.go" + ], + "application_service_query": [ + "internal/application/merchantpayment/routing.go" + ], + "domain_state_amount": [ + "internal/model/payment.go" + ], + "store_migration_config": [ + "migrations/000210_add_payment_merchant_pools.up.sql" + ], + "verification": { + "command": "代码中检索 SelectForNewPaymentWithTx 与 FreezeRoute", + "literal_output": [ + "internal/service/client_order/service.go:388:route, err := s.merchantRuntime.SelectForNewPaymentWithTx(ctx, tx, paymentMethod, time.Now())", + "internal/service/client_order/service.go:400:merchantpayment.FreezeRoute(payment, route)" + ], + "exit_status": 0 + } + }, { "capability": "agent-series-package-options", "requirement": "查询系列授权套餐候选项", diff --git a/openspec/changes/add-payment-merchant-pools/.openspec.yaml b/openspec/changes/archive/2026-09-10-add-payment-merchant-pools/.openspec.yaml similarity index 100% rename from openspec/changes/add-payment-merchant-pools/.openspec.yaml rename to openspec/changes/archive/2026-09-10-add-payment-merchant-pools/.openspec.yaml diff --git a/openspec/changes/add-payment-merchant-pools/design.md b/openspec/changes/archive/2026-09-10-add-payment-merchant-pools/design.md similarity index 100% rename from openspec/changes/add-payment-merchant-pools/design.md rename to openspec/changes/archive/2026-09-10-add-payment-merchant-pools/design.md diff --git a/openspec/changes/add-payment-merchant-pools/proposal.md b/openspec/changes/archive/2026-09-10-add-payment-merchant-pools/proposal.md similarity index 100% rename from openspec/changes/add-payment-merchant-pools/proposal.md rename to openspec/changes/archive/2026-09-10-add-payment-merchant-pools/proposal.md diff --git a/openspec/changes/add-payment-merchant-pools/specs/merchant-payment-routing/spec.md b/openspec/changes/archive/2026-09-10-add-payment-merchant-pools/specs/merchant-payment-routing/spec.md similarity index 100% rename from openspec/changes/add-payment-merchant-pools/specs/merchant-payment-routing/spec.md rename to openspec/changes/archive/2026-09-10-add-payment-merchant-pools/specs/merchant-payment-routing/spec.md diff --git a/openspec/changes/add-payment-merchant-pools/tasks.md b/openspec/changes/archive/2026-09-10-add-payment-merchant-pools/tasks.md similarity index 100% rename from openspec/changes/add-payment-merchant-pools/tasks.md rename to openspec/changes/archive/2026-09-10-add-payment-merchant-pools/tasks.md diff --git a/openspec/specs/merchant-payment-routing/spec.md b/openspec/specs/merchant-payment-routing/spec.md new file mode 100644 index 0000000..992e932 --- /dev/null +++ b/openspec/specs/merchant-payment-routing/spec.md @@ -0,0 +1,103 @@ +# 商户支付路由当前行为 + +## Purpose + +管理实际收款商户、商户池轮询和全局微信授权配置,使新线上支付的收款身份可冻结、历史支付可继续使用其原商户,并避免凭证泄露或无配置时静默回退;本能力不新增任何支付渠道退款能力。 + +## Requirements + +### Requirement: 商户与微信授权配置管理 +系统 SHALL 将实际收款商户与微信授权配置分离。一个商户 MUST 仅对应 `wechat` 或 `alipay` 一种支付方式,并保存名称、支付方式、服务商类型、商户号或应用标识、敏感凭证、状态和备注;微信直连与富友均为微信支付商户。平台最多存在一个启用的微信授权配置,该配置保存 C 端公众号 H5/JSSDK、小程序登录所需参数,C 端微信登录、OpenID 和微信支付 AppID MUST 只读取该配置。 + +超级管理员和平台用户可创建、编辑、启用、停用商户、商户池和微信授权配置,其他角色无管理入口。仅上述角色的专用管理列表和详情响应可返回完整凭证;日志、审计快照、错误、导出、支付快照和其他业务响应 MUST NOT 保存或返回敏感凭证。被支付单引用的商户 MUST NOT 删除且其支付方式、服务商类型、商户号/应用标识不得修改;未被引用商户仅可移出所有商户池并经二次确认删除。停用只影响新支付单,历史支付的回调、查单和已有可达的原路退款仍使用该商户当前凭证;服务商无既有退款能力时保持不支持,本能力不得为此新增渠道退款调用或人工开关。 + +#### Scenario: 受引用商户停用 +- **WHEN** 管理员停用已被支付单命中的商户 +- **THEN** 新支付单不再选择该商户,已命中支付单的回调、查询和原路退款仍按该商户处理 + +#### Scenario: 非管理角色读取配置 +- **WHEN** 不具备超级管理员或平台用户身份的账号请求商户或微信授权配置 +- **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/名称快照、轮询方式快照及统计世代快照,但不得复制敏感凭证。支付、回调验签、查单和已有可达的原路退款读取该实际商户当前凭证;服务商类型和退款必需凭证完整性只用于已有退款路径的能力判定,不提供人工开关,也不得新增渠道退款能力。 + +上线迁移在存在唯一当前生效综合支付配置时复制完整凭证:具备完整凭证的微信/支付宝方式创建商户和各自单成员启用池,微信授权字段完整时创建全局微信授权配置;不完整的支付方式不建池,授权字段不完整时不建授权配置并使相关 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** 系统保留该兼容基线且不新增富友退款;未第三方实测可以记录,但不得作为本 Change 的实施、验证或归档阻塞 + +#### 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`(停用商户池)。