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=ALL、CheckMacValue、EncryptType=1TradeDesc、ItemName(# 分隔)、OrderResultURL、PaymentInfoURL、ClientRedirectURL、NeedExtraPaidInfo(建议 Y,供退款查 gwsr)、CustomField1–4、LanguageInvoiceMark=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 幂等/pay/omg/paymentInfo,返回虚帐/缴费码(BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate 等,详见文档 02 页),带 CheckMacValue 需验签1|OKCashier/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;分期必须全额;ATM/CVS/BarcodeATM 不适用(走人工);自动关帳开启时避开 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="7"/pay_url{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=ALL, EncryptType=1, ItemName, CheckMacValue, ... },前端构建隐藏 form 自动 submit 到 gatewayUrlPOST /pay/omg/notify(US2,@Anonymous)1|OKGET|POST /pay/omg/return(US2,@Anonymous)POST /pay/omg/paymentInfo(US3,@Anonymous)1|OKPOST /pay/omg/query(US6 方案A,@Anonymous @Auth,Header token)orderid(= ddId)token → userId == order.userId);order.payType 必须为 "7"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 补单 + 返回 {1, true};10200095 失败 → markFail + 返回 {2, false};0 未付(延期支付付款前常态) 或其他未知值 → 保持未支付 + 返回 {0, false}(前端继续轮询 / 定时任务下轮再查)
自愈:若流水已 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 被动补单同一核销链路)window-hours=168(7 天),覆盖延期支付(ATM/超商) ExpireDate(1~3 天);grace-minutes=2 给回调/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}本期固定 ALL(聚合全部)。可细分值:Credit(信用卡/銀聯/Apple Pay)、ATM(FIRST/CHINATRUST/UBOT/KGI)、CVS(CVS/FAMILY/IBON/HILIFE)、BarcodeATM(CHINATRUST)、AFTEE。
⚠️ Apple Pay 不是独立 ChoosePayment,归在 Credit 下(付款页内选);回覆 PaymentType 同为 Credit_CreditCard,仅凭 PaymentType 无法区分信用卡与 Apple Pay(本期不区分)。
PaymentType,落 pos_order_omg_payment.pay_type)| PaymentType | 名称 | 即时/延期 |
|---|---|---|
Credit_CreditCard |
信用卡(含銀聯/Apple Pay) | 即时 |
BarcodeATM_CHINATRUST |
超商快付代碼繳費 | 即时 |
ATM_FIRST / ATM_CHINATRUST / ATM_UBOT / ATM_KGI |
各银行 ATM | 延期(取虚帐→期限内付款) |
CVS_CVS / CVS_FAMILY / CVS_IBON / CVS_HILIFE |
超商代碼繳費 | 延期 |
AFTEE_AFTEE |
AFTEE 先享後付 | 延期 |
延期方式(ATM/CVS/AFTEE)走 PaymentInfoURL 取号 → US3。
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)。