|
|
@@ -0,0 +1,368 @@
|
|
|
+# 派车日历「其他日历」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:** 在 Web 与小程序 H5 派车日历页的「碱派车日历」后增加「其他日历」Tab,`calendarKind=other`,功能与样式与酸/碱手工日历一致。
|
|
|
+
|
|
|
+**Architecture:** 扩展现有 `calendar_kind` 枚举(`acid | alkali | other`),复用 `ManualCalendar` / API / 权限;仅改校验、类型、入口 Tab 与字段 COMMENT。不建新表、不新增权限码。
|
|
|
+
|
|
|
+**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / MySQL 5.7 / Vue 3 / Element Plus / Vant / pytest / tsx。
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-10-29-dispatch-calendar-other-kind-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| File | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| `backend/tests/test_pl_dispatch_calendar.py` | 扩展隔离与校验测试覆盖 `other` |
|
|
|
+| `backend/app/services/pl_dispatch_calendar_service.py` | `CALENDAR_KINDS` 与 422 文案 |
|
|
|
+| `backend/app/schemas/pl/dispatch_calendar.py` | CreateBody `calendar_kind` 校验 |
|
|
|
+| `backend/app/models/pl_dispatch_calendar.py` | 列 COMMENT |
|
|
|
+| `docs/sql/migrations/2026-10-29-dispatch-calendar-other-kind.sql` | 幂等更新 COMMENT |
|
|
|
+| `docs/sql/migrations/2026-10-26-manual-dispatch-calendar.sql` | 建表 COMMENT 与现状对齐(文档一致性) |
|
|
|
+| `docs/sql/migrations/2026-10-27-dispatch-calendar-kind.sql` | ADD COLUMN COMMENT 与现状对齐 |
|
|
|
+| `web-admin/src/api/pl/dispatch-calendar.ts` | `DispatchCalendarKind` 加 `'other'` |
|
|
|
+| `web-admin/src/views/pl/dispatch-calendar/index.vue` | Web Tab + 主题 |
|
|
|
+| `web-admin/src/views/m/pl/dispatch-calendar/index.vue` | H5 Tab |
|
|
|
+| `web-admin/scripts/pl-dispatch-calendar.test.ts` | 可选:断言 Tab 文案顺序(若已有入口断言则扩展) |
|
|
|
+
|
|
|
+不改:`useManualCalendar.ts`、`ManualCalendar.vue` 内部逻辑(已按 prop `calendarKind` 工作)、车辆/产品 API、实际派车信息、菜单权限 SQL。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 后端 — `other` 校验与隔离测试先行
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/tests/test_pl_dispatch_calendar.py`
|
|
|
+- Modify: `backend/app/services/pl_dispatch_calendar_service.py`
|
|
|
+- Modify: `backend/app/schemas/pl/dispatch_calendar.py`
|
|
|
+- Modify: `backend/app/models/pl_dispatch_calendar.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展失败测试(先跑应红)**
|
|
|
+
|
|
|
+在 `test_acid_and_alkali_calendars_stay_separate` 末尾追加 `other` 断言,或新增同文件测试:
|
|
|
+
|
|
|
+```python
|
|
|
+def test_other_calendar_stays_separate_from_acid_and_alkali(db):
|
|
|
+ _, update, _, svc = modules()
|
|
|
+ create(db) # acid default
|
|
|
+ create(db, calendarKind='alkali', origin='碱库')
|
|
|
+ other = create(db, calendarKind='other', origin='其他库')
|
|
|
+ other_page = svc.list_records(db, 't1', user(), month='2026-10', calendar_kind='other')
|
|
|
+ assert other_page['total'] == 1 and other_page['list'][0]['id'] == other.id
|
|
|
+ assert other_page['list'][0]['calendarKind'] == 'other'
|
|
|
+ assert svc.list_records(db, 't1', user(), month='2026-10')['total'] == 1
|
|
|
+ assert svc.list_records(db, 't1', user(), month='2026-10', calendar_kind='alkali')['total'] == 1
|
|
|
+ with pytest.raises(HTTPException) as moved:
|
|
|
+ svc.update_record(db, 't1', user(), other.id, update(**payload(calendarKind='acid')))
|
|
|
+ assert moved.value.status_code == 422
|
|
|
+ kept = svc.update_record(
|
|
|
+ db, 't1', user(), other.id, update(**payload(calendarKind='other', destination='合肥'))
|
|
|
+ )
|
|
|
+ assert kept.destination == '合肥' and kept.calendar_kind == 'other'
|
|
|
+```
|
|
|
+
|
|
|
+在 `test_http_read_only_manage_permissions_both_clients_and_routes` 中碱断言后追加:
|
|
|
+
|
|
|
+```python
|
|
|
+ other = client.post(path, json=payload(calendarKind='other'))
|
|
|
+ assert other.status_code == 200 and other.json()['calendarKind'] == 'other'
|
|
|
+ assert client.get(path, params={'month': '2026-10', 'calendarKind': 'other'}).json()['total'] == 1
|
|
|
+ assert client.put(f"{path}/{other.json()['id']}", json=payload(calendarKind='alkali')).status_code == 422
|
|
|
+```
|
|
|
+
|
|
|
+(`{'calendarKind': 'salt'}` 已在 schema 参数化测试中,保持即可。)
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -m pytest tests/test_pl_dispatch_calendar.py::test_other_calendar_stays_separate_from_acid_and_alkali -q
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(schema/service 仍拒绝 `other`)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 最小实现**
|
|
|
+
|
|
|
+`backend/app/services/pl_dispatch_calendar_service.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+CALENDAR_KINDS = ("acid", "alkali", "other")
|
|
|
+
|
|
|
+
|
|
|
+def _calendar_kind(kind: Optional[str]) -> str:
|
|
|
+ value = kind or "acid"
|
|
|
+ if value not in CALENDAR_KINDS:
|
|
|
+ raise HTTPException(status_code=422, detail="日历类型须为 acid、alkali 或 other")
|
|
|
+ return value
|
|
|
+```
|
|
|
+
|
|
|
+`backend/app/schemas/pl/dispatch_calendar.py` 的 `validate_calendar_kind`:
|
|
|
+
|
|
|
+```python
|
|
|
+ def validate_calendar_kind(cls, value: str) -> str:
|
|
|
+ if value not in ("acid", "alkali", "other"):
|
|
|
+ raise ValueError("日历类型须为 acid、alkali 或 other")
|
|
|
+ return value
|
|
|
+```
|
|
|
+
|
|
|
+`backend/app/models/pl_dispatch_calendar.py` COMMENT:
|
|
|
+
|
|
|
+```python
|
|
|
+ comment="日历类型:acid酸派车 alkali碱派车 other其他",
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑全文件测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -m pytest tests/test_pl_dispatch_calendar.py -q
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS(全部绿)。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/tests/test_pl_dispatch_calendar.py \
|
|
|
+ backend/app/services/pl_dispatch_calendar_service.py \
|
|
|
+ backend/app/schemas/pl/dispatch_calendar.py \
|
|
|
+ backend/app/models/pl_dispatch_calendar.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat: allow dispatch calendar kind other on backend.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: SQL COMMENT 迁移
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `docs/sql/migrations/2026-10-29-dispatch-calendar-other-kind.sql`
|
|
|
+- Modify: `docs/sql/migrations/2026-10-26-manual-dispatch-calendar.sql`(COMMENT 文案)
|
|
|
+- Modify: `docs/sql/migrations/2026-10-27-dispatch-calendar-kind.sql`(COMMENT 文案)
|
|
|
+
|
|
|
+- [ ] **Step 1: 新建幂等迁移**
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 手工派车日历增加 other(其他日历)。仅更新 COMMENT,无结构变更。
|
|
|
+-- MySQL 5.7 / utf8mb4。可重复执行。
|
|
|
+SET NAMES utf8mb4;
|
|
|
+
|
|
|
+SET @col_exists = (
|
|
|
+ SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
|
+ WHERE TABLE_SCHEMA = DATABASE()
|
|
|
+ AND TABLE_NAME = 'pl_dispatch_calendar'
|
|
|
+ AND COLUMN_NAME = 'calendar_kind'
|
|
|
+);
|
|
|
+SET @ddl = IF(
|
|
|
+ @col_exists = 1,
|
|
|
+ 'ALTER TABLE `pl_dispatch_calendar` MODIFY COLUMN `calendar_kind` varchar(16) NOT NULL DEFAULT ''acid'' COMMENT ''日历类型:acid酸派车 alkali碱派车 other其他''',
|
|
|
+ 'SELECT 1'
|
|
|
+);
|
|
|
+PREPARE stmt FROM @ddl;
|
|
|
+EXECUTE stmt;
|
|
|
+DEALLOCATE PREPARE stmt;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 同步历史迁移文件中的 COMMENT 字符串**
|
|
|
+
|
|
|
+将两处 `acid酸派车 alkali碱派车` 改为 `acid酸派车 alkali碱派车 other其他`(仅文档/幂等 ADD 文案,不影响已执行库;新环境建表即含完整注释)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 在本地库执行一次并复查**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 使用项目惯用方式连接当前库后执行该 SQL;第二次执行应无结构报错
|
|
|
+```
|
|
|
+
|
|
|
+Expected: `calendar_kind` COMMENT 含 `other`;重复执行成功。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/sql/migrations/2026-10-29-dispatch-calendar-other-kind.sql \
|
|
|
+ docs/sql/migrations/2026-10-26-manual-dispatch-calendar.sql \
|
|
|
+ docs/sql/migrations/2026-10-27-dispatch-calendar-kind.sql
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+docs(sql): note other kind on dispatch calendar_kind.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 前端类型与双端 Tab
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/pl/dispatch-calendar.ts`
|
|
|
+- Modify: `web-admin/src/views/pl/dispatch-calendar/index.vue`
|
|
|
+- Modify: `web-admin/src/views/m/pl/dispatch-calendar/index.vue`
|
|
|
+- Modify: `web-admin/scripts/pl-dispatch-calendar.test.ts`(仅当该文件已断言路由/入口时可追加 Tab 顺序;否则可跳过,改用浏览器手测)
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展类型**
|
|
|
+
|
|
|
+`web-admin/src/api/pl/dispatch-calendar.ts`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+export type DispatchCalendarKind = 'acid' | 'alkali' | 'other'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Web 入口**
|
|
|
+
|
|
|
+`web-admin/src/views/pl/dispatch-calendar/index.vue` 完整替换为:
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- P08 页内 Tab,P27 日历视图;原实际信息保持只读统计。 -->
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ class="dispatch-calendar-tabs"
|
|
|
+ :class="{
|
|
|
+ 'manual-calendar-theme': active === 'acid' || active === 'alkali' || active === 'other'
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <el-tabs v-model="active">
|
|
|
+ <el-tab-pane label="酸派车日历" name="acid" />
|
|
|
+ <el-tab-pane label="碱派车日历" name="alkali" />
|
|
|
+ <el-tab-pane label="其他日历" name="other" />
|
|
|
+ <el-tab-pane label="实际派车信息" name="actual" />
|
|
|
+ </el-tabs>
|
|
|
+ <ManualCalendar v-if="active === 'acid'" calendar-kind="acid" title="酸派车日历" />
|
|
|
+ <ManualCalendar v-else-if="active === 'alkali'" calendar-kind="alkali" title="碱派车日历" />
|
|
|
+ <ManualCalendar v-else-if="active === 'other'" calendar-kind="other" title="其他日历" />
|
|
|
+ <ActualCalendar v-else />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+ import ManualCalendar from './ManualCalendar.vue'
|
|
|
+ import ActualCalendar from './ActualCalendar.vue'
|
|
|
+ defineOptions({ name: 'PlDispatchCalendar' })
|
|
|
+ const active = ref('acid')
|
|
|
+</script>
|
|
|
+<style scoped>
|
|
|
+ .dispatch-calendar-tabs {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 0;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ .dispatch-calendar-tabs > :deep(.el-tabs) {
|
|
|
+ --el-color-primary: #1f4e79;
|
|
|
+
|
|
|
+ flex: none;
|
|
|
+ padding: 0 18px;
|
|
|
+ background: white;
|
|
|
+ }
|
|
|
+ .dispatch-calendar-tabs > :deep(.el-tabs .el-tabs__header) {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .dispatch-calendar-tabs > :deep(.page-container) {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: H5 入口**
|
|
|
+
|
|
|
+在 `web-admin/src/views/m/pl/dispatch-calendar/index.vue`:
|
|
|
+
|
|
|
+模板 Tab 区改为:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <van-tabs v-model:active="active" shrink color="#0a9f6d" title-active-color="#087f57">
|
|
|
+ <van-tab title="酸派车日历" name="acid" />
|
|
|
+ <van-tab title="碱派车日历" name="alkali" />
|
|
|
+ <van-tab title="其他日历" name="other" />
|
|
|
+ <van-tab title="实际派车信息" name="actual" />
|
|
|
+ </van-tabs>
|
|
|
+```
|
|
|
+
|
|
|
+内容区改为:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <ManualCalendar v-if="active === 'acid'" calendar-kind="acid" title="酸派车日历" />
|
|
|
+ <ManualCalendar v-else-if="active === 'alkali'" calendar-kind="alkali" title="碱派车日历" />
|
|
|
+ <ManualCalendar v-else-if="active === 'other'" calendar-kind="other" title="其他日历" />
|
|
|
+ <ActualCalendar v-else />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 定向测试与 lint**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx --test scripts/manual-dispatch-calendar.test.ts scripts/pl-dispatch-calendar.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 既有测试 PASS。
|
|
|
+
|
|
|
+对改动文件跑 ESLint / Prettier(项目惯用命令)。Expected: 无新增错误。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/pl/dispatch-calendar.ts \
|
|
|
+ web-admin/src/views/pl/dispatch-calendar/index.vue \
|
|
|
+ web-admin/src/views/m/pl/dispatch-calendar/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat: add other dispatch calendar tab on web and h5.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 验证与图谱
|
|
|
+
|
|
|
+**Files:** 无新业务文件;执行命令与记录。
|
|
|
+
|
|
|
+- [ ] **Step 1: 后端回归**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -m pytest tests/test_pl_dispatch_calendar.py -q
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS。
|
|
|
+
|
|
|
+- [ ] **Step 2: 手测清单(Web + H5)**
|
|
|
+
|
|
|
+1. 四个 Tab 顺序:酸 → 碱 → 其他日历 → 实际派车信息
|
|
|
+2. 「其他日历」新增一条,切到酸/碱看不到;酸/碱记录在「其他」看不到
|
|
|
+3. 编辑保存、删除、筛选车辆、复制当天计划可用;Web 打印可用
|
|
|
+4. 「实际派车信息」筛选/汇总无异常;手工主题色在「其他日历」仍为深蓝 `#1f4e79`
|
|
|
+
|
|
|
+- [ ] **Step 3: 更新知识图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh all
|
|
|
+# 或按仓库规则:backend + web-admin 增量;根目录 graphify update .
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 最终 commit(若有图谱或小修)**
|
|
|
+
|
|
|
+仅暂存图谱/修复相关文件后:
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+chore: refresh graph after other calendar kind.
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage self-check
|
|
|
+
|
|
|
+| Spec 要求 | Task |
|
|
|
+|-----------|------|
|
|
|
+| 两端增加「其他日历」Tab,顺序在碱后、实际前 | Task 3 |
|
|
|
+| 功能/样式与酸碱一致,复用 ManualCalendar | Task 3 |
|
|
|
+| `calendarKind=other`,产品车辆选择器不变 | Task 1 + 3(不改 options API) |
|
|
|
+| 数据隔离、不可改 kind | Task 1 |
|
|
|
+| 无新权限码 | 无菜单任务(刻意不做) |
|
|
|
+| COMMENT 迁移 | Task 2 |
|
|
|
+| 实际派车信息不回归 | Task 4 手测 |
|
|
|
+| 测试 / lint / 图谱 | Task 1、3、4 |
|