瀏覽代碼

文档:规划 OMG 信用卡与 Apple Pay 双入口

测试环境改为由 App 选择受控支付渠道,后端分别映射信用卡与 Apple Pay 表单参数。
同步规格、API 契约、App 交接、任务与验收说明。
qmj 1 周之前
父節點
當前提交
e7e85c54bb

+ 12 - 7
specs/020-omg-payment-rebuild/contracts/api.md

@@ -16,16 +16,18 @@ Content-Type: application/json
 token: <login-token>
 
 {
-  "orderId": "991786433092835"
+  "orderId": "991786433092835",
+  "paymentMethod": "CREDIT"
 }
 ```
 
 Rules:
 
-- DTO has exactly one field: `orderId`.
+- DTO has exactly two fields: `orderId` and `paymentMethod`.
 - Controller uses explicit `@RequestBody(required = false)`.
 - `orderId` is trimmed, non-empty and at most 64 characters.
-- Client cannot provide amount, MerchantID, gateway URL, ReturnURL, text, channel or signature fields.
+- `paymentMethod` must be exactly `CREDIT` or `APPLE_PAY`.
+- Client cannot provide amount, MerchantID, gateway URL, ReturnURL, text, raw OMG payment parameters or signature fields.
 
 ### Success
 
@@ -45,8 +47,8 @@ Outer response keeps the project `AjaxResult` format. `data` is:
     "ItemName": "Order 991786433092835",
     "ReturnURL": "https://foodieapi.waimai-paotui.com/pay/omg/notify",
     "OrderResultURL": "https://foodieapi.waimai-paotui.com/pay/omg/result",
-    "ChoosePayment": "ALL",
-    "IgnorePayment": "ATM#CVS#BarcodeATM",
+    "ChoosePayment": "Credit",
+    "UnionPay": "2",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",
@@ -55,6 +57,8 @@ Outer response keeps the project `AjaxResult` format. `data` is:
 }
 ```
 
+The example above is the `CREDIT` variant. For `APPLE_PAY`, `ChoosePayment` is `ApplePay`, and `UnionPay` and `IgnorePayment` are absent. `ChoosePayment=ALL` is never generated.
+
 The client must submit every `formFields` entry in the current page as an `application/x-www-form-urlencoded` POST to `gatewayUrl`. It must not use iframe, a new window or a GET link.
 
 ### Business failure
@@ -81,6 +85,7 @@ Stable statuses:
 | `ORDER_ALREADY_PAID` | `pay_status` is not 0 |
 | `ORDER_AMOUNT_INVALID` | Amount is null or not positive |
 | `PAYMENT_TYPE_INVALID` | `pay_type` is not `"2"` |
+| `PAYMENT_METHOD_INVALID` | `paymentMethod` is missing or is not `CREDIT/APPLE_PAY` |
 | `STORE_CREDENTIAL_UNAVAILABLE` | No enabled credential exists for the order store |
 | `PAYMENT_ATTEMPT_EXISTS` | The order already has an active `CREATED` attempt |
 | `PAYMENT_CONFIGURATION_INVALID` | Stage/ReturnURL safety validation failed |
