ソースを参照

修复:恢复 OMG 官方支付方式选择页

创建和重试表单固定发送 ALL 与 IgnorePayment,保留信用卡及 Apple Pay,并明确 AFTEE 由商户侧关闭。
qmj 2 週間 前
コミット
62d8dd342d

+ 2 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java

@@ -46,8 +46,8 @@ public class OmgPaymentFormFactory {
         fields.put("ItemName", "Order " + safeOrderId);
         fields.put("ReturnURL", properties.requireSafeReturnUrl());
         fields.put("OrderResultURL", properties.requireSafeOrderResultUrl());
-        fields.put("ChoosePayment", "Credit");
-        fields.put("UnionPay", "2");
+        fields.put("ChoosePayment", "ALL");
+        fields.put("IgnorePayment", "ATM#CVS#BarcodeATM");
         fields.put("EncryptType", "1");
         fields.put("InvoiceMark", "N");
         fields.put("NeedExtraPaidInfo", "Y");

+ 7 - 7
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java

@@ -36,12 +36,12 @@ class OmgPaymentFormFactoryTest {
         assertEquals(OmgPaymentFormFactory.STAGE_GATEWAY_URL, form.gatewayUrl());
         assertEquals(Set.of("MerchantID", "MerchantTradeNo", "MerchantTradeDate", "PaymentType",
                 "TotalAmount", "TradeDesc", "ItemName", "ReturnURL", "ChoosePayment", "EncryptType",
-                "InvoiceMark", "NeedExtraPaidInfo", "UnionPay", "OrderResultURL",
+                "InvoiceMark", "NeedExtraPaidInfo", "IgnorePayment", "OrderResultURL",
                 "CheckMacValue"), form.fields().keySet());
         assertEquals("2026/08/13 15:30:23", form.fields().get("MerchantTradeDate"));
-        assertEquals("Credit", form.fields().get("ChoosePayment"));
-        assertEquals("2", form.fields().get("UnionPay"));
-        assertFalse(form.fields().containsKey("IgnorePayment"));
+        assertEquals("ALL", form.fields().get("ChoosePayment"));
+        assertEquals("ATM#CVS#BarcodeATM", form.fields().get("IgnorePayment"));
+        assertFalse(form.fields().containsKey("UnionPay"));
         assertEquals("Y", form.fields().get("NeedExtraPaidInfo"));
         assertEquals("https://foodieapi.waimai-paotui.com/pay/omg/result",
                 form.fields().get("OrderResultURL"));
@@ -54,9 +54,9 @@ class OmgPaymentFormFactoryTest {
         assertFalse(form.fields().get("ItemName").contains("|"));
         assertTrue(form.fields().get("ItemName").length() <= 120);
         verify(signer).sign(org.mockito.ArgumentMatchers.argThat(fields -> fields.size() == 14
-                && "Credit".equals(fields.get("ChoosePayment"))
-                && "2".equals(fields.get("UnionPay"))
-                && !fields.containsKey("IgnorePayment")
+                && "ALL".equals(fields.get("ChoosePayment"))
+                && "ATM#CVS#BarcodeATM".equals(fields.get("IgnorePayment"))
+                && !fields.containsKey("UnionPay")
                 && !fields.containsKey("CheckMacValue")), eq("KEY"), eq("IV"));
         assertThrows(UnsupportedOperationException.class, () -> form.fields().put("extra", "value"));
     }

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

@@ -45,8 +45,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": "Credit",
-    "UnionPay": "2",
+    "ChoosePayment": "ALL",
+    "IgnorePayment": "ATM#CVS#BarcodeATM",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",

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

@@ -177,8 +177,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": "Credit",
-      "UnionPay": "2",
+      "ChoosePayment": "ALL",
+      "IgnorePayment": "ATM#CVS#BarcodeATM",
       "EncryptType": "1",
       "InvoiceMark": "N",
       "NeedExtraPaidInfo": "Y",
@@ -197,8 +197,8 @@ token: <用户登录 token>
 | `formFields` | object | 所有键值都必须原样创建为隐藏 input 并 POST |
 | `MerchantTradeNo` | string | 后端生成的 OMG 交易编号;前端只透传,不作为业务订单号 |
 | `TotalAmount` | string | 后端根据订单金额生成;前端不得修改 |
-| `ChoosePayment` | string | 当前固定 `Credit`,只进入信用卡类支付;前端不得修改 |
-| `UnionPay` | string | 当前固定 `2`,由 OMG 隐藏银联;前端不得修改 |
+| `ChoosePayment` | string | 当前固定 `ALL`,使用 OMG 官方付款方式选择页面;前端不得修改 |
+| `IgnorePayment` | string | 当前固定 `ATM#CVS#BarcodeATM`,由 OMG 隐藏 ATM、CVS 和 BarcodeATM;前端不得修改 |
 | `ReturnURL` | string | OMG 服务端付款通知地址;前端不调用 |
 | `OrderResultURL` | string | OMG 浏览器付款结果地址;前端不直接调用 |
 | `CheckMacValue` | string | 表单签名;修改任意已签名字段都会导致 OMG 拒绝 |
@@ -651,7 +651,7 @@ token: <用户登录 token>
 10. `tradeNo`、`merchantTradeNo` 始终按字符串处理,不转换为 JavaScript Number。
 11. query 会访问 OMG 网关,不做高频轮询。
 12. `PAYMENT_ATTEMPT_EXISTS` 不能靠重复调用 create 解决,应进入 query 确认流程。
-13. 后端通过 `ChoosePayment=Credit` 与 `UnionPay=2` 只开放信用卡和 Apple Pay,并隐藏银联;Apple Pay 是否显示取决于门店开通状态和当前设备环境
+13. 后端通过 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM` 保留信用卡和 Apple Pay;AFTEE 必须由 OMG 商户侧关闭,App 不得通过 CSS 或 DOM 注入修改第三方支付页面
 14. 当前没有真实退款能力。
 
 ## 10. App 验收清单
@@ -664,7 +664,7 @@ token: <用户登录 token>
 - [ ] create 成功后,当前完整 WebView 使用 Form POST 打开 `gatewayUrl`。
 - [ ] 所有 `formFields` 原样提交,`gatewayUrl` 不作为 input。
 - [ ] 没有 iframe、GET 跳转或新窗口。
-- [ ] 测试收银台显示正确金额,信用卡可用,Apple Pay 在门店已开通且设备支持时可用,ATM/CVS/BarcodeATM/AFTEE/银联均隐藏
+- [ ] 测试收银台显示正确金额,信用卡可用,Apple Pay 在门店已开通且设备支持时可用,ATM/CVS/BarcodeATM 已隐藏,AFTEE 已在 OMG 商户侧关闭
 
 ### 10.2 App 返回与状态确认
 

+ 15 - 15
specs/020-omg-payment-rebuild/plan.md

@@ -17,7 +17,7 @@
 - 每个门店独立使用 `pos_store_omg` 中已启用的 `MerchantID / HashKey / HashIV`;不修改可信凭证存储与管理代码。
 - 新创建入口固定 `POST /pay/omg/create`;Controller 使用 `@RequestHeader String token`、显式 `@RequestBody` DTO,禁止 Map 入参和 Bean Validation。
 - 客户端请求只含 `orderId`;金额、门店、网关、ReturnURL、说明、支付方式和签名全部由服务端产生。
-- `ChoosePayment=Credit`、`UnionPay=2`、`NeedExtraPaidInfo=Y`;OMG 官方页面只开放信用卡和 Apple Pay,并隐藏银联。Apple Pay 是否显示取决于门店开通状态和设备环境。所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
+- `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`、`NeedExtraPaidInfo=Y`;OMG 官方页面保留信用卡和 Apple Pay,AFTEE 由 OMG 商户侧关闭。Apple Pay 是否显示取决于门店开通状态和设备环境。所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
 - 回调必须对全部实际返回字段验签,包含未知额外字段与空值字段,只有 `CheckMacValue` 排除。
 - 网关固定 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`,不得配置正式环境回退。
 - 同一 `ddId` 最多一条 `CREATED`;存在时返回 `PAYMENT_ATTEMPT_EXISTS`,不重放旧表单、不生成新 MerchantTradeNo。
@@ -301,8 +301,8 @@ fields.put("TradeDesc", "Foodie order " + safeOrderId);
 fields.put("ItemName", "Order " + safeOrderId);
 fields.put("ReturnURL", properties.requireSafeReturnUrl());
 fields.put("OrderResultURL", properties.requireSafeOrderResultUrl());
-fields.put("ChoosePayment", "Credit");
-fields.put("UnionPay", "2");
+fields.put("ChoosePayment", "ALL");
+fields.put("IgnorePayment", "ATM#CVS#BarcodeATM");
 fields.put("EncryptType", "1");
 fields.put("InvoiceMark", "N");
 fields.put("NeedExtraPaidInfo", "Y");
@@ -1000,7 +1000,7 @@ rg -n "com\.ruoyi\.app\.pay\.OmgPayController|com\.ruoyi\.app\.utils\.omg|pos_or
 rg -n "HashKey|HashIV|CheckMacValue|formFields|token" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
 ```
 
-Interpret results manually: stage URL、`OrderResultURL`、`ChoosePayment=Credit` 与 `UnionPay=2` 是预期结果;production URL、`ChoosePayment=ALL`、`IgnorePayment`、其他禁用字段、旧 import 和敏感值日志不是预期结果。
+Interpret results manually: stage URL、`OrderResultURL`、`ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM` 是预期结果;production URL、`UnionPay`、其他禁用字段、旧 import 和敏感值日志不是预期结果。
 
 - [ ] **Step 4: Check comments and log statements**
 
@@ -1130,11 +1130,11 @@ No constitution violations. The separate form factory, signer, generator and per
 5. 两个并发 retry 即使都查到同一旧交易,也只有先取得订单锁者能替换;后取得者看到活动交易号变化后返回 `PAYMENT_RETRY_NOT_AVAILABLE`。
 6. retry 成功复用 create 响应,返回新 `MerchantTradeNo` 的新表单。无需数据库结构变更,现有 `attempt_status=3` 与生成列唯一键已覆盖该流程。
 
-## 2026-08-17 OMG 官方收银台严格渠道过滤实施计划
+## 2026-08-17 OMG 官方支付方式选择页实施计划
 
-**Goal:** 保持 App 直接进入 OMG 官方收银台,通过服务端固定表单参数严格只开放信用卡与 Apple Pay。
+**Goal:** 保持 App 直接进入 OMG 官方支付方式选择页,通过服务端隐藏 ATM、CVS 和 BarcodeATM,同时由 OMG 商户侧关闭 AFTEE,最终保留信用卡与 Apple Pay。
 
-**Architecture:** 不修改 `POST /pay/omg/create`、`POST /pay/omg/retry` 的请求或响应结构。两条路径继续复用 `OmgPaymentFormFactory#create(...)`;工厂固定发送 `ChoosePayment=Credit` 与 `UnionPay=2`,移除 `ChoosePayment=ALL` 和 `IgnorePayment`,并把两个固定渠道字段纳入 `CheckMacValue`
+**Architecture:** 不修改 `POST /pay/omg/create`、`POST /pay/omg/retry` 的请求或响应结构。两条路径继续复用 `OmgPaymentFormFactory#create(...)`;工厂固定发送 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM`,不发送 `UnionPay`,并把实际过滤字段纳入 `CheckMacValue`。不通过前端 CSS、DOM 注入或未公开参数修改 OMG 第三方页面
 
 **Tech Stack:** Java 21、Spring Boot、JUnit 5、Mockito、Maven、OMG AIO stage。
 
@@ -1148,15 +1148,15 @@ No constitution violations. The separate form factory, signer, generator and per
 **Interfaces:**
 
 - Consumes: `OmgPaymentFormFactory#create(String orderId, Integer amount, String merchantId, String hashKey, String hashIv, String merchantTradeNo)`
-- Produces: 原接口不变;`OmgPaymentForm.fields()` 返回包含 `ChoosePayment=Credit`、`UnionPay=2` 且不含 `IgnorePayment` 的不可变字段集合。
+- Produces: 原接口不变;`OmgPaymentForm.fields()` 返回包含 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM` 且不含 `UnionPay` 的不可变字段集合。
 
-- [x] 将测试改为 `buildsSignedFormThatUsesOmgPageForCreditAndApplePay`:字段集合移除 `IgnorePayment`、加入 `UnionPay`;断言 `ChoosePayment=Credit`、`UnionPay=2`、无 `IgnorePayment`;签名入参仍为 14 个非 `CheckMacValue` 字段。
-- [ ] 按项目 OMG 延后验证约束,等待统一验证阶段使用 JDK 21 运行 `OmgPaymentFormFactoryTest`,确认旧 `ChoosePayment=ALL` 实现会使新断言失败。
+- [x] 将测试改为 `buildsSignedFormThatUsesOmgPageForCreditAndApplePay`:字段集合移除 `UnionPay`、加入 `IgnorePayment`;断言 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`、无 `UnionPay`;签名入参仍为 14 个非 `CheckMacValue` 字段。
+- [ ] 按项目 OMG 延后验证约束,等待统一验证阶段使用 JDK 21 运行 `OmgPaymentFormFactoryTest`,确认旧 `ChoosePayment=Credit` 实现会使新断言失败。
 - [x] 修改 `OmgPaymentFormFactory`,改为以下固定字段:
 
 ```java
-fields.put("ChoosePayment", "Credit");
-fields.put("UnionPay", "2");
+fields.put("ChoosePayment", "ALL");
+fields.put("IgnorePayment", "ATM#CVS#BarcodeATM");
 ```
 
 - [ ] 在统一验证阶段再次运行 `OmgPaymentFormFactoryTest`,确认字段集合、值和签名入参全部通过。
@@ -1173,7 +1173,7 @@ fields.put("UnionPay", "2");
 - Consumes: create/retry 现有 `{status, gatewayUrl, formFields}` 响应。
 - Produces: App 仍直接 POST 全部 `formFields`,不增加支付渠道参数、二级选择页面或原生 Apple Pay SDK。
 
-- [x] 更新规格、契约、快速验收和 App 文档中的示例表单及字段说明:`ChoosePayment=Credit`、`UnionPay=2`,删除 `IgnorePayment`;注明 Apple Pay 取决于门店开通状态和设备环境。
+- [x] 更新规格、契约、快速验收和 App 文档中的示例表单及字段说明:`ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`,删除 `UnionPay`;注明 AFTEE 必须由 OMG 商户侧关闭,Apple Pay 取决于门店开通状态和设备环境。
 - [ ] 在统一验证阶段使用 JDK 21 运行全部 OMG 定向测试:
 
 ```powershell
@@ -1189,8 +1189,8 @@ mvn -pl ruoyi-admin -am -DskipTests package
 ```
 
 - [x] 静态检查源码中只存在预期渠道参数,`git diff --check` 无错误;暂存范围在提交前单独核对,不包含 `.claude/homunculus/*`。
-- [ ] 部署到 OMG stage 后人工确认信用卡可用,Apple Pay 在适用环境可用,其他渠道均不显示
-- [ ] 使用中文提交信息提交并推送当前分支:`修复:限制 OMG 收银台支付方式`。
+- [ ] 部署到 OMG stage 后人工确认信用卡可用、Apple Pay 在适用环境可用、ATM/CVS/BarcodeATM 不显示,并确认 OMG 商户侧已关闭 AFTEE
+- [ ] 使用中文提交信息提交并推送当前分支:`修复:恢复 OMG 官方支付方式选择页`。
 
 ## 2026-08-14 retry 查询稀疏响应修复计划
 

+ 2 - 2
specs/020-omg-payment-rebuild/quickstart.md

@@ -33,7 +33,7 @@ Expected:
 
 - `status=CREATED`。
 - `gatewayUrl` 精确等于 stage AioCheckOut V5 地址。
-- `formFields` 包含规格规定的 15 个字段,其中 `ChoosePayment=Credit`、`UnionPay=2`,且不包含 `IgnorePayment`。
+- `formFields` 包含规格规定的 15 个字段,其中 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`,且不包含 `UnionPay`。
 - `CheckMacValue` 是 64 位大写十六进制。
 - 响应不含 HashKey 或 HashIV。
 
@@ -41,7 +41,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 和银联均不显示
+Expected: 浏览器进入 OMG stage 收银台,显示正确金额;信用卡可用,Apple Pay 在门店已开通且设备支持时可用;ATM、CVS、BarcodeATM 不显示。AFTEE 必须预先在 OMG 商户侧关闭,代码不负责修改第三方页面
 
 ## Duplicate request
 

+ 4 - 4
specs/020-omg-payment-rebuild/research.md

@@ -10,10 +10,10 @@
 结论:
 
 - 创建订单使用 `POST application/x-www-form-urlencoded` 到 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`。
-- `ChoosePayment` 是单值字段,不能传 `Credit,ApplePay` 之类组合;OMG 付款方式表将 Apple Pay 包含在 `Credit_CreditCard` 中,因此本期固定 `Credit`,并发送 `UnionPay=2` 隐藏银联
+- `ChoosePayment` 是单值字段,不能传 `Credit,ApplePay` 之类组合;本期固定 `ALL`,并使用 `IgnorePayment=ATM#CVS#BarcodeATM` 隐藏官方允许排除的渠道
 - `MerchantTradeNo` 必须唯一、不可重复使用、最多 20 个 ASCII 英数字。
 - `PaymentType=aio`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y`。
-- 不发送 `ChoosePayment=ALL` 或 `IgnorePayment`,从请求入口排除 ATM、CVS、BarcodeATM、AFTEE 等非信用卡渠道
+- `IgnorePayment` 仅在 `ChoosePayment=ALL` 时生效,官方公开可用值不含 AFTEE;因此 AFTEE 必须由 OMG 商户侧关闭,代码不能通过前端 CSS、DOM 注入或未公开参数隐藏第三方支付页面选项
 - 创建请求中除 `CheckMacValue` 自身外,实际发送的每个字段都参加检查码计算。
 - 回调验签必须包含 OMG 实际返回的全部字段;开启额外信息后,额外字段、未知字段及空值字段同样不能被过滤,只有 `CheckMacValue` 排除。
 
@@ -46,9 +46,9 @@
 
 ## 3. 支付方式
 
-**Decision**: `ChoosePayment=Credit`,并发送 `UnionPay=2`。
+**Decision**: `ChoosePayment=ALL`,并发送 `IgnorePayment=ATM#CVS#BarcodeATM`。
 
-**Rationale**: 用户要求继续使用 OMG 官方页面且严格只开放信用卡与 Apple Pay,不增加 App 或自有中间选择页。OMG 官方付款方式表将 Apple Pay 归入 `Credit_CreditCard`;固定 `Credit` 可排除 ATM、CVS、BarcodeATM 与 AFTEE,再以 `UnionPay=2` 隐藏银联。Apple Pay 是否实际显示仍取决于门店开通状态和当前设备环境。
+**Rationale**: 用户要求继续使用 OMG 官方支付方式选择页面,同时显示信用卡与 Apple Pay,不增加 App 二级选择页。`Credit` 与 `ApplePay` 是独立 `ChoosePayment` 请求值,固定 `Credit` 只显示信用卡;使用 `ALL + IgnorePayment` 才能同时保留两者并隐藏 ATM、CVS 与 BarcodeATM。AFTEE 不在公开过滤值中,必须由 OMG 商户侧关闭;Apple Pay 是否实际显示仍取决于门店开通状态和当前设备环境。
 
 ## 4. 门店凭证
 

+ 9 - 9
specs/020-omg-payment-rebuild/spec.md

@@ -48,7 +48,7 @@
 
 - 每个门店使用独立的 `MerchantID / HashKey / HashIV`,不使用平台统一凭证。
 - 不传 `PlatformID`。
-- `ChoosePayment` 固定为 `Credit`,并发送 `UnionPay=2` 隐藏银联;OMG 官方收银台只允许信用卡与已开通且设备支持的 Apple Pay,不开放其他支付渠道
+- `ChoosePayment` 固定为 `ALL`,并发送 `IgnorePayment=ATM#CVS#BarcodeATM`;OMG 商户侧必须关闭 AFTEE 等代码无法过滤的渠道,最终在官方选择页面保留信用卡与已开通且设备支持的 Apple Pay
 - 客户端在当前页面提交表单,不使用 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=Credit` 与 `UnionPay=2` 只开放信用卡和 Apple Pay,并隐藏银联
+已登录用户为自己的单门店餐饮订单选择 OMG 后,调用创建接口并取得由服务端签名的表单。客户端在当前页面 POST 该表单,进入对应门店的 OMG 测试收银台;服务端通过 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM` 隐藏可由代码过滤的渠道,并保留信用卡与 Apple Pay
 
 **Why this priority**: 这是进入 OMG 收银台的基础,也是本期最终付款回调及后续查询、退款的前置能力。
 
@@ -72,8 +72,8 @@
 
 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=Credit` 与 `UnionPay=2`,**Then** OMG 页面只保留信用卡与当前设备支持的 Apple Pay,并隐藏银联及全部非信用卡渠道
-4. **Given** 门店未开通 Apple Pay 或当前设备环境不支持 Apple Pay,**When** 进入收银台,**Then** 页面仍可显示信用卡,但不得回退显示 ATM、CVS、BarcodeATM、AFTEE 或银联
+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 参数篡改第三方支付页面
 
 ---
 
@@ -205,13 +205,13 @@ iOS 用户在 OMG 收银台完成或结束支付流程后,OMG 加载后端 `/p
 - **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=Credit`、`UnionPay=2`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y` 和 `CheckMacValue`。
-- **FR-013**: 创建表单 MUST NOT 发送 `PlatformID`、`PaymentInfoURL`、`ClientRedirectURL`、`ClientBackURL`、`Language`、`IgnorePayment`、ATM/CVS/BarcodeATM 期限字段、分期、定期定额或记忆卡号参数。
+- **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-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=Credit`、`UnionPay=2` 和 `NeedExtraPaidInfo=Y`;不得挑选所谓核心字段计算。
+- **FR-018**: 除 `CheckMacValue` 自身外,创建请求实际发送的全部字段 MUST 参加签名,包括 `OrderResultURL`、`ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM` 和 `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 查询链接。
@@ -268,8 +268,8 @@ token: <login-token>
     "ItemName": "Order 991786433092835",
     "ReturnURL": "https://example.test/pay/omg/notify",
     "OrderResultURL": "https://example.test/pay/omg/result",
-    "ChoosePayment": "Credit",
-    "UnionPay": "2",
+    "ChoosePayment": "ALL",
+    "IgnorePayment": "ATM#CVS#BarcodeATM",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",

+ 5 - 5
specs/020-omg-payment-rebuild/tasks.md

@@ -145,13 +145,13 @@
 - [x] T086 [US7] 更新 API 契约与 App 接入文档,明确不保存/恢复 WebView、不重放旧表单,App 仍只有 OMG/LINE Pay 顶层选择
 - [x] T087 使用 JDK 21 运行 retry、OMG 回归、模块构建与最终 diff/暂存范围检查
 
-## Phase 15: OMG 官方收银台严格渠道过滤
+## Phase 15: OMG 官方支付方式选择页渠道过滤
 
-- [x] T088 [US1] 更新表单工厂测试源码,断言 `ChoosePayment=Credit`、`UnionPay=2` 且不发送 `IgnorePayment`,并检查两个固定字段进入签名集合
+- [x] T088 [US1] 更新表单工厂测试源码,断言 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM` 且不发送 `UnionPay`,并检查两个固定字段进入签名集合
 - [x] T089 [US1] 修改 `OmgPaymentFormFactory`,由服务端固定生成并签名上述渠道参数,不改变创建、retry 请求或响应结构
-- [x] T090 [US1] 更新规格、契约、快速验收和 App 接入文档,明确 App 继续直接提交 OMG 表单,不增加二级选择页面
+- [x] T090 [US1] 更新规格、契约、快速验收和 App 接入文档,明确 App 继续直接提交 OMG 表单、不增加二级选择页面,AFTEE 由 OMG 商户侧关闭
 - [ ] T091 [US1] 在全部 OMG 功能调整完成后的统一验证阶段,使用 JDK 21 运行表单定向测试、OMG 回归和模块构建,并检查签名字段集合
-- [ ] T092 [US1] 在 OMG stage 手动确认信用卡可用、Apple Pay 在适用环境可用,且 ATM/CVS/BarcodeATM/AFTEE/银联均不显示
+- [ ] T092 [US1] 在 OMG stage 手动确认信用卡可用、Apple Pay 在适用环境可用、ATM/CVS/BarcodeATM 不显示,并确认 OMG 商户侧已关闭 AFTEE
 
 ## Phase 16: retry 查询稀疏响应兼容
 
@@ -185,7 +185,7 @@
 - 成功仅修改订单 `payStatus`,不推进业务状态或触发推送/退款。
 - 同订单重复/并发创建最多一条 `CREATED`,后续请求为 `PAYMENT_ATTEMPT_EXISTS`。
 - 官方检查码向量一致,实际发送字段无遗漏。
-- 新支付表单固定 `ChoosePayment=Credit`、`UnionPay=2`,不发送 `ChoosePayment=ALL` 或 `IgnorePayment`
+- 新支付表单固定 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`,不发送 `UnionPay`;AFTEE 由 OMG 商户侧关闭
 - 新代码全在 `omgpay` 包,不引用旧 OMG 支付实现。
 - 旧 Controller、回调、查询、补单、退款、任务及旧表运行时引用为零。
 - `pos_store_omg` 凭证表、持久化和管理能力保留;联网探测只使用新 `omgpay` 实现。