Files
uniprop-manual/prop-acc/concepts/billing/bill-six-state-machine.md
2026-05-26 00:43:11 +08:00

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 · 账单六状态机
账单六状态机
Bill 状态机
BillStatus
6 个账单状态
概念
prop-acc
账单
状态机
核心概念
业务人员
财务
架构师
已发布 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

详见 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-tracksmart-bulk-delete-design

相关文档