# 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.*`、`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`。这些是平台公共能力,不属于蓝新。 --- ## 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 `%XX`。**Java 实现**:`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`。 **自测**:用官方示例 HashKey=`5294y06JbISpM5x9`、HashIV=`v77hoKGq4kWxNNIS` 的参数复算,应得 `AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266`(`OmgCheckMacValue.main` 自测)。 --- ## D2. 支付流水表:新建 pos_order_omg_payment(不复用蓝新的 pos_order_payment) **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`(发起 ALL)、`pay_type`(回调方式 Credit/ATM/CVS/ApplePay/BarcodeATM/AFTEE)、`amount`、`rtn_code`、`rtn_msg`、`pay_status`(0 未支付/1 已支付/2 失败/3 已退款)、`auth_code`、`pay_time`(PaymentDate)、`trade_date`、`callback_raw`、审计时间。唯一索引 `trade_no`。 **退款**:新建 `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_status`、`is_enabled`、`merchant_id`、`hash_key`、`hash_iv`、`enabled_payments`(本期固定 ALL,字段保留以备收敛)、`apply_time`、`approved_time`、`last_verify_result`、`remark` + 审计字段。 --- ## 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 → 按 `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 不再回头补。 --- ## D6. MerchantTradeNo 生成规则 **Decision**:`MerchantTradeNo = "OMG" + ddId`(ddId 清洗为英数,≤20 字元),前缀区别蓝新 `"NB"`;重新发起追加时间戳后缀,旧记录作废。OMG 要求英数大小写混合、≤20。 --- ## D7. 退款(本期 D3=做):DoAction,仅信用卡;ATM/超商人工 **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。 --- ## D8. 环境配置:application.yml 新增 omg 段 ```yaml 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 取值 **Decision**:`PosOrder.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_CreditCard`、`BarcodeATM_CHINATRUST`(即时);`ATM_FIRST/CHINATRUST/UBOT/KGI`、`CVS_CVS/FAMILY/IBON/HILIFE`、`AFTEE_AFTEE`(延期,走 PaymentInfoURL 取号 → US3)。 - **RtnCode**:`1`=成功,其余失败;完整表为**图片**且持续新增,须查「歐買尬廠商後台→系統開發管理→交易狀態代碼查詢」。实现:`==1` 成功核销/退款成功,非 1 失败并记 `rtn_code`+`rtn_msg`,**不硬编码错误码分支**。 - **URLEncode(CheckMacValue)**:照 reference 05 的 .NET 表,Java 精确实现见 contracts/api.md §C4 与上文 D1 step4。