Kaynağa Gözat

文档:明确 OMG 收银台渠道过滤方案

qmj 2 hafta önce
ebeveyn
işleme
3bb25e8888

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

@@ -44,13 +44,12 @@ Outer response keeps the project `AjaxResult` format. `data` is:
     "TradeDesc": "Foodie order 991786433092835",
     "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",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",
-    "ExpireDate": "1",
-    "StoreExpireDate": "30",
-    "BarcodeATMExpireDate": "1",
     "CheckMacValue": "<64 uppercase hexadecimal characters>"
   }
 }

+ 6 - 6
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=ALL`、`NeedExtraPaidInfo=Y`;所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
+- `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM`、`NeedExtraPaidInfo=Y`;优先在 OMG 官方页面保留信用卡与 Apple Pay。官方参数不能隐藏 AFTEE,stage 验收必须记录实际展示结果。所有实际发送的非 `CheckMacValue` 字段全部参加检查码计算。
 - 回调必须对全部实际返回字段验签,包含未知额外字段与空值字段,只有 `CheckMacValue` 排除。
 - 网关固定 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`,不得配置正式环境回退。
 - 同一 `ddId` 最多一条 `CREATED`;存在时返回 `PAYMENT_ATTEMPT_EXISTS`,不重放旧表单、不生成新 MerchantTradeNo。
@@ -300,13 +300,12 @@ fields.put("TotalAmount", String.valueOf(amount));
 fields.put("TradeDesc", "Foodie order " + safeOrderId);
 fields.put("ItemName", "Order " + safeOrderId);
 fields.put("ReturnURL", properties.requireSafeReturnUrl());
+fields.put("OrderResultURL", properties.requireSafeOrderResultUrl());
 fields.put("ChoosePayment", "ALL");
+fields.put("IgnorePayment", "ATM#CVS#BarcodeATM");
 fields.put("EncryptType", "1");
 fields.put("InvoiceMark", "N");
 fields.put("NeedExtraPaidInfo", "Y");
-fields.put("ExpireDate", "1");
-fields.put("StoreExpireDate", "30");
-fields.put("BarcodeATMExpireDate", "1");
 fields.put("CheckMacValue", signer.sign(fields, hashKey, hashIv));
 ```
 
@@ -995,12 +994,13 @@ Expected: `BUILD SUCCESS` on JDK 21.
 - [ ] **Step 3: Run static safety checks**
 
 ```powershell
-rg -n "payment\.funpoint\.com\.tw|PlatformID|PaymentInfoURL|OrderResultURL|ClientRedirectURL|ClientBackURL" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
+rg -n "payment\.funpoint\.com\.tw|PlatformID|PaymentInfoURL|ClientRedirectURL|ClientBackURL" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
+rg -n "OrderResultURL|ChoosePayment|IgnorePayment|UnionPay" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
 rg -n "com\.ruoyi\.app\.pay\.OmgPayController|com\.ruoyi\.app\.utils\.omg|pos_order_omg_payment|pos_order_omg_refund" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay ruoyi-system/src/main/java/com/ruoyi/system/omgpay
 rg -n "HashKey|HashIV|CheckMacValue|formFields|token" ruoyi-admin/src/main/java/com/ruoyi/app/omgpay
 ```
 
-Interpret results manually: stage URL and required form names are expected; production URL, prohibited fields, old imports and logging of values are not.
+Interpret results manually: stage URL、`OrderResultURL`、`ChoosePayment=ALL` 与 `IgnorePayment` 是预期结果;production URL、其他禁用字段、`UnionPay`、旧 import 和敏感值日志不是预期结果。
 
 - [ ] **Step 4: Check comments and log statements**
 

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

@@ -33,7 +33,7 @@ Expected:
 
 - `status=CREATED`。
 - `gatewayUrl` 精确等于 stage AioCheckOut V5 地址。
-- `formFields` 包含规格规定的 16 个字段
+- `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 收银台,显示正确金额与门店可用的 `ALL` 渠道
+Expected: 浏览器进入 OMG stage 收银台,显示正确金额;已开通的信用卡与 Apple Pay 保留,ATM、CVS、BarcodeATM 不显示。如果门店同时开通 AFTEE,记录它是否仍显示,因为官方 `IgnorePayment` 不支持排除 AFTEE
 
 ## 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` 之类组合;本期固定 `ALL`。
