From 051c6b6feae871f426a06e9c126e6e62f4f17ecc Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 30 Aug 2026 10:40:49 +0800 Subject: [PATCH] docs: add design for plan single-contract and load products from contract Co-authored-by: Cursor --- ...pl-plan-single-contract-products-design.md | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-30-pl-plan-single-contract-products-design.md diff --git a/docs/superpowers/specs/2026-08-30-pl-plan-single-contract-products-design.md b/docs/superpowers/specs/2026-08-30-pl-plan-single-contract-products-design.md new file mode 100644 index 000000000..314800751 --- /dev/null +++ b/docs/superpowers/specs/2026-08-30-pl-plan-single-contract-products-design.md @@ -0,0 +1,180 @@ +# 运输计划:段次合同单选 + 装货产品限合同内 + +**日期:** 2026-08-30 +**状态:** 已确认设计(方案 1:最小改动) +**范围:** `web-admin` 运输计划表单 / 合同完善页 + `backend` 计划段次写入与补合同接口 + +## 1. 背景与目标 + +现状:装货/送货非内置客户可多选合同;装货「添加产品」走全量 CRM 商品库,与合同无关。业务要求: + +1. 非内置客户(非安庆山峡 / 安徽云栋)每段合同**只能选 1 份**。 +2. 装货「添加产品」**只能选该合同明细内的产品**。 +3. 内置客户(`isPlInternalCustomerName` / `is_internal_customer_name`)不限制产品选择,且仍免合同。 +4. 送货产品仍从装货同步;送货段**允许删除**本段产品(如装 3 送 1)。 +5. 前后端均强校验。 + +## 2. 已确认决策 + +| 项 | 决策 | +|----|------| +| 产品约束范围 | 仅装货「添加产品」;送货从装货同步,自然受装货约束 | +| 换合同 | 仅移除「不在新合同内」的装货产品行,保留仍匹配的 | +| 历史多合同 | 打开时提示;用户手动删到 1 份;保存强校验 | +| 合同完善页 | 与计划表单同一规则(每段最多 1 份) | +| 校验落点 | 前端体验 + 后端 API 兜底 | +| 实现路径 | 方案 1:不改 `contracts[]` 模型,业务上限为 1 | +| 数量不平衡 | 保持现状:仅告警,可暂存 | + +内置客户全称(与现网一致): + +- `安庆市山峡化工有限公司` +- `安徽云栋化工有限公司` + +## 3. 非目标 + +- 不把段次 `contracts[]` 改为单字段 `contractId` +- 不做送货区独立「添加产品」 +- 不做数量不平衡硬拦截 +- 不改库表结构 / 迁移脚本 + +## 4. 前端设计 + +### 4.1 `PlPlanContractMultiSelect` + +- `ContractSelector` 改为**单选**(去掉 `multiple` / `@confirm-multiple`,走单份确认)。 +- 默认 `maxContracts = 1`;再选新合同为**替换**,非追加。 +- 提示:未选 →「请选择合同」;已选 →「已选 1 份」。 +- `modelValue.length > 1`(历史数据)→ 警告「只能保留一份合同,请移除多余合同后再保存」。 +- 装货、送货、合同完善页共用该组件行为。 + +### 4.2 装货区与产品选择 + +- `PlPlanLoadSection`:非内置且未选合同时,「添加产品」禁用并提示先选合同。 +- 内置客户:行为不变,全量 `ProductSelector`。 +- `ProductSelector` 增加可选 prop(如 `allowedProductIds?: string[]`):有值时列表仅展示/允许勾选这些 id(优先前端过滤已加载的合同明细,不强制改商品列表 API)。 +- `PlPlanFormDialog` 打开选择器时:非内置传入当前装货合同产品 id;内置不传。 +- 合同变更:用合同明细 `productId` 集合过滤装货 `products`,再走现有送货同步/清理逻辑。 + +### 4.3 送货区 + +- `PlPlanDeliverySection`:`PlPlanStopProductTable` 的 `allow-remove` 改为 `true`。 +- 删除只影响该送货段,不回写装货。 +- 「从装货同步」可将装货产品补回该段。 + +### 4.4 保存前校验(表单 + 合同完善) + +- 非内置且 `contracts.length > 1` → **一律拦截**(表单保存 / 安排运输 / 合同完善保存)。 +- 非内置且 `contracts.length === 0` → **保持现网**:计划草稿/暂存可不选合同;「安排运输」与「合同完善」保存仍按现有必填逻辑。 +- 非内置且已选 1 份合同时:装货每个产品 `productId` 必须属于该合同明细;否则拦截。未选合同时不做产品归属校验(与「可先建计划再补合同」一致);但若未选合同却已有装货产品,添加入口应在选合同时再按新合同过滤(见换合同规则)。 +- 内置:跳过合同份数与产品归属校验。 + +## 5. 后端设计 + +### 5.1 挂点 + +1. `validate_plan_stops_write`(创建/更新计划 full 写入前) +2. `update_plan_stop_contracts`(`PUT .../stop-contracts`) +3. 若仍走旧 `validate_plan_party_contracts` / side contracts,同样加「最多 1 份」 + +### 5.2 规则 + +**合同份数(非内置)** + +- 规范化后 `len(contracts) > 1` → `422`,文案如:`装货合同只能选择 1 份` / `送货段#N合同只能选择 1 份` +- 内置客户:继续清空合同关联(现有逻辑) + +**装货产品归属(非内置且已选合同)** + +- 仅当装货段已选 1 份合同时执行;未选合同则跳过(允许先建计划再补合同) +- 取合同 CRM 明细 `product_id` 集合 +- 装货段产品(及实际落库权威产品行)凡带 `productId` 的必须 ∈ 该集合 +- 不在集合 → `422`,文案如:`装货产品「xxx」不在所选合同明细中` +- 内置客户:不做产品归属校验 +- **送货段产品**:不做「必须在送货合同内」校验(产品来自装货同步/删减;送货合同只管商务关联) + +> 说明:本设计将「每段可多选合同」收紧为最多 1 份,覆盖 `2026-08-20-pl-plan-contract-complete-design.md` 中「每侧可多选」的约定。 + +**合同完善接口** + +- 仅校验该段规范化后 `contracts` 长度 ≤ 1(及现有客户匹配、可选状态) +- 不在此接口改产品 + +### 5.3 错误策略 + +- 与邻近 `validate_plan_stops_write` 一致,优先 `HTTP 422` +- 前端用接口 `detail` 直接展示,不吞错 + +## 6. 数据流 + +``` +选装货客户(非内置) + → 选择 1 份合同 + → 添加产品(仅合同 productId) + → 写入装货产品 → 同步到送货段 + → 送货段可删除不需要的行(可再「从装货同步」) + → 换合同:过滤装货不匹配行 → 影响送货同步 + → 保存:前端校验 → 后端校验 +``` + +## 7. 边界情况 + +| 情况 | 处理 | +|------|------| +| 打开编辑时已有 >1 份合同 | 提示;允许查看/移除;保存前必须 ≤1 | +| 合同无产品明细 | 允许选合同;添加产品列表为空并提示 | +| 产品无 `productId`(脏数据) | 非内置时保存拦截 | +| 装货删产品 | 送货对应行按现有联动处理 | +| 仅送货删产品 | 装货不变;数量不平衡仍只告警 | +| 客户从非内置改为内置 | 清空合同;产品保留 | +| 客户从内置改为非内置 | 要求选合同;保存时校验产品归属 | + +## 8. 测试计划 + +### 后端单测 + +- 非内置 2 份合同 → 失败 +- 非内置 1 份合同 + 合同外产品 → 失败 +- 非内置 1 份 + 合同内产品 → 通过 +- 内置客户多产品无合同 → 通过 +- `stop-contracts` 传 2 份 → 失败 + +### 前端 / 手工验收 + +- [ ] 非内置:合同选择器只能确认 1 份;再选则替换 +- [ ] 非内置未选合同:添加产品禁用 +- [ ] 非内置已选合同:只能勾选合同内产品 +- [ ] 换合同:仅移除不匹配装货行 +- [ ] 送货可删产品;同步可补回 +- [ ] 历史多合同:提示 + 保存失败直到剩 1 份 +- [ ] 合同完善页同样最多 1 份 +- [ ] 内置客户:无合同限制,产品全量可选 + +## 9. 主要改动文件(预期) + +**前端** + +- `web-admin/src/components/business/pl-plan-contract-multi-select/index.vue` +- `web-admin/src/components/business/pl-plan-load-section/index.vue` +- `web-admin/src/components/business/pl-plan-delivery-section/index.vue` +- `web-admin/src/components/business/pl-plan-form-dialog/index.vue` +- `web-admin/src/components/business/product-selector/index.vue` +- `web-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` / 合同相关单测增补 + +## 10. 方案取舍说明 + +采用方案 1(最小改动):保留 `contracts[]`,业务层上限为 1。相对方案 2(专用合同产品弹窗)改动更小;相对方案 3(模型改单字段)避免迁移与大范围兼容成本。 + +## 11. 自检记录 + +- 无 TBD/TODO 占位。 +- 合同「必填」与「最多 1 份」已区分:仅 `>1` 全路径强拦;`===0` 保持可先建计划再补合同。 +- 送货不做合同内产品校验,与「仅约束装货添加」一致。 +- 范围可单次实现计划覆盖,无需再拆子项目。