# 功能规格:门店级在线支付路由与支付商解耦 **功能标识**:`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__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//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 阶段。