日期: 2026-08-30
状态: 已确认设计(方案 1:最小改动)
范围: web-admin 运输计划表单 / 合同完善页 + backend 计划段次写入与补合同接口
现状:装货/送货非内置客户可多选合同;装货「添加产品」走全量 CRM 商品库,与合同无关。业务要求:
isPlInternalCustomerName / is_internal_customer_name)不限制产品选择,且仍免合同。| 项 | 决策 |
|---|---|
| 产品约束范围 | 仅装货「添加产品」;送货从装货同步,自然受装货约束 |
| 换合同 | 仅移除「不在新合同内」的装货产品行,保留仍匹配的 |
| 历史多合同 | 打开时提示;用户手动删到 1 份;保存强校验 |
| 合同完善页 | 与计划表单同一规则(每段最多 1 份) |
| 校验落点 | 前端体验 + 后端 API 兜底 |
| 实现路径 | 方案 1:不改 contracts[] 模型,业务上限为 1 |
| 数量不平衡 | 保持现状:仅告警,可暂存 |
内置客户全称(与现网一致):
安庆市山峡化工有限公司安徽云栋化工有限公司contracts[] 改为单字段 contractIdPlPlanContractMultiSelectContractSelector 改为单选(去掉 multiple / @confirm-multiple,走单份确认)。maxContracts = 1;再选新合同为替换,非追加。modelValue.length > 1(历史数据)→ 警告「只能保留一份合同,请移除多余合同后再保存」。PlPlanLoadSection:非内置且未选合同时,「添加产品」禁用并提示先选合同。ProductSelector。ProductSelector 增加可选 prop(如 allowedProductIds?: string[]):有值时列表仅展示/允许勾选这些 id(优先前端过滤已加载的合同明细,不强制改商品列表 API)。PlPlanFormDialog 打开选择器时:非内置传入当前装货合同产品 id;内置不传。productId 集合过滤装货 products,再走现有送货同步/清理逻辑。PlPlanDeliverySection:PlPlanStopProductTable 的 allow-remove 改为 true。contracts.length > 1 → 一律拦截(表单保存 / 安排运输 / 合同完善保存)。contracts.length === 0 → 保持现网:计划草稿/暂存可不选合同;「安排运输」与「合同完善」保存仍按现有必填逻辑。productId 必须属于该合同明细;否则拦截。未选合同时不做产品归属校验(与「可先建计划再补合同」一致);但若未选合同却已有装货产品,添加入口应在选合同时再按新合同过滤(见换合同规则)。validate_plan_stops_write(创建/更新计划 full 写入前)update_plan_stop_contracts(PUT .../stop-contracts)validate_plan_party_contracts / side contracts,同样加「最多 1 份」合同份数(非内置)
len(contracts) > 1 → 422,文案如:装货合同只能选择 1 份 / 送货段#N合同只能选择 1 份装货产品归属(非内置且已选合同)
product_id 集合productId 的必须 ∈ 该集合422,文案如:装货产品「xxx」不在所选合同明细中更新(2026-08-30): 送货段产品归属校验与「选择产品」已由
2026-08-30-pl-plan-delivery-contract-products-design.md覆盖;以上「送货不做校验」条款废止。
合同完善接口
contracts 长度 ≤ 1(及现有客户匹配、可选状态)validate_plan_stops_write 一致,优先 HTTP 422detail 直接展示,不吞错选装货客户(非内置)
→ 选择 1 份合同
→ 添加产品(仅合同 productId)
→ 写入装货产品 → 同步到送货段
→ 送货段可删除不需要的行(可再「从装货同步」)
→ 换合同:过滤装货不匹配行 → 影响送货同步
→ 保存:前端校验 → 后端校验
| 情况 | 处理 |
|---|---|
| 打开编辑时已有 >1 份合同 | 提示;允许查看/移除;保存前必须 ≤1 |
| 合同无产品明细 | 允许选合同;添加产品列表为空并提示 |
产品无 productId(脏数据) |
非内置时保存拦截 |
| 装货删产品 | 送货对应行按现有联动处理 |
| 仅送货删产品 | 装货不变;数量不平衡仍只告警 |
| 客户从非内置改为内置 | 清空合同;产品保留 |
| 客户从内置改为非内置 | 要求选合同;保存时校验产品归属 |
stop-contracts 传 2 份 → 失败前端
web-admin/src/components/business/pl-plan-contract-multi-select/index.vueweb-admin/src/components/business/pl-plan-load-section/index.vueweb-admin/src/components/business/pl-plan-delivery-section/index.vueweb-admin/src/components/business/pl-plan-form-dialog/index.vueweb-admin/src/components/business/product-selector/index.vueweb-admin/src/views/pl/plan-contract/index.vue(保存校验对齐)后端
backend/app/services/pl_plan_stop_service.py(validate_plan_stops_write / _resolve_stop_contracts)backend/app/services/pl_plan_contract_gap.py(update_plan_stop_contracts)backend/app/services/pl_plan_service.py(若 side contracts 仍在用)backend/tests/test_pl_plan_stop_*.py / 合同相关单测增补采用方案 1(最小改动):保留 contracts[],业务层上限为 1。相对方案 2(专用合同产品弹窗)改动更小;相对方案 3(模型改单字段)避免迁移与大范围兼容成本。
>1 全路径强拦;===0 保持可先建计划再补合同。