Просмотр исходного кода

docs: add implementation plan for mini ETC recharge apply.

Breaks down mineVehicles backend scope, menus, and miniprogram list/form tasks with TDD steps.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 месяц назад
Родитель
Сommit
6f6793d7d1
1 измененных файлов с 807 добавлено и 0 удалено
  1. 807 0
      docs/superpowers/plans/2026-09-27-mini-pl-vehicle-recharge-apply.md

+ 807 - 0
docs/superpowers/plans/2026-09-27-mini-pl-vehicle-recharge-apply.md

@@ -0,0 +1,807 @@
+# 小程序 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加油充值申请」,司机/押运员按车辆人员归属查看与申请,复用现有充值申请 API(`mineVehicles` 作用域)。
+
+**Architecture:** 后端在 `list/get/create/update/submit/cancel` 与车辆列表增加 `mineVehicles`;解析 `user → pl_personnel.sys_user_id → pl_vehicle_personnel → license_plate`。`mineVehicles=true` 时用车牌集过滤,**跳过** `apply_pl_scope`/`assert_pl_row_in_scope`(否则「本人数据范围」会挡住他人代申请)。写操作另校验申请人=当前用户。小程序页面骨架对齐加油登记。
+
+**Tech Stack:** FastAPI + SQLAlchemy;uni-app Vue3 小程序;MySQL 菜单 SQL;pytest(SQLite 内存库,对齐现有 `test_pl_vehicle_recharge_workflow.py`)。
+
+**Spec:** `docs/superpowers/specs/2026-09-27-mini-pl-vehicle-recharge-apply-design.md`
+
+---
+
+## File map
+
+| 文件 | 职责 |
+|------|------|
+| `backend/app/services/pl_vehicle_mine_vehicles.py` | **新建**:本人车辆车牌集解析(可复用 `resolve_personnel_for_user`) |
+| `backend/app/services/pl_vehicle_recharge_service.py` | `mine_vehicles` 接入 list/get/create/update/submit/cancel |
+| `backend/app/api/pl/vehicle_recharge.py` | 解析 `mineVehicles` Query/参数 |
+| `backend/app/api/pl/base_data.py` | `GET /vehicle` 支持 `mineVehicles` |
+| `backend/tests/test_pl_vehicle_recharge_mine_vehicles.py` | **新建**:过滤与写校验测试 |
+| `docs/sql/migrations/2026-09-27-mini-pl-vehicle-recharge-apply-menus.sql` | 菜单 + 司机/押运员授权 |
+| `miniprogram/api/pl.js` | `vehicleRechargeApplyApi`;车辆 options 传 `mineVehicles` |
+| `miniprogram/api/dbOptions.js` | `fetchVehicleOptions` 支持 `mineVehicles` |
+| `miniprogram/config/api.js` / `workbench-menus.js` / `pages.json` | 路由与菜单 |
+| `miniprogram/pages/pl/vehicle-recharge-apply/list.vue` | 列表 |
+| `miniprogram/pages/pl/vehicle-recharge-apply/form.vue` | 表单/详情 |
+
+---
+
+### Task 1: 后端 — 本人车辆车牌集 helper + 失败测试
+
+**Files:**
+- Create: `backend/app/services/pl_vehicle_mine_vehicles.py`
+- Create: `backend/tests/test_pl_vehicle_recharge_mine_vehicles.py`
+
+- [ ] **Step 1: 写失败测试(车牌集解析)**
+
+```python
+"""mineVehicles:车辆人员归属过滤与写校验。"""
+from __future__ import annotations
+
+from datetime import date, datetime
+from types import SimpleNamespace
+
+import pytest
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+from app.models.pl_models import (
+    PlPersonnel,
+    PlVehicle,
+    PlVehiclePersonnel,
+    PlVehicleRechargeApply,
+)
+
+
+def _make_session():
+    engine = create_engine("sqlite://")
+    for table in (
+        PlVehicle.__table__,
+        PlPersonnel.__table__,
+        PlVehiclePersonnel.__table__,
+        PlVehicleRechargeApply.__table__,
+    ):
+        table.create(engine)
+    return sessionmaker(bind=engine)()
+
+
+def _user(uid: str = "u-driver") -> SimpleNamespace:
+    return SimpleNamespace(id=uid, name="司机甲", dept_id="d1", role="driver", tenant_no="t1")
+
+
+def _seed_person_vehicle(db, *, user_id="u-driver", plate="鄂A11111", vehicle_id="veh-a"):
+    db.add(
+        PlVehicle(
+            id=vehicle_id,
+            vehicle_type="self",
+            license_plate=plate,
+            tenant_no="t1",
+            create_dept_id="d1",
+            del_flag=0,
+        )
+    )
+    db.add(
+        PlPersonnel(
+            id="p-1",
+            personnel_name="司机甲",
+            personnel_type="driver",
+            personnel_affiliation="self",
+            status="1",
+            sys_user_id=user_id,
+            tenant_no="t1",
+            create_dept_id="d1",
+            del_flag=0,
+        )
+    )
+    db.add(
+        PlVehiclePersonnel(
+            id="vp-1",
+            vehicle_id=vehicle_id,
+            personnel_id="p-1",
+            role="driver",
+            is_default=1,
+            tenant_no="t1",
+            create_dept_id="d1",
+            del_flag=0,
+        )
+    )
+
+
+def test_license_plates_for_user_personnel():
+    from app.services.pl_vehicle_mine_vehicles import license_plates_for_user_personnel
+
+    db = _make_session()
+    _seed_person_vehicle(db)
+    db.add(
+        PlVehicle(
+            id="veh-b",
+            vehicle_type="self",
+            license_plate="鄂A22222",
+            tenant_no="t1",
+            create_dept_id="d1",
+            del_flag=0,
+        )
+    )
+    db.commit()
+    plates = license_plates_for_user_personnel(db, "t1", "u-driver")
+    assert plates == {"鄂A11111"}
+
+
+def test_license_plates_empty_when_no_personnel():
+    from app.services.pl_vehicle_mine_vehicles import license_plates_for_user_personnel
+
+    db = _make_session()
+    db.commit()
+    assert license_plates_for_user_personnel(db, "t1", "u-nobody") == set()
+```
+
+- [ ] **Step 2: 运行确认失败**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_pl_vehicle_recharge_mine_vehicles.py::test_license_plates_for_user_personnel -v
+```
+
+Expected: FAIL(模块/函数不存在)
+
+- [ ] **Step 3: 实现 helper**
+
+```python
+# backend/app/services/pl_vehicle_mine_vehicles.py
+"""当前用户关联驾押人员所挂靠车辆的车牌集合。"""
+from __future__ import annotations
+
+from typing import Optional, Set
+
+from sqlalchemy.orm import Session
+
+from app.models.pl_models import PlPersonnel, PlVehicle, PlVehiclePersonnel
+from app.services.pl_driver_plan_service import resolve_personnel_for_user
+
+
+def license_plates_for_user_personnel(
+    db: Session, tenant_no: str, user_id: str
+) -> Set[str]:
+    personnel = resolve_personnel_for_user(db, tenant_no=tenant_no, user_id=user_id)
+    if not personnel:
+        return set()
+    rows = (
+        db.query(PlVehicle.license_plate)
+        .join(
+            PlVehiclePersonnel,
+            PlVehiclePersonnel.vehicle_id == PlVehicle.id,
+        )
+        .filter(
+            PlVehiclePersonnel.personnel_id == personnel.id,
+            PlVehiclePersonnel.tenant_no == tenant_no,
+            PlVehiclePersonnel.del_flag == 0,
+            PlVehicle.tenant_no == tenant_no,
+            PlVehicle.del_flag == 0,
+        )
+        .all()
+    )
+    return {str(r[0]).strip() for r in rows if r and r[0] and str(r[0]).strip()}
+
+
+def resolve_personnel_id_for_user(
+    db: Session, tenant_no: str, user_id: str
+) -> Optional[str]:
+    p = resolve_personnel_for_user(db, tenant_no=tenant_no, user_id=user_id)
+    return p.id if p else None
+```
+
+- [ ] **Step 4: 再跑测试通过**
+
+```bash
+pytest tests/test_pl_vehicle_recharge_mine_vehicles.py::test_license_plates_for_user_personnel tests/test_pl_vehicle_recharge_mine_vehicles.py::test_license_plates_empty_when_no_personnel -v
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/services/pl_vehicle_mine_vehicles.py backend/tests/test_pl_vehicle_recharge_mine_vehicles.py
+git commit -m "feat(pl): add helper for user personnel vehicle plates"
+```
+
+---
+
+### Task 2: 后端 — list_applies / get_apply 的 mine_vehicles
+
+**Files:**
+- Modify: `backend/app/services/pl_vehicle_recharge_service.py`
+- Modify: `backend/tests/test_pl_vehicle_recharge_mine_vehicles.py`
+
+- [ ] **Step 1: 追加失败测试**
+
+在测试文件中增加(并补齐必要 seed / monkeypatch,对齐 `test_pl_vehicle_recharge_workflow.py` 的 `_patch_scope`):
+
+```python
+def _seed_apply(db, *, apply_id, plate, applicant_id, status="draft"):
+    db.add(
+        PlVehicleRechargeApply(
+            id=apply_id,
+            apply_no=f"SXSQ{apply_id}",
+            license_plate=plate,
+            recharge_type="etc",
+            apply_date=date.today(),
+            applicant_id=applicant_id,
+            status=status,
+            tenant_no="t1",
+            create_by=applicant_id,
+            create_dept_id="d1",
+            create_time=datetime.now(),
+            update_time=datetime.now(),
+            del_flag=0,
+        )
+    )
+
+
+def test_list_applies_mine_vehicles_includes_others_on_my_plate(monkeypatch):
+    from app.services import pl_vehicle_recharge_service as svc
+
+    db = _make_session()
+    _seed_person_vehicle(db)
+    _seed_apply(db, apply_id="a1", plate="鄂A11111", applicant_id="u-office")
+    _seed_apply(db, apply_id="a2", plate="鄂A99999", applicant_id="u-office")
+    db.commit()
+    monkeypatch.setattr(svc, "apply_pl_scope", lambda query, *_a, **_k: query)
+    monkeypatch.setattr(svc, "assert_pl_row_in_scope", lambda *_a, **_k: None)
+    monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
+    monkeypatch.setattr(svc, "apply_to_read", lambda row, db, **kw: SimpleNamespace(
+        id=row.id, licensePlate=row.license_plate, applicantId=row.applicant_id
+    ))
+
+    items, total = svc.list_applies(
+        db, "t1", _user(), mine_vehicles=True, page=1, page_size=20
+    )
+    assert total == 1
+    assert items[0].id == "a1"
+
+
+def test_get_apply_mine_vehicles_allows_others_on_my_plate(monkeypatch):
+    from app.services import pl_vehicle_recharge_service as svc
+    from fastapi import HTTPException
+
+    db = _make_session()
+    _seed_person_vehicle(db)
+    _seed_apply(db, apply_id="a1", plate="鄂A11111", applicant_id="u-office")
+    db.commit()
+    monkeypatch.setattr(svc, "assert_pl_row_in_scope", lambda *_a, **_k: (_ for _ in ()).throw(
+        AssertionError("should not use data scope when mine_vehicles")
+    ))
+    monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
+
+    # 实现后:应成功返回,且不应调用 assert_pl_row_in_scope
+    # 先期望当前签名不接受 mine_vehicles → TypeError / 或 403
+    with pytest.raises((TypeError, HTTPException)):
+        svc.get_apply(db, "t1", _user(), "a1", mine_vehicles=True)
+```
+
+(实现后把 `get` 测试改为断言成功返回 `a1`,并删掉「应抛错」分支。)
+
+- [ ] **Step 2: 跑测试确认失败**
+
+```bash
+pytest tests/test_pl_vehicle_recharge_mine_vehicles.py::test_list_applies_mine_vehicles_includes_others_on_my_plate -v
+```
+
+Expected: FAIL(`mine_vehicles` 未定义)
+
+- [ ] **Step 3: 改 `list_applies` / `get_apply` / `_get_apply_row`**
+
+在 `list_applies` 签名增加 `mine_vehicles: bool = False`。在 `apply_pl_scope` **之前**:
+
+```python
+if mine_vehicles:
+    plates = license_plates_for_user_personnel(db, tenant_no, user.id)
+    if not plates:
+        return [], 0
+    query = query.filter(PlVehicleRechargeApply.license_plate.in_(list(plates)))
+    # 跳过 apply_pl_scope:本人数据范围会挡住他人代申请
+    total = query.count()
+    ...
+    return [...], total
+
+query = apply_pl_scope(...)
+```
+
+`_get_apply_row` 增加可选 `mine_vehicles: bool = False`:
+
+```python
+if mine_vehicles:
+    plates = license_plates_for_user_personnel(db, tenant_no, user.id)
+    if (row.license_plate or "").strip() not in plates:
+        raise HTTPException(status_code=403, detail="无权查看该申请")
+    return row  # 不走 assert_pl_row_in_scope
+assert_pl_row_in_scope(...)
+```
+
+`get_apply(..., mine_vehicles: bool = False)` 传入 `_get_apply_row`。
+
+- [ ] **Step 4: 修正 get 测试为成功断言并跑通**
+
+```bash
+pytest tests/test_pl_vehicle_recharge_mine_vehicles.py -v
+```
+
+Expected: PASS(本文件全部)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/services/pl_vehicle_recharge_service.py backend/tests/test_pl_vehicle_recharge_mine_vehicles.py
+git commit -m "feat(pl): filter recharge applies by mineVehicles plates"
+```
+
+---
+
+### Task 3: 后端 — create / update / submit / cancel 写校验
+
+**Files:**
+- Modify: `backend/app/services/pl_vehicle_recharge_service.py`
+- Modify: `backend/tests/test_pl_vehicle_recharge_mine_vehicles.py`
+
+- [ ] **Step 1: 写失败测试**
+
+```python
+def test_create_apply_mine_vehicles_rejects_foreign_plate(monkeypatch):
+    from app.services import pl_vehicle_recharge_service as svc
+    from app.schemas.pl.vehicle_recharge import VehicleRechargeApplyCreate
+    from fastapi import HTTPException
+
+    db = _make_session()
+    _seed_person_vehicle(db)
+    db.commit()
+    monkeypatch.setattr(svc, "apply_pl_scope", lambda query, *_a, **_k: query)
+    monkeypatch.setattr(svc, "assert_pl_row_in_scope", lambda *_a, **_k: None)
+    monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
+    monkeypatch.setattr(svc, "_resolve_applicant_id", lambda *_a, **_k: "u-driver")
+    monkeypatch.setattr(svc, "_assert_vehicle_exists", lambda *_a, **_k: None)
+    monkeypatch.setattr(svc, "generate_apply_no", lambda *_a, **_k: "SXSQTEST1")
+
+    payload = VehicleRechargeApplyCreate(
+        licensePlate="鄂A99999",
+        rechargeType="etc",
+        applyDate=date.today(),
+    )
+    with pytest.raises(HTTPException) as ei:
+        svc.create_apply(db, "t1", _user(), payload, mine_vehicles=True)
+    assert ei.value.status_code in (400, 403)
+
+
+def test_update_cancel_submit_require_own_applicant(monkeypatch):
+    from app.services import pl_vehicle_recharge_service as svc
+    from fastapi import HTTPException
+
+    db = _make_session()
+    _seed_person_vehicle(db)
+    _seed_apply(db, apply_id="a1", plate="鄂A11111", applicant_id="u-office", status="draft")
+    db.commit()
+    monkeypatch.setattr(svc, "assert_pl_row_in_scope", lambda *_a, **_k: None)
+    monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
+
+    with pytest.raises(HTTPException) as ei:
+        svc.cancel_apply(db, "t1", _user(), "a1", mine_vehicles=True)
+    assert ei.value.status_code == 403
+```
+
+- [ ] **Step 2: 跑测确认失败**
+
+```bash
+pytest tests/test_pl_vehicle_recharge_mine_vehicles.py::test_create_apply_mine_vehicles_rejects_foreign_plate -v
+```
+
+- [ ] **Step 3: 实现写校验**
+
+在 service 中:
+
+```python
+def _assert_mine_vehicle_plate(db, tenant_no, user, plate: str) -> None:
+    plates = license_plates_for_user_personnel(db, tenant_no, user.id)
+    if (plate or "").strip() not in plates:
+        raise HTTPException(status_code=403, detail="只能选择本人车辆")
+
+
+def _assert_own_apply_for_write(row, user) -> None:
+    if (row.applicant_id or "").strip() != (user.id or "").strip():
+        raise HTTPException(status_code=403, detail="只能操作本人申请")
+```
+
+- `create_apply(..., mine_vehicles=False)`:若 true → `_assert_mine_vehicle_plate`;`applicant_id` 强制 `user.id`(可在调用 `_resolve_applicant_id` 前覆盖 payload 或传 `force_self`)
+- `update_apply`:`mine_vehicles` 时先 `_get_apply_row(..., mine_vehicles=True)`,再 `_assert_own_apply_for_write`,再校验车牌
+- `submit_apply` / `cancel_apply`:同上(读用车牌集,写要求本人申请人)
+
+注意:`create` 时 Web 路径不变;仅 `mine_vehicles=True` 强制申请人。
+
+- [ ] **Step 4: 全量本文件测试 + 工作流回归**
+
+```bash
+pytest tests/test_pl_vehicle_recharge_mine_vehicles.py tests/test_pl_vehicle_recharge_workflow.py -v
+```
+
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/services/pl_vehicle_recharge_service.py backend/tests/test_pl_vehicle_recharge_mine_vehicles.py
+git commit -m "feat(pl): enforce mineVehicles write rules on recharge apply"
+```
+
+---
+
+### Task 4: 后端 — API 层接入 `mineVehicles`
+
+**Files:**
+- Modify: `backend/app/api/pl/vehicle_recharge.py`
+
+- [ ] **Step 1: list / get / create / update / submit / cancel 增加 Query**
+
+模式(与 `vehicle_fuel.py` 的 `mineOnly` 类似):
+
+```python
+mine_vehicles: Optional[bool] = Query(default=None, alias="mineVehicles"),
+...
+mine_vehicles=_as_bool(_param_get(params, "mineVehicles", "mine_vehicles")),
+```
+
+对 POST/PUT:允许 Query `mineVehicles=true`(小程序 `db.post(..., { params: { mineVehicles: true } })`)。
+
+辅助:
+
+```python
+def _as_bool(v) -> bool:
+    if v is True or v is False:
+        return v
+    if v is None or v == "":
+        return False
+    return str(v).strip().lower() in ("1", "true", "yes", "y")
+```
+
+(若文件内已有类似函数则复用。)
+
+把解析结果传入 `svc.list_applies(..., mine_vehicles=...)` 等。
+
+- [ ] **Step 2: 手动或用 TestClient 冒烟(可选)**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_pl_vehicle_recharge_mine_vehicles.py tests/test_pl_vehicle_recharge_workflow.py -v
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add backend/app/api/pl/vehicle_recharge.py
+git commit -m "feat(api): accept mineVehicles on vehicle-recharge-apply endpoints"
+```
+
+---
+
+### Task 5: 后端 — 车辆列表 `mineVehicles`
+
+**Files:**
+- Modify: `backend/app/api/pl/base_data.py`(`list_vehicles`)
+- Optional test in `test_pl_vehicle_recharge_mine_vehicles.py` 或轻量单测车牌过滤 SQL
+
+- [ ] **Step 1: 在 `list_vehicles` 解析 `mineVehicles`**
+
+在现有 `query` 构建后、分页前:
+
+```python
+if _as_bool(_param_get(params, "mineVehicles", "mine_vehicles")):
+    from app.services.pl_vehicle_mine_vehicles import license_plates_for_user_personnel
+    plates = license_plates_for_user_personnel(db, tenant_no, user.id)
+    if not plates:
+        return VehicleListResponse(items=[], total=0, page=page, pageSize=page_size)
+    query = query.filter(PlVehicle.license_plate.in_(list(plates)))
+```
+
+注意:`_merge_query` 需纳入 `mineVehicles`;若函数内无 `_as_bool`,内联或从公共处引入。
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add backend/app/api/pl/base_data.py
+git commit -m "feat(api): filter vehicle options by mineVehicles personnel"
+```
+
+---
+
+### Task 6: SQL — 小程序菜单与角色授权
+
+**Files:**
+- Create: `docs/sql/migrations/2026-09-27-mini-pl-vehicle-recharge-apply-menus.sql`
+
+- [ ] **Step 1: 写幂等迁移**(对齐 driver-plan 计划中的 SQL 风格)
+
+要点:
+
+1. 幂等创建角色 `司机` / `押运员`(`NOT EXISTS role_name`)
+2. 幂等创建/复用 `MINI_PL_ROOT`
+3. 插入菜单:
+
+```sql
+-- id: MINI_PL_VEHICLE_RECHARGE_APPLY
+-- menu_name: ETC加油充值申请
+-- parent_id: MINI_PL_ROOT
+-- path: vehicle-recharge-apply
+-- perms: mini:pl:vehicleRechargeApply:list
+-- order_num: 2
+```
+
+4. `sys_role_menu`:对 `role_name IN ('司机','押运员')` 绑定 `1`、`MINI_PL_ROOT`、`MINI_PL_VEHICLE_RECHARGE_APPLY`(`NOT EXISTS` 防重)
+
+完整列名复制仓库内已有 migration 或 `docs/superpowers/plans/2026-09-13-mini-pl-driver-plan.md` Task 7 片段。
+
+- [ ] **Step 2: 在测试库执行该 SQL;生产执行前确认角色名**
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/sql/migrations/2026-09-27-mini-pl-vehicle-recharge-apply-menus.sql
+git commit -m "docs(sql): add mini ETC recharge apply menu for driver/escort"
+```
+
+---
+
+### Task 7: 小程序 — API / 路由 / 菜单 / pages.json
+
+**Files:**
+- Modify: `miniprogram/api/pl.js`
+- Modify: `miniprogram/api/dbOptions.js`
+- Modify: `miniprogram/config/api.js`
+- Modify: `miniprogram/config/workbench-menus.js`
+- Modify: `miniprogram/pages.json`
+
+- [ ] **Step 1: `dbOptions.js`**
+
+```js
+export async function fetchVehicleOptions(q, opts = {}) {
+  const params = {
+    ...(q?.trim() ? { q: q.trim() } : {}),
+    ...(opts.vehicleType ? { vehicleType: opts.vehicleType } : {}),
+    ...(opts.mineVehicles ? { mineVehicles: true } : {}),
+    page: 1,
+    pageSize: 100
+  }
+  // 其余保持现有 db.get / request 逻辑
+}
+```
+
+- [ ] **Step 2: `pl.js` 增加 API**
+
+```js
+export const vehicleRechargeApplyApi = {
+  list: async (params = {}) => {
+    const data = await db.get('/transport/vehicle-recharge-apply/', {
+      mineVehicles: true,
+      ...params
+    })
+    return unwrapList(data)
+  },
+  get: async (id) =>
+    unwrapEntity(
+      await db.get(`/transport/vehicle-recharge-apply/${id}`, {
+        mineVehicles: true
+      })
+    ),
+  create: async (payload) =>
+    unwrapEntity(
+      await db.post('/transport/vehicle-recharge-apply/', payload, {
+        params: { mineVehicles: true }
+      })
+    ),
+  update: async (id, payload) =>
+    unwrapEntity(
+      await db.put(`/transport/vehicle-recharge-apply/${id}`, payload, {
+        params: { mineVehicles: true }
+      })
+    ),
+  submit: (id) =>
+    db.post(
+      `/transport/vehicle-recharge-apply/${id}/submit`,
+      {},
+      { params: { mineVehicles: true } }
+    ),
+  cancel: (id) =>
+    db.post(
+      `/transport/vehicle-recharge-apply/${id}/cancel`,
+      {},
+      { params: { mineVehicles: true } }
+    )
+}
+```
+
+确认 `db.get` 第二参为 query;`unwrapEntity` 识别 `applyNo` 字段(可扩展现有判断:`data.applyNo == null`)。
+
+- [ ] **Step 3: 配置**
+
+`config/api.js`:
+
+```js
+plVehicleRechargeApply: '/pages/pl/vehicle-recharge-apply/list',
+plVehicleRechargeApplyForm: '/pages/pl/vehicle-recharge-apply/form',
+// WORKBENCH_ROUTE_MAP:
+'pl-vehicle-recharge-apply': PAGE_PATHS.plVehicleRechargeApply,
+```
+
+`workbench-menus.js` 运输管理 menus 在加油登记后插入:
+
+```js
+{
+  name: 'ETC加油充值申请',
+  enabled: true,
+  code: 'pl-vehicle-recharge-apply',
+  icon: '⛽', // 或选更贴切图标,与现有风格一致即可
+  perms: 'mini:pl:vehicleRechargeApply:list'
+}
+```
+
+`pages.json` 的 `pages/pl` 分包增加:
+
+```json
+{
+  "path": "vehicle-recharge-apply/list",
+  "style": { "navigationStyle": "custom", "navigationBarTitleText": "ETC加油充值申请" }
+},
+{
+  "path": "vehicle-recharge-apply/form",
+  "style": { "navigationStyle": "custom", "navigationBarTitleText": "ETC加油充值申请" }
+}
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add miniprogram/api/pl.js miniprogram/api/dbOptions.js miniprogram/config/api.js miniprogram/config/workbench-menus.js miniprogram/pages.json
+git commit -m "feat(mini): wire ETC recharge apply menu routes and API"
+```
+
+---
+
+### Task 8: 小程序 — 列表页
+
+**Files:**
+- Create: `miniprogram/pages/pl/vehicle-recharge-apply/list.vue`
+
+- [ ] **Step 1: 以 `fuel-register/list.vue` 为模板复制后改写**
+
+关键差异:
+
+- 标题:`ETC加油充值申请`
+- API:`vehicleRechargeApplyApi.list`
+- Tab:
+
+```js
+const tabs = [
+  { label: '全部', value: '' },
+  { label: '草稿', value: 'draft' },
+  { label: '审批中', value: 'in_approval' },
+  { label: '已驳回', value: 'rejected' },
+  { label: '已发放', value: 'disbursed' },
+  { label: '已作废', value: 'cancelled' }
+]
+```
+
+- 卡片 KV:申请单号、充值类型(etc→ETC / fuel→加油)、申请日期、申请人、期初公里数、备注
+- 金额区可改为展示充值类型文案或申请日期主信息(勿抄加油升数)
+- 操作:`isOwn = item.applicantId === userStore.userInfo?.userId`(或项目内实际用户 id 字段,对齐加油登记/userStore);`draft|rejected` 且本人 → 编辑/提交/作废;否则查看
+- 路由:`PAGE_PATHS.plVehicleRechargeApplyForm`
+
+从 `userStore` 取当前用户 id 的方式与 `fuel-register/form.vue` 保持一致。
+
+- [ ] **Step 2: 本地打开小程序工作台点入列表,确认空态/请求带 `mineVehicles=true`**
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add miniprogram/pages/pl/vehicle-recharge-apply/list.vue
+git commit -m "feat(mini): add ETC recharge apply list page"
+```
+
+---
+
+### Task 9: 小程序 — 表单/详情页
+
+**Files:**
+- Create: `miniprogram/pages/pl/vehicle-recharge-apply/form.vue`
+
+- [ ] **Step 1: 以 `fuel-register/form.vue` 为骨架实现**
+
+**可编辑字段:** 申请人(disabled 当前用户名)、车牌 sheet(`fetchVehicleOptions('', { mineVehicles: true })`;仅 1 辆自动选中)、充值类型分段(ETC/加油)、申请日期、期初公里数、备注、附件(folder `pl/vehicle-recharge/apply`,对齐加油登记上传写法)。
+
+**只读块(`get` 返回后):**
+
+- 审批:渲染 `approvalFlow` / `approvalHistory`(节点名、状态、意见、审批人、时间);可简化为列表卡片,不必 1:1 复刻 Web Tabs
+- 发放:若 `record` 存在,展示订单号/日期/金额/卡号/渠道/发放人/备注/附件;否则「暂无」
+
+**底栏:**
+
+- 可编辑:`保存` → create/update;`保存并提交` → 保存后 `submit`
+- 本人草稿/驳回:作废按钮 → `cancel`
+- `readonly`:他人单或非可编辑状态
+
+**权限判定:**
+
+```js
+const isOwn = computed(() => !editId.value || detail.value?.applicantId === currentUserId)
+const canEdit = computed(() => {
+  if (!isOwn.value && editId.value) return false
+  if (!editId.value) return true
+  return ['draft', 'rejected'].includes(detail.value?.status)
+})
+```
+
+打开他人单:`readonly=true`,仍加载审批与发放。
+
+- [ ] **Step 2: 手工验收**
+
+按 spec §8:新建草稿 → 提交 → Web 审批可见;驳回再提;他人单只读;无车辆提示。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add miniprogram/pages/pl/vehicle-recharge-apply/form.vue
+git commit -m "feat(mini): add ETC recharge apply form and detail"
+```
+
+---
+
+### Task 10: 收尾 — 图谱与回归
+
+**Files:** 无强制新文件
+
+- [ ] **Step 1: 后端回归**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_pl_vehicle_recharge_mine_vehicles.py tests/test_pl_vehicle_recharge_workflow.py tests/test_vehicle_recharge_types.py -v
+```
+
+Expected: PASS
+
+- [ ] **Step 2: 更新知识图谱**
+
+```bash
+./scripts/update-graph.sh backend
+# 若脚本支持 frontend/miniprogram 则按需;否则
+cd /Users/wuzilong/work/00code/shanxia2.0 && graphify update . 
+```
+
+(以仓库 `AGENTS.md` / `scripts/update-graph.sh` 实际参数为准。)
+
+- [ ] **Step 3: 最终 commit(若有图谱变更)**
+
+```bash
+git add backend/graphify-out miniprogram/graphify-out graphify-out 2>/dev/null || true
+git commit -m "chore: update graphify after mini recharge apply"
+```
+
+---
+
+## Spec coverage checklist
+
+| Spec 项 | Task |
+|---------|------|
+| D1 列表按车辆人员 | Task 2 |
+| D2 ETC/加油 | Task 9 |
+| D3 闭环与他人只读 | Task 3, 8, 9 |
+| D4 详情审批+发放 | Task 9 |
+| D5 mineVehicles API | Task 2–5 |
+| D6 申请人锁定 | Task 3, 9 |
+| D7 车牌下拉 | Task 5, 7, 9 |
+| D8 司机/押运员菜单 | Task 6, 7 |
+| 跳过 data scope(他人代申请可见) | Task 2(显式) |
+| UI 对齐加油登记 | Task 8, 9 |
+| Web 不传参数不变 | Task 2–4(默认 false)+ workflow 回归 |
+
+## Placeholder / consistency self-check
+
+- 参数名统一:`mineVehicles`(API)/ `mine_vehicles`(Python)
+- 函数名统一:`license_plates_for_user_personnel`
+- 菜单 id / perms:`MINI_PL_VEHICLE_RECHARGE_APPLY` / `mini:pl:vehicleRechargeApply:list`
+- 无 TBD;TDD 步骤含命令与期望