Browse Source

文档:补充 OMG iOS 返回交接实施计划

qmj 2 tuần trước cách đây
mục cha
commit
3211052782

+ 179 - 1
specs/020-omg-payment-rebuild/plan.md

@@ -1072,8 +1072,10 @@ Do not create an empty commit.
 | FR-036–FR-041 | T049/T056–T059 success-priority state machine, order-only payment update and gateway facts |
 | FR-042–FR-044 | T071–T076 test-stage refund boundary, i18n and safe logging |
 | FR-045–FR-050 | T082–T087 verified retry orchestration, exact-attempt replacement, App contract and regression tests |
+| FR-051–FR-055 | T093–T098 sparse query response validation, fail-closed identity checks and retry recovery |
+| FR-056–FR-060 | T099–T103 current WebView return listener, exact URL handoff and compatibility verification |
 
-Self-review result: all 50 functional requirements have an implementation task and a verification point; no placeholders remain; interface names and signatures are consistent across tasks.
+Self-review result: all 60 functional requirements have an implementation task and a verification point; no placeholders remain; interface names and signatures are consistent across tasks.
 
 ## Risks and Controls
 
@@ -1231,3 +1233,179 @@ mvn -pl ruoyi-admin -am -DskipTests package
 - [x] 仅在签名和身份验证通过且 `TradeStatus=10200047 + TradeAmt=0` 时同步旧尝试失败,使 retry 复用既有 `FAILED -> create` 流程。
 - [x] 使用 JDK 21 运行查询定向测试、全部 OMG 回归与 `ruoyi-admin` 模块构建。
 - [x] 执行 `git diff --check`、最终差异和暂存范围检查,不包含 `.claude/homunculus/*`。
