Files

224 lines
7.0 KiB
Markdown
Raw Permalink Normal View History

2026-05-26 01:08:16 +08:00
---
title: prop-acc · billing · 场景 - 物理删除未付账单(误建立刻删)
aliases:
- 物理删除账单
- 删除未付账单
- delete-bill-unpaid
- 场景-删除未付账单
tags:
- 场景
- prop-acc
- 账单
- 删除
audience:
- 业务人员
status: 已发布
sub_feature: billing
last_review: 2026-05-26
code_version: 2026-05-22
---
# 场景:物理删除未付账单(误建立刻删)
业务人员**误建**了账单(选错业户 / 写错金额 / 误触发批量生成),**立即发现**且**业户还没付**任何钱 → 走**物理删除**。`canBeDeleted()` 守护:Unpaid + 无付款关联。
## 典型情境
> [!example] 真实情境
> 王主管月初手工建账单"5 月物业费 ¥800",**填错业户 ID**(应该是张阿姨,选成了陈先生)。提交后立刻发现错误。
>
> - 账单状态:Unpaid
> - 业户没付过任何钱
> - **可走物理删除**(走 [[delete-vs-void-dual-track|双轨制]] 的 delete 路径)
> - 再重新建一张正确的(选张阿姨)
## 业务人员视角
### 第 1 步:发现错误
| 发现时机 | 处置 |
|---|---|
| **立即**(几秒钟内)| 物理删(本场景)|
| **几小时**(可能业户已收到推送)| 仍可删(若 Unpaid + 无付款)|
| **几天**(可能业户已付)| **不可删**,走 [[void-paid-bill|作废]] |
### 第 2 步:打开账单
后台 → 账单 → 找到误建账单 → 进 `EditBill`(或 `ViewBill`)。
### 第 3 步:点击 `DeleteAction`(标签"删除")
> [!warning] 按钮可见性
> 守护:`canBeDeleted()` = Unpaid + 无付款 + `->authorize('delete')`(`bill.delete` 权限)。
>
> 任何其他状态(Partial / Paid / Suspended / Void)或有任何付款关联 → 按钮**灰化**。
### 第 4 步:Modal 确认
```
确认删除账单 #B-202605-501-001?
⚠️ 此账单状态为 Unpaid 且无付款关联,可物理删除。
删除后无法恢复(但 activitylog 会保留记录)。
[取消] [确认删除]
```
### 第 5 步:提交
系统:
1. 再次校验 `canBeDeleted()`(防 UI 缓存过期)
2. 物理删 Bill(从数据库消失)
3. 写 activitylog(event=deleted,记 bill_no / amount / 操作员 / 时间)
> [!info] activitylog 留什么
> 即使 Bill 物理删了,activitylog 表还有完整记录:
>
> ```sql
> SELECT * FROM activity_log
> WHERE event = 'deleted'
> AND properties->>'$.bill_no' = 'B-202605-501-001';
> ```
>
> 可看到"谁在什么时候删了什么账单"。详见 [[smart-bulk-delete-design]]"activitylog 设计"。
### 第 6 步:重新建正确账单(若需要)
走 [[create-single-bill-manual|手动建单]] 流程,选正确业户。
## 系统流程
```mermaid
sequenceDiagram
participant 业务
participant Filament
participant Action[DeleteAction]
participant Bill
participant Activity
participant DB
业务->>Filament: EditBill → DeleteAction
Filament->>Action: handle(bill)
Action->>Bill: canBeDeleted()? Unpaid + 无付款 = true
Action->>Activity: log(event=deleted, properties={bill_no, amount, ...})
Action->>DB: 物理删 Bill
Action-->>Filament: 跳回 ListBills
```
## 业户视角
业户**无感**:
- 误建的账单(尤其几秒内删的)业户**根本不知道**它存在过
- 即使业户已经收到推送,删除后再次刷新看不到了
- 物业可选择给业户发"前述账单作废,系统误建"(可选,看业务策略)
**最理想场景**:业务人员发现 → 立即删 → 业户从未感知。
## `canBeDeleted` 详解
`Bill::canBeDeleted()`:
```php
public function canBeDeleted(): bool
{
return $this->status === BillStatus::Unpaid
&& ! $this->hasAnyPayment();
}
```
`hasAnyPayment()`:
```php
public function hasAnyPayment(): bool
{
return $this->collectionOrderBills()->exists()
|| $this->prepaidTransactionsRelatedToBill()->exists();
}
```
两条都为真(Unpaid + 无付款关联) → 可删。
> [!info] 为什么 Partial 不能删?
> Partial 意味着业户已经付了一部分 → 有 CollectionOrderBill 关联 → `hasAnyPayment=true` → 拒绝物理删。
>
> 业户付的钱不能"凭空消失"。Partial 状态要么继续收款 → Paid;要么走 [[void-paid-bill|作废 + 退款]] 流程。
## 与作废的对比
| 维度 | **删除(本场景)** | [[void-paid-bill|作废]] |
|---|---|---|
| 适用 | Unpaid + 无付款 | 非 Paid 非 Void(基本是 Partial / Suspended) |
| 数据库 | **从数据库消失** | 留状态 = Void |
| activitylog | 留(event=deleted)| 留(event=voided)|
| 业户感知 | 通常无感(未付过)| 有(看到状态 Void) |
| 可恢复 | ❌ 不可(数据真没了)| ❌ 不可(Void 终态)|
| 适用场景 | 误建立刻删 | 任何已付 / 有付款 / 业户已知的 |
详见 [[delete-vs-void-dual-track]] 双轨制设计哲学。
## 批量删除(误建一批)
如果不是单张误建,而是**误触发批量生成**(例如点了一次"生成 5 月物业费"后忘了又点了一次):
→ 走 [[bulk-delete-batch-mistake|智能批量删除]],走 `BulkDeleteBillsAction` 的预检查 + 三档分类。
## 常见问题
> [!question] 删了后悔了能恢复吗?
> 不能(数据真的没了)。**只能重新建**(走 [[create-single-bill-manual]] 或 [[create-periodic-property-fee]])。
>
> 但**所有信息可从 activitylog 还原**(原 bill_no / amount / 业户 等),重建相对简单。
> [!question] 业务人员误删大量账单怎么办?
> 单删一次一张,影响有限。**批删**才容易误删大量(详见 [[smart-bulk-delete-design]] 的预检查防护)。
>
> 单张误删的预防:
> - Modal 确认(默认有)
> - 思考"是否真要删"再点
> [!question] 删除有审批吗?
> 当前**无审批流**(单签批操作)。但**权限层有控制**:`bill.delete` 是普通业务人员权限;`bill.bulkDelete` 是高敏独立权限(详见 [[smart-bulk-delete-design]])。
> [!question] activitylog 表会爆掉吗?
> 长期看会(每次单删 + 批删 + 作废 + 收款都加几条)。需归档策略(详见 [[smart-bulk-delete-design]]"常见问题"段)。
> [!question] 删除影响 reading.bill_id 吗(若是计量账单)?
> 看 cascade 设计:
> - 若 reading 的 `bill_id` 是 FK + cascade SET NULL → 自动 nullify
> - 若没 cascade → reading.bill_id 仍指向已删 Bill(孤儿 FK,需修复)
>
> 当前实施看代码。**预防**:计量账单不轻易物理删,走作废更稳妥。
> [!question] 已删账单的 activitylog 怎么用?
> ```sql
> -- 某员工某天的全部 delete
> SELECT
> id, causer_id,
> properties->>'$.bill_no' AS bill_no,
> properties->>'$.amount' AS amount,
> created_at
> FROM activity_log
> WHERE event = 'deleted'
> AND causer_id = ?
> AND created_at BETWEEN ? AND ?;
> ```
## 异常分支
- 误删后想重建 → [[create-single-bill-manual]]
- 误删一批 → 走批删的反向(无,只能 1 张张重建)
- 不能删(有付款)→ [[void-paid-bill]]
- 批量误建 → [[bulk-delete-batch-mistake]]
## 相关文档
- [[delete-vs-void-dual-track]]
- [[smart-bulk-delete-design]]
- [[bill-six-state-machine]]
- [[void-paid-bill]]
- [[bulk-delete-batch-mistake]]
- [[audit-activitylog-trace]]
- [[create-single-bill-manual]]