--- 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]]