189 lines
7.6 KiB
Markdown
189 lines
7.6 KiB
Markdown
---
|
|
title: prop-acc · deposit · 场景 - 冻结状态强制全退并关账
|
|
aliases:
|
|
- 强制全退关账
|
|
- ForceClose refund
|
|
- force-close-refund
|
|
- 场景-冻结强制退还关账
|
|
tags:
|
|
- 场景
|
|
- prop-acc
|
|
- 保证金
|
|
- 强制关账
|
|
audience:
|
|
- 业务人员
|
|
status: 已发布
|
|
sub_feature: deposit
|
|
last_review: 2026-05-25
|
|
code_version: 2026-05-22
|
|
---
|
|
|
|
# 场景:冻结状态强制全退并关账
|
|
|
|
账户处于 **Frozen** 状态、**还有余额**,但**纠纷结果明确利向业户**(损坏归责不在业户、调解结果全退、司法判决业户胜诉等)。物业用 `ForceCloseAction` 选 `refund` disposition,**一步完成"解冻 + 全额退还 + 关账"**。
|
|
|
|
## 典型情境
|
|
|
|
> [!example] 真实情境
|
|
> 陈先生家押金账户因墙面损坏归责争议被冻结。1 个月后第三方机构鉴定结论 —— **损坏与陈先生装修无关**(是先前住户造成的)。物业认可结论,要把 ¥5,000 全额退给陈先生并关账户。
|
|
>
|
|
> 不能走普通 [[refund-full-no-damage|RefundAction]],因为账户 Frozen 状态下 `canWithdraw=false` 守护会拦截。需要 ForceClose 走 `refund` disposition。
|
|
|
|
## 业户视角
|
|
|
|
### 您会感受到什么
|
|
|
|
- 收到通知:"您的押金账户已结清,全额 ¥5,000 退还"
|
|
- 收到**红字收据**:"装修保证金退还 ¥-5,000(强制关账退还,事由:鉴定结论无责)"
|
|
- 银行 / 微信收到退款
|
|
- 小程序"我的押金账户"显示 "🔒 已结清"
|
|
|
|
### 您要做什么
|
|
|
|
- 配合提供退款渠道
|
|
- 保管红字收据(税务凭证)
|
|
|
|
## 业务人员视角
|
|
|
|
### 第 1 步:确认全退判定
|
|
|
|
- 第三方鉴定 / 司法判决 / 调解协议(书面)
|
|
- 物业内部决定"按全退处理"
|
|
|
|
> [!warning] 这是不可逆操作
|
|
> ForceClose 提交后账户立即 Closed,**无法撤销**。务必在书面凭证就位后再操作。
|
|
|
|
### 第 2 步:打开 Frozen 账户
|
|
|
|
后台 → 保证金 → 账户列表 → 找 Frozen 账户(状态显示 🧊)→ 进 `ViewDepositAccount`。
|
|
|
|
> 右上角状态管理组只有 `UnfreezeAction` 和 `ForceCloseAction` 可点。
|
|
|
|
### 第 3 步:点击 `ForceCloseAction`(标签"强制关账")
|
|
|
|
Modal 表单(关键是 **disposition** Radio):
|
|
|
|
| 字段 | 填什么 |
|
|
|---|---|
|
|
| **处置方式 (disposition)** | 选 ✅ **`refund`(退还)** |
|
|
| 退款渠道(动态出现)| 选业户指定渠道 |
|
|
| **关账事由(memo)** | 必填,如 "鉴定结论:墙面损坏与业户装修无关,全额退还" |
|
|
|
|
> [!info] 不复用 RefundFromDepositAccountAction
|
|
> ForceClose 的 refund 逻辑**单独写**(`ForceCloseDepositAccountAction` ~230 行),不复用 `RefundFromDepositAccountAction`。理由:
|
|
>
|
|
> - 后者的 `canWithdraw` 守护刚好挡住 Frozen
|
|
> - 如果复用,普通调用方意外绕过冻结检查,语义边界模糊
|
|
> - 重复约 60 行核心代码换语义边界清晰,值得
|
|
>
|
|
> 这条权衡详见 issue.md Q3 "第二轮已落地"段。
|
|
|
|
> [!warning] Policy 守护
|
|
> `DepositAccountPolicy::forceClose()`:
|
|
> - `update` 权限
|
|
> - `isFrozen() && hasBalance()`(只允许 Frozen + 有余额)
|
|
>
|
|
> Active 账户 / Closed 账户 / Frozen 但 balance=0 的账户,按钮都灰化。
|
|
|
|
### 第 4 步:提交
|
|
|
|
系统调 `ForceCloseDepositAccountAction(disposition=refund)`,事务内:
|
|
|
|
1. 校验 `isFrozen() && hasBalance()`
|
|
2. 建 `CollectionOrder`(`actual_amount=-5000` 红字,`status=Completed`)
|
|
3. 加 `DepositTransaction`(`type=refund`,`amount=5000`,`balance_before=5000`,`balance_after=0`,关联红字 CO)
|
|
4. 更新 `balance=0`
|
|
5. 直接 `status=Closed`(从 Frozen)
|
|
6. 在 `meta.force_closed_disposition=refund`、`meta.force_closed_memo=...`、`meta.force_closed_at=...` 记审计字段
|
|
7. 触发 `CollectionOrderCompleted` → Listener 建红字 Receipt
|
|
|
|
### 第 5 步:走线下退款 + 给收据
|
|
|
|
银行 / 微信退 ¥5,000;红字收据交业户。
|
|
|
|
## 系统流程
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant 业户
|
|
participant 财务
|
|
participant Filament
|
|
participant ForceCloseDepositAccountAction
|
|
participant 数据库
|
|
participant 监听器
|
|
|
|
Note over 业户,财务: 账户 Frozen + balance=5000,鉴定结论全退
|
|
|
|
财务->>Filament: ViewDepositAccount → ForceCloseAction (disposition=refund)
|
|
Filament->>ForceCloseDepositAccountAction: handle(account, disposition=refund, channel, memo)
|
|
ForceCloseDepositAccountAction->>ForceCloseDepositAccountAction: isFrozen() && hasBalance()? yes
|
|
ForceCloseDepositAccountAction->>数据库: 开启事务
|
|
ForceCloseDepositAccountAction->>数据库: 1. 建 CO (-5000 红字, Completed)
|
|
ForceCloseDepositAccountAction->>数据库: 2. 建 DepositTransaction (refund, 5000→0)
|
|
ForceCloseDepositAccountAction->>数据库: 3. balance=0, status=Closed(直接 Frozen→Closed)
|
|
ForceCloseDepositAccountAction->>数据库: 4. meta.force_closed_disposition=refund 等审计字段
|
|
ForceCloseDepositAccountAction->>监听器: 5. 触发 CollectionOrderCompleted
|
|
监听器->>数据库: 6. 建 Receipt (强制关账退还 ¥-5,000)
|
|
ForceCloseDepositAccountAction->>数据库: 提交事务
|
|
Filament-->>财务: 成功通知
|
|
财务-->>业户: 银行/微信退 5000 + 红字收据
|
|
```
|
|
|
|
## 状态转换
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Active
|
|
Active --> Frozen : freeze() 纠纷
|
|
Frozen --> Closed : ForceClose(refund)<br/>+建红字流水<br/>+全额退款
|
|
Closed --> [*]
|
|
```
|
|
|
|
ForceClose 是**唯一**能从 Frozen 直接到 Closed(不经过 Active)的路径。
|
|
|
|
## 常见问题
|
|
|
|
> [!question] 为什么不先解冻再退?
|
|
> 可以走"解冻 + 普通 Refund"两步组合,效果一样。但 ForceClose 一步到位有几个好处:
|
|
>
|
|
> - **审计标记**:`meta.force_closed_*` 字段明确记录"这次关账是强制的、什么 disposition、什么事由",未来追溯一眼明白
|
|
> - **避免中间态**:Active 状态下账户能被其他人误操作(再缴款 / 再扣罚),ForceClose 跳过这个窗口
|
|
> - **业务语义清晰**:这次关账不是"正常退完关",是"特殊处置关",标志性强
|
|
|
|
> [!question] disposition 选错(原本应该 retain 选了 refund)能改吗?
|
|
> 不能。`DepositTransaction` 和 `CollectionOrder` 都不可变。如果选错:
|
|
> - 已实际退款给业户 → 找业户协商再缴回,系统记一笔 deposit;然后开新账户继续(原账户已 Closed 永久)
|
|
> - 还没实际线下退款 → 系统记录已成事实但线下不操作,业务上需在审计备注解释"系统记退款实际未执行";然后在新账户上做正确处置
|
|
>
|
|
> **预防胜于补救**:Modal 表单提交前务必再三确认 disposition。
|
|
|
|
> [!question] 退款金额能改成部分吗?
|
|
> **不能**。ForceClose 是"一步终结",退款金额自动 = 当前全部余额。如果需要部分退、部分扣、部分保留,**不应该用 ForceClose**,应该:
|
|
> 1. 先 [[unfreeze-after-mediation|解冻]] 回 Active
|
|
> 2. 按比例操作:部分 [[refund-full-no-damage|退]] + 部分 [[forfeit-damage-public-area|扣]]
|
|
> 3. 余额清零时自动 Closed
|
|
|
|
> [!question] 业户拒绝接受 ForceClose refund 的结果怎么办?
|
|
> 一旦 ForceClose 提交,账户 Closed 不可逆。**业户接受与否**只影响线下退款流程的执行:
|
|
> - 业户接受 → 收到退款
|
|
> - 业户拒收(罕见,觉得"应该是物业全责加罚款") → 走司法
|
|
>
|
|
> 系统层面的账面已平,后续纠纷与账户无关。
|
|
|
|
> [!question] ForceClose 后能反悔(Active 重启)吗?
|
|
> 不能。Closed 永久。详见 [[account-state-machine]]。
|
|
|
|
## 异常分支
|
|
|
|
- 全扣不退 → [[force-close-forfeit]]
|
|
- 资金保留待领 → [[force-close-retain]]
|
|
- 业户没失联且配合 → 优先 [[unfreeze-after-mediation]] + 普通 refund(更标准)
|
|
|
|
## 相关文档
|
|
|
|
- [[account-state-machine]]
|
|
- [[freeze-during-dispute]]
|
|
- [[force-close-forfeit]]
|
|
- [[force-close-retain]]
|
|
- [[red-receipt-design]]
|