Files
uniprop-manual/prop-acc/concepts/billing/bill-six-state-machine.md

223 lines
6.6 KiB
Markdown
Raw Permalink Normal View History

2026-05-26 00:43:11 +08:00
---
title: prop-acc · billing · 账单六状态机
aliases:
- 账单六状态机
- Bill 状态机
- BillStatus
- 6 个账单状态
tags:
- 概念
- prop-acc
- 账单
- 状态机
- 核心概念
audience:
- 业务人员
- 财务
- 架构师
status: 已发布
sub_feature: billing
last_review: 2026-05-26
code_version: 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 | 只读,审计可查 |
## 状态机图
```mermaid
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
```
详见 [[delete-vs-void-dual-track]]。
## 业务人员视角
后台账户列表的状态列对应这 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 等)。
>
> 详见 [[delete-vs-void-dual-track]] 和 [[smart-bulk-delete-design]]。
## 相关文档
- [[bill-types-and-sources]]
- [[bill-vs-collection-order]]
- [[delete-vs-void-dual-track]]
- [[smart-bulk-delete-design]]
- [[collect-payment-single]]
- [[suspend-bill]]
- [[void-paid-bill]]
- [[../meter/decommission-and-locking]](类似的"状态机+守护"对比)