+
+## 2026-08-17 iOS WebView 返回交接实施计划
+
+**Goal:** 让 OMG 专用页面 `pages/OrderList/buy/omgCheckout` 在 iOS 中可靠识别后端结果页已经加载,并把控制权交给 App 逻辑层;后端保留现有结果 HTML、“返回 App”按钮和 Scheme 兜底。
+
+**Architecture:** `omgCheckout` 的 renderjs 继续只负责提交 OMG HTML Form。普通 `<script>` 在 App-Plus iOS 环境获取当前页面 `this.$scope.$getAppWebview()`,监听原生 `loaded` 并通过 `getURL()` 精确识别 `/pay/omg/result`;识别后只触发一次前端业务处理入口。该页面没有 `<web-view>` 子组件,因此不得读取 `children()[0]`。
+
+**Tech Stack:** uni-app App-vue(Vue2)、renderjs、HTML5+ `WebviewObject`、iOS WKWebView、Java 21/Spring Boot 后端兼容回归。
+
+### Global Constraints
+
+- 实际前端文件位于用户端 App 仓库 `pages/OrderList/buy/omgCheckout.vue`;该仓库不在当前后端工作区,本计划不假设或修改其请求封装。
+- 当前页面是 OMG 专用页面,不增加 `payChannel` 参数。
+- 监听对象必须是当前页面 `WebviewObject`,不得使用 `children()[0]`。
+- 只在 App-Plus iOS 启用新监听;Android 与外部浏览器继续保留现有返回页兜底行为。
+- 必须精确匹配 `https://foodieapi.waimai-paotui.com/pay/omg/result` 及其 query/hash 形式。
+- 结果页到达只产生一次 `OMG_RETURNED` 信号,不代表支付成功;查询、轮询、提示和最终路由由前端业务实现。
+- `/pay/omg/result` 继续返回现有 HTML;本阶段不删除提示内容、“返回 App”按钮、Bridge 诊断或 App Scheme。
+- 日志只记录 URL 是否匹配、尝试次数和脱敏订单号,不记录完整 URL、token、表单字段或 `CheckMacValue`。
+
+### File Structure
+
+```text
+用户端 App 仓库(当前工作区外)
+└── pages/OrderList/buy/omgCheckout.vue  # 绑定当前页面 WebView、识别结果 URL、清理监听
+
+当前后端仓库
+├── ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/
+│   └── OmgPaymentClientReturnServiceTest.java  # 现有结果 HTML/按钮/Scheme 兼容回归
+└── specs/020-omg-payment-rebuild/
+    ├── omg-ios-webview-return-fix.md           # 已批准交接设计
+    ├── plan.md                                 # 本实施计划
+    └── tasks.md                                # T099-T103 执行清单
+```
+
+### Task 1: 用户端当前 WebView 返回监听
+
+**Files:**
+
+- Modify: `pages/OrderList/buy/omgCheckout.vue`
+- Test: 使用用户端 App 现有 Vue/uni-app 测试目录为该页面增加原生 WebView mock 测试;若该仓库没有自动化测试入口,以 Task 2 的 iPhone 真机日志作为原生交接验收,不在后端仓库创建伪前端测试。
+
+**Interfaces:**
+
+- Consumes: `this.$scope.$getAppWebview(): WebviewObject`、`WebviewObject#addEventListener('loaded', handler)`、`WebviewObject#getURL(): string`。
+- Produces: 同一次结果页加载最多调用一次前端业务入口 `handleOmgReturnDetected({ orderId, currentUrl })`;该入口的支付查询和路由实现由前端负责。
+
+- [ ] **Step 1: 先验证当前页面结构与失败基线**
+
+  在 iPhone 真机或 iOS 模拟环境记录当前页面 WebView URL 变化,确认 Form 提交后当前 `WebviewObject` 依次加载 OMG 收银台和 `/pay/omg/result`,并确认页面没有可供监听的 `<web-view>` 子组件。预期旧版本最终停留在后端结果页,控制台没有 `[OMG-APP] result_page_detected`。
+
+- [ ] **Step 2: 在普通 `<script>` 增加固定结果地址与状态**
+
+  在组件外声明:
+
+  ```js
+  const OMG_RESULT_URL =
+    'https://foodieapi.waimai-paotui.com/pay/omg/result'
+  ```
+
+  在 `data()` 中增加 `omgReturnHandled: false`。不得把完整 OMG 表单或返回 URL 保存到响应式数据。
+
+- [ ] **Step 3: 在 App-Plus iOS 生命周期绑定当前页面 WebView**
+
+  将以下行为合并进页面现有生命周期,不覆盖原有 `onReady`/`onUnload`:
+
+  ```js
+  onReady() {
+    // #ifdef APP-PLUS
+    if (uni.getSystemInfoSync().platform === 'ios') {
+      this.bindOmgReturnListener()
+    }
+    // #endif
+  },
+
+  onUnload() {
+    this.clearOmgReturnListener()
+  }
+  ```
+
+  `bindOmgReturnListener()` 必须使用当前页面对象:
+
+  ```js
+  const pageWebView = this.$scope.$getAppWebview()
+  ```
+
+  保存 `pageWebView` 和具名 `loaded` handler,绑定后立即调用一次 URL 检查。不得调用 `pageWebView.children()`。
+
+- [ ] **Step 4: 精确识别结果页并做一次性交接**
+
+  URL 判断必须等价于:
+
+  ```js
+  const isResultPage =
+    currentUrl === OMG_RESULT_URL ||
+    currentUrl.indexOf(OMG_RESULT_URL + '?') === 0 ||
+    currentUrl.indexOf(OMG_RESULT_URL + '#') === 0
+  ```
+
+  非结果地址直接返回;结果地址且 `omgReturnHandled === false` 时先把它设为 `true`,再记录脱敏日志并调用:
+
+  ```js
+  this.handleOmgReturnDetected({
+    orderId: this.orderId,
+    currentUrl
+  })
+  ```
+
+  `handleOmgReturnDetected` 是本交接单元提供给前端业务的唯一入口。该入口不得仅凭 URL 直接声明支付成功;查询和最终路由由前端在用户端仓库中完成。
+
+- [ ] **Step 5: 清理监听并验证重复事件**
+
+  `clearOmgReturnListener()` 使用保存的同一个 handler 调用 `removeEventListener('loaded', handler)`,然后清空引用。模拟两次相同 `loaded` 时,`handleOmgReturnDetected` 调用次数必须为 1;页面卸载后调用次数必须保持不变。
+
+- [ ] **Step 6: 使用中文提交用户端改动**
+
+  ```powershell
+  git add -- 'pages/OrderList/buy/omgCheckout.vue'
+  git commit -m '修复:接管 OMG iOS WebView 返回信号'
+  ```
+
+### Task 2: 后端兼容回归与真机验收
+
+**Files:**
+
+- Verify only: `ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentReturnPageRenderer.java`
+- Verify only: `ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentClientReturnServiceTest.java`
+- Reference: `specs/020-omg-payment-rebuild/omg-ios-webview-return-fix.md`
+
+**Interfaces:**
+
+- Consumes: `POST /pay/omg/result` 返回的现有 `text/html` 页面。
+- Produces: 保持 HTTP 200、繁体提示、“返回 App”按钮、App Scheme 和无敏感信息的现有兼容页面;iOS 主流程不依赖这些兜底成功。
+
+- [ ] **Step 1: 运行现有后端结果页定向测试**
+
+  ```powershell
+  $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
+  $env:Path="$env:JAVA_HOME\bin;$env:Path"
+  mvn -pl ruoyi-admin -am "-Dtest=OmgPaymentClientReturnServiceTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
+  ```
+
+  预期退出码为 0;`verifiedPaymentResultReturnsNoStoreHtmlThatOnlyNavigatesToApp` 和 `verifiedPaymentResultUsesUniAppBridgeBeforeKeepingSchemeFallback` 继续通过。除非测试暴露与已批准规格直接冲突,不修改返回页生产代码。
+
+- [ ] **Step 2: 执行 iPhone 真机主流程验收**
+
+  完成一笔 OMG stage 支付,确认日志顺序至少包含:
+
+  ```text
+  [OMG-APP] webview_loaded {"isOmgResultPage":true}
+  [OMG-APP] result_page_detected {"orderRef":"***7389"}
+  ```
+
+  同一次返回不得出现第二次 `result_page_detected`。即使结果页仍输出旧 Bridge/Scheme 失败日志,也不得阻止 App 逻辑层收到该信号。
+
+- [ ] **Step 3: 执行负例和兼容验收**
+
+  在 OMG 收银台中间页、取消页和其他 URL 上确认不触发 `result_page_detected`;使用 Android 和外部浏览器确认现有结果页提示、按钮与 Scheme 仍保留。
+
+- [ ] **Step 4: 检查交付范围**
+
+  后端仓库不应出现 `OmgPaymentReturnPageRenderer.java` 生产差异;用户端提交只包含 `omgCheckout.vue` 及该仓库确有的对应测试。不得混入 `.claude/homunculus/*`。
+
+### Coverage Self-Review
+
+| 设计要求 | 实施/验证位置 |
+|---|---|
+| 监听当前页面而非子 WebView | Task 1 Steps 1、3 |
+| 精确识别固定结果 URL | Task 1 Step 4 |
+| 只触发一次并在卸载时清理 | Task 1 Step 5 |
+| 结果页到达不等于支付成功 | Task 1 Step 4、Task 2 Step 2 |
+| 保留现有 HTML、按钮与 Scheme | Task 2 Steps 1、3 |
+| iOS 主流程不依赖旧 Bridge/Scheme | Task 2 Step 2 |
+| Android/外部浏览器不受影响 | Task 2 Step 3 |
+
+自检结论:交接设计的每项要求都有明确实施或验收点;前端查询、轮询、提示和最终路由明确属于交接入口的消费方,不在本计划中虚构实现。

