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

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 · 场景 - 冻结账户尝试缴款被拦截
冻结账户缴款被拒
exception-deposit-on-frozen
场景-冻结账户缴款异常
场景
prop-acc
保证金
异常
业务人员
已发布 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] 三道防御

  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 → 正常缴款流程

系统视角:守护链路

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() Policy Frozen + hasBalance only Active / Closed / Frozen-zero-balance 强制关账

这条对应详见 account-state-machine

[!question] 这个守护影响小程序业户自助操作吗? 当前没有小程序自助缴款 —— Action 假设手工操作。未来加小程序在线缴款时:

  • 前端调同一个 DepositIntoAccountAction,守护自动生效
  • 业户在小程序操作冻结账户时同样被拦截
  • UI 应在小程序侧显示更友好的错误信息(避免裸抛 RuntimeException 给业户)

[!question] 怎么手工验证某账户是否 Frozen? 后台 → 保证金 → 列表 → 状态列;或 tinker DepositAccount::find($id)->status

异常分支

相关文档