# 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 取值扩展不改表。)