@@ -159,7 +164,7 @@ The response whitelist includes normalized `status` (`PAID`, `UNPAID`, `FAILED`,
 Header `token` is required. The explicit JSON DTO contains only:
 
 ```json
-{"orderId":"991786433092835"}
+{"orderId":"991786433092835","paymentMethod":"APPLE_PAY"}
 ```
 
 The endpoint is used after the App has destroyed the original payment WebView and the user explicitly starts payment again. The server never returns or replays the old form. It first executes the same authenticated, signed gateway query as `/query`:
@@ -171,7 +176,7 @@ The endpoint is used after the App has destroyed the original payment WebView an
 
 At most one retry request can replace a given active attempt. A late trusted paid callback for a `SUPERSEDED` attempt remains eligible for the irreversible paid transition and closes any newer active attempt.
 
-Success has the same response contract as `/create`: `{status:"CREATED", gatewayUrl, formFields}` with a new `MerchantTradeNo`. Business errors include the existing query/create errors plus `PAYMENT_RETRY_NOT_AVAILABLE`; unexpected failures return `PAYMENT_RETRY_FAILED`. The client cannot send `MerchantTradeNo`, payment type, amount, credentials, gateway URL, or old form fields.
+Success has the same response contract as `/create`: `{status:"CREATED", gatewayUrl, formFields}` with a new `MerchantTradeNo` and fields for the newly selected `paymentMethod`. Business errors include the existing query/create errors plus `PAYMENT_RETRY_NOT_AVAILABLE`; unexpected failures return `PAYMENT_RETRY_FAILED`. The client cannot send `MerchantTradeNo`, OMG `paymentType`, raw OMG payment parameters, amount, credentials, gateway URL, or old form fields.
 
 ## POST `/pay/omg/refund`
 

+ 41 - 27
specs/020-omg-payment-rebuild/omg-app-integration.md

@@ -3,8 +3,8 @@
 **适用对象**:用户端 App(uni-app)前端开发人员
 **接口版本**:`specs/020-omg-payment-rebuild` 新实现
 **当前环境**:OMG 测试环境
-**支付方式目标**:OMG 官方页面优先展示信用卡、Apple Pay
-**更新时间**:2026-08-14
+**支付方式目标**:App 只提供信用卡、Apple Pay,OMG 直接进入所选渠道
+**更新时间**:2026-08-18
 
 > 本文只描述当前新 OMG 实现。`specs/016-omg-payment` 中的旧 Controller、旧流水、旧退款和旧前端文档已经退役,不得作为接入依据。
 
@@ -13,13 +13,14 @@
 OMG 是网页托管式收银台,App 不需要接入 OMG SDK。前端只需要完成以下流程:
 
 1. 创建订单时把 OMG 支付方式设置为 `payType = "2"`。
-2. 调用 `POST /pay/omg/create` 获取 `gatewayUrl` 和完整的 `formFields`。
-3. 在当前 App WebView 页面中,把所有 `formFields` 原样以 HTML Form POST 到 `gatewayUrl`。
-4. 用户付款后,OMG 将浏览器 POST 到后端 `/pay/omg/result`;后端验证返回资料后,优先通过 uni-app Bridge 打开支付结果页,Bridge 不可用时再通过 App Scheme 兜底。
-5. App 结果页取得 `ddId`,调用 `POST /pay/omg/query` 确认最终支付状态。
-6. 只有 query 返回 `status = "PAID"` 才能展示支付成功。
+2. 在 App 自有界面让用户选择信用卡或 Apple Pay,分别使用 `paymentMethod = "CREDIT"` 或 `"APPLE_PAY"`。
+3. 调用 `POST /pay/omg/create` 获取所选渠道的 `gatewayUrl` 和完整 `formFields`。
+4. 在当前 App WebView 页面中,把所有 `formFields` 原样以 HTML Form POST 到 `gatewayUrl`。
+5. 用户付款后,OMG 将浏览器 POST 到后端 `/pay/omg/result`;后端验证返回资料后,优先通过 uni-app Bridge 打开支付结果页,Bridge 不可用时再通过 App Scheme 兜底。
+6. App 结果页取得 `ddId`,调用 `POST /pay/omg/query` 确认最终支付状态。
+7. 只有 query 返回 `status = "PAID"` 才能展示支付成功。
 
-本次渠道过滤由后端表单完成。App 顶层仍然只显示“OMG 支付”和“LINE Pay”,不增加“信用卡/Apple Pay”二级选择页面,也不接入原生 Apple Pay SDK
+App 需要新增“信用卡 / Apple Pay”选择,但不接入原生 Apple Pay SDK。后端只接受两个稳定枚举并生成单渠道签名表单,因此 OMG 页面不会再展示超商快付、AFTEE 或其他渠道选择项。用户可见文案必须接入 App 现有 i18n
 
 当前测试环境不支持退款。`POST /pay/omg/refund` 只会返回“测试环境不支持退款”,不会发起真实退款,也不会修改订单。
 
@@ -27,16 +28,19 @@ OMG 是网页托管式收银台,App 不需要接入 OMG SDK。前端只需要
 
 ```text
 [App 创建订单,payType="2"]
+             |
+             v
+[App 选择 CREDIT / APPLE_PAY]
              |
              | POST /pay/omg/create
              | Header: token
-             | Body: {"orderId":"..."}
+             | Body: {"orderId":"...","paymentMethod":"..."}
              v
 [后端返回 gatewayUrl + formFields]
              |
              | 当前 WebView 原样 Form POST
              v
-[OMG 测试收银台:优先显示信用卡 / Apple Pay]
+[OMG 测试环境:直接进入所选付款流程]
              |
              +-------------------------------+
              |                               |
@@ -117,12 +121,14 @@ token 只发送给本项目后端,绝对不能放入 OMG Form,也不能发
 
 ### 3.4 订单号字段
 
-接口请求统一使用:
+create/retry 请求使用:
 
 ```json
-{"orderId":"业务订单号"}
+{"orderId":"业务订单号","paymentMethod":"CREDIT 或 APPLE_PAY"}
 ```
 
+query/refund 请求仍使用 `{"orderId":"业务订单号"}`。
+
 后端 App Scheme 回跳参数使用:
 
 ```text
@@ -141,7 +147,8 @@ Content-Type: application/json
 token: <用户登录 token>
 
 {
-  "orderId": "991786433092835"
+  "orderId": "991786433092835",
+  "paymentMethod": "CREDIT"
 }
 ```
 
@@ -154,7 +161,7 @@ token: <用户登录 token>
 - 订单门店已启用有效 OMG 凭证。
 - 同一订单当前不能存在另一个未结束的 OMG 支付尝试。
 
-客户端只能传 `orderId`。金额、商户号、回调地址、网关地址、付款渠道和签名都由后端决定。
+create/retry 只能传 `orderId` 与 `paymentMethod`;`paymentMethod` 仅允许 `CREDIT/APPLE_PAY`。query/refund 仍只传 `orderId`。金额、商户号、回调地址、网关地址、OMG 原始付款参数和签名都由后端决定。
 
 ### 4.2 成功响应
 
@@ -177,8 +184,8 @@ token: <用户登录 token>
       "ItemName": "Order 991786433092835",
       "ReturnURL": "https://foodieapi.waimai-paotui.com/pay/omg/notify",
       "OrderResultURL": "https://foodieapi.waimai-paotui.com/pay/omg/result",
-      "ChoosePayment": "ALL",
-      "IgnorePayment": "ATM#CVS#BarcodeATM",
+      "ChoosePayment": "Credit",
+      "UnionPay": "2",
       "EncryptType": "1",
       "InvoiceMark": "N",
       "NeedExtraPaidInfo": "Y",
@@ -188,6 +195,8 @@ token: <用户登录 token>
 }
 ```
 
+上例为信用卡响应。Apple Pay 响应的 `ChoosePayment` 为 `ApplePay`,且不包含 `UnionPay` 或 `IgnorePayment`。后端不会返回 `ChoosePayment=ALL`。
+
 字段说明:
 
 | 字段 | 类型 | App 用法 |
@@ -197,8 +206,8 @@ token: <用户登录 token>
 | `formFields` | object | 所有键值都必须原样创建为隐藏 input 并 POST |
 | `MerchantTradeNo` | string | 后端生成的 OMG 交易编号;前端只透传,不作为业务订单号 |
 | `TotalAmount` | string | 后端根据订单金额生成;前端不得修改 |
-| `ChoosePayment` | string | 当前固定 `ALL`,使用 OMG 官方付款方式选择页面;前端不得修改 |
-| `IgnorePayment` | string | 当前固定 `ATM#CVS#BarcodeATM`,由 OMG 隐藏 ATM、CVS 和 BarcodeATM;前端不得修改 |
+| `ChoosePayment` | string | 后端按 `paymentMethod` 生成 `Credit` 或 `ApplePay`;前端不得修改 |
+| `UnionPay` | string | 仅信用卡表单存在且固定为 `2`,用于隐藏银联;Apple Pay 表单不包含该字段 |
 | `ReturnURL` | string | OMG 服务端付款通知地址;前端不调用 |
 | `OrderResultURL` | string | OMG 浏览器付款结果地址;前端不直接调用 |
 | `CheckMacValue` | string | 表单签名;修改任意已签名字段都会导致 OMG 拒绝 |
@@ -251,7 +260,7 @@ function submitOmgForm(gatewayUrl, formFields) {
 发起页示例:
 
 ```js
-async function startOmgPayment(orderId) {
+async function startOmgPayment(orderId, paymentMethod) {
   if (this.omgSubmitting) return;
   this.omgSubmitting = true;
 
@@ -260,7 +269,7 @@ async function startOmgPayment(orderId) {
       url: '/pay/omg/create',
       method: 'POST',
       header: { token: uni.getStorageSync('token') },
-      data: { orderId }
+      data: { orderId, paymentMethod }
     });
 
     if (response.code !== 200) {
@@ -311,6 +320,7 @@ submitOmgForm(payload.gatewayUrl, payload.formFields);
 | `ORDER_ALREADY_PAID` | 订单已经付款 | 进入订单结果页并刷新状态 |
 | `ORDER_AMOUNT_INVALID` | 订单金额异常 | 展示后端 `msg` |
 | `PAYMENT_TYPE_INVALID` | 订单 `payType` 不是 `"2"` | 检查创建订单时的支付方式 |
+| `PAYMENT_METHOD_INVALID` | `paymentMethod` 缺失或不是 `CREDIT/APPLE_PAY` | 停止支付并检查 App 渠道映射 |
 | `STORE_CREDENTIAL_UNAVAILABLE` | 门店未启用有效 OMG 凭证 | 展示后端 `msg` |
 | `PAYMENT_ATTEMPT_EXISTS` | 已有未结束的支付尝试 | 用户确认重新支付时调用 `/pay/omg/retry`,不要循环调用 create |
 | `PAYMENT_CONFIGURATION_INVALID` | 后端测试网关或回调配置不合法 | 展示后端 `msg`,通知后端排查 |
@@ -513,7 +523,8 @@ Content-Type: application/json
 token: <用户登录 token>
 
 {
-  "orderId": "991786433092835"
+  "orderId": "991786433092835",
+  "paymentMethod": "APPLE_PAY"
 }
 ```
 
@@ -543,17 +554,17 @@ retry 成功响应与 create 完全相同。App 收到后创建新的支付 WebV
 }
 ```
 
-`paymentType` 是 OMG 查询结果,不是 App 传参,也不要求 App 增加“信用卡/Apple Pay”二级选择。用户界面仍然只有“OMG 支付”和“LINE Pay”
+`paymentType` 是 OMG 查询结果,不是 App 传参;`paymentMethod` 才是 App 本次选择并提交给 create/retry 的受控枚举。用户重新支付时可以重新选择信用卡或 Apple Pay
 
 App 推荐处理:
 
 ```js
-async function retryOmgPayment(orderId) {
+async function retryOmgPayment(orderId, paymentMethod) {
   const response = await request({
     url: '/pay/omg/retry',
     method: 'POST',
     header: { token: uni.getStorageSync('token') },
-    data: { orderId }
+    data: { orderId, paymentMethod }
   });
 
   if (response.code === 200) {
@@ -651,7 +662,7 @@ token: <用户登录 token>
 10. `tradeNo`、`merchantTradeNo` 始终按字符串处理,不转换为 JavaScript Number。
 11. query 会访问 OMG 网关,不做高频轮询。
 12. `PAYMENT_ATTEMPT_EXISTS` 不能靠重复调用 create 解决,应进入 query 确认流程。
-13. 后端通过 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM` 保留信用卡和 Apple Pay;AFTEE 必须由 OMG 商户侧关闭,App 不得通过 CSS 或 DOM 注入修改第三方支付页面。
+13. App 只传 `CREDIT/APPLE_PAY`;不得传 `ChoosePayment`、`UnionPay`、`IgnorePayment` 等 OMG 原始字段,也不得通过 CSS 或 DOM 修改第三方支付页面。
 14. 当前没有真实退款能力。
 
 ## 10. App 验收清单
@@ -659,12 +670,15 @@ token: <用户登录 token>
 ### 10.1 创建与收银台
 
 - [ ] 创建订单时 OMG 使用 `payType = "2"`。
-- [ ] create 请求 Header 包含 token,JSON 只传 `orderId`。
+- [ ] App 只显示信用卡与 Apple Pay,并把选择映射为 `CREDIT/APPLE_PAY`;用户可见文案使用现有 i18n。
+- [ ] create 请求 Header 包含 token,JSON 只传 `orderId` 与 `paymentMethod`。
 - [ ] 支付按钮有防重复点击处理。
 - [ ] create 成功后,当前完整 WebView 使用 Form POST 打开 `gatewayUrl`。
 - [ ] 所有 `formFields` 原样提交,`gatewayUrl` 不作为 input。
 - [ ] 没有 iframe、GET 跳转或新窗口。
-- [ ] 测试收银台显示正确金额,信用卡可用,Apple Pay 在门店已开通且设备支持时可用,ATM/CVS/BarcodeATM 已隐藏,AFTEE 已在 OMG 商户侧关闭。
+- [ ] `CREDIT` 直接进入信用卡流程,表单为 `ChoosePayment=Credit + UnionPay=2`,不显示其他渠道。
+- [ ] `APPLE_PAY` 在门店已开通且设备支持时直接进入 Apple Pay 流程,表单为 `ChoosePayment=ApplePay`,不显示其他渠道。
+- [ ] 两种表单都不包含 `ChoosePayment=ALL` 或 `IgnorePayment`,页面不出现超商快付、AFTEE。
 
 ### 10.2 App 返回与状态确认
 

+ 34 - 3
specs/020-omg-payment-rebuild/plan.md

@@ -16,8 +16,8 @@
 - `ruoyi-admin -> ruoyi-system`;`ruoyi-system` 禁止导入 `com.ruoyi.app.*`。
 - 每个门店独立使用 `pos_store_omg` 中已启用的 `MerchantID / HashKey / HashIV`;不修改可信凭证存储与管理代码。
 - 新创建入口固定 `POST /pay/omg/create`;Controller 使用 `@RequestHeader String token`、显式 `@RequestBody` DTO,禁止 Map 入参和 Bean Validation。
-- 客户端请求只含 `orderId`;金额、门店、网关、ReturnURL、说明、支付方式和签名全部由服务端产生。
-- `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`、`NeedExtraPaidInfo=Y`;OMG 官方页面保留信用卡和 Apple Pay,AFTEE 由 OMG 商户侧关闭。Apple Pay 是否显示取决于门店开通状态和设备环境。所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
+- create/retry 请求只含 `orderId` 与受控 `paymentMethod`;金额、门店、网关、ReturnURL、说明、OMG 原始支付参数和签名全部由服务端产生。
+- `paymentMethod` 仅允许 `CREDIT/APPLE_PAY`:分别映射为 `ChoosePayment=Credit + UnionPay=2` 与 `ChoosePayment=ApplePay`。禁止 `ChoosePayment=ALL` 和 `IgnorePayment`;所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
 - 回调必须对全部实际返回字段验签,包含未知额外字段与空值字段,只有 `CheckMacValue` 排除。
 - 网关固定 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`,不得配置正式环境回退。
 - 同一 `ddId` 最多一条 `CREATED`;存在时返回 `PAYMENT_ATTEMPT_EXISTS`,不重放旧表单、不生成新 MerchantTradeNo。
@@ -1124,7 +1124,7 @@ No constitution violations. The separate form factory, signer, generator and per
 ## 2026-08-14 已销毁 WebView 的重新支付
 
 1. App 首次付款仍调用 `/pay/omg/create`;退出支付页后不保存、不恢复原 WebView,也不重放旧表单。
-2. 用户明确再次付款时调用 `/pay/omg/retry`。retry 先通过可信 `/query` 链路向 OMG 核实旧交易,客户端不能指定交易号或支付渠道
+2. 用户明确再次付款时调用 `/pay/omg/retry`。retry 先通过可信 `/query` 链路向 OMG 核实旧交易;客户端只能重新选择受控 `paymentMethod`,不能指定交易号或 OMG 原始支付参数
 3. `PAID` 直接拒绝新建;`FAILED` 在旧尝试已释放后正常创建;`UNPAID` 仅允许 `paymentType` 为空的尝试被替换;其他状态 fail closed。
 4. `UNPAID` 替换在独立写事务中锁订单并再次验证订单,要求活动尝试交易号与 query 结果精确一致,然后以 `id + CREATED` 条件更新为 `SUPERSEDED` 并插入新尝试。
 5. 两个并发 retry 即使都查到同一旧交易,也只有先取得订单锁者能替换;后取得者看到活动交易号变化后返回 `PAYMENT_RETRY_NOT_AVAILABLE`。
@@ -1192,6 +1192,37 @@ mvn -pl ruoyi-admin -am -DskipTests package
 - [ ] 部署到 OMG stage 后人工确认信用卡可用、Apple Pay 在适用环境可用、ATM/CVS/BarcodeATM 不显示,并确认 OMG 商户侧已关闭 AFTEE。
 - [ ] 使用中文提交信息提交并推送当前分支:`修复:恢复 OMG 官方支付方式选择页`。
 
+## 2026-08-18 App 双入口严格支付渠道设计
+
+> 本节取代“2026-08-17 OMG 官方支付方式选择页实施计划”作为当前渠道实现依据。旧节保留用于说明已提交实现的历史背景,不再定义目标行为。
+
+**Goal:** 测试环境没有商户后台渠道开关时,仍严格保证用户只能发起信用卡或 Apple Pay,不显示超商快付与 AFTEE。
+
+**Architecture:** App 在进入 OMG 前显示“信用卡”和“Apple Pay”两个选项,并向 create/retry 传递稳定枚举 `paymentMethod`。后端只接受 `CREDIT/APPLE_PAY`,分别生成 `ChoosePayment=Credit + UnionPay=2` 或 `ChoosePayment=ApplePay` 的签名表单,不再使用 `ALL` 或 `IgnorePayment`。客户端不得传递 `ChoosePayment`、`UnionPay` 等 OMG 原始参数。支付尝试表、回调、查询、结果页和测试环境退款边界保持不变,无数据库变更。
+
+### 后端边界
+
+- `OmgCreatePaymentRequest` 与 `OmgRetryPaymentRequest` 只增加 `paymentMethod`;Controller 先校验白名单,再把领域枚举传给 Service。
+- create 与 retry 共用同一渠道映射和表单工厂,避免两条路径生成不同渠道参数。
+- `CREDIT`:发送并签名 `ChoosePayment=Credit`、`UnionPay=2`,直接进入信用卡流程并隐藏银联。
+- `APPLE_PAY`:发送并签名 `ChoosePayment=ApplePay`,不发送 `UnionPay` 或 `IgnorePayment`。
+- 缺失、空白、大小写不匹配或其他值统一返回 `PAYMENT_METHOD_INVALID`;校验失败不得创建、替换或更新支付尝试。
+
+### App 交互与契约
+
+- 用户选择 OMG 后,在 App 自有页面选择“信用卡”或“Apple Pay”;Apple Pay 的可用性仍受 OMG 门店开通状态和设备环境约束。
+- 首次支付调用 create,重新支付调用 retry;两者都传递本次用户选择的 `paymentMethod`。retry 可以重新选择渠道,但仍先执行现有可信查询和替换规则。
+- create/retry 成功后继续在当前完整 WebView 原样 Form POST 全部 `formFields`,不接入原生 Apple Pay SDK,不通过 CSS 或 DOM 修改 OMG 页面。
+- 当前工作区未包含用户端 App 源码;后端仓库完成契约、服务端实现和交接文档后,App UI 与请求改动需在用户端仓库实施和验收。
+
+### 测试与验收
+
+- DTO/Controller:覆盖 create/retry 的两个合法枚举及缺失、空白、未知值;非法值断言无 Service 调用或尝试变更。
+- 表单工厂:分别断言信用卡与 Apple Pay 字段集合、禁止字段及完整签名输入。
+- Service:断言 create/retry 将选择值贯穿到新尝试表单,原有并发、查询和不可逆支付规则不变。
+- OMG stage:分别验证信用卡和 Apple Pay 直接流程;页面不得出现超商快付、AFTEE 或其他渠道选择项。
+- 按项目 OMG 延后验证约束,测试源码与生产代码在同一实施批次完成,Maven、构建和 stage 验收等待全部 OMG 计划功能调整完成后统一执行。
+
 ## 2026-08-14 retry 查询稀疏响应修复计划
 
 **Goal:** 修复用户连续点击支付时,retry 查询到已签名但省略非核心字段的 OMG 响应而错误返回 `PAYMENT_QUERY_FAILED`。

+ 5 - 3
specs/020-omg-payment-rebuild/quickstart.md

@@ -22,7 +22,7 @@ mvn -pl ruoyi-admin -am -DskipTests package
 
 ```powershell
 $headers = @{ token = '<USER_TOKEN>' }
-$body = @{ orderId = '<SINGLE_STORE_DD_ID>' } | ConvertTo-Json
+$body = @{ orderId = '<SINGLE_STORE_DD_ID>'; paymentMethod = 'CREDIT' } | ConvertTo-Json
 $response = Invoke-RestMethod -Method Post `
   -Uri 'https://<API_HOST>/pay/omg/create' `
   -Headers $headers -ContentType 'application/json' -Body $body
@@ -33,7 +33,9 @@ Expected:
 
 - `status=CREATED`。
 - `gatewayUrl` 精确等于 stage AioCheckOut V5 地址。
-- `formFields` 包含规格规定的 15 个字段,其中 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`,且不包含 `UnionPay`。
+- `CREDIT` 响应包含 `ChoosePayment=Credit`、`UnionPay=2`,且不包含 `IgnorePayment`。
+- 将 `paymentMethod` 改为 `APPLE_PAY` 后,新响应包含 `ChoosePayment=ApplePay`,且不包含 `UnionPay` 或 `IgnorePayment`。
+- 两种响应都不包含 `ChoosePayment=ALL`。
 - `CheckMacValue` 是 64 位大写十六进制。
 - 响应不含 HashKey 或 HashIV。
 
@@ -41,7 +43,7 @@ Expected:
 
 在客户端当前页面创建 `<form method="post">`,action 设置为 `gatewayUrl`,逐一把 `formFields` 的键和值创建为 hidden input,然后调用 `form.submit()`。不要设置 iframe target,也不要 `window.open()`。
 
-Expected: 浏览器进入 OMG stage 收银台,显示正确金额;信用卡可用,Apple Pay 在门店已开通且设备支持时可用;ATM、CVS、BarcodeATM 不显示。AFTEE 必须预先在 OMG 商户侧关闭,代码不负责修改第三方页面
+Expected: 分别提交 `CREDIT` 和 `APPLE_PAY` 表单时,浏览器直接进入对应 OMG stage 付款流程并显示正确金额;不得出现超商快付、AFTEE 或其他渠道选择项。Apple Pay 仍需门店已开通且设备环境支持
 
 ## Duplicate request
 

+ 12 - 5
specs/020-omg-payment-rebuild/research.md

@@ -1,6 +1,7 @@
 # Research: OMG AIO 创建支付订单重建
 
 **Date**: 2026-08-13
+**Payment-method decision updated**: 2026-08-18
 **Decision source**: OMG AIO 官方技术文件 V1.5.3 与已批准的 [spec.md](spec.md)
 
 ## 1. 官方文档覆盖
@@ -10,10 +11,10 @@
 结论:
 
 - 创建订单使用 `POST application/x-www-form-urlencoded` 到 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`。
-- `ChoosePayment` 是单值字段,不能传 `Credit,ApplePay` 之类组合;本期固定 `ALL`,并使用 `IgnorePayment=ATM#CVS#BarcodeATM` 隐藏官方允许排除的渠道
+- `ChoosePayment` 是单值字段,不能传 `Credit,ApplePay` 之类组合;`Credit` 与 `ApplePay` 是两个独立取值
 - `MerchantTradeNo` 必须唯一、不可重复使用、最多 20 个 ASCII 英数字。
 - `PaymentType=aio`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y`。
-- `IgnorePayment` 仅在 `ChoosePayment=ALL` 时生效,官方公开可用值不含 AFTEE;因此 AFTEE 必须由 OMG 商户侧关闭,代码不能通过前端 CSS、DOM 注入或未公开参数隐藏第三方支付页面选项
+- `IgnorePayment` 仅在 `ChoosePayment=ALL` 时生效,官方公开可用值不含 AFTEE;测试环境又没有商户后台渠道开关,因此 `ALL` 无法满足“严格不显示 AFTEE”的需求
 - 创建请求中除 `CheckMacValue` 自身外,实际发送的每个字段都参加检查码计算。
 - 回调验签必须包含 OMG 实际返回的全部字段;开启额外信息后,额外字段、未知字段及空值字段同样不能被过滤,只有 `CheckMacValue` 排除。
 
@@ -46,9 +47,15 @@
 
 ## 3. 支付方式
 
-**Decision**: `ChoosePayment=ALL`,并发送 `IgnorePayment=ATM#CVS#BarcodeATM`。
+**Decision**: App 先选择 `CREDIT` 或 `APPLE_PAY`,后端白名单映射为 `ChoosePayment=Credit + UnionPay=2` 或 `ChoosePayment=ApplePay`;不发送 `ChoosePayment=ALL` 或 `IgnorePayment`。
 
-**Rationale**: 用户要求继续使用 OMG 官方支付方式选择页面,同时显示信用卡与 Apple Pay,不增加 App 二级选择页。`Credit` 与 `ApplePay` 是独立 `ChoosePayment` 请求值,固定 `Credit` 只显示信用卡;使用 `ALL + IgnorePayment` 才能同时保留两者并隐藏 ATM、CVS 与 BarcodeATM。AFTEE 不在公开过滤值中,必须由 OMG 商户侧关闭;Apple Pay 是否实际显示仍取决于门店开通状态和当前设备环境。
+**Rationale**: 用户要求测试环境只保留信用卡和 Apple Pay,但测试环境没有商户后台渠道开关。官方 `IgnorePayment` 无法排除 AFTEE,而一个请求也不能同时指定 `Credit` 与 `ApplePay`。因此必须在 App 自有界面先选择渠道,再由服务端生成单渠道签名表单;这样无需依赖第三方页面配置,也不会让客户端控制 OMG 原始参数。Apple Pay 是否能完成仍取决于门店开通状态和当前设备环境。
+
+**Rejected**:
+
+- `ALL + IgnorePayment`:只能隐藏 ATM、CVS、BarcodeATM,无法隐藏 AFTEE。
+- 固定 `ChoosePayment=Credit`:能隐藏其他渠道,但会同时失去独立 Apple Pay 入口。
+- 前端 CSS/DOM 隐藏第三方页面选项:脆弱且无法改变服务端实际允许的渠道,不作为安全边界。
 
 ## 4. 门店凭证
 
@@ -112,7 +119,7 @@
 
 ## 9. API 与错误
 
-`POST /pay/omg/create` 使用 `@RequestHeader String token` 和显式 `@RequestBody OmgCreatePaymentRequest`,DTO 只含 `orderId`。
+`POST /pay/omg/create` 使用 `@RequestHeader String token` 和显式 `@RequestBody OmgCreatePaymentRequest`,DTO 只含 `orderId` 与受控 `paymentMethod`。retry 使用相同渠道字段;query/refund 仍只含 `orderId`
 
 成功返回 `AjaxResult.success(data)`,其中 data 为:
 

+ 19 - 17
specs/020-omg-payment-rebuild/spec.md

@@ -48,7 +48,7 @@
 
 - 每个门店使用独立的 `MerchantID / HashKey / HashIV`,不使用平台统一凭证。
 - 不传 `PlatformID`。
-- `ChoosePayment` 固定为 `ALL`,并发送 `IgnorePayment=ATM#CVS#BarcodeATM`;OMG 商户侧必须关闭 AFTEE 等代码无法过滤的渠道,最终在官方选择页面保留信用卡与已开通且设备支持的 Apple Pay
+- App 在进入 OMG 前只提供 `CREDIT` 与 `APPLE_PAY` 两个受控选项;服务端分别映射为 `ChoosePayment=Credit + UnionPay=2` 与 `ChoosePayment=ApplePay`,不再进入 OMG 的 `ALL` 付款方式选择页
 - 客户端在当前页面提交表单,不使用 iframe,不打开新窗口。
 - 当前仅接 OMG 测试环境。
 - 新可信公开路径继续使用 `/pay/omg/*`;创建入口为 `POST /pay/omg/create`,可信回调为 `POST /pay/omg/notify`。
@@ -62,7 +62,7 @@
 
 ### User Story 1 - 首次创建并进入 OMG 收银台 (Priority: P1)
 
-已登录用户为自己的单门店餐饮订单选择 OMG 后,调用创建接口并取得由服务端签名的表单。客户端在当前页面 POST 该表单,进入对应门店的 OMG 测试收银台;服务端通过 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM` 隐藏可由代码过滤的渠道,并保留信用卡与 Apple Pay
+已登录用户为自己的单门店餐饮订单选择 OMG 后,先在 App 选择信用卡或 Apple Pay,再调用创建接口取得对应渠道的服务端签名表单。客户端在当前页面 POST 该表单,直接进入所选 OMG 测试付款流程,不展示包含超商快付或 AFTEE 的 OMG `ALL` 付款方式选择页
 
 **Why this priority**: 这是进入 OMG 收银台的基础,也是本期最终付款回调及后续查询、退款的前置能力。
 
@@ -72,8 +72,9 @@
 
 1. **Given** 用户拥有一笔 `payType="2"`、未取消、未付款、金额为正的单门店订单,且门店 OMG 凭证已启用,**When** 用户首次调用创建接口,**Then** 系统创建唯一支付尝试并返回可提交的 OMG 表单。
 2. **Given** 创建接口返回成功,**When** 客户端在当前页面向 `gatewayUrl` POST 全部 `formFields`,**Then** 浏览器进入 OMG 测试收银台,不通过 iframe 或新窗口加载。
-3. **Given** 门店已开通信用卡、Apple Pay、ATM、CVS 和 BarcodeATM,且商户侧已关闭 AFTEE,**When** 收银台接收 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM`,**Then** OMG 页面隐藏 ATM、CVS 和 BarcodeATM,并保留信用卡与当前设备支持的 Apple Pay。
-4. **Given** 门店未开通 Apple Pay 或当前设备环境不支持 Apple Pay,**When** 进入收银台,**Then** 页面仍可显示信用卡;系统不得通过前端 CSS、DOM 注入或未公开 OMG 参数篡改第三方支付页面。
+3. **Given** 用户选择信用卡,**When** 创建并提交表单,**Then** 服务端发送并签名 `ChoosePayment=Credit` 与 `UnionPay=2`,页面不得显示超商快付、AFTEE、Apple Pay 或银联选择项。
+4. **Given** 用户选择 Apple Pay,且门店与设备支持 Apple Pay,**When** 创建并提交表单,**Then** 服务端只发送并签名 `ChoosePayment=ApplePay`,直接进入 Apple Pay 流程,不显示信用卡、超商快付或 AFTEE 选择项。
+5. **Given** `paymentMethod` 缺失或不属于 `CREDIT/APPLE_PAY`,**When** 调用 create 或 retry,**Then** 服务端返回稳定错误 `PAYMENT_METHOD_INVALID`,且不创建或替换支付尝试。
 
 ---
 
@@ -99,12 +100,12 @@
 
 **Why this priority**: 创建错误门店、错误金额或泄露密钥会形成直接资金风险。
 
-**Independent Test**: 分别使用无效 token、他人订单、终态订单、异常金额、错误支付类型、无凭证门店和非测试网关配置调用接口,均被拒绝且不写入尝试表。
+**Independent Test**: 分别使用无效 token、他人订单、终态订单、异常金额、错误支付类型、非法 `paymentMethod`、无凭证门店和非测试网关配置调用接口,均被拒绝且不写入尝试表。
 
 **Acceptance Scenarios**:
 
 1. **Given** 请求用户不是订单所有者,**When** 调用创建接口,**Then** 系统拒绝且不透露订单或门店支付详情。
-2. **Given** 订单已取消、已付款、金额不大于零、不是单门店订单或 `payType` 不是 `"2"`,**When** 调用创建接口,**Then** 系统返回国际化业务错误且不创建尝试。
+2. **Given** 订单已取消、已付款、金额不大于零、不是单门店订单、`payType` 不是 `"2"` 或 `paymentMethod` 非法,**When** 调用创建接口,**Then** 系统返回国际化业务错误且不创建尝试。
 3. **Given** 门店没有已启用 OMG 凭证,**When** 调用创建接口,**Then** 系统拒绝且不创建尝试。
 4. **Given** 服务配置不是允许的 OMG 测试端点,**When** 调用创建接口,**Then** 系统拒绝生成表单。
 5. **Given** 创建成功或失败,**When** 检查 API 响应和应用日志,**Then** 所有响应和日志均不存在 `HashKey`、`HashIV`;完整 `CheckMacValue` 与签名表单只存在于订单本人获准取得的创建成功响应,不出现在错误响应或日志中。
@@ -177,7 +178,7 @@ iOS 用户在 OMG 收银台完成或结束支付流程后,OMG 加载后端 `/p
 - **FR-042**: `POST /pay/omg/refund` MUST 需要 token,并以显式 JSON DTO 只接收 `orderId`;不得接收客户端提供的金额、交易号、Action、凭证或地址。
 - **FR-043**: 当前测试阶段退款 MUST 返回 `PAYMENT_REFUND_UNAVAILABLE_IN_TEST_ENVIRONMENT`,MUST NOT 调用 OMG 正式 `CreditDetail/DoAction`、查询或写入任何订单/支付/退款状态。
 - **FR-044**: 测试环境退款拒绝 MUST 提供五套 i18n 提示和必要的脱敏日志;日志不得记录 token、凭证或完整支付签名。
-- **FR-045**: 系统 MUST 提供 `POST /pay/omg/retry`,使用 token 和只含 `orderId` 的显式 JSON DTO;客户端不得提交旧 `MerchantTradeNo`、支付方式、金额、凭证、网关地址或旧表单。
+- **FR-045**: 系统 MUST 提供 `POST /pay/omg/retry`,使用 token 和只含 `orderId`、`paymentMethod` 的显式 JSON DTO;`paymentMethod` 仅允许 `CREDIT/APPLE_PAY`。客户端不得提交旧 `MerchantTradeNo`、OMG 原始支付参数、金额、凭证、网关地址或旧表单。
 - **FR-046**: retry MUST 先复用可信 query 查询 OMG 实际状态;查询失败、响应不可信或状态为 `UNKNOWN` 时不得修改尝试或创建新表单。
 - **FR-047**: query 确认 `PAID` 时 retry MUST 返回 `ORDER_ALREADY_PAID`;确认 `FAILED` 时 MAY 创建新尝试;确认 `UNPAID` 时仅当 `paymentType` 为空才 MAY 替换旧尝试。
 - **FR-048**: 替换未付款尝试 MUST 在同一事务内锁定订单,精确比较 query 已验证的 `MerchantTradeNo` 与当前活动尝试,把该行原子更新为 `SUPERSEDED` 后再生成新交易号和新表单;任一条件变化 MUST 返回 `PAYMENT_RETRY_NOT_AVAILABLE`。
@@ -198,20 +199,20 @@ iOS 用户在 OMG 收银台完成或结束支付流程后,OMG 加载后端 `/p
 - **FR-002**: 系统 MUST 新建 `com.ruoyi.system.omgpay` 下的支付尝试 Entity、Mapper 和 Service;新支付尝试 MUST 使用 `pos_order_omg_attempt`,不得读取或写入旧 OMG 支付流水表。
 - **FR-003**: 系统 MAY 复用现有 `pos_store_omg` 门店凭证查询实现,且这是唯一允许复用的旧 OMG 实现;新创建流程 MUST 按订单门店读取该门店已启用的 `MerchantID / HashKey / HashIV`。
 - **FR-004**: 系统 MUST 保持公开创建入口为 `POST /pay/omg/create`,使用 `@RequestHeader String token` 和显式 `@RequestBody` DTO;Controller 入参不得使用 Map,DTO 不使用 Bean Validation 注解。
-- **FR-005**: 创建请求 DTO MUST 只接收 `orderId`;金额、门店、用户、支付类型、说明文字、网关地址、回调地址和支付渠道均必须由服务端决定。
+- **FR-005**: 创建请求 DTO MUST 只接收 `orderId` 与 `paymentMethod``paymentMethod` 仅允许 `CREDIT/APPLE_PAY`。金额、门店、用户、订单支付类型、说明文字、网关地址、回调地址及原始 OMG 参数均必须由服务端决定。
 - **FR-006**: 系统 MUST 校验登录用户为订单所有者,订单为单门店订单、未取消、未付款、未完成、金额为正且 `PosOrder.payType="2"`;任何校验失败 MUST NOT 创建支付尝试。
 - **FR-007**: 系统 MUST 使用订单的整数新台币金额作为 `TotalAmount`,不得接受或信任客户端金额。
 - **FR-008**: 系统 MUST 仅允许创建表单到 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`;第一阶段不得配置或回退到正式环境。
 - **FR-009**: 系统 MUST 为每次新尝试生成全局唯一、不可复用、长度不超过 20 且只含 ASCII 英数字的 `MerchantTradeNo`;不得从业务订单号直接派生可冲突或超长的编号。
 - **FR-010**: 同一业务订单在任一时刻 MUST 最多存在一条未结束 OMG 尝试;顺序重复或并发创建 MUST 返回业务状态 `PAYMENT_ATTEMPT_EXISTS`,不得返回旧表单、重复提交旧编号或创建新编号。
 - **FR-011**: 在可信查询尚未实现前,系统 MUST NOT 基于固定分钟窗口、本地创建时间或用户再次点击自动结束 `CREATED` 尝试。
-- **FR-012**: 创建表单 MUST 发送以下字段和值:`MerchantID`、`MerchantTradeNo`、`MerchantTradeDate`、`PaymentType=aio`、`TotalAmount`、`TradeDesc`、`ItemName`、`ReturnURL`、`OrderResultURL`、`ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y` 和 `CheckMacValue`。
-- **FR-013**: 创建表单 MUST NOT 发送 `PlatformID`、`PaymentInfoURL`、`ClientRedirectURL`、`ClientBackURL`、`Language`、`UnionPay`、ATM/CVS/BarcodeATM 期限字段、分期、定期定额或记忆卡号参数。
+- **FR-012**: 创建表单公共字段 MUST 包含 `MerchantID`、`MerchantTradeNo`、`MerchantTradeDate`、`PaymentType=aio`、`TotalAmount`、`TradeDesc`、`ItemName`、`ReturnURL`、`OrderResultURL`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y` 和 `CheckMacValue`。`CREDIT` 额外固定发送 `ChoosePayment=Credit`、`UnionPay=2`;`APPLE_PAY` 额外固定发送 `ChoosePayment=ApplePay` 且不发送 `UnionPay`。
+- **FR-013**: 创建表单 MUST NOT 发送 `ChoosePayment=ALL`、`IgnorePayment`、`PlatformID`、`PaymentInfoURL`、`ClientRedirectURL`、`ClientBackURL`、`Language`、ATM/CVS/BarcodeATM 期限字段、分期、定期定额或记忆卡号参数;`APPLE_PAY` 也 MUST NOT 发送仅适用于信用卡的 `UnionPay`
 - **FR-014**: `MerchantTradeDate` MUST 以 `Asia/Taipei` 时区格式化为 `yyyy/MM/dd HH:mm:ss`。
 - **FR-015**: `TradeDesc` 和 `ItemName` MUST 由服务端生成,禁止 HTML,符合 OMG 字符及长度限制;`ItemName` 不得超过中文 60 字或英数字 120 字的官方显示限制,字段总长度不得超过官方 `String(200)` 限制。
 - **FR-016**: `ReturnURL` MUST 是受控 HTTPS 地址并固定以 `/pay/omg/notify` 结尾;该路径 MUST 由新的 `omgpay` Controller 处理,不得被旧 Controller 接收。
 - **FR-017**: `CheckMacValue` MUST 严格按 OMG 官方规则生成:排除 `CheckMacValue` 本身,将其余全部实际发送字段按官方字母顺序排序,以 `&` 串接,前置 `HashKey=...&`、后置 `&HashIV=...`,执行符合官方 .NET 表的 URL 编码并转小写,使用 SHA-256,最后输出大写十六进制。
-- **FR-018**: 除 `CheckMacValue` 自身外,创建请求实际发送的全部字段 MUST 参加签名,包括 `OrderResultURL`、`ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM` 和 `NeedExtraPaidInfo=Y`;不得挑选所谓核心字段计算。
+- **FR-018**: 除 `CheckMacValue` 自身外,创建请求实际发送的全部字段 MUST 参加签名,包括 `OrderResultURL`、所选渠道对应的 `ChoosePayment`、信用卡渠道的 `UnionPay=2` 和 `NeedExtraPaidInfo=Y`;不得挑选所谓核心字段计算。
 - **FR-019**: 回调 MUST 遵守同一完整字段原则:除 `CheckMacValue` 外,OMG 实际返回的全部字段均参加验签;启用 `NeedExtraPaidInfo=Y` 后,全部额外回传字段、未知字段及空值字段也必须进入验签集合。重复参数名必须作为非法请求拒绝,不得静默选取其中一个值。
 - **FR-020**: 创建成功响应 MUST 使用明确对象 `{status, gatewayUrl, formFields}`;`status` 固定为 `CREATED`,`gatewayUrl` 为测试 AioCheckOut 端点,`formFields` 含实际需要 POST 的全部字段但不含 `gatewayUrl`。
 - **FR-021**: 客户端 MUST 在当前页面以 `application/x-www-form-urlencoded` 表单 POST 全部 `formFields` 到 `gatewayUrl`;不得使用 iframe,不得另开新窗口,不得把响应转换为 GET 查询链接。
@@ -248,7 +249,8 @@ Content-Type: application/json
 token: <login-token>
 
 {
-  "orderId": "991786433092835"
+  "orderId": "991786433092835",
+  "paymentMethod": "CREDIT"
 }
 ```
 
@@ -268,8 +270,8 @@ token: <login-token>
     "ItemName": "Order 991786433092835",
     "ReturnURL": "https://example.test/pay/omg/notify",
     "OrderResultURL": "https://example.test/pay/omg/result",
-    "ChoosePayment": "ALL",
-    "IgnorePayment": "ATM#CVS#BarcodeATM",
+    "ChoosePayment": "Credit",
+    "UnionPay": "2",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",
@@ -278,7 +280,7 @@ token: <login-token>
 }
 ```
 
-`HashKey` 和 `HashIV` 永远不属于响应。外层继续使用项目现有 `AjaxResult` 成功/失败封装;上例只定义 `data` 契约。
+`HashKey` 和 `HashIV` 永远不属于响应。上例是 `paymentMethod=CREDIT` 的响应;`paymentMethod=APPLE_PAY` 时 `ChoosePayment=ApplePay`,且不包含 `UnionPay` 或 `IgnorePayment`。外层继续使用项目现有 `AjaxResult` 成功/失败封装;上例只定义 `data` 契约。
 
 #### Existing-attempt failure
 
@@ -378,7 +380,7 @@ token: <login-token>
 
 - 使用门店测试凭证调用 `POST /pay/omg/create`。
 - 在当前页面将全部 `formFields` POST 到返回的测试 `gatewayUrl`。
-- 确认进入 OMG 测试收银台,金额、商品说明和 `ALL` 可用渠道显示正确
+- 分别使用 `CREDIT` 与 `APPLE_PAY` 创建并提交表单,确认直接进入所选 OMG 测试付款流程;不得出现超商快付或 AFTEE 选择项
 - 确认不使用 iframe 或新窗口。
 - 创建和付款结果回调按本规格完成;查询、补单、取号、退款与推送不作为本阶段完成标准。
 
@@ -390,7 +392,7 @@ token: <login-token>
 - **SC-002**: 官方 AioCheckOut 签名向量自动化测试与官方 `CheckMacValue` 完全一致。
 - **SC-003**: 实际发送的每一个非 `CheckMacValue` 字段均由测试证明参与签名,额外字段与空值字段不会被签名器丢弃。
 - **SC-004**: 对同一订单进行顺序或并发重复创建时,数据库未结束尝试数始终不超过 1,第二个 `MerchantTradeNo` 产生率为 0。
-- **SC-005**: 未授权、非法状态、非法金额、错误支付类型、无凭证和非测试环境请求的尝试写入数为 0。
+- **SC-005**: 未授权、非法状态、非法金额、错误支付类型、非法 `paymentMethod`、无凭证和非测试环境请求的尝试写入数为 0。
 - **SC-006**: 新 OMG 创建链路中对旧支付 Controller、旧工具类和旧支付流水服务的引用数为 0;对旧支付表的 SQL 访问数为 0。
 - **SC-007**: 旧 OMG Controller、旧补单/退款入口和旧定时任务的可达运行入口数为 0;`/pay/omg/create` 只映射到新 Controller。
 - **SC-008**: API 响应及应用日志中的登录 token、`HashKey`、`HashIV` 泄露数为 0;错误响应和应用日志中的完整 `CheckMacValue` 或完整签名表单泄露数为 0。

+ 11 - 2
specs/020-omg-payment-rebuild/tasks.md

@@ -170,9 +170,18 @@
 - [ ] T102 [US8] 使用 JDK 21 运行 `OmgPaymentClientReturnServiceTest`,确认后端现有 HTML、繁体提示、“返回 App”按钮与 Scheme 兜底保持不变
 - [ ] T103 [US8] 使用 iPhone 真机验证 `result_page_detected` 单次触发,并用中间页/取消页、Android 和外部浏览器完成负例及兼容验收
 
+## Phase 18: App 信用卡与 Apple Pay 双入口
+
+- [x] T104 [US1] 更新现有规格、研究结论、实施计划、API 契约、快速验收与 App 交接文档,以本阶段设计取代 `ALL + IgnorePayment` 方案
+- [ ] T105 [US1] 在同一批次补充 create/retry DTO、Controller、Service 与表单工厂测试源码,覆盖 `CREDIT/APPLE_PAY`、非法枚举无副作用、动态字段集合和完整签名输入
+- [ ] T106 [US1] 为 create/retry 增加受控 `paymentMethod`,统一映射 `CREDIT -> Credit + UnionPay=2`、`APPLE_PAY -> ApplePay`,新增五语言 `PAYMENT_METHOD_INVALID`,禁止客户端 OMG 原始参数
+- [ ] T107 [US1] 在用户端 App 增加信用卡/Apple Pay i18n 选择,并让首次支付与重新支付分别提交本次选择;当前工作区未包含 App 源码,需提供用户端仓库后实施
+- [ ] T108 [US1] 全部 OMG 计划功能调整完成后,使用 JDK 21 统一运行渠道定向测试、OMG 回归和模块构建;核对最终 diff、暂存范围及无 SQL 变更
+- [ ] T109 [US1] 在 OMG stage 分别验收信用卡和 Apple Pay 直接流程,确认不出现超商快付、AFTEE、银联或其他渠道选择项
+
 ## Dependencies & Execution Order
 
-- Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 → Phase 8 → Phase 9 → Phase 10 → Phase 11 → Phase 12 → Phase 13 → Phase 14 → Phase 15 → Phase 16 → Phase 17。
+- Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 → Phase 8 → Phase 9 → Phase 10 → Phase 11 → Phase 12 → Phase 13 → Phase 14 → Phase 15 → Phase 16 → Phase 17 → Phase 18
 - T008 与 T011/T012 可独立写测试,但实施时顺序执行以维持清晰 TDD 证据。
 - T031 必须早于任何脏文件修改;T041 必须在旧代码退役完成后执行。
 - 手动 stage 验收依赖开发者执行 DDL,自动测试和构建不依赖数据库变更。
@@ -185,7 +194,7 @@
 - 成功仅修改订单 `payStatus`,不推进业务状态或触发推送/退款。
 - 同订单重复/并发创建最多一条 `CREATED`,后续请求为 `PAYMENT_ATTEMPT_EXISTS`。
 - 官方检查码向量一致,实际发送字段无遗漏。
-- 新支付表单固定 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`,不发送 `UnionPay`;AFTEE 由 OMG 商户侧关闭
+- App 只提交 `CREDIT/APPLE_PAY`;新支付表单分别固定为 `ChoosePayment=Credit + UnionPay=2` 或 `ChoosePayment=ApplePay`,不发送 `ChoosePayment=ALL` 或 `IgnorePayment`
 - 新代码全在 `omgpay` 包,不引用旧 OMG 支付实现。
 - 旧 Controller、回调、查询、补单、退款、任务及旧表运行时引用为零。
 - `pos_store_omg` 凭证表、持久化和管理能力保留;联网探测只使用新 `omgpay` 实现。