# Research: 蓝新金流(NewebPay)线上支付接入

**Feature**: specs/011-newebpay-payment/spec.md
**Date**: 2026-06-22

本文记录实现蓝新 MPG 幕前支付所需的关键技术决策、理由与备选。所有决策已基于现有系统（ezPay 发票实现、PayController 回调链路、PosOrder 字段）与蓝新官方手册 NDNF-1.2.2。

---

## D1. 加密工具：新建独立 NewebPayEncryptUtil，不复用 EzPayEncryptUtil

**Decision**: 新建 `ruoyi-admin/.../app/utils/newebpay/NewebPayEncryptUtil.java` + `NewebPay.java`（HTTP 客户端）+ `NewebPayConfig.java`，与 ezPay 工具类并列，不抽取公共加密套件。

**Rationale**:
- 蓝新加密是**标准 AES-256-CBC + PKCS7Padding（块 16 字节）**，输出 hex；ezPay 是 AES/CBC/NoPadding + 手动 PKCS7（**块 32 字节**，ezPay 特殊）。两者填充块大小不同，加密实现无法共用。
- 蓝新的检查码语义：`TradeSha = SHA256("HashKey={key}&{tradeInfoHex}&HashIV={iv}")` 大写（请求时带）；`CheckCode = SHA256("HashIV={iv}&{Amt=&MerchantID=&MerchantOrderNo=&TradeNo= 排序}&HashKey={key}")` 大写（校验响应）；`CheckValue = SHA256("IV={iv}&{Amt=&MerchantID=&MerchantOrderNo= 排序}&Key={key}")` 大写（查询请求）。与 ezPay 的 CheckValue/CheckCode 形似但字段与前缀不同。
- 第一期使用 `EncryptType=0`（默认 AES/CBC/PKCS7Padding），不上 AES/GCM，降低复杂度。
- 不抽取公共套件遵循「简单优先」——两者细节差异大，强行抽象会引入间接层且只有两个使用点。

**Alternatives**:
- 抽取 `PaymentCryptoUtil` 公共类：被否，因填充块大小与检查码字段差异使抽象收益低、误用风险高。
- 复用 EzPayEncryptUtil：被否，块大小不同会导致蓝新解密失败。

---

## D2. MPG 幕前跳转：后端生成加密参数，前端 Form Post 跳转蓝新

**Decision**: 新增「发起支付」接口返回 `{ gatewayUrl, MerchantID, TradeInfo, TradeSha, Version, EncryptType }`；前端用隐藏 form 自动 `submit` 跳转至 `https://(c)core.newebpay.com/MPG/mpg_gateway`。

**Rationale**:
- MPG 是**幕前**支付，必须由用户浏览器跳转到蓝新付款页（NDNF §3.1、§4.1.3 Form Post）。后端不能直接 HTTP 调用完成付款。
- 与现有 VNPay 的 `payUrl`（单个跳转 URL）模式不同——蓝新需 POST 多个加密字段。故不复用 `PosOrder.payUrl` 存 URL，而是接口返回 form 参数；`payUrl` 字段保留存 gatewayUrl 供前端兜底/记录。
- 后端生成参数时机：用户下单后、点击「去支付」时调用发起接口。

**Alternatives**:
- 后端直接返回自动 submit 的 HTML 页面（蓝新 PHP 官方示例做法）：被否，前端是 Vue SPA，返回 HTML 与现有交互不一致；改为接口返回 form 字段由前端构建更贴合。
- 前端自行加密：被否，HashKey/HashIV 是敏感凭证，不能下发前端。

---

## D3. 凭证存储：新建 pos_store_newebpay 表（与 pos_store 1:1），独立于发票凭证

**Decision**: 新建 `pos_store_newebpay`，与 `pos_store` 一对一，结构与状态机复用 009 `pos_store_ezpay` 模式，但独立表、独立字段。

