api.md 11 KB

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/CTPOSTapplication/x-www-form-urlencoded
  • 方式:后端组参 + 计算 CheckMacValue 返回 form 字段,前端 Form Post 跳转 OMG 收银台(不可 iframe;iOS 不可另开窗)
  • 必填请求参数MerchantIDMerchantTradeNo(=OMG+ddId)、MerchantTradeDate(yyyy/MM/dd HH:mm:ss)、PaymentType=aioTotalAmount(整数元)、ReturnURLChoosePayment=CreditCheckMacValueEncryptType=1
  • 本项目固定参数UnionPay=2(隐藏银联)、InvoiceMark=NNeedExtraPaidInfo=YOrderResultURL;新订单不得传 ClientBackURLPaymentInfoURLClientRedirectURL 或延期缴费期限参数。
  • 固定InvoiceMark=N(发票走 ezPay);ReturnURL ≠ OrderResultURLMerchantTradeNo 全平台唯一

A2. 付款结果回调 — ReturnURL(盘合 → 我方,US2)

  • 盘合服务端 POST 我方 /pay/omg/notify(CT text/html
  • 参数MerchantIDMerchantTradeNoStoreIDRtnCode(1=成功,其余勿发货)、RtnMsgTradeNo(OMG 交易号,须存并与 MerchantTradeNo 关联)、TradeAmtPaymentDatePaymentTypeTradeDateSimulatePaid(1=模拟,勿发货)、CustomField1–4CheckMacValue
  • 我方响应:纯字符串 1|OK(首字符 1=成功);未收到 5–15 分重试,当天最多 4 次
  • 核销校验顺序:CheckMacValue 验签 → RtnCode==1SimulatePaid!=1TradeAmt==订单 amounttrade_no 幂等

A4. 订单查询 — Cashier/QueryTradeInfo/V5(凭证验证 / 补单)

  • URL:测试 https://payment-stage.funpoint.com.tw/Cashier/QueryTradeInfo/V5;正式 https://payment.funpoint.com.tw/Cashier/QueryTradeInfo/V5
  • 请求MerchantIDMerchantTradeNoTimeStamp(3 分钟内)、PlatformID?CheckMacValue
  • 响应(text/html,k=v):TradeStatus(0 未付/1 已付/10200095 失败)、TradeNoTradeAmtPaymentDatePaymentTypeTradeDateCheckMacValue
  • 用途:录入凭证探测金钥 + 回调漏收时补单

A5. 信用卡退款/取消 — CreditDetail/DoAction(US4)

  • URL:正式 https://payment.funpoint.com.tw/CreditDetail/DoActionstage 不可用
  • 请求MerchantIDMerchantTradeNoTradeNoAction(C 關帳/R 退刷/E 取消/N 放棄)、TotalAmountCheckMacValuePlatformID?
  • 响应(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 同链路)
  • 返回

    { "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=3pos_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_paymentpay_status=0create_time 落在 [now-windowHours, now-graceMinutes]、每订单取最新一笔、INNER JOIN pos_order 排除已取消(state≠4)
  • 补单:逐笔调 OmgPayController.reconcileByQuery(ddId,"scheduled")(与 §B5 被动补单同一核销链路)
  • 补单窗口:按当前配置扫描仍待支付的有效信用卡尝试;grace-minutes 给回调/OMG重试留送达时间,窗口外停止扫描避免无限轮询。
  • 多实例并发保护:Redisson 分布式锁 lock:omg:reconciletryLock 拿不到即让出),保证同一轮只一个节点执行;幂等能兜底,锁减少无效调用
  • 配置application.ymlomg.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)。