+- `ChoosePayment` 是单值字段,不能传 `Credit,ApplePay` 之类组合;本期固定 `ALL`,并使用 `IgnorePayment=ATM#CVS#BarcodeATM` 隐藏官方允许排除的渠道
 - `MerchantTradeNo` 必须唯一、不可重复使用、最多 20 个 ASCII 英数字。
 - `PaymentType=aio`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y`。
-- ATM、CVS、BarcodeATM 的期限字段分别为 `ExpireDate=1`、`StoreExpireDate=30`、`BarcodeATMExpireDate=1`
+- `IgnorePayment` 仅在 `ChoosePayment=ALL` 时生效;官方公开可用值只有 `Credit`、`ATM`、`CVS`、`BarcodeATM`,没有 AFTEE,因此不能保证在所有渠道都开通时严格只剩信用卡与 Apple Pay
 - 创建请求中除 `CheckMacValue` 自身外,实际发送的每个字段都参加检查码计算。
 - 回调验签必须包含 OMG 实际返回的全部字段;开启额外信息后,额外字段、未知字段及空值字段同样不能被过滤,只有 `CheckMacValue` 排除。
 
@@ -46,9 +46,9 @@
 
 ## 3. 支付方式
 
-**Decision**: `ChoosePayment=ALL`。
+**Decision**: `ChoosePayment=ALL`,并发送 `IgnorePayment=ATM#CVS#BarcodeATM`
 
-**Rationale**: 官方只允许单一枚举值;`ALL` 会按该门店在 OMG 后台实际开通的能力展示渠道。官方 `IgnorePayment` 没有可靠覆盖 Apple Pay 与所有未来渠道,因此本期不使用它尝试拼出 Credit + ApplePay
+**Rationale**: 用户要求继续使用 OMG 官方页面,不增加 App 或自有中间选择页。该组合能隐藏官方明确支持排除的 ATM、CVS 与 BarcodeATM,并保留已开通的 Credit 和 ApplePay。若门店同时开通 AFTEE 或 OMG 未来新增渠道,公开参数无法保证隐藏;本期接受该限制并通过 stage 手动验收实际效果,不发送未公开的过滤值
 
 ## 4. 门店凭证
 

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

@@ -27,7 +27,7 @@
 ### 1.2 Explicitly out of scope
 
 - ATM、CVS、BarcodeATM 取号结果通知及缴费信息展示。
-- `OrderResultURL`、`PaymentInfoURL`、`ClientRedirectURL`、`ClientBackURL`。
+- `PaymentInfoURL`、`ClientRedirectURL`、`ClientBackURL`。
 - 定时/批量自动补单、独立人工补单入口;用户触发的当前支付查询补偿属于本阶段范围。
 - 正式环境退款动作、取消交易、信用卡关账;本阶段只实现测试环境安全拒绝入口。
 - 分期、定期定额、记忆卡号、银联专用流程。
@@ -47,7 +47,7 @@
 
 - 每个门店使用独立的 `MerchantID / HashKey / HashIV`,不使用平台统一凭证。
 - 不传 `PlatformID`。
-- `ChoosePayment` 固定为 `ALL`;具体显示渠道以该门店在 OMG 后台实际开通的能力为准。
+- `ChoosePayment` 固定为 `ALL`,并发送 `IgnorePayment=ATM#CVS#BarcodeATM`;优先让 OMG 官方收银台只展示信用卡与 Apple Pay。官方公开参数不能隐藏 AFTEE,因此商户同时开通 AFTEE 时不承诺严格只剩两个渠道,必须以 stage 实测结果为准。
 - 客户端在当前页面提交表单,不使用 iframe,不打开新窗口。
 - 当前仅接 OMG 测试环境。
 - 新可信公开路径继续使用 `/pay/omg/*`;创建入口为 `POST /pay/omg/create`,可信回调为 `POST /pay/omg/notify`。
@@ -61,7 +61,7 @@
 
 ### User Story 1 - 首次创建并进入 OMG 收银台 (Priority: P1)
 
-已登录用户为自己的单门店餐饮订单选择 OMG 后,调用创建接口并取得由服务端签名的表单。客户端在当前页面 POST 该表单,进入对应门店的 OMG 测试收银台,并看到 `ALL` 下该门店已开通的付款方式
+已登录用户为自己的单门店餐饮订单选择 OMG 后,调用创建接口并取得由服务端签名的表单。客户端在当前页面 POST 该表单,进入对应门店的 OMG 测试收银台;服务端通过 `ALL + IgnorePayment` 隐藏 ATM、CVS 和 BarcodeATM,优先保留信用卡与 Apple Pay
 
 **Why this priority**: 这是进入 OMG 收银台的基础,也是本期最终付款回调及后续查询、退款的前置能力。
 
@@ -71,7 +71,8 @@
 
 1. **Given** 用户拥有一笔 `payType="2"`、未取消、未付款、金额为正的单门店订单,且门店 OMG 凭证已启用,**When** 用户首次调用创建接口,**Then** 系统创建唯一支付尝试并返回可提交的 OMG 表单。
 2. **Given** 创建接口返回成功,**When** 客户端在当前页面向 `gatewayUrl` POST 全部 `formFields`,**Then** 浏览器进入 OMG 测试收银台,不通过 iframe 或新窗口加载。
