spec.md 9.8 KB

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

功能标识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_CARDAPPLE_PAY
  • LINE Pay 固定使用 providerCode=LINE_PAY,不读取可切换的银行卡类路由。
  • 路由由平台管理员配置和切换,商家无权操作。
  • 新支付商凭证按门店独立保存。只有凭证验证通过,并确认信用卡和 Apple Pay 两项能力都可用后,才能切换该门店路由。
  • 配置的支付商不可用、凭证失效或创建支付失败时,不允许自动切换到备用支付商。

1.3 切换影响范围

  • 支付商切换只影响切换后创建的新支付尝试。
  • 每个支付尝试创建时必须永久绑定实际支付商、凭证版本和路由版本。
  • 已创建但结果未明的旧支付尝试继续由原支付商查询和接收回调。
  • 已付款订单的查询、对账和退款继续使用原支付商。
  • 只有旧尝试被可信确认失败、过期或不存在后,重试才能读取门店当前路由并创建新尝试。
  • 切换后不得把历史 OMG 交易发送给新支付商查询或退款。

2. 已选架构方案

采用“通用支付编排层 + 支付商适配器”,保留支付商协议实现,不在 Controller 中不断增加支付商条件分支。

App
  -> 通用在线支付接口
  -> PaymentOrchestrationService
       |-- StorePaymentRouteService
       |-- PaymentAttemptService
       `-- PaymentProviderRegistry
            |-- OmgPaymentProviderAdapter
            |-- LinePayProviderAdapter
            `-- FuturePaymentProviderAdapter

2.1 适配器职责

每个支付商适配器负责本支付商的:

  • 凭证读取和能力验证;
  • 签名、创建支付和原始响应校验;
  • 查询、退款和对账;
  • 回调验签及供应商事实解析;
  • 供应商返回向统一支付状态和 checkoutAction 的转换。

支付商通过注册表按 providerCode 选择,Controller 不包含 if OMG / else 业务分支。

3. 已确认的数据结构方向

采用“通用主表 + 各支付商一对一明细表”,LINE Pay 在逻辑上进入通用支付账本,但不把 LINE Pay 专属字段物理合并进通用表。

pos_payment_attempt
  |-- pos_order_omg_attempt
  |-- pos_order_line_payment
  `-- pos_order_<future_provider>_payment

3.1 pos_payment_attempt

通用支付尝试主表至少负责保存:

  • 公开支付尝试编号、订单号和门店 ID;
  • payment_methodCREDIT_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_idtransaction_id、LINE 凭证版本、Web/App 地址、Capture 金额、LINE 精确状态和对账租约。
  • 未来支付商使用自己的凭证表和支付明细表。
  • 每张支付商明细表通过唯一 payment_attempt_id 与通用主表一对一关联。

3.3 门店路由

新增门店支付路由,概念键为:

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,客户端支付入口直接调整为供应商无关接口:

POST /pay/online/create
POST /pay/online/query
POST /pay/online/retry
POST /pay/online/refund
GET  /pay/online/methods?storeId=...

由于订单 payType 已能唯一确定三种支付方式,创建接口只接收订单号:

{
  "orderId": "业务订单号"
}

客户端不得提交支付商、金额、商户交易号、凭证、网关地址或支付商原始字段。后端从订单读取支付方式、归属、门店和金额,再确定固定 LINE Pay 或门店银行卡类路由。

创建成功响应统一包含:

{
  "attemptId": "公开支付尝试编号",
  "orderId": "业务订单号",
  "paymentMethod": "CREDIT_CARD",
  "providerCode": "OMG",
  "providerName": "OMG",
  "status": "READY",
  "checkoutAction": {
    "type": "WEB_FORM_POST",
    "url": "支付地址",
    "fields": {}
  }
}

4.3 支付商回调

第三方回调的签名、请求格式和响应文本不同,不强制合并为一个外部协议入口。各支付商保留固定回调或浏览器返回路径,例如:

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 通用状态草案

CREATING
PENDING
PAID
FAILED
SUPERSEDED
REFUNDING
REFUNDED
MANUAL_REVIEW

待继续讨论:通用状态与 OMG、LINE Pay 精确状态的映射,活动键释放条件,支付成功迟到、重复付款及人工处理边界。

6.2 异常与退款草案

  • PAID/REFUNDED 不可逆。
  • 结果未知时继续阻止创建其他支付商尝试。
  • 退款永远通过原支付尝试定位原支付商,不读取门店当前路由。
  • 支付商原始错误转换为稳定业务错误码,响应和日志不得泄露凭证、完整签名或敏感原文。

待继续讨论:错误码清单、退款状态机、对账策略、人工处理入口、迟到成功后的处理方式。

6.3 尚未开始的部分

  • 测试场景与验收矩阵;
  • 现有 OMG、LINE Pay 代码向适配器迁移的分批顺序;
  • 新表完整字段、索引、唯一约束和 SQL;
  • 平台端凭证验证、路由切换页面与四语言 i18n;
  • 规格 plan.mdtasks.md 和实施计划;
  • JDK 21 定向测试、模块构建和完整回归方案。

7. 当前结论

本规格只记录 2026-08-31 已完成的架构讨论。当前没有授权实施,不修改生产代码、不执行数据库迁移,也不把待确认草案作为最终验收标准。后续继续讨论时,应从第 6 节开始确认,然后再进入 spec-kit 的 plan、tasks 和 implement 阶段。