+ 25 - 1
specs/020-omg-payment-rebuild/spec.md

@@ -23,6 +23,7 @@
 - 每次回调独立写入现有 `ipn_log`,并保存完整、可重放的 form-urlencoded 回传内容。
 - 成功回调只把订单 `payStatus` 更新为已付款,不推进订单、配送或推送流程。
 - 停用旧 OMG Controller 及其补单、退款、定时任务和订单取消调用入口。
+- 定义 iOS 用户端 `pages/OrderList/buy/omgCheckout` 通过当前原生 WebView 的 `loaded` 与固定结果 URL 接管返回流程。
 
 ### 1.2 Explicitly out of scope
 
@@ -33,7 +34,7 @@
 - 分期、定期定额、记忆卡号、银联专用流程。
 - 正式环境开放。
 - 旧 OMG 支付流水的数据迁移、兼容或清理。
-- 客户端页面代码;本阶段只定义客户端必须遵守的表单 POST 契约
+- 用户端 App 源码不在当前后端工作区;本仓库定义表单 POST 与 iOS 返回交接契约,实际页面修改和后续支付查询、提示及路由由前端实施
 
 ### 1.3 Trust decisions
 
@@ -129,6 +130,23 @@ OMG 向 `ReturnURL` 发送最终付款结果时,系统保存本次 HTTP 回传
 7. **Given** 回调无法验证或持久化,**When** 交易号不存在、商户/金额不符、验签失败、字段非法或业务事务失败,**Then** 不修改订单或支付尝试,并返回 `0|ERROR` 以允许 OMG 重试。
 8. **Given** 任意回调请求到达,**When** 后续验签或业务事务失败,**Then** 本次完整回传内容仍以独立事务新增到 `ipn_log`;日志表故障不得阻断真实支付处理。
 