-3. **Given** 门店在 OMG 后台开通多个付款渠道,**When** 收银台接收 `ChoosePayment=ALL`,**Then** 收银台按 OMG 与门店配置展示可用渠道。
+3. **Given** 门店已开通信用卡、Apple Pay、ATM、CVS 和 BarcodeATM,**When** 收银台接收 `ChoosePayment=ALL` 与 `IgnorePayment=ATM#CVS#BarcodeATM`,**Then** OMG 页面隐藏 ATM、CVS 和 BarcodeATM,并保留已开通的信用卡与 Apple Pay。
+4. **Given** 门店还开通 AFTEE 或 OMG 未来新增且不支持 `IgnorePayment` 的渠道,**When** 进入收银台,**Then** 手动验收如实记录额外渠道;本方案不通过非官方参数或自有页面强行隐藏。
 
 ---
 
@@ -176,13 +177,13 @@ OMG 向 `ReturnURL` 发送最终付款结果时,系统保存本次 HTTP 回传
 - **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`、`ChoosePayment=ALL`、`EncryptType=1`、`InvoiceMark=N`、`NeedExtraPaidInfo=Y`、`ExpireDate=1`、`StoreExpireDate=30`、`BarcodeATMExpireDate=1` 和 `CheckMacValue`。
-- **FR-013**: 创建表单 MUST NOT 发送 `PlatformID`、`PaymentInfoURL`、`OrderResultURL`、`ClientRedirectURL`、`ClientBackURL`、`Language`、分期、定期定额、记忆卡号或银联专用参数。
+- **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 参加签名,包括 `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 查询链接。
@@ -238,13 +239,12 @@ token: <login-token>
     "TradeDesc": "Food order 991786433092835",
     "ItemName": "Order 991786433092835",
     "ReturnURL": "https://example.test/pay/omg/notify",
+    "OrderResultURL": "https://example.test/pay/omg/result",
     "ChoosePayment": "ALL",
+    "IgnorePayment": "ATM#CVS#BarcodeATM",
     "EncryptType": "1",
     "InvoiceMark": "N",
     "NeedExtraPaidInfo": "Y",
-    "ExpireDate": "1",
-    "StoreExpireDate": "30",
-    "BarcodeATMExpireDate": "1",
     "CheckMacValue": "<64 uppercase hexadecimal characters>"
   }
 }

+ 9 - 1
specs/020-omg-payment-rebuild/tasks.md

@@ -24,7 +24,7 @@
 - [x] T009 [US1] 在 `com.ruoyi.app.omgpay.OmgCheckMacSigner` 实现排序、HashKey/HashIV 包夹、官方 .NET URL encode、小写、SHA-256 和大写输出
 - [x] T010 [US1] 运行 `OmgCheckMacSignerTest`,确认官方向量和字段完整性全部通过
 - [x] T011 [US1] 在 `OmgMerchantTradeNoGeneratorTest` 编写 20 位大写英数字与样本唯一性失败测试
-- [x] T012 [US1] 在 `OmgPaymentFormFactoryTest` 编写 Taipei 时间、stage URL、16 字段、固定字段、全部字段签名及不安全 ReturnURL 失败测试
+- [x] T012 [US1] 在 `OmgPaymentFormFactoryTest` 编写 Taipei 时间、stage URL、15 字段、固定字段、全部字段签名及不安全 ReturnURL 失败测试
 - [x] T013 [US1] 实现 `OmgPaymentProperties`、`OmgMerchantTradeNoGenerator`、`OmgPaymentForm` 与 `OmgPaymentFormFactory`
 - [x] T014 [US1] 在 `application.yml` 新增独立 `omgpay.return-url`,新代码不读取旧 `omg.*` 创建/回调配置
 - [x] T015 [US1] 运行生成器和表单测试并确认通过
@@ -145,6 +145,14 @@
 - [x] T086 [US7] 更新 API 契约与 App 接入文档,明确不保存/恢复 WebView、不重放旧表单,App 仍只有 OMG/LINE Pay 顶层选择
 - [x] T087 使用 JDK 21 运行 retry、OMG 回归、模块构建与最终 diff/暂存范围检查
 
+## Phase 15: OMG 官方收银台渠道过滤
+
+- [ ] T088 [US1] 更新表单工厂测试,先断言 `ChoosePayment=ALL`、`IgnorePayment=ATM#CVS#BarcodeATM` 且不发送 `UnionPay`,并观察当前实现失败
+- [ ] T089 [US1] 修改 `OmgPaymentFormFactory`,由服务端固定生成并签名上述渠道参数,不改变创建、retry 请求或响应结构
+- [ ] T090 [US1] 更新 App 接入文档,明确 App 继续直接提交 OMG 表单,不增加二级选择页面
+- [ ] T091 [US1] 使用 JDK 21 运行表单定向测试、OMG 回归和模块构建,并检查签名字段集合
+- [ ] T092 [US1] 在 OMG stage 手动确认信用卡与 Apple Pay 保留、ATM/CVS/BarcodeATM 隐藏,并记录 AFTEE 在门店开通时的实际展示结果
+
 ## Dependencies & Execution Order
 
 - Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 → Phase 8 → Phase 9 → Phase 10。