Feature: specs/016-omg-payment/spec.md Date: 2026-07-29
约束:OMG 全部独立实现,零
newebpay依赖(见 research.md §0)。签名统一用CheckMacValue(SHA256, EncryptType=1),无 AES、无解密。
Cashier/AioCheckOut/V5(US1)https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5;正式 https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5POST,application/x-www-form-urlencodedMerchantID、MerchantTradeNo(=OMG+ddId)、MerchantTradeDate(yyyy/MM/dd HH:mm:ss)、PaymentType=aio、TotalAmount(整数元)、ReturnURL、ChoosePayment=Credit、CheckMacValue、EncryptType=1UnionPay=2(隐藏银联)、InvoiceMark=N、NeedExtraPaidInfo=Y、OrderResultURL;新订单不得传 ClientBackURL、PaymentInfoURL、ClientRedirectURL 或延期缴费期限参数。InvoiceMark=N(发票走 ezPay);ReturnURL ≠ OrderResultURL;MerchantTradeNo 全平台唯一/pay/omg/notify(CT text/html)MerchantID、MerchantTradeNo、StoreID、RtnCode(1=成功,其余勿发货)、RtnMsg、TradeNo(OMG 交易号,须存并与 MerchantTradeNo 关联)、TradeAmt、PaymentDate、PaymentType、TradeDate、SimulatePaid(1=模拟,勿发货)、CustomField1–4、CheckMacValue1|OK(首字符 1=成功);未收到 5–15 分重试,当天最多 4 次RtnCode==1 → SimulatePaid!=1 → TradeAmt==订单 amount → trade_no 幂等Cashier/QueryTradeInfo/V5(凭证验证 / 补单)https://payment-stage.funpoint.com.tw/Cashier/QueryTradeInfo/V5;正式 https://payment.funpoint.com.tw/Cashier/QueryTradeInfo/V5MerchantID、MerchantTradeNo、TimeStamp(3 分钟内)、PlatformID?、CheckMacValueTradeStatus(0 未付/1 已付/10200095 失败)、TradeNo、TradeAmt、PaymentDate、PaymentType、TradeDate、CheckMacValue…CreditDetail/DoAction(US4)https://payment.funpoint.com.tw/CreditDetail/DoAction(stage 不可用)MerchantID、MerchantTradeNo、TradeNo、Action(C 關帳/R 退刷/E 取消/N 放棄)、TotalAmount、CheckMacValue、PlatformID?RtnCode(1=成功)、RtnMsg已關帳→R、已授權→N、要關帳→E 再 N 或 R;分期必须全额;自动关帳开启时避开 20:15–20:30POST /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{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=Credit, UnionPay=2, EncryptType=1, ItemName, CheckMacValue, ... },前端构建隐藏 form 自动 submit 到 gatewayUrlPOST /pay/omg/notify(US2,@Anonymous)1|OKPOST /pay/omg/result(US2,@Anonymous)OrderResultURL 接收 OMG Client POST,使用完整 DTO 严格解析并按支付尝试凭证快照验签;未知额外字段和空值全部进入 CheckMacValue。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。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 但订单未核销(跨事务中断残留),补推订单状态,避免「付了钱订单永远未支付」
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/system/storeOmg/*(US5,@PreAuthorize)GET /list(分页筛选)、GET /{storeId}、PUT /apply/{storeId}、PUT /saveCredentials(录入+QueryTradeInfo 探测验证)、PUT /toggleEnable/{storeId}、PUT /reset/{storeId}、PUT /enabledPayments/{storeId}PosStoreNewebpayController 模式,但独立 Controller/Service/实体/表OmgReconcileTask(US6 方案B,@Scheduled + Redisson)@Scheduled(fixedDelay) 默认每 3 分钟一轮(omg.reconcile.fixed-delay-ms),上一轮跑完才开始计时,启动延迟 60spaymentService.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重试留送达时间,窗口外停止扫描避免无限轮询。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}本期固定 Credit,并传 UnionPay=2 隐藏银联,只保留普通信用卡与 Apple Pay。OMG 官方将 Apple Pay 归在 Credit 下;回覆 PaymentType 同为 Credit_CreditCard,仅凭 PaymentType 无法区分两者(本期不区分)。
PaymentType,落 pos_order_omg_payment.pay_type)| PaymentType | 名称 | 即时/延期 |
|---|---|---|
Credit_CreditCard |
信用卡(含 Apple Pay;请求以 UnionPay=2 隐藏银联) |
即时 |
本项目当前只接受这一种回覆类型;开发测试阶段没有延期支付历史交易需要兼容。
1 = 成功;其余均为失败。完整代码表文档为图片且持续新增,须到「歐買尬廠商後台 → 系統開發管理 → 交易狀態代碼查詢」查全。
实现策略:RtnCode==1 视成功(核销 / 退款成功);非 1 一律失败,记录 rtn_code+rtn_msg 供诊断,不硬编码错误码分支。
.NET 风格 URLEncode。Java 实现:URLEncoder.encode(s, UTF_8)(空格已→+)后,照 PHP 范例做替换以对齐 .NET,再转小写、SHA256、hex 大写:
- _ . ! * ( );~→%7e(两侧同)。+(.NET/Java 表单编码一致;标准 URLEncode 是 %20,勿混用)。@ # $ % ^ & = + ; ? / \ > < [ ] { } : ' " , | 均为 %XX;中文按 UTF-8 %XX。%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)。