+---
+
+### User Story 8 - iOS 支付结果页返回 App 逻辑层 (Priority: P1)
+
+iOS 用户在 OMG 收银台完成或结束支付流程后,OMG 加载后端 `/pay/omg/result`。专用页面 `pages/OrderList/buy/omgCheckout` 监听当前页面原生 WebView 的加载地址,识别固定结果 URL 后只触发一次 App 业务交接;后端现有结果页、按钮和 Scheme 继续作为兼容兜底。
+
+**Why this priority**: 远程结果页在同 App WKWebView 中调用 Bridge、父 WebView 或 Scheme 已有真机失败证据;如果 App 逻辑层不能接管,iOS 用户会停留在返回页。
+
+**Independent Test**: 在 iPhone 中完成一笔 OMG stage 支付,确认当前页面 WebView 加载 `/pay/omg/result` 后仅出现一次 `result_page_detected`;中间页、取消页和其他 URL 不触发;Android 和外部浏览器的现有返回页按钮及 Scheme 不受影响。
+
+**Acceptance Scenarios**:
+
+1. **Given** `omgCheckout` 已经提交 OMG Form 并绑定当前页面 WebView 的 `loaded`,**When** 当前 URL 精确进入 `/pay/omg/result`,**Then** App 逻辑层只触发一次 `handleOmgReturnDetected({ orderId, currentUrl })`。
+2. **Given** 当前 WebView 正在加载 OMG 收银台、中间页、取消页或其他地址,**When** `loaded` 触发,**Then** App 不产生 `OMG_RETURNED` 交接信号。
+3. **Given** 后端返回现有结果 HTML,**When** iOS 主监听接管成功或从 Android/外部浏览器访问,**Then** 原提示、“返回 App”按钮和 App Scheme 仍可作为非关键兜底保留。
+4. **Given** App 已识别结果 URL,**When** 前端开始后续处理,**Then** 不得只凭结果页到达宣称支付成功,支付查询和最终路由由前端业务实现。
+
 ### Edge Cases
 
 - 支付成功回调丢失时,用户查询当前有效尝试;完整验签及商户号、交易号、金额核对后必须原子补偿为已付款。
@@ -170,6 +188,11 @@ OMG 向 `ReturnURL` 发送最终付款结果时,系统保存本次 HTTP 回传
 - **FR-053**: `TradeStatus=1` MUST 继续要求可信结算所需的 `TradeNo`、`PaymentDate`、`PaymentType`、`PaymentTypeChargeFee` 与 `TradeDate`,不得因兼容稀疏未付款响应而放宽已付款事实校验。
 - **FR-054**: 查询字段缺失日志 MAY 记录缺失字段名称,但 MUST NOT 记录完整网关响应、`CheckMacValue`、HashKey、HashIV 或登录 token。
 - **FR-055**: 当已签名查询响应的商户号和交易号精确匹配、`TradeStatus=10200047` 且 `TradeAmt=0` 时,系统 MUST 将其视为网关不存在该交易并同步旧尝试失败,使 retry 创建新表单;该状态返回非零金额或任一身份、签名校验失败时 MUST fail closed。
+- **FR-056**: OMG 专用页面 `pages/OrderList/buy/omgCheckout` MUST 在 App-Plus iOS 获取并监听当前 `this.$scope.$getAppWebview()`;该页面通过 renderjs 在当前 WebView 提交 Form,不得按 `<web-view>` 子组件使用 `children()[0]`。
+- **FR-057**: iOS 返回监听 MUST 精确匹配 `https://foodieapi.waimai-paotui.com/pay/omg/result` 及其 query/hash 形式;只包含 `omg`、`result` 或其他域名的地址 MUST NOT 触发交接。
+- **FR-058**: 同一次结果页返回 MUST 最多触发一次 `handleOmgReturnDetected({ orderId, currentUrl })`;页面卸载时 MUST 移除原生 `loaded` 监听并清理引用。
+- **FR-059**: `/pay/omg/result` MUST 继续返回现有安全 HTML;繁体提示、“返回 App”按钮和 App Scheme MAY 作为 Android、外部浏览器或监听失效时的兼容兜底保留,但 iOS 主流程 MUST NOT 依赖 Bridge、`parent.evalJS` 或 Scheme 成功。
+- **FR-060**: 结果页 URL 到达只表示 OMG 浏览器流程返回,MUST NOT 作为付款成功事实;App 后续查询、提示和路由由前端业务实现。
 
 - **FR-001**: 系统 MUST 新建 `com.ruoyi.app.omgpay` 下的 Controller、请求/响应 DTO、创建服务、表单生成器、签名器和配置类型;这些新类 MUST NOT 引用旧 `OmgPayController`、旧 `OmgPay`、旧 `OmgCheckMacValue` 或旧 OMG 支付流水服务。
 - **FR-002**: 系统 MUST 新建 `com.ruoyi.system.omgpay` 下的支付尝试 Entity、Mapper 和 Service;新支付尝试 MUST 使用 `pos_order_omg_attempt`,不得读取或写入旧 OMG 支付流水表。
