224 lines
7.0 KiB
Markdown
224 lines
7.0 KiB
Markdown
---
|
|
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]]
|