|
|
@@ -0,0 +1,509 @@
|
|
|
+# 运输计划段次稳定 ID + 外围单据就地复用 Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 保存/调整运输时保留装送货段与计划产品行主键,地磅关联就地更新、司机/现场 token 不轮换,避免反复编辑后详情出现「暂无关联地磅单」。
|
|
|
+
|
|
|
+**Architecture:** 将 `replace_plan_stops` 与计划产品更新改为按 payload `id` upsert;调整运输改为按段 `upsert_stop_dibang_links`(不再整批 revoke 再建);默认不 rotate driver/site token。历史悬空 link 用运维脚本改挂(含 `PO20260904024`)。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2 + pytest;Vue 3 + TypeScript
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-05-pl-stop-stable-id-reuse-links-design.md`
|
|
|
+
|
|
|
+**注意:** 工作区可能有无关未提交改动。只改本计划列出的文件,提交时勿夹带无关 diff。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 操作 | 文件 | 职责 |
|
|
|
+|------|------|------|
|
|
|
+| Modify | `docs/superpowers/specs/2026-09-05-pl-stop-stable-id-reuse-links-design.md` | 状态改为已确认 |
|
|
|
+| Modify | `backend/app/schemas/pl/plan.py` | `PlanLoadBody` / `PlanDeliveryBody` / `PlanProductCreate` 可选 `id` |
|
|
|
+| Modify | `backend/app/services/pl_plan_stop_service.py` | 送货段/段产品 upsert,校验 id 归属 |
|
|
|
+| Modify | `backend/app/services/pl_plan_service.py` | `build_plan_product` 支持沿用 id;产品更新 upsert |
|
|
|
+| Modify | `backend/app/api/pl/plan.py` | update 产品路径改 upsert;已安排保存后调用地磅 upsert |
|
|
|
+| Modify | `backend/app/services/pl_dibang_bridge_service.py` | `upsert_stop_dibang_links`;取消勾选阻断/软删 |
|
|
|
+| Modify | `backend/app/api/pl/transport.py` | rearrange 不 rotate token;调用 upsert 替代 revoke+全量新建 |
|
|
|
+| Modify | `web-admin/src/utils/pl-plan-stop-form.ts` | 表单保留/回传段 `id` |
|
|
|
+| Modify | `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | 计划产品 payload 回传 `id`(若此处组装) |
|
|
|
+| Create | `backend/tests/test_pl_plan_stop_upsert.py` | 段 upsert 单测 |
|
|
|
+| Create | `backend/tests/test_pl_dibang_upsert_on_arrange.py` | 地磅 upsert / 过磅阻断取消勾选 |
|
|
|
+| Create | `backend/scripts/fix_pl_transport_db_link_stop_ids.py` | 历史 link 改挂 |
|
|
|
+| Create | `docs/sql/migrations/2026-09-05-fix-pl-db-link-orphaned-stop.sql` | 可选手工 SQL(含案例) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: Schema — Body 可选 id
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/schemas/pl/plan.py`
|
|
|
+- Modify: `docs/superpowers/specs/2026-09-05-pl-stop-stable-id-reuse-links-design.md`(状态 → 已确认)
|
|
|
+
|
|
|
+- [ ] **Step 1: 为写入 Body 增加可选 id**
|
|
|
+
|
|
|
+在 `PlanProductCreate` 增加:
|
|
|
+
|
|
|
+```python
|
|
|
+id: Optional[str] = Field(default=None, description="已有计划产品行ID;编辑回传以保留主键")
|
|
|
+```
|
|
|
+
|
|
|
+在 `PlanLoadBody` / `PlanDeliveryBody` 顶部增加:
|
|
|
+
|
|
|
+```python
|
|
|
+id: Optional[str] = Field(default=None, description="已有段ID;编辑回传以保留主键")
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 更新设计文档状态**
|
|
|
+
|
|
|
+将 spec 头部 `状态:待审阅…` 改为 `状态:已确认`。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/schemas/pl/plan.py docs/superpowers/specs/2026-09-05-pl-stop-stable-id-reuse-links-design.md
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): allow optional stop/product ids on plan write bodies.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 送货段 upsert(核心)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/tests/test_pl_plan_stop_upsert.py`
|
|
|
+- Modify: `backend/app/services/pl_plan_stop_service.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败单测(期望 upsert 保留 delivery id)**
|
|
|
+
|
|
|
+```python
|
|
|
+"""replace_plan_stops 应按 payload.id upsert 送货段,而非每次换新 UUID。"""
|
|
|
+from unittest.mock import MagicMock
|
|
|
+
|
|
|
+import pytest
|
|
|
+from fastapi import HTTPException
|
|
|
+
|
|
|
+from app.services.pl_plan_stop_service import resolve_delivery_id_for_upsert
|
|
|
+
|
|
|
+
|
|
|
+def test_upsert_delivery_rejects_foreign_plan_id():
|
|
|
+ with pytest.raises(HTTPException) as ei:
|
|
|
+ resolve_delivery_id_for_upsert(
|
|
|
+ plan_id="plan-1",
|
|
|
+ payload_id="del-other-plan",
|
|
|
+ existing_by_id={},
|
|
|
+ )
|
|
|
+ assert ei.value.status_code == 422
|
|
|
+
|
|
|
+
|
|
|
+def test_upsert_delivery_keeps_payload_id():
|
|
|
+ existing = MagicMock()
|
|
|
+ existing.id = "del-keep"
|
|
|
+ existing.plan_id = "plan-1"
|
|
|
+ new_id, row = resolve_delivery_id_for_upsert(
|
|
|
+ plan_id="plan-1",
|
|
|
+ payload_id="del-keep",
|
|
|
+ existing_by_id={"del-keep": existing},
|
|
|
+ )
|
|
|
+ assert new_id == "del-keep"
|
|
|
+ assert row is existing
|
|
|
+
|
|
|
+
|
|
|
+def test_upsert_delivery_generates_id_when_missing(monkeypatch):
|
|
|
+ from app.services import pl_plan_stop_service as svc
|
|
|
+
|
|
|
+ monkeypatch.setattr(svc, "generate_uuid", lambda: "new-del")
|
|
|
+ new_id, row = resolve_delivery_id_for_upsert(
|
|
|
+ plan_id="plan-1",
|
|
|
+ payload_id=None,
|
|
|
+ existing_by_id={},
|
|
|
+ )
|
|
|
+ assert new_id == "new-del"
|
|
|
+ assert row is None
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate
|
|
|
+pytest tests/test_pl_plan_stop_upsert.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(`resolve_delivery_id_for_upsert` 未定义)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 upsert 辅助函数并改造 `replace_plan_stops`**
|
|
|
+
|
|
|
+在 `pl_plan_stop_service.py`:
|
|
|
+
|
|
|
+1. **不要再**对全部 delivery `del_flag=1` 后无脑 `generate_uuid()`。
|
|
|
+2. 算法:
|
|
|
+ - 查询本计划全部 delivery(含 `del_flag=1`,用于复活)。
|
|
|
+ - `kept_ids = set()`
|
|
|
+ - 对每个 `PlanDeliveryBody`:
|
|
|
+ - 若 `body.id`:必须在本计划 map 中,否则 422;取该行 UPDATE 字段并 `del_flag=0`;`delivery_id = body.id`
|
|
|
+ - 若无 `body.id`:`delivery_id = generate_uuid()` INSERT
|
|
|
+ - `kept_ids.add(delivery_id)`
|
|
|
+ - 段产品:对该 delivery 旧产品软删后按行写入;`plan_product_id` 继续走 `_resolve_plan_product_id`
|
|
|
+ - 未在 `kept_ids` 的旧有效 delivery → `del_flag=1`(及其段产品)
|
|
|
+3. 装货段:保持现有 `_resolve_or_create_plan_load`。
|
|
|
+4. 合同行:可继续按现逻辑重写;本期硬目标是段 id 稳定。
|
|
|
+
|
|
|
+```python
|
|
|
+from typing import Optional
|
|
|
+
|
|
|
+def resolve_delivery_id_for_upsert(
|
|
|
+ *,
|
|
|
+ plan_id: str,
|
|
|
+ payload_id: Optional[str],
|
|
|
+ existing_by_id: dict,
|
|
|
+) -> tuple[str, Optional[object]]:
|
|
|
+ pid = (payload_id or "").strip()
|
|
|
+ if not pid:
|
|
|
+ return generate_uuid(), None
|
|
|
+ row = existing_by_id.get(pid)
|
|
|
+ if row is None or getattr(row, "plan_id", None) != plan_id:
|
|
|
+ raise HTTPException(status_code=422, detail=f"送货段 id 无效或不属于本计划:{pid}")
|
|
|
+ return pid, row
|
|
|
+```
|
|
|
+
|
|
|
+将 `replace_plan_stops` 中送货循环的 `delivery_id = generate_uuid()` 替换为上述解析 + UPDATE/INSERT。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑通单测**
|
|
|
+
|
|
|
+```bash
|
|
|
+pytest tests/test_pl_plan_stop_upsert.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/pl_plan_stop_service.py backend/tests/test_pl_plan_stop_upsert.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): upsert plan deliveries by stable id on save.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 计划产品行 upsert
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/pl_plan_service.py`(`build_plan_product`)
|
|
|
+- Modify: `backend/app/api/pl/plan.py`(update 产品块 ~1647–1706)
|
|
|
+- Create/Extend: `backend/tests/test_pl_plan_product_upsert.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: `build_plan_product` 支持传入 id**
|
|
|
+
|
|
|
+```python
|
|
|
+def build_plan_product(..., product_row_id: Optional[str] = None) -> PlPlanProduct:
|
|
|
+ return PlPlanProduct(
|
|
|
+ id=(product_row_id or "").strip() or generate_uuid(),
|
|
|
+ ...
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: update_plan 产品路径改为 upsert**
|
|
|
+
|
|
|
+替换「全体 `del_flag=1` 再全新建」:
|
|
|
+
|
|
|
+```python
|
|
|
+existing_all = db.query(PlPlanProduct).filter(
|
|
|
+ PlPlanProduct.plan_id == id,
|
|
|
+ PlPlanProduct.tenant_no == tenant_no,
|
|
|
+).all()
|
|
|
+existing_all_by_id = {row.id: row for row in existing_all}
|
|
|
+kept: list[PlPlanProduct] = []
|
|
|
+kept_ids: set[str] = set()
|
|
|
+for idx, prod in enumerate(payload.products):
|
|
|
+ row_id = (getattr(prod, "id", None) or "").strip()
|
|
|
+ if row_id and row_id in existing_all_by_id and existing_all_by_id[row_id].plan_id == id:
|
|
|
+ row = existing_all_by_id[row_id]
|
|
|
+ # 更新名称/数量/证书等字段,del_flag=0
|
|
|
+ kept.append(row)
|
|
|
+ kept_ids.add(row_id)
|
|
|
+ else:
|
|
|
+ pp = build_plan_product(..., product_row_id=None)
|
|
|
+ db.add(pp)
|
|
|
+ kept.append(pp)
|
|
|
+ kept_ids.add(pp.id)
|
|
|
+for row in existing_all:
|
|
|
+ if row.del_flag == 0 and row.id not in kept_ids:
|
|
|
+ row.del_flag = 1
|
|
|
+plan_products = kept
|
|
|
+```
|
|
|
+
|
|
|
+create-full 新建仍可不传 id。
|
|
|
+
|
|
|
+- [ ] **Step 3: 单测 — 带 id 时主键不变**
|
|
|
+
|
|
|
+断言:`build_plan_product(..., product_row_id="pp-1").id == "pp-1"`;无 id 时走 `generate_uuid`。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/pl_plan_service.py backend/app/api/pl/plan.py backend/tests/test_pl_plan_product_upsert.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): upsert plan product rows by id on update.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 前端回传段 / 产品 id
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/utils/pl-plan-stop-form.ts`
|
|
|
+- Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue`(产品 payload)
|
|
|
+- Modify: `web-admin/src/api/pl/index.ts`(若类型缺 `products[].id`)
|
|
|
+
|
|
|
+- [ ] **Step 1: 表单类型与 map/build**
|
|
|
+
|
|
|
+`PlanLoadForm` / `PlanDeliveryForm` 增加 `id?: string`。
|
|
|
+
|
|
|
+`mapLoadFromApi` / `mapDeliveriesFromApi`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+id: load.id || undefined
|
|
|
+// deliveries:
|
|
|
+id: row.id || undefined
|
|
|
+```
|
|
|
+
|
|
|
+`createEmptyLoadForm` / `createEmptyDeliveryForm`:不设 id。
|
|
|
+
|
|
|
+`buildLoadPayload` / `buildDeliveriesPayload`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+...(load.id ? { id: load.id } : {}),
|
|
|
+// delivery:
|
|
|
+...(row.id ? { id: row.id } : {}),
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 计划产品提交带 id**
|
|
|
+
|
|
|
+在 `buildPlanCommonPayload` / 最终 `products` 数组中,对已有行带上 `id: row.id`。确认类型允许 `products[].id`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 手工确认**
|
|
|
+
|
|
|
+编辑已有计划 → PUT body 的 `load.id`、`deliveries[].id`、`products[].id` 非空;新增送货段无 `id`。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/pl-plan-stop-form.ts web-admin/src/components/business/pl-plan-form-dialog/index.vue web-admin/src/api/pl/index.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): submit stable load/delivery/product ids on plan save.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 地磅按段 upsert(替代整批 revoke 再建)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/tests/test_pl_dibang_upsert_on_arrange.py`
|
|
|
+- Modify: `backend/app/services/pl_dibang_bridge_service.py`
|
|
|
+- Modify: `backend/app/api/pl/transport.py`
|
|
|
+- Modify: `backend/app/api/pl/plan.py`(已安排 PUT 后调用)
|
|
|
+
|
|
|
+- [ ] **Step 1: 写单测骨架**
|
|
|
+
|
|
|
+```python
|
|
|
+def test_cancel_generate_dibang_blocked_when_weighing_exists():
|
|
|
+ # mock link + inventory + _inventory_has_weighing_slip True
|
|
|
+ # generate_dibang=0 → ValueError 含批次号
|
|
|
+
|
|
|
+def test_upsert_reuses_existing_inventory_id():
|
|
|
+ # 已有 link(stop_id=del-1, db_inventory_id=inv-1)
|
|
|
+ # upsert 同 stop → 返回同一 inv-1,不新建
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 实现 `upsert_stop_dibang_links`**
|
|
|
+
|
|
|
+相对现 `sync_dibang_for_plan_stops` + `_sync_stop_inventory_links`:
|
|
|
+
|
|
|
+1. 查本 plan/transport 有效 links。
|
|
|
+2. 勾选段+产品:已有 `(stop_type, stop_id, plan_product_id)` → 未过磅则更新可变字段;过磅则跳过并 `warnings`。无则新建 inventory + link。
|
|
|
+3. 取消勾选:有有效过磅 → `raise ValueError`;否则软删 link + 出入库并回减月计划贡献。
|
|
|
+4. link 存在且段 `product_id` 与出入库不一致 → `raise ValueError`(禁止静默换产品)。
|
|
|
+
|
|
|
+可扩展 `sync_linked_dibang_dates_for_plan` 或并列字段同步:`partner_company` / `vehicle_no` / `quantity`(未过磅可写)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 改 `transport.py` 安排路径**
|
|
|
+
|
|
|
+- 去掉 rearrange 前整批 `revoke_regeneratable_dibang_for_transport`(取消勾选改由 upsert 按条处理)。
|
|
|
+- `sync_dibang_for_plan_stops` 改为 upsert;首次安排无 link = 新建。
|
|
|
+
|
|
|
+- [ ] **Step 4: 已安排计划 PUT**
|
|
|
+
|
|
|
+在 `update_plan` 于 `replace_plan_stops` + 日期同步之后,若已安排则调用同一 upsert / 取消勾选校验(spec §4.2)。
|
|
|
+
|
|
|
+- [ ] **Step 5: 跑测**
|
|
|
+
|
|
|
+```bash
|
|
|
+pytest tests/test_pl_dibang_upsert_on_arrange.py tests/test_pl_dibang_date_sync_on_plan_save.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/pl_dibang_bridge_service.py backend/app/api/pl/transport.py backend/app/api/pl/plan.py backend/tests/test_pl_dibang_upsert_on_arrange.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): upsert dibang links in place instead of revoke-and-recreate.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 调整运输默认不 rotate token
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/pl/transport.py`
|
|
|
+- Extend: 相关 tests
|
|
|
+
|
|
|
+- [ ] **Step 1: 改 rearrange 分支**
|
|
|
+
|
|
|
+将 rotate 改为 ensure(仅缺失时生成):
|
|
|
+
|
|
|
+```python
|
|
|
+if is_rearrange:
|
|
|
+ _ensure_driver_access_token(transport)
|
|
|
+ if needs_site_link and plan_has_load_stop(db, tenant_no, plan.id):
|
|
|
+ ensure_transport_site_token(transport)
|
|
|
+ elif not needs_site_link:
|
|
|
+ transport.site_access_token = None
|
|
|
+ # 既有 void_io_orders 逻辑按需保留
|
|
|
+else:
|
|
|
+ _ensure_driver_access_token(transport)
|
|
|
+```
|
|
|
+
|
|
|
+保留 `_rotate_*` 供未来「作废并重发」,本期不调用。
|
|
|
+
|
|
|
+- [ ] **Step 2: 单测 — rearrange 后 token 不变**
|
|
|
+
|
|
|
+已有 token 的 transport 走 rearrange 后 `driver_access_token` / `site_access_token` 相等。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/api/pl/transport.py backend/tests/
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+fix(pl): keep driver/site tokens stable on rearrange.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 历史悬空 link 修复(含 PO20260904024)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/scripts/fix_pl_transport_db_link_stop_ids.py`
|
|
|
+- Create: `docs/sql/migrations/2026-09-05-fix-pl-db-link-orphaned-stop.sql`
|
|
|
+
|
|
|
+- [ ] **Step 1: 脚本逻辑**
|
|
|
+
|
|
|
+对每个有效 `pl_transport_db_link`:
|
|
|
+
|
|
|
+1. `stop_type=delivery` 且 stop 已软删/不存在 → 同计划有效 delivery:优先同 `customer_name`;仅 1 条则唯一匹配。
|
|
|
+2. `plan_product_id` 已软删 → 同计划有效产品按相同 CRM `product_id` 改挂。
|
|
|
+3. 默认 dry-run;`--apply` 才写库;支持 `--plan-no PO20260904024`。
|
|
|
+
|
|
|
+期望案例输出:
|
|
|
+
|
|
|
+```
|
|
|
+stop_id 1D4CB037… → 2036E1B2…
|
|
|
+plan_product_id DA6B3548… → 537B42A6…
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: SQL 备忘**
|
|
|
+
|
|
|
+```sql
|
|
|
+UPDATE pl_transport_db_link
|
|
|
+SET stop_id = '2036E1B2CEC54509ABC2B22DAA5786F3',
|
|
|
+ plan_product_id = '537B42A6B7E54D7CBD4C90DC73B185E7',
|
|
|
+ update_time = NOW()
|
|
|
+WHERE id = '4AAE759627054CAAB6B5F36D4888CBA4'
|
|
|
+ AND plan_id = '551307353C99489786756A3F26C49020'
|
|
|
+ AND del_flag = 0;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/scripts/fix_pl_transport_db_link_stop_ids.py docs/sql/migrations/2026-09-05-fix-pl-db-link-orphaned-stop.sql
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+chore(pl): add script to remount orphaned transport dibang stop links.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 生产修复(人工确认后执行,不自动跑)**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate
|
|
|
+python scripts/fix_pl_transport_db_link_stop_ids.py --plan-no PO20260904024
|
|
|
+python scripts/fix_pl_transport_db_link_stop_ids.py --plan-no PO20260904024 --apply
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: 回归清单与 graphify
|
|
|
+
|
|
|
+- [ ] **Step 1: 自动化回归**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate
|
|
|
+pytest tests/test_pl_plan_stop_upsert.py tests/test_pl_plan_product_upsert.py tests/test_pl_dibang_upsert_on_arrange.py tests/test_pl_dibang_date_sync_on_plan_save.py tests/test_pl_plan_stop_service.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 2: 手工验收(spec §8)**
|
|
|
+
|
|
|
+1. 安排 → 改地址保存/调整 → 段 id、出入库 id、批次号不变;详情有地磅。
|
|
|
+2. 过磅后改地址 → 计划可存;详情仍有地磅。
|
|
|
+3. 过磅后取消地磅勾选 → 失败提示。
|
|
|
+4. 新增多段勾地磅 → 只新建新段单据。
|
|
|
+5. 调整运输 → 司机/现场旧链接仍可用。
|
|
|
+6. 复制计划 → 无源单 link。
|
|
|
+7. 修复后 `PO20260904024` 送货段可见磅单。
|
|
|
+
|
|
|
+- [ ] **Step 3: 更新知识图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh backend
|
|
|
+./scripts/update-graph.sh frontend
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec 覆盖自检
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| G1 段/产品主键稳定 | Task 2–4 |
|
|
|
+| G2 地磅就地更新 | Task 5 |
|
|
|
+| G3 token 不 rotate | Task 6 |
|
|
|
+| G4 详情可见地磅 | Task 2+4+7 |
|
|
|
+| G5 历史修复 | Task 7 |
|
|
|
+| D4 过磅取消勾选阻断 | Task 5 |
|
|
|
+| D6 复制不带走 | Task 8 手工确认 |
|
|
|
+
|
|
|
+无 TBD/「稍后实现」占位。
|