# 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 | |-----------|------------|-------------------------------------| | — | — | — |