docs: add ETC-only vehicle recharge design/plan and refresh graphify

Persist the approved cutover spec and implementation plan; sync graphs after the ETC-only changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-12 08:50:12 +08:00
1 parent 9a8dd691d8
commit 9968fc72a3
7 files changed
+38936 -38407

No files matched your search

@@ -0,0 +1,397 @@
# 车辆充值模块收窄为仅 ETC 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:** 将「ETC加油申请 / ETC加油发放登记」收窄为仅 ETC:菜单改名、UI 只留 ETC、列表/选项/导出强制只出 etc,历史 fuel 留库不删。
**Architecture:** Schema 写路径只允许 `etc`;Service 读路径(列表/看板/导出/pending/按 id)统一 `recharge_type == 'etc'`;SQL migration 改菜单名并软停用字典「加油」;前端去掉加油选项并同步路由标题。
**Tech Stack:** FastAPI + Pydantic v2、SQLAlchemy、Vue 3 + Element Plus、MySQL migration
**Spec:** `docs/superpowers/specs/2026-09-12-pl-vehicle-recharge-etc-only-design.md`
---
## File map
| File | Responsibility |
|------|----------------|
| `docs/sql/migrations/2026-09-12-pl-vehicle-recharge-etc-only.sql` | 菜单改名 + 字典 fuel 软停用 |
| `backend/app/schemas/pl/vehicle_recharge.py` | `RECHARGE_TYPES = {"etc"}` |
| `backend/app/services/pl_vehicle_recharge_service.py` | 读强制 etc;get 对 fuel 等同不存在 |
| `backend/tests/test_vehicle_recharge_etc_only.py` | Schema + 常量断言单测 |
| `web-admin/src/router/modules/pl.ts` | `meta.title` |
| `web-admin/src/views/pl/vehicle-recharge-apply/index.vue` | 文案 + 下拉 |
| `web-admin/src/views/pl/vehicle-recharge-record/index.vue` | 文案 + 下拉 |
| `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts` | 列头枚举只留 ETC |
**Do not change:** API path、perms、表结构、`getPlRechargeTypeLabel` 对 `fuel` 的映射(历史标签仍可解析,列表已不可见)。
---
### Task 1: SQL migration(菜单 + 字典)
**Files:**
- Create: `docs/sql/migrations/2026-09-12-pl-vehicle-recharge-etc-only.sql`
- [ ] **Step 1: 写 migration**
```sql
-- 车辆充值模块收窄为仅 ETC:菜单改名 + 加油字典软停用(不删业务数据)
-- Spec: docs/superpowers/specs/2026-09-12-pl-vehicle-recharge-etc-only-design.md
UPDATE sys_menu
SET menu_name = 'ETC充值申请',
update_time = NOW()
WHERE id = 'PL_VEH_RECHARGE_APPLY'
AND del_flag = 0;
UPDATE sys_menu
SET menu_name = 'ETC充值发放登记',
update_time = NOW()
WHERE id = 'PL_VEH_RECHARGE_RECORD'
AND del_flag = 0;
UPDATE sys_dict_data
SET del_flag = 1,
remark = 'ETC模块收窄:加油选项停用,业务数据保留待新加油模块',
update_time = NOW()
WHERE id = 'PL_RECHARGE_TYPE_FUEL'
AND del_flag = 0;
-- 可选知情统计(只读,不写入变更):
-- SELECT COUNT(*) FROM pl_vehicle_recharge_apply
-- WHERE del_flag = 0 AND recharge_type = 'fuel' AND status = 'pending';
```
- [ ] **Step 2: Commit**
```bash
git add docs/sql/migrations/2026-09-12-pl-vehicle-recharge-etc-only.sql
git commit -m "$(cat <<'EOF'
docs(sql): narrow vehicle recharge menus to ETC-only labels
Rename ETC/fuel menus and soft-disable fuel dict for the ETC-only cutover.
EOF
)"
```
---
### Task 2: Schema 只允许 etc + 单测
**Files:**
- Modify: `backend/app/schemas/pl/vehicle_recharge.py`
- Create: `backend/tests/test_vehicle_recharge_etc_only.py`
- [ ] **Step 1: 写失败单测(当前仍接受 fuel)**
```python
"""车辆充值模块收窄为仅 ETC。"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.schemas.pl.vehicle_recharge import (
RECHARGE_TYPES,
VehicleRechargeApplyCreate,
VehicleRechargeRecordCreate,
)
def test_recharge_types_only_etc():
assert RECHARGE_TYPES == {"etc"}
def test_apply_create_rejects_fuel():
with pytest.raises(ValidationError):
VehicleRechargeApplyCreate.model_validate(
{
"licensePlate": "鄂A12345",
"rechargeType": "fuel",
"applyDate": "2026-09-12",
}
)
def test_apply_create_accepts_etc():
obj = VehicleRechargeApplyCreate.model_validate(
{
"licensePlate": "鄂A12345",
"rechargeType": "etc",
"applyDate": "2026-09-12",
}
)
assert obj.recharge_type == "etc"
def test_record_create_rejects_fuel():
with pytest.raises(ValidationError):
VehicleRechargeRecordCreate.model_validate(
{
"rechargeDate": "2026-09-12",
"rechargeType": "fuel",
"amount": "100.00",
"licensePlate": "鄂A12345",
}
)
```
- [ ] **Step 2: 跑测确认失败**
Run:
```bash
cd backend && source venv/bin/activate && pytest tests/test_vehicle_recharge_etc_only.py -v
```
Expected: `test_recharge_types_only_etc` FAIL(当前 `RECHARGE_TYPES == {"etc", "fuel"}`)。
- [ ] **Step 3: 改 Schema**
In `backend/app/schemas/pl/vehicle_recharge.py`:
```python
RECHARGE_TYPES = {"etc"}
```
Update both validators' error message to:
```python
if v not in RECHARGE_TYPES:
raise ValueError("本期仅支持 ETC")
return v
```
(`VehicleRechargeApplyCreate.validate_recharge_type` 与 `VehicleRechargeRecordCreate.validate_recharge_type` 两处。)
- [ ] **Step 4: 跑测确认通过**
```bash
cd backend && source venv/bin/activate && pytest tests/test_vehicle_recharge_etc_only.py -v
```
Expected: 全部 PASS。
- [ ] **Step 5: Commit**
```bash
git add backend/app/schemas/pl/vehicle_recharge.py backend/tests/test_vehicle_recharge_etc_only.py
git commit -m "$(cat <<'EOF'
fix(pl): allow only etc recharge type in vehicle recharge schemas
Reject fuel writes so the shared module can be narrowed to ETC-only.
EOF
)"
```
---
### Task 3: Service 读路径强制 etc + fuel 按 id 不可见
**Files:**
- Modify: `backend/app/services/pl_vehicle_recharge_service.py`
- [ ] **Step 1: 在 service 顶部增加模块常量**
```python
# 本期本模块仅服务 ETC;历史 fuel 行保留但不暴露
MODULE_RECHARGE_TYPE = "etc"
```
- [ ] **Step 2: `_get_apply_row` / `_get_record_row` 增加类型条件**
In `_get_apply_row` filter add:
```python
PlVehicleRechargeApply.recharge_type == MODULE_RECHARGE_TYPE,
```
In `_get_record_row` filter add:
```python
PlVehicleRechargeRecord.recharge_type == MODULE_RECHARGE_TYPE,
```
保留原有 404 文案「申请单不存在」/「发放登记不存在」(与 spec:等同不存在)。
- [ ] **Step 3: `list_applies` 基查询强制 etc**
在初始 `.filter(tenant_no, del_flag == 0)` 中增加:
```python
PlVehicleRechargeApply.recharge_type == MODULE_RECHARGE_TYPE,
```
(与列头 `recharge_type` / `recharge_type_in` AND 叠加:客户端传 fuel 时结果为空。)
- [ ] **Step 4: `list_pending_apply_options` 强制 etc**
在其 `.filter(...)` 增加:
```python
PlVehicleRechargeApply.recharge_type == MODULE_RECHARGE_TYPE,
```
- [ ] **Step 5: `list_records` 强制 etc**
将可选:
```python
if recharge_type:
query = query.filter(PlVehicleRechargeRecord.recharge_type == recharge_type)
```
改为始终:
```python
query = query.filter(PlVehicleRechargeRecord.recharge_type == MODULE_RECHARGE_TYPE)
```
(忽略请求里的 `recharge_type` 参数,避免绕过。)
- [ ] **Step 6: `list_disburse_board` 强制 etc**
在申请基查询 `.filter(...)` 增加:
```python
PlVehicleRechargeApply.recharge_type == MODULE_RECHARGE_TYPE,
```
- [ ] **Step 7: 跑既有 + 新单测**
```bash
cd backend && source venv/bin/activate && pytest tests/test_vehicle_recharge_etc_only.py tests/test_pl_vehicle_ops_list_filters.py -v
```
Expected: PASS。
- [ ] **Step 8: Commit**
```bash
git add backend/app/services/pl_vehicle_recharge_service.py
git commit -m "$(cat <<'EOF'
fix(pl): hide non-ETC vehicle recharge rows in list and get APIs
Force etc on reads so historical fuel data stays in DB but out of this module.
EOF
)"
```
---
### Task 4: 前端菜单标题 + 只留 ETC 选项
**Files:**
- Modify: `web-admin/src/router/modules/pl.ts`
- Modify: `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts`
- Modify: `web-admin/src/views/pl/vehicle-recharge-apply/index.vue`
- Modify: `web-admin/src/views/pl/vehicle-recharge-record/index.vue`
- [ ] **Step 1: 路由标题**
In `web-admin/src/router/modules/pl.ts`,四处替换:
- `'ETC加油申请'` → `'ETC充值申请'`
- `'ETC加油发放登记'` → `'ETC充值发放登记'`
- [ ] **Step 2: 列头枚举**
In `columnFilterDefs.ts`:
```typescript
const RECHARGE_TYPE_OPTIONS = [{ value: 'etc', label: 'ETC' }]
```
- [ ] **Step 3: 申请页**
1. File comment: `<!-- ETC充值申请 -->`
2. Form select — 删除加油 option,只保留:
```vue
<el-option label="ETC" value="etc" />
```
3. 类型收窄:`rechargeType: 'etc' as 'etc'`(去掉 `| 'fuel'`)
- [ ] **Step 4: 发放页**
1. File comment: `<!-- ETC充值发放登记 -->`
2. 同样删除 `<el-option label="加油" value="fuel" />`
3. 收窄 `rechargeType` 类型同上
4. Optional:placeholder「ETC卡号/油卡号」→「ETC卡号」
- [ ] **Step 5: 本地目视 / 类型检查**
```bash
cd web-admin && pnpm exec vue-tsc --noEmit --pretty false 2>&1 | head -n 40
```
浏览器:侧栏标题、新建下拉、列头漏斗仅 ETC。
- [ ] **Step 6: Commit**
```bash
git add web-admin/src/router/modules/pl.ts \
web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts \
web-admin/src/views/pl/vehicle-recharge-apply/index.vue \
web-admin/src/views/pl/vehicle-recharge-record/index.vue
git commit -m "$(cat <<'EOF'
fix(web-admin): rename ETC recharge menus and drop fuel type option
Align UI with ETC-only scope; list filtering is enforced by the backend.
EOF
)"
```
---
### Task 5: 图谱更新 + 手工验收清单
- [ ] **Step 1: 更新知识图谱**
```bash
./scripts/update-graph.sh all
```
- [ ] **Step 2: 手工验收(对照 spec)**
| # | 检查项 | 期望 |
|---|--------|------|
| 1 | 执行 migration 后侧栏 | ETC充值申请 / ETC充值发放登记 |
| 2 | 新建申请/补录发放 | 只能选 ETC,落库 etc |
| 3 | POST/PUT `rechargeType=fuel` | 422 / 校验失败 |
| 4 | 库中已有 fuel 行 | 列表、看板、导出、pending-options 均不可见 |
| 5 | GET fuel 的 id | 与「不存在」相同 |
| 6 | etc 全流程 | 申请→待发放→发放正常 |
| 7 | 字典管理 | 「加油」不再出现在有效选项 |
- [ ] **Step 3: Commit 图谱(若有 diff)**
```bash
git add graphify-out backend/graphify-out web-admin/graphify-out
git commit -m "$(cat <<'EOF'
chore: update graphify after ETC-only vehicle recharge cutover
EOF
)"
```
---
## Spec coverage self-check
| Spec 项 | Task |
|---------|------|
| D1 列表只出 etc | Task 3 |
| D2 菜单名 | Task 1 + Task 4 |
| D3 待发放加油可隐去 | Task 3(pending + board) |
| D4 UI 只留 ETC | Task 4 |
| D5 不删数据 / 不改 path·perms | 全任务均未 DELETE 业务行、未改 path |
| D6 写拒 fuel + 字典软停用 | Task 1 + Task 2 |
| get fuel ≡ 不存在 | Task 3 Step 2 |
| 导出只出 etc | Task 3(list/board 被 export 复用) |
## Out of scope(勿做)
- 新加油模块、数据迁移、改 API path、改权限码、删 `fuel` 历史行
@@ -0,0 +1,157 @@
# 车辆充值模块收窄为仅 ETC — 设计文档
> **日期**: 2026-09-12
> **状态**: 已批准
> **前置**: `docs/superpowers/specs/2026-06-29-pl-vehicle-recharge-design.md`(原 ETC/加油共用模块)
> **界面模式**: 沿用既有 P01 + 发放看板;本期仅文案与类型收窄,不换模式
## 背景与目标
现有「运输管理 → 车辆管理」下两个菜单同时支持 **ETC** 与 **加油**(`recharge_type`: `etc` | `fuel`):
| 当前菜单名 | 组件 | 表 |
|---|---|---|
| ETC加油申请 | `web-admin/src/views/pl/vehicle-recharge-apply/index.vue` | `pl_vehicle_recharge_apply` |
| ETC加油发放登记 | `web-admin/src/views/pl/vehicle-recharge-record/index.vue` | `pl_vehicle_recharge_record` |
客户希望将**加油申请**后续独立成模块;本期先把上述两菜单收窄为 **仅 ETC 充值**。生产环境历史数据**不可删除**。
## 已确认业务规则
| # | 规则 | 决策 |
|---|------|------|
| D1 | 列表范围 | 本模块只展示 `recharge_type = etc`;历史 `fuel` 留库、本模块不出现,等新加油模块再管 |
| D2 | 菜单名称 | `ETC充值申请` / `ETC充值发放登记` |
| D3 | 待发放加油单 | 可隐去;客户已知晓;本期不迁移、不另开临时入口 |
| D4 | 充值类型 UI | 仍显示「充值类型」字段,下拉**只保留 ETC** |
| D5 | 数据安全 | 不物理删表/行;不改 path、component、权限码 |
| D6 | 实现深度 | 前端约束 + 后端读强制 ETC + 写拒绝 `fuel` + 字典软停用「加油」 |
## 非目标(本期不做)
- 独立「加油申请 / 加油发放」模块与菜单
- 迁移、清洗或改写历史 `fuel` 行
- 变更表结构、API 路径、权限编码、单号规则、附件目录
- 审批流、小程序、报表统计等新能力
- 页面布局重构或更换界面模式
## 方案选型
| 方案 | 说明 | 结论 |
|---|---|---|
| 1 仅前端改名 + 下拉 | 列表仍出加油;API 仍可写 `fuel` | 否:不满足 D1/D6 |
| **2 前后端收窄 + 字典软停用** | 菜单改名;列表/选项/导出只出 ETC;写只允许 etc;`fuel` 字典软停用 | **采用** |
| 3 拆表迁数据 | 新建加油模块并迁 `fuel` | 否:超出本期;加油后续再做 |
---
## 后端设计
### 读路径(强制只出 ETC)
在 service 层对下列能力统一限定 `recharge_type = 'etc'`(**即使请求参数未传、传 `fuel` 或伪造查询,也不返回加油数据**):
- 申请列表与导出
- 发放看板(`disburse-board`)与导出
- 发放记录列表(若仍有入口)
- 待发放下拉 `pending-options`
列头「充值类型」漏斗:枚举仅 ETC。若客户端传入非法/`fuel` 过滤值,服务端**强制按 etc 过滤**(不扩大为加油数据)。
按 id 详情 / 编辑 / 作废 / 删除:若目标行为历史 `fuel`,本模块 **与「记录不存在」相同处理**(沿用现有 get 失败语义),**不删库数据**。
### 写路径(只允许 etc)
- Schema:`RECHARGE_TYPES` 本期改为仅 `{"etc"}`;create/update 校验失败时明确提示「本期仅支持 ETC」
- 申请 create/update、发放 create/update:拒绝 `fuel`
- 发放关联申请:若 `apply_id` 指向 `fuel` 申请 → 拒绝(正常 UI 已选不到)
### 不变项
- 表名、字段、软删除语义、申请↔发放 1:0~1 关系、无申请补录、车牌范围、附件规则、单号规则均沿用原设计
---
## 前端设计
### 菜单与标题
| 位置 | 改动 |
|---|---|
| SQL migration `sys_menu` | `PL_VEH_RECHARGE_APPLY` → `ETC充值申请`;`PL_VEH_RECHARGE_RECORD` → `ETC充值发放登记` |
| `web-admin/src/router/modules/pl.ts` | 正式路由 + redirect 隐藏路由共四处 `meta.title` 同步 |
| 两页 Vue 注释 / 页内标题文案 | 与菜单名一致 |
**不动:** `path`、`component`、`perms` / `menuCode`、菜单图标、badge 编码。
### 充值类型控件
| 位置 | 改动 |
|---|---|
| 申请页、发放页表单 `el-select` | 去掉「加油」选项,只留 ETC |
| `columnFilterDefs.ts` 中 `RECHARGE_TYPE_OPTIONS` | 只保留 `{ value: 'etc', label: 'ETC' }` |
| 新建默认值 | `rechargeType: 'etc'`(字段仍展示) |
列表请求不必额外依赖前端传 `rechargeType=etc`(后端已强制);传了也不冲突。
当前页面选项为 Vue 硬编码,不依赖字典;字典停用仍做,避免系统字典管理或其他调用方再拉出「加油」。
---
## 数据与 Migration
新建:`docs/sql/migrations/2026-09-12-pl-vehicle-recharge-etc-only.sql`(名称可微调),内容至少包括:
1. **菜单改名**(仅 `menu_name`)
2. **字典软停用**:`PL_RECHARGE_TYPE_FUEL` 置 `del_flag = 1`,`remark` 注明「ETC 模块收窄停用,数据保留待新加油模块」
**禁止:** `DELETE` 业务表行;禁止改 `recharge_type` 历史值;禁止 drop/alter 业务表结构。
上线前可选(运维/告知用,不阻塞发版):
```sql
-- 待发放加油申请笔数(知情统计)
SELECT COUNT(*) FROM pl_vehicle_recharge_apply
WHERE del_flag = 0 AND recharge_type = 'fuel' AND status = 'pending';
```
---
## 影响文件(实现清单)
| 层 | 文件(预期) |
|---|---|
| SQL | `docs/sql/migrations/2026-09-12-pl-vehicle-recharge-etc-only.sql`;基线快照按仓库惯例择机同步 |
| Backend schema | `backend/app/schemas/pl/vehicle_recharge.py`(`RECHARGE_TYPES`) |
| Backend service | `backend/app/services/pl_vehicle_recharge_service.py`;必要时 `vehicle_recharge_list_filters.py` |
| Backend API | `backend/app/api/pl/vehicle_recharge.py`(若 get-by-id 需在路由/服务层拦截 fuel) |
| Frontend | `vehicle-recharge-apply/index.vue`、`vehicle-recharge-record/index.vue`、`columnFilterDefs.ts`、`router/modules/pl.ts` |
---
## 测试要点
1. 侧栏与 Tab 标题为「ETC充值申请」「ETC充值发放登记」
2. 新建申请/发放:只能选 ETC;默认 ETC;提交成功且落库 `etc`
3. API 传 `rechargeType=fuel` 创建/更新 → 失败
4. 库中已有 `fuel` 申请/发放:两菜单列表、看板、导出、pending-options **均不可见**
5. 用已知 `fuel` 的 id 调详情/编辑 → 与记录不存在相同失败语义
6. `etc` 单据全流程(申请 → 待发放 → 发放登记)回归正常
7. 字典管理中「加油」不再作为有效选项出现(软删后)
8. 回滚演练思路:恢复菜单名 + 恢复 `RECHARGE_TYPES`/`fuel` 读过滤与字典 `del_flag`;业务数据无需恢复
---
## 后续(不在本期)
独立「加油申请 / 加油发放」模块时,可:
- 复用或迁出 `recharge_type = fuel` 历史行,或新建业务表
- 恢复/新建加油相关菜单与字典项
- 本 ETC 模块保持仅 etc 不变
---
## 与原设计文档关系
本变更是对 `2026-06-29-pl-vehicle-recharge-design.md` 的**产品收窄补丁**:原 D2 菜单名、充值类型双值语义在本期被 D1–D6 覆盖;其余申请/发放规则仍有效。