# 功能规格：门店级在线支付路由与支付商解耦

**功能标识**：`023-payment-provider-routing`

**创建日期**：2026-08-31

**状态**：设计讨论暂停；已记录确认项，尚未进入 plan、tasks 或 implement

**2026-09-03 部分落地**：Apple Pay（payType=5）线上联调被 OMG 创建校验拒绝（只认 payType=2）。按本规格已确认的「信用卡与 Apple Pay 同组同商」语义，先落地渠道归组判断 `OrderLifecycleService.isCardWalletPayType`（2 或 5 均走 OMG），替换了 OMG 创建校验、接单支付门槛、状态推进在线支付要求、平台交易号查询四处单值比较。门店级路由表与支付商切换仍按本规格后续推进。

**关联规格**：

- `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 阶段。
