382 lines
14 KiB
Markdown
382 lines
14 KiB
Markdown
# 运输计划 · 一车多卸重构 — 设计文档
|
||
|
||
> **日期**: 2026-08-26
|
||
> **状态**: 待审阅(brainstorming 已确认核心决策)
|
||
> **前置**: `docs/运输计划管理-需求说明文档.md`、`docs/superpowers/specs/2026-06-25-pl-plan-archive-design.md`、`docs/superpowers/specs/2026-06-21-pl-arrange-transport-refine-design.md`
|
||
> **示意稿**: `docs/fresh-mint-pl-multi-stop-understanding-demo.html`、`docs/fresh-mint-pl-multi-stop-print-rules-demo.html`
|
||
|
||
## 目标
|
||
|
||
支持**一车多卸**:一个运输计划可包含 **1 个装货段 + N 个送货段**;保留计划级基础信息、产品总量、运输信息与运费;将原「供方/需方」拆为装货/送货停靠段,并联动归档、地磅、司机/现场链接、打印与运费结算。
|
||
|
||
---
|
||
|
||
## 已确认决策(2026-08-26)
|
||
|
||
| # | 决策 | 说明 |
|
||
|---|------|------|
|
||
| D1 | **装货仅 1 段** | 不支持一车多装;取货/装货信息唯一 |
|
||
| D2 | **送货多段** | 支持多客户、多地址、多合同 |
|
||
| D3 | **数量校验** | 装货合计 / 送货合计 / 计划产品总量不平衡时 **仅告警**,允许暂存 |
|
||
| D4 | **内部主体** | `安庆市山峡化工有限公司`、`安徽云栋化工有限公司`(`is_internal_customer_name`) |
|
||
| D5 | **装货 + 内部主体** | 免合同;对公司=**出库** → 默认勾选 **地磅出库单** |
|
||
| D6 | **送货 + 内部主体** | 免合同;对公司=**入库** → 默认勾选 **地磅入库单** |
|
||
| D7 | **归档字段命名** | 装货段固定 **入库数量/单价/金额**;送货段固定 **出库数量/单价/金额**(不因客户切换字段名) |
|
||
| D8 | **归档粒度** | **整单一起归档**;所有段数据齐备后一次提交 |
|
||
| D9 | **司机链接** | **1 个/计划**;页内按装货 + 各送货段分别上传附件 |
|
||
| D10 | **现场链接** | **1 个/计划**;**仅展示**山峡/云栋且勾选地磅的段;外部客户段不展示;先数量后附件 |
|
||
| D11 | **送提货单打印** | 多送货客户时先选客户;**同客户多段合并一张**;地址/联系人取该客户 **第一个送货段**;地址不同不拆单 |
|
||
| D12 | **入库单打印** | **送货单位** = 装货客户(唯一) |
|
||
| D13 | **分装单打印** | 不考虑客户信息 |
|
||
| D14 | **运费** | 仍计划级;新增 **`freight_remark` 运费备注** |
|
||
| D15 | **历史迁移** | 旧计划自动拆为 **1 装货 + 1 送货** |
|
||
|
||
---
|
||
|
||
## 非目标(本期不做)
|
||
|
||
- 一车多装(装货多段)
|
||
- 分段先后归档
|
||
- 运费按装货/送货段拆分
|
||
- 分装单按客户筛选
|
||
- CRM / 地磅系统本体改造(仅联动字段与生成规则)
|
||
|
||
---
|
||
|
||
## 一、业务模型
|
||
|
||
### 1.1 典型场景(客户举例)
|
||
|
||
供应商装 **30t 硫酸** → A 客户 **10t** → B 客户 **15t** → 山峡入库 **5t**。
|
||
|
||
```
|
||
pl_plan(计划总量 30t)
|
||
├── pl_plan_load(装货 ×1) XX 供应商 · 30t
|
||
└── pl_plan_delivery(送货 ×N)
|
||
├── #1 A 客户 · 10t
|
||
├── #2 B 客户 · 15t
|
||
└── #3 山峡 · 5t(内部入库地磅)
|
||
```
|
||
|
||
### 1.2 保留 vs 改造
|
||
|
||
| 区域 | 处理 |
|
||
|------|------|
|
||
| 基础信息 | **保留**于 `pl_plan` |
|
||
| 产品信息 | **保留**于 `pl_plan_product`(计划总量主数据) |
|
||
| 运输信息 | **保留**于 `pl_plan` + `pl_transport`(安排后) |
|
||
| 供方/需方 | **废弃 UI**;数据迁入装货/送货段 |
|
||
| 取送地址 | **下沉**至装货段 / 各送货段 |
|
||
| 运费 | **保留计划级** + `freight_remark` |
|
||
|
||
### 1.3 `plan_type` 处理
|
||
|
||
- 用户 **不再手选** 直销/入库/出库。
|
||
- 系统根据段组合 **推导展示 Tag**(列表/查看用),规则示例:
|
||
- 存在内部装货段 → 含「出库」
|
||
- 存在内部送货段 → 含「入库」
|
||
- 仅外部装货+外部送货 → 「直销/调拨」
|
||
- 数据库 `pl_plan.plan_type` **保留列**(兼容报表/迁移),由段推导写入或置 `direct` 兜底。
|
||
|
||
---
|
||
|
||
## 二、数据模型
|
||
|
||
### 2.1 新增表
|
||
|
||
#### `pl_plan_load`(装货段,plan 1:1)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | VARCHAR(64) PK | UUID |
|
||
| plan_id | VARCHAR(64) | 计划 ID,唯一(每计划一条) |
|
||
| customer_id | VARCHAR(64) | 装货客户 |
|
||
| customer_name | VARCHAR(200) | 冗余 |
|
||
| address | VARCHAR(500) | 装货地址 |
|
||
| lng/lat | DECIMAL | 坐标 |
|
||
| load_time | DATETIME | 装货时间 |
|
||
| contact | VARCHAR(200) | 联系方式 |
|
||
| notes | VARCHAR(500) | 注意事项 |
|
||
| generate_dibang | TINYINT | 是否生成地磅(内部默认 1) |
|
||
| dibang_direction | VARCHAR(10) | `outbound`(内部装货)/ `none` |
|
||
| 公共字段 | … | tenant_no … del_flag |
|
||
|
||
#### `pl_plan_load_product`(装货产品行)
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| load_id / plan_id | 外键 |
|
||
| plan_product_id | 关联计划产品主行 |
|
||
| product_id / product_name / model / unit | 快照 |
|
||
| plan_qty | 本段计划数量 |
|
||
| package_spec | 包装规格(可覆盖) |
|
||
| delivery_method | 送货方式(可覆盖) |
|
||
| remark | 段内备注 |
|
||
| inbound_qty / inbound_price / inbound_amount | **归档**(装货侧入库字段) |
|
||
|
||
#### `pl_plan_delivery`(送货段,plan 1:N)
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | VARCHAR(64) PK | UUID |
|
||
| plan_id | VARCHAR(64) | 计划 ID |
|
||
| seq | INT | 顺序(打印、链接展示) |
|
||
| customer_id / customer_name | | 送货客户 |
|
||
| address / lng / lat | | 送货地址 |
|
||
| delivery_time | DATETIME | 送货时间 |
|
||
| contact / notes | | 联系 / 注意事项 |
|
||
| generate_dibang | TINYINT | 内部默认 1 |
|
||
| dibang_direction | VARCHAR(10) | `inbound`(内部送货)/ `none` |
|
||
| 公共字段 | … | |
|
||
|
||
#### `pl_plan_delivery_product`(送货产品行)
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| delivery_id / plan_id | 外键 |
|
||
| plan_product_id | 关联计划产品 |
|
||
| plan_qty / package_spec / delivery_method / remark | 段内可编辑 |
|
||
| outbound_qty / outbound_price / outbound_amount | **归档**(送货侧出库字段) |
|
||
|
||
#### `pl_plan` 新增字段
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| freight_remark | VARCHAR(512) 运费备注 |
|
||
|
||
### 2.2 合同关联
|
||
|
||
扩展 `pl_plan_contract`:
|
||
|
||
| 变更 | 说明 |
|
||
|------|------|
|
||
| `side` | 新增枚举 `load` / `delivery`(保留 `supplier`/`demander` 只读兼容旧数据) |
|
||
| `stop_id` | VARCHAR(64),装货段 ID 或送货段 ID |
|
||
|
||
规则:内部客户段 **不强制合同**;外部客户段 **至少选一个合同**(与现逻辑一致)。
|
||
|
||
### 2.3 链接与回传存储
|
||
|
||
**司机附件(按段)** — 新增 `pl_transport_driver_feedback` 或扩展现有司机回传 JSON:
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| transport_id / plan_id | |
|
||
| stop_type | `load` / `delivery` |
|
||
| stop_id | 段 ID |
|
||
| attachment_urls | TEXT JSON |
|
||
| remark | 可选 |
|
||
|
||
**现场回传(仅内部+地磅段)** — 扩展 `pl_transport_site_link` 或等价结构:
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| stop_type / stop_id | 关联装货或送货段 |
|
||
| quantity | 先填数量 |
|
||
| site_feedback_images | 后传附件 |
|
||
| business_type | 入库/出库(地磅方向) |
|
||
|
||
**Token 策略(D9/D10)**:
|
||
|
||
- `pl_transport.driver_access_token`:**1 个/运输单**
|
||
- 现场:运输单级 **1 个 site token**(可复用现有 transport 级 token 字段);公网页按 `generate_dibang=1` 且内部客户的段渲染分区;**不展示外部段**
|
||
|
||
### 2.4 地磅联动
|
||
|
||
`pl_transport_db_link` 增加 `stop_type`、`stop_id`,按段生成地磅计划:
|
||
|
||
| 段 | 客户 | generate_dibang | 地磅业务类型 |
|
||
|----|------|-----------------|--------------|
|
||
| load | 外部 | 0 | — |
|
||
| load | 内部 | 1 | **出库** |
|
||
| delivery | 外部 | 0 | — |
|
||
| delivery | 内部 | 1 | **入库** |
|
||
|
||
安排运输弹窗:按段列出勾选状态,可改;调整运输时沿用现有 revoke 规则。
|
||
|
||
---
|
||
|
||
## 三、表单(`PlPlanFormDialog`)
|
||
|
||
### 3.1 区域顺序
|
||
|
||
```
|
||
基础信息 → 产品信息 → 装货信息(单段)→ 送货信息(可增删)→ 运输信息(安排前只读/占位)
|
||
```
|
||
|
||
### 3.2 装货信息
|
||
|
||
- 客户选择 → 合同多选(内部免选)
|
||
- 产品表:从 `pl_plan_product` 带出,可改 `plan_qty`、包装、送货方式、备注
|
||
- 地址 / 时间 / 联系 / 注意事项
|
||
- 勾选「生成地磅单」:外部默认关;内部默认开(出库)
|
||
|
||
### 3.3 送货信息
|
||
|
||
- **+ 添加送货段**;每段独立客户、合同、产品行、地址信息
|
||
- 内部客户:免合同 + 默认入库地磅
|
||
- 删除段、排序(`seq`)
|
||
|
||
### 3.4 保存校验
|
||
|
||
| 校验 | 行为 |
|
||
|------|------|
|
||
| 装货段缺失 | **阻断** |
|
||
| 送货段 0 条 | **阻断** |
|
||
| 数量不平衡 | **ElMessage.warning**,允许保存 |
|
||
| 外部段无合同 | **阻断**(提交/安排时) |
|
||
|
||
### 3.5 列表列调整(建议)
|
||
|
||
- 「客户」列改为摘要:`装货: XX / 送货: A,B,山峡` 或主送货客户 + `等N家`
|
||
- 保留车牌、跟进状态等
|
||
|
||
---
|
||
|
||
## 四、安排运输
|
||
|
||
- 仍 **一车一次** `create_transport_for_plan`
|
||
- 地磅勾选汇总各段 `generate_dibang`
|
||
- 生成司机 token ×1、现场 token ×1
|
||
- 入库/出库类型计划 IO 单:按内部段生成(与地磅方向一致)
|
||
- 危化证 `hazard_cert_ready`:**仍计划级**,安排前校验
|
||
|
||
---
|
||
|
||
## 五、司机 / 现场公网页
|
||
|
||
### 5.1 司机页(1 URL)
|
||
|
||
```
|
||
[装货] {customerName} — 附件上传区
|
||
[送货 #1] {customerName} — 附件上传区
|
||
[送货 #2] ...
|
||
```
|
||
|
||
### 5.2 现场页(1 URL)
|
||
|
||
仅渲染满足 `is_internal_customer_name && generate_dibang` 的段:
|
||
|
||
```
|
||
[装货·山峡出库] 数量 → 附件
|
||
[送货·山峡入库] 数量 → 附件
|
||
```
|
||
|
||
外部客户段 **不出现**。
|
||
|
||
---
|
||
|
||
## 六、打印
|
||
|
||
| 单据 | 入口 | 数据规则 |
|
||
|------|------|----------|
|
||
| **送(提)货单** | 计划列表/查看/安排 | 送货客户数 ≥2 → `PlPlanDeliveryCustomerSelectDialog`;同 `customer_id` 合并产品行;地址/联系人取 **seq 最小** 的送货段;忽略地址差异 |
|
||
| **入库单** | 同上 | **送货单位** = `pl_plan_load.customer_name`;产品取装货段或计划总量(实现时与模板对齐) |
|
||
| **分装单** | 同上 | 不读客户;按计划产品 |
|
||
|
||
涉及文件:
|
||
|
||
- `web-admin/src/components/business/pl-delivery-slip-print-dialog/`
|
||
- `web-admin/src/components/business/pl-inbound-slip-print-dialog/`
|
||
- `web-admin/src/components/business/pl-subpack-slip-print-dialog/`
|
||
- `web-admin/src/utils/pl-delivery-slip-template.ts` 等
|
||
|
||
---
|
||
|
||
## 七、归档(`PlPlanArchiveDialog`)
|
||
|
||
### 7.1 弹窗结构
|
||
|
||
```
|
||
上区
|
||
├── 装货归档(入库数量/单价/金额 × 产品行)
|
||
├── 送货归档 #1 … #N(出库数量/单价/金额 × 产品行)
|
||
└── 运费(计划级 + freight_remark)
|
||
|
||
下区:PlPlanViewTabs(含分段链接回传)
|
||
```
|
||
|
||
### 7.2 提交规则(D8)
|
||
|
||
- 装货 + **全部**送货段归档字段必填(含品牌分量若启用)
|
||
- **一次 API** 原子:写各段归档字段 + 计划运费 + `archive_status=archived` + `follow_status=completed`
|
||
- 已归档调整:同现有「调整」模式
|
||
|
||
### 7.3 库存 / 金额汇总
|
||
|
||
- 内部装货出库、内部送货入库:同步库存台账(沿用 `apply_archive_product_quantities` 思路,改为按段聚合)
|
||
- `pl_plan_product` 汇总字段可由段归档回写或仅展示段级(实现时 `_row_to_dict` 为权威)
|
||
|
||
---
|
||
|
||
## 八、运费结算
|
||
|
||
- **不变**:`pl_freight_settlement` 仍关联计划/运输;准入「已归档 + has_freight」
|
||
- **新增展示**:归档页、结算草稿可显示 `freight_remark`
|
||
- 未结算计划列表:客户摘要可展示多送货客户
|
||
|
||
---
|
||
|
||
## 九、历史数据迁移
|
||
|
||
脚本:`backend/scripts/migrate_pl_plan_to_multi_stop.py`(新建)
|
||
|
||
对每条 `pl_plan`(`del_flag=0`):
|
||
|
||
1. 创建 `pl_plan_load`:`supplier_id/name` → customer;`pickup_*` → address/time/contact/notes
|
||
2. 创建 `pl_plan_delivery` seq=1:`customer_id/name`、`destination_*` → 送货地址
|
||
3. 复制 `pl_plan_product` → `load_product` + `delivery_product`(数量用原 `inbound_estimate`/`outbound_estimate` 或 `quantity`)
|
||
4. 迁移 `pl_plan_contract`:`supplier`→`load`,`demander`→`delivery`(挂 stop_id)
|
||
5. 内部客户:按 D5/D6 设置 `generate_dibang`
|
||
6. **不修改**已归档计划的归档数量(从原 `pl_plan_product.inbound_*` / `outbound_*` 写入对应段产品行)
|
||
|
||
迁移后 `pl_plan` 上 `supplier_*` / `customer_*` / `pickup_*` / `destination_*` **保留冗余**(只读兼容),新写入走段表。
|
||
|
||
---
|
||
|
||
## 十、API 概要
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/transport/plan/{id}` | 返回 load + deliveries + products |
|
||
| POST | `/transport/plan/create-full` | Body 含 load、deliveries[] |
|
||
| PUT | `/transport/plan/{id}` | 全量更新段 |
|
||
| POST | `/transport/plan/{id}/archive` | Body 含各段归档明细 + freight_remark |
|
||
| GET | `/transport/plan/{id}/print-delivery-slip?customerId=` | 按客户聚合送提货单数据 |
|
||
|
||
Schema 权威:`_row_to_dict` + Pydantic `PlanLoadRead` / `PlanDeliveryRead`;跑 `check_field_consistency.py`。
|
||
|
||
---
|
||
|
||
## 十一、测试要点
|
||
|
||
| 场景 | 期望 |
|
||
|------|------|
|
||
| 30=10+15+5 保存 | 成功;不平衡时 warning |
|
||
| 装货山峡 | 默认出库地磅 |
|
||
| 送货山峡 | 默认入库地磅 |
|
||
| 送提货单 3 客户 | 弹出客户选择 |
|
||
| 同客户 2 段 | 合并 1 张;地址取第一段 |
|
||
| 司机页 | 全段可见;现场页仅内部段 |
|
||
| 归档 | 缺一段字段 → 阻断 |
|
||
| 迁移旧单 | 1 装货 + 1 送货;打印/归档可用 |
|
||
|
||
---
|
||
|
||
## 十二、界面模式
|
||
|
||
- 计划表单:P06 + `PlPlanFormDialog` 扩展
|
||
- 归档:P06 + `PlPlanArchiveDialog`
|
||
- 打印选客户:新小弹窗(P09 选型)
|
||
|
||
---
|
||
|
||
## 附录:决策对照(原文 vs 实现口径)
|
||
|
||
| 客户表述 | 实现口径 |
|
||
|----------|----------|
|
||
| 取货信息叫「入库」 | 归档字段名 `inbound_*`;内部装货时地磅为 **出库** |
|
||
| 送货信息叫「出库」 | 归档字段名 `outbound_*`;内部送货时地磅为 **入库** |
|
||
| 一个计划一个现场/司机链接 | transport 级 token;页内按段分区 |
|
||
| 送提货单同客户 | 合并;地址取首段 |
|