Files
uniprop-manual/prop-acc/concepts/deposit/account-state-machine.md

134 lines
4.6 KiB
Markdown
Raw Permalink Normal View History

2026-05-25 22:07:35 +08:00
---
title: prop-acc · deposit · 押金账户状态机
aliases:
- 押金账户状态机
- DepositAccount 状态机
- Active / Frozen / Closed
tags:
- 概念
- prop-acc
- 保证金
- 状态机
audience:
- 业户
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 押金账户状态机
押金账户三种状态:**Active(在押)** / **Frozen(冻结)** / **Closed(已结清)**
> [!warning] 重要原则
> 一旦 Closed,永远 Closed。**不允许重开**。新业务一律开新账户。理由见本文末"为什么不允许 reopen"。
## 三状态速查
| 状态 | 中文 | 何时进入 | 能做什么 |
|---|---|---|---|
| `Active` | 在押 | 新账户首次缴款后 | 缴款 / 退款 / 扣罚 / 冻结 / 关账(余额 0) |
| `Frozen` | 冻结 | 发生纠纷、内审等 | 看流水(只读)/ 解冻 / 强制关账 |
| `Closed` | 已结清 | 余额清零正常关账 OR ForceClose | 看流水(只读),没有任何可写操作 |
## 状态机图
```mermaid
stateDiagram-v2
[*] --> Active : 开户 + 首次缴款
Active --> Active : 追加缴款 / 部分退款 / 扣罚
Active --> Frozen : freeze() 纠纷/审计
Frozen --> Active : unfreeze() 调解完成
Active --> Closed : close() 余额=0
Frozen --> Closed : forceClose() 强制结账
Closed --> [*]
note right of Frozen
冻结期间禁止任何资金进出
canDeposit = false
canWithdraw = false
end note
note right of Closed
永久终态
canBeReopened = false
新业务请开新账户
end note
```
## 守护方法(代码层)
`DepositAccount` 模型上有一组 `can*()` 守护方法,**所有写入 Action 必须先调用**:
| 方法 | 返回 true 的状态 | 用途 |
|---|---|---|
| `canDeposit()` | Active **only** | DepositAction 准入 |
| `canWithdraw()` | Active **only** | RefundAction / ForfeitureAction 准入 |
| `canBeFreezed()` | Active | FreezeAction 准入 |
| `canBeUnfreezed()` | Frozen | UnfreezeAction 准入 |
| `canBeClosed()` | balance=0 且 ≠Closed | CloseAction 准入 |
| `canBeReopened()` | **永远 false** | 占位,刻意禁止 |
| `canOperate()` | Active | 复合判断:既不在 Frozen 也不在 Closed |
| `hasBalance()` | balance>0 | 配合判定能否 Close / 是否需 ForceClose |
| `isAvailable()` | Active | UI 显示"可用"或灰化 |
> [!info] 关键守护:Frozen 不允许任何资金动作
> `canDeposit()` 与 `canWithdraw()` **都只允许 Active**,Frozen 一律拒绝。这条规则比直觉更严:
>
> - 原本曾允许 `[Active, Frozen]` 都能缴款(看着"反正多存钱不亏")
> - 但与"冻结 = 暂停所有交易"的语义矛盾
> - 真实风险:纠纷期间装修公司继续往受冻结账户灌钱 → 资金被困、责任更复杂
>
> 现在两个方向严格一致:Frozen = 完全冻结,只能解冻或 ForceClose。
## 业务人员视角
后台账户列表的"状态"列对应这三个值。
- 看到 `Active`:绿色,可点开操作
- 看到 `Frozen`:橙色,所有按钮变灰,只剩 `Unfreeze` / `ForceClose`
- 看到 `Closed`:灰色,完全只读,只能看流水
## 业户视角
业户**通常感受不到状态机**,只感受到结果:
- 余额能正常用 → Active
- 申请退款被拒,前台告知"账户冻结中,等纠纷处理完才能动" → Frozen
- 账户被关 → 收到一张红字收据 + 短信告知"您的押金账户已结清"
## 为什么不允许 Reopen
`canBeReopened()` 永远返回 `false`,是**刻意的设计**。
| 假设允许 reopen | 风险 |
|---|---|
| Closed → Active 又开放 | 流水台账"已结清"的语义被破坏,审计难追责 |
| 业务方:"业户搬回来了就 reopen 老账户" | 鼓励混账;两次业务关系应有清晰边界 |
| 系统级:"误操作 close 了能反悔" | close 已经守护"余额 0",误操作不会丢钱;新业务开新账户即可 |
替代做法:业户搬回来续约,**开新账户**。旧账户保留作为历史台账,与"曾经的业务关系结束"语义一致。
## 异常路径:Frozen + 有余额 + 想关账?
矛盾:
- `Frozen` 不允许 `withdraw`(包括退款 / 扣罚)
- `canBeClosed()` 要求余额=0
- 又不能 unfreeze 直接关
这种困境通过 **ForceClose** 解决,见 [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] 三种 disposition。
ForceClose 是**唯一**能合法从 Frozen 直接到 Closed 的路径,通过专门的 `DepositAccountPolicy::forceClose()` 守护(`update` 权限 + `isFrozen() && hasBalance()`)。
## 相关文档
- [[deposit-account-vs-transaction]]
- [[transaction-types]]
- [[freeze-during-dispute]]
- [[unfreeze-after-mediation]]
- [[close-after-zero-balance]]
- [[force-close-refund]]