**Rationale**:
- 支付与发票是两条独立业务线，凭证、开通流程、启用开关各自独立；混表会造成字段语义混乱、停用相互影响。
- 复用 009 的「未申请 0 / 申请中 1 / 已开通 2 + is_enabled」状态机与 Controller/Service 分层（Service 纯 DB，联网验证在 Controller 层，避免 ruoyi-system 反向依赖 ruoyi-admin 工具类）。
- 额外字段 `enabled_payments`：记录该门店启用的支付方式组合（信用卡/LINE Pay/Apple Pay），发起 MPG 时据此设置 CREDIT/LINEPAY/APPLEPAY 开关。

**Alternatives**:
- 复用 pos_store_ezpay 加列：被否，语义混淆、发票停用会误伤支付。
- 全局 application.yml 单组凭证：被否，用户已选定门店级。

---

## D4. 凭证联网验证：用单笔查询 QueryTradeInfo 探测

**Decision**: 录入凭证后调蓝新 `QueryTradeInfo`（查一个不存在的订单号），根据返回判断凭证是否有效。

**Rationale**:
- 蓝新无等价的「纯凭证校验」接口。QueryTradeInfo 用 CheckValue（Amt/MerchantID/MerchantOrderNo）签名，若 HashKey/HashIV/MerchantID 错误，蓝新返回商店/金钥相关错误码（如 `MPG01001` 系列、Status≠SUCCESS 且 Message 提示金钥）；若凭证正确则返回「交易不存在」类结果（Status≠SUCCESS 但 Message 为查询无资料）。
- 与 009 ezPay 用只读 invoice_search 验证金钥的思路一致（ezPay 靠响应含 `KEY1` 判定金钥错误）。
- 验证逻辑放 Controller 层（同 009），Service 仅持久化结果。

**Alternatives**:
- MPG 测试环境发起真实小额交易再取消：被否，流程重、产生垃圾交易。
- 不验证直接保存：被否，错误凭证会导致所有交易失败，需录入即校验。

---

## D5. 支付流水表：新建 pos_order_payment 承载幂等与对账

**Decision**: 新建 `pos_order_payment`，每笔蓝新交易一条，回调按 `trade_no`（蓝新交易序号）幂等。

**Rationale**:
- `PosOrder.payStatus` 只能存最终状态，无法承载 TradeNo、支付方式、授权码、原始回调、多次发起。蓝新要求按 TradeNo 幂等（NotifyURL 可能重试），必须有专门表。
- 字段：dd_id、merchant_order_no、trade_no（蓝新交易序号，幂等键）、store_id、merchant_id、pay_type(CREDIT/LINEPAY/APPLEPAY)、amount、pay_status、auth_code、pay_time、callback_raw、create_time、update_time。
- 同一订单可有多条（多次发起支付，每次新 merchant_order_no），但成功回调只处理一次（按 trade_no）。

**Alternatives**:
- 用 PosOrder 字段 + Redis 幂等标记：被否，无法留存对账明细与多次发起记录。
- 用现有 IpnLog：被否，IpnLog 是通用日志无结构化字段、无 trade_no 索引。

---

## D6. 商店订单号 MerchantOrderNo 生成规则

**Decision**: `merchant_order_no = "NB" + ddId`（ddId 为系统订单号），保证门店内唯一、格式合法（英数+下划线）、可由 ddId 反查订单。

**Rationale**:
- 蓝新要求同 MerchantID 下 MerchantOrderNo 不重复、限英数+下划线、≤30 字元。
- ddId 全局唯一 ⇒ 加固定前缀 "NB" 后门店内必然唯一；前缀便于识别蓝新订单、避免与其他渠道订单号冲突。
- 实现时需确认 ddId 不含 `-`/`/` 等非法字符；若含则清洗（替换为 `_`）。
- 重新发起支付时生成新 merchant_order_no（追加时间戳后缀），旧记录作废。

**Alternatives**:
- 直接用 ddId：可行但缺渠道标识，调试时难辨来源；加前缀更稳。
- 独立序列号：被否，需额外维护映射，ddId 已满足唯一性。

---

## D7. NotifyURL 回调处理链路：复用 PayController.payipn 业务链路

