research.md 11 KB

Research: OMG(歐買尬/FunPoint)AIO 支付接入

Feature: specs/016-omg-payment/spec.md Date: 2026-07-29

本文记录 OMG 支付接入的关键技术决策、理由与备选。所有外部接口字段已对照 OMG 官方文档(developers.omg.com.tw/payment/aio/)确认。


0. 总体约束:OMG 全部独立实现,不复用蓝新(NewebPay)代码

Decision:OMG 支付的所有代码(签名工具、HTTP 客户端、实体、表、Service、Controller)均为 OMG 独立新建不得 import 或依赖任何蓝新金流代码/表,包括:com.ruoyi.app.utils.newebpay.*NewebPayEncryptUtilNewebPayNewebpayPayControllerPosStoreNewebpay*、以及蓝新建的流水表 pos_order_payment

Rationale:用户明确要求。蓝新(011)代码就绪但从未上线、本期 D2 被 OMG 替代且不再启用;让 OMG 与蓝新在代码与数据上彻底解耦,避免被废弃代码牵连,便于将来直接删除蓝新代码。OMG 与蓝新结构相似仅因同属「幕前 AIO 支付」问题域(API 形态决定),属独立重写而非复用。

允许使用的是「平台共享基础设施」(非蓝新代码)PosOrder/IPosOrderService(订单状态机)、PayPush + PushEventService(推送)、OrderLogHelper(订单日志)、sendAcceptRiderPush(可接单骑手推送)、IpnLog(通用 IPN 日志)、InfoUser@Anonymous/PermitAllUrlProperties。这些是平台公共能力,不属于蓝新。


D1. 签名工具:新建 OmgCheckMacValue(CheckMacValue / SHA256),独立于蓝新

Decision:新建 ruoyi-admin/.../app/utils/omg/OmgCheckMacValue.java(静态,CheckMacValue 生成 + 校验)+ OmgPay.java(HTTP 客户端)+ OmgPayConfig.java(MerchantID/HashKey/HashIV),独立包 utils/omg,不依赖 newebpay

算法(EncryptType=1,SHA256):

  1. 去掉 CheckMacValue 本身;其余参数按 key 字母序升序排列。
  2. k1=v1&k2=v2...
  3. 包夹:HashKey={key}&{query}&HashIV={iv}
  4. .NET 风格 URL 编码(见 reference 05 表):- _ . ! * ( ) 不编码、~%7e、空格→+、其余特殊符 %XX、中文 UTF-8 %XXJava 实现URLEncoder.encode(s, UTF_8)(空格已→+)后,照 PHP 范例替换 %2d→- %5f→_ %2e→. %21→! %2a→* %28→( %29→)(Java 本就不编 -_.!*() 需替换回),再转小写。
  5. 整串转小写
  6. SHA256 → hex 转大写 = CheckMacValue。
  7. 校验:对回调参数同算法重算,与回传 CheckMacValue 比对,不一致即拒绝。

与蓝新的本质区别:蓝新是 AES-256-CBC 加密 TradeInfo + SHA256 TradeSha,回调需 AES 解密;OMG 无 AES、无解密,请求与回调都是「明文参数 + 单个 CheckMacValue」。实现更简单,故绝不复用 NewebPayEncryptUtil

自测:仓库保留 ECPay 官方 MD5 向量锁定排序与 .NET URL 编码流水线;旧草稿中的 SHA256 期望值与其参数集不一致,已不再作为自测断言。OMG SHA256 应使用当前商户凭证在 stage/正式环境做端到端验签。


D2. 支付流水表:新建 pos_order_omg_payment(不复用蓝新的 pos_order_payment)

Decision:新建 pos_order_omg_payment(每笔 OMG 交易一条,按 trade_no = OMG TradeNo 幂等),不复用 011 的 pos_order_payment(复用即复用蓝新代码/数据,违背约束 0)。

