plan.md 13 KB

Implementation Plan: OMG(歐買尬/FunPoint)AIO 支付接入

Branch: (待创建,暂未建分支/未提交) | Date: 2026-07-29 | Spec: spec.md

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

Summary

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

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

追加(2026-08-12):账本重构(问题 A 多 MTN 堆积 + 问题 B 验签/补单修复)落地追加式 S(双轴 is_active ⊥ pay_status),设计见 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)

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)

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,不配置 ClientBackURLClientRedirectURL
  • 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 隐藏银联。
  • 新表单移除 ExpireDateStoreExpireDateBarcodeATMExpireDateClientRedirectURL,所有保留字段(含 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.argumentsnewintent、当前路由与支付 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_loadedbridge_ready_timeout,没有 bridge_readybridge_environment 或 JavaScript 异常;线上访问原 /static/omg/uni.webview.1.5.8.js 实际返回 401 JSON,SDK 因 nosniff 未执行。
  • 不放开整个 /static/**,改由 OmgPaymentReturnControllerGET /pay/omg/bridge.js@Anonymousapplication/javascript;charset=UTF-8no-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