diff --git a/docs/superpowers/plans/2026-09-27-mini-pl-vehicle-recharge-apply.md b/docs/superpowers/plans/2026-09-27-mini-pl-vehicle-recharge-apply.md new file mode 100644 index 000000000..3d1471613 --- /dev/null +++ b/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 步骤含命令与期望