vault backup: 2026-05-26 01:08:16

This commit is contained in:
Willie
2026-05-26 01:08:16 +08:00
parent d3a786ba76
commit 1bc1884255
5 changed files with 941 additions and 4 deletions

View File

@@ -0,0 +1,253 @@
---
title: prop-acc · billing · 场景 - 恢复挂起的账单
aliases:
- 恢复账单
- ResumeBillAction
- 解除挂起
- resume-bill
- 场景-恢复挂起账单
tags:
- 场景
- prop-acc
- 账单
- 调整
audience:
- 业务人员
- 财务
status: 已发布
sub_feature: billing
last_review: 2026-05-26
code_version: 2026-05-22
---
# 场景:恢复挂起的账单
[[suspend-bill|挂起]] 状态的账单,在**纠纷解决 / 业户回来**后恢复到 Unpaid / Partial,后续可正常收款。`ResumeBillAction` 对称于 SuspendBillAction。
## 典型情境
> [!example] 真实情境(一):业户回来了
> 王先生(15-7-203)出国 3 个月后回来,到物业前台:"我去美国出差 3 个月,没顾上缴物业费,现在补"。
>
> - 王主管查看王先生账户 → 3 张挂起的物业费(Suspended)
> - 走 `ResumeBillAction` 逐张恢复 → 状态 Suspended → Unpaid
> - 然后走 [[collect-payment-batch|批量收款]] 一次性 ¥2,400 付清
> [!example] 真实情境(二):纠纷解决
> 陈先生与物业 5 月物业费纠纷调解结果:物业有部分过错,**协议金额 ¥600**(而不是原 ¥800)。
>
> - 物业要做:
> - 走 ResumeBill(挂起 → Unpaid)
> - 改账单金额(Edit Bill,若 Policy 允许 update 字段)/ 或作废原账单 + 重建 ¥600 账单
> - 业户付 ¥600 → 走 [[collect-payment-single]]
## 业务人员视角
### 第 1 步:确认恢复场景
| 场景 | 后续 |
|---|---|
| 业户失联回来要付 | 恢复 → 收款 |
| 纠纷解决(物业胜)| 恢复 → 收原金额 |
| 纠纷解决(妥协)| 恢复 → 改金额(或作废+重建) |
| 误挂起 | 恢复(reason = "误操作解除") |
### 第 2 步:打开账单
后台 → 账单 → 过滤"状态=Suspended" → 找到目标账单 → 进 `ViewBill`
状态显示 "🧊 Suspended",右上角只有 `ResumeBillAction``VoidBillAction` 可点。
### 第 3 步:点击 `ResumeBillAction`(标签"恢复")
> [!warning] 按钮可见性
> 守护:`bill.status === Suspended` + Policy `->authorize('resume')`。
Modal 表单:
| 字段 | 填什么 |
|---|---|
| **恢复原因(reason)** | 必填,如 "业户出差回来,主动付清"|
### 第 4 步:提交
`ResumeBillAction` 业务层逻辑:
```php
class ResumeBillAction
{
public function handle(Bill $bill, string $reason, User $user): void
{
if ($bill->status !== BillStatus::Suspended) {
throw new RuntimeException("账单非 Suspended 状态,不可恢复");
}
// 智能恢复:有部分付 → Partial;无付款 → Unpaid
$newStatus = $bill->paid_amount > 0
? BillStatus::Partial
: BillStatus::Unpaid;
$bill->update([
'status' => $newStatus,
'meta' => array_merge($bill->meta ?? [], [
'resume_reason' => $reason,
'resumed_at' => now(),
'resumed_by' => $user->id,
// 可选:把这次"挂起-恢复"完整记录追加到 suspend_history 数组
]),
]);
activity()
->performedOn($bill)
->causedBy($user)
->withProperties([
'reason' => $reason,
'from_status' => BillStatus::Suspended->value,
'to_status' => $newStatus->value,
'bill_no' => $bill->bill_no,
])
->event('resumed')
->log('账单已恢复');
}
}
```
### 第 5 步:通知业户(可选)
恢复后立即提示业户付款:
> 王先生,您的 3 张挂起账单已恢复(合计 ¥2,400),现在可以付清。
### 第 6 步:走收款
恢复后走 [[collect-payment-single]] 或 [[collect-payment-batch]]。
## 系统流程
```mermaid
sequenceDiagram
participant 业户
participant 业务
participant Filament
participant Action[ResumeBillAction]
participant DB
业户->>业务: 我要付挂起的账单
业务->>Filament: ViewBill(Suspended)→ ResumeBillAction(modal, reason)
Filament->>Action: handle(bill, reason, user)
Action->>Action: 校验 status === Suspended
Action->>Action: 判定恢复后状态(Unpaid 或 Partial)
Action->>DB: 1. bill.status = Unpaid / Partial
Action->>DB: 2. bill.meta.resume_reason / resumed_at / resumed_by
Action->>Activity: 3. log(event=resumed)
Filament-->>业务: 成功
业务->>Filament: 继续走收款流程
```
## 智能恢复:Partial vs Unpaid
`ResumeBillAction` 判断恢复到哪个状态:
| 挂起前的状态 | 恢复后的状态 |
|---|---|
| Unpaid(无付款)→ Suspended | **Unpaid** |
| Partial(部分付)→ Suspended | **Partial** |
代码层用 `paid_amount > 0` 判断。这样恢复后业户的"已付部分"还在账户上。
## 多次"挂起-恢复"的历史记录
如果同一账单被多次挂起 / 恢复:
```json
// Bill.meta 推荐结构(看实现是否如此)
{
"suspend_reason": "最近一次挂起原因",
"suspended_at": "最近一次挂起时间",
"resume_reason": "最近一次恢复原因",
"resumed_at": "最近一次恢复时间",
"suspend_history": [
{
"suspended_at": "...",
"suspended_by": "...",
"suspend_reason": "...",
"resumed_at": "...",
"resumed_by": "...",
"resume_reason": "..."
},
{ ...... },
...
]
}
```
第一次"挂-恢复"完整存进 `suspend_history` 数组,新一次的"挂"覆盖 `suspend_reason`/`suspended_at`。完整审计可追溯。
> [!info] 实施细节
> 当前 `SuspendBillAction` / `ResumeBillAction` 是否实现 `suspend_history` 数组看代码。简版实现可能只覆盖最近一次(无 history)。
## 业户视角
### 您会感受到什么
- 收到通知"您的账单已恢复,请尽快付款"
- 小程序"我的账单"看到状态:Suspended → Unpaid
- 后续付款流程同正常
### 业户配合
业户应:
- 立即付款(避免再次进入逾期)
- 若有付款困难,提前告诉物业(可能再次挂起 + 协商)
## 与其他模块的对比
| 模块 | 类似 Suspend / Resume |
|---|---|
| **billing(本)** | SuspendBillAction / ResumeBillAction |
| deposit | freeze / unfreeze(账户级,详见 [[../deposit/unfreeze-after-mediation]]) |
| prepaid | FreezeAccountAction / ReactivateAccountAction([[../prepaid/unfreeze-after-verification]]) |
| meter | 无(meter 用 decommission,不可恢复)|
**billing / deposit / prepaid 都有"挂起 / 恢复"的对偶设计** —— 这是金融类业务的通用模式。
## 常见问题
> [!question] 恢复后业户又找不到了怎么办?
> 走 [[exception-overdue-bills|逾期催收]] → 多次催不到 → 再 [[suspend-bill|挂起]] 一次(reason 改"再次失联")。多次"挂-恢复"循环说明业户有问题,需法律 / 走绕监管路径。
> [!question] 恢复时账单期次已经过去很久(例如 5 月账单 11 月才恢复),还按原 due_at 算逾期吗?
> 看业务策略:
>
> - 严格:仍按原 due_at(5 月底 + 宽限期)→ 一恢复就是逾期(可能加滞纳金)
> - 宽松:挂起期间不算逾期 → 恢复时给新的宽限期(例如恢复后 + 7 天)
>
> 当前实施看 `OverdueBillsListWidget` 的判断逻辑。
> [!question] 误挂起立即恢复,activitylog 显示两条(suspended + resumed)对吗?
> 对。每次状态变化各一条 log。可在 reason 备注"误操作 + 立即恢复"。
> [!question] 恢复后能直接改金额吗?
> 看 Policy / EditBill。Unpaid 状态可能允许 Edit(改 amount)。Partial 状态(已付款部分)改金额复杂(原 paid_amount 怎么算)→ 不推荐改,改用"作废 + 重建"。
> [!question] 恢复操作需要审批吗?
> 当前**无审批流**(单签批操作)。业务上若需要审批(例如金额大),靠人员制度保障(高权限人员才操作)。
## 异常分支
- 误恢复(应作废)→ [[void-paid-bill]] 走作废路径
- 恢复后改金额 → 复杂,走作废 + 重建([[create-single-bill-manual]])
- 长期挂起最终决定作废 → [[void-paid-bill]]
## 相关文档
- [[suspend-bill]]
- [[bill-six-state-machine]]
- [[void-paid-bill]]
- [[collect-payment-single]]
- [[exception-overdue-bills]]
- [[audit-activitylog-trace]]
- [[../deposit/unfreeze-after-mediation]](deposit 同类对比)
- [[../prepaid/unfreeze-after-verification]](prepaid 同类对比)