فهرست منبع

修复:OMG 支付回跳优先使用 uni-app Bridge

qmj 2 هفته پیش
والد
کامیت
2f34568892

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

@@ -62,7 +62,7 @@ public class OmgPaymentReturnController {
                 .header("Referrer-Policy", "no-referrer")
                 .header("X-Content-Type-Options", "nosniff")
                 .header("Content-Security-Policy",
-                        "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'")
+                        "default-src 'none'; style-src 'unsafe-inline'; script-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'none'")
                 .body(page.html());
     }
 }

+ 40 - 7
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentReturnPageRenderer.java

@@ -2,12 +2,14 @@ package com.ruoyi.app.omgpay;
 
 import org.springframework.stereotype.Component;
 
+import java.net.URI;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 
 /** Renders a fixed, escaped page that opens the configured App route and keeps a manual fallback button. */
 @Component
 public class OmgPaymentReturnPageRenderer {
+    private static final String UNI_WEB_VIEW_BRIDGE = "/static/omg/uni.webview.1.5.8.js";
     private final OmgPaymentProperties properties;
 
     public OmgPaymentReturnPageRenderer(OmgPaymentProperties properties) {
@@ -19,9 +21,13 @@ public class OmgPaymentReturnPageRenderer {
         if (appLink == null) {
             return error(503, "App 返回網址尚未設定");
         }
+        String appRoute = buildAppRoute(appLink);
         String escapedLink = escapeHtml(appLink);
+        String escapedRoute = escapeHtml(appRoute);
         String html = page("正在返回 App", "請稍候,App 開啟後會確認最終付款狀態。",
-                "<a id=\"open-app\" href=\"" + escapedLink + "\">返回 App</a>"
+                "<a id=\"open-app\" href=\"" + escapedLink + "\" data-app-route=\""
+                        + escapedRoute + "\">返回 App</a>"
+                        + "<script src=\"" + UNI_WEB_VIEW_BRIDGE + "\"></script>"
                         + diagnosticScript());
         return new OmgPaymentReturnPage(200, html);
     }
@@ -39,6 +45,11 @@ public class OmgPaymentReturnPageRenderer {
         return base + "?ddId=" + encode(orderId);
     }
 
+    private static String buildAppRoute(String appLink) {
+        URI uri = URI.create(appLink);
+        return "/" + uri.getHost() + uri.getPath() + "?" + uri.getRawQuery();
+    }
+
     private static String page(String title, String message, String action) {
         return "<!doctype html><html lang=\"zh-Hant-TW\"><head><meta charset=\"utf-8\">"
                 + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
@@ -52,16 +63,19 @@ public class OmgPaymentReturnPageRenderer {
 
     private static String diagnosticScript() {
         return "<script>(function(){'use strict';"
-                + "var PREFIX='[OMG-RETURN]',startedAt=Date.now(),link=document.getElementById('open-app'),target=link.href;"
+                + "var PREFIX='[OMG-RETURN]',startedAt=Date.now(),link=document.getElementById('open-app'),"
+                + "target=link.href,appRoute=link.getAttribute('data-app-route'),handoffObserved=false,bridgeStarted=false,schemeStarted=false;"
                 + "function targetSummary(value){try{var parsed=new URL(value);return {protocol:parsed.protocol,host:parsed.host,pathname:parsed.pathname};}"
                 + "catch(error){return {parseError:true};}}"
                 + "function orderRef(value){try{var id=new URL(value).searchParams.get('ddId')||'';return id?'***'+id.slice(-4):'';}"
                 + "catch(error){return '';}}"
+                + "function routePath(value){return String(value||'').split('?')[0];}"
                 + "function snapshot(details){var data={elapsedMs:Date.now()-startedAt,readyState:document.readyState,"
                 + "visibilityState:document.visibilityState,hidden:document.hidden,hasFocus:document.hasFocus()};"
                 + "if(details){Object.keys(details).forEach(function(key){data[key]=details[key];});}return data;}"
-                + "function log(event,details){console.info(PREFIX,event,snapshot(details));}"
-                + "function lifecycle(event){log(event.type,{persisted:event.persisted===true});}"
+                + "function log(event,details){console.info(PREFIX+' '+event+' '+JSON.stringify(snapshot(details)));}"
+                + "function lifecycle(event){if(event.type==='pagehide'||event.type==='beforeunload'||document.hidden){handoffObserved=true;}"
+                + "log(event.type,{persisted:event.persisted===true});}"
                 + "document.addEventListener('visibilitychange',function(event){lifecycle(event);});"
                 + "window.addEventListener('pagehide',lifecycle);window.addEventListener('pageshow',lifecycle);"
                 + "window.addEventListener('blur',lifecycle);window.addEventListener('focus',lifecycle);"
@@ -69,16 +83,35 @@ public class OmgPaymentReturnPageRenderer {
                 + "window.addEventListener('error',function(event){log('js_error',{line:event.lineno||0,column:event.colno||0});});"
                 + "window.addEventListener('unhandledrejection',function(event){var reason=event.reason;"
                 + "log('unhandledrejection',{reasonType:typeof reason,reasonName:reason&&reason.name?String(reason.name):''});});"
+                + "function bridgeAvailable(){return !!(window.uni&&uni.webView&&uni.webView.getEnv&&uni.webView.redirectTo);}"
+                + "function schemeFallback(reason){if(handoffObserved||schemeStarted){return;}schemeStarted=true;"
+                + "log('scheme_fallback_start',{reason:reason,target:targetSummary(target),orderRef:orderRef(target)});"
+                + "try{window.location.replace(target);}catch(error){log('scheme_fallback_exception',{name:error&&error.name?String(error.name):'Error'});}}"
+                + "function bridgeRedirect(trigger,force){if(handoffObserved||(bridgeStarted&&!force)||!bridgeAvailable()){return false;}"
+                + "bridgeStarted=true;var envResolved=false;var envTimer=setTimeout(function(){if(!envResolved){"
+                + "log('bridge_environment_timeout',{trigger:trigger});schemeFallback('bridge_environment_timeout');}},500);"
+                + "try{uni.webView.getEnv(function(env){envResolved=true;clearTimeout(envTimer);env=env||{};"
+                + "log('bridge_environment',{trigger:trigger,plus:env.plus===true,nvue:env.nvue===true,uvue:env.uvue===true,h5:env.h5===true});"
+                + "if(!(env.plus||env.nvue||env.uvue)){schemeFallback('bridge_not_app');return;}"
+                + "log('bridge_redirect_start',{trigger:trigger,routePath:routePath(appRoute),orderRef:orderRef(target)});"
+                + "try{uni.webView.redirectTo({url:appRoute});setTimeout(function(){if(!handoffObserved){"
+                + "schemeFallback('bridge_no_handoff');}},1200);}catch(error){log('bridge_redirect_exception',"
+                + "{name:error&&error.name?String(error.name):'Error'});schemeFallback('bridge_exception');}});}"
+                + "catch(error){envResolved=true;clearTimeout(envTimer);log('bridge_environment_exception',"
+                + "{name:error&&error.name?String(error.name):'Error'});schemeFallback('bridge_environment_exception');}return true;}"
                 + "link.addEventListener('touchstart',function(event){log('manual_open_touch',{isTrusted:event.isTrusted===true,"
                 + "target:targetSummary(target),orderRef:orderRef(target)});},{passive:true});"
                 + "link.addEventListener('click',function(event){log('manual_open_click',{isTrusted:event.isTrusted===true,"
-                + "target:targetSummary(target),orderRef:orderRef(target)});});"
+                + "target:targetSummary(target),orderRef:orderRef(target)});if(bridgeAvailable()){event.preventDefault();"
+                + "schemeStarted=false;bridgeRedirect('manual',true);}});"
+                + "document.addEventListener('UniAppJSBridgeReady',function(){log('bridge_ready',{});bridgeRedirect('ready',false);});"
                 + "log('page_loaded',{userAgent:navigator.userAgent,platform:navigator.platform||'',language:navigator.language||'',"
                 + "page:{origin:window.location.origin,pathname:window.location.pathname},target:targetSummary(target),orderRef:orderRef(target)});"
                 + "[250,1000,3000].forEach(function(delay){setTimeout(function(){log('auto_open_observation',{delayMs:delay,"
                 + "stillVisible:!document.hidden,target:targetSummary(target),orderRef:orderRef(target)});},delay);});"
-                + "log('auto_open_start',{target:targetSummary(target),orderRef:orderRef(target)});"
-                + "try{window.location.replace(target);}catch(error){log('auto_open_exception',{name:error&&error.name?String(error.name):'Error'});}"
+                + "log('auto_open_start',{target:targetSummary(target),routePath:routePath(appRoute),orderRef:orderRef(target)});"
+                + "if(window.UniAppJSBridge){setTimeout(function(){bridgeRedirect('already_ready',false);},0);}"
+                + "setTimeout(function(){if(!bridgeStarted){schemeFallback('bridge_ready_timeout');}},1000);"
                 + "})();</script>";
     }
 

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
ruoyi-admin/src/main/resources/static/omg/uni.webview.1.5.8.js


+ 22 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentClientReturnServiceTest.java

@@ -88,12 +88,34 @@ class OmgPaymentClientReturnServiceTest {
                 () -> assertTrue(page.html().contains("unhandledrejection")),
                 () -> assertTrue(page.html().contains("targetSummary")),
                 () -> assertTrue(page.html().contains("orderRef")),
+                () -> assertTrue(page.html().contains("JSON.stringify(snapshot(details))")),
                 () -> assertFalse(page.html().contains("rawForm")),
                 () -> assertFalse(page.html().contains(HASH_KEY)),
                 () -> assertFalse(page.html().contains(HASH_IV)),
                 () -> assertFalse(page.html().contains("CheckMacValue")));
     }
 
+    @Test
+    void verifiedPaymentResultUsesUniAppBridgeBeforeKeepingSchemeFallback() {
+        OmgPaymentReturnPage page = service.paymentResult(signedRequest(paymentFields()));
+
+        assertEquals(200, page.statusCode());
+        assertAll(
+                () -> assertTrue(page.html().contains(
+                        "<script src=\"/static/omg/uni.webview.1.5.8.js\"></script>")),
+                () -> assertTrue(page.html().contains(
+                        "data-app-route=\"/pages/OrderList/paySuccess/paySuccess?ddId=DD-1%26next%3Devil\"")),
+                () -> assertTrue(page.html().contains("UniAppJSBridgeReady")),
+                () -> assertTrue(page.html().contains("uni.webView.getEnv")),
+                () -> assertTrue(page.html().contains("uni.webView.redirectTo")),
+                () -> assertTrue(page.html().contains("bridge_redirect_start")),
+                () -> assertTrue(page.html().contains("scheme_fallback_start")),
+                () -> assertTrue(page.html().contains("routePath:routePath(appRoute)")),
+                () -> assertFalse(page.html().contains("route:appRoute")),
+                () -> assertTrue(page.html().contains(
+                        "com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId=DD-1%26next%3Devil")));
+    }
+
     @Test
     void tamperingAnyExtraFieldRejectsTheWholeClientResult() {
         LinkedHashMap<String, String> fields = paymentFields();

+ 6 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentReturnControllerTest.java

@@ -39,6 +39,12 @@ class OmgPaymentReturnControllerTest {
         assertEquals("no-store", response.getHeaders().getFirst("Cache-Control"));
         assertEquals("no-referrer", response.getHeaders().getFirst("Referrer-Policy"));
         assertTrue(response.getHeaders().getFirst("Content-Security-Policy").contains("default-src 'none'"));
+        assertTrue(response.getHeaders().getFirst("Content-Security-Policy").contains("script-src 'self' 'unsafe-inline'"));
         org.mockito.Mockito.verify(audit).append("127.0.0.1", "raw");
     }
+
+    @Test
+    void packagesTheOfficialUniAppWebViewBridgeLocally() {
+        assertNotNull(getClass().getResource("/static/omg/uni.webview.1.5.8.js"));
+    }
 }

+ 1 - 1
specs/016-omg-payment/contracts/api.md

@@ -56,7 +56,7 @@
 ### B3. 支付完成返回页 — `POST /pay/omg/result`(US2,@Anonymous)
 
 - 作为 `OrderResultURL` 接收 OMG Client POST,使用完整 DTO 严格解析并按支付尝试凭证快照验签;未知额外字段和空值全部进入 CheckMacValue。
-- 验签通过后只返回 no-store HTML,固定跳转 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`;不修改订单状态,App 必须再调 `/pay/omg/query`。
+- 验签通过后只返回 no-store HTML:加载后端自托管的 `uni.webview.1.5.8.js`,在 `UniAppJSBridgeReady` 后优先调用 `uni.webView.redirectTo({ url: '/pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}' })`;Bridge 不可用、非 App 环境或未完成页面交接时,回退到 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`,手动按钮保留同一 Scheme。该入口不修改订单状态,App 必须再调 `/pay/omg/query`。
 
 ### B5. 订单查询/被动补单 — `POST /pay/omg/query`(US6 方案A,@Anonymous @Auth,Header token)
 

+ 17 - 7
specs/016-omg-payment/frontend-integration.md

@@ -269,7 +269,15 @@ export default {
 
 ### 4.3 支付结果页 `pages/pay/omgResult`
 
-后端 `/pay/omg/result` 返回桥接页并打开 `pages/OrderList/paySuccess/paySuccess`。App 结果页取得 `ddId` 后查询后端确认支付状态。
+后端 `/pay/omg/result` 返回桥接页并打开 `pages/OrderList/paySuccess/paySuccess`。桥接页已由后端自托管 `uni.webview.1.5.8.js`:在 App 的 `<web-view>` 内优先等待 `UniAppJSBridgeReady` 并调用 `uni.webView.redirectTo`,Bridge 不可用或未完成页面交接时再使用固定 App Scheme。Android、iOS 共用这套返回页逻辑,外部浏览器仍可通过 Scheme 返回。
+
+App 前端不需要加载 Bridge SDK,也不需要在支付页增加按钮;只需确保目标页面已在 `pages.json` 注册且不是 `tabBar` 页面:
+
+```text
+/pages/OrderList/paySuccess/paySuccess?ddId=<订单号>
+```
+
+Bridge 成功属于 uni-app 内部路由,不会触发 `plus.runtime.arguments` 或 `newintent`;只有进入 Scheme 兜底时才会出现这些 App 唤起事件。
 
 ```js
 // 支付成功页面加载后,从链接参数取得 ddId 并开始查询。
@@ -299,7 +307,7 @@ const poll = (ddId) => {
 
 ### 4.4 iOS / uni-app App 唤起全链路诊断日志
 
-后端中转页会在浏览器控制台输出统一前缀 `[OMG-RETURN]`,覆盖页面加载、自动唤起、手动触摸/点击、页面可见性和脚本异常。App 端需要按下面代码输出 `[OMG-APP]`,两组日志必须在同一次支付中一起收集。
+后端中转页会在浏览器控制台输出统一前缀 `[OMG-RETURN]`,覆盖页面加载、Bridge 就绪与环境、Bridge 跳转、Scheme 兜底、手动触摸/点击、页面可见性和脚本异常。日志对象已序列化为 JSON,不再只显示 `[object Object]`。App 端需要按下面代码输出 `[OMG-APP]`,两组日志在同一次支付中一起收集。
 
 所有日志只保留 Scheme 的协议/主机/路径、参数名和订单号后四位,不打印 token、CheckMacValue、OMG 表单或完整订单号。
 
@@ -478,8 +486,8 @@ export default {
 
 1. 使用 HBuilderX 运行 iOS 真机,先清空控制台并过滤 `OMG-APP`。
 2. iPhone 打开“设置 → Safari → 高级 → Web 检查器”;Mac Safari 在“开发”菜单选择该设备和支付子 WebView,过滤 `OMG-RETURN`。
-3. 从 App 发起一笔 OMG 信用卡支付,支付完成后等待中转页停留至少 3 秒,再点击一次“返回 App”。
-4. 导出从 `[OMG-RETURN] page_loaded` 到最后一条 `auto_open_observation` 的浏览器日志,以及同一时段全部 `[OMG-APP]` 日志。
+3. 从 App 发起一笔 OMG 信用卡支付。正常情况会在 `bridge_redirect_start` 后直接进入结果页;若中转页停留超过 3 秒,再点击一次“返回 App”。
+4. 导出从 `[OMG-RETURN] page_loaded` 到 `bridge_redirect_start` 或 `scheme_fallback_start` 后最后一条生命周期日志,以及同一时段全部 `[OMG-APP]` 日志。
 5. 日志不得包含 token、CheckMacValue、HashKey、HashIV、完整订单号或银行卡资料;发现后先删除敏感内容再传给后端排查。
 
 #### 4.4.4 日志判读
@@ -487,7 +495,9 @@ export default {
 | 日志表现 | 失败边界 |
 |----------|----------|
 | 没有 `[OMG-RETURN] page_loaded` | OMG 没有进入 `/pay/omg/result`,或 Safari 检查器选错 WebView |
-| 有 `page_loaded`、`auto_open_start`,3 秒后 `stillVisible=true`,App 没有 `onHide/newintent/onShow_runtime` | 自定义 Scheme 被支付子 WebView 拒绝或未交给 iOS |
+| 有 `bridge_environment` 且 `plus=true`,随后出现 `bridge_redirect_start` 并进入结果页 | App 内 Bridge 回跳成功;不会出现 `newintent`,属于正常行为 |
+| 出现 `bridge_environment` 但 `plus/nvue/uvue` 均为 `false` | 当前页面不在 uni-app App Bridge 环境,后端会进入 Scheme 兜底 |
+| 出现 `scheme_fallback_start`,外部浏览器可打开同一 Scheme,但 App 内页面仍停留 | 当前子 WebView 未把同 App Scheme 交给 iOS;继续检查 Bridge SDK 是否返回 200 及 `bridge_environment` 日志 |
 | 中转页出现 `visibilitychange/pagehide`,但 App 没有任何新日志 | iOS 没有找到可处理该 Scheme 的安装包,重点检查 iOS `urltypes` 与重新打包结果 |
 | App 出现 `newintent/onShow_runtime` 且 `argument.present=true`,但路由没变化 | App 已收到 Scheme,路由解析或 `navigateTo/redirectTo` 逻辑有问题 |
 | App 已进入 `pages/OrderList/paySuccess/paySuccess`,但结果不更新 | 回跳已成功,继续检查 `/pay/omg/query` 请求与返回 |
@@ -520,10 +530,10 @@ export default {
 | 配置项 | 含义 | 需要的值(示例) |
 |--------|------|----------------|
 | `omgpay.order-result-url` | OMG 付款完成 Client POST 地址 | `https://api.xxx.com/pay/omg/result` |
-| `omgpay.app-return-url` | 后端桥接页唤醒 App 的固定地址 | `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess` |
+| `omgpay.app-return-url` | Bridge 不可用时唤醒 App 的固定 Scheme,同时用于推导固定 App 内路由 | `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess` |
 | `omgpay.notify-url` | 服务端结果回调(前端不用管) | `https://api.xxx.com/pay/omg/notify` |
 
-> App 必须注册 `com.twanmsdyh.app` scheme,并保证该路径映射到现有支付成功页面
+> App 必须在 `pages.json` 注册 `/pages/OrderList/paySuccess/paySuccess`,同时注册 `com.twanmsdyh.app` Scheme 作为外部浏览器和 Bridge 异常时的兜底
 
 ---
 

+ 8 - 0
specs/016-omg-payment/plan.md

@@ -135,6 +135,14 @@ application.yml (+application-dev.yml) # 新增 omg.* 配置段
 - `frontend-integration.md` 提供 uni-app App 生命周期、`plus.runtime.arguments`、`newintent`、当前路由与支付 WebView 事件的可复制日志代码;App 源码不在当前工作区,本批不直接修改 App。
 - 不新增日志上传接口或持久化表;前端通过 HBuilderX 或 iOS Safari Web Inspector 导出本地日志,先定位失败边界再决定修复方案。
 
+## 2026-08-14 iOS uni-app Bridge 回跳修复增量计划
+
+- 真机证据确认同一 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId=...` 可从 iOS 外部浏览器打开 App,但在 App 内 OMG 子 WKWebView 中自动与手动 Scheme 均未发生页面交接;因此 Scheme 注册和固定路由有效,失败边界位于同 App 子 WKWebView 的 Scheme 交接。
+- `/pay/omg/result` 返回页自托管官方 `uni.webview.1.5.8.js`,等待 `UniAppJSBridgeReady`,通过 `uni.webView.getEnv` 确认 App 环境后优先 `redirectTo` 到现有支付结果页;不新增 App 页面,不改变 `/pay/omg/query` 最终状态判定。
+- Bridge 未就绪、非 App 环境、环境检测异常或跳转后仍未交接时使用现有固定 Scheme;Android 与外部浏览器继续保留已验证的 Scheme 兜底,手动按钮仍可重试。
+- CSP 只新增同源脚本许可,SDK 固定打包在 `/static/omg/uni.webview.1.5.8.js`,不依赖第三方运行时 CDN;订单号继续 URL 编码和 HTML 转义,跳转目标不接受请求参数覆盖。
+- `[OMG-RETURN]` 日志增加 Bridge 环境、Bridge 跳转与 Scheme 兜底事件,并将载荷 JSON 序列化,避免 HBuilderX 输出 `[object Object]`;仍不记录凭证、签名、token 或原始 Client POST。
+
 ## Complexity Tracking
 
 > Constitution Check 无违规,无需填表。

+ 2 - 2
specs/016-omg-payment/spec.md

@@ -163,8 +163,8 @@ OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,
 
 - **FR-017**:系统 MUST 自动补偿仍为 `CREATED` 的当前有效支付尝试。调度器按 `next_query_time` 取有限批次,并在真实查询前用条件更新预留该行;查询确认已付款或明确失败时必须复用主动查询的验签与同一状态机,未付款或暂时异常不得关闭支付尝试。
 
-- **FR-018**:系统 MUST 将新订单的 `OrderResultURL` 指向 `/pay/omg/result`,由后端返回禁止缓存的轻量 HTML,并固定唤醒 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`。不得生成 `ClientBackURL`。`OrderResultURL` 的全部实际回传字段(包括未知额外字段与空值)必须参加 CheckMacValue 验签;入口仅负责导航,不得直接改变付款状态。App 唤醒后必须调用 `/pay/omg/query` 确认最终状态。
-- **FR-019**:支付结果中转页 MUST 为自动唤起、手动点击、页面可见性/焦点变化和 JavaScript 异常输出统一前缀的浏览器诊断日志,并在自动尝试后输出分阶段观察结果。日志只能包含浏览器环境、页面生命周期、目标 Scheme 的协议/主机/路径及脱敏订单号,不得包含 token、CheckMacValue、HashKey、HashIV 或 OMG 原始支付表单。App 端必须同步记录生命周期、启动参数、`newintent`、当前路由和支付 WebView 事件;本期日志仅输出到本地调试控制台,不新增远程上报接口。
+- **FR-018**:系统 MUST 将新订单的 `OrderResultURL` 指向 `/pay/omg/result`,由后端返回禁止缓存的轻量 HTML。中转页 MUST 自托管 `uni.webview.1.5.8.js`,在 `UniAppJSBridgeReady` 后确认处于 uni-app App 环境并优先调用 `uni.webView.redirectTo` 打开 `/pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`;Bridge 不可用、环境不是 App 或调用后未发生页面交接时,MUST 回退到 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`,手动按钮也 MUST 保留同一 Scheme 兜底。不得生成 `ClientBackURL`。`OrderResultURL` 的全部实际回传字段(包括未知额外字段与空值)必须参加 CheckMacValue 验签;入口仅负责导航,不得直接改变付款状态。App 页面打开后必须调用 `/pay/omg/query` 确认最终状态。
+- **FR-019**:支付结果中转页 MUST 为 Bridge 就绪/环境/跳转、Scheme 兜底、自动唤起、手动点击、页面可见性/焦点变化和 JavaScript 异常输出统一前缀的浏览器诊断日志,并在自动尝试后输出分阶段观察结果;日志载荷 MUST 序列化为 JSON,避免 HBuilderX 只显示 `[object Object]`。日志只能包含浏览器环境、页面生命周期、固定 App 路由、目标 Scheme 的协议/主机/路径及脱敏订单号,不得包含 token、CheckMacValue、HashKey、HashIV 或 OMG 原始支付表单。App 端必须同步记录生命周期、启动参数、`newintent`、当前路由和支付 WebView 事件;Bridge 内部路由成功不要求出现 `newintent`。本期日志仅输出到本地调试控制台,不新增远程上报接口。
 
 ### Key Entities *(include if feature involves data)*
 

+ 13 - 0
specs/016-omg-payment/tasks.md

@@ -248,6 +248,19 @@ description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
 
 ---
 
+## Phase 17: iOS uni-app Bridge 回跳修复(Priority: P0)
+
+- [x] T096 增加返回页回归测试,覆盖自托管 SDK、固定 App 内路由、`UniAppJSBridgeReady`、App 环境检测、`uni.webView.redirectTo`、Scheme 兜底和 CSP 同源脚本许可。
+- [x] T097 在 `OmgPaymentReturnPageRenderer` 实现 Bridge 优先回跳;Bridge 不可用、非 App 环境、环境检测异常或调用后未发生交接时回退现有固定 Scheme,手动按钮保持可重试。
+- [x] T098 将官方 `uni.webview.1.5.8.js` 固定打包到 `/static/omg/`,CSP 只允许同源脚本;不引入运行时第三方 CDN,不开放动态跳转目标。
+- [x] T099 将 `[OMG-RETURN]` 载荷改为 JSON 字符串并补充 Bridge/兜底事件,同步 `spec.md`、`plan.md`、`contracts/api.md` 与 `frontend-integration.md`。
+- [x] T100 使用 JDK 21 运行返回页定向测试、OMG 回归测试和 `ruoyi-admin` 模块构建,并校验内联脚本语法及本地 SDK 与官方 1.5.8 源码一致。
+- [ ] T101 部署后分别进行 iOS/Android 真机 OMG 信用卡支付回跳验收,确认结果页进入后继续调用 `/pay/omg/query`。
+
+**Checkpoint**:App 内 OMG 返回页优先走 uni-app 内部路由,Android和外部浏览器仍保留固定 Scheme;支付状态继续只由 `/pay/omg/query` 确认。
+
+---
+
 ## Dependencies & Execution Order
 
 ### Phase 依赖

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است