268 lines
8.3 KiB
Markdown
268 lines
8.3 KiB
Markdown
|
|
---
|
|||
|
|
title: prop-acc · billing · 场景 - 批量误建,智能 Modal 三档分类清理
|
|||
|
|
aliases:
|
|||
|
|
- 批量删除账单
|
|||
|
|
- BulkDeleteBillsAction 实战
|
|||
|
|
- 智能批删
|
|||
|
|
- bulk-delete-batch-mistake
|
|||
|
|
- 场景-批量删除账单
|
|||
|
|
tags:
|
|||
|
|
- 场景
|
|||
|
|
- prop-acc
|
|||
|
|
- 账单
|
|||
|
|
- 删除
|
|||
|
|
- 批量
|
|||
|
|
audience:
|
|||
|
|
- 业务人员
|
|||
|
|
- 财务
|
|||
|
|
status: 已发布
|
|||
|
|
sub_feature: billing
|
|||
|
|
last_review: 2026-05-26
|
|||
|
|
code_version: 2026-05-22
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 场景:批量误建,智能 Modal 三档分类清理
|
|||
|
|
|
|||
|
|
业务人员**误触发批量生成**(例如点了一次"生成 5 月物业费"后忘了又点了一次,导致每户有两张同样账单),需要清理。走 `BulkDeleteBillsAction` 的智能 Modal,自动按状态分三档处理。
|
|||
|
|
|
|||
|
|
## 典型情境
|
|||
|
|
|
|||
|
|
> [!example] 真实情境
|
|||
|
|
> 王主管 5 月 1 日上午点了"生成 5 月物业费"批量(已生成 300 张)。下午接电话被打断,**忘了之前已经生成**,又点了一次 → 系统按 `SkipExisting` 策略**全部跳过**(因为已存在)。
|
|||
|
|
>
|
|||
|
|
> **但**:王主管 Modal 没看清,选了 `Replace` 策略 → 系统 **作废原 300 张 + 重新生成 300 张** → 同业户有重复账单(状态 Void 的旧账单 + Unpaid 的新账单)。
|
|||
|
|
>
|
|||
|
|
> 业务人员发现后需清理 300 张 Void 状态的旧账单。
|
|||
|
|
|
|||
|
|
或者:
|
|||
|
|
|
|||
|
|
> [!example] 真实情境(更典型)
|
|||
|
|
> 业务人员手工建了 100 张维修分摊账单,分摊金额算错,需要全部清理重建。
|
|||
|
|
|
|||
|
|
## 业务人员视角
|
|||
|
|
|
|||
|
|
### 第 1 步:筛选要清理的账单
|
|||
|
|
|
|||
|
|
后台 → 账单 → 列表 → 用过滤(按期次 / 状态 / 费用类型 / 生成时间)选出要清理的批。
|
|||
|
|
|
|||
|
|
例:5 月物业费 + Void 状态 + 创建时间 today → 300 张 Void 旧账单。
|
|||
|
|
|
|||
|
|
### 第 2 步:选中
|
|||
|
|
|
|||
|
|
Table 上**勾选**这 300 张(全选 / 多选)。
|
|||
|
|
|
|||
|
|
### 第 3 步:触发批量删除
|
|||
|
|
|
|||
|
|
顶部 **"批量删除"** 按钮(`BulkDeleteBillsAction`)。
|
|||
|
|
|
|||
|
|
> [!warning] 权限守护
|
|||
|
|
> 需要 `bill.bulkDelete` **独立高敏权限**(`Policy::deleteAny`)。普通业务人员可能没此权限,需主管 / 财务总监操作。
|
|||
|
|
>
|
|||
|
|
> 详见 [[smart-bulk-delete-design]]"权限设计"段。
|
|||
|
|
|
|||
|
|
### 第 4 步:智能 Modal(关键)
|
|||
|
|
|
|||
|
|
Modal 自动**预检查**所选账单 + 分三档:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
批量删除账单 (选中 300 张)
|
|||
|
|
|
|||
|
|
预检查统计:
|
|||
|
|
✅ 可删: 200 张 (Unpaid + 无付款关联)
|
|||
|
|
⚠️ 需作废: 50 张 (Void 已经 / Partial 已经 = 状态)
|
|||
|
|
❌ 跳过: 50 张 (Paid 已付 / 其他异常)
|
|||
|
|
|
|||
|
|
请选择处理模式:
|
|||
|
|
⚪ 仅删除可删的(200 张物理删,100 张跳过)
|
|||
|
|
⚫ 删可删 + 作废需作废的(200 删 + 50 作废,50 跳过)
|
|||
|
|
|
|||
|
|
批量操作原因(必填,审计留痕):
|
|||
|
|
[多行输入框:
|
|||
|
|
5 月 1 日批量生成时误选 Replace 策略,
|
|||
|
|
导致 300 张旧账单 Void。现批量清理。
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
⚠️ 注意:
|
|||
|
|
- 物理删除不可恢复(但 activitylog 保留 bill_no)
|
|||
|
|
- 作废不可撤销
|
|||
|
|
- 跳过的账单不动
|
|||
|
|
|
|||
|
|
[取消] [确认执行]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第 5 步:选模式 + 提交
|
|||
|
|
|
|||
|
|
业务人员看到统计,做决定:
|
|||
|
|
|
|||
|
|
| 选 | 业务效果 |
|
|||
|
|
|---|---|
|
|||
|
|
| 仅删可删的 | 200 物理删,100 暂留(包括 50 已作废的 + 50 跳过的)|
|
|||
|
|
| 删可删 + 作废需作废 | 200 物理删,50 翻 Void(变作废),50 跳过 |
|
|||
|
|
|
|||
|
|
填原因 → 提交。
|
|||
|
|
|
|||
|
|
### 第 6 步:看执行结果
|
|||
|
|
|
|||
|
|
系统返回:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
批量操作完成:
|
|||
|
|
- 物理删除:200 张
|
|||
|
|
- 作废:50 张(若选 DeleteAndVoid)
|
|||
|
|
- 跳过:50 张
|
|||
|
|
|
|||
|
|
详细 bill_no 见 activitylog(本次操作)。
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 系统流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant 业务
|
|||
|
|
participant Filament
|
|||
|
|
participant Modal
|
|||
|
|
participant Action[BulkDeleteBillsAction 业务层]
|
|||
|
|
participant Activity
|
|||
|
|
participant DB
|
|||
|
|
|
|||
|
|
业务->>Filament: BillsList → 选 300 张 → 批量删除
|
|||
|
|
Filament->>Modal: 预检查(分类三档)
|
|||
|
|
Modal->>Filament: 显示 ✅200 ⚠️50 ❌50
|
|||
|
|
|
|||
|
|
业务->>Modal: 选模式 + 填原因 + 提交
|
|||
|
|
Modal->>Action: handle(bills, mode, reason, user)
|
|||
|
|
|
|||
|
|
Action->>Action: 再次分类(防 UI 缓存过期)
|
|||
|
|
|
|||
|
|
Action->>DB: 开启事务
|
|||
|
|
|
|||
|
|
loop 200 可删
|
|||
|
|
Action->>DB: bill.delete()
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
alt mode = DeleteAndVoid
|
|||
|
|
loop 50 可作废
|
|||
|
|
Action->>Action: VoidBillAction.handle(bill, reason, user)
|
|||
|
|
Action->>Activity: log voided(单条)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
Action->>Activity: log bulk_deleted(总体 + affected_bill_nos 数组)
|
|||
|
|
Action->>DB: 提交
|
|||
|
|
|
|||
|
|
Action-->>Filament: 结果 + 通知
|
|||
|
|
Filament-->>业务: 显示"200 删 / 50 作废 / 50 跳过"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## activitylog 设计
|
|||
|
|
|
|||
|
|
详见 [[smart-bulk-delete-design]]"activitylog 设计"段。
|
|||
|
|
|
|||
|
|
**单条 bulk_deleted log**(批量操作只一条):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"log_name": "default",
|
|||
|
|
"subject_type": null,
|
|||
|
|
"subject_id": null,
|
|||
|
|
"event": "bulk_deleted",
|
|||
|
|
"causer_id": 42, // 王主管 ID
|
|||
|
|
"properties": {
|
|||
|
|
"mode": "DeleteAndVoid",
|
|||
|
|
"reason": "5 月 1 日批量生成时误选 Replace,300 张 Void 清理",
|
|||
|
|
"total_selected": 300,
|
|||
|
|
"deleted_count": 200,
|
|||
|
|
"voided_count": 50,
|
|||
|
|
"blocked_count": 50,
|
|||
|
|
"affected_bill_nos": [
|
|||
|
|
"B-202605-501-001 [DELETED]",
|
|||
|
|
"B-202605-502-001 [DELETED]",
|
|||
|
|
...
|
|||
|
|
"B-202605-501-002 [VOIDED]",
|
|||
|
|
...
|
|||
|
|
"B-202605-501-003 [SKIPPED]",
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`affected_bill_nos` 是数组,每条标记处理结果。事后可完整还原。
|
|||
|
|
|
|||
|
|
**同时,每张被作废的 Bill 还各有一条 voided log**(VoidBillAction 内部触发,subject=bill)。
|
|||
|
|
|
|||
|
|
## 业户视角
|
|||
|
|
|
|||
|
|
业户**通常不感知**:
|
|||
|
|
|
|||
|
|
- 被物理删的 200 张:业户未付款,通常未通知 → 无感
|
|||
|
|
- 被作废的 50 张:业户**可能**收到通知(看物业策略),或无感
|
|||
|
|
- 跳过的 50 张:不动,不影响
|
|||
|
|
|
|||
|
|
**理想**:批量误建及时清理 → 业户全程无感 → 物业体面化解错误。
|
|||
|
|
|
|||
|
|
**不理想**:清理前业户已经收到推送 + 已付款 → 跳过(Paid 不可删 / 不可作废)→ 需要走单独的"已付作废 + 退款"流程([[void-paid-bill]])。
|
|||
|
|
|
|||
|
|
## 与单删 / 单作废的对比
|
|||
|
|
|
|||
|
|
| 维度 | [[delete-bill-unpaid|单删]] | [[void-paid-bill|单作废]] | **智能批删(本)** |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 一次操作多少张 | 1 | 1 | **多张(N)** |
|
|||
|
|
| 权限 | `bill.delete` | `bill.void` | **`bill.bulkDelete`(独立高敏)** |
|
|||
|
|
| 处理混合状态 | 无(只删 1 张)| 无 | **✅ 三档分类自动** |
|
|||
|
|
| activitylog | 1 条单条 | 1 条单条 | **1 条总体 + N 条单条** |
|
|||
|
|
| 业务人员效率 | 低(逐张)| 低 | **高(一次)** |
|
|||
|
|
| 业务人员风险 | 低 | 低 | **中(影响面大)** |
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
> [!question] 批删的"跳过"档具体包括哪些?
|
|||
|
|
> `canBeDeleted=false && canBeVoided=false`:
|
|||
|
|
>
|
|||
|
|
> - **Paid**(已付清,无法走任何"消除"路径,需走专门退款流程)
|
|||
|
|
> - **Void**(已经作废,无需重复)
|
|||
|
|
> - **Processing**(罕见,处理中,等回调)
|
|||
|
|
>
|
|||
|
|
> 跳过的账单不动,业务人员需单独处理。
|
|||
|
|
|
|||
|
|
> [!question] 实际执行时与 Modal 预检查不一致(其他人在中间改了)?
|
|||
|
|
> Action 内部**再次分类**(防 UI 缓存过期),实际按最新状态分。可能与 Modal 显示统计**略有差异**(但极少)。
|
|||
|
|
|
|||
|
|
> [!question] 批删后想撤销?
|
|||
|
|
> - **物理删的**:不可恢复(数据没了)
|
|||
|
|
> - **作废的**:不可撤销(Void 终态)
|
|||
|
|
> - 唯一办法:重新创建(走 [[create-periodic-property-fee]] 或 [[create-single-bill-manual]])
|
|||
|
|
>
|
|||
|
|
> 信息可从 activitylog 还原(bill_no / amount 等)。
|
|||
|
|
|
|||
|
|
> [!question] 批删的影响面太大会有审批吗?
|
|||
|
|
> 系统层面**无审批流**。靠**权限控制**(只主管 / 财务总监能批删)+ **必填原因**(留审计)+ **Modal 预检查统计**(让操作者看到影响面后决策)。
|
|||
|
|
>
|
|||
|
|
> 业务上若需"超过 100 张需双签",需扩展加审批 workflow。
|
|||
|
|
|
|||
|
|
> [!question] 批删失败怎么办?
|
|||
|
|
> 事务内执行,某张失败 → 整批回滚。事务边界是**全成功或全失败**。
|
|||
|
|
>
|
|||
|
|
> 例外:`Action` 实现可能优化为"个别失败不影响其他",但破坏原子性 → 通常不推荐。
|
|||
|
|
|
|||
|
|
> [!question] activitylog 表太多 bulk_deleted 怎么管?
|
|||
|
|
> 每次批删一条 log(总体 + affected_bill_nos)。即使 100 次批删 = 100 条 log。**不会爆表**。
|
|||
|
|
>
|
|||
|
|
> 但 voided 的子 log(每张作废一条) = 大量。批删 50 张作废 = 50 条 voided log。归档策略详见 [[smart-bulk-delete-design]]。
|
|||
|
|
|
|||
|
|
## 异常分支
|
|||
|
|
|
|||
|
|
- 单张删 → [[delete-bill-unpaid]]
|
|||
|
|
- 单张作废 → [[void-paid-bill]]
|
|||
|
|
- 跳过的 Paid 账单要消除 → [[void-paid-bill|Paid 作废 + 退款]](当前手工)
|
|||
|
|
- 批删后想审计 → [[audit-activitylog-trace]]
|
|||
|
|
|
|||
|
|
## 相关文档
|
|||
|
|
|
|||
|
|
- [[smart-bulk-delete-design]](核心概念)
|
|||
|
|
- [[delete-vs-void-dual-track]]
|
|||
|
|
- [[delete-bill-unpaid]]
|
|||
|
|
- [[void-paid-bill]]
|
|||
|
|
- [[bill-six-state-machine]]
|
|||
|
|
- [[audit-activitylog-trace]]
|