**Decision**: 新建 `NewebpayPayController`（`/pay/newebpay/*`），回调接口 `/pay/newebpay/notify` 加 `@Anonymous`；处理流程复用现有 `PayController.payipn` 的业务链路（IPN 日志 → 解密验签 → 金额校验 → 幂等 → 更新 payStatus → 订单日志 → 推送），仅替换加解密/字段层。

**Rationale**:
- `PayController.payipn` 已实现完整的「支付成功后业务动作」：`orderLogHelper.logSync` 记日志、`userBillingService` 记账、`push.apppush/shpush` + `pushEventService.PublisherEvent` 推送用户/商家/骑手、`sendAcceptRiderPush` 推送可接单骑手。蓝新回调成功后必须触发相同动作，否则订单虽已支付但商家/骑手收不到通知。
- `@Anonymous` 注解经 `PermitAllUrlProperties` 自动加入 SecurityConfig 匿名白名单（蓝新服务器无登录态）。
- 新建独立 Controller 而非塞入已 440 行的 PayController，保持单一职责、便于维护。
- 订单状态更新语义沿用 payipn：`state` 保持不变（0 待处理，支付不改业务状态）、`payStatus=1`（已支付）。

**Alternatives**:
- 抽取公共「支付成功处理」方法供 VNPay/蓝新共用：理想但 VNPay 代码耦合度高、改造范围大，第一期不做（surgical changes）；仅复用其调用的组件（orderLogHelper/pushEventService 等）。
- 在 PayController 加 /pay/newebpay/notify 方法：被否，Controller 已臃肿。

---

## D8. payType 取值与支付方式承载

**Decision**: `PosOrder.payType` 发起时设固定值表示「蓝新在线支付」（取 `"6"`），具体支付方式(CREDIT/LINEPAY/APPLEPAY) 由 `pos_order_payment.pay_type` 在回调后承载；订单列表展示支付方式时联查 pos_order_payment。

**Rationale**:
- MPG 幕前：发起交易时用户**尚未选择**支付方式（在蓝新付款页才选），故发起瞬间只能知道「走蓝新」，无法预知信用卡还是 LINE Pay。
- payType 历史取值 1-5（越南到付/vnpay/zalopay/银行卡），台湾已不用；新增 `"6"` 表示蓝新，避免语义冲突。
- 回调到达后明确方式，写入 pos_order_payment；订单列表/详情需展示具体方式时由该表提供。

**Alternatives**:
- 回调后把 payType 更新为细分(7/8/9)：增加枚举且与「发起时未知方式」矛盾，被否。

---

## D9. 环境配置：application.yml 新增 newebpay 段

**Decision**: 仿 `ezpay.base-url` 模式新增：
```yaml
newebpay:
  base-url: https://ccore.newebpay.com   # 测试 ccore / 正式 core
  notify-url: https://<公网域名>/pay/newebpay/notify
  return-url: https://<公网域名>/pay/newebpay/return
  mpg-version: "2.3"
```

**Rationale**:
- 与 ezpay.base-url 一致的注入风格（`@Value`），便于环境切换。
- notify-url/return-url 必须是公网可访问的 80/443（蓝新要求），配置化便于不同环境部署。
- 第一期固定 EncryptType=0（CBC），不入配置。

---

## D10. 测试环境与端到端自测

**Decision**: 使用蓝新测试环境 `ccore.newebpay.com` + 文档附录1测试卡号验证；本地开发用内网穿透（ngrok/frp）暴露 NotifyURL。

**Rationale**:
- 蓝新测试区提供测试商店凭证与测试卡（附录1），不产生真实扣款。
- NotifyURL 须被蓝新服务器访问，本地 8082 端口需穿透到公网。
- 自测链路：门店录入测试凭证 → 下单 → 发起蓝新支付 → 测试卡付款 → 回调到达 → 订单已支付 + 推送。

**Alternatives**:
- 直接用正式环境：被否，风险高、产生真实交易。

---

## D11. 数据库变更管理

**Decision**: 所有 DDL 写入 `updatesql/sql.md`（标注日期与用途），不直接执行，由开发者统一手动执行（遵循项目规范）。

涉及：`pos_store_newebpay` 建表、`pos_order_payment` 建表。（`pos_order` 无需改结构，payType 取值扩展不改表。）
