# Contracts: OMG(歐買尬/FunPoint)AIO 支付接入 **Feature**: specs/016-omg-payment/spec.md **Date**: 2026-07-29 > **约束**:OMG 全部独立实现,零 `newebpay` 依赖(见 research.md §0)。签名统一用 `CheckMacValue`(SHA256, EncryptType=1),无 AES、无解密。 --- ## A. OMG 外部接口(我方 → 盘合) ### A1. 幕前下单 — `Cashier/AioCheckOut/V5`(US1) - **URL**:测试 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`;正式 `https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5` - **Method/CT**:`POST`,`application/x-www-form-urlencoded` - **方式**:后端组参 + 计算 CheckMacValue 返回 form 字段,**前端 Form Post** 跳转 OMG 收银台(不可 iframe;iOS 不可另开窗) - **必填请求参数**:`MerchantID`、`MerchantTradeNo`(=OMG+ddId)、`MerchantTradeDate`(yyyy/MM/dd HH:mm:ss)、`PaymentType=aio`、`TotalAmount`(整数元)、`ReturnURL`、`ChoosePayment=Credit`、`CheckMacValue`、`EncryptType=1` - **本项目固定参数**:`UnionPay=2`(隐藏银联)、`InvoiceMark=N`、`NeedExtraPaidInfo=Y`、`OrderResultURL`;新订单不得传 `ClientBackURL`、`PaymentInfoURL`、`ClientRedirectURL` 或延期缴费期限参数。 - **固定**:`InvoiceMark=N`(发票走 ezPay);`ReturnURL ≠ OrderResultURL`;`MerchantTradeNo` 全平台唯一 ### A2. 付款结果回调 — ReturnURL(盘合 → 我方,US2) - 盘合**服务端 POST** 我方 `/pay/omg/notify`(CT `text/html`) - **参数**:`MerchantID`、`MerchantTradeNo`、`StoreID`、`RtnCode`(**1=成功**,其余勿发货)、`RtnMsg`、`TradeNo`(OMG 交易号,须存并与 MerchantTradeNo 关联)、`TradeAmt`、`PaymentDate`、`PaymentType`、`TradeDate`、`SimulatePaid`(**1=模拟,勿发货**)、`CustomField1–4`、`CheckMacValue` - **我方响应**:纯字符串 `1|OK`(首字符 1=成功);未收到 5–15 分重试,当天最多 4 次 - **核销校验顺序**:CheckMacValue 验签 → `RtnCode==1` → `SimulatePaid!=1` → `TradeAmt==订单 amount` → `trade_no` 幂等 ### A4. 订单查询 — `Cashier/QueryTradeInfo/V5`(凭证验证 / 补单) - **URL**:测试 `https://payment-stage.funpoint.com.tw/Cashier/QueryTradeInfo/V5`;正式 `https://payment.funpoint.com.tw/Cashier/QueryTradeInfo/V5` - **请求**:`MerchantID`、`MerchantTradeNo`、`TimeStamp`(3 分钟内)、`PlatformID?`、`CheckMacValue` - **响应**(text/html,k=v):`TradeStatus`(**0 未付/1 已付/10200095 失败**)、`TradeNo`、`TradeAmt`、`PaymentDate`、`PaymentType`、`TradeDate`、`CheckMacValue`… - **用途**:录入凭证探测金钥 + 回调漏收时补单 ### A5. 信用卡退款/取消 — `CreditDetail/DoAction`(US4) - **URL**:正式 `https://payment.funpoint.com.tw/CreditDetail/DoAction`(**stage 不可用**) - **请求**:`MerchantID`、`MerchantTradeNo`、`TradeNo`、`Action`(**C** 關帳/**R** 退刷/**E** 取消/**N** 放棄)、`TotalAmount`、`CheckMacValue`、`PlatformID?` - **响应**(k=v):`RtnCode`(**1=成功**)、`RtnMsg` - **规则**:`已關帳→R`、`已授權→N`、`要關帳→E 再 N 或 R`;分期必须全额;自动关帳开启时避开 20:15–20:30 --- ## B. 平台内部接口(暴露给前端/管理端) ### B1. 发起 OMG 支付 — `POST /pay/omg/create`(US1,@Anonymous @Auth,Header token) - 入参:`orderid`(= ddId) - 逻辑:校验订单归属/金额/未支付 → 查门店启用凭证 → 生成 `MerchantTradeNo` → 组参 + CheckMacValue → 落 `pos_order_omg_payment`(pay_status=0) + 更新 `pos_order.pay_type="2"`/`pay_url` - 返回:form 字段 `{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=Credit, UnionPay=2, EncryptType=1, ItemName, CheckMacValue, ... }`,前端构建隐藏 form 自动 submit 到 `gatewayUrl` ### B2. OMG 支付结果回调 — `POST /pay/omg/notify`(US2,@Anonymous) - 处理 A2:collectForm → IpnLog → 凭证(MerchantID) → 验签 CheckMacValue → 幂等(trade_no) → 金额校验 → RtnCode==1 && SimulatePaid!=1 → markSuccess → 更新订单(state=0, payStatus=1) + 订单日志 + 推送用户/商家/骑手 + sendAcceptRiderPush → 回纯串 `1|OK` ### B3. 支付完成返回页 — `POST /pay/omg/result`(US2,@Anonymous) - 作为 `OrderResultURL` 接收 OMG Client POST,使用完整 DTO 严格解析并按支付尝试凭证快照验签;未知额外字段和空值全部进入 CheckMacValue。 - 验签通过后只返回 no-store HTML:从匿名只读的 `GET /pay/omg/bridge.js` 加载后端自托管 `uni.webview.1.5.8.js`;在 `UniAppJSBridgeReady` 后,iOS App-Plus 优先通过支付子 WebView 的父 uni-app 页面执行 `uni.redirectTo`,其他 App 环境使用 `uni.webView.redirectTo`,统一打开 `/pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`。Bridge 不可用、非 App 环境或未完成页面交接时,回退到 `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId={订单ddId}`,手动按钮保留同一 Scheme。该入口不修改订单状态,App 必须再调 `/pay/omg/query`。 ### B5. 订单查询/被动补单 — `POST /pay/omg/query`(US6 方案A,@Anonymous @Auth,Header token) - **入参**:`orderid`(= ddId) - **鉴权**:登录用户 + 订单本人(`token → userId == order.userId`);`order.payType` 必须为 `"2"` - **逻辑**:订单已 `payStatus∈{1,2}` 直接返回不查 OMG;否则复用 `OmgPay.queryTrade()`(§A4) → 按 `TradeStatus` 分支(见下),严格幂等(`markSuccess` 按 trade_no CAS,回调+补单并发不重复核销/推送) - **复用**:`paymentService.getLatestByDdId` + `storeOmgService.getCredentialByMerchantId` + `OmgPayController.reconcileByQuery()`(与定时补单共用)→ `applyPaidResult()`(markSuccess + `handlePaymentSuccess` 推送,与 notify 同链路) - **返回**: ```json { "code": 200, "data": { "payStatus": 1, "reconciled": true } } // payStatus: 0未付 / 1已付(含本次补单) / 2失败 ; reconciled: 本次是否触发了补单核销 ``` - **TradeStatus 分支**:`1` 已付 → 金额/字段校验通过 → `applyPaidResult` 补单 + 返回已支付;`10200095` 失败 → `markFail` + 返回支付失败;`0` 或其他未知值 → 保持待支付(前端继续轮询 / 定时任务下轮再查) - **自愈**:若流水已 `pay_status=1` 但订单未核销(跨事务中断残留),补推订单状态,避免「付了钱订单永远未支付」 ### B6. 订单取消退款(US4) - 触发点:订单取消链路(`PosOrderShOprate` 商家取消 / `UserOrderController` 用户取消)内,对 OMG 已支付订单调 A5 DoAction;或 `POST /pay/omg/refund?orderid=`(落点 tasks 定) - 写 `pos_order_omg_refund`;成功置 `pos_order_omg_payment.pay_status=3`、`pos_order.pay_status=2` ### B7. 门店 OMG 凭证管理 — `/system/storeOmg/*`(US5,@PreAuthorize) - `GET /list`(分页筛选)、`GET /{storeId}`、`PUT /apply/{storeId}`、`PUT /saveCredentials`(录入+QueryTradeInfo 探测验证)、`PUT /toggleEnable/{storeId}`、`PUT /reset/{storeId}`、`PUT /enabledPayments/{storeId}` - 镜像 011 `PosStoreNewebpayController` 模式,但**独立 Controller/Service/实体/表** ### B8. 漏单定时补单 — `OmgReconcileTask`(US6 方案B,@Scheduled + Redisson) - **触发**:`@Scheduled(fixedDelay)` 默认每 3 分钟一轮(`omg.reconcile.fixed-delay-ms`),上一轮跑完才开始计时,启动延迟 60s - **扫描**:`paymentService.scanLeakOrders(windowStart, graceCutoff, batchSize)` → `pos_order_omg_payment` 中 `pay_status=0` 且 `create_time` 落在 `[now-windowHours, now-graceMinutes]`、每订单取最新一笔、`INNER JOIN pos_order` 排除已取消(`state≠4`) - **补单**:逐笔调 `OmgPayController.reconcileByQuery(ddId,"scheduled")`(与 §B5 被动补单同一核销链路) - **补单窗口**:按当前配置扫描仍待支付的有效信用卡尝试;`grace-minutes` 给回调/OMG重试留送达时间,窗口外停止扫描避免无限轮询。 - **多实例并发保护**:Redisson 分布式锁 `lock:omg:reconcile`(`tryLock` 拿不到即让出),保证同一轮只一个节点执行;幂等能兜底,锁减少无效调用 - **配置**:`application.yml` → `omg.reconcile.{fixed-delay-ms, initial-delay-ms, window-hours, grace-minutes, batch-size, lock-wait-seconds, lock-lease-seconds}` --- ## C. 代码表与枚举(OMG reference 02–05,2026-07-29 补读) ### C1. ChoosePayment(请求参数,US1) 本期固定 `Credit`,并传 `UnionPay=2` 隐藏银联,只保留普通信用卡与 Apple Pay。OMG 官方将 Apple Pay 归在 `Credit` 下;回覆 PaymentType 同为 `Credit_CreditCard`,仅凭 PaymentType 无法区分两者(本期不区分)。 ### C2. 回覆 PaymentType(回调 `PaymentType`,落 `pos_order_omg_payment.pay_type`) | PaymentType | 名称 | 即时/延期 | |---|---|---| | `Credit_CreditCard` | 信用卡(含 Apple Pay;请求以 `UnionPay=2` 隐藏银联) | 即时 | 本项目当前只接受这一种回覆类型;开发测试阶段没有延期支付历史交易需要兼容。 ### C3. RtnCode(回调 / 退款响应) `1` = 成功;其余均为失败。完整代码表文档为**图片**且持续新增,须到「歐買尬廠商後台 → 系統開發管理 → 交易狀態代碼查詢」查全。 **实现策略**:`RtnCode==1` 视成功(核销 / 退款成功);非 1 一律失败,记录 `rtn_code`+`rtn_msg` 供诊断,**不硬编码错误码分支**。 ### C4. URLEncode(CheckMacValue 计算,须完全照此 · reference 05) .NET 风格 URLEncode。**Java 实现**:`URLEncoder.encode(s, UTF_8)`(空格已→`+`)后,照 PHP 范例做替换以对齐 .NET,再转小写、SHA256、hex 大写: - 不编码(字面):`-` `_` `.` `!` `*` `(` `)`;`~`→`%7e`(两侧同)。 - 空格→`+`(.NET/Java 表单编码一致;标准 URLEncode 是 `%20`,勿混用)。 - 其余特殊符 `@ # $ % ^ & = + ; ? / \ > < [ ] { } : ' " , |` 均为 `%XX`;中文按 UTF-8 `%XX`。 - 替换集(防御性,照 PHP 范例):`%2d→-`、`%5f→_`、`%2e→.`、`%21→!`、`%2a→*`、`%28→(`、`%29→)`(Java `URLEncoder` 本就不编 `-_.`,`!*()` 需替换回)。 ## 签名约定(所有请求/回调通用) `CheckMacValue`(EncryptType=1, SHA256): 去 CheckMacValue → 参数按 key 字母序 → `k=v&...` → 包夹 `HashKey={k}&..&HashIV={iv}` → .NET 风格 URL 编码(`-_.!*()` 不编码) → 转小写 → SHA256 → hex 大写。校验同理重算比对。实现:`OmgCheckMacValue`(独立,不复用 `NewebPayEncryptUtil`)。