Files
oil/企油通对接现状分析报告.md

927 lines
83 KiB
Markdown
Raw Permalink 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.
# 企油通对接现状分析报告(第一阶段:项目理解与代码调研)
> 版本:v1 ・ 生成时间:2026-09-01
> 范围:仅调研,未修改任何代码 / 数据库 / 配置。
> 结论中凡无法从代码确证的,均标注 **【待确认】**。
---
## 0. 术语与角色对照(先统一叫法)
| 报告用词 | 含义 | 代码 / 数据中的体现 |
|---|---|---|
| **用户** | 终端加油司机 / 消费者 | 小程序用户、下单平台的 `mobile` |
| **下单平台** | 调用我们接口的外部对接公司 | `yoshop_partners` 表;样例数据只有 1 家「华集云慧」(`orgNo=S99535450`),回调地址 `https://api.chenan.vip/hmb/oilCardChange` |
| **我们的系统** | 本项目 server-tp6 | 对外品牌名「**慧明宝**」(见 `慧明宝供应链平台接口文档V1.0.16.docx`),主体「广西慧明石化有限公司」 |
| **上游油品平台 / 四方平台** | 真正出油券、管油站、管核销的上游 | `yoshop_company` 表;目前有两代对接实现,见第 3、4 节 |
| **企油通** | 本次要接入的新上游平台 | `企油通接口对接文档.md`,域名 `worknoya.vip` |
| 智慧油客 | 上一代上游(`suoyoubao.com.cn`) | `SybOil` / `Crypto`;企油通文档里就是「passageway=3 已停用」的那个通道 |
| 定聚 | 当前在用上游(`idingju.com`,中石油 BP `/api/cnpcbp/*`) | `IdingjuOil` / `Idingju` |
**关键判断:** 业务链路 `用户 → 下单平台 → 我们的系统 → 企油通` 中,
- 「用户 → 下单平台 → 我们的系统」这一段 = **慧明宝入站 API**(`app/api/controller/OilCard.php`),是我们对下单平台的对外契约,**本次仅做最小扩展(新增 `name` 字段),其余 method/URL/字段/加密不变**。
- 「我们的系统 → 上游」这一段 = 用**企油通**整体替换(智慧油客 + 定聚 全部废弃)。
---
## 0.5 决策记录(2026-09-01 与需求方确认)
| # | 议题 | 决策 |
|---|---|---|
| 1 | 废弃范围 | **智慧油客(`SybOil`) + 定聚(`IdingjuOil`) 两套上游全部废弃**,统一换成企油通;不需要与定聚灰度并存 |
| 2 | 企油通企业 | 我们侧**只有一个企业**,`business_license` 为单一配置值;**线下已开通账户**(不需要在系统里做 7.2 创建 / 7.4 开户回调流程) |
| 3 | 订单映射粒度 | **一张订单 ↔ 一次 `createAndSendQuota`(7.7)**,其中 1 个 batch → 1 条 waybill → 1 个 employee(该订单的司机) |
| 4 | 分油用户来源 | **`yoshop_account` 用户池不保留**;司机信息由**下单平台提供** |
| 5 | 小程序油品链路 | **停用**:mobile-uniapp 代码不动,但后端支付成功后**不再触发**任何油品发放/分油/核销逻辑(`PaySuccess::shareOil`、小程序 `order/qrCode(Img)`、`Station.php` 等油品分支停用或摘除) |
| 6 | 入站契约变更 | `syb.oil.oilCard.share` **新增 `name`(司机姓名)入参**;**不新增 `id_card`**。其余字段与加密不变 |
| 7 | 支付通道 | **默认 = 支付宝企业码(`passageway=1`)**,需配置 `institution_id`;通道值做成**后台/配置可切换** |
| 8 | 范围外 | `app/admin` 模块、`yoshop_merchant` 支付聚合链路(`submitOrder`/`Notify`/乐刷)**不在本次范围**,不动 |
**由决策派生的新约束(需在方案中处理):**
- 决策 6 + 7:`passageway=1` 时 `employee_list` 可不传 `id_card`,只需 `mobile + name + quota_amount + voucher_name + institution_id`,与「下单平台只加传 `name`」正好吻合。
- **但 7.16 司机额度调整 / 7.17 获取支付凭证 均要求 `id_card`(必填)** → 在没有 `id_card` 的前提下,「**撤销分油 / 额度回收**」能力受限(见风险 R-07)。
- 决策 5:小程序历史油券订单(`yoshop_order_goods` 中大量 `zsy_ticket` 数据)后续只读展示,不再有新增。
---
## 1. 项目架构
### 1.1 server-tp6(后端,ThinkPHP 6)
多应用结构(`app/` 下按模块分目录):
| 模块 | 作用 | 与本次相关度 |
|---|---|---|
| `app/api` | 对外 HTTP 接口:小程序端 + 下单平台入站 API + 上游回调接收 | ★★★ 核心 |
| `app/store` | 后台管理端接口(admin-vue 实际对接的是这里,`renderSuccess` 风格) | ★★★ |
| `app/admin` | yoshop 原生后台模块,仅 `controller/{admin,setting,store}`,**疑似未启用** 【待确认】 | ★ |
| `app/console` | 命令行 / 队列消费者 / 定时任务 | ★★★ |
| `app/common` | 公共 model / service / crypto / enum | ★★★ |
- **路由**:`route/app.php` 基本为空,全部走 TP6 默认「模块/控制器/操作」约定路由。
- **多入口文件**(`public/*.php`):除 `index.php` 外,为特定回调 / 场景硬编码了 `PATH_INFO`:
| 入口文件 | 绑定路由 | 用途 |
|---|---|---|
| `public/partners.php` | 透传到 `api` 应用 | 下单平台调用慧明宝 API 的入口(`/partners.php/oilCard/share` 等) |
| `public/oil.php` | `/sybNotify/notify` | 上游「分油/发放结果」回调(小程序链路) |
| `public/oiluse.php` | `/sybNotify/use` | 上游「加油/核销结果」回调(小程序链路) |
| `public/partnersoil.php` | `/sybNotify/partnersNotify` | 上游「分油结果」回调(下单平台链路) |
| `public/partnersoiluse.php` | `/sybNotify/partnersUse` | 上游「核销结果」回调(下单平台链路) |
| `public/leshua.php` / `public/notice.php` / `public/ocp.php` | — | 乐刷支付 / 通知 / opcache 面板(与油品无关) |
- **队列**:Redis(Predis),`app/common/service/QueueService.php`,支持普通队列 + ZSET 延迟队列。消费者在 `config/console.php` 注册:
| 命令 | 队列 | 作用 |
|---|---|---|
| `callback:queue` | `callback_queue` | 处理上游回调(method: `oil` / `use` / `oil_partners` / `use_partners`) |
| `partners:queue` | `partners_queue` | 下单平台订单入队后 → 调用上游发放 |
| `notify:queue` | `notify_queue` | 我们 → 下单平台的结果回调(带 3 次退避重试 60/300/1800s) |
| `card:queue` / `process:queue` / `timer` | — | 兑换卡 / 其他 / 定时任务(与油品无关或次要) |
- **其他**:GatewayWorker(WebSocket 推送,`Gateway::sendToUid`);`app/common/lock/RedisLock`(分油抢占锁)。
- **日志**:上游相关日志统一写 `runtime/storelog/{Ym}/{Ymd}/{YmdH}.txt`(`saveLogStore()`)。
### 1.2 admin-vue(后台管理前端)
- 技术栈:Vue 2 + Ant Design Vue(vue-antd-pro 脚手架),`src/config/router.config.js` 静态菜单 + `src/router/generator-routers.js`。
- API 层:`src/api/**`,`axios` 封装(`src/utils/request`),基地址指向 server-tp6 的 `store` 应用。
- 与本次相关的菜单 / 页面见第 6 节。
### 1.3 mobile-uniapp(小程序端,**本次禁止修改**)
- 技术栈:uni-app + uView UI。
- `config.js`:`apiUrl = https://www.hmzkw.com/index.php?s=/api/`,指向 server-tp6 `api` 应用。
- 标准 yoshop 商城客户端(商品 / 购物车 / 结算 / 订单 / 微信支付 / 售后)。
- **仅两处直接触达「油品」语义**(其余全部是服务端在支付成功后自动处理):
| api 文件 | 接口 | 服务端 | 依赖上游? |
|---|---|---|---|
| `api/station.js` | `station/list` | `api/controller/Station.php` → `SybOil::baseGasStationList` | 是(且当前已坏,见风险) |
| `api/order.js` | `order/qrCode`、`order/qrCodeImg` | `api/controller/Order.php` → `IdingjuOil::payQrCodeRequest` | 是(核销码/图) |
| `api/order.js` | `order/list`、`order/detail`、`order/cancel`、`order/pay` … | 标准商城订单 | 否(间接) |
> `order/qrCodeImg` 出参是 `{ base64: "data:image/png;base64,..." }`,服务端通过 `shell_exec("node public/myscript.js <url>")` 把上游返回的二维码链接转成图片。**这是企油通改造的强约束点**(企油通给的是 H5 免登 URL,不是二维码字符串)。
---
## 2. 当前订单相关业务流程
### 2.1 流程 A:小程序商城消费链路(`yoshop_order` + `yoshop_order_goods`)
```
小程序 goods/list,detail
→ checkout/submit ── 创建 yoshop_order + yoshop_order_goods(order_type='zsy_ticket',oilCardType='zsy_ticket')
→ order/pay(微信支付)
→ 支付回调 → app\api\service\order\PaySuccess::onPaySuccess
├─ updatePayStatus → shareOil($detail) ★ 触发「分油」
│ · getCompany($goods_no) 硬编码 goods_no→orgNo,取 yoshop_company(status=10)
│ · processUserOrders($company_id) RedisLock 抢一个「空闲」分油用户(yoshop_account,state=0 且 last_called_at<今天)
│ · IdingjuOil::oilOilCardShare(...) → 定聚 /api/cnpcbp/order
│ 入参 mobile / out_order_id=oilCardNo / shortname=goods_no / notify_url=oil.php / use_notify_url=oiluse.php
│ 出参 code==1,data.trade_no(=shareNo)、data.state(2=已发券)、data.useUrl
│ · 回写 order_goods:account_id / company_id / shareNo / oil_status_zsy / oil_status
└─ Event 'OrderPaySuccess'
→ 上游异步回调 public/oil.php (/sybNotify/notify) → 塞 callback_queue(method='oil')
→ CallbackQueue case 'oil':按 shareNo 命中 order_goods,useStatus==1 → oil_status=1、qrCode=useUrl
→ 用户点核销 → order/qrCode | order/qrCodeImg
→ IdingjuOil::payQrCodeRequest(/api/cnpcbp/getUseCode)→ 写 yoshop_order_qrcode → 返回二维码 URL / base64 图
→ 加油完成 → 上游回调 public/oiluse.php (/sybNotify/use) → callback_queue(method='use')
→ CallbackQueue case 'use':写 yoshop_order_record,useStatus==2 → order_goods.use_status=1、use_time
若该 order 下所有 order_goods 都 use_status=1 → yoshop_order.order_status=30(已完成)
释放 yoshop_account(state=0)
```
订单状态枚举(`app/common/enum/order/`):
- `OrderStatus`:10 进行中 / 20 已取消 / 21 待取消 / 30 已完成
- `PayStatus`:10 待支付 / 20 成功 / 30 超时
- `PayType`:10 余额 / 20 微信 / 30 转账
- 油品自定义(在 `order_goods` / `partners_order` 字段上,非枚举类):
- `oil_status`:0 未分油 / 1 已分油
- `oil_status_zsy`:1 待处理 / 2 已发券 / 3 发券失败 / 5 已退单(注释写「中石油下单状态」)
- `use_status`:0 未核销 / 1 已核销
- `oil_callback` / `use_callback`:是否已回调下单平台
**取消 / 退款**:`app/api/model/Order.php::cancel()` 仅改订单状态(已付款→21 待取消,待后台审核)。`app/common/service/order/Refund.php`、`order/Complete.php` **均不触达上游**——即小程序订单退款/取消**不会**撤销已发放的油券。属流程缺口(见风险 14)。
### 2.2 流程 B:下单平台对接链路(`yoshop_partners_order`)
**入站(下单平台 → 我们,"慧明宝" API,全部 RSA+AES 信封)**,控制器 `app/api/controller/OilCard.php`,鉴权 `app/common/crypto/RsaUtil.php`(平台私钥签名下发 + 合作商公钥验签):
| method 头 | 请求 URL | 动作 |
|---|---|---|
| `syb.oil.oilCard.stockList` | `/oilCard/stock` | 查合作商可下单数量 / 已用 / 未用(读 `yoshop_partners_goods` + `yoshop_partners_order` 计数) |
| `syb.oil.oilCard.share` | `/oilCard/share` | 下单:校验 appKey→`yoshop_partners`、`requestId` 幂等(`yoshop_request_log` 唯一索引)、商品/库存/合作商库存校验 → 创建 `yoshop_partners_order`(order_status=10) → 扣 `goods_sku` 库存 → **push `partners_queue`** → 返回 `oilCardNo` + `amount` |
| `syb.pay.qrCode.request` | `/oilCard/getPayQrCode` | 申请核销码:`IdingjuOil::payQrCodeRequest` → 写 `yoshop_order_qrcode` |
| `syb.oil.oilCard.shareList` | `/oilCard/shareList` | 分油记录分页查询 |
| `syb.oil.oilCard.orderStatus` | `/oilCard/orderStatus` | 单笔订单状态查询 |
**中段(我们 → 上游)**:`app/console/command/PartnersQueue.php` 消费 `partners_queue`:
- 取 `yoshop_partners_order`,`order_status==10 && oil_status==0 && oilCardType=='zsy_ticket'`
- `getCompany($goods_no)` 硬编码取 `yoshop_company`
- `IdingjuOil::oilOilCardShare(...)`,`notify_url=partnersoil.php`、`use_notify_url=partnersoiluse.php`
- 回写 `shareNo / qrCode / oil_status_zsy / oil_status`;若 `state==2` → push `notify_queue`(optType=`share_oil`)
**上游回调(上游 → 我们)**:
- `public/partnersoil.php` → `/sybNotify/partnersNotify`(method 置为 `oil_partners`)→ `callback_queue`
- `public/partnersoiluse.php` → `/sybNotify/partnersUse`(method 置为 `use_partners`)→ `callback_queue`
- `CallbackQueue` case `oil_partners`:按 `shareNo` 命中 `yoshop_partners_order`,`useStatus=='1'` → `oil_status=1 / qrCode`,push `notify_queue`(share_oil)
- `CallbackQueue` case `use_partners`:写 `yoshop_order_record`,`useStatus=='2'` → `use_status=1 / use_time`,push `notify_queue`(add_oil)
**出站回调(我们 → 下单平台)**:`app/console/command/NotifyQueue.php` 消费 `notify_queue`:
- 取 `yoshop_partners_order` + `yoshop_partners`
- `PartnersOil::sendRequest($partners['callback'], ...)`,header `method='syb.notify.oilCard.change'`,body `{oilCardNo, amount, shareNo, type(add/sub), optType}`
- 期望响应 `code=='00'`;成功回写 `oil_callback / use_callback`;失败按 1/3 次退避重试(延迟队列)
### 2.3 流程 C:后台手动运维(admin-vue → app/store)
见第 6 节。核心:手动分油 / 撤销分油 / 补单回调 / 核销码 / 合作商与公司管理 / 分油用户池管理 / 库存同步。
### 2.4 【待确认】另一条支付聚合链路
`app/api/controller/Order.php::submitOrder / searchOrder`、`app/api/controller/Notify.php`、`yoshop_merchant`(+balance_log+order)、`public/leshua.php`、`config` 中的 `patformKey.php` 等,构成一条「商户 / 代理商 + passageway(WX) + notifyUrl + 乐刷」的**第四方支付聚合**链路,与油券链路数据表有交叉(`order_goods.merchant_id`、`Company::processUserOrders`)。它与本次「企油通油品对接」的关系**未确认**——可能是历史遗留、可能仍在跑。**建议本次不动,单独确认。**
---
## 3. 当前上游(四方平台)对接流程
### 3.1 第一代:智慧油客 `SybOil`(`suoyoubao.com.cn`)—— **基本已废弃**
- 代码:`app/common/service/SybOil.php`(+ 早期 `app/api/controller/syb/Syb*.php` 一组「透传壳」控制器)
- 加密:`app/common/crypto/Crypto.php`——RSA(平台公钥加密 AES key) + AES-256-ECB 加密 body + SHA256withRSA 签名,method 头 `syb.*`,接口路径 `/api/1.0.0/...`
- 配置:`config/crypto.php`(单租户默认参数)**或** 按 `yoshop_company` 行(`orgNo/appKey/privateKey/publicKey/platformPublicKey`)
- 现状:
- `CallbackQueue` 里整段 `syb.notify.*` 处理逻辑**全部被注释**,替换为 `oil` / `use` / `*_partners`
- `PaySuccess::shareOil` 的 `SybOil` 版本**被注释**,改用 `IdingjuOil`
- `yoshop_company` 里 `suoyoubao` / `124.70.136.175` 两行 `status=20`(停用)
- **仍在引用但实际已不可用**:`app/api/controller/Station.php`(油站列表,`CompanyModel::detail('2')` 指向停用公司)、`app/store/controller/Goods.php`(库存同步)
- 结论:**这是「已废弃的四方平台」最贴切的候选**。
### 3.2 第二代:定聚 `IdingjuOil`(`idingju.com`,中石油 BP `/api/cnpcbp/*`)—— **当前在用**
- 代码:`app/common/service/IdingjuOil.php` + `app/common/crypto/Idingju.php`
- 鉴权:`/api/open/getToken`(appid=orgNo, appsecret=appKey)拿 `access_token`,本地缓存到 `public/temp/{orgNo}.txt`(含 `expires_time`);业务请求把 `access_token / timestamp / signature` 拼到 **URL query**,body 为明文 JSON(**不加密**)
- 签名:`signature = sha1(api + token + implode(sort(params+token)) + timestamp)`
- 多租户:按 `yoshop_company` 行(`status=10` 的 3 行都是 `idingju.com` 域名)
- 已实现的接口:
| 方法 | 定聚接口 | 用途 |
|---|---|---|
| `baseGasStationList` | `/api/cnpcbp/stations` | 油站列表(**当前 Station.php 用的是 SybOil,未切到这里**) |
| `stockList` | `/api/cnpcbp/queryStock` | 库存(`data.quantity`) |
| `oilOilCardSelect` | `/api/cnpcbp/detail` | 订单详情(`out_order_id` 空,形同占位) |
| `oilOilCardShare` | `/api/cnpcbp/order` | **下单 / 分油**(`data.trade_no / state / useUrl`) |
| `payQrCodeRequest` | `/api/cnpcbp/getUseCode` | **核销二维码**(`data.useQrCodeUrl`) |
- 回调:上游 GET/POST 到 `partnersoil.php` / `partnersoiluse.php` / `oil.php` / `oiluse.php`(**当前回调体未验签**,`sybNotify` 里验签代码被注释,直接原样塞队列);回调关键字段 `trade_no`(=shareNo)、`useStatus`('1' 分油成功 / '2' 核销成功)、`useUrl`
- 说明:定聚没有「分油成功」的独立推送语义——`oilOilCardShare` 同步返回 `state`,或靠 `partnersoil.php` 回调补。
### 3.3 出站客户端:`PartnersOil`(不是油品上游,是「回调下单平台」)
- `app/common/service/PartnersOil.php`:方法名沿用 `syb.*` 风格,但实际是 POST 到 `yoshop_partners.callback`
- 加密:`RsaUtil`(`config/rsa.php` 的平台私钥 + 合作商公钥),与入站 API 同一套信封
### 3.4 加密类总览
| 类 | 用途 | 算法 | 密钥来源 |
|---|---|---|---|
| `crypto/Crypto.php` | 智慧油客 SybOil | RSA + AES-256-ECB + SHA256withRSA | `config/crypto.php` 或 `yoshop_company` |
| `crypto/Idingju.php` | 定聚 IdingjuOil | token + sha1 签名,body 明文 | `yoshop_company` |
| `crypto/RsaUtil.php` | 慧明宝入站 API + 回调下单平台 | RSA(加密 AES key) + AES-256-ECB + SHA256withRSA | `config/rsa.php`(平台私钥文件)+ `yoshop_partners`(合作商公钥) |
| (企油通需新建) | — | appId+timestamp+sign(RSA),`params` 为 JSON 字符串;7.15 特殊 MD5→SHA1withRSA;7.16/7.17 强制 RSA 验签 | 线下索取 |
---
## 4. 旧四方平台代码位置(清单)
### 4.1 智慧油客 `SybOil`(第一代,基本死代码)
| 文件 | 说明 |
|---|---|
| `app/common/service/SybOil.php` | 上游客户端(油站/库存/分油/撤销/核销/发票/用户/优惠券等一整套 `syb.*`) |
| `app/common/crypto/Crypto.php` | 配套加密 |
| `config/crypto.php` | 配套配置(含**明文私钥**) |
| `app/api/controller/syb/SybBase.php` `SybOil.php` `SybOrder.php` `SybPay.php` `SybUser.php` `SybVoucher.php` `SybNotify.php` | 早期一组透传壳控制器(`sendRequest` 依赖 `config('crypto.*')`) |
| `app/api/controller/SybNotify.php` | **仍在用**(回调入口路由到这里),含 `notify/use/partnersNotify/partnersUse` + 被注释的 `notifyOld` |
| `app/api/controller/Station.php` | 油站列表,`SybOil` + 停用公司,实际已坏 |
| `app/store/controller/Goods.php`(`stock`/`all` 附近,~L155-211) | 库存同步用 `SybOil::stockList` |
| `app/console/command/CallbackQueue.php` | 顶部大段注释的 `syb.notify.*` 处理逻辑(历史实现) |
| `app/api/service/order/PaySuccess.php`(~L182-221 注释块) | `SybOil` 版 `shareOil` |
### 4.2 定聚 `IdingjuOil`(第二代,当前在用)
| 文件 | 说明 |
|---|---|
| `app/common/service/IdingjuOil.php` | 上游客户端 |
| `app/common/crypto/Idingju.php` | token + sha1 |
| `app/console/command/PartnersQueue.php` | 下单平台链路:消费 `partners_queue` → `IdingjuOil::oilOilCardShare` |
| `app/api/service/order/PaySuccess.php`(`shareOil()`,~L253-318) | 小程序链路:`IdingjuOil::oilOilCardShare` |
| `app/api/controller/Order.php`(`qrCode` L310 / `qrCodeImg` L349) | 小程序核销:`IdingjuOil::payQrCodeRequest` |
| `app/api/controller/OilCard.php`(`getPayQrCode`) | 下单平台核销:`IdingjuOil::payQrCodeRequest` |
| `app/store/controller/Order.php`(`qrCode` / `share`) | 后台手动核销 / 分油 |
| `app/store/controller/partners/Order.php`(`qrCode` / `share`) | 后台合作商订单手动核销 / 分油 |
| `app/store/controller/Company.php`(`stock()`) | 后台库存同步 `IdingjuOil::stockList` |
| `app/console/command/CallbackQueue.php`(case `oil` / `use` / `oil_partners` / `use_partners`) | 回调处理(当前生效实现) |
### 4.3 出站回调下单平台(本次不改)
| 文件 | 说明 |
|---|---|
| `app/common/service/PartnersOil.php` + `app/console/command/NotifyQueue.php` + `config/rsa.php` + `crypto/RsaUtil.php` | 我们 → 下单平台的结果回调 |
### 4.4 硬编码映射(迁移时必须统一收敛,现散落 7 处)
`getCompany($goodsNo)`(`goods_no → orgNo`)与 `getGoodsNo($orgNo)` / `getGoodsNo` 反向映射,重复出现在:
`app/api/controller/OilCard.php`、`app/api/controller/Order.php`、`app/api/service/order/PaySuccess.php`、`app/store/controller/Order.php`、`app/store/controller/partners/Order.php`、`app/store/controller/Company.php`、`app/console/command/PartnersQueue.php`。
---
## 5. 订单创建 / 提交 / 支付 / 取消 / 退款 / 状态同步 / 回调 代码定位
| 环节 | 小程序链路 | 下单平台链路 | 后台 |
|---|---|---|---|
| 创建 / 提交 | `api/service/order/Checkout.php`、`api/service/order/source/**`、`api/model/Order.php::putInto` | `api/controller/OilCard.php::share` → `api/model/partners/Order.php::saveOrder` | — |
| 支付 | `api/controller/Order.php::pay` → `api/model/Order.php::onPay` → 微信支付回调 → `api/service/order/PaySuccess.php::onPaySuccess` | 无「支付」概念(下单即扣合作商库存 / 余额) | — |
| **发放 / 分油(触上游)** | `PaySuccess::shareOil()` → `IdingjuOil::oilOilCardShare` | `console/command/PartnersQueue.php` → `IdingjuOil::oilOilCardShare` | `store/controller/Order.php::share`、`store/controller/partners/Order.php::share` |
| 核销 / 二维码(触上游) | `api/controller/Order.php::qrCode` / `qrCodeImg` | `api/controller/OilCard.php::getPayQrCode` | `store/controller/Order.php::qrCode`、`store/controller/partners/Order.php::qrCode` |
| 取消 | `api/controller/Order.php::cancel` → `api/model/Order.php::cancel`(仅改状态,不触上游) | 无 | `store/controller/Order.php::updatestatus` |
| **撤销分油** | `store/controller/Order.php::cancelShare` —— **调用未定义方法 `$this->oilOilCardCancelShare()`,当前会致命错误 / 功能已损坏** | — | admin-vue `CancelShareForm.vue` → `/order/cancelShare` |
| 退款 | `common/service/order/Refund.php`(**不触上游**) | 无 | — |
| 状态同步(我方←上游) | 回调 `oil.php` / `oiluse.php` → `SybNotify::notify/use` → `callback_queue` → `CallbackQueue` case `oil`/`use` | 回调 `partnersoil.php` / `partnersoiluse.php` → `SybNotify::partnersNotify/partnersUse` → `callback_queue` case `oil_partners`/`use_partners` | 补单:`store/controller/Order.php::callback` / `partners/Order.php::oilCallback` / `useCallback` |
| 结果回调(我方→下单平台) | — | `console/command/NotifyQueue.php` → `PartnersOil::sendRequest($partners.callback)` | 手动触发同 `oilCallback` / `useCallback` |
| 完成 | `api/model/Order.php::receipt` / `CallbackQueue` 使所有券核销后置 `order_status=30` | `CallbackQueue` 更新 `partners_order` | — |
---
## 6. admin-vue 相关页面
菜单定义:`admin-vue/src/config/router.config.js`
| 菜单 | 路由 | 页面文件 | 对接 store 接口 | 与油品对接关系 |
|---|---|---|---|---|
| 分油公司 › 公司列表 | `/company/index` | `views/company/Index.vue` | `/company/index` `/company/all` `/company/detail` | **上游平台账号管理**(url/orgNo/appKey/privateKey/publicKey/platformPublicKey) |
| 分油公司 › 创建 / 编辑 | `/company/create` `/company/update` | `views/company/Create.vue` `Update.vue` | `/company/add` `/company/edit` | 同上;表单字段 = `url, orgNo, appKey, privateKey, publicKey, platformPublicKey` + 公司资料 |
| — | modal | `views/company/modules/ApiParaForm.vue` | — | 只读展示对接参数 |
| — | modal | `views/company/modules/AccountForm.vue` | `/company/addAccount` | 批量生成随机身份「分油用户」写入 `yoshop_account` |
| — | modal | `views/company/modules/RechargeForm.vue` | `/company/recharge` | 公司余额充值(`yoshop_company_balance_log`) |
| 分油公司 › 余额明细 | `/company/balance/index` | `views/company/balance/Index.vue` | `/company.balance/*` | 公司余额流水 |
| 分油公司 › 分油用户 | `/account/index` | `views/account/Index.vue` | `/account/*` | `yoshop_account` 用户池(小程序分油借用的身份) |
| 合作商管理 › 列表 | `/partners/index` | `views/partners/Index.vue` | `/partners/index` `/partners/all` | **下单平台账号管理**(orgNo/appKey/privateKey/publicKey/callback/balance) |
| 合作商管理 › 创建 / 编辑 | `/partners/create` `/partners/update` | `views/partners/Create.vue` `Update.vue` | `/partners/add` `/partners/edit` | 含勾选可售商品 + 库存分配(`yoshop_partners_goods`) |
| — | modal | `views/partners/modules/ApiParaForm.vue` | — | 只读展示对接参数(含回调地址) |
| 合作商管理 › 订单列表 | `/partners/order/index` | `views/partners/order/Index.vue` | `/partners.order/list` `share` `qrCode` `oilCallback` `useCallback` | `yoshop_partners_order` 运维:手动分油 / 核销码 / 重推回调 |
| — | modal | `views/partners/order/modules/{OilForm,UseForm,ShareForm,PayForm}.vue` | 同上 | |
| 合作商管理 › 余额明细 | `/partners/balance/index` | `views/partners/balance/Index.vue` | `/partners.balance/*` | |
| 订单管理 › 全部 / 油券 / 优惠券 | `/order/list/all` `/oil` `/coupon` | `views/order/Index.vue` | `/order/list` `/order/detail` | `yoshop_order` + `order_goods` 运维 |
| — | modals | `views/order/modules/{ShareForm,CancelShareForm,FillUpForm,PayForm,CancelForm,DeliveryForm,PriceForm,UpdateStatusForm}.vue` | `/order/share` `/order/cancelShare` `/order/callback` `/order/qrCode` `/order.event/*` | **手动分油 / 撤销分油(当前坏) / 补单 / 付款码** |
| 订单管理 › 售后 | (路由已注释,页面文件仍在) | `views/order/refund/**` | — | 当前未启用 |
企油通改造时预计需要动的 admin-vue 页面:**`views/company/**`(改造为企业管理:营业执照 / 通道 / 企业状态 / 开户流程)、`views/order/**` 与 `views/partners/order/**`(补企油通字段、查发放结果、回单下载)**。其余尽量不动。
---
## 7. 数据库相关表(`oil.sql`,表前缀 `yoshop_`)
### 7.1 下单平台侧
| 表 | 关键字段 | 说明 |
|---|---|---|
| `yoshop_partners` | `orgNo, appKey, privateKey, publicKey, callback, balance` | 下单平台(对接公司)账号 & 回调地址 & 余额。样例仅 1 条「华集云慧」 |
| `yoshop_partners_goods` | `partners_id, goods_id, stock_num, use_num` | 分配给下单平台的可售商品与库存 |
| `yoshop_partners_order` | `province, mobile, oilCardType, oilCardNo, shareNo, partners_order_no, partners_id, goods_id/name/no, goods_price, total_price, order_status(10/20/30), qrCode, oil_status, oil_status_zsy, oil_time, oil_text, oil_callback(+time), use_status, use_time, use_text, use_callback(+time), expiration_time` | 下单平台订单主表。**无 user_id**(`user_id=0`)。样例数据 `oil_status_zsy` 多为 3(发券失败,测试环境 `face003`) |
| `yoshop_partners_balance_log` / `yoshop_partners_stock_log` | `scene, money / goods_id, num` | 余额 / 库存变动明细 |
### 7.2 上游平台侧
| 表 | 关键字段 | 说明 |
|---|---|---|
| `yoshop_company` | `url, orgNo, appKey, privateKey, publicKey, platformPublicKey, canOrderNum, unUseNum, useNum, balance, status(10启用/20停用)` | 上游平台账号。样例:row 2/6 = 智慧油客(`suoyoubao` / `124.70...`),`status=20`;row 7/8/9 = 定聚(`idingju.com`),`status=10`,`privateKey/publicKey/platformPublicKey` 存 `'1'`(定聚不用 RSA) |
| `yoshop_company_balance_log` | `scene(10充值/20消费/30管理员/40退款), money` | 公司余额流水 |
| `yoshop_account` | `userNo, mobile, userName, userIdCard, userType, company_id, state(0空闲/1占用), last_called_at, is_delete` | **分油用户池**——小程序订单分油时「借」一个身份去调上游 |
### 7.3 小程序商城订单侧
| 表 | 关键字段(仅列油品相关) | 说明 |
|---|---|---|
| `yoshop_order` | `order_no, order_type, order_status(10/20/21/30), total_price, pay_price, pay_type, pay_status, merchant_id, user_id, order_source, platform` | 商城订单主表(yoshop 标准 + `merchant_id` 代理) |
| `yoshop_order_goods` | `account_id, userNo, mobile, province, oilCardType, oilCardNo, shareNo, company_id, merchant_id, goods_id/name/no, goods_price, total_price, total_pay_price, qrCode, oil_status, oil_status_zsy, oil_time, oil_text, use_status, use_time, use_text, expiration_time` | **油品状态实际承载在这张表**(一单可多券) |
| `yoshop_order_qrcode` | `oilCardNo, qrCode, amount, order_id, order_goods_id, user_id` | 每次申请核销码留痕(样例可见 URL 形态:`https://t-h5.einrkv.com/...`) |
| `yoshop_order_record` | `oilCardNo, shareNo, amount, type(add/sub), optType(share_oil/add_oil/cancel_share), order_id, order_goods_id, user_id` | 油卡变动通知落库 |
| `yoshop_request_log` | `request_id(唯一), app_key, create_time` | 入站 API `requestId` 幂等去重 |
### 7.4 其他相关
| 表 | 说明 |
|---|---|
| `yoshop_goods` / `yoshop_goods_sku` | 商品(`goods_no, oilCardType, province, stock_total`)/ SKU(`stock_num`)。库存被「下单减」与「上游库存同步」两套逻辑写 |
| `yoshop_merchant` / `yoshop_merchant_balance_log` / `yoshop_merchant_order` | 「商户 / 代理商」体系 + 发券订单,与支付聚合链路相关,**与企油通关系待确认** |
| `yoshop_order_refund` 系列 | 售后单(当前后台未启用该菜单) |
> 表结构注释在 `oil.sql` 中为 UTF-8;用 `sed`/编辑器读取正常,用 Windows PowerShell `Get-Content` 会乱码。
---
## 8. 企油通接口清单(据 `企油通接口对接文档.md` 第 7 章,共 23 个)
公共约定:
- 全部 `POST`,路径 `/enterprise/api/...`,域名测试/正式不同,**线下索取**。
- 外层信封:`{ appId, timestamp, sign, params }`,其中 **`params` 必须是 JSON 字符串(转义),不是对象**。
- 响应:`{ status: ok|error, data, message, serialNo, sign }`。
- 业务接口**不需要登录态 token**(`appId + business_license` 即可)。
- 金额单位不统一:**分** → 7.7/7.8/7.9/7.10/7.11/7.12/7.13/7.14;**元** → 7.16/7.17/7.19/7.20/7.21/7.23。
- `passageway`:1 支付宝企业码 / 2 平安银行 / 3 智慧油客(🚫停用,勿传) / 4 企油通(仅出现在返回值)。
- `7.16 / 7.17` **必须真实 RSA 验签**,否则 403。
- `batch_no / waybill_no / waybill_no_code` 当前**全平台唯一**,需加企业前缀。
| 编号 | 名称 | 路径 | 用途 / 备注 |
|---|---|---|---|
| 7.1 | 企业登录(可选) | `/enterprise/api/auth/login` | 业务接口不需要,一般不用 |
| 7.2 | 企业申请创建 | `/enterprise/api/zhyk/openAnAccount` | 提交营业执照等,返回待审核;之后线下开户 |
| 7.3 | 企业账户信息推送(智慧油客) | 回调 | 🚫 已停用 |
| 7.4 | 企业账户信息推送(平安银行) | 回调 `.../callback/ygzf/enterpriseAccountCallback` | 平台→我方,开户完成后推送银行账户 |
| 7.5 | 企业账户信息查询 | `/enterprise/api/zhyk/accountQuery` | 查企业账户号/开户行/状态 |
| 7.6 | 企业信息查询 | `/enterprise/api/zhyk/getEnterpriseInfo` | `is_new / can_pay / password` |
| 7.7 | **运单油费下发** | `/enterprise/api/zhyk/createAndSendQuota` | **核心:给司机按运单发额度**,单批≤100 人,**异步**,`quota_amount` 单位分,平安通道 `id_card` 必填 |
| 7.8 | 运单发放结果查询 | `/enterprise/api/zhyk/waybillBatch` | 按 `batch_no` 查发放明细与 `handle_status` |
| 7.9 | 运单加油明细查询 | `/enterprise/api/zhyk/waybillQuotaDetail` | 返回 `order_code`(→7.19 回单入参)、加油站、升数、油品、实付 |
| 7.10 | **运单加油明细推送(异步)** | 回调 `.../callback/ygzf/waybillConsumeCallback` | 平台→我方,消费推送;**我方须返回 `{"code":"100","msg":"Success"}`** |
| 7.11 | 获取企业账户余额 | `/enterprise/api/zhyk/distributable` | 平安实时余额(勿高频轮询);支付宝通道不支持 |
| 7.12 | 司机钱包金额查询 | `/enterprise/api/zhyk/driverBalanceQuery` | 司机可用余额(分) |
| 7.13 | 司机钱包明细查询 | `/enterprise/api/zhyk/driverWalletQuery` | 流水,默认只回溯近 1 个月 |
| 7.14 | 企业发票下载 | `/enterprise/api/zhyk/invoice` | 按月,返回发票地址集 + 加油明细 |
| 7.15 | **H5 跳转地址(司机免登加油)** | H5 URL | 替代「核销二维码」;签名:`ksort → http_build_query → urldecode → 大写MD5 → SHA1withRSA → base64`;带 `creditcode` 时须一并入签且企业须 `approved` |
| 7.16 | 司机额度调整 | `/enterprise/api/quota/adjustVoucher` | `change_type` 02 增 / 03 减(≈撤销分油),金额**元**,**须 RSA 验签** |
| 7.17 | 获取支付凭证 | `/enterprise/api/quota/getVoucherList` | 返回 `quota_id`(→7.16 入参),金额**元**,**须 RSA 验签** |
| 7.18 | 司机三证信息保存 | `/enterprise/api/zhyk/saveDriverDoc` | 身份证/驾照/行驶证,含 OCR + 实名二要素;仅更新已存在司机 |
| 7.19 | 支付回单获取(平安) | `/enterprise/api/receipt/getPaymentReceipt` | 按 `order_code` 取银行电子回单 PDF;`generating` 需重试(可能 T+1) |
| 7.20 | 司机提现到银行卡(平安) | `/enterprise/api/driver-withdraw/withdrawToBank` | 异步,`out_trade_no` 幂等,金额**元** |
| 7.21 | 提现结果查询(平安) | `/enterprise/api/driver-withdraw/queryWithdraw` | 轮询至 `success/failed` |
| 7.22 | 企业列表查询 | `/enterprise/api/enterprise/list` | 枚举本 appId 名下企业与 `business_license`、已绑定 `passageway`;**需开通权限** |
| 7.23 | 平安消费回调测试接口 | `/enterprise/api/zhyk/testPinganConsume` | 🚫 仅测试环境,模拟一笔消费联调 7.10 |
**四阶段流程**:① 企业创建(7.2→线下开户→7.4/7.5)② 发放油费(7.7→7.8;额度调整 7.17→7.16)③ 司机加油(7.15→7.9/7.10→7.19)④ 获取发票(7.14)。
---
## 9. 旧平台接口 ↔ 企油通接口 对应关系
> ⚠️ **前提:两者业务模型不同**(详见风险 1)。下表是「功能意图」层面的映射,不是字段级等价。
| 现有能力(慧明宝入站 method / 上游调用) | 现实现(定聚 / 智慧油客) | 企油通对应 | 差异要点 |
|---|---|---|---|
| 下单 / 分油 `syb.oil.oilCard.share` → `oilOilCardShare` | 定聚 `/api/cnpcbp/order`,同步返回 `trade_no/state` | **7.7 `createAndSendQuota`**(+ 7.8 查结果) | 由「给手机号发一张固定面值油券」变为「按 `waybill_list.employee_list` 给司机发额度」;**异步**,需 7.8 轮询;平安通道 `id_card` 必填;金额单位分;`batch_no/waybill_no/waybill_no_code` 需加企业前缀 |
| 库存查询 `syb.oil.oilCard.stockList` → `stockList` | 定聚 `/api/cnpcbp/queryStock`(`quantity`) | **7.11 `distributable`**(企业资金余额,平安) | 企油通无「油券库存」概念,是**企业账户可发放资金上限**;支付宝通道不支持;勿高频轮询 |
| 分油记录查询 `syb.oil.oilCard.shareList` → — | — | **7.9 `waybillQuotaDetail`** / **7.17 `getVoucherList`** | 7.17 须 RSA 验签;返回结构差异大(凭证 / 运单明细) |
| 订单状态查询 `syb.oil.oilCard.orderStatus` → — | 读本地 `partners_order` | **7.8 `waybillBatch`**(发放态)/ **7.9**(消费态)/ **7.12**(余额态) | 状态分散在多个接口,无「单笔订单聚合状态」 |
| 核销二维码 `syb.pay.qrCode.request` → `payQrCodeRequest` | 定聚 `/api/cnpcbp/getUseCode`(`useQrCodeUrl` 二维码链接) | **7.15 H5 跳转地址(免登)** | **不再是二维码字符串,是 H5 URL**;签名算法完全不同(大写 MD5 → SHA1withRSA);带 `creditcode` 时须企业 `approved`。**小程序 `order/qrCodeImg` 出参 base64 图不能变** → 服务端需把 H5 URL 转二维码图 |
| 撤销分油(后台 `cancelShare`,当前坏) | 曾经 `SybOil::oilOilCardCancelShare` | **7.16 `adjustVoucher`**(`change_type=03` 减额度) | 须 RSA 验签;金额单位元;先 7.17 拿 `quota_id` |
| 分油结果回调 `partnersoil.php` / `oil.php`(method `oil`/`oil_partners`,`trade_no`+`useStatus=1`) | 定聚主动推 | **无对应推送** → 改为主动查 **7.8** | 企油通发放无「成功推送」,须轮询 |
| 加油/核销结果回调 `partnersoiluse.php` / `oiluse.php`(method `use`/`use_partners`,`useStatus=2`) | 定聚主动推 | **7.10 `waybillConsumeCallback`** | 回调地址由我方提供并配置;**我方须返回 `{"code":"100","msg":"Success"}`**(现有返回 `OK` / `{"code":"00"}`,不匹配) |
| 油站列表 `syb.base.gasStation.list` → `baseGasStationList` | 定聚 `/api/cnpcbp/stations`(Station.php 却用 SybOil,已坏) | **文档无公开油站接口**(H5 内自带选站) | 小程序 `station/list` 需重新评估:或保留定聚、或去掉、或改为静态/其它源 **【待确认】** |
| 发票(SybOil 有 `payInvoiceRequest/Status`) | 未接入下单平台链路 | **7.14 `invoice`** | 按月企业维度 |
| 我方 → 下单平台回调 `syb.notify.oilCard.change`(`PartnersOil`) | 生效 | **保持不变**,内部把企油通字段翻译回该结构 | 契约层,不改 |
| 企业开户 / 账户 | **旧系统无此概念**(`yoshop_company` 直接填 orgNo/appKey) | **7.2 / 7.4 / 7.5 / 7.6 / 7.22** | 全新流程:企业创建须审核 + 线下开户 + 异步回调 |
| 司机建档 / 三证 | 旧系统「借」`yoshop_account` 池身份 | **7.18 `saveDriverDoc`** + 7.7 `employee_list` 建档 | 实名二要素核验;司机须属于企业 |
| 提现 | 无 | **7.20 / 7.21**(新增能力,平安) | 可选 |
---
## 10. 当前发现的问题与风险(已按 0.5 决策收敛)
> 图例:✅ 决策已消解 ・ 🔴 待处理阻塞项(需你拍板 / 需对接人补料) ・ 🟡 施工注意项
| # | 风险 | 级别 | 结论 / 处理方向 |
|---|---|---|---|
| R-01 | 业务模型差异(油券库存 vs 企业运单额度) | ✅ | 决策 2/3/4:单企业 + 一张订单一次 `createAndSendQuota`(1 batch / 1 waybill / 1 employee);「商品面值」= `quota_amount`(元→分);不再有「平台油券库存」概念 |
| R-02 | 「废弃四方平台」范围 | ✅ | 决策 1:`SybOil` + `IdingjuOil` 全部废弃,不灰度并存 |
| R-03 | 企业开户不可即时(7.2→7.4) | ✅ | 决策 2:线下已开户,系统内**不实现** 7.2/7.4,`business_license` 直接配置;7.5/7.6/7.22 作可选核对 |
| R-04 | 发放异步两段式(7.7 受理 → 7.8 轮询) | 🟡 | 新增 `qyt_poll_queue` 延迟队列轮询 7.8;`partners_order` 增发放态字段 |
| R-05 | 无「发放成功」推送 | 🟡 | 同 R-04,靠轮询 7.8 `handle_status` 驱动 `notify_queue` |
| R-06 | 核销形态:二维码字符串 → H5 免登 URL(7.15) | 🟡 | 下单平台链路 `getPayQrCode` 出参 `qrCode` 直接放 H5 URL(本就是链接,下单平台侧生成二维码逻辑不受影响);7.15 特殊签名需新写。小程序 `order/qrCodeImg` 因链路停用(决策 5)不涉及 |
| R-07 | 7.16/7.17 需 `id_card`,但下单平台不传(决策 6) | 🟡→🔴 | 决策 9:**撤销/退款做**(企油通有 7.16)。但 7.16 的 `quota_id` 来自 7.17,而 7.17 需 `id_card`。落地方案:先尝试从 7.7 受理响应 / 7.8 结果 / 7.9 明细里拿到可用的凭证标识并落库(`quota_id`);若均拿不到 → 撤销请求要求下单平台补传 `id_card`。**具体以联调时对接人确认的「7.16 定位方式」为准(R-19)** |
| R-08 | 金额单位混用(分/元) | 🟡 | 新增单位换算 helper,`QytOil` 内部统一 |
| R-09 | 全平台唯一编号需企业前缀 | 🟡 | 定义前缀规则(如 `HM` + 环境标识),`batch_no/waybill_no/waybill_no_code` 由 `saleOrderNo`/`order_id` 派生并落库 |
| R-10 | 库存查询语义(`syb.oil.oilCard.stockList` → `/oilCard/stock`) | ✅ | 决策:企油通没有就不接。接口壳保留(防下单平台报错),数据来源改为我方 `yoshop_partners_goods.stock_num`(后台仍可分配),不再调用上游;`Company::stock` / `store/Goods.php` 的上游库存同步移除。**【待确认下单平台是否仍调用该接口】**,如不调用后续可直接下线 |
| R-11 | 签名/加密体系全新 | 🟡 | 新建 `app/common/crypto/Qyt.php`(业务信封 RSA 签名 + 7.15 H5 特殊签名 + 7.16/7.17 RSA 验签) |
| R-12 | 7.10 回调需返回 `{"code":"100","msg":"Success"}` 且需验签 | 🟡 | 新回调入口独立实现,不复用 `SybNotify` 的 `OK` 返回 |
| R-13 | `store/controller/Order.php::cancelShare` 调用未定义方法(已坏) | 🟡 | 属小程序链路(决策 5 停用);本次移除该分支或改指向「按订单 7.16 撤销」(受 R-07 约束) |
| R-14 | `goods_no ↔ orgNo` 硬编码散落 7 处 | 🟡 | 单企业后 `orgNo` 概念消失,`goods_no → voucher_name/面值` 收敛为 `config/qyt.php` 映射或读 `yoshop_goods` |
| R-15 | 退款/取消无上游回收 | 🟡 | 决策 9:做。新增入站 method `syb.oil.oilCard.cancelShare`(.docx 未定义,属慧明宝契约小扩展)→ 我方查凭证 → 7.16 `change_type=03` 减额度 → 回写 + 回调下单平台(optType=cancel_share, type=sub) |
| R-16 | `Station.php` / `store/Goods.php` 依赖已停用 SybOil | 🟡 | 决策 5:一并停用/摘除;`store/Goods.php` 库存同步改为不依赖上游 |
| R-17 | `app/admin`、`yoshop_merchant` 支付聚合 | ✅ | 决策 8:范围外,不动 |
| R-18 | 敏感信息明文(config/DB 私钥) | 🟡 | 企油通密钥放 `config/qyt.php` + `.env`,不写进 `yoshop_*` 表 |
| R-19 | 环境参数需线下索取 | 🔴 | 需对接人给:测试/正式域名、`appId`、业务密钥/公私钥、`institution_id`、7.10 回调地址配置方式、7.15 落地页 appId |
| R-20 | 文档只覆盖第 7 章 | 🔴 | 7.15 落地页参数、`institution_id` 分配、拼签字段明细需补 |
| R-21 | 小程序历史油券订单数据 | 🟡 | `yoshop_order_goods` 大量 `zsy_ticket` 旧数据只读保留;后台订单页兼容旧/新字段展示 |
### 决策补充(2026-09-01 第二轮)
| # | 议题 | 决策 |
|---|---|---|
| 9 | 撤销 / 退款 | **做**(企油通有 7.16 `adjustVoucher`)。新增入站 method `syb.oil.oilCard.cancelShare` → 我方定位凭证 → 7.16 `change_type=03` 减额度 → 回写 + 回调下单平台。凭证定位方式(是否需 `id_card`)以联调时对接人确认为准 |
| 10 | `/oilCard/stock` 库存 | 企油通没有就不接。接口壳保留、数据取我方 `yoshop_partners_goods`,不再同步上游 |
| 11 | 回调下单平台字段 | **不硬凑**。所有字段严格按企油通实际返回来填,企油通没有的(如旧 `shareNo`)就填空 / 不下发。发放阶段 `shareNo` 可留空或填 `waybill_no_code`;消费阶段填企油通 `order_code` |
| 12 | 后台范围 | 「满足接口 + 满足运营」——凡企油通有、运营用得上的都进后台:发放结果查询(7.8)、加油明细(7.9)、H5 加油链接(7.15)、消费流水、企业余额(7.11)、企业信息(7.5/7.6)、发票下载(7.14) |
### 决策补充(2026-09-01 第三轮 · 施工细节)
| # | 议题 | 决策 |
|---|---|---|
| 13 | 企油通订单字段落库 | 直接加到 `yoshop_partners_order` 主表(不建关联表);发放态复用 `oil_status_zsy` |
| 14 | `name` 缺失 | 兜底占位(`"司机"+手机尾4位`),不阻断下单 |
| 15 | `qrCode` 返回 | 直接把 7.15 H5 URL 放进 `qrCode` 字段,不转二维码图 |
| 16 | 发放结果获取 | 延迟队列退避轮询 7.8(新增 `qyt_poll_queue` + `qyt:poll` 命令) |
| 17 | 7.10 消费回调入口 | 新建独立入口 `public/qytconsume.php` |
| 18 | 小程序油品链路 | 后端停止触发,相关代码**保留不删** |
| 19 | 旧上游代码(SybOil/IdingjuOil/…) | 本期保留不删、只停止调用;全链路联调通过后在收尾阶段统一删除 |
| 20 | 后台「企业管理」 | 改造现有「分油公司」菜单为「企业管理」页(复用菜单/路由/权限) |
| 21 | 施工节奏 | 先出 DB 迁移 + `config/qyt.php` + `Qyt.php` / `QytOil.php` 骨架审核,确认后再动队列/控制器 |
### 仍需对接人提供(R-19/R-20,阻塞联调,不阻塞脚手架开发)
测试 + 正式域名、`appId`、业务 RSA 公私钥与**业务接口拼签字段顺序**、`institution_id`(支付宝企业码通道必填)、7.10 回调地址如何配置、7.15 H5 落地页 `appId` 与签名字段清单、7.16 凭证定位方式。
---
## 附 B. 第二阶段 A-1 交付物(骨架,待审)
| 文件 | 状态 | 说明 |
|---|---|---|
| `server-tp6/qyt_migration.sql` | 新增 | `yoshop_partners_order` 加 14 字段 + 2 索引;新建 `yoshop_qyt_consume`;含回滚脚本。**执行前请审字段** |
| `server-tp6/config/qyt.php` | 新增 | 配置骨架;标 ★ 的值待对接人给(用 `.env` 覆盖) |
| `server-tp6/app/common/crypto/Qyt.php` | 新增 | 业务信封 / 7.15 H5 签名 / 验签;`bizSign()` 的拼签规则标 **TODO 待对接人确认**(默认走 7.15 同族算法) |
| `server-tp6/app/common/service/QytOil.php` | 新增 | 企油通客户端:7.7/7.8/7.9/7.11/7.14/7.15/7.16/7.17;元↔分、编号生成、日志工具 |
> 均为**新增文件,未触碰任何现有文件**。审核通过后再进入 A-2(改 `PartnersQueue` / 新增 `QytPollQueue` / `QytNotify` / 改 `OilCard.php` / 改 `CallbackQueue`)。
---
## 附 C. 第二阶段 A-2 交付物(下单平台链路打通,待审)
> ⚠️ **部署前必须先执行 `qyt_migration.sql`**(`yoshop_partners_order` 会用到新列)。
### 新增文件
| 文件 | 说明 |
|---|---|
| `app/console/command/QytPollQueue.php` | `qyt:poll` 命令:退避轮询 7.8 拿发放结果,成功→写订单+投 `notify_queue`;超次数标记人工核对 |
| `app/api/controller/QytNotify.php` | `/qytNotify/consume`:接收 7.10,验签(TODO),落 `callback_queue`(method `qyt_consume`),返回 `{"code":"100","msg":"Success"}` |
| `public/qytconsume.php` | 7.10 回调入口(配到企油通后台的地址) |
### 改动文件(现有)
| 文件 | 改动 |
|---|---|
| `config/console.php` | 注册 `qyt:poll` |
| `app/console/command/PartnersQueue.php` | 整个 `processMessage` 重写:`IdingjuOil::oilOilCardShare` → 生成企业前缀编号 + `QytOil::createAndSendQuota`(7.7 受理) → 受理成功投 `qyt_poll_queue`。删除硬编码 `getCompany()` |
| `app/console/command/CallbackQueue.php` | **仅新增** `case "qyt_consume"`(写 `yoshop_qyt_consume` + 更新订单核销 + 投 `notify_queue`);旧 `oil/use/oil_partners/use_partners` 分支保留不动 |
| `app/api/controller/OilCard.php` | ①`use QytOil` ②`share()` 加 `name` 入参 + 兜底占位 + `saveOrder` 增 `driver_name/passageway` ③`getPayQrCode()` 改 `QytOil::h5Url`(出参 `qrCode` 结构不变)④**新增 `cancelShare()`**(method `syb.oil.oilCard.cancelShare` → 7.17 定位 `quota_id` → 7.16 减额度 → 投 `notify_queue` optType=`cancel_share`)。`getCompany()`/`IdingjuOil` import 保留(死代码,收尾清) |
| `app/api/service/order/PaySuccess.php` | `shareOil()` 的 `zsy_ticket` 分支加 `continue` —— 小程序支付成功后不再触发上游(优惠券分支保留) |
### A-2 新增待确认
- **`optType=cancel_share`**:`NotifyQueue` 把撤销通知发给下单平台时 `type=sub / optType=cancel_share`(`.docx` 回调 optType 枚举只有 share_volume/share_oil/add_oil)。需与下单平台确认能否识别,或改用其约定值。
- **7.7 响应 `data.status`**:文档写 `1 成功 / 2 失败`,此处「成功」按「受理成功」处理;如企油通实际语义不同需调 `PartnersQueue`。
- **7.8 `list[].handle_status`**:按 `1待处理/2成功/3失败` 匹配;如字段名/取值不同需调 `QytPollQueue`。
### 未动(留给 A-3)
小程序核销 `app/api/controller/Order.php::qrCode/qrCodeImg`、`Station.php`、`store/controller/Order.php`(小程序订单后台)、`store/controller/Company.php::stock`、`store/controller/Goods.php`、`store/controller/partners/Order.php`(下单平台订单后台运维)、admin-vue 页面。
---
## 附 D. 并发 / 原子性加固(A-2 补丁,已并入 A-2 交付物)
### 状态机(`yoshop_partners_order`)
| `oil_status_zsy` | 含义 | 迁入方 | 迁入条件 |
|---|---|---|---|
| 0 | 未提交 | 建单默认 | — |
| 1 | 已受理待发放 | `PartnersQueue` | `WHERE oil_status_zsy=0`(7.7 受理成功后) |
| 2 | 发放成功 | `QytPollQueue` | `WHERE oil_status=0 AND oil_status_zsy=1`(7.8 handle_status=2) |
| 3 | 发放失败 | `PartnersQueue` / `QytPollQueue` | `WHERE oil_status_zsy IN (0,1)`(7.7 拒绝 / 7.8=3 / 轮询超时) |
| 5 | 已撤销 | `OilCard::cancelShare` | `WHERE oil_status=1 AND oil_status_zsy=2 AND use_status=0`(7.16 成功) |
`use_status` 0→1:`CallbackQueue` `WHERE use_status=0 AND oil_status=1`(7.10 消费)。
**每个状态迁移都是条件 `UPDATE ... WHERE 期望值`,`affected_rows=1` 的进程才推送下游队列** → 天然幂等,可多进程 / 重复消息安全。
### 具体措施
| 场景 | 风险 | 措施 |
|---|---|---|
| `OilCard::share` 同一 `saleOrderNo` 并发 | 先查后插竞态 → 重复建单 / 库存超扣 | DB 唯一键 `uk_partner_saleorder(partners_id, partners_order_no)`;命中已存在订单 → **幂等返回原结果**;`saveOrder` 捕获 Duplicate 再幂等返回 |
| `PartnersQueue` 同一 order 被多 worker / 重复消息处理 | 重复调 7.7 | `RedisLock('qyt:send:{order_id}', 60s)` 串行 + 锁内重读状态;`batch_no` 确定性派生,重试用同一值,依赖企油通按 `batch_no` 幂等(`dup_batch_hints` 命中即按已受理处理) |
| 7.7 成功但 worker 崩溃(未及入轮询队列) | 订单卡在「已受理」永不发放 | `oil_status_zsy` 只在 7.7 结果确定后离开 0;`QytPollQueue::reconcile()`(空闲每 60s)补投:`oil_status_zsy=0` 超 2min → 重投 `partners_queue`;`oil_status_zsy=1` 且 15min 无进展 → 重投 `qyt_poll_queue` |
| `QytPollQueue` 多 worker / 延迟队列并发迁移 | 同一 order 重复 7.8 → 重复回调下单平台 | `QueueService::migrateDelayedToReady` 改为**先 `zrem` 抢占再入队**;发放成功用条件 UPDATE 1→2,仅胜出者推 `notify_queue` |
| 企油通重推 7.10 | 重复消费流水 / 重复回调 | `yoshop_qyt_consume` 唯一键 `uk_consume(waybill_no_code, order_code)` + 插入捕获 Duplicate;`use_status` 条件跃迁 0→1,仅胜出者推 `notify_queue` |
| `cancelShare` 并发 / 与 7.10 消费竞争 | 重复调 7.16 减额度 / 撤销与加油冲突 | `RedisLock('qyt:cancel:{order_id}', 60s)` + 条件占位 `WHERE oil_status_zsy=2 AND use_status=0`;7.16 失败回滚 `oil_text`;成功再条件跃迁 2→5(仍要求未核销),冲突则返回人工核对 |
| `callback_queue` / `notify_queue` 单消费者假设 | 与旧代码一致 | 建议单进程;即使多进程,因全链路条件跃迁,最坏只是日志重复 |
### 遗留(联调时与对接人确认)
- **7.7 幂等键**:确认企油通是否严格按 `batch_no` 去重,重复提交返回什么(用于 `dup_batch_hints` 精确匹配)。
- **7.16 幂等**:文档无幂等键;当前靠 `RedisLock` + 条件占位防重,但跨进程锁过期(60s)后仍有极小窗口。若企油通提供幂等键(如 `out_trade_no`)应接入。
- **`RedisLock`** 用 `config('redis')`,`QueueService` 用硬编码 `127.0.0.1:6379` —— 生产需统一(本次未动 `QueueService` 连接配置)。
---
## 附 E. 对下单平台接口文档更新(`慧明宝供应链平台接口文档V1.0.17.docx`)
在原 `V1.0.16.docx` 基础上做增量修改,保留原文件为历史。改动:
| # | 章节 | 变更 |
|---|---|---|
| 1 | 3.2.1 用户分油 请求体 | 新增 `name`(司机姓名,可选,String V50,缺失平台兜底) |
| 2 | 3.5.1 油卡变动通知 `optType` | 新增取值 `cancel_share`(撤销分油,配 `type=sub`) |
| 3 | 文末新增《附录:V1.0.17 变更说明》 | ①`share` 增 `name` + 异步发放语义 + `saleOrderNo` 幂等;②**新增接口 `syb.oil.oilCard.cancelShare`**(撤销分油,`/oilCard/cancelShare`,入参 `orgNo` / `oilCardNo` 或 `saleOrderNo` / `id_card`,出参 `oilCardNo` `amount`);③`qrCode.request` 返回值由二维码串改为 H5 免登地址;④`stockList` 返回值改为平台内部额度统计;⑤`notify` 空值兼容说明;⑥修订记录 |
制作方式:`unpack → merge_runs.py → 编辑 document.xml → 重新打包`,`validate.py` XSD 校验通过。
**注意**:①本机无 LibreOffice,未能渲染 PDF 目视核对,建议用 Word 打开确认排版;②新增《附录》为普通标题,不在目录中,Word 打开时可「更新域」重建目录;③`merge_runs` 合并了冗余样式标签(内容与渲染不变),故 `.docx` 体积小于原文件属正常。
---
## 附 F. 范围收敛(2026-09-03 · 最终版)—— 支付宝企业码通道,仅 3 个上游接口
> 需求方最终确认:**只对接支付宝企业码通道**,上游实际只用 3 个接口:
> **7.7 运单油费下发 / 7.15 H5 跳转地址 / 7.10 运单加油明细推送(异步回调)**。
> 司机姓名 + 手机号由下单平台必传。不做撤销(企油通仅 7.16 调整额度,本期不用)。
> 本节内容**取代**前文附 B / C / D / E 中与轮询、撤销、平安通道、7.8/7.9/7.11/7.14/7.16/7.17 相关的描述。
### 与之前版本的差异
| 项 | 之前(附 B/C/D) | 现在(最终) |
|---|---|---|
| 上游接口 | 7.7 + 7.8 轮询 + 7.15 + 7.10 + 7.16/7.17 撤销 + 可选 7.9/7.11/7.14 | **仅 7.7 + 7.15 + 7.10** |
| 发放结果 | 7.7 受理 → `qyt_poll_queue` 退避轮询 7.8 | **7.7 受理成功即视为发放成功**(支付宝企业码通道),直接回调下单平台。删除 `QytPollQueue` / `qyt:poll` / `qyt_poll_queue` |
| 通道 | 1/2 可切换 + `institution_id` | **固定 `passageway=1`**(支付宝企业码)+ `institution_id` 必填 |
| 司机姓名 `name` | 可选,缺失兜底占位 | **必填**,下单平台不传 → 返回 `0001 缺少必填请求参数 name`。(`mobile` 本就必填) |
| 撤销分油 | 新增入站 `syb.oil.oilCard.cancelShare` → 7.17/7.16 | **不做**。移除该 method、`RedisLock('qyt:cancel:…')`、`QytOil::getVoucherList/adjustVoucher` |
| 7.7 入参 | 含 `id_card`、`effective_*`、`institution_config`、`passageway` 动态 | 精简为:`batch_no` / `business_license` / `passageway="1"` / `institution_id` / `waybill_list:[{waybill_no, employee_list:[{mobile, name, quota_amount(分), voucher_name, effective_start:"", effective_end:"", waybill_no_code}]}]` |
| DB 字段 | 含 `passageway` / `quota_id` / `poll_times` / `last_poll_time` | 移除以上 4 个;保留 `driver_name` / `batch_no` / `waybill_no` / `waybill_no_code` / `qyt_order_code` / 消费明细 5 项 |
| `oil_status_zsy` 取值 | 0/1/2/3/5 | **0 未提交 / 2 发放成功 / 3 发放失败**(无 1 待处理、无 5 撤销) |
| `yoshop_qyt_consume` | 含 `quota_id` / `passageway` 列 | 移除该 2 列 |
### 最终文件清单
| 文件 | 状态 |
|---|---|
| `config/qyt.php` | 精简(去通道切换 / 轮询配置) |
| `app/common/crypto/Qyt.php` | 不变(信封 + 7.15 签名 + 验签) |
| `app/common/service/QytOil.php` | **精简为 `createAndSendQuota`(7.7) + `h5Url`(7.15)** + `post/curlPost` + 工具 |
| `app/console/command/PartnersQueue.php` | 7.7 受理成功=发放成功 → 条件跃迁 0→2 → 回调;含 `reconcile()` 兜底补投 |
| `app/console/command/QytPollQueue.php` | **已删除** |
| `config/console.php` | 移除 `qyt:poll` 注册 |
| `app/api/controller/QytNotify.php` + `public/qytconsume.php` | 不变(7.10 入口,返回 `{"code":"100","msg":"Success"}`) |
| `app/console/command/CallbackQueue.php` | `qyt_consume` 分支:`use_status` 条件跃迁 0→1 + `yoshop_qyt_consume` 去重;旧定聚分支保留 |
| `app/api/controller/OilCard.php` | `share` 加必填 `name`;`getPayQrCode` → `QytOil::h5Url`;**移除 `cancelShare`**;`saveOrder` 幂等 |
| `app/api/service/order/PaySuccess.php` | `shareOil()` 的 `zsy_ticket` 分支 `continue`(小程序停用) |
| `app/common/service/QueueService.php` | `migrateDelayedToReady` 改 zrem-first(仅 `notify_queue` 用到延迟队列) |
| `qyt_migration.sql` | 精简字段 + 保留 2 个唯一键 |
| `慧明宝供应链平台接口文档V1.0.17.docx` | `name` 必填;移除 cancelShare;发放语义改「受理即成功」;无 cancel_share |
### 并发/原子性(最终,简化后)
- **`share`**:`uk_partner_saleorder(partners_id, partners_order_no)` 唯一键 + 幂等返回原结果 + `saveOrder` 捕获 Duplicate。
- **`PartnersQueue`**:`RedisLock('qyt:send:{order_id}', 60s)` 串行 + 锁内重读;发放态跃迁 `UPDATE ... WHERE oil_status_zsy=0`,`affected=1` 才回调下单平台;`batch_no` 确定性派生,重试用同一值,靠企油通按 `batch_no` 幂等(`dup_batch_hints` 命中即按已发放);空闲 `reconcile()` 每 60s 补投丢失的 `partners_queue` 消息。
- **`CallbackQueue` 7.10**:`yoshop_qyt_consume` 唯一键去重 + `use_status` 跃迁 `WHERE use_status=0 AND oil_status=1`,`affected=1` 才回调;企油通重推安全。
- **`NotifyQueue`**:原有 3 次退避重试不变;报文结构 `syb.notify.oilCard.change` 不变。
### 联调前仍需对接人提供
测试/正式域名、`appId`、业务接口 **RSA 拼签字段顺序**、`institution_id`、7.10 回调地址配置方式、7.15 H5 落地页 `appId`;确认 **7.7 支付宝企业码通道响应 `data.status` 语义**(是否 1=受理即发放成功)与 **batch_no 重复提交的返回文案**(校准 `dup_batch_hints`)。
## 11. 改造方案(基于 0.5 决策,待你对 §10「🔴 待拍板」项给结论后即可开工)
### 11.1 边界原则
- **完全不动**:`mobile-uniapp` 全部;`NotifyQueue` + `PartnersOil` 对下单平台回调的**报文结构**(`syb.notify.oilCard.change`);`yoshop_partners` / `yoshop_partners_goods` 表结构;`app/admin`;`yoshop_merchant` 相关。
- **最小扩展**:`app/api/controller/OilCard.php` 的 `syb.oil.oilCard.share` 仅新增可选入参 `name`。其余入站 method / URL / 加密不变。
- **整体替换**:「我们 → 上游」这一层——删除 `SybOil` / `IdingjuOil` 调用,换 `QytOil`。
- **停用**:小程序油品发放链路(`PaySuccess::shareOil` 油券分支、`Order::qrCode / qrCodeImg`、`Station`)。
### 11.2 新增 / 改动文件清单(预估)
| 类型 | 文件 | 说明 |
|---|---|---|
| 新增 | `config/qyt.php` | 域名、`appId`、公私钥、`business_license`、`passageway` 默认值、`institution_id`、编号前缀、7.10 回调地址、7.15 落地页地址 |
| 新增 | `app/common/crypto/Qyt.php` | 三种签名/验签:①业务信封 ②7.15 H5 签名 ③7.16/7.17 RSA 验签 |
| 新增 | `app/common/service/QytOil.php` | `createAndSendQuota(7.7)` / `waybillBatch(7.8)` / `waybillQuotaDetail(7.9)` / `h5Url(7.15)` / `getVoucherList(7.17)` / `adjustVoucher(7.16)` / (可选)`distributable / invoice / enterpriseList` |
| 新增 | `public/qytconsume.php` + `app/api/controller/QytNotify.php` | 7.10 消费回调入口,验签,返回 `{"code":"100","msg":"Success"}`,落 `callback_queue`(method `qyt_consume`) |
| 新增 | `app/console/command/QytPollQueue.php`(注册 `qyt:poll`) | 退避轮询 7.8 发放结果 |
| 新增 | `app/common/library/Amount.php`(或并入 helper) | 元↔分 |
| 改 | `app/console/command/PartnersQueue.php` | 换 `QytOil::createAndSendQuota`;受理成功入 `qyt_poll_queue` |
| 改 | `app/console/command/CallbackQueue.php` | 删定聚 `oil/use/oil_partners/use_partners` 分支,加 `qyt_consume` 分支 |
| 改 | `app/api/controller/OilCard.php` | `share` 加 `name`;`getPayQrCode` → `QytOil::h5Url`;`stock` 数据源改我方 `partners_goods`;**新增 `cancelShare` method**(7.16 减额度) |
| 改 | `app/api/service/order/PaySuccess.php` | 移除 `shareOil()` 油券分支(保留优惠券分支) |
| 改 | `app/store/controller/partners/Order.php` | `share` 改为「查发放结果(7.8)」;`qrCode` → h5Url;回调按钮走新队列 |
| 改 | `app/store/controller/Order.php` | 移除/改造小程序链路 `cancelShare / qrCode / share` |
| 改 | `app/api/controller/Station.php`、`app/store/controller/Goods.php` | 停用/摘除上游依赖 |
| 删/弃 | `SybOil.php`、`IdingjuOil.php`、`crypto/{Crypto,Idingju}.php`、`config/crypto.php`、`app/api/controller/syb/*`、`public/{oil,oiluse,partnersoil,partnersoiluse}.php` | 确认无引用后移除(可先保留一版做回滚) |
| 改 | admin-vue `src/views/partners/order/**`、`src/views/company/**` | 见 11.6 |
### 11.3 数据库改动(全部增量加字段,不改现有字段 / 类型)
`yoshop_partners_order` 增加:`driver_name`(司机姓名)、`passageway`(本单通道)、`batch_no`、`waybill_no`、`waybill_no_code`(唯一索引,回调匹配)、`quota_id`(7.17 凭证 ID)、`qyt_order_code`(7.9/7.19 订单号)、`handle_status`(7.8 单条发放态 1/2/3)、`poll_times` / `last_poll_time`、以及消费明细 `station_name / litter / oil_name / pay_fee(分) / consume_time`。
新增表(建议):`yoshop_qyt_consume`(7.10 每笔消费流水)。
> `yoshop_company` / `yoshop_account`:单企业 + 取消用户池后闲置。**不删表**(保历史数据),改为不再写入;`business_license` 等改放 `config/qyt.php`。
### 11.4 下单平台链路 端到端流程(改造后)
```
下单平台 POST /partners.php/oilCard/share (method=syb.oil.oilCard.share, RSA信封)
body: orgNo, goodsNo, mobile, name(新增), saleOrderNo, oilCardType=zsy_ticket
↓ OilCard.php::share
· 校验 appKey→yoshop_partners、requestId 幂等、库存(R-10)
· 建 yoshop_partners_order(order_status=10, driver_name=name)
· 生成 batch_no / waybill_no / waybill_no_code(企业前缀 + saleOrderNo/order_id)
· push partners_queue → 返回 { oilCardNo, amount }(结构不变)
↓ PartnersQueue → QytOil::createAndSendQuota(7.7)
params = { batch_no, business_license(配置), passageway(配置, 默认1),
institution_id(配置, 通道1必填),
waybill_list:[{ waybill_no, waybill_no_code,
employee_list:[{ mobile, name, quota_amount(元→分),
voucher_name, effective_start:"", effective_end:"" }] }] }
· status=1 受理成功 → 记 batch_no, handle_status=1 → push qyt_poll_queue(延迟)
· status=2 → oil_status_zsy=3, oil_text=msg
↓ QytPollQueue → QytOil::waybillBatch(7.8) by batch_no(退避轮询)
· list[0].handle_status==2 → oil_status=1, oil_status_zsy=2, quota_id → push notify_queue(share_oil)
· ==3 → oil_status_zsy=3, oil_text=error_msg
· ==1 且未超次数 → 再次延迟入队
↓ 核销:下单平台 POST /oilCard/getPayQrCode → QytOil::h5Url(7.15)
· 返回 { qrCode: <H5免登URL> }(结构不变;下单平台侧原有「拿字符串生成二维码」逻辑不受影响)
↓ 司机加油后:企油通 POST public/qytconsume.php (7.10)
· QytNotify 验签 → 按 waybill_no_code / order_code 命中 order
· 写 yoshop_qyt_consume;use_status=1, use_time;返回 {"code":"100","msg":"Success"}
· push notify_queue(add_oil)
↓ NotifyQueue(报文结构不变)→ PartnersOil::sendRequest($partners.callback,
method=syb.notify.oilCard.change, body={ oilCardNo, amount, shareNo, type, optType })
```
**`name` 缺失兜底**:下单平台未传时用 `"司机"+mobile后4位` 占位(企业码通道不做姓名实名核验)—— **待你确认是否可接受**。
### 11.5 对下单平台回调(`syb.notify.oilCard.change`)字段映射(决策 11:不硬凑,按企油通实际返回填)
| 回调字段 | 旧值(定聚) | 新值(企油通) |
|---|---|---|
| `oilCardNo` | `partners_order.oilCardNo` | 不变(我方自生成) |
| `amount` | `total_price` | 不变 |
| `shareNo` | 定聚 `trade_no` | 发放阶段:留空或填 `waybill_no_code`;消费阶段:填企油通回调的 `order_code`。企油通无对应值时**填空字符串**,不硬造 |
| `type` / `optType` | add/sub、share_oil/add_oil/cancel_share | 不变;新增撤销场景 `type=sub, optType=cancel_share` |
> 保持报文**结构**与字段名不变(下单平台既有解析不受影响),只是个别字段值可能为空。
### 11.6 admin-vue 改动(决策 12:满足接口 + 满足运营)
- `views/partners/order/Index.vue`:列表增列「批次号 / 运单号 / 发放态 / 通道」;「手动分油」→「查询发放结果(7.8)」;「核销码」→「获取加油链接(7.15)」;增「撤销分油(7.16)」「加油明细(7.9)」。
- 新增「企业管理」页:企业对接参数 + 通道切换(1/2) + `institution_id` + 企业信息(7.5/7.6) + 企业余额(7.11) + 发票下载(7.14) + 消费流水查询。
- 订单详情页兼容「旧定聚字段 + 新企油通字段」两种展示。
- 旧「分油公司 / 分油用户」菜单:保留只读(历史数据),不再新增。
### 11.7 建议实施顺序
**A. 不依赖对接人环境料,可先做(脚手架 + 内部改造):**
1. DB 迁移脚本(`yoshop_partners_order` 加字段 + 新建 `yoshop_qyt_consume`)。
2. `config/qyt.php`(占位)+ `app/common/library` 单位/编号 helper。
3. `app/common/crypto/Qyt.php`(信封结构 + 7.15 算法已文档化可实现;业务接口拼签字段顺序留 TODO 待确认)。
4. `app/common/service/QytOil.php`(各接口入参组装 + 调用骨架)。
5. 改 `app/api/controller/OilCard.php`:`share` 加 `name`;新增 `cancelShare` method;`stock` 改内部数据源。
6. 改 `PartnersQueue` + 新增 `QytPollQueue`(`qyt:poll`) + `QytNotify` + `public/qytconsume.php` + 改 `CallbackQueue`。
7. 停用小程序油品链路(`PaySuccess::shareOil` 油券分支、`Order::qrCode/qrCodeImg`、`Station`、`store/Goods::stock`)。
8. admin-vue:`views/partners/order/**` + 新增「企业管理」页。
**B. 拿到对接人环境料后(R-19/R-20):**
9. 填 `config/qyt.php` 真值 + 补 `Qyt.php` 拼签字段顺序 + 7.16 凭证定位方式。
10. 测试环境联调:7.7 → 7.8 → 7.15 → 7.10(用 7.23 模拟消费)→ 7.16 撤销。
11. 全链路回归(下单→发放→轮询→核销→消费回调→回调下单平台→撤销)+ 旧数据展示。
**C. 收尾:**
12. 确认无引用后清理 `SybOil` / `IdingjuOil` / `crypto/{Crypto,Idingju}` / `config/crypto.php` / `app/api/controller/syb/*` / 旧入口文件。
---
## 附:本阶段已核查的关键文件清单
- 后端上游对接:`app/common/service/{SybOil,IdingjuOil,PartnersOil}.php`、`app/common/crypto/{Crypto,Idingju,RsaUtil}.php`、`config/{crypto,rsa}.php`
- 入站 API:`app/api/controller/OilCard.php`、`app/api/controller/SybNotify.php`、`app/api/controller/syb/*`、`app/api/controller/Station.php`
- 队列:`app/console/command/{CallbackQueue,PartnersQueue,NotifyQueue}.php`、`config/console.php`、`app/common/service/QueueService.php`
- 订单:`app/api/controller/Order.php`、`app/api/service/order/PaySuccess.php`、`app/api/service/main/order/PaySuccess.php`、`app/api/model/Order.php`、`app/api/model/partners/Order.php`、`app/common/model/partners/Order.php`
- 后台:`app/store/controller/{Order,Company,Partners}.php`、`app/store/controller/partners/Order.php`、`app/store/model/{Company,Partners}.php`、`app/common/enum/order/*`
- 入口:`public/{partners,partnersoil,partnersoiluse,oil,oiluse}.php`
- admin-vue:`src/config/router.config.js`、`src/api/{partners,company,order}/*`、`src/views/{company,partners,order,account}/**`
- mobile-uniapp:`config.js`、`api/{station,order,card,goods,checkout}.js`
- 数据库:`oil.sql` 中 `yoshop_{account,company,company_balance_log,partners,partners_goods,partners_order,order,order_goods,order_qrcode,order_record,request_log,merchant}` 建表与样例数据
- 文档:`企油通接口对接文档.md`(第 7 章 23 接口)、`慧明宝供应链平台接口文档V1.0.16.docx`(= 我们对下单平台的入站契约)
---
## 附 G. 后台功能适配 + 接口文档就地修订(2026-09-04)
### G1. 后台(app/store + admin-vue)适配
**下单平台订单管理**(运营日常用的「查看订单 / 处理订单」)
| 层 | 文件 | 改动 |
|---|---|---|
| 后端 | `app/store/controller/partners/Order.php` | **整体重写**。移除 `IdingjuOil` / `CompanyModel` / `getCompany()` 硬编码;`qrCode()` 改调 `QytOil::h5Url`(7.15);`share()` 改为「重新发放」——仅允许 `oil_status_zsy=3(发放失败)` 条件重置为 0 后重投 `partners_queue`(避免与在途单冲突);新增 `detail()` 返回订单 + `yoshop_qyt_consume` 加油流水;`oilCallback()/useCallback()` 保留为「重推结果给下单平台」并加状态前置校验 |
| 后端 | `app/store/model/partners/Order.php` | `getFilter()` 新增检索:司机姓名 `driver_name`、单号 `orderNo`(油卡编号/销售单号/批次号/运单号 模糊)、企油通发放态 `oil_status_zsy` |
| 前端 | `views/partners/order/Index.vue` | 搜索区加「司机姓名 / 单号 / 企油通发放态」,「用户电话」改「司机手机号」;列表新增「企油通单号(批次/运单)」「司机(姓名+手机)」「加油信息(油站/油品/升数/实付)」列,去掉旧「分油记录编号」列;状态列增加企油通发放态标签 + `oil_text` 失败原因;按钮文案改为 重新发放 / 加油链接 / 重推发放结果 / 重推加油结果 |
| 前端 | `views/partners/order/modules/PayForm.vue` | **重写**:原来把二维码串塞 `<iframe>`,现改为用 `vue-qr` 渲染 7.15 H5 免登地址二维码 + 只读链接框 + 一键复制(运营可直接把链接发给司机) |
| 前端 | `modules/{ShareForm,OilForm,UseForm}.vue`、`api/partners/order.js` | 标题与注释文案同步 |
> `views/partners/**`(合作商列表/新增/编辑/对接参数/余额)维护的是**下游接口账户**(orgNo / appKey / RSA 公私钥 / 回调地址 / 可售商品与额度),与上游换成企油通无关,**无需改动**;其中「商品额度」正是 `/oilCard/stock` 现在的数据来源。
**停用旧上游依赖的后台/小程序端点**(上游定聚、智慧油客已下线,这些接口再调只会超时或报错,统一改为明确提示,页面仍可查历史数据)
| 文件 | 方法 | 处理 |
|---|---|---|
| `app/api/controller/Order.php` | `qrCode()` / `qrCodeImg()` | 返回「该功能已停用」(小程序不改代码,只是拿到提示) |
| `app/api/controller/Station.php` | `list()` | 返回空列表(企油通无油站查询能力),小程序页面不报错 |
| `app/store/controller/Order.php` | `share()` / `cancelShare()` / `qrCode()` | 返回「小程序油品链路已停用」(顺带修掉了原先调用未定义方法的致命错误) |
| `app/store/controller/Company.php` | `stock()` | 返回「上游库存同步已停用」 |
| `app/store/controller/Goods.php` | `onReloadStock()` | 同上 |
| `app/store/controller/User.php` | `add()` / `oil()` | 分油用户池已停用,不再向上游注册用户 / 手动分油 |
> 其余 `new SybOil()` 残留经核对**均只用于写日志**(`saveLogStore`),无业务副作用;`PaySuccess::shareOil` 内的 `IdingjuOil` 调用位于已 `continue` 的分支,不可达。
### G2. 接口文档就地修订(不加变更说明章节)
`慧明宝供应链平台接口文档V1.0.17.docx` **直接在原文覆盖修改**,未新增任何「变更说明 / 附录」章节,并删除本期无用的接口与参数。
**版本 / 接口层面**
| # | 位置 | 改动 |
|---|---|---|
| 1 | 1.2 版本号 | V1.0.16 → **V1.0.17** |
| 2 | **3.4 基础数据查询 / 油站列表查询** | **整章删除**(企油通无油站查询能力;含请求应答表与报文示例,约 277KB) |
| 3 | 2.7 接口清单列表 | 删「油站查询」行;序号 5→4;付款码申请用途改为「获取司机免登加油地址(H5)」 |
| 4 | 目录 | 删除对应两条 TOC 条目 |
最终对外接口收敛为 4 个:`syb.oil.oilCard.share` / `syb.oil.oilCard.shareList` / `syb.pay.qrCode.request` / `syb.notify.oilCard.change`(另有正文中的 `stockList`、`orderStatus` 两个查询接口)。
**参数层面(去掉本期无用的传参)**
| 接口 | 删除 / 修改 |
|---|---|
| `syb.oil.oilCard.share` | **删请求体 `amount`**(金额由 `goodsNo` 面额决定,本期不传);**新增必填 `name`**(司机姓名 V50);`goodsNo` 约束 `?`→`1`;使用场景⑵ 改为「同步返回受理结果,发放结果由油卡变动通知推送(optType=share_oil)或用订单状态查询主动查」;请求报文示例同步删 `amount` 与注释行、补 `"name"` |
| `syb.pay.qrCode.request` | **删请求体 `longitude` / `latitude` / `oilCardType`** 三个参数(企油通 H5 免登不需要);API 描述改为「获取司机免登加油地址(H5)」;使用场景改写;返回值 `qrCode` 描述由「二维码链接」改为「**H5免登加油地址**」并去掉「2分钟有效/扫一次失效」旧口径;请求与应答报文示例同步更新(应答示例换成企油通 H5 地址样例) |
| `syb.oil.oilCard.stockList` | 使用场景改为「查询自身可下单额度与用量统计(平台内部额度,非上游实时库存)」 |
| `syb.oil.oilCard.shareList` | 返回字段 `num` 口径由「分油数量(升)」修正为「**购买数量(张)**」,与实现一致 |
| `syb.notify.oilCard.change` | `optType` 删除未使用的 `发券:share_volume`,仅保留 `share_oil` / `add_oil` |
**校验**:XML 良构 ✓、XSD `validate.py` 全部通过 ✓、段落 1717 → 1392、残留检查 `gasStation`/`油站`/`share_volume`/`二维码链接`/`longitude`/`latitude`/「变更说明」均为 **0**。
**仍未做**:本机无 LibreOffice,无法渲染 PDF 目视核对排版;删章后小节序号由 Word 样式自动重排,打开时建议「更新域」刷新目录。
---
## 附 H. 下单改为按金额直传(2026-09-06)
下单平台不再传 `goodsNo`(商品编码),改为**直接传 `amount`(金额,元)**。商品/面额概念从下单链路整体移除。
### 后端
| 文件 | 改动 |
|---|---|
| `app/api/controller/OilCard.php` | `share()` 入参 `goodsNo` → **`amount`**(必填,正则校验:元、≤2 位小数、>0);**删除**商品查找、`goods_sku` 平台库存校验、`partners_goods` 合作商额度校验、下单扣库存;落单金额三字段(`goods_price`/`total_price`/`total_pay_price`)统一取 `amount`;返回 `amount` 用传入值 |
| 同上 | **删除整个 `stock()` 方法**(`syb.oil.oilCard.stockList` 完全建立在 `goodsNo` 之上,已无意义);删除已失效的 `updateGoodsStockNum()`、`getCompany()`;清理 6 个不再使用的 `use` |
| 同上 | `shareList` 响应去 `productName`;`orderStatus` 响应去 `province` / `goods_name` / `goods_no`(这些字段现在恒为空) |
| `app/api/model/partners/Order.php` | `saveOrder()` 去掉 `PartnersGoods` 的 `setIncUseNum` / `setDecStockNum`,事务包装随之取消(单条写入无需事务) |
| `app/store/model/partners/Order.php` | 列表查询去掉 `goods.images.file` 关联与 `goods_no` 检索项 |
### 前端 admin-vue
`views/partners/order/Index.vue`:删除「商品编码」搜索项、「商品信息」列与 `GoodsItem` 组件引用;列名「油卡数量/油卡金额」→「数量/发放金额」,汇总文案→「发放总金额」。eslint **0 error**(warning 数与改动前一致)。
### 接口文档(就地修订,无变更说明章节)
| 位置 | 改动 |
|---|---|
| 3.2.1 用户分油 请求体 | **删 `goodsNo`**,**新增 `amount`**(必填,String V13,「本单发放给司机的金额,单位元,最多两位小数且必须大于 0」);请求示例同步 |
| **油券库存查询 `syb.oil.oilCard.stockList`** | **整节删除** + 目录条目删除 |
| 分油记录查询 | 响应删 `productName` 字段与示例行 |
| 订单状态查询 | 响应删 `province` / `goods_name` 字段行;两处 JSON 示例删对应 3 行;`oil_status_zsy` 描述由「中石油下单状态 1待处理/2已发券/3发券失败」改为「**发放状态:0未提交 / 2发放成功 / 3发放失败**」 |
校验:XML 良构 ✓、XSD 全过 ✓、段落 1717 → **1234**;残留检查 `goodsNo`/`stockList`/`油券库存`/`productName`/`goods_name`/`goods_no`/`province`/`中石油` 均为 **0**。
最终 `share` 请求体仅剩 6 个参数:`orgNo` / `amount` / `mobile` / `name` / `saleOrderNo` / `oilCardType`。
### 额度管控:改用合作商余额(已实现)
去掉 `goodsNo` 后,原先由 `yoshop_partners_goods.stock_num` 承担的下单额度管控,改由 **`yoshop_partners.balance`(合作商余额)** 承担。
**新增** `app/common/service/PartnersBalance.php`:
| 方法 | 说明 |
|---|---|
| `deduct()` | **原子扣减**:`UPDATE yoshop_partners SET balance = balance - amt WHERE partners_id=? AND balance >= amt`,`affected=1` 才算成功 —— 天然防并发超扣。成功返回流水ID |
| `refund()` | 回补余额并写退款流水 |
| `bindOrder()` | 建单后把 `order_id` 回填到扣款流水(`deduct` 时还没有订单ID) |
| `balance()` | 查询当前余额 |
流水写入 `yoshop_partners_balance_log`,场景沿用现有枚举:扣减 = `CONSUME(20)`,回补 = `REFUND(40)`;金额拼接前用 `number_format` 规范化,避免注入。
**接入点**
| 位置 | 行为 |
|---|---|
| `OilCard::share` | **幂等检查通过后**才扣款(重复 `saleOrderNo` 直接返回原单,不重复扣);扣款失败 → `2028 账户余额不足`;建单抛异常或未成功 → 立即回补;建单成功 → `bindOrder` 回填订单ID |
| `PartnersQueue` | 三条发放失败路径(缺司机信息 / 7.7 调用失败 / `data.status=2`)在**状态条件跃迁 `affected=1` 时**回补余额,天然幂等 |
| `store/controller/partners/Order::share`(后台重新发放) | 失败时余额已回补,重发前**重新扣减**;若状态被其它进程改变导致重置失败 → 回补刚扣的金额 |
**资金一致性**:先扣后建单、失败即回补;所有回补都挂在「条件跃迁胜出」之后,重复消息 / 多进程不会重复回补。
**文档**:`amount` 字段取值说明补充「下单时平台校验并扣减对接公司账户余额,余额不足返回错误码 2028;发放失败时该笔金额原路回补」。错误码 `2028 账户余额不足` 原本就在附录 4.2 中,无需新增。
**运营入口**:后台「合作商管理 → 充值」已可维护余额,「合作商 → 余额明细」可查流水,无需新增页面。
### 合作商「可售商品 + 库存」下线(已清理)
额度改由余额承担后,`yoshop_partners_goods` / `yoshop_partners_stock_log` 已无任何读写方,整体移除。
**删除的文件(5 个 PHP + 1 个 Vue)**
`app/api/model/PartnersGoods.php`、`app/common/model/PartnersGoods.php`、`app/store/model/PartnersGoods.php`、`app/common/model/partners/StockLog.php`、`app/store/model/partners/StockLog.php`、`admin-vue/src/views/partners/modules/MultiGoods.vue`
**改动的文件**
| 文件 | 改动 |
|---|---|
| `app/store/model/Partners.php` | `add()` / `edit()` 去掉 `checkGoodsData`、`PartnersGoodsModel::increased/updates`、`StockLogModel::increased` 及两个 import |
| `app/store/controller/Partners.php` | `detail()` 不再返回 `goodsList`(连同 `allot_num` 填充逻辑),去 import |
| `admin-vue/views/partners/{Create,Update}.vue` | 移除 `<MultiGoods>` 组件、`goodsList` data、提交时的 `values.goodsList`、`handleGoodsChange`、import 与 components 注册 |
| `admin-vue/views/partners/modules/index.js` | 移除 MultiGoods 导入导出 |
| `admin-vue/common/model/partners/Index.js` | 移除 `GoodsApi` import、`formData.goods`、`getGoodsList()` 方法及其在 `getFromData` 中的调用 |
**数据库**:`qyt_migration.sql` 第 4 节把两张表注释标记为「已下线,仅历史数据」,并附上 `DROP TABLE` 语句(**已注释**)。两表仍有历史数据,是否物理删除由你决定,确认后取消注释执行即可。
校验:PHP 端 `PartnersGoods` / `StockLog` / `partners_goods` / `partners_stock_log` 全仓残留 **0**;admin-vue `MultiGoods` / `goodsList` 残留 **0**,eslint **0 problems**。
---
## 附 I. 后台「分油公司 / 分油用户」菜单下线(2026-09-06)
企油通改为单企业配置(`config/qyt.php`)、分油用户池取消后,这两个菜单对应的 `yoshop_company` / `yoshop_account` 已无业务意义,后端接口也早已返回「已停用」。本次把前端入口整体收掉。
### 删除
| 类型 | 路径 |
|---|---|
| 页面 | `views/company/`(Index / Create / Update / balance / modules 共 8 个文件 + style.less)、`views/account/Index.vue` |
| API | `api/company/{index,balance,recharge}.js`、`api/account/index.js` |
| 模型 / 枚举 | `common/model/company/Index.js`、`common/enum/company/**`(3 个) |
| 用户分油弹窗 | `views/user/modules/OilForm.vue`(依赖已停用的 `/user/oil`) |
### 改动
| 文件 | 改动 |
|---|---|
| `config/router.config.js` | 移除整个 `/company` 路由块(含嵌套的 `/account`),菜单「分油公司」「分油用户」「公司余额明细」一并消失 |
| `common/model/user/Index.js` | 去 `CompanyApi` import、`formData.companyList`、`getCompanyList()` 及其在 `getFromData` 的调用 |
| `views/user/{Create,Update}.vue` | 移除表单里的「分油公司」下拉(数据源已不存在) |
| `views/user/Index.vue` | 移除 `OilForm` 组件、import、注册、`handleOil()` 与那行已注释的「分油」按钮 |
| `views/user/modules/index.js` | 移除 OilForm 导入导出 |
| `api/user/index.js` | 移除 `oil: '/user/oil'` 与 `oil()` 方法 |
### 校验
- 残留引用检查:`views/company` / `api/company` / `model/company` / `enum/company` / `views/account/Index` / `api/account` 均为 **0**;`OilForm` 仅剩合作商订单页那个(不同文件,需保留)。
- eslint:本次改动引入 **0 新错误**。改动过程中在 `views/partners/{Create,Update}.vue` 留下的 3 处 `comma-dangle` 已修复。当前全量仅剩 2 个**改动前就存在**的错误:`config/router.config.js` 的 `comma-dangle`、`views/user/grade/modules/index.js` 的 `eol-last`(两者均未被本次改动触碰)。
### 后端未动(说明)
`app/store/controller/{Company,Account,User}.php` 及 `yoshop_company` / `yoshop_account` 的模型仍在仓库中,相关方法已返回「已停用」提示,现在没有任何前端调用方。保留是为了不影响历史数据查询与可能的权限表(`yoshop_store_api` / `yoshop_store_menu`)记录;如需彻底清除,可在确认权限表无关联后单独处理。