# 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`。

**自测**：仓库保留 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_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`）。

---

## 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`（历史字段保留；运行时固定 Credit）、`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/超商虚帐/缴费码回调
```

`InvoiceMark=N` 固定（发票走 ezPay，与支付解耦）；`EncryptType=1` 固定。

---

## D9. payType 取值

**Decision**：`PosOrder.payType` 使用 `"2"` = OMG 在线支付；具体方式由 `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**（请求，2026-08-13 调整）：新订单固定 `Credit` 并传 `UnionPay=2`，只显示信用卡与 Apple Pay；不保留其他支付方式的历史兼容入口。
- **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。
