6.6 KiB
6.6 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 · 账单六状态机 |
|
|
|
已发布 | billing | 2026-05-26 | 2026-05-22 |
账单六状态机
Bill 是 prop-acc 状态机最复杂的子模块,有 6 个状态:Unpaid / Partial / Paid / Suspended / Processing / Void。比 deposit / prepaid / meter 的 3-4 态都细,反映账单从生成到关闭的全生命周期 + 异常处置路径。
6 状态速查
| 状态 | 中文 | 何时进入 | 能做什么 |
|---|---|---|---|
Unpaid |
未付 | 账单刚创建 | 收款 / 拆单 / 挂起 / 删 / 作废 |
Partial |
部分付 | 收到一部分钱(小于账单金额) | 继续收款 / 作废(已收的需退) |
Paid |
已付 | 收齐全部金额 | 作废(走退款) |
Suspended |
挂起 | 业务人员主动暂停(纠纷 / 失联) | 恢复 / 作废 |
Processing |
处理中 | 收款流程中(罕见,如银行扣款排队) | 等回调 |
Void |
作废 | 主动 void | 只读,审计可查 |
状态机图
stateDiagram-v2
[*] --> Unpaid : 创建
Unpaid --> Partial : 部分收款
Unpaid --> Paid : 全额收款
Unpaid --> Suspended : suspend(纠纷)
Unpaid --> Void : void
Unpaid --> [*] : 物理删除(无付款关联)
Partial --> Partial : 继续收款(仍未付清)
Partial --> Paid : 收齐
Partial --> Suspended : suspend
Partial --> Void : void(已收部分需退)
Paid --> Void : void(已收全额需退)
Suspended --> Unpaid : resume(纠纷解决)
Suspended --> Partial : resume(回到部分付状态,若之前有付款)
Suspended --> Void : void
Processing --> Paid : 收款回调成功
Processing --> Unpaid : 收款回调失败
Void --> [*]
守护方法(模型层)
Bill 模型上的辅助方法:
| 方法 | 返回 true 的状态 | 用途 |
|---|---|---|
canBePaid() |
Unpaid / Partial | CollectPaymentAction 准入 |
canBeIssued() |
(用于"发出账单"流程,看实现) | — |
isVoid() |
Void | UI 灰化判断 |
isPaid() |
Paid | 报表 / 收款率统计 |
hasAnyPayment() |
有任何 CollectionOrderBill / prepaid_transaction 关联 | 决定能否物理删 |
canBeDeleted() |
Unpaid + 无任何付款关联 | DeleteAction 守护 |
canBeVoided() |
非 Paid 非 Void | VoidBillAction 守护 |
[!warning]
canBeVoided的微妙之处 Paid 状态也可以走 void(已付全额作废 → 退款),但canBeVoided()返回 false。原因:
- 已付账单的作废需要走退款流程,不是单纯改状态
- 简单的
VoidBillAction只翻状态 + 留 meta,不处理钱- 已付的作废应该走专门的"作废 + 退款"组合流程(类似 ../meter/exception-readings-locked-after-bill)
即:
canBeVoided()检查的是"能否走简单作废",不是"能否所有方式作废"。
状态转换详解
Unpaid → Partial(部分收款)
业户付了 ¥300 给 ¥800 的账单:
- Bill.status: Unpaid → Partial
- 建 CollectionOrderBill(allocated_amount=300)
- 关联 CollectionOrder(actual_amount=+300)
- Bill.paid_amount(若有此字段) = 300
- 余 500 待付
Partial → Paid(收齐)
业户再付 ¥500:
- Bill.status: Partial → Paid
- 建第 2 条 CollectionOrderBill(allocated_amount=500)
- Bill.paid_amount = 800(=账单金额)
- 收齐
Unpaid → Suspended(挂起)
业户与物业纠纷 / 业户失联:
- Bill.status: Unpaid → Suspended
- 业务人员走 SuspendBillAction
- 记 suspend_reason + suspended_at(meta)
- 后续 CollectPaymentAction 守护拒绝(canBePaid=false)
Suspended → Unpaid(恢复)
纠纷解决:
- Bill.status: Suspended → Unpaid(或回到 Partial,若之前有付款)
- 业务人员走 ResumeBillAction
- 记 resume_reason + resumed_at
- 后续 CollectPaymentAction 重新可用
Unpaid → Void(作废)
误开的账单,业户没付:
- Bill.status: Unpaid → Void
- VoidBillAction 守护通过(canBeVoided=true)
- 记 voided_reason + voided_at + voided_by
- 业户无影响(没付钱)
Paid → Void(已付作废)
走专门的"作废 + 退款"流程(未来扩展):
- 当前 VoidBillAction **不允许**(canBeVoided=false for Paid)
- 需要走类似 meter 的修正流程
- 后续若实施 RefundBillAction:作废 + 退款一次性
Unpaid → 物理删除(误建立刻删)
- 状态层面消失(从数据库删除)
- 走 DeleteAction
- 守护:canBeDeleted = Unpaid + 无任何付款关联
- 留 activitylog
业务人员视角
后台账户列表的状态列对应这 6 个值:
- 🟢 Unpaid:绿色,可操作(收款 / 拆 / 挂起 / 删 / 作废)
- 🟡 Partial:黄色,部分付,可继续收款
- ✅ Paid:绿色对号,已完成,无操作
- 🧊 Suspended:灰色雪花,挂起,只能恢复 / 作废
- ⏳ Processing:旋转图标,处理中(等回调)
- ❌ Void:红色叉,已作废,只读
业户视角
业户感受到的:
| 状态 | 业户感知 |
|---|---|
| Unpaid | 收到"账单 ¥800 待付"通知 |
| Partial | 看到"已付 ¥300,还差 ¥500" |
| Paid | 收到"已付清"通知 + 收据 |
| Suspended | (通常通知)"账单挂起,事由 XXX,请联系物业" |
| Processing | 几乎不感知(过渡态) |
| Void | 收到"账单已作废,理由 XXX" |
与其他模块状态机的对比
| 模块 | 状态数 | 主要状态 |
|---|---|---|
| deposit | 3 | Active / Frozen / Closed |
| prepaid | 3 | Active / Frozen / Closed |
meter(is_active) |
2 | active / inactive |
| bill | 6 | Unpaid / Partial / Paid / Suspended / Processing / Void |
bill 的复杂度反映收款全流程:从应收到已收,中间有挂起、部分付、回调中等多种异常路径。
历史:Policy 修复
[!info] issue.md Q6 第一轮发现的漏洞 原
BillPolicy几乎是空壳(只有getPermissionPrefix),所有授权全靠AbstractPolicy默认(只查权限,不查状态)。修复后补 7 个方法:
update/delete/deleteAny/void/collect/suspend/resume。每个方法都加了状态守护(canBeDeleted / canBeVoided 等)。