6.9 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 · deposit · 场景 - 冻结账户尝试缴款被拦截 |
|
|
|
已发布 | deposit | 2026-05-25 | 2026-05-22 |
场景:冻结账户尝试缴款被拦截
业务人员误对一个 Frozen 账户点击"追加缴款"按钮,系统按 account-state-machine 守护直接拦截,不让任何资金进入。
典型情境
[!example] 真实情境 物业职员小李在前台办业户陈先生的押金追加业务。陈先生说"我再补 ¥1,000"。小李没注意到陈先生的账户当前处于 Frozen 状态(因之前墙面损坏纠纷),习惯性打开账户点了"追加缴款"按钮。
系统提示:"账户处于冻结状态,无法缴款",操作被拒绝。
业务人员视角
您看到什么
| 时刻 | 看到 |
|---|---|
进入 ViewDepositAccount |
状态栏显示 "🧊 Frozen" |
| 状态管理组里的按钮 | "追加缴款 / 退款 / 扣罚" 全部灰化,只有 "Unfreeze / ForceClose" 可点 |
| 如果硬调 API | 系统抛出 RuntimeException:"账户处于冻结状态,canDeposit returns false" |
[!info] 三道防御
- UI 层:按钮根据
canDeposit()状态自动灰化- Policy 层:
DepositAccountPolicy::update()+ 状态检查- 业务 Action 层:
DepositIntoAccountAction入口校验canDeposit(),失败抛异常即使前两道被绕过(直接调 API、tinker 操作),第三道仍兜底。
第 1 步:看到操作被拒,理解原因
提示框信息:"账户处于冻结状态,canDeposit returns false"(系统消息 + 业务消息)。
第 2 步:与业户沟通
不要硬要绕过。说明:
"您的账户当前因纠纷冻结,需先调解完成解冻后才能继续缴款。"
业户的几个可能反应:
| 业户反应 | 处理 |
|---|---|
| "那快帮我解冻" | 看是否有调解结果。无书面凭证不解冻 |
| "我先把钱给你,等解冻再录" | 不要这么做。物业不能"暂存"业户的钱 —— 没有凭证 = 不合规。让业户先离开,等解冻 |
| "那这账户先不动了" | OK,告诉业户冻结期间状态、预计解冻时间 |
第 3 步:正确流程
- 调解 → 拿到书面调解协议
- unfreeze-after-mediation → 账户变 Active
- deposit-additional-topup → 正常缴款流程
系统视角:守护链路
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 不允许任何资金动作" 段。
真实风险:
| 反例 | 后果 |
|---|---|
| 装修公司持续往受冻结的押金账户灌钱 | 资金被困、责任更复杂(冻结期间是否构成新债权?) |
| 业户与物业纠纷期间业户继续存钱 | 资金混同,扣罚 / 退款时算不清"哪一笔是原押金、哪一笔是纠纷后存的" |
| 系统给"暂存"开口子 | 业务人员可能用它做不规范操作("先放着,以后调整") |
收紧后所有冻结期间的资金问题都必须先解冻,语义清晰、审计可追。
测试断言
代码层有专门测试覆盖此异常路径:
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()PolicyFrozen + 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