# API Contracts: 蓝新金流(NewebPay)线上支付接入 **Feature**: specs/011-newebpay-payment/spec.md **Date**: 2026-06-22 本文定义本期对外与内部接口契约。Controller 位于 `ruoyi-admin`;Service/Domain 位于 `ruoyi-system`;加密工具位于 `ruoyi-admin/.../app/utils/newebpay/`。 --- ## A. 对外接口(蓝新服务器调用,必须匿名) ### A1. NotifyURL — 支付结果背景通知 蓝新在交易完成/失败后以 Form Post 通知本接口。本接口解密验签并更新订单。 - **路径**: `POST /pay/newebpay/notify` - **鉴权**: `@Anonymous`(蓝新无登录态,经 PermitAllUrlProperties 放行) - **Content-Type**: `application/x-www-form-urlencoded` - **请求参数**(蓝新 Form Post): | 参数 | 说明 | |------|------| | MerchantID | 商店代号 | | Status | SUCCESS 或错误代码 | | Version | 串接版本 | | TradeInfo | AES-256-CBC 加密的交易结果(hex) | | TradeSha | SHA256 检查码(大写 hex) | | EncryptType | 加密模式(若发起时设 0/不传,可能不回传) | - **处理流程**: 1. 记录原始回调到 IPN 日志(复用 `IpnLog`)。 2. 用 MerchantID 查 `pos_store_newebpay` 取该门店 HashKey/HashIV;无凭证 → 记录并返回。 3. **验签**: `SHA256("HashKey={key}&{TradeInfo}&HashIV={iv}")` 大写 == TradeSha;不符 → 拒绝、记录。 4. **解密**: AES-256-CBC(PKCS7) 解 TradeInfo → 得 Status/Message/MerchantOrderNo/TradeNo/Amt/PaymentType/PayTime/Auth 等。 5. **幂等**: 按 TradeNo 查 `pos_order_payment`;已存在且 pay_status=1 → 直接返回成功应答,不重复处理。 6. **金额校验**: 解密所得 Amt == 订单 amount;不符 → 拒绝更新、记录告警。 7. **订单关联**: 由 MerchantOrderNo("NB"+ddId)反查 pos_order;订单不存在 → 记录待查、返回。 8. **状态更新**(仅 Status=SUCCESS 且以上校验通过): - `pos_order_payment` 写入/更新 trade_no、pay_type、auth_code、pay_time、pay_status=1、callback_raw。 - `pos_order.payStatus = 1`,`pos_order.state` 保持不变(沿用 payipn 语义)。 - `orderLogHelper.logSync(...)` 记订单日志。 - 触发推送(用户/商家/骑手,复用 pushEventService / sendAcceptRiderPush 链路)。 9. **失败回调**(Status≠SUCCESS): 记录失败原因到 pos_order_payment(pay_status=2),订单保持未支付。 - **响应**: 返回 JSON `{"Status":"SUCCESS","Message":"OK"}`(HTTP 200);蓝新收到非成功应答会重试。 --- ### A2. ReturnURL — 支付完成返回页(前端引导) 蓝新在用户付款后将浏览器 Form Post 回本接口(携带与 NotifyURL 相同的加密参数)。 - **路径**: `GET/POST /pay/newebpay/return` - **鉴权**: `@Anonymous` - **行为**: 仅作引导——重定向到前端支付结果页(带订单号);**不**据此修改订单状态(订单状态以 NotifyURL 为准,见 spec FR-015)。 - **响应**: 302 重定向至前端结果页 URL(含 ddId 与轻量结果提示)。 --- ## B. 内部接口(前端/管理端调用) ### B1. 发起蓝新支付(C 端用户下单后调用) - **路径**: `POST /pay/newebpay/create` - **鉴权**: `@Anonymous` + `@Auth`(C 端用户 token,参照 /pay/VNPay) - **请求参数**: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | orderid | String | V | 系统订单号 ddId | - **处理**: 1. 按 ddId 查 pos_order;校验订单存在、未支付、属于当前用户。 2. 按订单 md_id 查 `pos_store_newebpay`;校验已开通(status=2)且启用(is_enabled=1),否则报错「该门店暂不支持在线支付」。 3. 生成 merchant_order_no = "NB" + ddId(重新发起时追加时间戳避免重复)。 4. 组装 TradeInfo 明文: MerchantID、RespondType=JSON、TimeStamp、Version=2.3、MerchantOrderNo、Amt=amount、ItemDesc、NotifyURL、ReturnURL、CREDIT/LINEPAY/APPLEPAY(按门店 enabled_payments 开关)。 5. AES-256-CBC(PKCS7) 加密 → TradeInfo(hex);SHA256 → TradeSha。 6. 落 `pos_order_payment`(merchant_order_no、store_id、merchant_id、amount、pay_status=0、create_time)。 7. 更新 `pos_order.payType="6"`、`pos_order.payUrl=gatewayUrl`。 - **响应** `AjaxResult.success`: ```json { "code": 200, "msg": "...", "data": { "gatewayUrl": "https://ccore.newebpay.com/MPG/mpg_gateway", "MerchantID": "MS12345678", "TradeInfo": "", "TradeSha": "", "Version": "2.3", "EncryptType": "0" } } ``` 前端用隐藏 form 自动 submit 到 gatewayUrl,字段名对应 data 内 key。 --- ### B2. 单笔交易查询(商家/运营,回调补单与对账) - **路径**: `POST /pay/newebpay/query` - **鉴权**: `@Auth`(商家/运营 token) - **请求参数**: `orderid`(ddId) - **处理**: 1. 查 pos_order 与其门店 pos_store_newebpay 凭证。 2. 组装 CheckValue = `SHA256("IV={iv}&Amt=&MerchantID=&MerchantOrderNo= 排序&Key={key}")`。 3. 调蓝新 `POST https://(c)core.newebpay.com/API/QueryTradeInfo`(MerchantID/Version=1.3/RespondType=JSON/CheckValue/TimeStamp/MerchantOrderNo/Amt)。 4. 解析 TradeStatus(0未付/1成功/2失败/3取消/6退款)、PaymentType、PayTime、CheckCode。 5. **补单**: 若蓝新返回 TradeStatus=1 且订单 payStatus 仍=0(回调丢失),则更新 pos_order.pay_status=1、写 pos_order_payment、触发推送(同 A1 步骤 8)。 - **响应**: `AjaxResult.success` 含 TradeStatus、PaymentType、PayTime、订单当前 payStatus。 --- ## C. 门店蓝新凭证管理(平台后台) 复用 009 `PosStoreEzpayController` 模式,路径 `/system/storeNewebpay`,`@PreAuthorize("@ss.hasPermi('chanting:storeNewebpay:xxx')")`。Service 纯 DB,联网验证在 Controller 层。 | 方法 | 路径 | 权限后缀 | 说明 | |------|------|----------|------| | GET | `/system/storeNewebpay/list` | `:list` | 开通管理列表(分页+筛选) | | GET | `/system/storeNewebpay/{storeId}` | `:query` | 门店蓝新详情 | | PUT | `/system/storeNewebpay/apply/{storeId}` | `:apply` | 发起申请 0→1 | | PUT | `/system/storeNewebpay/saveCredentials` | `:saveCredentials` | 录入凭证+验证(调 QueryTradeInfo 探测)→2 | | PUT | `/system/storeNewebpay/toggleEnable/{storeId}` | `:toggleEnable` | 停用/恢复 | | PUT | `/system/storeNewebpay/reset/{storeId}` | `:query` | 重置状态(凭证作废) | | PUT | `/system/storeNewebpay/enabledPayments/{storeId}` | `:toggleEnable` | 设置启用的支付方式 CREDIT/LINEPAY/APPLEPAY | **saveCredentials 请求体** `StoreNewebpayCredentialDto`: ```json { "storeId": 1, "merchantId": "MS...", "hashKey": "...", "hashIv": "...", "enabledPayments": "CREDIT,LINEPAY,APPLEPAY" } ``` 验证:Controller 调 NewebPay QueryTradeInfo(查虚构订单);返回金钥/商店错误 → 视为无效(拒绝、状态不变);否则通过置 status=2、is_enabled=1。 --- ## D. NewebPay 工具类(内部,非 HTTP 接口) 位于 `ruoyi-admin/.../app/utils/newebpay/`,仿 ezPay 结构: - `NewebPayEncryptUtil`: `encrypt(query,key,iv)`→hex(AES-256-CBC/PKCS7)、`decrypt(hex,key,iv)`→明文、`genTradeSha(tradeInfoHex,key,iv)`→大写、`genCheckValue(amt,mid,orderNo,key,iv)`→大写、`genCheckCode(fields,key,iv)`→大写。含 main 自测(用 NDNF §4.1 示例 Key=Fs5cX1TGqYM2PpdbE14a9H83YQSQF5jn/IV=C6AcmfqJILwgnhIP/MID=MS127874575 验证加解密 round-trip 与 TradeSha 规则)。 - `NewebPay`: 端点常量(`URL_MPG_GATEWAY`、`URL_QUERY_TRADE_INFO`)、`createMpgForm(baseUrl,cfg,params)` 返回加密后的 form 字段、`queryTrade(baseUrl,cfg,amt,orderNo)` 调查询。 - `NewebPayConfig`: merchantId/hashKey/hashIV(运行时由 Service 从 pos_store_newebpay 构造)。 --- ## 配置(application.yml,新增) ```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" ```