@@ -376,6 +399,7 @@ token: <login-token>
 - **SC-011**: 新 `omgpay` 的公开类型、签名编码、事务和数据库并发边界均有必要注释,显然样板代码上的重复注释数为 0。
 - **SC-012**: 生产源码中旧 `com.ruoyi.app.utils.omg` 类定义、导入和调用数为 0;旧支付/退款表没有运行 SQL,测试源码只允许用类名或表名做退役断言。
 - **SC-013**: 已验证的 `UNPAID + paymentType 为空` 重试 100% 使用新 `MerchantTradeNo`;同一旧尝试并发重试时新活动尝试数不超过 1,旧表单重放次数为 0。
+- **SC-014**: iPhone 真机每次加载精确 `/pay/omg/result` 最多产生一次 `result_page_detected`;中间页、取消页和其他 URL 的误触发次数为 0,Android 与外部浏览器的现有返回页兜底保持可用。
 
 ## Assumptions
 

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

@@ -162,9 +162,17 @@
 - [x] T097 [US7] 将签名有效且身份匹配的 `10200047 + TradeAmt=0` 同步为失败,释放旧尝试并复用 retry 新表单流程
 - [x] T098 使用 JDK 21 运行查询定向测试、全部 OMG 回归、模块构建与最终 diff/暂存范围检查
 
+## Phase 17: iOS WebView 返回交接
+
+- [ ] T099 [US8] 在用户端 App `pages/OrderList/buy/omgCheckout.vue` 确认 renderjs Form 替换当前页面 WebView,并记录旧版本停留结果页的失败基线
+- [ ] T100 [US8] 在 `omgCheckout.vue` 的普通 `<script>` 中仅对 App-Plus iOS 监听当前 `this.$scope.$getAppWebview()` 的 `loaded`,不得读取 `children()[0]`
+- [ ] T101 [US8] 精确识别 `https://foodieapi.waimai-paotui.com/pay/omg/result` 及其 query/hash 形式,只触发一次 `handleOmgReturnDetected({ orderId, currentUrl })`,页面卸载时移除监听
+- [ ] T102 [US8] 使用 JDK 21 运行 `OmgPaymentClientReturnServiceTest`,确认后端现有 HTML、繁体提示、“返回 App”按钮与 Scheme 兜底保持不变
+- [ ] T103 [US8] 使用 iPhone 真机验证 `result_page_detected` 单次触发,并用中间页/取消页、Android 和外部浏览器完成负例及兼容验收
+
 ## Dependencies & Execution Order
 
-- Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 → Phase 8 → Phase 9 → Phase 10。
+- Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 → Phase 8 → Phase 9 → Phase 10 → Phase 11 → Phase 12 → Phase 13 → Phase 14 → Phase 15 → Phase 16 → Phase 17
 - T008 与 T011/T012 可独立写测试,但实施时顺序执行以维持清晰 TDD 证据。
 - T031 必须早于任何脏文件修改;T041 必须在旧代码退役完成后执行。
 - 手动 stage 验收依赖开发者执行 DDL,自动测试和构建不依赖数据库变更。
@@ -182,3 +190,4 @@
 - `pos_store_omg` 凭证表、持久化和管理能力保留;联网探测只使用新 `omgpay` 实现。
 - 日志足以定位问题且无敏感信息;非预期异常保留堆栈。
 - JDK 21 定向测试与构建通过,最终 diff 无无关改动。
+- iPhone 中 `omgCheckout` 监听当前页面 WebView,精确结果 URL 单次触发交接;后端现有结果页、按钮和 Scheme 兜底保持兼容。