Преглед на файлове

记录门店级在线支付路由设计

整理已确认的支付方式、门店路由、通用支付账本、支付商适配器和客户端统一接口设计。`n明确标记尚待继续讨论的状态机、退款、测试及实施计划,当前不修改生产代码。
qmj преди 49 минути
родител
ревизия
1eb3ff4ce4
променени са 1 файла, в които са добавени 256 реда и са изтрити 0 реда
  1. 256 0
      specs/023-payment-provider-routing/spec.md

+ 256 - 0
specs/023-payment-provider-routing/spec.md

@@ -0,0 +1,256 @@
+# 功能规格:门店级在线支付路由与支付商解耦
+
+**功能标识**:`023-payment-provider-routing`
+
+**创建日期**:2026-08-31
+
+**状态**:设计讨论暂停;已记录确认项,尚未进入 plan、tasks 或 implement
+
+**关联规格**:
+
+- `specs/019-line-pay`
+- `specs/020-omg-payment-rebuild`
+
+**背景**:当前订单 `payType=2`、OMG 创建接口、OMG 支付尝试表及查询/退款入口均直接绑定 OMG;LINE Pay 则使用另一套 Controller、Service 和支付尝试表。后续可能因费率变化,将信用卡与 Apple Pay 从 OMG 整体切换到其他支付商,因此需要把用户支付方式、门店支付路由和实际支付商分离。
+
+## 1. 本轮已确认的目标与边界
+
+### 1.1 在线支付方式
+
+用户端在线支付只保留以下三种稳定支付方式:
+
+| `PosOrder.payType` | 统一支付方式 | 说明 |
+|---|---|---|
+| `2` | `CREDIT_CARD` | 信用卡支付,不再表示 OMG |
+| `3` | `LINE_PAY` | LINE Pay,支付商固定为 LINE Pay |
+| `5` | `APPLE_PAY` | Apple Pay |
+
+现有数据库中 `pay_type=5` 的历史订单已确认没有业务价值,因此可把 `5` 重新定义为 Apple Pay,不保留原“余额支付”语义。
+
+### 1.2 门店支付路由
+
+- 按门店配置银行卡类支付商。
+- 信用卡与 Apple Pay 必须使用同一个支付商并统一切换,不允许为同一门店分别选择不同支付商。
+- 门店路由使用稳定分组 `CARD_WALLET`,同时覆盖 `CREDIT_CARD` 与 `APPLE_PAY`。
+- LINE Pay 固定使用 `providerCode=LINE_PAY`,不读取可切换的银行卡类路由。
+- 路由由平台管理员配置和切换,商家无权操作。
+- 新支付商凭证按门店独立保存。只有凭证验证通过,并确认信用卡和 Apple Pay 两项能力都可用后,才能切换该门店路由。
+- 配置的支付商不可用、凭证失效或创建支付失败时,不允许自动切换到备用支付商。
+
+### 1.3 切换影响范围
+
+- 支付商切换只影响切换后创建的新支付尝试。
+- 每个支付尝试创建时必须永久绑定实际支付商、凭证版本和路由版本。
+- 已创建但结果未明的旧支付尝试继续由原支付商查询和接收回调。
+- 已付款订单的查询、对账和退款继续使用原支付商。
+- 只有旧尝试被可信确认失败、过期或不存在后,重试才能读取门店当前路由并创建新尝试。
+- 切换后不得把历史 OMG 交易发送给新支付商查询或退款。
+
+## 2. 已选架构方案
+
+采用“通用支付编排层 + 支付商适配器”,保留支付商协议实现,不在 Controller 中不断增加支付商条件分支。
+
+```text
+App
+  -> 通用在线支付接口
+  -> PaymentOrchestrationService
+       |-- StorePaymentRouteService
+       |-- PaymentAttemptService
+       `-- PaymentProviderRegistry
+            |-- OmgPaymentProviderAdapter
+            |-- LinePayProviderAdapter
+            `-- FuturePaymentProviderAdapter
+```
+
+### 2.1 适配器职责
+
+每个支付商适配器负责本支付商的:
+
+- 凭证读取和能力验证;
+- 签名、创建支付和原始响应校验;
+- 查询、退款和对账;
+- 回调验签及供应商事实解析;
+- 供应商返回向统一支付状态和 `checkoutAction` 的转换。
+
+支付商通过注册表按 `providerCode` 选择,Controller 不包含 `if OMG / else` 业务分支。
+
+## 3. 已确认的数据结构方向
+
+采用“通用主表 + 各支付商一对一明细表”,LINE Pay 在逻辑上进入通用支付账本,但不把 LINE Pay 专属字段物理合并进通用表。
+
+```text
+pos_payment_attempt
+  |-- pos_order_omg_attempt
+  |-- pos_order_line_payment
+  `-- pos_order_<future_provider>_payment
+```
+
+### 3.1 `pos_payment_attempt`
+
+通用支付尝试主表至少负责保存:
+
+- 公开支付尝试编号、订单号和门店 ID;
+- `payment_method`:`CREDIT_CARD/LINE_PAY/APPLE_PAY`;
+- `provider_code` 和支付商展示名称快照;
+- 金额与币种;
+- 统一支付状态;
+- `checkout_action_type`;
+- 凭证版本和路由版本;
+- 跨支付商唯一活动尝试约束;
+- 支付、退款、创建和更新时间。
+
+通用主表负责统一路由、客户端查询、历史支付商定位和跨支付商防重复支付。
+
+### 3.2 支付商明细表
+
+- `pos_order_omg_attempt` 继续保存 OMG 交易号、凭证快照、CheckMacValue 相关事实、OMG 回调和查询字段。
+- `pos_order_line_payment` 继续保存 `line_order_id`、`transaction_id`、LINE 凭证版本、Web/App 地址、Capture 金额、LINE 精确状态和对账租约。
+- 未来支付商使用自己的凭证表和支付明细表。
+- 每张支付商明细表通过唯一 `payment_attempt_id` 与通用主表一对一关联。
+
+### 3.3 门店路由
+
+新增门店支付路由,概念键为:
+
+```text
+store_id + route_group=CARD_WALLET
+  -> provider_code
+  -> provider_credential_id
+  -> route_version
+  -> enabled/status
+```
+
+路由切换必须使用事务和版本条件原子完成,并记录旧支付商、新支付商、凭证版本、操作人和操作时间。
+
+## 4. 已确认的客户端契约
+
+App 只认识稳定支付方式和客户端动作类型,不根据支付商名称执行不同支付逻辑。
+
+### 4.1 客户端动作
+
+首批动作类型包括:
+
+- `WEB_FORM_POST`:在当前 WebView 向指定地址提交服务端返回的表单字段;
+- `WEB_REDIRECT`:在当前支付 WebView 打开指定地址;
+- `NATIVE_SDK`:为未来必须使用原生 SDK 的支付商预留,实际接入时仍需 App 增加相应 SDK 能力。
+
+后端同时返回:
+
+- `providerCode`:稳定机器码;
+- `providerName`:展示名称;
+- `checkoutAction.type`:App 支付流程的唯一分流依据。
+
+`providerCode/providerName` 可用于展示、日志和客服定位,但 App 不得据此决定支付流程。
+
+### 4.2 通用接口
+
+测试阶段不兼容旧 App,客户端支付入口直接调整为供应商无关接口:
+
+```text
+POST /pay/online/create
+POST /pay/online/query
+POST /pay/online/retry
+POST /pay/online/refund
+GET  /pay/online/methods?storeId=...
+```
+
+由于订单 `payType` 已能唯一确定三种支付方式,创建接口只接收订单号:
+
+```json
+{
+  "orderId": "业务订单号"
+}
+```
+
+客户端不得提交支付商、金额、商户交易号、凭证、网关地址或支付商原始字段。后端从订单读取支付方式、归属、门店和金额,再确定固定 LINE Pay 或门店银行卡类路由。
+
+创建成功响应统一包含:
+
+```json
+{
+  "attemptId": "公开支付尝试编号",
+  "orderId": "业务订单号",
+  "paymentMethod": "CREDIT_CARD",
+  "providerCode": "OMG",
+  "providerName": "OMG",
+  "status": "READY",
+  "checkoutAction": {
+    "type": "WEB_FORM_POST",
+    "url": "支付地址",
+    "fields": {}
+  }
+}
+```
+
+### 4.3 支付商回调
+
+第三方回调的签名、请求格式和响应文本不同,不强制合并为一个外部协议入口。各支付商保留固定回调或浏览器返回路径,例如:
+
+```text
+POST /pay/provider/omg/notify
+POST /pay/provider/<future-provider>/notify
+GET  /pay/provider/line/confirm
+GET  /pay/provider/line/cancel
+```
+
+支付商入口只处理协议解析和验签,之后必须通过支付商明细定位 `pos_payment_attempt`,进入统一支付状态处理。App 不调用这些路径。
+
+旧 `/pay/omg/create|query|retry|refund` 和 `/pay/line/create|query` 不作为新客户端兼容接口保留。当前均为测试数据,不要求回填旧支付尝试。
+
+## 5. 已确认的创建与路由规则
+
+1. App 创建订单时写入 `payType=2/3/5` 中的一种。
+2. App 调用通用创建接口并只提交订单号。
+3. 后端验证订单归属、状态、金额,以及 `payType` 是否为受支持的在线支付方式。
+4. `payType=3` 固定选择 LINE Pay;`payType=2/5` 读取门店当前 `CARD_WALLET` 路由。
+5. 后端锁定订单,并在通用主表预占跨支付商唯一活动尝试。
+6. 本次尝试保存实际支付商、凭证版本和路由版本。
+7. 适配器执行支付商创建流程,并转换为统一 `checkoutAction`。
+8. 支付商不可用或创建失败时不读取备用支付商。
+
+支付商创建结果必须能区分:
+
+- `READY`:支付入口已明确创建,可返回客户端动作;
+- `REJECTED`:支付商明确拒绝且确认没有产生交易,可结束本次尝试;
+- `UNKNOWN`:超时、断线或响应无法验证,必须保留活动尝试并查询原支付商,禁止改走其他支付商。
+
+## 6. 待继续确认的设计草案
+
+以下内容在暂停前已经提出,但用户尚未逐项确认,因此不得视为最终需求:
+
+### 6.1 通用状态草案
+
+```text
+CREATING
+PENDING
+PAID
+FAILED
+SUPERSEDED
+REFUNDING
+REFUNDED
+MANUAL_REVIEW
+```
+
+待继续讨论:通用状态与 OMG、LINE Pay 精确状态的映射,活动键释放条件,支付成功迟到、重复付款及人工处理边界。
+
+### 6.2 异常与退款草案
+
+- `PAID/REFUNDED` 不可逆。
+- 结果未知时继续阻止创建其他支付商尝试。
+- 退款永远通过原支付尝试定位原支付商,不读取门店当前路由。
+- 支付商原始错误转换为稳定业务错误码,响应和日志不得泄露凭证、完整签名或敏感原文。
+
+待继续讨论:错误码清单、退款状态机、对账策略、人工处理入口、迟到成功后的处理方式。
+
+### 6.3 尚未开始的部分
+
+- 测试场景与验收矩阵;
+- 现有 OMG、LINE Pay 代码向适配器迁移的分批顺序;
+- 新表完整字段、索引、唯一约束和 SQL;
+- 平台端凭证验证、路由切换页面与四语言 i18n;
+- 规格 `plan.md`、`tasks.md` 和实施计划;
+- JDK 21 定向测试、模块构建和完整回归方案。
+
+## 7. 当前结论
+
+本规格只记录 2026-08-31 已完成的架构讨论。当前没有授权实施,不修改生产代码、不执行数据库迁移,也不把待确认草案作为最终验收标准。后续继续讨论时,应从第 6 节开始确认,然后再进入 spec-kit 的 plan、tasks 和 implement 阶段。