Selaa lähdekoodia

fix(omgpay): remove deferred payment return path

qmj 2 viikkoa sitten
vanhempi
sitoutus
5faac2698d

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

@@ -28,8 +28,7 @@ public class OmgNotifyRawBodyFilter extends OncePerRequestFilter {
         String contextPath = request.getContextPath() == null ? "" : request.getContextPath();
         String path = request.getRequestURI().substring(contextPath.length());
         return !"POST".equalsIgnoreCase(request.getMethod()) || !("/pay/omg/notify".equals(path)
-                || "/pay/omg/result".equals(path)
-                || "/pay/omg/payment-info-result".equals(path));
+                || "/pay/omg/result".equals(path));
     }
 
     @Override

+ 0 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentClientReturnService.java

@@ -18,11 +18,6 @@ public class OmgPaymentClientReturnService {
             "TradeAmt", "PaymentDate", "PaymentType", "PaymentTypeChargeFee", "TradeDate",
             "SimulatePaid", "CustomField1", "CustomField2", "CustomField3", "CustomField4",
             "CheckMacValue");
-    private static final List<String> INFO_REQUIRED = List.of(
-            "MerchantID", "MerchantTradeNo", "StoreID", "RtnCode", "RtnMsg", "TradeNo",
-            "TradeAmt", "PaymentType", "TradeDate", "CustomField1", "CustomField2",
-            "CustomField3", "CustomField4", "CheckMacValue");
-
     private final IOmgPaymentAttemptService attempts;
     private final OmgCheckMacSigner signer;
     private final OmgPaymentReturnPageRenderer renderer;
@@ -39,10 +34,6 @@ public class OmgPaymentClientReturnService {
         return verifiedReturn(request, PAYMENT_REQUIRED, "payment-result");
     }
 
