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>
This commit is contained in:
1 parent
3d7d999f48
commit
6f6793d7d1
1 file changed
+807
@@ -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 步骤含命令与期望
|
||||
Reference in new issue
Block a user