# 运输计划创建日期 + 取货/送达默认 — 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:** 为 `pl_plan` 增加业务字段「创建日期」(`create_date`/`createDate`),在新增/编辑弹窗中可编辑;新增时取货/送达时间若为空则默认等于计划日期。 **Architecture:** MySQL 增量迁移回填存量;SQLAlchemy 模型 + Pydantic Create/Update/Read 贯通;创建接口缺省时服务端用当天。前端仅改 `pl-plan-form-dialog`(及仍在用的 `plan/form.vue`),列表/查看暂不展示。 **Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Element Plus;MySQL 5.7 **Spec:** `docs/superpowers/specs/2026-07-27-pl-plan-create-date-design.md` --- ## 文件结构 | 文件 | 职责 | |------|------| | `backend/scripts/migrate_20260727_pl_plan_create_date.sql` | 增量 DDL + 存量回填 | | `docs/sql/shanxia2.0.sql` | `pl_plan` 建表定义同步加 `create_date` | | `backend/app/models/pl_models.py` | `PlPlan.create_date` | | `backend/app/schemas/pl/plan.py` | Create/Update/Read/`CreateFromContractRequest` 的 `createDate` | | `backend/app/api/pl/plan.py` | create / create-full / create-from-contract / update 写入 | | `backend/tests/test_pl_plan_create_date.py` | 缺省日期解析纯函数单测(若抽 helper)或创建默认行为说明见 Task 3 | | `web-admin/src/api/pl/index.ts` | `Plan` / `PlanCreateFullPayload` 增加 `createDate` | | `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | UI + 默认值 + 提交 | | `web-admin/src/views/pl/plan/form.vue` | 与弹窗对齐(路由仍挂载时) | --- ### Task 1: 迁移 SQL + DDL 快照 **Files:** - Create: `backend/scripts/migrate_20260727_pl_plan_create_date.sql` - Modify: `docs/sql/shanxia2.0.sql`(`CREATE TABLE pl_plan` 中 `plan_date` 前插入 `create_date`) - [ ] **Step 1: 写增量迁移脚本** ```sql -- backend/scripts/migrate_20260727_pl_plan_create_date.sql -- 运输计划:业务创建日期 create_date(与系统 create_time 独立) ALTER TABLE `pl_plan` ADD COLUMN `create_date` date DEFAULT NULL COMMENT '创建日期' AFTER `plan_no`; UPDATE `pl_plan` SET `create_date` = DATE(`create_time`) WHERE `create_date` IS NULL AND `create_time` IS NOT NULL; UPDATE `pl_plan` SET `create_date` = CURDATE() WHERE `create_date` IS NULL; ALTER TABLE `pl_plan` MODIFY COLUMN `create_date` date NOT NULL COMMENT '创建日期'; ``` - [ ] **Step 2: 同步 `docs/sql/shanxia2.0.sql`** 在 `CREATE TABLE \`pl_plan\`` 中,于 `` `plan_no` `` 行之后、`` `plan_date` `` 行之前增加: ```sql `create_date` date NOT NULL COMMENT '创建日期', ``` (不必改历史 INSERT 行;本地/环境以迁移脚本为准。若本机有库,执行 Step 1 脚本。) - [ ] **Step 3: Commit** ```bash git add backend/scripts/migrate_20260727_pl_plan_create_date.sql docs/sql/shanxia2.0.sql git commit -m "$(cat <<'EOF' chore(pl): add pl_plan.create_date migration and DDL snapshot EOF )" ``` --- ### Task 2: ORM + Schema **Files:** - Modify: `backend/app/models/pl_models.py`(`PlPlan`,约 `plan_no` / `plan_date` 附近) - Modify: `backend/app/schemas/pl/plan.py` - [ ] **Step 1: 模型增加列** 在 `plan_no` 与 `plan_date` 之间: ```python plan_no = Column(String(32), nullable=False, unique=True, comment="计划订单编号 POYYYYMMDD###") create_date = Column(Date, nullable=False, comment="创建日期") plan_date = Column(Date, nullable=False, comment="计划日期") ``` - [ ] **Step 2: Schema 贯通** `PlanCreate`(约 126 行)增加(紧挨 `plan_date`): ```python class PlanCreate(PlanBase): create_date: Optional[date] = Field( default=None, alias="createDate", description="创建日期;缺省则服务端用当天" ) plan_date: date = Field(..., alias="planDate", description="计划日期") products: Optional[list[PlanProductCreate]] = Field(default=None, description="产品明细列表") model_config = {"populate_by_name": True} ``` `PlanUpdate` 增加: ```python create_date: Optional[date] = Field(default=None, alias="createDate") plan_date: Optional[date] = Field(default=None, alias="planDate") ``` `PlanRead` 在 `plan_date` 旁增加: ```python create_date: date = Field(alias="createDate") plan_date: date = Field(alias="planDate") ``` `CreateFromContractRequest` 增加可选: ```python create_date: Optional[date] = Field( default=None, alias="createDate", description="创建日期;缺省当天" ) plan_date: date = Field(..., alias="planDate", description="计划日期") ``` 确保文件顶部已有 `Optional`、`date` 导入。 - [ ] **Step 3: Commit** ```bash git add backend/app/models/pl_models.py backend/app/schemas/pl/plan.py git commit -m "$(cat <<'EOF' feat(pl): add create_date to PlPlan model and schemas EOF )" ``` --- ### Task 3: API 写入 create_date **Files:** - Modify: `backend/app/api/pl/plan.py`(`create_plan`、`create_plan_full`、`create_from_contract`、`update_plan`) 说明:`update_plan` 已用 `payload.model_dump(exclude_unset=True)` + `setattr`,Schema 加字段后更新路径自动生效,无需改循环逻辑。 - [ ] **Step 1: 抽一小段解析(同文件顶部附近或内联)** 在 `plan.py` 模块级增加: ```python from datetime import date def _resolve_create_date(value: date | None) -> date: return value or date.today() ``` (若文件已 `from datetime import date`,勿重复导入。) - [ ] **Step 2: 三处创建赋值** 在 `PlPlan(...)` 构造处,`plan_date=payload.plan_date` 旁增加: ```python create_date=_resolve_create_date(getattr(payload, "create_date", None)), plan_date=payload.plan_date, ``` 覆盖: 1. `create_plan`(约 396–400 行) 2. `create_plan_full`(约 478–482 行) 3. `create_from_contract` 内 `PlPlan(...)`(约 598–602 行)— 使用 `payload.create_date` - [ ] **Step 3: 写纯函数单测** ```python # backend/tests/test_pl_plan_create_date.py from datetime import date from app.api.pl.plan import _resolve_create_date def test_resolve_create_date_uses_explicit(): assert _resolve_create_date(date(2026, 1, 15)) == date(2026, 1, 15) def test_resolve_create_date_defaults_today(): assert _resolve_create_date(None) == date.today() ``` Run: `cd backend && source venv/bin/activate && pytest tests/test_pl_plan_create_date.py -v` Expected: PASS - [ ] **Step 4: Commit** ```bash git add backend/app/api/pl/plan.py backend/tests/test_pl_plan_create_date.py git commit -m "$(cat <<'EOF' feat(pl): persist create_date on plan create APIs EOF )" ``` --- ### Task 4: 前端 API 类型 **Files:** - Modify: `web-admin/src/api/pl/index.ts` - [ ] **Step 1: 类型增加 `createDate`** `Plan` 接口(约 `planDate` 旁): ```typescript planNo: string createDate: string planDate: string ``` `PlanCreateFullPayload`: ```typescript contractId: string createDate: string planDate: string ``` `createFromContract` 可选扩展(若合同入口会传): ```typescript createFromContract: (data: { contractId: string planDate: string createDate?: string followUserId?: string followUserName?: string }) => ``` - [ ] **Step 2: Commit** ```bash git add web-admin/src/api/pl/index.ts git commit -m "$(cat <<'EOF' feat(web-admin): add createDate to transport plan API types EOF )" ``` --- ### Task 5: 新增/编辑弹窗 UI 与默认值 **Files:** - Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue` - [ ] **Step 1: 模板 — 计划日期前加创建日期** 在基础信息 `form-grid-4` 内,「计划日期」`el-form-item` **之前**插入: ```vue ``` - [ ] **Step 2: formData / rules / reset / load / payload** 辅助函数(script 内): ```typescript function todayYmd(): string { return new Date().toISOString().split('T')[0] } ``` `formData` 初始: ```typescript createDate: todayYmd(), planDate: todayYmd(), // ... pickupTime: '', // reset 时再按计划日期填充 deliveryTime: '', ``` `basicRules`: ```typescript createDate: [{ required: true, message: '请选择创建日期', trigger: 'change' }], planDate: [{ required: true, message: '请选择计划日期', trigger: 'change' }], ``` `resetForm` 的 `Object.assign`: ```typescript createDate: todayYmd(), planDate: todayYmd(), // ...其它字段同现有... pickupTime: todayYmd(), // 与默认 planDate 相同;用户改后不再联动 deliveryTime: todayYmd(), ``` 注意:`resetForm` 里先算一次 `const d = todayYmd()`,`createDate`/`planDate`/`pickupTime`/`deliveryTime` 都用同一个 `d`,避免跨日边界不一致。 `loadPlanForEdit` 的 `Object.assign`: ```typescript createDate: data.createDate || formatDate(data.createTime) || todayYmd(), planDate: data.planDate, // pickup/delivery 仍用接口值,空则保持空(不要用计划日期覆盖) pickupTime: formatDate(data.pickupTime) || '', deliveryTime: formatDate(data.deliveryTime) || '', ``` `buildPlanCommonPayload`: ```typescript createDate: formData.createDate, planDate: formData.planDate, ``` 确认 **没有** 对 `planDate` 的 `watch` 去改取货/送达。 - [ ] **Step 3: 手工核对(开发者)** 1. 打开新增:创建日期、计划日期、取货、送达均为当天。 2. 只改计划日期:取货/送达不变。 3. 改取货后再改计划日期:取货仍为用户改过的值。 4. 保存后再编辑:创建日期回填正确;`createTime` 未变。 - [ ] **Step 4: Commit** ```bash git add web-admin/src/components/business/pl-plan-form-dialog/index.vue git commit -m "$(cat <<'EOF' feat(web-admin): add createDate and default pickup/delivery in plan form dialog EOF )" ``` --- ### Task 6: 对齐独立表单页(若仍使用) **Files:** - Modify: `web-admin/src/views/pl/plan/form.vue` - [ ] **Step 1: 与弹窗同构的最小改动** 若该页仍可从路由 `/pl/plan/form` 打开:同样在计划日期前加「创建日期」,`formData`/`rules`/提交/`reset` 增加 `createDate`;新增时 `pickupTime`/`deliveryTime` 空则填 `planDate`。 若页面已废弃不用,可在 PR 说明中注明「未改 form.vue」,并跳过本 Task。 - [ ] **Step 2: Commit(有改动时)** ```bash git add web-admin/src/views/pl/plan/form.vue git commit -m "$(cat <<'EOF' feat(web-admin): align plan form page with createDate field EOF )" ``` --- ### Task 7: 一致性检查与图谱 **Files:** 无强制新文件 - [ ] **Step 1: 字段一致性(若脚本覆盖 pl)** ```bash cd backend && source venv/bin/activate && python scripts/check_field_consistency.py ``` 若报 `createDate`/`create_date` 相关缺失,按脚本提示补齐后重跑至通过或确认脚本未覆盖 `pl_plan`。 - [ ] **Step 2: 更新 graphify** ```bash ./scripts/update-graph.sh all ``` 或按仓库约定:`graphify update .`(根目录若配置)/ 仅 backend+web-admin。 - [ ] **Step 3: Commit 图谱变更(若有)** ```bash git add backend/graphify-out web-admin/graphify-out graphify-out 2>/dev/null || true git status # 仅提交实际变更的 graph 文件 git commit -m "$(cat <<'EOF' chore: update graphify after pl_plan create_date EOF )" ``` --- ## Spec 覆盖自检 | Spec 项 | Task | |---------|------| | D1 `create_date` 独立字段 | 1–3 | | D2 存量回填 | 1 | | D3 仅弹窗 UI | 5(列表/查看不改) | | D4 新增取货/送达默认计划日期、不联动 | 5 | | D5 计划日期默认当天不变 | 5 | | D6 安排弹窗不改 | —(无 Task) | | 后端缺省当天 | 3 | | 验收:createTime 不被改写 | 3(未写 create_time)+ 5 手工 | ## Placeholder 扫描 无 TBD/TODO;步骤含具体 SQL/代码/命令。 ## 类型一致性 全程使用:DB `create_date`、API/前端 `createDate`、Python `create_date` / alias `createDate`。