441 lines
12 KiB
Markdown
441 lines
12 KiB
Markdown
# 运输计划创建日期 + 取货/送达默认 — 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`。
|