字段dd_idmerchant_trade_no(商店交易编号 MerchantTradeNo,发起生成,唯一索引)、trade_no(OMG 交易编号 TradeNo,回调获得,幂等键,唯一索引)、store_idmerchant_idchoose_payment(发起 ALL)、pay_type(回调方式 Credit/ATM/CVS/ApplePay/BarcodeATM/AFTEE)、amountrtn_codertn_msgpay_status(0 未支付/1 已支付/2 失败/3 已退款/4 退款中)、auth_codepay_time(PaymentDate)、trade_datecallback_raw、审计时间。

退款:新建 pos_order_omg_refund(每次 DoAction 一行:payment_id/trade_no/action/amount/rtn_code/rtn_msg/callback_raw/time),与流水解耦,支持多次动作与对账。

幂等:回调按 trade_no 查重;已 pay_status=1 则跳过更新(仍回 1|OK)。


D3. 门店凭证表:新建 pos_store_omg(独立于蓝新凭证表)

Decision:新建 pos_store_omg(与 pos_store 1:1),状态机沿用 009/011 的「0 未申请 / 1 申请中 / 2 已开通 + is_enabled」模式,但独立表独立字段,不复用 pos_store_newebpay。字段:store_id(UK)、omg_statusis_enabledmerchant_idhash_keyhash_ivenabled_payments(本期固定 ALL,字段保留以备收敛)、apply_timeapproved_timelast_verify_resultremark + 审计字段。


D4. 凭证联网验证:用 OMG QueryTradeInfo/V5 探测

Decision:录入凭证后调 OMG Cashier/QueryTradeInfo/V5(stage 可用)查一个虚构 MerchantTradeNo 探测金钥。金钥/商店代号错误 → 响应状态非正常且 Message 提示金钥/商店;金钥正确 → 返回「无此交易」类结果(TradeStatus 异常但非金钥错误)。验证逻辑放 Controller 层(同 011/009 思路,避免 ruoyi-system 反向依赖 ruoyi-admin 工具类),Service 仅持久化结果。


D5. 回调链路:OMG Controller 独立实现,复用平台共享推送基础设施

