Files
oil/hmb-java-demo/README.md
T
vipnub 45452c1799 1、合作商余额查询
2、分油账户司机可用额度
2026-10-08 23:12:33 +08:00

279 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 慧明宝供应链平台 · Java 对接示例
对接方(下单平台)调用慧明宝接口的完整示例,与平台 PHP 端口径一一对应。
配套文档:《慧明宝供应链平台接口文档 V1.0.17》。本示例等价于平台自带的联调脚本 `qyt_test.php`。
---
## 一、对接链路
```mermaid
flowchart LR
subgraph A["你方(下单平台)"]
A1["业务系统"]
end
subgraph B["慧明宝平台"]
B1["开放接口<br/>partners.php"]
B2["发放队列"]
B3["回调队列"]
end
subgraph C["上游油品平台(企油通)"]
C1["运单油费下发"]
C2["司机加油"]
end
A1 -->|"① 分油 share<br/>RSA+AES 加密"| B1
B1 -->|"② 同步返回 oilCardNo<br/>(仅代表受理)"| A1
B1 --> B2
B2 -->|"③ 下发油费"| C1
C1 -->|"④ 查发放结果"| B2
B2 -->|"⑤ 通知 share_oil"| A1
C2 -->|"⑥ 加油消费推送"| B3
B3 -->|"⑦ 通知 add_oil(每笔一次)"| A1
A1 -->|"⑧ 退款 refund(可选)"| B1
B1 -->|"⑨ 通知 refund"| A1
```
不看图的话,一句话版本:**分油是异步的**。同步返回只代表平台受理,真正到账要等 `share_oil` 通知,或自己查订单状态。
```mermaid
sequenceDiagram
participant Y as 你方系统
participant P as 慧明宝平台
participant Q as 企油通
Y->>P: share 分油(金额/司机/身份证/车牌)
P-->>Y: code=00 + oilCardNo(受理)
P->>Q: 运单油费下发
Q-->>P: 受理
P->>Q: 查发放结果(轮询)
Q-->>P: 发放成功
P-->>Y: 通知 optType=share_oil
Note over Y,P: 此时 oil_status_zsy=2,油费才真正到账
Q-->>P: 司机加油(一单可多次)
P-->>Y: 通知 optType=add_oil(每笔推一次)
opt 司机没加完,需要退款
Y->>P: refund 退款
P->>Q: 调低司机凭证额度
P-->>Y: 通知 optType=refund
end
```
---
## 二、快速开始
```bash
mvn -q compile exec:java -Dexec.mainClass=com.hmb.openapi.demo.ShareDemo
```
运行前改 `ShareDemo.java` 顶部 5 个配置(平台分配):
| 配置 | 说明 |
|---|---|
| `BASE_URL` | 接口域名,不含结尾斜杠 |
| `APP_KEY` | 平台分配的 appKey |
| `ORG_NO` | 公司编号,作为业务参数 `orgNo` |
| `PARTNER_PRIVATE_KEY` | **你方**私钥,PKCS#8 一行 base64,用于加签 |
| `PLATFORM_PUBLIC_KEY` | **平台**公钥,X.509 一行 base64,用于加密与验签回调 |
私钥若是 PKCS#1(`-----BEGIN RSA PRIVATE KEY-----`),Java 读不了,先转换:
```bash
openssl pkcs8 -topk8 -inform PEM -in old.pem -outform PEM -nocrypt -out pkcs8.pem
```
### 目录结构
```
src/main/java/com/hmb/openapi/
├── HmbConfig.java 配置与地址拼接
├── HmbCrypto.java 签名、加解密、PHP 兼容的 urlencode
├── JsonPhp.java 请求体序列化(必须与 PHP 口径一致)
├── HmbApiClient.java 五个业务接口
├── HmbResponse.java 响应封装
├── HmbNotifyVerifier.java 接收油卡变动通知(解密 + 验签)
└── demo/
├── ShareDemo.java 下单 → 查状态 → 取加油地址
└── NotifyControllerDemo.java 接收通知的 Spring 示例
```
---
## 三、请求地址
**域名 + `/partners.php` + 接口路径**。平台没配 URL 重写,中间的入口文件不能省。
| 接口 | method(请求头) | 路径 |
|---|---|---|
| 用户分油 | `syb.oil.oilCard.share` | `/oilCard/share` |
| 订单状态查询 | `syb.oil.oilCard.orderStatus` | `/oilCard/orderStatus` |
| 订单退款 | `syb.oil.oilCard.refund` | `/oilCard/refund` |
| 付款码申请 | `syb.pay.qrCode.request` | `/oilCard/getPayQrCode` |
| 分油记录查询 | `syb.oil.oilCard.shareList` | `/oilCard/shareList` |
| 合作商余额查询 | `syb.oil.account.balance` | `/oilCard/balance` |
| 司机可用额度 | `syb.oil.driver.quota` | `/oilCard/driverQuota` |
例:`http://47.104.73.7/partners.php/oilCard/share`
---
## 四、报文结构
### 请求
请求头(自定义 HTTP 头):
| 头 | 说明 |
|---|---|
| `version` | 固定 `1.0.0` |
| `appKey` | 平台分配 |
| `method` | 接口标识,填错返回 `0005` |
| `requestId` | **必须全局唯一**,重复返回 `0012` |
| `sign` | 见下方加签步骤 |
请求体(业务参数不出现在明文里):
```json
{"key": "RSA加密后的AES密钥(base64)", "data": "AES加密后的业务报文(base64)"}
```
### 加签与加密(顺序不能变)
1. 业务参数序列化成 `bodyJson`(PHP 口径,见「六、常见坑」第 2 条)
2. 拼待签串:`http_build_query([version, appKey, method, requestId, body])`
3. `SHA256withRSA` 用**你方私钥**签名,base64 → 放进请求头 `sign`
4. 随机 32 字节 AES 密钥,`AES-256-ECB/PKCS5Padding` 加密 `bodyJson`,base64 → `data`
5. 用**平台公钥** `RSA/ECB/PKCS1Padding` 加密该 AES 密钥,base64 → `key`
待签串长这样(注意 body 整体被 urlencode 了):
```
version=1.0.0&appKey=abc&method=syb.oil.oilCard.share&requestId=abcXXXX&body=%7B%22orgNo%22%3A%22U230%22%2C%22name%22%3A%22%E5%BC%A0%E4%B8%89%22%7D
```
### 响应
平台响应是**明文 JSON,不加密**:
```json
{"code":"00","message":"请求成功","errCode":null,"errMessage":null,"data":{"oilCardNo":"OIL1756...","amount":"100.00"}}
```
`code=00` 为成功,否则看 `errCode` / `errMessage`。
---
## 五、接口字段
### 5.1 用户分油 share
| 参数 | 必填 | 说明 |
|---|---|---|
| `orgNo` | 是 | 公司编号 |
| `amount` | 是 | 分油金额(元),最多两位小数且大于 0 |
| `mobile` | 是 | 司机手机号 |
| `name` | 是 | 司机姓名,用于上游实名二要素核验 |
| `idCard` | 是 | 司机身份证号,18 位 |
| `plateNo` | 是 | 车牌号,平台侧留存展示,**不上送上游** |
| `saleOrderNo` | 是 | 你方内部销售单号,**同一单号重复提交返回原订单(幂等)** |
| `oilCardType` | 是 | 固定 `zsy_ticket` |
返回 `data`:`oilCardNo`(油卡编号,后续接口都用它)、`amount`。
### 5.2 订单状态查询 orderStatus
入参 `orgNo` + `oilCardNo`。关键返回字段:
| 字段 | 说明 |
|---|---|
| `oil_status_zsy` | **0 未提交 / 1 发放中 / 2 发放成功 / 3 发放失败**。只有 2 才是真正到账 |
| `used_amount` | 已加油金额(一单可多次加油,累计值) |
| `used_num` | 加油次数 |
| `remain_amount` | 剩余金额 = 分油金额 − 已加油 − 已退款,未退款时即可退金额 |
| `refund_status` | 0 未退款 / 1 退款中 / 2 已退款 / 3 退款失败 / 4 退款结果确认中 |
| `use_status` | 0 未加油 / 1 有加油记录(可能只加了一部分) |
### 5.3 订单退款 refund
入参 `orgNo` + `oilCardNo` + `reason`(选填)。
- 退款金额**固定为剩余未加油金额**,不能指定,已加油部分不退
- 每单只能成功退一次
- 返回 `refundStatus=2` 成功;`=4` 表示结果确认中,**此时切勿重复发起**,稍后查订单状态或等 `refund` 通知
---
## 六、常见坑
**1. `http_build_query` 的编码规则和 Java `URLEncoder` 不一样**
Java 的 `URLEncoder` 不编码 `*`,PHP 会编成 `%2A`;报文里只要出现一个这样的字符,验签就失败。本示例用 `HmbCrypto.phpUrlEncode()` 复刻了 PHP 规则:只有 `A-Z a-z 0-9 - _ .` 原样保留,空格编成 `+`,其余按 UTF-8 编成 `%XX` 大写十六进制。
**2. 请求体 JSON 里的 `/` 必须转义成 `\/`**
平台收到后会把 body 反序列化再**重新序列化**去验签,用的是 PHP `json_encode($body, JSON_UNESCAPED_UNICODE)`,它默认转义斜杠,而 Java 的 JSON 库不会。备注里带个日期 `2026/09/29` 就会对不上。所以请求体请用本示例的 `JsonPhp.encode()`,**不要**换成 Jackson/Gson。中文要原样输出,不能转 `\uXXXX`。
**3. `requestId` 必须全局唯一**
平台按它拦重复请求,重复返回 `0012`。
**4. 分油是异步的**
同步返回只代表受理。真正到账看 `oil_status_zsy=2`,或等 `share_oil` 通知。
**5. 一张订单可以分多次加油**
每笔加油单独推一次 `add_oil` 通知,`amount` 是**本次**占用金额,`orderAmount` 才是订单总金额。请按 `shareNo`(企油通消费订单号,每笔不同)做幂等去重。
**6. 验签回调时,body 用解密出来的明文原串**
不要反序列化后再序列化 —— 不同语言的 JSON 输出不一定一致。本示例已这样处理。
---
## 七、接收油卡变动通知
把你的接收地址填到平台后台的合作商「回调地址」。报文与请求方向同构,解析步骤:
1. 用**你方私钥**解出 AES 密钥 → 解出业务报文明文
2. 用**平台公钥**验签,待签串是**原样拼接**(不 urlencode):
`version=..&appKey=..&method=..&requestId=..&body={解密出的明文}`
业务字段:
| 字段 | 说明 |
|---|---|
| `oilCardNo` | 油卡编号,对应下单返回值 |
| `optType` | `share_oil` 分油 / `add_oil` 加油 / `refund` 退款 |
| `amount` | 分油=分油金额;加油=**本次**加油占用金额;退款=退款金额 |
| `orderAmount` | 订单实付金额(即分油金额),三种场景都推,便于与 `amount` 对照 |
| `shareNo` | 分油=分油记录编号;加油=企油通消费订单号(**每笔不同,用于去重**) |
| `type` | `add` 增加 / `sub` 减少 |
处理成功必须返回 `{"code":"00","message":"请求成功"}`,否则平台按 60 秒 / 5 分钟 / 30 分钟退避重试 3 次。
---
## 八、错误码
| errCode | 含义 |
|---|---|
| 0001 | 缺少必填请求参数(如未传 `idCard` / `plateNo`) |
| 0005 | 请求头 method 不正确 |
| 0006 | 签名验证不通过 |
| 0012 | 请求流水号重复 |
| 0013 | 请求参数不正确(格式校验未过) |
| 0014 | `orgNo` 与 `appKey` 不匹配 |
| 2023 | 销售单号重复且原单发放失败,请换新单号 |
| 2028 | 账户余额不足 |
| 2032 | 该订单退款正在处理中 |
| 2036 | 未找到对应的分油记录 |
| 2048 | 退单状态异常(不满足退款前置条件,如已全部加油) |
| 2049 | 调用上游退款失败(凭证余额不足或上游拒绝) |
完整列表见接口文档附录 4.3。