# 慧明宝供应链平台 · Java 对接示例 对接方(下单平台)调用慧明宝接口的完整示例,与平台 PHP 端口径一一对应。 配套文档:《慧明宝供应链平台接口文档 V1.0.17》。本示例等价于平台自带的联调脚本 `qyt_test.php`。 --- ## 一、对接链路 ```mermaid flowchart LR subgraph A["你方(下单平台)"] A1["业务系统"] end subgraph B["慧明宝平台"] B1["开放接口
partners.php"] B2["发放队列"] B3["回调队列"] end subgraph C["上游油品平台(企油通)"] C1["运单油费下发"] C2["司机加油"] end A1 -->|"① 分油 share
RSA+AES 加密"| B1 B1 -->|"② 同步返回 oilCardNo
(仅代表受理)"| 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。