Decision:新建 OmgPayController/pay/omg/*),/pay/omg/notify@Anonymous。流程:collectForm → IpnLog → 按 MerchantIDpos_store_omg 凭证 → 验签 CheckMacValue(无需解密) → 幂等(trade_no)→ 金额校验(TradeAmt)→ RtnCode==1 且 SimulatePaid!=1 → markSuccess → 更新订单(state=0payStatus=1)+ 订单日志 + PayPush/PushEventService 推送用户/商家/骑手 + sendAcceptRiderPush 推送可接单骑手

回应差异:OMG 要求回应纯字符串 1|OK(蓝新是 JSON {Status:SUCCESS},不同),未收到会在 5–15 分钟后重试,当天最多 4 次。

sendAcceptRiderPush 迁移:CLAUDE.md 记「sendAcceptRiderPush 迁移推迟到接入新支付时再做」——OMG 即新支付,本期在 OmgPayController 内完成该推送接线(复用 PosOrderController 货到付款路径同款调用);蓝新侧的空 TODO 不再回头补。


D6. MerchantTradeNo 生成规则

DecisionMerchantTradeNo = "OMG" + ddId(ddId 清洗为英数,≤20 字元),前缀区别蓝新 "NB";重新发起追加时间戳后缀,旧记录作废。OMG 要求英数大小写混合、≤20。


D7. 退款(本期 D3=做):DoAction,仅信用卡;ATM/超商人工

Decision:订单取消时,信用卡订单调 /CreditDetail/DoAction,Action 按状态:已關帳→R(退刷)已授權→N(放棄)要關帳→E(取消)再 N 或 R全额退款(分期必须全额);写入 pos_order_omg_refundATM/超商/BarcodeATM 无退款 API → 记录待人工在盘合后台处理。

Action 选择依赖状态:需先 CreditDetail/QueryTrade/V2 查信用卡状态(需下单时 NeedExtraPaidInfo=Ygwsr + 盘合后台 CreditCheckCode)。MVP 可:已支付订单统一先尝试 R(退刷),失败再按状态分支。测试环境 DoAction 不可用 → 退款联调需正式小额或 mock。


D8. 环境配置:application.yml 新增 omg 段

omg:
  base-url: https://payment-stage.funpoint.com.tw      # 测试 stage / 正式 payment.funpoint.com.tw
  return-url: https://<公网域名>/pay/omg/notify         # 服务端回调(须回 1|OK)
  order-result-url: https://<前端>/pay-result           # 客户端跳转(可选,即时方式)
  payment-info-url: https://<公网域名>/pay/omg/paymentInfo  # ATM/超商虚帐/缴费码回调
  client-redirect-url: https://<前端>/payment-info       # ATM/超商客户端展示

InvoiceMark=N 固定(发票走 ezPay,与支付解耦);EncryptType=1 固定。


D9. payType 取值

DecisionPosOrder.payType 新增 "7" = OMG 在线支付(蓝新 "6",OMG "7",独立值便于辨识与过渡);具体方式由 pos_order_omg_payment.pay_type 在回调后承载。不改 pos_order 表结构(代码常量体现)。


D10. 测试环境

stage payment-stage.funpoint.com.tw + 测试商店凭证/测试卡(文档附录)。NotifyURL 经内网穿透(ngrok/frp)暴露。链路:录入测试凭证 → 下单 → 发起 OMG → 测试卡付款 → 回调 → 订单已支付 + 推送。退款:stage DoAction 不可用,需正式小额或 mock。


D11. 数据库变更管理

pos_store_omg / pos_order_omg_payment / pos_order_omg_refund 建表 DDL 写入 updatesql/sql.md(标注日期与用途),不直接执行,由开发者统一手动执行(遵循项目规范)。pos_order 不改结构。


D12. 残留待确认(不阻塞 plan 推进,留待 tasks/实现)

  • NeedExtraPaidInfo 是否本期开启(影响退款状态查询与 gwsr)——建议开启。
  • CreditCheckCode 来源(盘合后台)与是否配置化(影响信用卡明细查询/退款 Action 判定)。
  • OrderResultURL/PaymentInfoURL 对应前端页是否本期做(US3 ATM/超商展示依赖前端页)。
  • 自动关帳(每日自動關帳)是否开启(影响 DoAction 调用时段,避开 20:15–20:30)。

D13. 代码表与枚举(reference 02–05,2026-07-29 补读)

  • ChoosePayment(请求):本期 ALL;细分 Credit(信用卡/銀聯/Apple Pay)/ATM(FIRST/CHINATRUST/UBOT/KGI)/CVS(CVS/FAMILY/IBON/HILIFE)/BarcodeATM(CHINATRUST)/AFTEE。
  • Apple Pay 归在 Credit 下(非独立 ChoosePayment);回覆 PaymentType 为 Credit_CreditCard仅凭 PaymentType 无法区分信用卡与 Apple Pay(本期不区分)。
  • 回覆 PaymentType(回调,落 pos_order_omg_payment.pay_type):Credit_CreditCardBarcodeATM_CHINATRUST(即时);ATM_FIRST/CHINATRUST/UBOT/KGICVS_CVS/FAMILY/IBON/HILIFEAFTEE_AFTEE(延期,走 PaymentInfoURL 取号 → US3)。
  • RtnCode1=成功,其余失败;完整表为图片且持续新增,须查「歐買尬廠商後台→系統開發管理→交易狀態代碼查詢」。实现:==1 成功核销/退款成功,非 1 失败并记 rtn_code+rtn_msg不硬编码错误码分支
  • URLEncode(CheckMacValue):照 reference 05 的 .NET 表,Java 精确实现见 contracts/api.md §C4 与上文 D1 step4。