Files
uniprop-manual/prop-acc/scenarios/billing/delete-bill-unpaid.md
2026-05-26 01:08:16 +08:00

7.0 KiB

title, aliases, tags, audience, status, sub_feature, last_review, code_version
title aliases tags audience status sub_feature last_review code_version
prop-acc · billing · 场景 - 物理删除未付账单(误建立刻删)
物理删除账单
删除未付账单
delete-bill-unpaid
场景-删除未付账单
场景
prop-acc
账单
删除
业务人员
已发布 billing 2026-05-26 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 表还有完整记录:

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 流程,选正确业户。

系统流程

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():

public function canBeDeleted(): bool
{
    return $this->status === BillStatus::Unpaid
        && ! $this->hasAnyPayment();
}

hasAnyPayment():

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-manualcreate-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 怎么用?

-- 某员工某天的全部 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 ?;

异常分支

相关文档