14 KiB
运输计划 · 一车多卸重构 — 设计文档
日期: 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):
- 创建
pl_plan_load:supplier_id/name→ customer;pickup_*→ address/time/contact/notes - 创建
pl_plan_deliveryseq=1:customer_id/name、destination_*→ 送货地址 - 复制
pl_plan_product→load_product+delivery_product(数量用原inbound_estimate/outbound_estimate或quantity) - 迁移
pl_plan_contract:supplier→load,demander→delivery(挂 stop_id) - 内部客户:按 D5/D6 设置
generate_dibang - 不修改已归档计划的归档数量(从原
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;页内按段分区 |
| 送提货单同客户 | 合并;地址取首段 |