|
@@ -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-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-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-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
|
|
## Risks and Controls
|
|
|
|
|
|
|
@@ -1231,3 +1233,179 @@ mvn -pl ruoyi-admin -am -DskipTests package
|
|
|
- [x] 仅在签名和身份验证通过且 `TradeStatus=10200047 + TradeAmt=0` 时同步旧尝试失败,使 retry 复用既有 `FAILED -> create` 流程。
|
|
- [x] 仅在签名和身份验证通过且 `TradeStatus=10200047 + TradeAmt=0` 时同步旧尝试失败,使 retry 复用既有 `FAILED -> create` 流程。
|
|
|
- [x] 使用 JDK 21 运行查询定向测试、全部 OMG 回归与 `ruoyi-admin` 模块构建。
|
|
- [x] 使用 JDK 21 运行查询定向测试、全部 OMG 回归与 `ruoyi-admin` 模块构建。
|
|
|
- [x] 执行 `git diff --check`、最终差异和暂存范围检查,不包含 `.claude/homunculus/*`。
|
|
- [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 |
|
|
|
|
|
+
|
|
|
|
|
+自检结论:交接设计的每项要求都有明确实施或验收点;前端查询、轮询、提示和最终路由明确属于交接入口的消费方,不在本计划中虚构实现。
|