# Implementation Plan: OMG（歐買尬/FunPoint）AIO 支付接入

**Branch**: `（待创建，暂未建分支/未提交）` | **Date**: 2026-07-29 | **Spec**: [spec.md](spec.md)

**Input**: Feature specification from `/specs/016-omg-payment/spec.md`

## Summary

接入台湾 OMG（歐買尬，金流引擎 FunPoint）AIO 幕前支付作为餐饮订单的在线支付，替代蓝新 NewebPay（不再启用）。OMG 用 `CheckMacValue`(SHA256) 单字段签名（无 AES、无解密），新支付订单仅提供信用卡和 Apple Pay（`ChoosePayment=Credit`、`UnionPay=2`），门店级凭证，本期含信用卡退款。**OMG 全部代码独立新建，不复用任何蓝新金流代码/表**；仅复用平台共享基础设施（订单状态机、推送、订单日志）。

技术决策与备选见 [research.md](research.md)，表结构见 [data-model.md](data-model.md)，接口契约见 [contracts/api.md](contracts/api.md)，验证场景见 [quickstart.md](quickstart.md)。

**追加（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` 不改结构，OMG 使用 `pay_type="2"`；后台回调复用共享 `ipn_log` 保存脱敏内容并写 `type=omg`）

**Testing**: 工具类 `main` 自测（CheckMacValue 用官方示例值复算）+ 测试环境端到端（测试卡，见 quickstart.md）；项目无统一自动化测试框架约定

**Target Platform**: Linux server（后端服务），配合 uni-app 用户端 Form Post 跳转 OMG 收银台

**Project Type**: web-service（后端）

**Performance Goals**: 发起支付接口 < 1s；回调核销幂等无重复发货

**Constraints**: CheckMacValue 必须与 OMG 完全一致（含 .NET 风格 URL 编码与小写）；回调须回纯串 `1|OK`；门店级凭证；`InvoiceMark=N`（发票走 ezPay 解耦）；外部 HTTP 工具类须放 `ruoyi-admin`（模块依赖 admin→system 不可反向）

**Scale/Scope**: 全平台餐饮订单在线支付（旅游·机票 015 后续衔接，不在本期）

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

`.specify/memory/constitution.md` 为未填写模板（占位符），无实际治理原则可校验。改为遵循项目 `CLAUDE.md` 与记忆中的硬约束：

| 约束 | 本期遵守 |
|------|----------|
| 模块依赖 admin→system 不可反向 | ✅ OMG 外部 HTTP/签名工具类放 `ruoyi-admin/.../app/utils/omg/`，联网验证在 Controller 层；实体/Service/Mapper 放 `ruoyi-system` |
| 外部 HTTP 工具类（httpclient4+fastjson2）放 admin | ✅ `OmgPay`/`OmgCheckMacValue` 在 admin |
| 后端文件 CRLF | ✅ 新建 Java/md 文件保持 CRLF |
| DB 变更写 `updatesql/sql.md` 不直接执行 | ✅ 三表 DDL 写入，不执行 |
| 前端新功能必须 i18n | 本期后端为主；若涉及前端支付跳转/结果页文字，按规范加 4 语言 |
| 范围控制 / 简单优先 | ✅ 不复用蓝新、不抽公共支付套件（仅一个使用点） |
| **OMG 不复用蓝新代码**（用户新增） | ✅ 零 `newebpay` import，独立凭证/流水/退款表 |

**结论**：无违规，Phase 0 可继续。Phase 1 设计后再次确认无蓝新耦合。

## Project Structure

### Documentation (this feature)

```text
specs/016-omg-payment/
├── plan.md              # 本文件
├── research.md          # Phase 0：技术决策
├── data-model.md        # Phase 1：表结构 + DDL
├── quickstart.md        # Phase 1：端到端验证
├── contracts/
│   └── api.md           # Phase 1：OMG 外部接口 + 平台内部接口
└── tasks.md             # Phase 2 (/speckit-tasks 生成，本阶段不创建)
```

### Source Code (repository root)

```text
ruoyi-admin/src/main/java/com/ruoyi/app/
├── utils/omg/                        # OMG 专属，零 newebpay 依赖
│   ├── OmgCheckMacValue.java         # CheckMacValue(SHA256) 生成/校验 + main 自测
│   ├── OmgPayConfig.java             # MerchantID / HashKey / HashIV
│   └── OmgPay.java                   # HTTP 客户端：createAioForm / queryTrade / doAction
├── pay/
│   └── OmgPaymentController.java     # /pay/omg/{create,notify,query,refund}
└── mendian/
    └── PosStoreOmgController.java    # /system/storeOmg/* 凭证开通管理（US5）

ruoyi-system/src/main/java/com/ruoyi/system/
├── domain/
│   ├── PosStoreOmg.java              # 门店 OMG 凭证（pos_store_omg）
│   ├── PosOrderOmgPayment.java       # OMG 支付流水（pos_order_omg_payment）
│   ├── PosOrderOmgRefund.java        # OMG 退款记录（pos_order_omg_refund）
│   ├── vo/PosStoreOmgVo.java
│   └── dto/StoreOmgCredentialDto.java
├── mapper/
│   ├── PosStoreOmgMapper.java (+xml)
│   ├── PosOrderOmgPaymentMapper.java (+xml)
│   └── PosOrderOmgRefundMapper.java (+xml)
└── service/
    ├── IPosStoreOmgService.java + impl
    ├── IPosOrderOmgPaymentService.java + impl
    └── IPosOrderOmgRefundService.java + impl

