Branch: (待创建,暂未建分支/未提交) | Date: 2026-07-29 | Spec: spec.md
Input: Feature specification from /specs/016-omg-payment/spec.md
接入台湾 OMG(歐買尬,金流引擎 FunPoint)AIO 幕前支付作为餐饮订单的在线支付,替代蓝新 NewebPay(不再启用)。OMG 用 CheckMacValue(SHA256) 单字段签名(无 AES、无解密),新支付订单仅提供信用卡和 Apple Pay(ChoosePayment=Credit、UnionPay=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,执行自动退款。
支付尝试生命周期已经收敛为每个订单一条有效支付尝试;重复点击复用该尝试,查询、回调与自动补偿都只围绕它处理。当前开发测试数据不保留延期支付或历史多尝试兼容。
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 后续衔接,不在本期)
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 设计后再次确认无蓝新耦合。
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 生成,本阶段不创建)
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 阶段定具体落点。
next_query_time/query_count/last_query_time 和到期扫描索引,SQL 仅登记不执行。omgpay 新目录内的任务竞争全局 Redisson 锁,按到期时间最多扫描 20 条 CREATED 尝试。attempt_status=CREATED AND next_query_time<=now 条件更新进行预留,并把下一次查询推迟 5 分钟;预留失败不访问 OMG。OrderResultURL,不配置 ClientBackURL 或 ClientRedirectURL。POST /pay/omg/result 接收支付完成 Client POST,复用严格原始表单解析并以支付尝试的凭证快照验签,所有实际字段都参加 CheckMacValue。/pay/omg/back;取消或 OTP 失败时由用户关闭收银台或返回上一页,不影响付款结果回调。com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId},订单号进行 URL 与 HTML 双重转义;同时保留手动按钮,App 页面打开后调用 /pay/omg/query 确认状态。Credit 下,因此固定传 ChoosePayment=Credit;同时传 UnionPay=2 隐藏银联。ExpireDate、StoreExpireDate、BarcodeATMExpireDate、ClientRedirectURL,所有保留字段(含 UnionPay)统一参加 CheckMacValue 计算。OrderResultURL -> /pay/omg/result -> App Scheme 路径和支付状态判定不变,只增强可观测性。[OMG-RETURN] 为统一前缀,记录页面加载、自动唤起、手动点击、visibilitychange/pagehide/pageshow/blur/focus、全局脚本异常和分阶段观察结果。frontend-integration.md 提供 uni-app App 生命周期、plus.runtime.arguments、newintent、当前路由与支付 WebView 事件的可复制日志代码;App 源码不在当前工作区,本批不直接修改 App。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 最终状态判定。/pay/omg/bridge.js 输出,不依赖第三方运行时 CDN;订单号继续 URL 编码和 HTML 转义,跳转目标不接受请求参数覆盖。[OMG-RETURN] 日志增加 Bridge 环境、Bridge 跳转与 Scheme 兜底事件,并将载荷 JSON 序列化,避免 HBuilderX 输出 [object Object];仍不记录凭证、签名、token 或原始 Client POST。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 兜底逻辑不变。getEnv 返回 plus=true,但 uni.webView.redirectTo 同步抛出 ReferenceError;SDK 在当前运行时找不到 __uniapp__service 后会转到启动 WebView 调用 UniPlusBridge,失败点已定位到 Bridge 内部路由派发。evalJS 让父 uni-app 页面执行 uni.redirectTo;该父页面即 App 当前的支付 WebView 宿主页。uni.webView.redirectTo;父 WebView 不可用、执行异常或未发生交接时保留固定 Scheme 兜底。异常日志增加脱敏后的异常消息,便于真机复核。Constitution Check 无违规,无需填表。
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| — | — | — |