Files
shanxia2.0/docs/superpowers/specs/2026-08-26-pl-multi-stop-redesign-design.md
T
2026-08-27 14:23:18 +08:00

14 KiB
Raw Blame History

运输计划 · 一车多卸重构 — 设计文档

日期: 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;页内按段分区
送提货单同客户 合并;地址取首段