Files
shanxia2.0/docs/superpowers/plans/2026-07-27-pl-plan-create-date.md
T
2026-07-27 16:30:35 +08:00

441 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运输计划创建日期 + 取货/送达默认 — 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
<el-form-item label="创建日期" prop="createDate">
<el-date-picker
v-model="formData.createDate"
type="date"
placeholder="选择日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
```
- [ ] **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`。