updatesql/sql.md                       # 追加三表 DDL（2026-07-29）
application.yml (+application-dev.yml) # 新增 omg.* 配置段
```

**Structure Decision**：沿用项目既有分层（utils 在 `ruoyi-admin/.../app/utils/omg/`，domain/mapper/service 在 `ruoyi-system`，Controller 在 `ruoyi-admin/.../app/{pay,mendian}`），与 ezPay/newebpay 同构但**独立包/独立类/独立表**。退款入口（US4）并入 `OmgPayController` 或订单取消链路（`PosOrderShOprate`/`UserOrderController` 取消时触发），tasks 阶段定具体落点。

## 2026-08-13 自动补偿增量计划

- 在新支付尝试表增加 `next_query_time/query_count/last_query_time` 和到期扫描索引，SQL 仅登记不执行。
- 每分钟由 `omgpay` 新目录内的任务竞争全局 Redisson 锁，按到期时间最多扫描 20 条 `CREATED` 尝试。
- 每条记录先以 `attempt_status=CREATED AND next_query_time<=now` 条件更新进行预留，并把下一次查询推迟 5 分钟；预留失败不访问 OMG。
- 复用已经完成的查询响应验签和支付状态机：已付款补成已付款，明确失败同步失败，未付款或暂时异常保留并等待下一轮。
- 单轮限制 45 秒预算，单条异常记录完整异常上下文并继续，不记录 HashKey/HashIV。

## 2026-08-13 App 返回页增量计划

- 新支付表单只签入 `OrderResultURL`，不配置 `ClientBackURL` 或 `ClientRedirectURL`。
- `POST /pay/omg/result` 接收支付完成 Client POST，复用严格原始表单解析并以支付尝试的凭证快照验签，所有实际字段都参加 CheckMacValue。
- 不实现 `/pay/omg/back`；取消或 OTP 失败时由用户关闭收银台或返回上一页，不影响付款结果回调。
- HTML 固定跳往 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`，订单号进行 URL 与 HTML 双重转义；同时保留手动按钮，App 页面打开后调用 `/pay/omg/query` 确认状态。

## 2026-08-13 支付方式范围重大调整

- 新支付订单仅允许信用卡和 Apple Pay。OMG 官方将 Apple Pay 包含在 `Credit` 下，因此固定传 `ChoosePayment=Credit`；同时传 `UnionPay=2` 隐藏银联。
- 新表单移除 `ExpireDate`、`StoreExpireDate`、`BarcodeATMExpireDate`、`ClientRedirectURL`，所有保留字段（含 `UnionPay`）统一参加 CheckMacValue 计算。
- ATM、CVS、BarcodeATM、AFTEE 不开放；当前只有开发测试数据，不保留延期支付历史回调或返回路由。

## 2026-08-14 iOS App 唤起诊断增量计划

- 保持现有 `OrderResultURL -> /pay/omg/result -> App Scheme` 路径和支付状态判定不变，只增强可观测性。
- 中转页以 `[OMG-RETURN]` 为统一前缀，记录页面加载、自动唤起、手动点击、`visibilitychange/pagehide/pageshow/blur/focus`、全局脚本异常和分阶段观察结果。
- 浏览器日志只输出 Scheme 的协议、主机、路径和脱敏订单号；不输出查询参数、OMG Client POST 原文、token、CheckMacValue 或门店凭证。
- `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 固定打包在 classpath 并由匿名只读地址 `/pay/omg/bridge.js` 输出，不依赖第三方运行时 CDN；订单号继续 URL 编码和 HTML 转义，跳转目标不接受请求参数覆盖。
- `[OMG-RETURN]` 日志增加 Bridge 环境、Bridge 跳转与 Scheme 兜底事件，并将载荷 JSON 序列化，避免 HBuilderX 输出 `[object Object]`；仍不记录凭证、签名、token 或原始 Client POST。

## 2026-08-14 Bridge SDK 匿名访问修复增量计划

- 真机日志只有 `page_loaded` 与 `bridge_ready_timeout`，没有 `bridge_ready`、`bridge_environment` 或 JavaScript 异常；线上访问原 `/static/omg/uni.webview.1.5.8.js` 实际返回 `401` JSON，SDK 因 `nosniff` 未执行。
- 不放开整个 `/static/**`，改由 `OmgPaymentReturnController` 的 `GET /pay/omg/bridge.js` 以 `@Anonymous`、`application/javascript;charset=UTF-8` 和 `no-store` 返回已打包 SDK；返回页只替换脚本地址，其余支付状态、路由和 Scheme 兜底逻辑不变。

## 2026-08-14 iOS 父 WebView 回跳修复增量计划

- 真机日志确认 SDK 已加载且 `getEnv` 返回 `plus=true`，但 `uni.webView.redirectTo` 同步抛出 `ReferenceError`；SDK 在当前运行时找不到 `__uniapp__service` 后会转到启动 WebView 调用 `UniPlusBridge`，失败点已定位到 Bridge 内部路由派发。
- iOS App-Plus 环境不再进入该派发分支，改为从支付子 WebView 获取父 WebView，并通过 `evalJS` 让父 uni-app 页面执行 `uni.redirectTo`；该父页面即 App 当前的支付 WebView 宿主页。
- Android 和其他环境继续使用官方 `uni.webView.redirectTo`；父 WebView 不可用、执行异常或未发生交接时保留固定 Scheme 兜底。异常日志增加脱敏后的异常消息，便于真机复核。

## Complexity Tracking

> Constitution Check 无违规，无需填表。

| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| — | — | — |
