Feature: specs/016-omg-payment/spec.md Date: 2026-07-29
本文记录 OMG 支付接入的关键技术决策、理由与备选。所有外部接口字段已对照 OMG 官方文档(developers.omg.com.tw/payment/aio/)确认。
Decision:OMG 支付的所有代码(签名工具、HTTP 客户端、实体、表、Service、Controller)均为 OMG 独立新建,不得 import 或依赖任何蓝新金流代码/表,包括:com.ruoyi.app.utils.newebpay.*、NewebPayEncryptUtil、NewebPay、NewebpayPayController、PosStoreNewebpay*、以及蓝新建的流水表 pos_order_payment。
Rationale:用户明确要求。蓝新(011)代码就绪但从未上线、本期 D2 被 OMG 替代且不再启用;让 OMG 与蓝新在代码与数据上彻底解耦,避免被废弃代码牵连,便于将来直接删除蓝新代码。OMG 与蓝新结构相似仅因同属「幕前 AIO 支付」问题域(API 形态决定),属独立重写而非复用。
允许使用的是「平台共享基础设施」(非蓝新代码):PosOrder/IPosOrderService(订单状态机)、PayPush + PushEventService(推送)、OrderLogHelper(订单日志)、sendAcceptRiderPush(可接单骑手推送)、IpnLog(通用 IPN 日志)、InfoUser、@Anonymous/PermitAllUrlProperties。这些是平台公共能力,不属于蓝新。
Decision:新建 ruoyi-admin/.../app/utils/omg/OmgCheckMacValue.java(静态,CheckMacValue 生成 + 校验)+ OmgPay.java(HTTP 客户端)+ OmgPayConfig.java(MerchantID/HashKey/HashIV),独立包 utils/omg,不依赖 newebpay。
算法(EncryptType=1,SHA256):
CheckMacValue 本身;其余参数按 key 字母序升序排列。k1=v1&k2=v2...。HashKey={key}&{query}&HashIV={iv}。- _ . ! * ( ) 不编码、~→%7e、空格→+、其余特殊符 %XX、中文 UTF-8 %XX。Java 实现:URLEncoder.encode(s, UTF_8)(空格已→+)后,照 PHP 范例替换 %2d→- %5f→_ %2e→. %21→! %2a→* %28→( %29→)(Java 本就不编 -_.,!*() 需替换回),再转小写。与蓝新的本质区别:蓝新是 AES-256-CBC 加密 TradeInfo + SHA256 TradeSha,回调需 AES 解密;OMG 无 AES、无解密,请求与回调都是「明文参数 + 单个 CheckMacValue」。实现更简单,故绝不复用 NewebPayEncryptUtil。
自测:仓库保留 ECPay 官方 MD5 向量锁定排序与 .NET URL 编码流水线;旧草稿中的 SHA256 期望值与其参数集不一致,已不再作为自测断言。OMG SHA256 应使用当前商户凭证在 stage/正式环境做端到端验签。
Decision:新建 pos_order_omg_payment(每笔 OMG 交易一条,按 trade_no = OMG TradeNo 幂等),不复用 011 的 pos_order_payment(复用即复用蓝新代码/数据,违背约束 0)。
字段:dd_id、merchant_trade_no(商店交易编号 MerchantTradeNo,发起生成,唯一索引)、trade_no(OMG 交易编号 TradeNo,回调获得,幂等键,唯一索引)、store_id、merchant_id、choose_payment(固定 Credit)、pay_type(回调为 Credit)、amount、rtn_code、rtn_msg、pay_status(0 未支付/1 已支付/2 失败/3 已退款/4 退款中)、auth_code、pay_time(PaymentDate)、trade_date、callback_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)。
Decision:新建 pos_store_omg(与 pos_store 1:1),状态机沿用 009/011 的「0 未申请 / 1 申请中 / 2 已开通 + is_enabled」模式,但独立表独立字段,不复用 pos_store_newebpay。字段:store_id(UK)、omg_status、is_enabled、merchant_id、hash_key、hash_iv、enabled_payments(历史字段保留;运行时固定 Credit)、apply_time、approved_time、last_verify_result、remark + 审计字段。
Decision:录入凭证后调 OMG Cashier/QueryTradeInfo/V5(stage 可用)查一个虚构 MerchantTradeNo 探测金钥。金钥/商店代号错误 → 响应状态非正常且 Message 提示金钥/商店;金钥正确 → 返回「无此交易」类结果(TradeStatus 异常但非金钥错误)。验证逻辑放 Controller 层(同 011/009 思路,避免 ruoyi-system 反向依赖 ruoyi-admin 工具类),Service 仅持久化结果。
Decision:新建 OmgPayController(/pay/omg/*),/pay/omg/notify 加 @Anonymous。流程:collectForm → IpnLog → 按 MerchantID 查 pos_store_omg 凭证 → 验签 CheckMacValue(无需解密) → 幂等(trade_no)→ 金额校验(TradeAmt)→ RtnCode==1 且 SimulatePaid!=1 → markSuccess → 更新订单(state=0,payStatus=1)+ 订单日志 + PayPush/PushEventService 推送用户/商家/骑手 + sendAcceptRiderPush 推送可接单骑手。
回应差异:OMG 要求回应纯字符串 1|OK(蓝新是 JSON {Status:SUCCESS},不同),未收到会在 5–15 分钟后重试,当天最多 4 次。
sendAcceptRiderPush 迁移:CLAUDE.md 记「sendAcceptRiderPush 迁移推迟到接入新支付时再做」——OMG 即新支付,本期在 OmgPayController 内完成该推送接线(复用 PosOrderController 货到付款路径同款调用);蓝新侧的空 TODO 不再回头补。
Decision:MerchantTradeNo = "OMG" + ddId(ddId 清洗为英数,≤20 字元),前缀区别蓝新 "NB";重新发起追加时间戳后缀,旧记录作废。OMG 要求英数大小写混合、≤20。
Decision:订单取消时,信用卡订单调 /CreditDetail/DoAction,Action 按状态:已關帳→R(退刷)、已授權→N(放棄)、要關帳→E(取消)再 N 或 R,全额退款(分期必须全额);写入 pos_order_omg_refund。ATM/超商/BarcodeATM 无退款 API → 记录待人工在盘合后台处理。
Action 选择依赖状态:需先 CreditDetail/QueryTrade/V2 查信用卡状态(需下单时 NeedExtraPaidInfo=Y 拿 gwsr + 盘合后台 CreditCheckCode)。MVP 可:已支付订单统一先尝试 R(退刷),失败再按状态分支。测试环境 DoAction 不可用 → 退款联调需正式小额或 mock。
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/超商虚帐/缴费码回调
InvoiceMark=N 固定(发票走 ezPay,与支付解耦);EncryptType=1 固定。
Decision:PosOrder.payType 使用 "2" = OMG 在线支付;具体方式由 pos_order_omg_payment.pay_type 在回调后承载。不改 pos_order 表结构(代码常量体现)。
stage payment-stage.funpoint.com.tw + 测试商店凭证/测试卡(文档附录)。NotifyURL 经内网穿透(ngrok/frp)暴露。链路:录入测试凭证 → 下单 → 发起 OMG → 测试卡付款 → 回调 → 订单已支付 + 推送。退款:stage DoAction 不可用,需正式小额或 mock。
pos_store_omg / pos_order_omg_payment / pos_order_omg_refund 建表 DDL 写入 updatesql/sql.md(标注日期与用途),不直接执行,由开发者统一手动执行(遵循项目规范)。pos_order 不改结构。
NeedExtraPaidInfo 是否本期开启(影响退款状态查询与 gwsr)——建议开启。CreditCheckCode 来源(盘合后台)与是否配置化(影响信用卡明细查询/退款 Action 判定)。OrderResultURL/PaymentInfoURL 对应前端页是否本期做(US3 ATM/超商展示依赖前端页)。Credit 并传 UnionPay=2,只显示信用卡与 Apple Pay;不保留其他支付方式的历史兼容入口。Credit_CreditCard,仅凭 PaymentType 无法区分信用卡与 Apple Pay(本期不区分)。pos_order_omg_payment.pay_type):Credit_CreditCard、BarcodeATM_CHINATRUST(即时);ATM_FIRST/CHINATRUST/UBOT/KGI、CVS_CVS/FAMILY/IBON/HILIFE、AFTEE_AFTEE(延期,走 PaymentInfoURL 取号 → US3)。1=成功,其余失败;完整表为图片且持续新增,须查「歐買尬廠商後台→系統開發管理→交易狀態代碼查詢」。实现:==1 成功核销/退款成功,非 1 失败并记 rtn_code+rtn_msg,不硬编码错误码分支。