docs: add recharge 发放中 status design and plan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-12 19:48:55 +08:00
1 parent 553c1ef316
commit 7ebbaa2784
2 files changed
+622

No files matched your search

@@ -0,0 +1,498 @@
# 充值申请「发放中」状态 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:** 末级审批通过进入发放节点后,申请状态变为 `pending`,Web 展示为「发放中」;待办/徽章/发放登记与撤销回退一并兼容。
**Architecture:** 油卡/ETC 共用后端状态机。`approve_apply` 在下一节点为 `disburse` 时写入 `pending`;`_current_node` / `list_pending` / 菜单徽章同时接受 `in_approval` 与 `pending`;撤销发放有流程时回退 `pending`。前端仅改充值申请文案与筛选项(尿素等其它业务不动)。
**Tech Stack:** FastAPI / SQLAlchemy、pytest、Vue 3 + Element Plus(web-admin)
**Spec:** `docs/superpowers/specs/2026-11-12-pl-recharge-pending-disburse-status-design.md`
---
## File map
| 文件 | 职责 |
| --- | --- |
| `backend/app/schemas/pl/vehicle_recharge.py` | `APPLY_STATUSES` 纳入 `pending` |
| `backend/app/services/pl_vehicle_recharge_approval_service.py` | 审批推进切 `pending`;待办/当前节点准入 |
| `backend/app/services/pl_vehicle_recharge_service.py` | 撤销/换绑回退;关联提示文案 |
| `backend/app/services/pl_menu_badge_service.py` | 徽章计数含 `pending` |
| `backend/app/api/pl/vehicle_recharge.py` | 导出状态标签「发放中」 |
| `backend/tests/test_pl_vehicle_recharge_workflow.py` | 状态机单测 |
| `backend/tests/test_pl_menu_badge_recharge_approval.py` | 徽章含 pending |
| `web-admin/src/utils/pl-display.ts` | 充值申请 `pending` → 发放中 |
| `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts` | 筛选项 |
| `web-admin/src/views/pl/etc-vehicle-recharge-apply/columnFilterDefs.ts` | 筛选项 |
| `web-admin/src/views/pl/vehicle-recharge-record/index.vue` | 撤销确认文案 |
| `web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue` | 撤销确认文案 |
---
### Task 1: Schema + 审批推进写 pending(TDD)
**Files:**
- Modify: `backend/app/schemas/pl/vehicle_recharge.py`
- Modify: `backend/app/services/pl_vehicle_recharge_approval_service.py`
- Modify: `backend/tests/test_pl_vehicle_recharge_workflow.py`
- [ ] **Step 1: 改现有断言并加失败用例**
在 `test_apply_statuses_include_workflow_states` 中让集合包含 `pending`:
```python
assert {"draft", "in_approval", "pending", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
```
将 `test_approve_advances_to_next_node` 中通过后断言改为(默认种子流程下一节点是发放):
```python
assert result.status == "pending"
assert result.current_node == node_fin
```
新增三级流程用例(一级通过仍审批中,二级通过变发放中):
```python
def _seed_three_node_workflow(db) -> tuple[str, str, str, str]:
wf_id = "wf-3"
n1, n2, n3 = "node-a1", "node-a2", "node-dis"
db.add(
BxWorkflowDef(
id=wf_id,
biz_type="vehicle_recharge",
wf_name="三级充值流程",
status=1,
tenant_no="t1",
create_dept_id="d1",
)
)
for nid, name, order, uid, uname, action in (
(n1, "一审", 1, "u-a1", "一审", "approve"),
(n2, "二审", 2, "u-a2", "二审", "approve"),
(n3, "发放", 3, "u-fin", "财务", "disburse"),
):
db.add(
BxWorkflowNode(
id=nid,
wf_id=wf_id,
node_name=name,
node_order=order,
approver_id=uid,
approver_name=uname,
action_type=action,
tenant_no="t1",
create_dept_id="d1",
)
)
return wf_id, n1, n2, n3
def test_mid_approve_stays_in_approval_final_approve_sets_pending(monkeypatch):
from app.services import pl_vehicle_recharge_approval_service as approval_svc
from app.services import pl_vehicle_recharge_service as svc
db = _make_session()
_seed_vehicle(db)
wf_id, n1, n2, n3 = _seed_three_node_workflow(db)
db.add(
PlVehicleRechargeApply(
id="apply-1",
apply_no="YK2026092601",
license_plate="鄂A00001",
recharge_type="fuel",
apply_date=date(2026, 9, 26),
applicant_id="u-applicant",
status="in_approval",
wf_id=wf_id,
current_node=n1,
approval_round=1,
tenant_no="t1",
create_dept_id="d1",
)
)
db.commit()
_patch_scope(monkeypatch, svc)
mid = approval_svc.approve_apply(db, "t1", _user("u-a1", "一审"), "apply-1", opinion="ok")
assert mid.status == "in_approval"
assert mid.current_node == n2
final = approval_svc.approve_apply(db, "t1", _user("u-a2", "二审"), "apply-1", opinion="ok")
assert final.status == "pending"
assert final.current_node == n3
```
新增:`pending` 状态下可发放:
```python
def test_disburse_accepts_pending_status(monkeypatch):
from app.services import pl_vehicle_recharge_approval_service as approval_svc
from app.services import pl_vehicle_recharge_service as svc
from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate
db = _make_session()
_seed_vehicle(db)
wf_id, _gm, node_fin = _seed_workflow(db)
db.add(
PlVehicleRechargeApply(
id="apply-1",
apply_no="ETC2026092601",
license_plate="鄂A00001",
recharge_type="etc",
apply_date=date(2026, 9, 26),
applicant_id="u-applicant",
status="pending",
wf_id=wf_id,
current_node=node_fin,
approval_round=1,
tenant_no="t1",
create_dept_id="d1",
)
)
db.commit()
_patch_scope(monkeypatch, svc)
payload = VehicleRechargeRecordCreate(
rechargeDate=date(2026, 9, 26),
rechargeType="etc",
amount=Decimal("50.00"),
applyId="apply-1",
)
result = approval_svc.disburse_apply(
db, "t1", _user("u-fin", "财务"), "apply-1", payload
)
assert result.status == "disbursed"
```
- [ ] **Step 2: 跑测确认失败**
Run(在 `backend/`,已激活项目 venv):
```bash
pytest tests/test_pl_vehicle_recharge_workflow.py::test_apply_statuses_include_workflow_states \
tests/test_pl_vehicle_recharge_workflow.py::test_approve_advances_to_next_node \
tests/test_pl_vehicle_recharge_workflow.py::test_mid_approve_stays_in_approval_final_approve_sets_pending \
tests/test_pl_vehicle_recharge_workflow.py::test_disburse_accepts_pending_status -v
```
Expected: FAIL(`pending` 不在集合 / 状态仍为 `in_approval` / `申请单不在审批中`)
- [ ] **Step 3: 最小实现**
`vehicle_recharge.py` schema:
```python
APPLY_STATUSES = {"draft", "in_approval", "pending", "rejected", "disbursed", "cancelled"}
```
`pl_vehicle_recharge_approval_service.py` 中 `_current_node`:
```python
if apply.status not in ("in_approval", "pending") or not apply.current_node:
raise HTTPException(status_code=400, detail="申请单不在可审批/发放状态")
```
`list_pending` 过滤:
```python
PlVehicleRechargeApply.status.in_(("in_approval", "pending")),
```
`approve_apply` 在推进节点后:
```python
next_node = nodes[idx + 1]
apply.current_node = next_node.id
next_action = (next_node.action_type or "approve").strip() or "approve"
if next_action == "disburse":
apply.status = "pending"
```
(勿改中间 approve 的 status。)
- [ ] **Step 4: 再跑 Step 2 命令**
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add backend/app/schemas/pl/vehicle_recharge.py \
backend/app/services/pl_vehicle_recharge_approval_service.py \
backend/tests/test_pl_vehicle_recharge_workflow.py
git commit -m "$(cat <<'EOF'
feat(pl): set recharge apply to pending when entering disburse node
EOF
)"
```
---
### Task 2: 徽章计数含 pending
**Files:**
- Modify: `backend/app/services/pl_menu_badge_service.py`
- Modify: `backend/tests/test_pl_menu_badge_recharge_approval.py`
- [ ] **Step 1: 扩展徽章测试**
在现有 `add_apply` 种子后增加:
```python
add_apply("a6", status="pending", current_node="node-me")
```
并将断言改为:
```python
assert counts[MENU_CODE_RECHARGE_APPROVAL] == 3
```
- [ ] **Step 2: 跑测确认失败**
```bash
pytest tests/test_pl_menu_badge_recharge_approval.py -v
```
Expected: FAIL(仍为 2)
- [ ] **Step 3: 实现**
`count_recharge_approval_pending` 中:
```python
PlVehicleRechargeApply.status.in_(("in_approval", "pending")),
```
- [ ] **Step 4: 再跑测**
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add backend/app/services/pl_menu_badge_service.py \
backend/tests/test_pl_menu_badge_recharge_approval.py
git commit -m "$(cat <<'EOF'
fix(pl): include pending recharge applies in approval menu badge
EOF
)"
```
---
### Task 3: 撤销发放回退为 pending + 文案「发放中」
**Files:**
- Modify: `backend/app/services/pl_vehicle_recharge_service.py`(`delete_records` / 换绑旧申请回退两处:约 1136、1205 行,有 `wf_id` 时现为 `in_approval`)
- Modify: `backend/app/api/pl/vehicle_recharge.py`(`RECHARGE_APPLY_STATUS_LABELS`)
- Modify: `backend/app/services/pl_vehicle_recharge_service.py`(`仅待发放申请可关联` → `仅发放中申请可关联`)
- [ ] **Step 1: 写回退单测(可放在 workflow 测试文件)**
```python
def test_delete_record_with_workflow_reverts_to_pending(monkeypatch):
from app.services import pl_vehicle_recharge_service as svc
db = _make_session()
_seed_vehicle(db)
wf_id, _gm, node_fin = _seed_workflow(db)
db.add(
PlVehicleRechargeApply(
id="apply-1",
apply_no="ETC2026092601",
license_plate="鄂A00001",
recharge_type="etc",
apply_date=date(2026, 9, 26),
applicant_id="u-applicant",
status="disbursed",
wf_id=wf_id,
current_node=None,
approval_round=1,
tenant_no="t1",
create_by="u-applicant",
create_dept_id="d1",
)
)
db.add(
PlVehicleRechargeRecord(
id="rec-1",
recharge_order_no="RO1",
recharge_date=date(2026, 9, 26),
recharge_type="etc",
amount=Decimal("10.00"),
apply_id="apply-1",
license_plate="鄂A00001",
tenant_no="t1",
create_by="u-fin",
create_dept_id="d1",
del_flag=0,
)
)
db.commit()
_patch_scope(monkeypatch, svc)
svc.delete_records(db, "t1", _user("u-fin", "财务"), ["rec-1"])
apply = db.query(PlVehicleRechargeApply).filter_by(id="apply-1").one()
assert apply.status == "pending"
assert apply.current_node == node_fin
```
若 `PlVehicleRechargeRecord` 构造缺必填字段,按模型补齐后保持断言不变。
- [ ] **Step 2: 跑测确认失败**
```bash
pytest tests/test_pl_vehicle_recharge_workflow.py::test_delete_record_with_workflow_reverts_to_pending -v
```
Expected: FAIL(仍为 `in_approval`)
- [ ] **Step 3: 实现回退与文案**
两处有 wf 回退改为:
```python
old_apply.status = "pending" # 或 apply.status = "pending"
```
API 标签:
```python
"pending": "发放中",
```
服务提示:
```python
detail="仅发放中申请可关联",
```
- [ ] **Step 4: 跑回退测 + 相关 workflow 测**
```bash
pytest 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/app/api/pl/vehicle_recharge.py \
backend/tests/test_pl_vehicle_recharge_workflow.py
git commit -m "$(cat <<'EOF'
fix(pl): revert disbursed recharge apply to pending and rename label
EOF
)"
```
---
### Task 4: Web 文案与筛选项
**Files:**
- Modify: `web-admin/src/utils/pl-display.ts`(仅 `getPlRechargeApplyStatusLabel`,**不要**改 `getPlUreaApplyStatusLabel`)
- Modify: `web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts`
- Modify: `web-admin/src/views/pl/etc-vehicle-recharge-apply/columnFilterDefs.ts`
- Modify: `web-admin/src/views/pl/vehicle-recharge-record/index.vue`
- Modify: `web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue`
- [ ] **Step 1: 改充值状态文案**
```typescript
pending: '发放中',
```
- [ ] **Step 2: 筛选项插入 pending(建议放在审批中之后)**
油卡与 ETC 的 `RECHARGE_STATUS_OPTIONS`:
```typescript
{ value: 'draft', label: '草稿' },
{ value: 'in_approval', label: '审批中' },
{ value: 'pending', label: '发放中' },
{ value: 'rejected', label: '已驳回' },
{ value: 'disbursed', label: '已发放' },
{ value: 'cancelled', label: '已作废' }
```
- [ ] **Step 3: 撤销确认文案**
两处 record 页:
```typescript
'确定撤销该发放登记吗?关联申请将回退为发放中。'
```
全文检索 `web-admin` 内充值相关「待发放」字样,仅改充值申请/发放语境;跳过尿素、运费、备用金。
- [ ] **Step 4: 静态核对**
```bash
# 在 web-admin/
rg "待发放" src/utils/pl-display.ts src/views/pl/vehicle-recharge-apply src/views/pl/etc-vehicle-recharge-apply src/views/pl/vehicle-recharge-record src/views/pl/etc-vehicle-recharge-record
```
Expected: 上述路径无残留「待发放」(尿素等其它目录可仍有)
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/utils/pl-display.ts \
web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts \
web-admin/src/views/pl/etc-vehicle-recharge-apply/columnFilterDefs.ts \
web-admin/src/views/pl/vehicle-recharge-record/index.vue \
web-admin/src/views/pl/etc-vehicle-recharge-record/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): show recharge pending status as 发放中
EOF
)"
```
---
### Task 5: 回归验证
**Files:** 无新文件
- [ ] **Step 1: 后端相关测试全跑**
```bash
pytest tests/test_pl_vehicle_recharge_workflow.py \
tests/test_pl_menu_badge_recharge_approval.py \
tests/test_pl_vehicle_recharge_summary.py -v
```
Expected: PASS
- [ ] **Step 2: 手工冒烟清单(记结果,未跑勿称通过)**
1. 两级审批 + 发放节点:一审后「审批中」;二审后「发放中」;发放后「已发放」
2. 发放节点人审批待办仍可见并发放成功
3. 申请列表可用「发放中」筛选
4. 油卡、ETC 各至少一单(或同后端断言后单侧冒烟 + 说明)
- [ ] **Step 3: 若用户要求再 push;默认本任务不 push**
---
## Spec coverage check
| Spec 项 | Task |
| --- | --- |
| 末级 approve → pending | Task 1 |
| 中间仍 in_approval | Task 1 |
| 待办/准入认 pending | Task 1 |
| 徽章含 pending | Task 2 |
| 撤销回退 pending | Task 3 |
| 文案「发放中」 | Task 3–4 |
| 筛选项 | Task 4 |
| 不做历史刷库 / 不改尿素文案 | 明确不做 |
| 回归 | Task 5 |
@@ -0,0 +1,124 @@
# 充值申请「发放中」状态(审批通过后)
## 背景与问题
车辆充值(油卡 / ETC 共用后端)走审批流程时,单据从提交起一直是 `in_approval`(审批中),直到发放登记才变为 `disbursed`(已发放)。
用户配置多级审批人后,期望:
- 中间审批级等待处理:仍显示「审批中」
- **末级审批人通过、进入发放节点后**:显示「发放中」
- 发放登记完成后:显示「已发放」
系统里已有 `pending` 枚举(原前端文案「待发放」),但有工作流时主路径几乎不会写入该状态;发放撤销在有流程时还会回退为 `in_approval`。申请列表筛选项也未暴露该状态。
**文案约定(已确认)**:充值申请 `pending` 的对外名称统一为 **「发放中」**(不再用「待发放」)。状态码仍为 `pending`,不改库字段。尿素 / 运费等其它业务的「待发放」文案不在本期范围。
## 已确认决策
| 项 | 结论 |
| --- | --- |
| 切换时机 | 末级 `approve` 节点通过后,若下一节点为 `disburse`(发放登记),将 `status` 设为 `pending` |
| 展示文案 | `pending` → **发放中**(Web `getPlRechargeApplyStatusLabel`、导出/后端 `RECHARGE_APPLY_STATUS_LABELS`、筛选项、相关提示语) |
| 中间审批 | 仍保持 `in_approval` |
| 发放完成 | 仍为 `disbursed` |
| 撤销发放 | 有工作流时回退为 `pending`(停在发放节点),不再回退为 `in_approval`;提示改为「回退为发放中」 |
| 无工作流路径 | 维持现有 `pending` 语义不变,仅文案改为「发放中」 |
| 历史数据 | 本期**不做**批量刷库;已停在发放节点但仍为 `in_approval` 的单据继续可在审批待办发放,列表可能仍显示「审批中」直至被处理 |
| 范围 | 油卡 + ETC 共用后端一并生效;Web 列表文案/筛选项同步;小程序不单独改(若共用展示映射则自然带上) |
| 不另拆 | 不新建独立「发放中」菜单;发放仍在现有「充值审批」待办中对发放节点操作 |
## 目标状态机(有工作流)
```text
draft / rejected
└─提交─► in_approval # 当前节点为 approve*
│
├─中间 approve 通过──► in_approval(推进 current_node)
│
└─末级 approve 通过且下一节点为 disburse
─► pending # 发放中;current_node = 发放节点
│
└─发放登记─► disbursed
任意审批节点驳回 ─► rejected
撤回(规则不变)─► draft
撤销发放登记 ─► pending(有 wf)或 pending(无 wf,保持现状)
```
\* 流程末节点必须仍是 `disburse`(现有约束:`approve_apply` 不得收尾)。
## 后端改动要点
### 1. 状态常量与文案
- `APPLY_STATUSES` 正式纳入 `pending`(schema / 单测断言同步)
- `RECHARGE_APPLY_STATUS_LABELS["pending"]`:`待发放` → `发放中`
- 关联申请错误提示等用户可见「待发放」字样同步改为「发放中」
### 2. `approve_apply`
通过当前 `approve` 节点后:
- `apply.current_node = nodes[idx + 1].id`(不变)
- 若下一节点 `action_type == "disburse"`:`apply.status = "pending"`
- 否则:保持 `in_approval`
### 3. 待办与发放登记准入
当前 `_current_node` / `list_pending` / 菜单徽章只认 `status == "in_approval"`。改为认:
`status in ("in_approval", "pending")`
且仍要求 `current_node` 存在、当前用户为该节点审批人。这样「发放中」单据继续出现在审批待办,发放人可完成登记。
错误文案可区分「不在可处理状态」,但行为上两者同等可办。
### 4. 撤销 / 换绑发放记录回退
`delete_records` / 换绑旧申请回退时:有 `wf_id` 时改为 `status = "pending"` + `current_node = 发放节点`(与文案「回退为发放中」一致);无流程仍 `pending`。
### 5. 发放登记页关联申请
现有 `_resolve_apply_for_record`:拒绝 `in_approval`、仅允许 `pending`。与新状态机一致——走审批发放应走审批菜单的 `disburse_apply`;记录页手动关联仍只认 `pending` 单。无需为发放节点旁路放开记录页。
### 6. 看板 / 汇总
`pl_vehicle_recharge_summary_service` 已将 `pending` 与 `in_approval` 计入 pending 类统计,保持即可;回归确认油卡看板数字仍合理。
## 前端改动要点
- `getPlRechargeApplyStatusLabel`:`pending` → `发放中`(仅充值申请映射;尿素等其它模块不动)
- 油卡 / ETC 申请列表列头筛选项:增加 `{ value: 'pending', label: '发放中' }`
- 发放页撤销确认等「回退为待发放」→「回退为发放中」
- 发放页提示「审批中请到审批菜单发放」逻辑可保留(`in_approval`);`pending` 仍可按现有关联逻辑处理
- 审批页「待我审批」Tab 不改名(含审批 + 发放节点待办)
## 测试与验收
**后端单测(优先)**
- 两级 approve + 末节点 disburse:一级通过后仍 `in_approval`;二级通过后 `pending` 且 `current_node` 为发放节点
- `list_pending` / 徽章计数包含 `pending` 且当前节点审批人匹配的单据
- `disburse_apply` 在 `pending` 状态下成功 → `disbursed`
- 撤销发放回退为 `pending`(有流程)
- 导出/标签映射:`pending` 文案为「发放中」
**手工验收**
- 申请列表:二级通过后状态显示「发放中」,可按「发放中」筛选
- 发放节点处理人在审批待办仍能看到并完成发放 →「已发放」
- 油卡与 ETC 模块各走一单冒烟
## 明确不做
- 历史 `in_approval` + 已在发放节点的数据迁移脚本
- 拆分发放独立菜单 / 独立待办 API
- 修改流程配置 UI(仍要求末节点为发放登记)
- 小程序专项改造
- 尿素 / 运费 / 备用金等其它业务「待发放」文案改名
## 风险
- 若漏改徽章或 `list_pending` 状态条件,会出现「列表已是发放中但待办/角标消失」;实现时与 `approve_apply` 同批改、同批测。
- 历史停在发放节点的 `in_approval` 单据列表文案仍为「审批中」,验收时需知悉;若后续需要可另开刷数任务。