-    public OmgPaymentReturnPage paymentInfoResult(OmgNotifyRequest request) {
-        return verifiedReturn(request, INFO_REQUIRED, "payment-info");
-    }
-
     public OmgPaymentReturnPage back(String merchantTradeNo) {
         OmgPaymentAttempt attempt = findAttempt(merchantTradeNo);
         if (attempt == null) {

+ 0 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentReturnController.java

@@ -39,15 +39,6 @@ public class OmgPaymentReturnController {
         return html(service.paymentResult(request));
     }
 
-    @Anonymous
-    @PostMapping(value = "/payment-info-result", produces = MediaType.TEXT_HTML_VALUE)
-    public ResponseEntity<String> paymentInfoResult(@OmgNotifyForm OmgNotifyRequest request) {
-        log.info("OMG client payment-info result received ip={}, rawForm={}",
-                sourceIp(request), rawForm(request));
-        appendAudit(request);
-        return html(service.paymentInfoResult(request));
-    }
-
     @Anonymous
     @GetMapping(value = "/back", produces = MediaType.TEXT_HTML_VALUE)
     public ResponseEntity<String> back(@RequestParam(name = "merchantTradeNo") String merchantTradeNo) {

+ 11 - 13
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgNotifyRawBodyFilterTest.java

@@ -28,20 +28,18 @@ class OmgNotifyRawBodyFilterTest {
     }
 
     @Test
-    void preservesBothClientPostBodiesButDoesNotWrapUnrelatedRequests() throws Exception {
+    void preservesPaymentResultButDoesNotWrapUnrelatedRequests() throws Exception {
         OmgNotifyRawBodyFilter filter = new OmgNotifyRawBodyFilter();
-        for (String path : new String[]{"/pay/omg/result", "/pay/omg/payment-info-result"}) {
-            MockHttpServletRequest request = new MockHttpServletRequest("POST", path);
-            request.setContent("MerchantTradeNo=FULL".getBytes(StandardCharsets.UTF_8));
-            MockFilterChain chain = new MockFilterChain();
-            filter.doFilter(request, new MockHttpServletResponse(), chain);
-            assertEquals("MerchantTradeNo=FULL", new String(
-                    chain.getRequest().getInputStream().readAllBytes(), StandardCharsets.UTF_8));
-        }
-
-        MockHttpServletRequest unrelated = new MockHttpServletRequest("POST", "/pay/omg/query");
+        MockHttpServletRequest result = new MockHttpServletRequest("POST", "/pay/omg/result");
+        result.setContent("MerchantTradeNo=FULL".getBytes(StandardCharsets.UTF_8));
+        MockFilterChain resultChain = new MockFilterChain();
+        filter.doFilter(result, new MockHttpServletResponse(), resultChain);
+        assertEquals("MerchantTradeNo=FULL", new String(
+                resultChain.getRequest().getInputStream().readAllBytes(), StandardCharsets.UTF_8));
+
+        MockHttpServletRequest request = new MockHttpServletRequest("POST", "/pay/omg/query");
         MockFilterChain chain = new MockFilterChain();
-        filter.doFilter(unrelated, new MockHttpServletResponse(), chain);
-        assertSame(unrelated, chain.getRequest());
+        filter.doFilter(request, new MockHttpServletResponse(), chain);
+        assertSame(request, chain.getRequest());
     }
 }

+ 1 - 9
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentClientReturnServiceTest.java

@@ -72,17 +72,9 @@ class OmgPaymentClientReturnServiceTest {
     }
 
     @Test
-    void verifiedPaymentInfoAndPlainBackUseDifferentNavigationSources() {
-        LinkedHashMap<String, String> info = commonFields("2", "ATM_TAISHIN");
-        info.put("BankCode", "812");
-        info.put("vAccount", "1234567890123456");
-        info.put("ExpireDate", "2026/08/14 23:59:59");
-
-        OmgPaymentReturnPage paymentInfo = service.paymentInfoResult(signedRequest(info));
+    void plainBackUsesItsOwnNavigationSource() {
         OmgPaymentReturnPage back = service.back("OMG123");
 
-        assertEquals(200, paymentInfo.statusCode());
-        assertTrue(paymentInfo.html().contains("source=payment-info"));
         assertTrue(back.html().contains("source=back"));
         assertFalse(back.html().contains("付款成功"));
     }

+ 1 - 5
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentReturnControllerTest.java

@@ -19,17 +19,13 @@ import static org.mockito.Mockito.when;
 class OmgPaymentReturnControllerTest {
 
     @Test
-    void exposesThreeAnonymousExplicitDtoContracts() throws Exception {
+    void exposesPaymentResultAndBackContracts() throws Exception {
         Method result = OmgPaymentReturnController.class.getDeclaredMethod("result", OmgNotifyRequest.class);
-        Method info = OmgPaymentReturnController.class.getDeclaredMethod(
-                "paymentInfoResult", OmgNotifyRequest.class);
         Method back = OmgPaymentReturnController.class.getDeclaredMethod("back", String.class);
 
         assertArrayEquals(new String[]{"/result"}, result.getAnnotation(PostMapping.class).value());
-        assertArrayEquals(new String[]{"/payment-info-result"}, info.getAnnotation(PostMapping.class).value());
         assertArrayEquals(new String[]{"/back"}, back.getAnnotation(GetMapping.class).value());
         assertNotNull(result.getAnnotation(Anonymous.class));
-        assertNotNull(info.getAnnotation(Anonymous.class));
         assertNotNull(back.getAnnotation(Anonymous.class));
         assertNotNull(result.getParameters()[0].getAnnotation(OmgNotifyForm.class));
         assertEquals("merchantTradeNo", back.getParameters()[0].getAnnotation(RequestParam.class).name());

+ 12 - 29
specs/016-omg-payment/contracts/api.md

@@ -25,11 +25,6 @@
 - **我方响应**:纯字符串 `1|OK`(首字符 1=成功);未收到 5–15 分重试,当天最多 4 次
 - **核销校验顺序**:CheckMacValue 验签 → `RtnCode==1` → `SimulatePaid!=1` → `TradeAmt==订单 amount` → `trade_no` 幂等
 
-### A3. ATM/超商 取号回调 — PaymentInfoURL(US3)
-
-- 盘合服务端 POST 我方 `/pay/omg/paymentInfo`,返回虚帐/缴费码(`BankCode`/`vAccount`/`ExpireDate` 或 `PaymentNo`/`ExpireDate` 等,详见文档 02 页),带 `CheckMacValue` 需验签
-- 我方落库展示并回 `1|OK`
-
 ### A4. 订单查询 — `Cashier/QueryTradeInfo/V5`(凭证验证 / 补单)
 
 - **URL**:测试 `https://payment-stage.funpoint.com.tw/Cashier/QueryTradeInfo/V5`;正式 `https://payment.funpoint.com.tw/Cashier/QueryTradeInfo/V5`
@@ -42,7 +37,7 @@
 - **URL**:正式 `https://payment.funpoint.com.tw/CreditDetail/DoAction`(**stage 不可用**)
 - **请求**:`MerchantID`、`MerchantTradeNo`、`TradeNo`、`Action`(**C** 關帳/**R** 退刷/**E** 取消/**N** 放棄)、`TotalAmount`、`CheckMacValue`、`PlatformID?`
 - **响应**(k=v):`RtnCode`(**1=成功**)、`RtnMsg`
-- **规则**:`已關帳→R`、`已授權→N`、`要關帳→E 再 N 或 R`;分期必须全额;ATM/CVS/BarcodeATM **不适用**(走人工);自动关帳开启时避开 20:15–20:30
+- **规则**:`已關帳→R`、`已授權→N`、`要關帳→E 再 N 或 R`;分期必须全额;自动关帳开启时避开 20:15–20:30
 
 ---
 
@@ -51,34 +46,26 @@
 ### B1. 发起 OMG 支付 — `POST /pay/omg/create`(US1,@Anonymous @Auth,Header token)
 
 - 入参:`orderid`(= ddId)
-- 逻辑:校验订单归属/金额/未支付 → 查门店启用凭证 → 生成 `MerchantTradeNo` → 组参 + CheckMacValue → 落 `pos_order_omg_payment`(pay_status=0) + 更新 `pos_order.pay_type="7"`/`pay_url`
+- 逻辑:校验订单归属/金额/未支付 → 查门店启用凭证 → 生成 `MerchantTradeNo` → 组参 + CheckMacValue → 落 `pos_order_omg_payment`(pay_status=0) + 更新 `pos_order.pay_type="2"`/`pay_url`
 - 返回:form 字段 `{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=Credit, UnionPay=2, EncryptType=1, ItemName, CheckMacValue, ... }`,前端构建隐藏 form 自动 submit 到 `gatewayUrl`
 
 ### B2. OMG 支付结果回调 — `POST /pay/omg/notify`(US2,@Anonymous)
 
 - 处理 A2:collectForm → IpnLog → 凭证(MerchantID) → 验签 CheckMacValue → 幂等(trade_no) → 金额校验 → RtnCode==1 && SimulatePaid!=1 → markSuccess → 更新订单(state=0, payStatus=1) + 订单日志 + 推送用户/商家/骑手 + sendAcceptRiderPush → 回纯串 `1|OK`
 
-### B3. ??????? ? `POST /pay/omg/result`?US2?@Anonymous?
-
-- ?? `OrderResultURL` ?? OMG Client POST????? DTO ?????????????????????????????? CheckMacValue?
-- ???????? no-store HTML?????? HTTPS Universal/App Link ?? App?????????App ???? `/pay/omg/query`?
-
-### B3a. ??????? ? `POST /pay/omg/payment-info-result`?US3?@Anonymous?
-
-- ?? `ClientRedirectURL` ?? ATM/CVS/BarcodeATM Client POST??????????????? App ?????????????
-
-### B3b. ????? ? `GET /pay/omg/back?merchantTradeNo=`?@Anonymous?
+### B3. 支付完成返回页 — `POST /pay/omg/result`(US2,@Anonymous)
 
-- ?? `ClientBackURL`???? MerchantTradeNo ????????????? App?OMG ????????????????????????
+- 作为 `OrderResultURL` 接收 OMG Client POST,使用完整 DTO 严格解析并按支付尝试凭证快照验签;未知额外字段和空值全部进入 CheckMacValue。
+- 验签通过后只返回 no-store HTML,通过配置的 HTTPS Universal/App Link 唤醒 App;不修改订单状态,App 必须再调 `/pay/omg/query`。
 
-### B4. ATM/超商 取号回调 — `POST /pay/omg/paymentInfo`(US3,@Anonymous)
+### B3b. 普通返回页 — `GET /pay/omg/back?merchantTradeNo=`(@Anonymous)
 
-- 处理 A3:验签 → 落虚帐/缴费码 + 期限 → 回 `1|OK`
+- 作为 `ClientBackURL`,只验证 MerchantTradeNo 能定位新支付尝试并引导返回 App;OMG 不携带付款结果,因此不得据此改变或宣称付款状态。
 
 ### B5. 订单查询/被动补单 — `POST /pay/omg/query`(US6 方案A,@Anonymous @Auth,Header token)
 
 - **入参**:`orderid`(= ddId)
-- **鉴权**:登录用户 + 订单本人(`token → userId == order.userId`);`order.payType` 必须为 `"7"`
+- **鉴权**:登录用户 + 订单本人(`token → userId == order.userId`);`order.payType` 必须为 `"2"`
 - **逻辑**:订单已 `payStatus∈{1,2}` 直接返回不查 OMG;否则复用 `OmgPay.queryTrade()`(§A4) → 按 `TradeStatus` 分支(见下),严格幂等(`markSuccess` 按 trade_no CAS,回调+补单并发不重复核销/推送)
 - **复用**:`paymentService.getLatestByDdId` + `storeOmgService.getCredentialByMerchantId` + `OmgPayController.reconcileByQuery()`(与定时补单共用)→ `applyPaidResult()`(markSuccess + `handlePaymentSuccess` 推送,与 notify 同链路)
 - **返回**:
@@ -86,7 +73,7 @@
   { "code": 200, "data": { "payStatus": 1, "reconciled": true } }
   // payStatus: 0未付 / 1已付(含本次补单) / 2失败 ; reconciled: 本次是否触发了补单核销
   ```
-- **TradeStatus 分支**:`1` 已付 → 金额/字段校验通过 → `applyPaidResult` 补单 + 返回 `{1, true}`;`10200095` 失败 → `markFail` + 返回 `{2, false}`;`0` 未付(延期支付付款前常态) 或其他未知值 → 保持未支付 + 返回 `{0, false}`(前端继续轮询 / 定时任务下轮再查)
+- **TradeStatus 分支**:`1` 已付 → 金额/字段校验通过 → `applyPaidResult` 补单 + 返回已支付;`10200095` 失败 → `markFail` + 返回支付失败;`0` 或其他未知值 → 保持待支付(前端继续轮询 / 定时任务下轮再查)
 - **自愈**:若流水已 `pay_status=1` 但订单未核销(跨事务中断残留),补推订单状态,避免「付了钱订单永远未支付」
 
 ### B6. 订单取消退款(US4)
@@ -104,7 +91,7 @@
 - **触发**:`@Scheduled(fixedDelay)` 默认每 3 分钟一轮(`omg.reconcile.fixed-delay-ms`),上一轮跑完才开始计时,启动延迟 60s
 - **扫描**:`paymentService.scanLeakOrders(windowStart, graceCutoff, batchSize)` → `pos_order_omg_payment` 中 `pay_status=0` 且 `create_time` 落在 `[now-windowHours, now-graceMinutes]`、每订单取最新一笔、`INNER JOIN pos_order` 排除已取消(`state≠4`)
 - **补单**:逐笔调 `OmgPayController.reconcileByQuery(ddId,"scheduled")`(与 §B5 被动补单同一核销链路)
-- **补单窗口**:默认 `window-hours=168`(7 天),覆盖延期支付(ATM/超商) `ExpireDate`(1~3 天);`grace-minutes=2` 给回调/OMG重试留送达时间;窗口外停止扫描(避免无限轮询)
+- **补单窗口**:按当前配置扫描仍待支付的有效信用卡尝试;`grace-minutes` 给回调/OMG重试留送达时间,窗口外停止扫描避免无限轮询。
 - **多实例并发保护**:Redisson 分布式锁 `lock:omg:reconcile`(`tryLock` 拿不到即让出),保证同一轮只一个节点执行;幂等能兜底,锁减少无效调用
 - **配置**:`application.yml` → `omg.reconcile.{fixed-delay-ms, initial-delay-ms, window-hours, grace-minutes, batch-size, lock-wait-seconds, lock-lease-seconds}`
 
@@ -120,13 +107,9 @@
 
 | PaymentType | 名称 | 即时/延期 |
 |---|---|---|
-| `Credit_CreditCard` | 信用卡(含銀聯/Apple Pay) | 即时 |
-| `BarcodeATM_CHINATRUST` | 超商快付代碼繳費 | 即时 |
-| `ATM_FIRST` / `ATM_CHINATRUST` / `ATM_UBOT` / `ATM_KGI` | 各银行 ATM | 延期(取虚帐→期限内付款) |
-| `CVS_CVS` / `CVS_FAMILY` / `CVS_IBON` / `CVS_HILIFE` | 超商代碼繳費 | 延期 |
-| `AFTEE_AFTEE` | AFTEE 先享後付 | 延期 |
+| `Credit_CreditCard` | 信用卡(含 Apple Pay;请求以 `UnionPay=2` 隐藏银联) | 即时 |
 
-延期方式(ATM/CVS/AFTEE)走 `PaymentInfoURL` 取号 → US3
+本项目当前只接受这一种回覆类型;开发测试阶段没有延期支付历史交易需要兼容。
 
 ### C3. RtnCode(回调 / 退款响应)
 

+ 57 - 119
specs/016-omg-payment/frontend-integration.md

@@ -22,7 +22,7 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
 ```
 [App: 订单详情页 点"去支付"]
-        │  ① POST /pay/omg/create?orderid={ddId}   (Header: token)
+        │  ① POST /pay/omg/create   JSON {"orderId":"..."} (Header: token)
 [后端] 校验订单 → 组参+签名 → 返回 form 字段(含 gatewayUrl)
@@ -35,7 +35,7 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
 [OMG 收银台网页]  用户使用信用卡或 Apple Pay 付款
         │  ③ 付款后 OMG 把 web-view 跳回 OrderResultURL
-        │     后端 /pay/omg/return 302 → 前端结果页?ddId=xxx
+        │     后端 /pay/omg/result 返回安全桥接页 → App Link
 [App: 支付结果页] 读 ddId → 查订单状态 → 展示结果
 
@@ -54,8 +54,8 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
 |---|------|--------|
 | 1 | 放一个 `omgPay.html` 到 `static/`,作为 web-view 收银台跳转页 | uni-app 工程 |
 | 2 | 发起支付:调 `/pay/omg/create` → 拼 URL → 打开 web-view | 发起支付页 |
-| 3 | 支付结果页:接收 `ddId` → 查状态 → 展示成功/失败/取号 | 支付结果页 |
-| 4 | 不再实现 ATM/超商取号展示;后端旧接口仅兼容变更前交易 | 无新增页面 |
+| 3 | 支付结果页:接收 `orderId` → 调 `/pay/omg/query` → 展示成功/失败/确认中 | 支付结果页 |
+| 4 | 不实现 ATM/超商取号及延期支付返回入口 | 无新增页面 |
 | 5 | (可选)取消订单退款:调 `/pay/omg/refund` | 订单取消逻辑 |
 | 6 | 告诉后端两个前端地址(见 §6 配置依赖) | 沟通 |
 | 7 | 所有面向用户文字走 i18n 4 语言 | zh/tw/en/vi |
@@ -75,13 +75,16 @@ OMG(欧买尬/FunPoint)是**网页托管式支付网关**(AIO = All-In-One
 |----|------|
 | Method | `POST` |
 | Header | `token: <用户JWT>` |
-| 入参 | query:`orderid` = 订单号 ddId |
+| 入参 | JSON:`{"orderId":"订单号"}` |
 | 鉴权 | 登录用户 + 必须是订单本人 |
 
 **请求示例**
 ```
-POST {baseURL}/pay/omg/create?orderid=DD202608060001
+POST {baseURL}/pay/omg/create
 Header: token: eyJhbGciOi...
+Content-Type: application/json
+
+{"orderId":"DD202608060001"}
 ```
 
 **成功返回**(`code=200`,取 `data`)
@@ -90,23 +93,26 @@ Header: token: eyJhbGciOi...
   "code": 200,
   "msg": "操作成功",
   "data": {
+    "status": "CREATED",
     "gatewayUrl": "https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5",
-    "MerchantID": "3xxxxxx2",
-    "MerchantTradeNo": "OMG20260806143012xxx",
-    "MerchantTradeDate": "2026/08/06 14:30:12",
-    "PaymentType": "aio",
-    "TotalAmount": "350",
-    "TradeDesc": "food order DD202608060001",
-    "ItemName": "order DD202608060001",
-    "ReturnURL": "https://api.xxx.com/pay/omg/notify",
-    "ChoosePayment": "Credit",
-    "UnionPay": "2",
-    "EncryptType": "1",
-    "InvoiceMark": "N",
-    "NeedExtraPaidInfo": "Y",
-    "OrderResultURL": "https://app.xxx.com/.../omgResult",
-    "PaymentInfoURL": "https://api.xxx.com/pay/omg/paymentInfo",
-    "CheckMacValue": "A1B2C3D4E5F6...(64位)"
+    "formFields": {
+      "MerchantID": "3xxxxxx2",
+      "MerchantTradeNo": "OMG20260806143012xxx",
+      "MerchantTradeDate": "2026/08/06 14:30:12",
+      "PaymentType": "aio",
+      "TotalAmount": "350",
+      "TradeDesc": "food order DD202608060001",
+      "ItemName": "order DD202608060001",
+      "ReturnURL": "https://api.xxx.com/pay/omg/notify",
+      "ChoosePayment": "Credit",
+      "UnionPay": "2",
+      "EncryptType": "1",
+      "InvoiceMark": "N",
+      "NeedExtraPaidInfo": "Y",
+      "OrderResultURL": "https://api.xxx.com/pay/omg/result",
+      "ClientBackURL": "https://api.xxx.com/pay/omg/back?merchantTradeNo=...",
+      "CheckMacValue": "A1B2C3D4E5F6...(64位)"
+    }
   }
 }
 ```
@@ -125,67 +131,27 @@ Header: token: eyJhbGciOi...
 
 ---
 
-### 3.2 支付结果页跳转 — `GET/POST /pay/omg/return`(后端自动 302)
+### 3.2 支付结果返回 — `POST /pay/omg/result` / `GET /pay/omg/back`
 
-**不是前端直接调的接口**。付款完成后 OMG 把网页跳到后端 `/pay/omg/return`,后端反查出 `ddId` 后 **302 重定向**到前端结果页:
+**不是前端直接调的接口**。付款完成后 OMG Client POST 到 `/pay/omg/result`;用户点收银台“返回商店”时进入 `/pay/omg/back`。两个入口都只返回 no-store 安全桥接页,通过配置的 App Link 唤醒 App,不修改支付状态。
 
 ```
-{omg.order-result-url}?ddId=DD202608060001
+{omgpay.app-return-url}?orderId=DD202608060001
 ```
 
-前端只需:**注册一个结果页路由**接收 query 上的 `ddId`(见 §4.3)。`omg.order-result-url` 由后端配置(§6),需要前端把这个页面地址给后端
+App 打开后必须调用 `/pay/omg/query` 确认结果;`/result` 验签通过也不等于可以直接宣称已付款
 
 ---
 
-### 3.3 历史 ATM/超商取号查询(新客户端不得调用)
-
-**用途**:仅用于排查或兼容变更前已经创建的延期支付交易。新客户端统一调用 `/pay/omg/query` 查询信用卡/Apple Pay 状态,不再展示虚拟帐号或缴费码。
-
-| 项 | 内容 |
-|----|------|
-| Method | `GET` |
-| Header | `token: <用户JWT>` |
-| 入参 | path:`orderid` = ddId |
-| 鉴权 | 登录用户 + 订单本人 |
+### 3.3 支付状态查询 — `POST /pay/omg/query`
 
-**请求示例**
-```
-GET {baseURL}/pay/omg/paymentInfo/DD202608060001
-Header: token: eyJ...
-```
-
-**返回**
-```json
-{
-  "code": 200,
-  "data": {
-    "payType": "ATM_FIRST",        // 付款方式(见 §5)
-    "amount": 350,
-    "payStatus": 0,                // 0未支付/1已支付/2失败/3已退款
-    "info": {                       // 取号信息(信用卡即时支付时 info 为空 {})
-      "BankCode": "807",
-      "vAccount": "99123450000012",
-      "ExpireDate": "2026/08/09",
-      "TradeNo": "..."
-    }
-  }
-}
-```
-
-`info` 字段说明(延期支付才有):
-
-| 付款方式 payType | info 里的关键字段 |
-|-----------------|------------------|
-| `ATM_*` | `BankCode`(银行代码) `vAccount`(虚拟帐号) `ExpireDate`(缴费期限) |
-| `CVS_*` | `PaymentNo`(缴费码) `ExpireDate`(期限) |
-| `BarcodeATM_*` | 同 CVS |
-| `AFTEE_AFTEE` | AFTEE 先享后付信息 |
+App 返回后用 Header `token` 和 JSON `{"orderId":"..."}` 调用。接口只查询该订单唯一有效支付尝试;若成功回调丢失,会向 OMG 查询并同步本地支付与订单状态。
 
 ---
 
 ### 3.4 退款(取消已支付订单)— `POST /pay/omg/refund`
 
-**用途**:用户取消已支付订单时退款(仅信用卡/Apple Pay 可自动退;ATM/超商需人工,接口会返回提示)
+**用途**:用户取消已支付的信用卡/Apple Pay 订单时退款。
 
 | 项 | 内容 |
 |----|------|
@@ -202,7 +168,6 @@ Header: token: eyJ...
 
 **返回**
 | code=200 | 信用卡/Apple Pay 退刷成功,`pos_order.pay_status` 置 2 |
-| code=500 + 「该支付方式需人工在 OMG 后台退款」 | ATM/超商/BarcodeATM,无自动退款 API,后端已记工单 |
 
 ---
 
@@ -304,29 +269,27 @@ export default {
 
 ### 4.3 支付结果页 `pages/pay/omgResult`
 
-后端 `/pay/omg/return` 会 302 到 `omgResult?ddId=xxx`。这个页面在 web-view 内被加载——加载后建议**立即跳回原生页**或直接渲染结果。最稳的做法:web-view 监听 URL 变化,命中 `omgResult` 就 `navigateBack` 回原生订单详情,由原生页轮询状态。
+后端 `/pay/omg/result` 或 `/pay/omg/back` 返回桥接页并尝试打开 `omgpay.app-return-url`。App 结果页取得 `orderId` 后查询后端确认支付状态。
 
 ```js
-// pages/pay/omgCheckout.vue 增强:监听收银台跳到结果页
-// uni-app web-view 可通过 @message 或 bindmessage 接收 H5 postMessage
-// 更简单:让 omgResult 页 postMessage 通知原生关闭 web-view
+// App Link 对应页面加载后,从链接参数取得 orderId 并开始查询。
 ```
 
 原生订单详情页(回来后)**轮询确认状态**:
 
 ```js
-// 信用卡即时支付:轮询 paymentInfo 看是否 payStatus=1
+// 信用卡/Apple Pay:查询唯一有效支付尝试
 const poll = (ddId) => {
   uni.request({
-    url: baseURL + '/pay/omg/paymentInfo/' + ddId,
+    url: baseURL + '/pay/omg/query',
+    method: 'POST',
     header: { token: uni.getStorageSync('token') },
+    data: { orderId: ddId },
     success: (res) => {
       const d = res.data.data;
-      if (d.payStatus === 1)      { showSuccess(); }       // 支付成功
-      else if (d.payStatus === 2) { showFail(); }          // 失败
-      else if (d.payType && d.payType.startsWith('ATM') ||
-               d.payType && d.payType.startsWith('CVS'))   { showPickup(d.info); } // 延期→展示取号
-      else { setTimeout(() => poll(ddId), 2000); }         // 待回调,继续轮询
+      if (d.status === 'PAID')        { showSuccess(); }
+      else if (d.status === 'FAILED') { showFail(); }
+      else { setTimeout(() => poll(ddId), 2000); }
     }
   });
 };
@@ -334,21 +297,6 @@ const poll = (ddId) => {
 
 > 轮询建议:每 2s 一次,最多 15 次(30s);超过仍未成功提示「支付结果确认中,请稍后在订单列表查看」。
 
-### 4.4 历史 ATM/超商取号展示(不再开发)
-
-以下示例只用于维护变更前交易,不属于新客户端范围:
-
-```html
-<view>银行代码:{{ info.BankCode }}</view>
-<view>虚拟帐号:{{ info.vAccount }}</view>
-<view>缴费期限:{{ info.ExpireDate }}</view>
-<!-- 超商则展示 PaymentNo -->
-```
-
-并提示(i18n):「请在期限内完成缴费,缴费后订单将自动变为已支付」。
-
----
-
 ## 5. 状态码 / 付款方式速查
 
 ### `payStatus`(订单 + 流水)
@@ -362,14 +310,9 @@ const poll = (ddId) => {
 ### `payType`(OMG 回覆的付款方式)
 | payType | 名称 | 即时/延期 | 可自动退款 |
 |---------|------|----------|-----------|
-| `Credit_CreditCard` | 信用卡(含 Apple Pay / 银联) | 即时 | ✅ |
-| `BarcodeATM_CHINATRUST` | 超商快付代碼繳费 | 即时 | ❌ 人工 |
-| `ATM_FIRST/CHINATRUST/UBOT/KGI` | 各银行 ATM | **延期(取虚帐)** | ❌ 人工 |
-| `CVS_CVS/FAMILY/IBON/HILIFE` | 超商代碼繳费 | **延期(取码)** | ❌ 人工 |
-| `AFTEE_AFTEE` | AFTEE 先享后付 | 延期 | ❌ 人工 |
+| `Credit_CreditCard` | 信用卡(含 Apple Pay;银联已隐藏) | 即时 | ✅ |
 
-> **即时支付**(信用卡/Apple Pay)→ 结果页轮询 `payStatus` 变 1 即成功。
-> 新订单不再产生延期支付。历史 ATM/超商/AFTEE 交易仍由后端回调兼容,不应重新开放其前端入口。
+> 当前开发测试范围只有信用卡和 Apple Pay,不存在延期支付历史交易兼容。
 
 ---
 
@@ -379,11 +322,12 @@ const poll = (ddId) => {
 
 | 配置项 | 含义 | 需要的值(示例) |
 |--------|------|----------------|
-| `omg.order-result-url` | 付款后跳回的前端结果页 | `https://app.xxx.com/#/pages/pay/omgResult` |
-| 历史 `paymentInfo` 路由 | 仅兼容变更前 ATM/超商交易,**前端不用管** | 不进入新表单 |
-| `omg.return-url` | 服务端结果回调(后端地址,**前端不用管**) | `https://api.xxx.com/pay/omg/notify` |
+| `omgpay.order-result-url` | OMG 付款完成 Client POST 地址 | `https://api.xxx.com/pay/omg/result` |
+| `omgpay.client-back-url` | 收银台“返回商店”地址 | `https://api.xxx.com/pay/omg/back` |
+| `omgpay.app-return-url` | 后端桥接页唤醒 App 的 HTTPS App Link | `https://app.xxx.com/pay/omg-result` |
+| `omgpay.notify-url` | 服务端结果回调(前端不用管) | `https://api.xxx.com/pay/omg/notify` |
 
-> 前端只需要确认:**`omgResult` 页面的完整 URL**(含 hash 路由或 history 路由),交给后端填 `omg.order-result-url`。测试期可用测试域名
+> 前端需要提供已完成 iOS Universal Link / Android App Link 关联的 HTTPS 地址,交给后端配置 `omgpay.app-return-url`
 
 ---
 
@@ -394,8 +338,8 @@ const poll = (ddId) => {
 3. **iOS web-view 不可另开窗**——收银台必须是完整页 web-view,不能 `window.open`、不能 iframe 嵌套。
 4. **query 传参必须 encodeURIComponent**——`MerchantTradeDate` 含 `/` 和空格,不编码会截断。
 5. **多次发起**——同一订单可重复发起(每次新 `MerchantTradeNo`),后端按 OMG 交易号 `tradeNo` 幂等,前端重复点「支付」不会重复扣款,但仍建议按钮防抖(后端已加 1s `@RepeatSubmit`)。
-6. **不要重新开放延期支付入口**——新表单只认 `ChoosePayment=Credit`、`UnionPay=2`;历史回调兼容不代表可创建 ATM/超商交易
-7. **退款范围**——新订单只有信用卡/Apple Pay;历史 ATM/超商交易若出现退款仍按后台人工流程处理
+6. **付款方式固定**——表单只使用 `ChoosePayment=Credit`、`UnionPay=2`,不实现延期支付入口
+7. **退款范围**——当前订单只有信用卡/Apple Pay
 
 ---
 
@@ -410,13 +354,7 @@ const poll = (ddId) => {
 | `payOmg.success` | 支付成功 | 结果页 |
 | `payOmg.fail` | 支付失败 | 结果页 |
 | `payOmg.confirming` | 支付结果确认中 | 轮询中 |
-| `payOmg.bankCode` | 银行代码 | 取号展示 |
-| `payOmg.vAccount` | 虚拟帐号 | 取号展示 |
-| `payOmg.paymentNo` | 缴费代码 | 超商取号 |
-| `payOmg.expireDate` | 缴费期限 | 取号展示 |
-| `payOmg.atmTip` | 请在期限内完成缴费,缴费后订单将自动变为已支付 | 延期支付提示 |
 | `payOmg.storeNotSupported` | 该门店暂不支持在线支付 | 门店未开通 |
-| `payOmg.refundManual` | 该支付方式需人工退款,请联系客服 | 退款人工提示 |
 
 > key 必须用有意义英文(驼峰),加到 4 个语言文件**对应层级对象内**(参考项目 CLAUDE.md 的 i18n 规范)。
 
@@ -427,9 +365,9 @@ const poll = (ddId) => {
 | 前端动作 | 后端方法 | 文件 |
 |---------|---------|------|
 | 发起支付 | `OmgPayController.create` | `app/pay/OmgPayController.java` |
-| 结果页 302 | `OmgPayController.returnCallback` | 同上 |
-| 取号查询 | `OmgPayController.getPaymentInfo` | 同上 |
-| 退款 | `OmgPayController.refund` | 同上 |
-| 服务端回调(前端无感) | `OmgPayController.notify` / `paymentInfoCallback` | 同上 |
+| 支付结果桥接页 | `OmgPaymentReturnController.result` / `back` | `app/omgpay/OmgPaymentReturnController.java` |
+| 状态查询 | `OmgPaymentController.query` | `app/omgpay/OmgPaymentController.java` |
+| 退款 | `OmgPaymentController.refund` | 同上 |
+| 服务端回调(前端无感) | `OmgPaymentController.notify` | 同上 |
 
 后端契约详见 `specs/016-omg-payment/contracts/api.md`。

+ 3 - 1
specs/016-omg-payment/payment-attempt-lifecycle.md

@@ -1,5 +1,7 @@
 # OMG 支付流水生命周期重设计(bug 修复 + 账本重构调查)
 
+> **废弃说明(2026-08-13)**:本文中的多支付尝试、延期支付和历史数据讨论只保留为调查记录,不再指导当前实现。当前每个订单只保留一条有效支付尝试,仅支持信用卡和 Apple Pay,不迁移或兼容开发测试阶段旧数据。
+
 **日期**:2026-08-11
 **状态**:追加式 S 已用户确认 / bugB 验签已修(commit `1a424c3`)/ 正在写实现 plan(见 §0 现状)
 **关联**:`specs/016-omg-payment/spec.md`、`callback-reconcile.md`、`contracts/api.md`;记忆 `project-016-omg-payment.md`
@@ -79,7 +81,7 @@ OMG 后台截图证明付的正是 **id=26(最新那条)**,不是较早的
 ## 3. 硬约束
 
 1. **MerchantTradeNo 提交到收银台后永久绑定、不可复用**(OMG 官方明文)。
-2. **历史延期支付兼容**:2026-08-13 前创建的 ATM/超商/BarcodeATM/AFTEE 交易仍可能在有效期内付款,取号与回调继续按原 `MerchantTradeNo` 归属;新订单已固定 `ChoosePayment=Credit`,不再产生延期支付入口
+2. **延期支付方案已撤销**:当前只有开发测试数据,不兼容 ATM/超商/BarcodeATM/AFTEE 历史交易;新订单固定 `ChoosePayment=Credit`
 3. **notify 可能丢/迟到**(stage 尤甚),`queryTrade` 是兜底;但补单现依赖"最新一条=被付的那条"假设,多 MTN 时失效。
 4. **换号有丢钱风险**:旧 MTN 实付但 notify 未到 → 把行 UPDATE 成新 MTN → 旧 MTN 迟到 notify `getByMerchantTradeNo` 查不到 → 钱付了订单永未支付。安全换号必须先 `queryTrade` 确认旧 MTN 未付;而 `queryTrade` 当前不可靠(问题 B)→ **问题 B 必须先修,换号才有安全网**。
 5. **模块边界**:外部 HTTP 工具/锁在 `ruoyi-admin`;实体/Mapper/XML 在 `ruoyi-system`,不可反向依赖。

+ 15 - 9
specs/016-omg-payment/plan.md

@@ -12,13 +12,19 @@
 
 **追加(2026-08-12):账本重构(问题 A 多 MTN 堆积 + 问题 B 验签/补单修复)落地追加式 S(双轴 is_active ⊥ pay_status),设计见 [payment-attempt-lifecycle.md](payment-attempt-lifecycle.md),任务见 tasks.md Phase 10(T051–T062)。bugB 验签已修(commit `1a424c3` CASE_INSENSITIVE_ORDER,queryTrade 可靠)。**
 
+**追加(2026-08-12):支付成功核销统一在 `handlePaymentSuccess` 使用 `id + state IN (0,1) + pay_status=0` 原子条件,使普通待接单订单与堂食自动接单订单都能由回调或主动补单核销;其他状态继续拒绝核销。**
+
+**订单取消后迟到的 OMG 成功回调/主动补单:回调只持久化付款事实且禁止履约,再由 OMG 定时任务扫描符合条件的订单并复用退款服务;当前付款范围只有信用卡与 Apple Pay,执行自动退款。**
+
+**支付尝试生命周期已经收敛为每个订单一条有效支付尝试;重复点击复用该尝试,查询、回调与自动补偿都只围绕它处理。当前开发测试数据不保留延期支付或历史多尝试兼容。**
+
 ## Technical Context
 
 **Language/Version**: Java 21(Spring Boot 3 / RuoYi-Vue-Plus,MyBatis-Plus,Lombok)
 
 **Primary Dependencies**: Spring Boot、MyBatis-Plus(@TableName/@TableId,XML mapper)、Apache HttpClient 4(幕后 POST QueryTradeInfo/DoAction)、fastjson2、`@Anonymous`/`PermitAllUrlProperties`(回调白名单)
 
-**Storage**: MySQL(新增 `pos_store_omg` / `pos_order_omg_payment` / `pos_order_omg_refund` 三表,DDL 写 `updatesql/sql.md` 不直接执行;`pos_order` 不改结构,仅扩展 `pay_type="7"`;后台回调复用共享 `ipn_log` 保存脱敏内容并写 `type=omg`)
+**Storage**: MySQL(新增 `pos_store_omg` / `pos_order_omg_payment` / `pos_order_omg_refund` 三表,DDL 写 `updatesql/sql.md` 不直接执行;`pos_order` 不改结构,OMG 使用 `pay_type="2"`;后台回调复用共享 `ipn_log` 保存脱敏内容并写 `type=omg`)
 
 **Testing**: 工具类 `main` 自测(CheckMacValue 用官方示例值复算)+ 测试环境端到端(测试卡,见 quickstart.md);项目无统一自动化测试框架约定
 
@@ -74,7 +80,7 @@ ruoyi-admin/src/main/java/com/ruoyi/app/
 │   ├── OmgPayConfig.java             # MerchantID / HashKey / HashIV
 │   └── OmgPay.java                   # HTTP 客户端:createAioForm / queryTrade / doAction
 ├── pay/
-│   └── OmgPayController.java         # /pay/omg/{create,notify,return,paymentInfo}(US1/US2/US3)
+│   └── OmgPaymentController.java     # /pay/omg/{create,notify,query,refund}
 └── mendian/
     └── PosStoreOmgController.java    # /system/storeOmg/* 凭证开通管理(US5)
 
@@ -108,19 +114,19 @@ application.yml (+application-dev.yml) # 新增 omg.* 配置段
 - 复用已经完成的查询响应验签和支付状态机:已付款补成已付款,明确失败同步失败,未付款或暂时异常保留并等待下一轮。
 - 单轮限制 45 秒预算,单条异常记录完整异常上下文并继续,不记录 HashKey/HashIV。
 
-## 2026-08-13 App ???????
+## 2026-08-13 App 返回页增量计划
 
-- ?????????? `OrderResultURL`?`ClientRedirectURL`?`ClientBackURL`???????????????????
-- `POST /pay/omg/result` ?????? Client POST?`POST /pay/omg/payment-info-result` ???????? Client POST?????????????????????????????????? CheckMacValue?
-- `GET /pay/omg/back` ??????? MerchantTradeNo ????????????? no-store HTML????????????
-- HTML ????????? HTTPS Universal Link/App Link?????? URL ? HTML ??????????????App ????? `/pay/omg/query` ?????
-- `OMG_APP_RETURN_URL` ?????????????????????????????? App scheme ?????
+- 新支付表单签入 `OrderResultURL`、`ClientBackURL`,不配置 `ClientRedirectURL`。
+- `POST /pay/omg/result` 接收支付完成 Client POST,复用严格原始表单解析并以支付尝试的凭证快照验签,所有实际字段都参加 CheckMacValue。
+- `GET /pay/omg/back` 仅按不可猜测的 MerchantTradeNo 找回订单;两个入口都只生成 no-store HTML,不修改支付或订单状态。
+- HTML 只跳往服务端配置的 HTTPS Universal Link/App Link,订单号进行 URL 与 HTML 双重转义;同时保留手动按钮,App 打开后调用 `/pay/omg/query` 确认状态。
+- `OMG_APP_RETURN_URL` 未配置或配置不安全时不执行跳转,页面明确提示配置缺失;不猜测 App scheme 或包信息。
 
 ## 2026-08-13 支付方式范围重大调整
 
 - 新支付订单仅允许信用卡和 Apple Pay。OMG 官方将 Apple Pay 包含在 `Credit` 下,因此固定传 `ChoosePayment=Credit`;同时传 `UnionPay=2` 隐藏银联。
 - 新表单移除 `ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`、`ClientRedirectURL`,所有保留字段(含 `UnionPay`)统一参加 CheckMacValue 计算。
-- ATM、CVS、BarcodeATM、AFTEE 不再向新订单开放。已存在的历史回调解析与路由暂时保留,避免变更前交易无法归属
+- ATM、CVS、BarcodeATM、AFTEE 不开放;当前只有开发测试数据,不保留延期支付历史回调或返回路由
 
 ## Complexity Tracking
 

+ 4 - 5
specs/016-omg-payment/research.md

@@ -40,7 +40,7 @@
 
 **Decision**:新建 `pos_order_omg_payment`(每笔 OMG 交易一条,按 `trade_no` = OMG `TradeNo` 幂等),**不复用** 011 的 `pos_order_payment`(复用即复用蓝新代码/数据,违背约束 0)。
 
-**字段**:`dd_id`、`merchant_trade_no`(商店交易编号 MerchantTradeNo,发起生成,唯一索引)、`trade_no`(OMG 交易编号 TradeNo,回调获得,幂等键,唯一索引)、`store_id`、`merchant_id`、`choose_payment`(发起 ALL)、`pay_type`(回调方式 Credit/ATM/CVS/ApplePay/BarcodeATM/AFTEE)、`amount`、`rtn_code`、`rtn_msg`、`pay_status`(0 未支付/1 已支付/2 失败/3 已退款/4 退款中)、`auth_code`、`pay_time`(PaymentDate)、`trade_date`、`callback_raw`、审计时间。
+**字段**:`dd_id`、`merchant_trade_no`(商店交易编号 MerchantTradeNo,发起生成,唯一索引)、`trade_no`(OMG 交易编号 TradeNo,回调获得,幂等键,唯一索引)、`store_id`、`merchant_id`、`choose_payment`(固定 Credit)、`pay_type`(回调为 Credit)、`amount`、`rtn_code`、`rtn_msg`、`pay_status`(0 未支付/1 已支付/2 失败/3 已退款/4 退款中)、`auth_code`、`pay_time`(PaymentDate)、`trade_date`、`callback_raw`、审计时间。
 
 **退款**:新建 `pos_order_omg_refund`(每次 DoAction 一行:`payment_id`/`trade_no`/`action`/`amount`/`rtn_code`/`rtn_msg`/`callback_raw`/`time`),与流水解耦,支持多次动作与对账。
 
@@ -50,7 +50,7 @@
 
 ## D3. 门店凭证表:新建 pos_store_omg(独立于蓝新凭证表)
 
-**Decision**:新建 `pos_store_omg`(与 `pos_store` 1:1),状态机沿用 009/011 的「0 未申请 / 1 申请中 / 2 已开通 + is_enabled」模式,但**独立表独立字段**,不复用 `pos_store_newebpay`。字段:`store_id`(UK)、`omg_status`、`is_enabled`、`merchant_id`、`hash_key`、`hash_iv`、`enabled_payments`(本期固定 ALL,字段保留以备收敛)、`apply_time`、`approved_time`、`last_verify_result`、`remark` + 审计字段。
+**Decision**:新建 `pos_store_omg`(与 `pos_store` 1:1),状态机沿用 009/011 的「0 未申请 / 1 申请中 / 2 已开通 + is_enabled」模式,但**独立表独立字段**,不复用 `pos_store_newebpay`。字段:`store_id`(UK)、`omg_status`、`is_enabled`、`merchant_id`、`hash_key`、`hash_iv`、`enabled_payments`(历史字段保留;运行时固定 Credit)、`apply_time`、`approved_time`、`last_verify_result`、`remark` + 审计字段。
 
 ---
 
@@ -92,7 +92,6 @@ omg:
   return-url: https://<公网域名>/pay/omg/notify         # 服务端回调(须回 1|OK)
   order-result-url: https://<前端>/pay-result           # 客户端跳转(可选,即时方式)
   payment-info-url: https://<公网域名>/pay/omg/paymentInfo  # ATM/超商虚帐/缴费码回调
-  client-redirect-url: https://<前端>/payment-info       # ATM/超商客户端展示
 ```
 
 `InvoiceMark=N` 固定(发票走 ezPay,与支付解耦);`EncryptType=1` 固定。
@@ -101,7 +100,7 @@ omg:
 
 ## D9. payType 取值
 
-**Decision**:`PosOrder.payType` 新增 `"7"` = OMG 在线支付(蓝新 `"6"`,OMG `"7"`,独立值便于辨识与过渡);具体方式由 `pos_order_omg_payment.pay_type` 在回调后承载。不改 `pos_order` 表结构(代码常量体现)。
+**Decision**:`PosOrder.payType` 使用 `"2"` = OMG 在线支付;具体方式由 `pos_order_omg_payment.pay_type` 在回调后承载。不改 `pos_order` 表结构(代码常量体现)。
 
 ---
 
@@ -128,7 +127,7 @@ stage `payment-stage.funpoint.com.tw` + 测试商店凭证/测试卡(文档附
 
 ## D13. 代码表与枚举(reference 02–05,2026-07-29 补读)
 
-- **ChoosePayment**(请求,2026-08-13 调整):新订单固定 `Credit` 并传 `UnionPay=2`,只显示信用卡与 Apple Pay;其余方式仅保留历史回调兼容
+- **ChoosePayment**(请求,2026-08-13 调整):新订单固定 `Credit` 并传 `UnionPay=2`,只显示信用卡与 Apple Pay;不保留其他支付方式的历史兼容入口
 - **Apple Pay 归在 Credit 下**(非独立 ChoosePayment);回覆 PaymentType 为 `Credit_CreditCard`,**仅凭 PaymentType 无法区分信用卡与 Apple Pay**(本期不区分)。
 - **回覆 PaymentType**(回调,落 `pos_order_omg_payment.pay_type`):`Credit_CreditCard`、`BarcodeATM_CHINATRUST`(即时);`ATM_FIRST/CHINATRUST/UBOT/KGI`、`CVS_CVS/FAMILY/IBON/HILIFE`、`AFTEE_AFTEE`(延期,走 PaymentInfoURL 取号 → US3)。
 - **RtnCode**:`1`=成功,其余失败;完整表为**图片**且持续新增,须查「歐買尬廠商後台→系統開發管理→交易狀態代碼查詢」。实现:`==1` 成功核销/退款成功,非 1 失败并记 `rtn_code`+`rtn_msg`,**不硬编码错误码分支**。

+ 3 - 1
specs/016-omg-payment/return-to-app-flow.md

@@ -1,4 +1,6 @@
-# OMG 支付完成回跳 App 原生页 —— 方案对比与决策
+# OMG 支付完成回跳 App 原生页 —— 已废弃的方案研究
+
+> **废弃说明(2026-08-13)**:本文比较的是曾包含延期支付的旧方案,不是当前实现依据。当前仅支持信用卡和 Apple Pay,只保留 `/pay/omg/result`、`/pay/omg/back` 与服务端 `/pay/omg/notify`;不存在需要兼容的历史延期交易。当前契约以 `spec.md`、`plan.md`、`contracts/api.md` 为准。
 
 > 状态:已验证(2026-08-11,经并行研究工作流 + 对抗审视)
 > 范围:OMG(歐買尬/FunPoint) AIO 线上支付;客户端为 uni-app 原生 App;收银台跑在 App 内 `<web-view>`

+ 18 - 13
specs/016-omg-payment/spec.md

@@ -20,7 +20,7 @@
 - **本项目启用的支付方式**:仅信用卡和 Apple Pay。OMG 官方将 Apple Pay 包含在 `ChoosePayment=Credit` 内;同时传 `UnionPay=2` 隐藏银联,不使用 `ALL`。
   - ATM、CVS、BarcodeATM、AFTEE 和 LINE Pay 均不属于新支付订单范围。
 - **幕前流程**:商户组参 + 计算 `CheckMacValue` → POST 到 `AioCheckOut/V5` → 消费者在 OMG 收银台支付 → OMG **服务端 POST 回 `ReturnURL`**(商户须回 `1|OK`)→ 可选 `OrderResultURL` 客户端跳转(仅即时支付方式支持)。
-- **历史非即时支付兼容**:既有 ATM/超商回调入口保留用于接收变更前交易的迟到通知;新支付订单不再生成相关参数或入口。
+- **非即时支付已移出范围**:开发测试阶段没有需要兼容的历史订单;不提供 `PaymentInfoURL`、`ClientRedirectURL` 或延期支付返回接口。
 - **退款**:`05_refund` API;**订单查询**:`04_order_query`;**付款结果通知**:`03_payment_notify`。
 - **发票**:`InvoiceMark` 必须 `N` —— 电子发票仍走本项目 ezPay 链路(010/014),与支付解耦。
 - **约束**:不可用 iframe;iOS 下不可另开新窗口;`MerchantTradeNo` 全平台唯一;`ReturnURL` 与 `OrderResultURL` 不可相同。
@@ -81,19 +81,18 @@ OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,
 
 ---
 
-### User Story 3 - 非即时支付(ATM / 超商)(Priority: P3
+### User Story 3 - 非即时支付(已撤销
 
-消费者选择 ATM 转账或超商缴费时,平台展示 OMG 生成的虚帐/缴费码,消费者在期限内完成后由 OMG 回调核销
+当前仍在开发测试阶段,不保留 ATM、CVS、BarcodeATM、AFTEE 的创建、取号、返回或兼容处理
 
-**Why this priority**:扩大支付覆盖面,但非即时到账,可晚于信用卡 MVP
+**Why removed**:最终支付范围已经收敛为信用卡与 Apple Pay,且不存在生产历史订单
 
-**Independent Test**:选择 ATM/超商,能拿到虚帐/缴费码并展示;模拟付款完成回调能核销订单
+**Independent Test**:新建表单不含延期支付参数,应用也不暴露延期支付返回接口
 
 **Acceptance Scenarios**:
 
-1. **Given** 消费者选 ATM/超商,**When** OMG 生成虚帐/缴费码回调 `PaymentInfoURL`,**Then** 平台展示付款信息与期限。
-2. **Given** 消费者在期限内完成付款,**When** OMG 回调 `ReturnURL`,**Then** 订单核销为已支付并触发履约。
-3. **Given** 超过付款期限未付,**When** 逾期,**Then** 订单按规则失效/可重试。
+1. **Given** 新支付订单,**When** 平台生成 OMG 表单,**Then** 表单不得包含 `PaymentInfoURL`、`ClientRedirectURL` 或延期缴费期限参数。
+2. **Given** 请求旧延期支付返回路径,**When** 访问应用,**Then** 不得命中 OMG Controller。
 
 ---
 
@@ -109,6 +108,7 @@ OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,
 
 1. **Given** 一笔已支付订单可退款,**When** 发起取消,**Then** 调 OMG 退款 API 并将订单标记为退款中/已退款。
 2. **Given** 退款失败,**When** OMG 返回失败,**Then** 记录原因并可重试,不误标已退款。
+3. **Given** 订单已先取消且 OMG 成功付款回调或主动补单随后到达,**When** 系统记录真实付款事实,**Then** 不触发履约,并必须将该笔付款纳入自动退款或人工退款待办,且由持久化兜底机制保证进程中断后仍可继续处理。
 
 ---
 
@@ -133,7 +133,9 @@ OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,
 - 重复回调(网络重试)—— 按 `MerchantTradeNo` 幂等,不重复发货/推送。
 - 消费者支付页关闭/超时未付 —— 订单不误判,可重试或按规则失效。
 - 金额不一致(下单价 vs 支付价,如改了单/用了券)—— 以平台实际应收金额为准。
-- 回调与「用户取消」并发 —— 明确最终状态优先级,避免已付款却被取消。
+- 回调与「用户取消」并发 —— 先保留已付款资金事实且不触发履约;已取消订单的迟到付款必须进入可恢复的退款闭环,不能只写异常日志。
+- 支付尝试重复创建 —— 同一业务订单只允许一个当前有效 OMG 支付入口;重复点击必须复用或查询现有尝试,不能产生多个同时可付款的 `MerchantTradeNo`。
+- 支付尝试轮换 —— 只有 OMG 查询明确失败才能关闭原有效入口并创建新 `MerchantTradeNo`;查询超时、限流或结果不确定时必须保留原尝试。
 - 退款失败 / 部分退款 / 跨日退款。
 - 多门店凭证:A 门店凭证不可用于 B 门店订单。
 - 测试凭证误用到生产、或反之。
@@ -145,20 +147,23 @@ OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,
 - **FR-001**:系统 MUST 支持消费者对未支付订单发起 OMG 幕前在线支付,跳转 OMG 收银台(金额、订单号、商品描述正确)。
 - **FR-002**:系统 MUST 只向新支付订单提供信用卡和 Apple Pay,固定传 `ChoosePayment=Credit` 与 `UnionPay=2`;不得传 `ALL`,不得展示 ATM、CVS、BarcodeATM、AFTEE、银联或 LINE Pay。
 - **FR-003**:系统 MUST 使用 OMG `CheckMacValue`(SHA256,`EncryptType=1`)对请求签名、对回调验签;签名工具**独立于**现有 NewebPay 工具。
-- **FR-004**:系统 MUST 在 OMG `ReturnURL` 回调时验签 + 按 `MerchantTradeNo` 幂等更新订单为已支付,并回 `1|OK`。
+- **FR-004**:系统 MUST 在 OMG `ReturnURL` 回调时验签 + 按 `MerchantTradeNo` 幂等更新订单为已支付,并回 `1|OK`;订单核销的原子条件允许待接单 `state=0` 和堂食已自动接单 `state=1`,同时要求 `pay_status=0`,不得核销配送中、已完成或已取消订单
 - **FR-005**:系统 MUST 在支付成功后触发现有订单履约链路(状态流转 + iOS 推送 + `sendAcceptRiderPush`)。
-- **FR-006**:系统 MUST NOT 为新支付订单生成 ATM/超商取号或延期缴费参数;历史回调入口仅为变更前交易保留
-- **FR-007**:系统 MUST 支持订单取消时经 OMG 退款 API(`05_refund`)退款并同步状态(本期范围,D3)。
+- **FR-006**:系统 MUST NOT 生成 ATM/超商取号或延期缴费参数,也 MUST NOT 暴露延期支付专用返回接口
+- **FR-007**:系统 MUST 支持订单取消时经 OMG 退款 API(`05_refund`)退款并同步状态(本期范围,D3);若取消先于 OMG 成功回调或主动补单到达,系统 MUST 记录真实付款、不触发履约,并通过持久化扫描自动退款,不能依赖单次回调线程完成退款
 - **FR-008**:系统 MUST 支持门店级 OMG 凭证配置(MerchantID/HashKey/HashIV,启用开关,测试/生产切换)。
 - **FR-009**:本期功能覆盖范围**仅餐饮订单**的在线支付;旅游·机票订单(015)后续衔接,不在本期(D5)。
 - **FR-010**:系统 MUST 持久化支付流水(`MerchantTradeNo`、金额、方式、时间、回调原始报文、退款记录),并将脱敏后的后台回调内容写入共享 `ipn_log`,以 `type=omg` 区分支付渠道,支持排查与对账。
 - **FR-011**:系统 MUST 在 `InvoiceMark=N` 下与 ezPay 电子发票链路解耦,支付不干涉开票。
 - **FR-012**:OMG **替代蓝新 NewebPay(011)** 作为订单在线支付启用,NewebPay 不再启用(D2)。
 - **FR-013**:系统 MUST NOT 复用或依赖任何蓝新 NewebPay 代码/表;OMG 凭证、流水、退款表与工具类/Controller 全部独立新建,仅可使用平台共享基础设施(订单状态机、推送、订单日志)。
+- **FR-014**:系统 MUST 保证同一业务订单同时只有一个可向消费者展示并完成付款的 OMG 支付入口;重复点击必须复用该有效尝试,不得另建第二个可付款入口。
+- **FR-015**:系统 MUST NOT 保留 ATM、CVS、BarcodeATM、AFTEE 的历史订单兼容分支;当前测试数据可直接清理。
+- **FR-016**:系统 MUST 以公平、可恢复的方式调度 OMG `queryTrade`。未实际发出查询时不得消耗该订单的查询间隔;限流不得使固定排序后的后续订单长期饥饿。查询频率参数须标明其来源是官方契约或环境实测,不能把查询 `TimeStamp` 的 3 分钟有效期解释为支付尝试有效期。
 
 - **FR-017**:系统 MUST 自动补偿仍为 `CREATED` 的当前有效支付尝试。调度器按 `next_query_time` 取有限批次,并在真实查询前用条件更新预留该行;查询确认已付款或明确失败时必须复用主动查询的验签与同一状态机,未付款或暂时异常不得关闭支付尝试。
 
-- **FR-018**??? MUST ? `OrderResultURL`?`ClientRedirectURL`?`ClientBackURL` ??? `omgpay` ????????????????? HTML ??????? HTTPS Universal Link/App Link ?? App?`OrderResultURL` ? `ClientRedirectURL` ?????????????????????????? CheckMacValue ?????????????????????????App ??????? `/pay/omg/query` ???????
+- **FR-018**:系统 MUST 将新订单的 `OrderResultURL`、`ClientBackURL` 指向新 `omgpay` 后端入口,由后端返回禁止缓存的轻量 HTML 并通过已配置的 HTTPS Universal Link/App Link 唤醒 App。`OrderResultURL` 的全部实际回传字段(包括未知额外字段与空值)必须参加 CheckMacValue 验签;入口仅负责导航,不得直接改变付款状态。App 唤醒后必须调用 `/pay/omg/query` 确认最终状态。
 
 ### Key Entities *(include if feature involves data)*
 

+ 37 - 35
specs/016-omg-payment/tasks.md

@@ -55,13 +55,13 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 **Goal**: C 端用户下单后调发起接口拿到 form 参数,前端 Form Post 跳转 OMG 收银台(新订单仅信用卡与 Apple Pay)
 
-**Independent Test**: 对已开通 OMG 的门店下单 → 调 `/pay/omg/create` → 返回 `gatewayUrl` + 全参数(含 CheckMacValue) → 前端跳转 OMG 页出现支付方式;`pos_order_omg_payment` 落一行(pay_status=0)、`pos_order.pay_type="7"`
+**Independent Test**: 对已开通 OMG 的门店下单 → 调 `/pay/omg/create` → 返回 `gatewayUrl` + 全参数(含 CheckMacValue) → 前端跳转 OMG 页出现支付方式;`pos_order_omg_payment` 落一行(pay_status=0)、`pos_order.pay_type="2"`
 
 - [x] T012 [P] [US1] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/StoreOmgCredentialDto.java`(storeId/merchantId/hashKey/hashIv/enabledPayments,`@Valid`)与 `ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PosStoreOmgVo.java`(列表/详情 VO)
 - [x] T013 [US1] 实现 `ruoyi-system/.../service/IPosStoreOmgService.java` + `impl/PosStoreOmgServiceImpl.java`(纯 DB:`getOrCreateByStoreId`、`getEnabledConfig(storeId)` 返回已开通且启用的凭证或 null、`getEnabledByMerchantId(merchantId)` 回调按商店代号反查凭证),仿 PosStoreEzpayServiceImpl
 - [x] T014 [US1] 实现 `ruoyi-system/.../service/IPosOrderOmgPaymentService.java` + `impl/PosOrderOmgPaymentServiceImpl.java`:`createPayment(ddId,merchantTradeNo,storeId,merchantId,amount,choosePayment)`、`getByTradeNo`、`getByMerchantTradeNo`、`getLatestByDdId`、`markSuccess(...payType,rtnCode,rtnMsg,authCode,payTime,callbackRaw)`(幂等:tradeNo 已 pay_status=1 返回 0)、`markFail`、`markRefunded`
-- [x] T015 [US1] 在 `ruoyi-system/.../domain/PosOrder.java` 附近或新建常量类定义 `PAY_TYPE_OMG="7"`(OMG 在线支付),不改表结构
-- [x] T016 [US1] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java` 实现 `POST /pay/omg/create`(`@Anonymous`+`@Auth`,Header token):校验订单存在/未支付/属当前用户/金额>0 → `getEnabledConfig(mdId)`(无则 `error("该门店暂不支持线上支付")`)→ `MerchantTradeNo="OMG"+清洗(ddId)` → 组参(MerchantID/MerchantTradeNo/MerchantTradeDate=yyyy/MM/dd HH:mm:ss/PaymentType=aio/TotalAmount=amount/ReturnURL/ChoosePayment=ALL/EncryptType=1/ItemName/InvoiceMark=N)→ `OmgPay.createAioForm` 生成 CheckMacValue 与 form 字段 → `createPayment` 落流水(pay_status=0) → 更新 `pos_order.pay_type="7"`/`pay_url=gatewayUrl` → 返回 form 字段(见 contracts/api.md B1)
+- [x] T015 [US1] 在 `ruoyi-system/.../domain/PosOrder.java` 附近或新建常量类定义 `PAY_TYPE_OMG="2"`(OMG 在线支付),不改表结构
+- [x] T016 [US1] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java` 实现 `POST /pay/omg/create`(`@Anonymous`+`@Auth`,Header token):校验订单存在/未支付/属当前用户/金额>0 → `getEnabledConfig(mdId)`(无则 `error("该门店暂不支持线上支付")`)→ `MerchantTradeNo="OMG"+清洗(ddId)` → 组参(MerchantID/MerchantTradeNo/MerchantTradeDate/PaymentType=aio/TotalAmount/ReturnURL/ChoosePayment=Credit/UnionPay=2/EncryptType=1/ItemName/InvoiceMark=N)→ 生成 CheckMacValue 与 form 字段 → 落流水(pay_status=0) → 更新 `pos_order.pay_type="2"`/`pay_url=gatewayUrl` → 返回 form 字段(见 contracts/api.md B1)
 - [ ] T017 [US1] 前端 `E:\QtwCode\foodie\foodie-store\` 新增「OMG 支付」入口:下单后调 `/pay/omg/create`,用返回字段构建隐藏 form 自动 submit 到 `gatewayUrl`;支付相关文案用 `$t()` 四语 i18n(zh/tw/en/vi,key 加到对应对象层级,有意义命名)  ⏸️ 推迟:属客户侧流程(客户下单→Form Post 跳 OMG),foodie-store 是商家端无此入口,客户 uni-app 不在当前工作区,待其就位再做
 
 **Checkpoint**: 发起支付链路通,可跳转 OMG 测试收银台(此时回调未接,订单状态待 US2 更新)
@@ -77,7 +77,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 - [x] T018 [US2] 在 `OmgPayController.java` 实现 `POST /pay/omg/notify`(`@Anonymous`):collectForm 收参 → 将脱敏回调内容写入 IpnLog(`type=omg`)→ 由 `MerchantID` 调 `getEnabledByMerchantId` → 无凭证记录并返回
 - [x] T019 [US2] notify 验签:`OmgCheckMacValue.verify(全部回调参, hashKey, hashIv)`,与回传 `CheckMacValue` 不符 → 拒绝并记录(绝不变更订单),仍回 `1|OK`
 - [x] T020 [US2] notify 幂等+金额+状态校验:按 `TradeNo` 查 `pos_order_omg_payment`(已 pay_status=1 → 直接返回 `1|OK`);`TradeAmt==订单 amount`(不符→拒绝核销、告警);由 `MerchantTradeNo` 反查流水与订单(不存在→记录待查返回);判 `RtnCode==1 且 SimulatePaid!=1`(模拟支付 `SimulatePaid=1` → 不发货、记录)
-- [x] T021 [US2] notify 成功业务链路:`markSuccess`(pay_type=回覆 PaymentType 如 `Credit_CreditCard`、rtn_code、pay_time=PaymentDate、callback_raw)→ 更新 `pos_order`(state=0, payStatus=1) → `orderLogHelper.logSync` 记日志 → `PayPush`+`pushEventService.PublisherEvent` 推送用户/商家(参照 `PosOrderController.sendHdfkMessage` 货到付款同款 `OrderPushBodyDto.getJson`)→ **补全可接单骑手推送**:在 `OmgPayController` 内独立实现(复用 `PayPush.sendAcceptRiderPush` / 货到付款链路调用方式,**不复用 NewebPay 代码**,见 CLAUDE.md「sendAcceptRiderPush 迁移」)→ 回应**纯字符串 `1|OK`**(注意:非 JSON,与蓝新不同)
+- [x] T021 [US2] notify 成功业务链路:`markSuccess`(pay_type=回覆 PaymentType 如 `Credit_CreditCard`、rtn_code、pay_time=PaymentDate、callback_raw)→ 原子更新 `pos_order`(`state IN (0,1)`, `pay_status=0→1`) → `orderLogHelper.logSync` 记日志 → `PayPush`+`pushEventService.PublisherEvent` 推送用户/商家(参照 `PosOrderController.sendHdfkMessage` 货到付款同款 `OrderPushBodyDto.getJson`)→ **补全可接单骑手推送**:在 `OmgPayController` 内独立实现(复用 `PayPush.sendAcceptRiderPush` / 货到付款链路调用方式,**不复用 NewebPay 代码**,见 CLAUDE.md「sendAcceptRiderPush 迁移」)→ 回应**纯字符串 `1|OK`**(注意:非 JSON,与蓝新不同)
 - [x] T022 [US2] notify 失败处理(`RtnCode!=1`):`markFail` 写流水(pay_status=2)、订单保持未支付、记 `rtn_code`+`rtn_msg`(不硬编码错误码分支,见 contracts §C3)、回 `1|OK`
 - [x] T023 [US2] 在 `OmgPayController.java` 实现 `GET|POST /pay/omg/return`(`@Anonymous`):仅 302 重定向前端结果页(带 ddId),**不改订单状态**(以 notify 为准)
 - [ ] T024 [US2] 前端 `E:\QtwCode\foodie\foodie-store\` 支付结果页:展示支付状态、轮询订单 payStatus(ReturnURL 不改状态,以轮询/回调后状态为准);文案四语 i18n  ⏸️ 推迟:同 T017,支付结果页属客户侧
@@ -102,28 +102,26 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 ---
 
-## Phase 6: User Story 3 - ATM / 超商 非即时支付 (Priority: P3)
+## Phase 6: User Story 3 - 非即时支付(已撤销)
 
-**Goal**: `ChoosePayment=ALL` 下消费者选 ATM/超商时,平台展示 OMG 虚帐/缴费码,消费者期限内付款后回调核销
+**Goal**: 不实现 ATM、超商或其他非即时支付;当前全是开发测试数据,没有历史交易兼容范围。
 
-**Independent Test**: 选 ATM/超商 → OMG 回调 `/pay/omg/paymentInfo` 带虚帐/缴费码 → 平台展示 + 期限 → 消费者付款 → `/pay/omg/notify`(RtnCode=1) → 订单核销(quickstart 场景 D)
+**Independent Test**: 新支付表单不包含 `PaymentInfoURL`、`ClientRedirectURL` 或延期缴费期限参数,且控制器不暴露延期支付回调/返回接口。
 
-- [x] T030 [US3] 在 `OmgPayController.java` 实现 `POST /pay/omg/paymentInfo`(`@Anonymous`):`OmgCheckMacValue.verify` → 解析 BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate → 落 `pos_order_omg_payment.callback_raw`(并可用专用字段/`pay_url` 存取号码供前端读)→ 回 `1|OK`
-- [x] T031 [US3] 暴露取号信息给前端:`GET /pay/omg/paymentInfo/{orderid}`(或复用订单查询)返回虚帐/缴费码 + 期限;前端 `E:\QtwCode\foodie\foodie-store\` ATM/超商结果页展示 + 倒计时;文案四语 i18n
-- [x] T032 [US3] 延期付款最终核销:消费者实际付款后 OMG 回调 `/pay/omg/notify`(RtnCode=1)→ 复用 US2 的 `markSuccess` + 推送链路(pay_type=`ATM_*`/`CVS_*`)→ 订单已支付
+- [x] T030–T032 [US3] 原延期支付任务撤销;相关入口不进入新 `omgpay` 实现。
 
-**Checkpoint**: ATM/超商 全链路(取号→展示→付款→核销)
+**Checkpoint**: 当前范围只有信用卡和 Apple Pay。
 
 ---
 
 ## Phase 7: User Story 4 - 订单取消与退款 (Priority: P3)
 
-**Goal**: 订单取消时,信用卡订单调 OMG `DoAction` 退款并同步状态;ATM/超商记录待人工
+**Goal**: 订单取消时,信用卡/Apple Pay 订单调 OMG `DoAction` 退款并同步状态
 
 **Independent Test**: 对一笔 OMG 已支付信用卡订单发起取消 → 调 `DoAction`(Action=R) → `pos_order_omg_refund` 记录、`pos_order_omg_payment.pay_status=3`、`pos_order.pay_status=2`;失败→记 `rtn_msg` 可重试(quickstart 场景 E)
 
 - [x] T033 [US4] 实现 `ruoyi-system/.../service/IPosOrderOmgRefundService.java` + `impl`(`record(paymentId,ddId,tradeNo,action,amount,rtnCode,rtnMsg,callbackRaw)`、`listByPayment`、`listByDdId`)
-- [x] T034 [US4] 在 `OmgPayController.java` 实现退款(`POST /pay/omg/refund?orderid=` 或由取消链路调用):查 OMG 已支付订单 → 信用卡(pay_type=`Credit_CreditCard`) → `OmgPay.doAction(Action=R, TotalAmount=amount)` → 写 `pos_order_omg_refund` → `RtnCode==1` 则 `markRefunded`(pay_status=3) + `pos_order.pay_status=2`;`ATM_*/CVS_*/BarcodeATM_*` 无 API → 记录待人工处理
+- [x] T034 实现 `POST /pay/omg/refund`:查唯一有效的已支付尝试 → `DoAction(Action=R, TotalAmount=amount)` → 写退款流水;成功后同步支付尝试和订单退款状态。
 - [x] T035 [US4](可选/增强)状态化 Action:下单时 `NeedExtraPaidInfo=Y` 取 `gwsr`,退款前查 `CreditDetail/QueryTrade/V2`(需盘合后台 `CreditCheckCode`) 决定 `N(已授權)/E(要關帳)/R(已關帳)`;MVP 可先统一 `R`,失败再分支
 - [x] T036 [US4] 接入订单取消链路:`PosOrderShOprateController`(商家取消) / `UserOrderController`(用户取消) 取消 OMG 已支付订单时触发退款;相关提示文案四语 i18n
 
@@ -136,7 +134,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 **Purpose**: 多 story 共享的收尾与护栏
 
 - [ ] T037 [P] 四语 i18n 统一校验:foodie-store 与 foodie-admin-vue 的 zh.js/tw.js/en.js/vi.js 中 OMG 支付相关 key 四文件齐全、命名有意义、位于正确对象层级(用 i18n-consistency-checker 或人工核)
-- [x] T038 [P] 安全复核:`/pay/omg/notify`、`/pay/omg/paymentInfo`、`/pay/omg/return` 确为 `@Anonymous` 且仅这些匿名;HashKey/HashIV 不出现在任何前端响应/日志明文;CheckMacValue 验签为强制前置;金额校验与 `SimulatePaid` 守卫不可绕过
+- [x] T038 [P] 安全复核:`/pay/omg/notify`、`/pay/omg/result`、`/pay/omg/back` 为外部返回入口;HashKey/HashIV 不出现在前端响应或日志;CheckMacValue 验签、金额校验与 `SimulatePaid` 守卫不可绕过
 - [x] T039 [P] **零蓝新依赖复核**:grep `com.ruoyi.app.utils.omg` 及 `OmgPayController`/`PosStoreOmg*`/`PosOrderOmg*` 全部 OMG 新建类,确认无任何 `newebpay` import(必须为 0,违反硬约束)
 - [ ] T040 运行 `specs/016-omg-payment/quickstart.md` 全部场景 A–E 端到端验证(stage + 测试卡 + 内网穿透),记录结果(退款场景 E 因 stage 不可用需正式小额/mock)
 - [x] T041 文档一致性:确认 spec.md/plan.md/tasks.md 描述一致;`updatesql/sql.md` 中 SQL 齐全(三建表 + OMG 菜单权限)
@@ -155,7 +153,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 - [x] T042 [P] [US6] contracts/api.md:§B5 `POST /pay/omg/query` 由「可选/未实现」补全为正式契约(入参/鉴权/返回/TradeStatus 分支/幂等/自愈);新增 §B8 定时补单(@Scheduled+Redisson)说明
 - [x] T043 [US6] 抽取共享补单核销逻辑(`OmgPayController`):`applyPaidResult(...)`(notify 成功分支改调此,消除重复)+ `public reconcileByQuery(ddId, source)`(queryTrade→金额校验→applyPaidResult/markFail,含幂等与跨事务中断自愈),供 /query 与定时任务复用
-- [x] T044 [US6] 方案A:`OmgPayController` 新增 `POST /pay/omg/query`(@Anonymous @Auth,校验登录+订单本人+payType=7+未支付未取消)→ 调 `reconcileByQuery` → 返回 `{payStatus:0/1/2, reconciled}`;补单写 `orderLogHelper` 日志
+- [x] T044 [US6] 方案A:`OmgPayController` 新增 `POST /pay/omg/query`(@Anonymous @Auth,校验登录+订单本人+payType=2+未支付未取消)→ 调 `reconcileByQuery` → 返回 `{payStatus:0/1/2, reconciled}`;补单写 `orderLogHelper` 日志
 - [x] T045 [US6] 方案B 数据层:`PosOrderOmgPaymentMapper` 加 `selectLeakOrderDdIds(windowStart, graceCutoff, batchSize)`(每订单取最新一笔 `pay_status=0`,INNER JOIN pos_order 排除 state=4)+ XML;`IPosOrderOmgPaymentService` 加 `scanLeakOrders(...)`
 - [x] T046 [US6] 方案B 任务:新建 `ruoyi-admin/.../app/task/OmgReconcileTask.java`(@Scheduled fixedDelay 默认3分钟 + Redisson 分布式锁 `lock:omg:reconcile`,多实例单节点执行)→ 扫漏单逐笔调 `reconcileByQuery(ddId,"scheduled")`;`RuoYiApplication` 加 `@EnableScheduling`
 - [x] T047 [P] [US6] 配置:`application.yml` 加 `omg.reconcile.*`(fixed-delay-ms 默认180000 / window-hours 默认168=7天覆盖延期 ExpireDate / grace-minutes 默认2 / batch-size / lock-wait-seconds / lock-lease-seconds)
@@ -176,7 +174,7 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 **Prerequisites**: ✅ bugB 验签已修(`1a424c3` CASE_INSENSITIVE_ORDER,queryTrade 可靠);✅ parseKvResponse 尾随& 放宽(`7a8e248`);⏳ collation 3 表 CONVERT 待执行(sql.md 2026-08-12 节)。
 
-**Independent Test**: 同订单连点 create 9 次 → 只 1 条 is_active=1 活跃行(不堆积);ATM 取号后过期重新 create → 新行 + 旧行 is_active=0(虚帐仍可付,迟到 notify 落旧行 markSuccess);id=26 已付历史行迁移后 reconcile 补单核销;同门店多单未付 reconcile 不触发 OMG 403
+**Independent Test**: 同订单重复 create 只保留并复用一条有效支付尝试;查询、回调和自动补偿均定位该尝试;同门店多订单查询不触发 OMG 限流
 
 - [x] T051 [P] **前置:parseKvResponse 重复键放宽**(`ruoyi-admin/.../utils/omg/OmgPay.java`):重复键 last-wins 不抛(用同一份 Map 重算 CheckMacValue 验签,避 kill-shot ⑥ 解析放宽与验签打架);仅整段无 `=` 或缺 CheckMacValue 才抛;`OmgPayTest` 补重复键用例。S 依赖 queryTrade 做换号/补单安全网,重复键仍抛会让安全网失效。
 - [x] T052 **collation 对齐执行确认**:执行 `updatesql/sql.md` 2026-08-12 节(pos_order_omg_payment/pos_order_omg_refund/pos_store_omg CONVERT 到 utf8mb4_unicode_ci);执行后 `selectLeakOrderDdIds` JOIN 不再报 Illegal mix of collations。
@@ -189,11 +187,16 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 - [x] T059 **读侧 getLatestByDdId 迁移** ✅(OmgPayController 4 处 db043b4 + OrderLifecycleService 3 处 b9a0514;buildContext canReconcileOmg 重构为 hasOmgActivity):`getPaymentInfo`/`refundOrderOutcome`/L569 早退/`markRefunding` 并发/`confirmManualRefundOutcome`/`reconcileByQuery`(T054)/`OrderLifecycleService.buildContext`/`validateOmgReconcile`/`validateOmgRefund` → `selectLatestRefundableByDdId`(pay_status IN(1,3,4))或 `listPaidByDdId`;修"订单未支付"早退为 listPaidByDdId 为空才 FAILED。
 - [x] T060 **砍掉** ~~退款遍历~~(2026-08-12 用户拍板:双付不现实——用户不会对同一订单付两笔,实测 bugA 9 行也只 1 笔 paid。退款维持退单条已付行即可,无需遍历多笔)(`refundOrderOutcome`):改 `listPaidByDdId` 遍历所有 pay_status=1 且 trade_no 非空行;每行先查 `pos_order_omg_refund` 是否 action=R 且 rtnCode=1 → 跳过;否则 `markRefundingIfPaid`(1→4 CAS)→ `DoAction(Action=R)` 逐笔退(每笔独立 tradeNo);ATM/超商无退款 API 走 manual pending;孤立已付(双付第二笔)写 `ipn_log(type=omg_orphan_paid)` + 运营告警。
 - [x] T061 **砍掉** ~~历史 9 行迁移~~(2026-08-12 用户:不管历史数据。新读路径对历史行仍兼容——可退款行优先)(orders 如 991786433092835 id=18~26):迁移前对每行 queryTrade 确认(已可靠);TradeStatus=1 的行无论本地 pay_status 一律保留并回填 pay_status=1(如 id=26 OMGFE0494FB7AD1474A8);仅 TradeStatus=10200095 或(0 且过 ExpireDate 且非信用卡)才允许清理;优先 is_active 软标记而非物理删除(lifecycle §9 Q10)。
-- [ ] T062 **端到端验证**(stage):create 连点复用(单活跃 is_active=1)、reconcile 两段式补单(含 id=26)、过期延期支付轮换、两道节流闸不触发 403;抓 stage 真实 QueryTradeInfo 大响应(ATM/超商 NeedExtraPaidInfo=Y)fixture 补 OmgPayTest(重复键验证)。(T060 退款遍历/T061 历史迁移已砍,不纳入验证)
+- [ ] T062 **端到端验证**(stage):create 重复点击复用单一有效尝试、query/notify/自动补偿完成核销、两道节流闸不触发 403;历史数据与延期支付不纳入验证。
+- [x] T063 **统一 OMG 支付类型为 2**:`OmgPayController`、`OrderLifecycleService`、订单创建逻辑、实体/DTO 注释、016 规格与测试夹具统一使用 `payType=2`;`paymentMethod=2` 不再写成货到付款 `collectPayment=1`;增加常量一致性回归测试。不迁移历史数据,不处理其他 OMG 评审问题。
+- [x] T064 **修复堂食订单支付核销**:`OmgPayController.handlePaymentSuccess` 的订单 CAS 从仅允许 `state=0` 改为允许 `state IN (0,1)`,并继续要求 `pay_status=0`;回调和主动补单复用同一路径,配送中、已完成、已取消状态不核销;增加 CAS 条件回归测试。
+- [x] T065 取消后迟到付款由自动补偿扫描并复用信用卡退款服务,保持幂等且不触发履约。
+- [x] T066 支付尝试生命周期收敛为单一有效尝试;开发测试阶段不迁移历史数据、不兼容延期支付。
+- [ ] T067 **P1 修正 OMG 查询节流公平性(随 T066 设计)**:只有实际准备发送 QueryTradeInfo 时才同时获取 ddId 与 MerchantID 额度,任一额度失败不得留下无效 ddId 占位;扫描不得长期固定由相同首批订单抢占门店 token,应增加公平轮转/`next_query_at`;将 3 秒 MerchantID 限制标注为实测配置而非当前公开官方契约,并补多订单同门店无饥饿测试。
 
 **Checkpoint**: 问题 A 收敛(连点单活跃 T058)、问题 B 闭环(reconcile 全量补单 T054 + 两道频率节流闸 T055)、追加式 S 落地(is_active 双轴 + mapper 杜绝换号 + 读侧迁移 T059)。**T060 退款遍历砍掉**(双付不现实,退款维持退单条已付行)、**T061 历史 9 行迁移砍掉**(不管历史数据)。T062 stage 端到端待测试环境。
 
-**§9 待拍板(实现/stage 确认,不阻塞)**:freshMin(建议 3min,stage 实测同 MTN 重 POST)、生产 MySQL 版本(决定生成列唯一索引)、reuse 保留原 MerchantTradeDate、节流精确参数、AFTEE 窗口(现 168h 不够,建议 720h 或按 expire_date 动态)、/query 轮询 cadence(配合前端 30-60s)。
+**待 stage 确认(不阻塞)**:同 MTN 重复 POST、生产 MySQL 版本、节流精确参数、`/query` 轮询频率
 
 ---
 
@@ -209,28 +212,28 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 ---
 
-## Phase 14: OMG ?????? App ???Priority: P1?
+## Phase 14: OMG 后端返回页与 App 唤醒(Priority: P1)
 
-- [x] T081 ?????? `OrderResultURL`?`ClientRedirectURL`?`ClientBackURL`?????? App Link ??? `omgpay` ????? HTTPS/path ?????
-- [x] T082 ?? Client POST ????????????? DTO?????????????? CheckMacValue???????????
-- [x] T083 ?? `/pay/omg/result`?`/pay/omg/payment-info-result`?`/pay/omg/back` ???? no-store/CSP HTML ????????????????
-- [x] T084 ??? Client POST ????????? `ipn_log` ???????????HashKey/HashIV ?????????
-- [x] T085 ?????????????????????????????HTML ?????????????
-- [ ] T086 ?? `OMG_APP_RETURN_URL` ??? iOS Universal Link / Android App Link ? App ????App ????? `/pay/omg/query`??? App ??? bundle/package ?????????????????
-- [ ] T087 ?? OMG ??????????? Maven ???????? stage ???????????????
+- [x] T081 创建表单加入 `OrderResultURL`、`ClientBackURL`,两个地址和 App Link 均通过 `omgpay` 配置并执行 HTTPS/path 安全校验。
+- [x] T082 新建 Client POST 验签服务,复用完整原始表单 DTO;未知额外字段与空值全部参与 CheckMacValue,验签页不写订单状态。
+- [x] T083 新建 `/pay/omg/result`、`/pay/omg/back` 控制器和 no-store/CSP HTML 渲染器,支持自动唤醒与手动按钮。
+- [x] T084 将支付结果 Client POST 的完整内容写入现有 `ipn_log` 并记录可复制调试日志,HashKey/HashIV 不进入日志或页面。
+- [x] T085 增加表单字段、配置安全、完整验签、额外字段篡改、两个路由、HTML 头和原始请求缓存测试源码。
+- [ ] T086 配置 `OMG_APP_RETURN_URL` 并完成 iOS Universal Link / Android App Link 的 App 端关联;App 打开后调用 `/pay/omg/query`。当前 App 工程与 bundle/package 信息不在本后端工作区,不猜测其值。
+- [ ] T087 全部 OMG 功能调整结束后统一运行 Maven 编译、定向测试和 stage 联调;本批按用户要求暂不执行。
 
-**Checkpoint**??????????????????? `OMG_APP_RETURN_URL`???????????????????
+**Checkpoint**:后端返回页已实现;部署前必须配置真实 `OMG_APP_RETURN_URL`,否则安全页只提示尚未配置,不会跳转。
 
 ---
 
 ## Phase 15: 支付方式收敛为信用卡与 Apple Pay(Priority: P0)
 
 - [x] T088 将新支付表单固定为 `ChoosePayment=Credit`、`UnionPay=2`,并保证两个字段都参加 CheckMacValue 计算。
-- [x] T089 从新支付表单移除 `ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`、`ClientRedirectURL` 及对应无用配置;保留历史延期支付回调路由处理变更前交易
-- [x] T090 更新表单契约测试源码,精确断言只生成信用卡/Apple Pay 所需字段且不生成延期支付字段
+- [x] T089 从新支付表单移除 `ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`、`ClientRedirectURL` 及对应无用配置;删除延期支付客户端返回路由,不保留历史兼容
+- [x] T090 更新表单与返回路由测试源码,精确断言只生成信用卡/Apple Pay 所需字段且不暴露延期支付返回接口
 - [ ] T091 全部 OMG 功能调整结束后统一运行 Maven 编译、定向测试和 stage 收银台验证;本批按用户要求暂不执行。
 
-**Checkpoint**:代码层已限制订单只进入信用卡(含 Apple Pay)收银台,并隐藏银联;历史延期交易兼容入口未删除
+**Checkpoint**:代码层已限制订单只进入信用卡(含 Apple Pay)收银台并隐藏银联;开发测试阶段不保留延期支付兼容入口
 
 ---
 
@@ -242,14 +245,14 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 - **Phase 3 US1**: 依赖 Phase 2
 - **Phase 4 US2**: 依赖 US1(回调处理依赖发起产生的流水与订单关联)
 - **Phase 5 US5(凭证管理)**: 依赖 Phase 2(凭证表/Mapper);**可与 US1/US2 并行**(US1 测试可临时手插凭证数据)
-- **Phase 6 US3(ATM/超商)**: 依赖 US1+US2(复用发起+回调主链路,增量在取号回调与展示)
+- **Phase 6 US3(非即时支付)**: 已撤销,不进入当前交付范围
 - **Phase 7 US4(退款)**: 依赖 US1+US2(依赖已支付流水与 trade_no)
 - **Phase 8 Polish**: 依赖所有欲交付的 story 完成
 
 ### User Story 独立性
 - **US1**: 依赖 Foundational,无跨 story 依赖
 - **US2**: 依赖 US1(闭环下半段)
-- **US3**: 依赖 US1+US2,增量在 PaymentInfoURL 取号与展示
+- **US3**: 已撤销
 - **US4**: 依赖 US1+US2
 - **US5**: 依赖 Foundational,可与 US1/US2 并行
 
@@ -288,9 +291,8 @@ Task: "OmgPay HTTP 客户端骨架 in ruoyi-admin/.../utils/omg/OmgPay.java"
 ### 增量交付
 1. MVP(US1+US2)→ 验证 → 可上测试环境
 2. + US5(凭证管理)→ 去除手插数据、运营自助
-3. + US3(ATM/超商)→ 覆盖延期方式
-4. + US4(退款)→ 售后资金闭环
-5. Polish(i18n/安全/零蓝新复核/全场景验证)→ 可转正式
+3. + US4(退款)→ 售后资金闭环
+4. Polish(i18n/安全/零蓝新复核/全场景验证)→ 可转正式
 
 ### 并行策略
 - Foundational 完成后:开发者 A 做 US1+US2(主链路),开发者 B 做 US5(凭证管理前后端),互不阻塞