176 lines
6.9 KiB
Markdown
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]]
|