# 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": "<hex>",
    "TradeSha": "<UPPER hex>",
    "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"
```
