Files
uniprop-manual/prop-acc/scenarios/deposit/exception-deposit-on-frozen.md
2026-05-25 22:37:41 +08:00

176 lines
6.9 KiB
Markdown

---
title: prop-acc · deposit · 场景 - 冻结账户尝试缴款被拦截
aliases:
- 冻结账户缴款被拒
- exception-deposit-on-frozen
- 场景-冻结账户缴款异常
tags:
- 场景
- prop-acc
- 保证金
- 异常
audience:
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 场景:冻结账户尝试缴款被拦截
业务人员**误对一个 Frozen 账户**点击"追加缴款"按钮,系统按 [[account-state-machine|状态机]] 守护**直接拦截**,不让任何资金进入。
## 典型情境
> [!example] 真实情境
> 物业职员小李在前台办业户陈先生的押金追加业务。陈先生说"我再补 ¥1,000"。小李没注意到陈先生的账户当前处于 **Frozen 状态**(因之前墙面损坏纠纷),习惯性打开账户点了"追加缴款"按钮。
>
> 系统提示:**"账户处于冻结状态,无法缴款"**,操作被拒绝。
## 业务人员视角
### 您看到什么
| 时刻 | 看到 |
|---|---|
| 进入 `ViewDepositAccount` | 状态栏显示 "🧊 Frozen" |
| 状态管理组里的按钮 | "追加缴款 / 退款 / 扣罚" 全部**灰化**,只有 "Unfreeze / ForceClose" 可点 |
| 如果硬调 API | 系统抛出 `RuntimeException`:"账户处于冻结状态,canDeposit returns false" |
> [!info] 三道防御
> 1. **UI 层**:按钮根据 `canDeposit()` 状态自动灰化
> 2. **Policy 层**:`DepositAccountPolicy::update()` + 状态检查
> 3. **业务 Action 层**:`DepositIntoAccountAction` 入口校验 `canDeposit()`,失败抛异常
>
> 即使前两道被绕过(直接调 API、tinker 操作),第三道仍兜底。
### 第 1 步:看到操作被拒,理解原因
提示框信息:**"账户处于冻结状态,canDeposit returns false"**(系统消息 + 业务消息)。
### 第 2 步:与业户沟通
不要硬要绕过。说明:
> "您的账户当前因纠纷冻结,需先调解完成解冻后才能继续缴款。"
业户的几个可能反应:
| 业户反应 | 处理 |
|---|---|
| "那快帮我解冻" | 看是否有调解结果。无书面凭证不解冻 |
| "我先把钱给你,等解冻再录" | **不要这么做**。物业不能"暂存"业户的钱 —— 没有凭证 = 不合规。让业户先离开,等解冻 |
| "那这账户先不动了" | OK,告诉业户冻结期间状态、预计解冻时间 |
### 第 3 步:正确流程
1. 调解 → 拿到书面调解协议
2. [[unfreeze-after-mediation|解冻]] → 账户变 Active
3. [[deposit-additional-topup|追加缴款]] → 正常缴款流程
## 系统视角:守护链路
```mermaid
sequenceDiagram
participant 职员
participant Filament
participant DepositIntoAccountAction
participant DepositAccount
职员->>Filament: 点击"追加缴款" → DepositAction modal
Note over Filament: UI 守护:Frozen 状态按钮已灰化(防误点)
职员->>Filament: 如果绕过 UI,直接提交
Filament->>DepositIntoAccountAction: handle(account, amount)
DepositIntoAccountAction->>DepositAccount: canDeposit()?
DepositAccount-->>DepositIntoAccountAction: false (status=Frozen)
DepositIntoAccountAction-->>Filament: throw RuntimeException
Filament-->>职员: 显示错误:"账户处于冻结状态,无法缴款"
Note over 职员: 不会建任何 CO/Transaction/Receipt
```
## 为什么这条守护比直觉更严
历史上曾经的代码允许 `[Active, Frozen]` 都能缴款,直觉上"反正多存钱不亏",收紧成只允许 Active 是经过教训的。详见 [[account-state-machine]] "关键守护:Frozen 不允许任何资金动作" 段。
真实风险:
| 反例 | 后果 |
|---|---|
| 装修公司持续往受冻结的押金账户灌钱 | 资金被困、责任更复杂(冻结期间是否构成新债权?) |
| 业户与物业纠纷期间业户继续存钱 | 资金混同,扣罚 / 退款时算不清"哪一笔是原押金、哪一笔是纠纷后存的" |
| 系统给"暂存"开口子 | 业务人员可能用它做不规范操作("先放着,以后调整") |
收紧后**所有冻结期间的资金问题都必须先解冻**,语义清晰、审计可追。
## 测试断言
代码层有专门测试覆盖此异常路径:
```php
test('cannot deposit on frozen account', function () {
$account = DepositAccount::factory()->frozen()->create(['balance' => 5000]);
expect(fn () => app(DepositIntoAccountAction::class)->handle($account, 1000))
->toThrow(RuntimeException::class, '账户处于冻结状态');
expect($account->fresh()->balance)->toBe(5000.0); // 余额未变
expect(DepositTransaction::count())->toBe(0); // 流水未建
});
```
任何对 `canDeposit()` 的修改都会触发此测试,确保守护不被无意中放宽。
## 常见问题
> [!question] 业户坚持要存钱,职员怎么办?
> 解释 + 引导:
> - 解释为什么不能存(冻结期间所有资金动作禁止,无论方向)
> - 引导先完成调解 / 解冻
> - 如果业户着急,加快内部调解流程(联系物业管家)
> [!question] 业户已经把钱转过来了(对公转账已到账)但账户冻结,怎么办?
> 这是**业务问题**,不是系统问题:
> - 物业账户已收到这笔钱,但**不能挂在该业户的 DepositAccount 上**(冻结不允许)
> - 选项:
> - 退回业户(原路退,清晰)
> - 系统外暂留(物业银行账户里的"挂账" / 财务备查)
> - 等解冻后再录入
> - **推荐第一个**(退回业户),最干净
> [!question] 同样的守护对 ForceClose 适用吗?
> ForceClose 走**独立的 Policy 方法** `forceClose()`,守护是 `isFrozen() && hasBalance()` —— 这是**专门**为 Frozen 状态设计的,与 deposit / refund / forfeit 守护刚好"互补":
>
> | 守护 | 适用状态 | 阻止 |
> |---|---|---|
> | `canDeposit()` | Active only | Frozen / Closed 缴款 |
> | `canWithdraw()` | Active only | Frozen / Closed 退款 / 扣罚 |
> | `forceClose()` Policy | Frozen + hasBalance only | Active / Closed / Frozen-zero-balance 强制关账 |
>
> 这条对应详见 [[account-state-machine]]。
> [!question] 这个守护影响小程序业户自助操作吗?
> 当前没有小程序自助缴款 —— Action 假设手工操作。**未来加小程序在线缴款时**:
> - 前端调同一个 `DepositIntoAccountAction`,守护自动生效
> - 业户在小程序操作冻结账户时同样被拦截
> - UI 应在小程序侧显示更友好的错误信息(避免裸抛 RuntimeException 给业户)
> [!question] 怎么手工验证某账户是否 Frozen?
> 后台 → 保证金 → 列表 → 状态列;或 tinker `DepositAccount::find($id)->status`。
## 异常分支
- 想加缴款 → 先 [[unfreeze-after-mediation|解冻]] → [[deposit-additional-topup|追加]]
- 想退款 → 同上,或 [[force-close-refund|强制关账退还]]
- 想扣罚 → 同上,或 [[force-close-forfeit|强制关账扣罚]]
- 长期冻结无解 → [[force-close-retain|资金保留关账]]
## 相关文档
- [[account-state-machine]]
- [[freeze-during-dispute]]
- [[deposit-additional-topup]]
- [[force-close-refund]]
- [[force-close-forfeit]]