diff --git a/docs/superpowers/plans/2026-11-12-pl-recharge-pending-disburse-status.md b/docs/superpowers/plans/2026-11-12-pl-recharge-pending-disburse-status.md new file mode 100644 index 000000000..436dcff70 --- /dev/null +++ b/docs/superpowers/plans/2026-11-12-pl-recharge-pending-disburse-status.md @@ -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 | diff --git a/docs/superpowers/specs/2026-11-12-pl-recharge-pending-disburse-status-design.md b/docs/superpowers/specs/2026-11-12-pl-recharge-pending-disburse-status-design.md new file mode 100644 index 000000000..899e1d001 --- /dev/null +++ b/docs/superpowers/specs/2026-11-12-pl-recharge-pending-disburse-status-design.md @@ -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` 单据列表文案仍为「审批中」,验收时需知悉;若后续需要可另开刷数任务。