Compare commits
9 Commits
81c09219ea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d07ffa758 | ||
|
|
42c135a5cf | ||
|
|
7cdf0ec9a4 | ||
|
|
669d3b4400 | ||
|
|
1bc1884255 | ||
|
|
d3a786ba76 | ||
|
|
82de3396bb | ||
|
|
085de29cc8 | ||
|
|
5934191115 |
66
.obsidian/workspace.json
vendored
66
.obsidian/workspace.json
vendored
@@ -13,12 +13,12 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "prop-acc/scenarios/prepaid/deposit-first-time.md",
|
||||
"file": "prop-acc/maps/billing-knowledge-map.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "deposit-first-time"
|
||||
"title": "billing-knowledge-map"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -94,7 +94,7 @@
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "prop-acc/scenarios/prepaid/deposit-first-time.md",
|
||||
"file": "prop-acc/maps/billing-knowledge-map.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
@@ -104,7 +104,7 @@
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for deposit-first-time"
|
||||
"title": "Backlinks for billing-knowledge-map"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -113,12 +113,12 @@
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "prop-acc/scenarios/prepaid/deposit-first-time.md",
|
||||
"file": "prop-acc/maps/billing-knowledge-map.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from deposit-first-time"
|
||||
"title": "Outgoing links from billing-knowledge-map"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -156,13 +156,13 @@
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "prop-acc/scenarios/prepaid/deposit-first-time.md",
|
||||
"file": "prop-acc/maps/billing-knowledge-map.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of deposit-first-time"
|
||||
"title": "Outline of billing-knowledge-map"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -195,43 +195,43 @@
|
||||
"obsidian-git:Open Git source control": false
|
||||
}
|
||||
},
|
||||
"active": "849c5ff8936a2b67",
|
||||
"active": "b06ed69835363258",
|
||||
"lastOpenFiles": [
|
||||
"prop-acc/index.md",
|
||||
"prop-acc/scenarios/prepaid/deposit-first-time.md",
|
||||
"prop-acc/scenarios/billing/audit-activitylog-trace.md",
|
||||
"prop-acc/scenarios/billing/audit-monthly-billing-vs-collection.md",
|
||||
"prop-acc/scenarios/billing/exception-overdue-bills.md",
|
||||
"prop-acc/scenarios/billing/exception-partial-payment.md",
|
||||
"prop-acc/scenarios/billing/bulk-delete-batch-mistake.md",
|
||||
"prop-acc/scenarios/billing/void-paid-bill.md",
|
||||
"prop-acc/scenarios/billing/delete-bill-unpaid.md",
|
||||
"prop-acc/scenarios/billing/resume-bill.md",
|
||||
"prop-acc/scenarios/billing/suspend-bill.md",
|
||||
"prop-acc/scenarios/billing/split-bill.md",
|
||||
"prop-acc/scenarios/billing/collect-via-prepaid-auto.md",
|
||||
"prop-acc/scenarios/billing/collect-payment-batch.md",
|
||||
"prop-acc/scenarios/billing/collect-payment-single.md",
|
||||
"prop-acc/scenarios/billing/create-single-bill-manual.md",
|
||||
"prop-acc/scenarios/billing/create-meter-bill-auto.md",
|
||||
"prop-acc/scenarios/billing/create-periodic-property-fee.md",
|
||||
"prop-acc/scenarios/billing",
|
||||
"prop-acc/maps/billing-knowledge-map.md",
|
||||
"prop-acc/concepts/billing/smart-bulk-delete-design.md",
|
||||
"prop-acc/concepts/billing/delete-vs-void-dual-track.md",
|
||||
"prop-acc/concepts/billing/periodic-bill-generation.md",
|
||||
"prop-acc/concepts/billing/bill-vs-collection-order.md",
|
||||
"prop-acc/concepts/billing/bill-types-and-sources.md",
|
||||
"prop-acc/concepts/billing/bill-six-state-machine.md",
|
||||
"prop-acc/concepts/billing",
|
||||
"prop-acc/scenarios/meter/audit-meters-needing-reading.md",
|
||||
"prop-acc/scenarios/meter/exception-readings-locked-after-bill.md",
|
||||
"prop-acc/scenarios/meter/exception-high-consumption.md",
|
||||
"prop-acc/scenarios/meter/generate-bill-min-max-cap.md",
|
||||
"prop-acc/scenarios/meter/generate-bill-with-multiplier.md",
|
||||
"prop-acc/scenarios/meter/generate-bill-tiered-pricing.md",
|
||||
"prop-acc/scenarios/meter/read-with-photo-proof.md",
|
||||
"prop-acc/scenarios/meter/read-via-iot-remote-source.md",
|
||||
"prop-acc/scenarios/meter/read-batch-via-excel-import.md",
|
||||
"prop-acc/scenarios/meter/read-single-meter-manual.md",
|
||||
"prop-acc/scenarios/meter/decommission-without-replacement.md",
|
||||
"prop-acc/scenarios/meter/replace-broken-meter.md",
|
||||
"prop-acc/scenarios/meter/register-single-meter.md",
|
||||
"prop-acc/scenarios/meter/init-new-community-batch.md",
|
||||
"prop-acc/scenarios/meter",
|
||||
"prop-acc/maps/meter-knowledge-map.md",
|
||||
"prop-acc/concepts/meter/decommission-and-locking.md",
|
||||
"prop-acc/concepts/meter/reading-source-and-photo-proof.md",
|
||||
"prop-acc/concepts/meter/bill-generation-pipeline.md",
|
||||
"prop-acc/concepts/meter/multiplier-and-tiered-pricing.md",
|
||||
"prop-acc/concepts/meter/replacement-chain.md",
|
||||
"prop-acc/concepts/meter/meter-vs-meter-reading.md",
|
||||
"prop-acc/concepts/meter",
|
||||
"prop-acc/scenarios/prepaid/audit-low-balance-and-overdue.md",
|
||||
"prop-acc/scenarios/prepaid/exception-refund-on-frozen.md",
|
||||
"prop-acc/scenarios/prepaid",
|
||||
"prop-acc/concepts/prepaid",
|
||||
"prop-acc/scenarios/deposit",
|
||||
"prop-acc/concepts/deposit",
|
||||
"prop-acc/scenarios/adhoc",
|
||||
"prop-acc/concepts/adhoc",
|
||||
"resident-portal/scenarios"
|
||||
"prop-acc/concepts/adhoc"
|
||||
]
|
||||
}
|
||||
320
prop-acc/concepts/billing/delete-vs-void-dual-track.md
Normal file
320
prop-acc/concepts/billing/delete-vs-void-dual-track.md
Normal file
@@ -0,0 +1,320 @@
|
||||
---
|
||||
title: prop-acc · billing · 删除 vs 作废双轨制
|
||||
aliases:
|
||||
- 删除 vs 作废
|
||||
- 双轨制
|
||||
- delete-vs-void-dual-track
|
||||
- VoidBillAction
|
||||
- canBeDeleted
|
||||
- canBeVoided
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 数据治理
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 删除 vs 作废双轨制
|
||||
|
||||
billing 模块对"账单消失"提供**两条路径**:**物理删除**(Delete,Unpaid + 无付款关联)和 **作废**(Void,留状态 + 留审计)。这是 prop-acc 其他子模块**都没有**的设计:deposit/prepaid/meter 都只支持 Close / Decommission 类的"终态保留"。
|
||||
|
||||
## 为什么要双轨制
|
||||
|
||||
业务方提了"想要批量删除功能 + 单张删除功能"。深入讨论后形成双轨:
|
||||
|
||||
| 场景 | 推荐路径 | 理由 |
|
||||
|---|---|---|
|
||||
| **误开账单立刻发现**(还没付款)| **物理删除** | 干净,没钱被卷入,删了不留痕(activitylog 仍有) |
|
||||
| **已生成 + 已收一部分钱**(Partial) | **作废** | 钱已经卷入,删了等于消灭凭证;作废留状态 + 退款 |
|
||||
| **已生成 + 已付清**(Paid)| **作废(需走退款)** | 同上 |
|
||||
| **业户失联 / 长期不付** | **挂起(Suspend)** | 暂停状态,不是终态;后续可恢复或作废 |
|
||||
|
||||
## 删除路径
|
||||
|
||||
### 守护
|
||||
|
||||
`Bill::canBeDeleted()`:
|
||||
|
||||
```php
|
||||
public function canBeDeleted(): bool
|
||||
{
|
||||
return $this->status === BillStatus::Unpaid
|
||||
&& ! $this->hasAnyPayment();
|
||||
}
|
||||
```
|
||||
|
||||
`Bill::hasAnyPayment()` 检查:
|
||||
|
||||
- `CollectionOrderBill`(走收款的关联)
|
||||
- `PrepaidTransaction`(走预存款抵的关联)
|
||||
|
||||
**Unpaid + 无任何付款关联** = 业户没付过任何钱 + 账单状态干净 → 安全删。
|
||||
|
||||
### 触发入口
|
||||
|
||||
| 入口 | UI |
|
||||
|---|---|
|
||||
| 单删 | `EditBill` 页面的 `DeleteAction`(`->visible(canBeDeleted)` + `->authorize('delete')`)|
|
||||
| 批删 | `BulkDeleteBillsAction`(智能 Modal,详见 [[smart-bulk-delete-design]])|
|
||||
|
||||
### Policy 守护
|
||||
|
||||
```php
|
||||
// BillPolicy
|
||||
public function delete(AuthUser $user, Model $record): bool
|
||||
{
|
||||
return $user->can('delete bills') && $record->canBeDeleted();
|
||||
}
|
||||
|
||||
public function deleteAny(AuthUser $user): bool
|
||||
{
|
||||
return $user->can('bulkDelete bills'); // 独立高敏权限
|
||||
}
|
||||
```
|
||||
|
||||
`deleteAny` 是**独立权限**(`bill.bulkDelete`),比单删更敏感(批删一次可能 100+ 张),需单独授权。
|
||||
|
||||
### 删除后留什么
|
||||
|
||||
| 留下 | 不留 |
|
||||
|---|---|
|
||||
| **activitylog** 记录(谁删了 / 什么时候 / 哪些 bill_no)| **Bill 数据库记录** |
|
||||
| 业户 / asset 等关联实体不动 | **关联的 CollectionOrderBill**(应该 0 个,因守护保证)|
|
||||
|
||||
activitylog 详见 [[smart-bulk-delete-design]]"activitylog 审计"段。
|
||||
|
||||
## 作废路径
|
||||
|
||||
### 守护
|
||||
|
||||
`Bill::canBeVoided()`:
|
||||
|
||||
```php
|
||||
public function canBeVoided(): bool
|
||||
{
|
||||
return $this->status !== BillStatus::Paid
|
||||
&& $this->status !== BillStatus::Void;
|
||||
}
|
||||
```
|
||||
|
||||
**非 Paid 非 Void** = 可作废。包括 Unpaid / Partial / Suspended / Processing。
|
||||
|
||||
> [!warning] Paid 的作废需走专门流程
|
||||
> `canBeVoided()` 对 Paid 返 false,因为单纯翻状态会让业户已付的钱"凭空消失"。Paid 账单作废需要**配套退款流程**(未来扩展,目前手工 / tinker 操作)。
|
||||
>
|
||||
> issue.md Q6 未具体规划"Paid 作废"业务流程,留作未来扩展。
|
||||
|
||||
### 触发入口
|
||||
|
||||
`VoidBillAction`(Filament UI)挂在 `EditBill` / `ViewBill` / Table 行。Modal 必填**作废原因**(`reason`):
|
||||
|
||||
```
|
||||
请填写作废原因(必填,审计留痕):
|
||||
[多行输入框]
|
||||
|
||||
[取消] [确认作废]
|
||||
```
|
||||
|
||||
### 业务 Action 实现
|
||||
|
||||
`src/Actions/Bills/VoidBillAction.php`(业务层,与 Filament UI 分离):
|
||||
|
||||
```php
|
||||
class VoidBillAction
|
||||
{
|
||||
public function handle(Bill $bill, string $reason, User $user): void
|
||||
{
|
||||
if (! $bill->canBeVoided()) {
|
||||
throw new RuntimeException("账单状态 {$bill->status->value} 不可作废");
|
||||
}
|
||||
|
||||
$bill->update([
|
||||
'status' => BillStatus::Void,
|
||||
'meta' => array_merge($bill->meta ?? [], [
|
||||
'voided_reason' => $reason,
|
||||
'voided_at' => now(),
|
||||
'voided_by' => $user->id,
|
||||
]),
|
||||
]);
|
||||
|
||||
activity()
|
||||
->performedOn($bill)
|
||||
->causedBy($user)
|
||||
->withProperties([
|
||||
'reason' => $reason,
|
||||
'from_status' => $bill->getOriginal('status'),
|
||||
'to_status' => BillStatus::Void->value,
|
||||
'bill_no' => $bill->bill_no,
|
||||
'amount' => $bill->amount,
|
||||
])
|
||||
->event('voided')
|
||||
->log('账单已作废');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Policy 守护
|
||||
|
||||
```php
|
||||
public function void(AuthUser $user, Bill $bill): bool
|
||||
{
|
||||
return $user->can('void bills') && $bill->canBeVoided();
|
||||
}
|
||||
```
|
||||
|
||||
### 作废后留什么
|
||||
|
||||
| 留下 | 改变 |
|
||||
|---|---|
|
||||
| **Bill 数据库记录**(只读)| `status = Void` |
|
||||
| **关联的 CollectionOrderBill**(若有)| 不动 |
|
||||
| **meta**:voided_reason / voided_at / voided_by | 新增 |
|
||||
| **activitylog**:event=voided | 新增 |
|
||||
| **Receipt 等下游凭证** | 不动(已经发出去的不撤销)|
|
||||
|
||||
业户可以看到账单"已作废",物业有完整审计链。
|
||||
|
||||
## 双轨决策树
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[要让账单"消失"] --> B{账单状态?}
|
||||
|
||||
B -->|Unpaid| C{有付款关联吗?}
|
||||
B -->|Partial / Suspended / Processing| D[走作废 Void]
|
||||
B -->|Paid| E[当前 canBeVoided=false<br/>需走专门退款流程<br/>未来扩展]
|
||||
B -->|Void| F[已经是 Void,无需重复]
|
||||
|
||||
C -->|无| G[物理删除 Delete]
|
||||
C -->|有| D
|
||||
```
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 决策时
|
||||
|
||||
| 误开发现时间 | 推荐 |
|
||||
|---|---|
|
||||
| 几秒内(业户还在面前)| 物理删除(干净)|
|
||||
| 几小时(可能业户已付)| 先查是否已付:已付 → 作废 + 退;未付 → 删 |
|
||||
| 几天(已发账单)| 通常作废(留状态让业户知道) |
|
||||
| 几月(已查到)| 作废 + 走退款流程(若已付) |
|
||||
|
||||
### Modal 操作
|
||||
|
||||
**单删 Modal**(`EditBill` → DeleteAction):
|
||||
|
||||
```
|
||||
确认删除账单 #B-202605-501-001?
|
||||
|
||||
⚠️ 此账单状态为 Unpaid 且无付款关联,可物理删除。
|
||||
删除后无法恢复(但 activitylog 会保留记录)。
|
||||
|
||||
[取消] [确认删除]
|
||||
```
|
||||
|
||||
**作废 Modal**(`VoidBillAction`):
|
||||
|
||||
```
|
||||
作废账单 #B-202605-501-001
|
||||
|
||||
账单状态:Unpaid
|
||||
账单金额:¥800
|
||||
|
||||
请填写作废原因(必填):
|
||||
[多行输入]
|
||||
|
||||
[取消] [确认作废]
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
| 操作 | 业户感知 |
|
||||
|---|---|
|
||||
| 物理删除 | **无感**(账单从未真正"存在过",未通知业户) |
|
||||
| 作废 | 收到通知"您的账单 #XXX 已作废,理由 YYY" + 账单状态显示 Void |
|
||||
| Paid 作废(未来) | 收到通知 + 退款到账 + 红字凭证 |
|
||||
|
||||
## 与 prop-acc 其他模块的对比
|
||||
|
||||
| 模块 | 删 / 作废 / 退役 / 关账 |
|
||||
|---|---|
|
||||
| **deposit** | 无 delete UI(Policy 严格)+ ForceClose(终态)|
|
||||
| **prepaid** | 无 delete UI + CloseAction(终态)|
|
||||
| **meter** | 无 delete UI(仅退役 + 严格条件下删空表)+ Decommission |
|
||||
| **bill(本模块)** | **delete + void 双轨** + Suspend / Resume |
|
||||
|
||||
bill 的双轨是 prop-acc **最完整的"账单消失"设计**。其他模块"没有删,只有 终态"是因为它们的对象天然不该消失(账户、押金、表都是长期实体)。bill 的"账单"本质上是**可消失的事务记录**,所以双轨合理。
|
||||
|
||||
## 历史:issue.md Q6 的修复
|
||||
|
||||
> [!info] 修复前
|
||||
> `BillPolicy` 几乎是空壳(只有 `getPermissionPrefix`),`DeleteAction` 暴露在 EditBill 页 + `DeleteBulkAction` 暴露在 Table toolbar(`visible(false)` 假关闭),Unpaid + Paid + Void 全状态都能删 → 删 Paid Bill = 业户付的钱凭空消失 + 完全无审计痕迹。
|
||||
>
|
||||
> **风险等级**:严重(类似 deposit / prepaid 第二轮修复的"消灭法律证据"级别)。
|
||||
|
||||
> [!info] 修复后(issue.md Q6 第一轮)
|
||||
>
|
||||
> 1. Bill 模型加 3 辅助方法(`hasAnyPayment` / `canBeDeleted` / `canBeVoided`)
|
||||
> 2. BillPolicy 补 7 方法(`update / delete / deleteAny / void / collect / suspend / resume`)
|
||||
> 3. 新增 `VoidBillAction` 业务 Action + Filament UI
|
||||
> 4. 新增 `BulkDeleteBillsAction` 智能批删(详见 [[smart-bulk-delete-design]])
|
||||
> 5. 删除 `DeleteBulkAction` 的 `visible(false)` 反模式 → 替换为真正的智能批删
|
||||
> 6. EditAction / DeleteAction 三处加 visible 守护
|
||||
> 7. CollectPaymentAction authorize 改为 `->authorize('collect')`(取代跨模型 authorize)
|
||||
> 8. Plugin.php 扩权限位:`bill.bulkDelete` 独立高敏权限
|
||||
>
|
||||
> 全栈守护(UI / Policy / Action / Model)+ activitylog 审计。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 为什么不全部走作废,简化设计?
|
||||
> **物理删的"干净"是有价值的**:
|
||||
>
|
||||
> - 误开的账单不留痕,业务人员不慌
|
||||
> - 业户看不到"已作废"的诡异状态(从未存在过最自然)
|
||||
> - 数据库无 Void 状态垃圾堆积
|
||||
>
|
||||
> 但**只对没动钱的账单适用**。一旦动了钱,必须作废留痕,不能"假装从未发生"。
|
||||
|
||||
> [!question] 作废后能撤销吗?
|
||||
> 不能(Void 是终态,无 reactivate 路径)。如需"恢复":新建一张同信息的 Bill。
|
||||
|
||||
> [!question] hasAnyPayment 检查的"付款关联"具体是?
|
||||
> 看 `Bill::hasAnyPayment()` 实现。通常:
|
||||
>
|
||||
> - `collectionOrderBills()->exists()`(有任何分配记录)
|
||||
> - `prepaidTransactions()->where('related_bill_id', this->id)->exists()`(被 prepaid consume 过)
|
||||
>
|
||||
> 任意一个为真 → 有付款关联 → 不可物理删。
|
||||
|
||||
> [!question] 批量删的智能 Modal 怎么工作?
|
||||
> 详见专门概念 [[smart-bulk-delete-design]]。
|
||||
|
||||
> [!question] activitylog 怎么查?
|
||||
>
|
||||
> ```sql
|
||||
> -- 某员工某月的全部批量删除
|
||||
> SELECT * FROM activity_log
|
||||
> WHERE causer_id = ?
|
||||
> AND event IN ('voided', 'bulk_deleted')
|
||||
> AND created_at BETWEEN '2026-05-01' AND '2026-05-31';
|
||||
> ```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[smart-bulk-delete-design]]
|
||||
- [[delete-bill-unpaid]]
|
||||
- [[void-paid-bill]]
|
||||
- [[bulk-delete-batch-mistake]]
|
||||
- [[suspend-bill]]
|
||||
- [[../meter/decommission-and-locking]](类似"保留 vs 删除"对比)
|
||||
254
prop-acc/concepts/billing/periodic-bill-generation.md
Normal file
254
prop-acc/concepts/billing/periodic-bill-generation.md
Normal file
@@ -0,0 +1,254 @@
|
||||
---
|
||||
title: prop-acc · billing · 周期账单生成机制
|
||||
aliases:
|
||||
- 周期账单生成
|
||||
- 月度物业费生成
|
||||
- GeneratePeriodicBillsAction
|
||||
- BillingMergeStrategy
|
||||
- 合单策略
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 业务流程
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 周期账单生成机制
|
||||
|
||||
物业费、停车费、电视费等**按周期(月 / 季 / 年)固定收**的账单,通过 **`GeneratePeriodicBillsAction`** 批量生成。配合 **`BillingMergeStrategy` 枚举**决定"同业户多个费用类型是否合并到一张账单"。
|
||||
|
||||
## 业务场景
|
||||
|
||||
每月 1 日,物业为本社区 **300 户业主**生成本月物业费账单(每户 ¥800 起步,按房屋面积浮动)。系统应:
|
||||
|
||||
- 自动算每户的金额(按 RatePlan + 房屋面积)
|
||||
- 批量建 Bill(300 张)
|
||||
- 各张 Bill 期次清晰(billing_period 5/1 - 5/31)
|
||||
- 不重复生成(本月已生成的不再生成)
|
||||
- 业务人员收到结果报告
|
||||
|
||||
## 批量生成流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[业务人员触发 GeneratePeriodicBillsAction] --> B[Modal 输入参数]
|
||||
B --> C[参数:期次 / 费用类型 / 社区范围 / 合并策略]
|
||||
C --> D[扫描候选业户清单]
|
||||
|
||||
D --> E[每户:计算应付金额]
|
||||
E --> F{已有本期 Bill?}
|
||||
|
||||
F -->|否| G[建新 Bill]
|
||||
F -->|是,根据策略| H{合并策略}
|
||||
|
||||
H -->|MERGE 合单| I[追加到既有 Bill]
|
||||
H -->|SKIP 跳过| J[不动]
|
||||
H -->|REPLACE 替换| K[作废旧 Bill + 建新]
|
||||
|
||||
G --> L[报告完成]
|
||||
I --> L
|
||||
J --> L
|
||||
K --> L
|
||||
```
|
||||
|
||||
## `GeneratePeriodicBillsAction` 参数
|
||||
|
||||
业务人员触发(`Bills` List 页 → 顶部 "批量生成周期账单" 按钮):
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| **期次(billing_period)** | 如 "2026 年 5 月",决定 start / end |
|
||||
| **费用类型(fee_type_id)** | 物业费 / 停车费 / 有线电视费 / ... |
|
||||
| **社区范围(community_id)** | 单社区 / 全平台 |
|
||||
| **业户范围** | 单个业户 / 全社区业户 / 自定义清单 |
|
||||
| **合并策略(merge_strategy)** | MERGE / SKIP / REPLACE |
|
||||
| 备注 | 选填 |
|
||||
|
||||
提交后系统扫描候选业户清单,逐户计算 + 建账。
|
||||
|
||||
## `BillingMergeStrategy` 枚举
|
||||
|
||||
> [!info] 实际枚举值看 `packages/prop-acc/src/Enums/BillingMergeStrategy.php`
|
||||
> 可能值(推测):
|
||||
>
|
||||
> | 值 | 含义 |
|
||||
> |---|---|
|
||||
> | `SkipExisting` | 同业户同期次已有 Bill → 跳过(不重复生成)|
|
||||
> | `Merge` | 追加到既有 Bill(同业户同期次的不同费用类型合一张)|
|
||||
> | `Replace` | 作废旧 Bill + 建新(罕见,数据修复用)|
|
||||
>
|
||||
> 默认策略通常是 **`SkipExisting`**(最安全)。
|
||||
|
||||
## 三种策略对照
|
||||
|
||||
### 策略 1:SkipExisting(默认,推荐)
|
||||
|
||||
业务人员 5 月 1 日点击"生成 5 月物业费"。系统:
|
||||
|
||||
- 张阿姨已经有 5 月物业费 Bill → **跳过**(避免重复)
|
||||
- 陈先生没有 5 月物业费 Bill → 新建
|
||||
|
||||
**适用**:正常月度操作,业务人员不确定是否之前已经生成过,跳过最安全。
|
||||
|
||||
### 策略 2:Merge(合单)
|
||||
|
||||
业务人员同时为业户生成"5 月物业费 + 5 月电视费"。系统:
|
||||
|
||||
- 找到张阿姨已有 5 月物业费 Bill(¥800)
|
||||
- **追加电视费 ¥40 到同一张 Bill**(amount: 800 + 40 = 840)
|
||||
- 业户收到**一张账单 ¥840**(明细两项)
|
||||
|
||||
**适用**:多种费用类型一起发(减少业户收到的账单数,体验好)。
|
||||
|
||||
> [!warning] Merge 的局限
|
||||
> 合并后**单一 Bill 的金额 = 各费用类型总和**,但**关联的 sourceable** 怎么处理?Bill 表 sourceable 字段是 1:1。**当前实现可能**:
|
||||
> - 不挂 sourceable(`sourceable_type=null`)
|
||||
> - 或挂主费用类型的 source
|
||||
>
|
||||
> 具体看代码。Merge 策略实施细节复杂,生产环境**谨慎用**。
|
||||
|
||||
### 策略 3:Replace(替换)
|
||||
|
||||
业务人员发现 5 月物业费金额算错了(RatePlan 改过),要全部重算:
|
||||
|
||||
- 找到张阿姨已有的 5 月物业费 Bill
|
||||
- **作废**(VoidBillAction,状态翻 Void)
|
||||
- **重新生成**新 Bill(按新 RatePlan)
|
||||
|
||||
**适用**:数据修复场景(罕见,需高权限 + 必填原因)。
|
||||
|
||||
> [!warning] Replace 的风险
|
||||
> 已付的 Bill 作废后,业户已经付的钱**怎么办**?
|
||||
>
|
||||
> - Bill 状态翻 Void
|
||||
> - 已付的 CollectionOrderBill 关联保留(审计需要)
|
||||
> - 但业户的钱 = 物业账面多收了
|
||||
> - **必须走退款**(建红字 CollectionOrder)
|
||||
>
|
||||
> Replace 策略**只对 Unpaid Bill 使用相对安全**;Paid Bill 慎用,需配套退款流程。
|
||||
|
||||
## 金额计算
|
||||
|
||||
周期账单金额怎么算?取决于 RatePlan 配置:
|
||||
|
||||
| 算法 | 说明 |
|
||||
|---|---|
|
||||
| **固定金额** | 每户每月固定 ¥800(简单)|
|
||||
| **按面积** | 房屋面积 × 单价(例如物业费 ¥3 / m² / 月)|
|
||||
| **按车位** | 每个停车位固定金额(停车费)|
|
||||
| **按设备** | 每台空调 ¥X / 月(中央空调费)|
|
||||
| **阶梯** | 类似计量账单的阶梯(罕见)|
|
||||
|
||||
具体看 `RatePlan` 的配置 + 业务计算逻辑。可能在 `PeriodicBillGenerationService`(若有)实现,或在 Action 内联。
|
||||
|
||||
## 生成的 Bill 字段
|
||||
|
||||
每张生成的 Bill:
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `bill_no` | 自动编号(`B-202605-501-001` 或类似)|
|
||||
| `community_id` | 所属社区 |
|
||||
| `asset_id` | 业户房屋 |
|
||||
| `resident_id` | 业户 |
|
||||
| `fee_type_id` | 费用类型 |
|
||||
| `bill_type` | `Periodic`(枚举) |
|
||||
| `amount` | 算出的金额 |
|
||||
| `paid_amount` | 0(刚生成,未付)|
|
||||
| `status` | `Unpaid` |
|
||||
| `billing_period_start` | 期次开始(2026-05-01)|
|
||||
| `billing_period_end` | 期次结束(2026-05-31)|
|
||||
| `due_at` | 到期日(通常本期末 + 宽限期,如 2026-06-15)|
|
||||
| `sourceable_*` | null(周期账单通常无 source)|
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户每月初(或月底,看物业策略)收到推送:
|
||||
|
||||
> 张阿姨您好,您的 2026 年 5 月物业费 ¥800 已生成。请于 6 月 15 日前付清。
|
||||
|
||||
业户可选:
|
||||
|
||||
- 现金 / 微信付 → 走 [[collect-payment-single]]
|
||||
- 预存款抵 → 走 [[collect-via-prepaid-auto]](自动)
|
||||
- 与其他账单一起付 → 走 [[collect-payment-batch]]
|
||||
- 暂时不付 → 到期日后变逾期,走 [[exception-overdue-bills]] 催收
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 月初执行
|
||||
|
||||
每月 1-3 日,业务人员王主管:
|
||||
|
||||
1. 打开 BillsListPage → 顶部 "生成周期账单"
|
||||
2. 选费用类型(物业费 / 停车费 / 等)+ 期次 + 策略 + 备注
|
||||
3. 提交 → 系统扫描 + 生成 → 报告"已生成 N 张账单,跳过 M 张"
|
||||
4. 抽样核对几张 Bill 的金额 + 业户
|
||||
|
||||
### 异常处置
|
||||
|
||||
| 异常 | 处置 |
|
||||
|---|---|
|
||||
| 某户 RatePlan 配置缺失 | 跳过该户 + 在报告里标记 → 单独建 RatePlan + 单独生成 |
|
||||
| 某户已有同期 Bill(被跳过)| 看是否需要 Merge / Replace |
|
||||
| 全部失败(系统错)| 联系运维查日志 |
|
||||
|
||||
## 自动化的可能
|
||||
|
||||
issue.md Q6 未列入"待补",但业务上可能需要:
|
||||
|
||||
- **Scheduled job 月初自动跑** — 不需要业务人员手动触发
|
||||
- **跟 prepaid 自动抵扣 job 串联** — 账单生成 → 立即触发 [[../prepaid/auto-deduction-design|预存款自动抵]] → 业户感受"无感扣账单"
|
||||
|
||||
当前**仍是手动触发**。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 同业户同期次同费用类型能有两张 Bill 吗?
|
||||
> 业务上**不应该**(违反业务规则:一户一月一物业费)。系统层面看是否有 unique 约束:
|
||||
>
|
||||
> ```sql
|
||||
> UNIQUE INDEX (community_id, resident_id, fee_type_id, billing_period_start)
|
||||
> ```
|
||||
>
|
||||
> 若有 → 数据库层挡住重复;若无 → 全靠 Action 的 SkipExisting 策略判断,理论上可能因并发产生重复。
|
||||
|
||||
> [!question] 业户搬走中途,本月物业费要不要全收?
|
||||
> 看物业策略 + 合同约定:
|
||||
>
|
||||
> - **全月收**(简单,业户合同到月底)
|
||||
> - **按天分摊**(按搬走前的天数算)
|
||||
> - **不收**(罕见)
|
||||
>
|
||||
> 系统**当前简版**通常是全月收。按天分摊需要业务层算法支持。
|
||||
|
||||
> [!question] 周期账单生成后才发现 RatePlan 配错怎么办?
|
||||
> 走 Replace 策略**只对 Unpaid 安全**。如果已付:
|
||||
>
|
||||
> - 看错的金额方向:
|
||||
> - 收多了 → 退差额(走 [[void-paid-bill]] 类似流程)
|
||||
> - 收少了 → 补开账单(新建一张 ¥差额 的临时账单)
|
||||
|
||||
> [!question] 批量生成多久?300 户 5 秒?
|
||||
> 单户 ~50-100ms(查 RatePlan + 查业户 + 建 Bill + 日志)。300 户**顺序执行** ~15-30 秒。可优化为并发(若业务量大)。
|
||||
|
||||
> [!question] 周期账单和计量账单可以一起生成吗?
|
||||
> 不在同一个 Action(周期是 `GeneratePeriodicBillsAction`,计量是 [[../meter/bill-generation-pipeline|GenerateBillsFromMeterReadingsAction]])。**业务流程上**业务人员通常先做完抄表 + 计量账单生成 → 再触发周期账单生成。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-types-and-sources]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[create-periodic-property-fee]]
|
||||
- [[create-meter-bill-auto]]
|
||||
- [[../meter/bill-generation-pipeline]](类似的 Calculator + Service + Action 模式)
|
||||
- [[../prepaid/auto-deduction-design]](账单生成后的下游消费)
|
||||
359
prop-acc/concepts/billing/smart-bulk-delete-design.md
Normal file
359
prop-acc/concepts/billing/smart-bulk-delete-design.md
Normal file
@@ -0,0 +1,359 @@
|
||||
---
|
||||
title: prop-acc · billing · 智能批量删除设计
|
||||
aliases:
|
||||
- 智能批量删除
|
||||
- BulkDeleteBillsAction
|
||||
- 三档分类
|
||||
- activitylog 审计
|
||||
- smart-bulk-delete-design
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 数据治理
|
||||
- 审计
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 智能批量删除设计
|
||||
|
||||
`BulkDeleteBillsAction` 是 prop-acc **最精巧的批量操作设计**:选中 N 张 Bill 后,系统**预检查 + 分类三档**(可删 / 可作废 / 跳过),Modal 显示统计 + 选模式 + 必填原因,最后**一条 activitylog** 记录全过程,`affected_ids` 串联具体哪些账单。
|
||||
|
||||
## 为什么要"智能"
|
||||
|
||||
业务人员选了 100 张账单点"批量删除",其中可能:
|
||||
|
||||
- 92 张是 Unpaid + 无付款 → 可物理删
|
||||
- 5 张是 Partial / Paid → 不能删(动了钱),但可作废
|
||||
- 3 张是 Suspended / 已 Void → 跳过
|
||||
|
||||
**简单批量删**(一刀切删全部)的灾难:
|
||||
|
||||
- 5 张 Partial / Paid 的被删 → 业户付的钱凭空消失 + 审计断链
|
||||
- 业务人员可能根本不知道里面有 Partial / Paid(没逐张点开看)
|
||||
|
||||
**智能批删**(本设计):
|
||||
|
||||
1. **预检查** 每张 Bill 分类
|
||||
2. **Modal 显示**:`✅ 可删 92 ⚠️ 需作废 5 ❌ 跳过 3`
|
||||
3. **业务人员选模式**:`仅删可删的` 或 `删可删 + 作废需作废的`
|
||||
4. **必填原因**(审计要求)
|
||||
5. **执行 + 一条 activitylog**:`affected_ids` 串联每张账单的处理结果
|
||||
|
||||
## 三档分类逻辑
|
||||
|
||||
```php
|
||||
// 伪代码,来自 BulkDeleteBillsAction
|
||||
foreach ($selectedBills as $bill) {
|
||||
if ($bill->canBeDeleted()) {
|
||||
$deletable[] = $bill;
|
||||
} elseif ($bill->canBeVoided()) {
|
||||
$voidable[] = $bill;
|
||||
} else {
|
||||
$blocked[] = $bill; // Paid / Void 已经 / 异常状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 档 | 条件 | 处置 |
|
||||
|---|---|---|
|
||||
| **可删**(Deletable) | `canBeDeleted()` = Unpaid + 无付款 | 物理 delete |
|
||||
| **可作废**(Voidable) | `canBeVoided()` = 非 Paid 非 Void | 翻状态 Void + 留 meta |
|
||||
| **跳过**(Blocked) | Paid / Void / 其他异常 | 不动 |
|
||||
|
||||
## Modal 设计
|
||||
|
||||
业务人员在 `BillsTable` 选 N 张 → 点 "批量删除" → 弹出:
|
||||
|
||||
```
|
||||
批量删除账单 (选中 100 张)
|
||||
|
||||
预检查统计:
|
||||
✅ 可删: 92 张
|
||||
⚠️ 需作废: 5 张
|
||||
❌ 跳过: 3 张(已付清 / 已作废 / 其他)
|
||||
|
||||
请选择处理模式:
|
||||
⚪ 仅删除可删的(92 张物理删,5 张需作废 / 3 张跳过)
|
||||
⚫ 删可删 + 作废需作废的(92 删 + 5 作废,3 跳过)
|
||||
|
||||
批量操作原因(必填,审计留痕):
|
||||
[多行输入框]
|
||||
|
||||
⚠️ 注意:
|
||||
- 物理删除不可恢复(但 activitylog 保留 bill_no)
|
||||
- 作废不可撤销
|
||||
- 跳过的账单不动
|
||||
|
||||
[取消] [确认执行]
|
||||
```
|
||||
|
||||
## 业务 Action 实现(`src/Actions/Bills/BulkDeleteBillsAction`)
|
||||
|
||||
```php
|
||||
class BulkDeleteBillsAction
|
||||
{
|
||||
public function handle(
|
||||
iterable $bills,
|
||||
BulkDeleteMode $mode, // OnlyDeletable / DeleteAndVoid
|
||||
string $reason,
|
||||
User $user,
|
||||
): BulkDeleteResult {
|
||||
$deletable = [];
|
||||
$voidable = [];
|
||||
$blocked = [];
|
||||
|
||||
// 第 1 步:分类
|
||||
foreach ($bills as $bill) {
|
||||
if ($bill->canBeDeleted()) {
|
||||
$deletable[] = $bill;
|
||||
} elseif ($bill->canBeVoided()) {
|
||||
$voidable[] = $bill;
|
||||
} else {
|
||||
$blocked[] = $bill;
|
||||
}
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
$voidedCount = 0;
|
||||
$affectedBillNos = [];
|
||||
|
||||
// 第 2 步:执行
|
||||
DB::transaction(function () use (...) {
|
||||
foreach ($deletable as $bill) {
|
||||
$affectedBillNos[] = $bill->bill_no . ' [DELETED]';
|
||||
$bill->delete();
|
||||
$deletedCount++;
|
||||
}
|
||||
|
||||
if ($mode === BulkDeleteMode::DeleteAndVoid) {
|
||||
foreach ($voidable as $bill) {
|
||||
$affectedBillNos[] = $bill->bill_no . ' [VOIDED]';
|
||||
app(VoidBillAction::class)->handle($bill, $reason, $user);
|
||||
$voidedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($blocked as $bill) {
|
||||
$affectedBillNos[] = $bill->bill_no . ' [SKIPPED]';
|
||||
}
|
||||
|
||||
// 第 3 步:一条总体 activitylog
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->withProperties([
|
||||
'mode' => $mode->value,
|
||||
'reason' => $reason,
|
||||
'total_selected' => count($bills),
|
||||
'deleted_count' => $deletedCount,
|
||||
'voided_count' => $voidedCount,
|
||||
'blocked_count' => count($blocked),
|
||||
'affected_bill_nos' => $affectedBillNos,
|
||||
])
|
||||
->event('bulk_deleted')
|
||||
->log('批量删除账单');
|
||||
});
|
||||
|
||||
return new BulkDeleteResult(
|
||||
deletedCount: $deletedCount,
|
||||
voidedCount: $voidedCount,
|
||||
blockedCount: count($blocked),
|
||||
affectedBillNos: $affectedBillNos,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## activitylog 设计
|
||||
|
||||
利用 `spatie/laravel-activitylog`(项目已装但本次首次启用)。
|
||||
|
||||
### 单条作废日志
|
||||
|
||||
`VoidBillAction` 触发,subject 是被作废的 Bill:
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `log_name` | `default` |
|
||||
| `subject_type` | `Bill` |
|
||||
| `subject_id` | 被作废 bill 的 ID |
|
||||
| `event` | `voided` |
|
||||
| `causer_id` | 操作员 ID |
|
||||
| `properties` | `{reason, from_status, to_status, bill_no, amount}` |
|
||||
|
||||
### 批量删除日志(本场景)
|
||||
|
||||
`BulkDeleteBillsAction` 触发,**无 subject**(批量操作不绑单个对象):
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `log_name` | `default` |
|
||||
| `subject_type` | null |
|
||||
| `subject_id` | null |
|
||||
| `event` | `bulk_deleted` |
|
||||
| `causer_id` | 操作员 ID |
|
||||
| `properties` | `{mode, reason, total_selected, deleted_count, voided_count, blocked_count, affected_bill_nos[]}` |
|
||||
|
||||
`properties.affected_bill_nos` 是一个数组,每条形如 `"B-202605-501-001 [DELETED]"` / `"B-202605-502-003 [VOIDED]"`,审计可完整还原。
|
||||
|
||||
## 审计追溯 SQL
|
||||
|
||||
```sql
|
||||
-- 某员工某月的全部批量删除
|
||||
SELECT
|
||||
id,
|
||||
event,
|
||||
causer_id,
|
||||
properties->>'$.mode' AS mode,
|
||||
properties->>'$.reason' AS reason,
|
||||
properties->>'$.deleted_count' AS deleted,
|
||||
properties->>'$.voided_count' AS voided,
|
||||
properties->>'$.affected_bill_nos' AS affected,
|
||||
created_at
|
||||
FROM activity_log
|
||||
WHERE causer_id = ?
|
||||
AND event = 'bulk_deleted'
|
||||
AND created_at BETWEEN '2026-05-01' AND '2026-05-31'
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
| created_at | mode | reason | deleted | voided | affected (部分) |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-05-15 14:32 | DeleteAndVoid | "5 月物业费 RatePlan 配错,清理重生成" | 92 | 5 | ["B-202605-501-001 [DELETED]", ...] |
|
||||
|
||||
## 完整业务流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务[业务人员]
|
||||
participant Filament
|
||||
participant BulkDelete[BulkDeleteBillsAction Filament UI]
|
||||
participant Action[BulkDeleteBillsAction 业务层]
|
||||
participant Bill[Bill 模型]
|
||||
participant Activity[activitylog]
|
||||
participant DB
|
||||
|
||||
业务->>Filament: 选 100 张 → 点"批量删除"
|
||||
Filament->>BulkDelete: 显示 Modal
|
||||
|
||||
BulkDelete->>BulkDelete: 预检查每张 Bill 分类
|
||||
BulkDelete->>Filament: Modal 显示 ✅92 ⚠️5 ❌3
|
||||
|
||||
业务->>Filament: 选"删可删 + 作废需作废" + 填原因 + 提交
|
||||
Filament->>Action: handle(100 bills, mode=DeleteAndVoid, reason, user)
|
||||
Action->>Bill: 第 1 步:再次分类(防 UI 缓存过期)
|
||||
Bill-->>Action: 92 deletable / 5 voidable / 3 blocked
|
||||
|
||||
Action->>DB: 开启事务
|
||||
|
||||
loop 92 deletable
|
||||
Action->>Bill: delete()
|
||||
Bill->>DB: 物理删
|
||||
end
|
||||
|
||||
loop 5 voidable
|
||||
Action->>Action: 调 VoidBillAction.handle(bill, reason, user)
|
||||
Action->>Bill: 翻状态 Void + meta
|
||||
Action->>Activity: log voided(单条)
|
||||
end
|
||||
|
||||
Action->>Activity: log bulk_deleted(总体 + affected_bill_nos)
|
||||
Action->>DB: 提交事务
|
||||
|
||||
Action-->>Filament: 结果(92 删 / 5 作废 / 3 跳过)
|
||||
Filament-->>业务: 通知"批量操作完成"
|
||||
```
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
完整流程:
|
||||
|
||||
1. **筛选**:在 `BillsTable` 用过滤(按期次 / 状态 / 费用类型)选出要清理的批
|
||||
2. **选中**:Table 的勾选框
|
||||
3. **触发**:顶部 "批量删除" 按钮
|
||||
4. **看预检查**:Modal 显示 ✅ / ⚠️ / ❌ 三档统计
|
||||
5. **决策**:选"仅删可删的"或"删可删 + 作废需作废的"
|
||||
6. **填原因**(必填):"5 月物业费配错,清理重生成"
|
||||
7. **提交**:系统执行 + 通知
|
||||
8. **审计**:事后可查 activitylog 追溯
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户感知:
|
||||
|
||||
| 业户类型 | 感知 |
|
||||
|---|---|
|
||||
| 被物理删账单的业户(未付款)| **无感**(账单从未发出 / 未通知)|
|
||||
| 被作废账单的业户(已付款)| 收到通知 + 看到 Void 状态 + 可能涉及退款 |
|
||||
| 不在批量内的业户 | 无感 |
|
||||
|
||||
## 权限设计
|
||||
|
||||
| 权限 | 谁该有 |
|
||||
|---|---|
|
||||
| `bill.delete` | 普通业务人员(单删)|
|
||||
| `bill.bulkDelete` | 主管 / 财务总监(批删独立权限)|
|
||||
| `bill.void` | 普通业务人员(作废)|
|
||||
|
||||
`bulkDelete` 是**独立高敏权限**,因为:
|
||||
|
||||
- 一次可能影响 100+ 张账单
|
||||
- 影响面大 → 限定高权限人员
|
||||
- 责任明确 → 出问题能追溯到这个人
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 为什么不直接走"全部作废"?
|
||||
> 物理删的"干净"价值见 [[delete-vs-void-dual-track]]。批量场景里,**绝大多数账单是 Unpaid 误建**,物理删一次清理干净;少数已动钱的走作废留痕,两全其美。
|
||||
|
||||
> [!question] Modal 预检查后实际执行时状态变了怎么办?
|
||||
> Action **再次分类**(防 UI 缓存),实际执行用最新状态。例如:
|
||||
>
|
||||
> - Modal 显示 Bill #X 可删(Unpaid 无付款)
|
||||
> - 业务人员看 Modal 时,另一人付了款 → Bill #X 变 Partial
|
||||
> - 业务人员点提交 → Action 重新分类 → Bill #X 进 voidable 档
|
||||
> - 按当前模式处理(若选 "DeleteAndVoid",作废;若选 "OnlyDeletable",跳过)
|
||||
|
||||
> [!question] activitylog 表会无限膨胀吗?
|
||||
> 是的(每条单作废 + 每次批量都加一条)。需要**归档策略**:
|
||||
>
|
||||
> - 超 X 年的 activitylog 归档到冷存储
|
||||
> - 或拆表(按月分区)
|
||||
>
|
||||
> 当前未配置归档,需运维介入(若数据量大)。
|
||||
|
||||
> [!question] 业务人员误操作批删大量账单怎么办?
|
||||
> activitylog 留有 `affected_bill_nos`,可以**反向恢复**(理论上):
|
||||
>
|
||||
> - 物理删的 Bill:无法恢复(数据真的没了),只能重新生成(走 [[create-periodic-property-fee]] 或 [[create-single-bill-manual]])
|
||||
> - 作废的 Bill:状态翻回 Unpaid 是不允许的(Void 是终态)→ 重新创建一张同信息的 Bill
|
||||
>
|
||||
> **预防**:必填原因 + 高权限 + Modal 显示统计 + 强调"不可恢复"。
|
||||
|
||||
> [!question] BulkDelete vs 直接逐条 Delete 100 次有什么区别?
|
||||
> | 维度 | BulkDelete | 逐条 100 次 |
|
||||
> |---|---|---|
|
||||
> | activitylog | 1 条(汇总)| 100 条(详细)|
|
||||
> | 业务人员效率 | 高(一次操作)| 低(100 次点击)|
|
||||
> | 模式灵活性 | 选 DeleteAndVoid 一次处理混合状态 | 逐条决策 |
|
||||
> | 审计可读性 | 中(看汇总 + affected)| 高(每条独立)|
|
||||
>
|
||||
> 当前设计**汇总一条 log + affected 数组** = 兼顾效率与可审计。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[delete-bill-unpaid]]
|
||||
- [[void-paid-bill]]
|
||||
- [[bulk-delete-batch-mistake]]
|
||||
- [[audit-activitylog-trace]]
|
||||
@@ -27,7 +27,7 @@ last_review: 2026-05-25
|
||||
| **保证金** | 装修押金等代管资金,完工后退还 | [deposit 知识地图](maps/deposit-knowledge-map.md) | ✅ 25 篇 |
|
||||
| **预存款** | 业户预存,自动抵扣月度账单 | [prepaid 知识地图](maps/prepaid-knowledge-map.md) | ✅ 23 篇 |
|
||||
| **计量表** | 水表/电表/燃气表,抄表生成账单 | [meter 知识地图](maps/meter-knowledge-map.md) | ✅ 21 篇 |
|
||||
| **账单** | 周期性账单 + 计量账单 | _待补_ | 🚧 |
|
||||
| **账单** | 周期性账单 + 计量账单 | [billing 知识地图](maps/billing-knowledge-map.md) | ✅ 23 篇 |
|
||||
| **收款订单** | 一次收款的支付方式、银行账户记录 | _待补_ | 🚧 |
|
||||
| **收据** | 成功收款后生成的凭证 | _待补_ | 🚧 |
|
||||
|
||||
|
||||
145
prop-acc/maps/billing-knowledge-map.md
Normal file
145
prop-acc/maps/billing-knowledge-map.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
title: prop-acc · billing · 知识地图
|
||||
aliases:
|
||||
- billing 知识地图
|
||||
- 账单知识地图
|
||||
tags:
|
||||
- 规范
|
||||
- prop-acc
|
||||
- 知识地图
|
||||
- 账单
|
||||
sub_feature: billing
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 抄表员
|
||||
status: 已发布
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 账单(billing)知识地图
|
||||
|
||||
> 本子模块 = Bill + CollectionOrderBill(中间表)。覆盖物业收款的**应收应付侧**:账单生成、状态管理、收款关联、删除 / 作废 / 挂起 / 拆单全套。
|
||||
>
|
||||
> billing 是 prop-acc **最复杂的子模块**,也是**收款流程的中枢**(各业务源 → Bill → CollectionOrder)。
|
||||
|
||||
## 这是什么?
|
||||
|
||||
物业管理软件最核心的对象之一 —— **应收账款的记录**。从抄表 / 周期任务 / 手工建单产生,经历状态变化(Unpaid → Partial → Paid),最终通过 [CollectionOrder](../concepts/adhoc/collection-order-and-receipt.md) 完成收款。
|
||||
|
||||
## 与其他子模块的关系
|
||||
|
||||
| 关系 | 说明 |
|
||||
|---|---|
|
||||
| **上游**:meter → bill | 抄表 → 生成计量账单 |
|
||||
| **上游**:周期任务 → bill | 月度物业费等批量生成 |
|
||||
| **上游**:手工 → bill | 临时收费 |
|
||||
| **下游**:bill → CollectionOrder | 收款时建 CO + Receipt |
|
||||
| **侧链**:bill ← prepaid | 业户预存款抵账单(走 Bill consume,见 [prepaid 模块](prepaid-knowledge-map.md))|
|
||||
|
||||
## 与其他子模块的核心差异
|
||||
|
||||
| 维度 | bill | 其他子模块 |
|
||||
|---|---|---|
|
||||
| 状态数 | **6**(最复杂)| deposit/prepaid 3,meter 2 |
|
||||
| 删 / 作废 | **双轨制** | 只有 Close / Decommission |
|
||||
| Policy 方法数 | **7** | deposit 12 / prepaid 9 / meter 5 |
|
||||
| 审计 | **activitylog + meta** | meta JSON only |
|
||||
| 批删 | **智能 Modal(3 档分类)** | 无 |
|
||||
| 与 CollectionOrder 关系 | **多对多**(中间表)| 1:1(adhoc / deposit / prepaid) |
|
||||
|
||||
## 核心概念(6 篇)
|
||||
|
||||
| 文档 | 一句话 |
|
||||
|---|---|
|
||||
| [账单六状态机](../concepts/billing/bill-six-state-machine.md) | 6 状态(Unpaid / Partial / Paid / Suspended / Processing / Void),prop-acc 最复杂 |
|
||||
| [账单类型与来源](../concepts/billing/bill-types-and-sources.md) | 周期 / 计量 / 临时 三类 + sourceable polymorphism |
|
||||
| [Bill 与 CollectionOrder 关系](../concepts/billing/bill-vs-collection-order.md) | 应收 vs 已收,CollectionOrderBill 多对多 |
|
||||
| [周期账单生成机制](../concepts/billing/periodic-bill-generation.md) | `GeneratePeriodicBillsAction` + `BillingMergeStrategy` 三种合并策略 |
|
||||
| [删除 vs 作废双轨制](../concepts/billing/delete-vs-void-dual-track.md) | 物理删(Unpaid 无付款)vs 作废(留状态留审计)的设计哲学 |
|
||||
| [智能批量删除设计](../concepts/billing/smart-bulk-delete-design.md) | 预检查三档分类 + 必填原因 + activitylog 完整审计 |
|
||||
|
||||
## 场景手册(16 篇,**全部完成 ✅**)
|
||||
|
||||
### 📝 账单创建(3 篇)
|
||||
|
||||
- ✅ [月度物业费批量生成(`GeneratePeriodicBillsAction`)](../scenarios/billing/create-periodic-property-fee.md)
|
||||
- ✅ [抄表自动生成计量账单(走 meter pipeline)](../scenarios/billing/create-meter-bill-auto.md)
|
||||
- ✅ [手动建单(临时收费 / 调整账单)](../scenarios/billing/create-single-bill-manual.md)
|
||||
|
||||
### 💰 收款(3 篇)
|
||||
|
||||
- ✅ [单张账单收款(`CollectPaymentAction`)](../scenarios/billing/collect-payment-single.md)
|
||||
- ✅ [同业户多账单批量收款(`BatchCollectPaymentAction`)](../scenarios/billing/collect-payment-batch.md)
|
||||
- ✅ [预存款抵扣自动收款(关联 prepaid)](../scenarios/billing/collect-via-prepaid-auto.md)
|
||||
|
||||
### ✂️ 账单调整(3 篇)
|
||||
|
||||
- ✅ [拆账单(`SplitBillAction`,租户与房东分摊)](../scenarios/billing/split-bill.md)
|
||||
- ✅ [挂起账单(业户失联 / 纠纷)](../scenarios/billing/suspend-bill.md)
|
||||
- ✅ [恢复挂起的账单](../scenarios/billing/resume-bill.md)
|
||||
|
||||
### 🗑️ 删除 / 作废(3 篇)
|
||||
|
||||
- ✅ [物理删除未付账单(误建立刻删)](../scenarios/billing/delete-bill-unpaid.md)
|
||||
- ✅ [作废已付账单(走作废 + 退款)](../scenarios/billing/void-paid-bill.md)
|
||||
- ✅ [批量误建,智能 Modal 三档清理](../scenarios/billing/bulk-delete-batch-mistake.md)
|
||||
|
||||
### 🛡️ 异常 / 审计(4 篇)
|
||||
|
||||
- ✅ [部分付状态处理(Partial)](../scenarios/billing/exception-partial-payment.md)
|
||||
- ✅ [逾期账单清单 + 催收(`OverdueBillsListWidget`)](../scenarios/billing/exception-overdue-bills.md)
|
||||
- ✅ [月度账单生成 vs 收款对比(`MonthlyBillingVsCollectionChart`)](../scenarios/billing/audit-monthly-billing-vs-collection.md)
|
||||
- ✅ [activitylog 审计追溯](../scenarios/billing/audit-activitylog-trace.md)
|
||||
|
||||
## 跨域引用
|
||||
|
||||
本子模块引用以下跨域共享概念:
|
||||
|
||||
- [业户](../../cross/concepts/resident.md) — 账单收方
|
||||
- [房屋单元](../../cross/concepts/housing-unit.md) — `asset_id`,账单关联房屋
|
||||
- [组织结构](../../cross/concepts/org-hierarchy.md) — `community_id`,物业项目归属
|
||||
|
||||
## 跨子模块引用
|
||||
|
||||
- [adhoc · CollectionOrder 与 Receipt](../concepts/adhoc/collection-order-and-receipt.md) — 收款侧的核心对象
|
||||
- [meter · 账单生成的三层分层](../concepts/meter/bill-generation-pipeline.md) — 计量账单的生成器
|
||||
- [prepaid · Consume 走 CollectionType=Bill](../concepts/prepaid/consume-via-bill-collection-type.md) — 预存款抵账单的资金流
|
||||
- [meter · 表退役与读数锁定](../concepts/meter/decommission-and-locking.md) — 类似的"状态机+守护"对比
|
||||
- [deposit · 账户与流水](../concepts/deposit/deposit-account-vs-transaction.md) — 双对象模式对比
|
||||
|
||||
## 相关代码
|
||||
|
||||
- 模型:[`Bill.php`](../../../packages/prop-acc/src/Models/Bill.php)、[`CollectionOrderBill.php`](../../../packages/prop-acc/src/Models/CollectionOrderBill.php)
|
||||
- 枚举:`BillStatus`(6 种)、`BillType`、`BillingMergeStrategy`、`FeeTypeBillType`
|
||||
- Policy:`BillPolicy`(7 个方法:update / delete / deleteAny / void / collect / suspend / resume)
|
||||
- 业务 Actions(src/Actions/Bills/):
|
||||
- [`VoidBillAction`](../../../packages/prop-acc/src/Actions/Bills/VoidBillAction.php)
|
||||
- [`SplitBillAction`](../../../packages/prop-acc/src/Actions/Bills/SplitBillAction.php)
|
||||
- [`SuspendBillAction`](../../../packages/prop-acc/src/Actions/Bills/SuspendBillAction.php)
|
||||
- [`ResumeBillAction`](../../../packages/prop-acc/src/Actions/Bills/ResumeBillAction.php)
|
||||
- `BulkDeleteBillsAction`(智能批删)
|
||||
- Filament Resource:[`packages/prop-acc/src/Filament/Resources/Bills/`](../../../packages/prop-acc/src/Filament/Resources/Bills/)
|
||||
- Filament Actions(UI 入口):8 个(CollectPayment / BatchCollectPayment / GeneratePeriodicBills / Split / Suspend / Resume / Void / BulkDelete)
|
||||
- Widgets:`BillingStatsOverviewWidget`、`MonthlyBillingVsCollectionChart`、`FeeTypeRevenueDistributionChart`、`OverdueBillsListWidget`、`MonthlyRevenueTrendChart`
|
||||
- 业务设计决策:`packages/prop-acc/issue.md` 的 Q6 段(最详细的 issue 之一)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [prop-acc 域知识地图](knowledge-map.md)
|
||||
- [prop-acc 域首页](../index.md)
|
||||
- [adhoc 子模块知识地图](adhoc-knowledge-map.md)
|
||||
- [deposit 子模块知识地图](deposit-knowledge-map.md)
|
||||
- [prepaid 子模块知识地图](prepaid-knowledge-map.md)
|
||||
- [meter 子模块知识地图](meter-knowledge-map.md)
|
||||
- [跨域协作地图](../../cross/maps/cross-domain-map.md)
|
||||
|
||||
---
|
||||
|
||||
> [!success] billing 子模块:6 概念 + 16 场景 + 1 知识地图 = **23 篇完成**
|
||||
>
|
||||
> 写作日期:2026-05-26
|
||||
> 对应代码版本:2026-05-22(详见 `packages/prop-acc/issue.md` Q6 段,最详细 issue 之一)
|
||||
>
|
||||
> 如果发现遗漏的场景或需要补充的细节,告诉我,可以单独补充新文档。
|
||||
@@ -23,7 +23,7 @@ last_review: 2026-05-25
|
||||
| prepaid | 预存款 | 业户预存,自动抵扣月度账单 | [prepaid 知识地图](prepaid-knowledge-map.md) | ✅ 16 场景 + 6 概念 + 1 地图 = 23 篇 |
|
||||
| deposit | 保证金 | 装修押金等代管资金,完工后退还 | [deposit 知识地图](deposit-knowledge-map.md) | ✅ 18 场景 + 6 概念 + 1 地图 = 25 篇 |
|
||||
| meter | 计量表 | 水表/电表/燃气表,抄表生成账单 | [meter 知识地图](meter-knowledge-map.md) | ✅ 14 场景 + 6 概念 + 1 地图 = 21 篇 |
|
||||
| billing | 账单 | 周期性账单 + 计量账单 | _待补_ | 🚧 |
|
||||
| billing | 账单 | 周期性账单 + 计量账单 | [billing 知识地图](billing-knowledge-map.md) | ✅ 16 场景 + 6 概念 + 1 地图 = 23 篇 |
|
||||
| payment-order | 收款订单 | 一次收款的支付方式、银行账户记录 | _待补_ | 🚧 |
|
||||
| receipt | 收据 | 成功收款后生成的凭证 | _待补_ | 🚧 |
|
||||
|
||||
|
||||
311
prop-acc/scenarios/billing/audit-activitylog-trace.md
Normal file
311
prop-acc/scenarios/billing/audit-activitylog-trace.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - activitylog 审计追溯
|
||||
aliases:
|
||||
- activitylog 审计
|
||||
- 操作日志查询
|
||||
- audit-activitylog-trace
|
||||
- 场景-activitylog 追溯
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 审计
|
||||
- 合规
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 审计师
|
||||
- 法务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:activitylog 审计追溯
|
||||
|
||||
billing 模块**首次启用** `spatie/laravel-activitylog`(prop-acc 其他模块仅用 meta JSON)。所有关键操作(作废 / 批删 / 挂起 / 恢复 / 收款 / 创建)记 activitylog,审计 / 内审 / 法务可**精准追溯**:谁 / 什么时候 / 在哪 / 改了什么。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 5 月 20 日,内审师审计 5 月嘉禾花园账单数据,发现:
|
||||
>
|
||||
> - 5 月 15 日有一次**批量删除 92 张账单**(`bulk_deleted`)
|
||||
> - 同日有 5 条 **bill voided**(账单作废)
|
||||
> - 还有几张账单的 status 从 Suspended → Unpaid(恢复)
|
||||
>
|
||||
> 审计师要查清:
|
||||
> - 谁操作的?
|
||||
> - 操作的原因?
|
||||
> - 影响了哪些具体账单?
|
||||
> - 是否合规?
|
||||
|
||||
## 业务人员 / 审计师视角
|
||||
|
||||
### 第 1 步:确定查询维度
|
||||
|
||||
- **谁操作**:`causer_id`(操作员)
|
||||
- **什么时候**:`created_at` 范围
|
||||
- **操作类型**:`event`(created / voided / bulk_deleted / suspended / resumed / collected / split)
|
||||
- **针对哪个对象**:`subject_type` + `subject_id`(单条操作)/ properties.affected_bill_nos(批量)
|
||||
- **操作详情**:`properties` JSON 字段
|
||||
|
||||
### 第 2 步:运行 SQL 查询
|
||||
|
||||
#### 查询 1:某员工某月所有操作
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
event,
|
||||
subject_type,
|
||||
subject_id,
|
||||
properties,
|
||||
created_at
|
||||
FROM activity_log
|
||||
WHERE causer_id = ? -- 王主管 ID
|
||||
AND created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59'
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
| id | event | subject | properties (节选) | created_at |
|
||||
|---|---|---|---|---|
|
||||
| 1023 | bulk_deleted | (null) | mode=DeleteAndVoid, reason="...", deleted=92, voided=5 | 2026-05-15 14:32 |
|
||||
| 1022 | voided | Bill #500 | reason="业务调整", from_status=Partial | 2026-05-15 14:31 |
|
||||
| 1021 | collected | Bill #321 | amount=800, channel=微信 | 2026-05-10 11:23 |
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
#### 查询 2:某 Bill 的所有操作历史
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
event,
|
||||
causer_id,
|
||||
properties,
|
||||
created_at
|
||||
FROM activity_log
|
||||
WHERE subject_type = 'App\\Models\\Bill'
|
||||
AND subject_id = ? -- 具体 Bill ID
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
返回该 Bill 的**全生命周期**:created → collected → voided 等。
|
||||
|
||||
#### 查询 3:批量删除的详情
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
causer_id,
|
||||
properties->>'$.mode' AS mode,
|
||||
properties->>'$.reason' AS reason,
|
||||
properties->>'$.total_selected' AS selected,
|
||||
properties->>'$.deleted_count' AS deleted,
|
||||
properties->>'$.voided_count' AS voided,
|
||||
properties->>'$.blocked_count' AS blocked,
|
||||
JSON_LENGTH(properties->'$.affected_bill_nos') AS affected_count,
|
||||
created_at
|
||||
FROM activity_log
|
||||
WHERE event = 'bulk_deleted'
|
||||
AND created_at BETWEEN ? AND ?
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
可看出**每次批删**的统计 + 原因。
|
||||
|
||||
#### 查询 4:某 bill_no 是否被批删 / 作废过
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM activity_log
|
||||
WHERE event = 'bulk_deleted'
|
||||
AND JSON_CONTAINS(
|
||||
properties->'$.affected_bill_nos',
|
||||
JSON_QUOTE('B-202605-501-001 [DELETED]')
|
||||
);
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```sql
|
||||
-- 灵活模糊匹配
|
||||
SELECT *
|
||||
FROM activity_log
|
||||
WHERE event = 'bulk_deleted'
|
||||
AND properties->'$.affected_bill_nos' LIKE '%B-202605-501-001%';
|
||||
```
|
||||
|
||||
可追溯到**已物理删的 Bill** 是何时被谁删的。
|
||||
|
||||
### 第 3 步:解读 properties
|
||||
|
||||
不同 event 的 properties 结构不同:
|
||||
|
||||
| event | properties 主要字段 |
|
||||
|---|---|
|
||||
| `created`(账单创建)| `bill_no, amount, fee_type_id, resident_id` |
|
||||
| `collected`(收款)| `amount, channel, receipt_id` |
|
||||
| `voided`(单作废)| `reason, from_status, to_status, bill_no, amount, paid_amount` |
|
||||
| `suspended`(挂起)| `reason, from_status, to_status, bill_no` |
|
||||
| `resumed`(恢复)| `reason, from_status, to_status, bill_no` |
|
||||
| `split`(拆账单)| `target_resident, amount_split, original_bill_id, new_bill_id` |
|
||||
| `bulk_deleted`(批删)| `mode, reason, total_selected, deleted_count, voided_count, blocked_count, affected_bill_nos[]` |
|
||||
|
||||
详见 [[smart-bulk-delete-design]]"activitylog 设计"段。
|
||||
|
||||
### 第 4 步:出审计报告
|
||||
|
||||
```markdown
|
||||
# 2026 年 5 月 嘉禾花园账单操作审计报告
|
||||
|
||||
## 审计范围
|
||||
- 时段:2026-05-01 至 2026-05-31
|
||||
- 模块:billing
|
||||
- 关注操作:bulk_deleted, voided
|
||||
|
||||
## 高敏操作统计
|
||||
- 批量删除(bulk_deleted):2 次
|
||||
- 5/15 王主管:92 删 + 5 作废,原因"5 月 1 日 Replace 策略误用清理"
|
||||
- 5/28 李经理:30 删,原因"测试数据清理"
|
||||
- 单条作废(voided):8 次
|
||||
- 大多与上述批删事件关联
|
||||
- 1 次独立:5/22 王主管作废 Bill #321(原因:陈先生纠纷调解结果)
|
||||
|
||||
## 异常发现
|
||||
- 无未授权操作(所有 bulk_deleted 操作员均有 bill.bulkDelete 权限)
|
||||
- 无超规模操作(单次最多 92 张,合规)
|
||||
- 所有操作都填了 reason(合规)
|
||||
|
||||
## 合规结论
|
||||
- ✅ 所有高敏操作均有 audit trail
|
||||
- ✅ 操作员权限符合岗位
|
||||
- ✅ 原因填写规范
|
||||
|
||||
## 建议
|
||||
- 长期保留 activitylog(至少 7 年,与会计档案同周期)
|
||||
- 季度审计抽查
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**通常不直接接触** activitylog。但**法律纠纷时**:
|
||||
|
||||
- 业户对某账单的操作有疑问 → 业户可申请查看 activitylog
|
||||
- 物业有义务**留存 + 展示**操作历史(透明化、可追溯)
|
||||
- 业户/法院通过 activitylog 评估物业操作合规性
|
||||
|
||||
## 法务 / 监管视角
|
||||
|
||||
| 用途 | 怎么用 |
|
||||
|---|---|
|
||||
| **司法纠纷举证** | 业户起诉物业不当操作 → 物业拿 activitylog 证明操作合规 |
|
||||
| **政府监管检查** | 检查批量删除 / 作废操作是否合理 |
|
||||
| **行业自律审计** | 行业协会定期抽查 |
|
||||
| **内部审计** | 财务总监 / 审计部门定期审 |
|
||||
|
||||
## 与 prop-acc 其他模块的对比
|
||||
|
||||
| 模块 | 审计方案 |
|
||||
|---|---|
|
||||
| **billing(本)** | **activitylog + meta** |
|
||||
| deposit | meta JSON(`force_closed_*` / `freeze_reason` 等)|
|
||||
| prepaid | meta JSON(`freeze_reason` / `unfreeze_reason`)|
|
||||
| meter | meta JSON(`decommission_reason`)|
|
||||
| adhoc | meta JSON 或 `voided_at` 字段 |
|
||||
|
||||
billing 的 activitylog 是 prop-acc **首个启用 spatie 审计日志的模块**(issue.md Q6 标志性改进)。其他模块**可以借鉴**(未来若启用,有 billing 实施经验)。
|
||||
|
||||
## activitylog 表结构(spatie 标准)
|
||||
|
||||
```sql
|
||||
CREATE TABLE activity_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
log_name VARCHAR(255),
|
||||
description TEXT, -- 简单描述
|
||||
subject_type VARCHAR(255), -- 对象类(Bill)
|
||||
subject_id BIGINT, -- 对象 ID
|
||||
causer_type VARCHAR(255), -- 操作员类(User)
|
||||
causer_id BIGINT, -- 操作员 ID
|
||||
properties JSON, -- 详情(reason / amount / etc.)
|
||||
event VARCHAR(255), -- 自定义事件名
|
||||
batch_uuid VARCHAR(36), -- 可关联多条 log
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
INDEX idx_subject(subject_type, subject_id),
|
||||
INDEX idx_causer(causer_type, causer_id),
|
||||
INDEX idx_event(event),
|
||||
INDEX idx_created_at(created_at)
|
||||
);
|
||||
```
|
||||
|
||||
## 数据保留与归档
|
||||
|
||||
> [!warning] activitylog 表会快速增长
|
||||
> 每次高敏操作 1 条 + 收款 / 创建 / 编辑等可能也加 → 数据增长快。
|
||||
>
|
||||
> 建议:
|
||||
>
|
||||
> - **生产环境**:保留 1-3 年热数据(查询性能优)
|
||||
> - **冷数据归档**:超 1 年的迁到 archive 表 / 对象存储
|
||||
> - **永久保留**:某些事件(bulk_deleted / voided)**法定 7+ 年**
|
||||
>
|
||||
> 当前未实施归档,数据量大后需要运维介入。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] activitylog 与 meta JSON 的关系?
|
||||
> 互补:
|
||||
>
|
||||
> | 方面 | activitylog | meta JSON |
|
||||
> |---|---|---|
|
||||
> | 保留多久 | 全平台共表(易归档)| 与对象同存(无法独立归档)|
|
||||
> | 查询性能 | 按时间 / 类型查快 | 在对象上 random access 快 |
|
||||
> | 关联多个对象 | ✅(properties 数组) | ❌(只在自己 meta)|
|
||||
> | 跨模块查询 | ✅ | ❌(各模块字段不同)|
|
||||
> | 操作上下文 | ✅(causer_id 直接)| ❌(若没存)|
|
||||
>
|
||||
> 推荐:meta 存"对象当前状态的辅助"(如 voided_at);activitylog 存"事件链"(谁干了什么)。两者不冲突。
|
||||
|
||||
> [!question] 业务人员如何看 activitylog?
|
||||
> 当前**没有 UI**(spatie 包提供数据存储,UI 需自建)。审计师 / 业务方查询:
|
||||
>
|
||||
> - 直接 SQL(本场景示范)
|
||||
> - 让运维查 / 出导出
|
||||
> - 未来加 `ActivityLogResource` Filament UI(优先级看需求)
|
||||
|
||||
> [!question] activitylog 能改 / 删吗?
|
||||
> 系统层面**理论上可改**(就是普通表)。**合规上不应改**(篡改审计 = 大罪)。
|
||||
>
|
||||
> 高级实施:用**append-only**表(数据库层面 disallow UPDATE/DELETE)+ 定期写校验和(若数据被改可发现)。当前未实施。
|
||||
|
||||
> [!question] 跨模块审计能合并查吗?
|
||||
> 可以(activitylog 是全平台共表)。但其他模块当前**没启用 activitylog**(用 meta),所以跨模块审计**目前只看 bill 模块**。未来若其他模块启用,可统一查。
|
||||
|
||||
> [!question] activitylog 与系统日志(Laravel log)的差异?
|
||||
> | 维度 | activitylog | Laravel log |
|
||||
> |---|---|---|
|
||||
> | 内容 | 业务事件(带 subject / causer / properties) | 技术日志(error / debug / info)|
|
||||
> | 持久化 | 数据库 | 文件 / 集中日志服务 |
|
||||
> | 业务查询 | ✅ 结构化,SQL 查 | ❌ 全文搜索 |
|
||||
> | 合规价值 | **高** | 低(辅助排错)|
|
||||
>
|
||||
> 两者并存,各管各的。activitylog 关注**业务操作**,Laravel log 关注**系统行为**。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 发现疑似篡改 → 物业内部审计 + 法务介入
|
||||
- 数据量太大查询慢 → 归档老数据
|
||||
- 业户申请查 activitylog → 出报告(由审计师 / 法务出)
|
||||
- 长期审计需求 → 加 `ActivityLogResource` UI(未来扩展)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[smart-bulk-delete-design]](核心 activitylog 设计)
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[bulk-delete-batch-mistake]]
|
||||
- [[void-paid-bill]]
|
||||
- [[suspend-bill]]
|
||||
- [[resume-bill]]
|
||||
- [[audit-monthly-billing-vs-collection]]
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 月度账单生成 vs 收款对比
|
||||
aliases:
|
||||
- 账单 vs 收款对比
|
||||
- MonthlyBillingVsCollectionChart
|
||||
- 收款率
|
||||
- audit-monthly-billing-vs-collection
|
||||
- 场景-月度账单收款对比
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 审计
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 管理层
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:月度账单生成 vs 收款对比
|
||||
|
||||
物业**月度报表**核心指标 —— **本月生成多少账单 vs 本月收到多少款**。`MonthlyBillingVsCollectionChart` Widget 直观对比。是评估**收款率 / 应收账款健康度**的标准动作。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 5 月底,王主管打开 dashboard 看 `MonthlyBillingVsCollectionChart`:
|
||||
>
|
||||
> - 本月生成账单:¥120,000(300 户 × 平均 ¥400)
|
||||
> - 本月已收款:¥95,000
|
||||
> - **收款率:79.2%**
|
||||
> - **应收账款增长**:¥25,000(120 - 95)
|
||||
>
|
||||
> 与 4 月对比:
|
||||
> - 4 月生成:¥118,000,4 月收:¥110,000(93.2%)
|
||||
> - **5 月收款率下降 14%**,需排查原因
|
||||
|
||||
## Widget 显示
|
||||
|
||||
`MonthlyBillingVsCollectionChart`(在 BillingDashboard 或主 Dashboard):
|
||||
|
||||
```
|
||||
2026 年月度账单 vs 收款对比图
|
||||
|
||||
| 生成 | 收款 | 收款率
|
||||
2026-01 | 120k | 115k | 95.8%
|
||||
2026-02 | 119k | 117k | 98.3%
|
||||
2026-03 | 121k | 113k | 93.4%
|
||||
2026-04 | 118k | 110k | 93.2%
|
||||
2026-05 | 120k | 95k | 79.2% ⚠️ 异常
|
||||
|
||||
(柱状图)
|
||||
```
|
||||
|
||||
可下钻看:
|
||||
|
||||
- 按费用类型分布(物业费 vs 水电气 vs 临时)
|
||||
- 按业户类别(住宅 vs 商铺)
|
||||
- 按收款方式(现金 / 微信 / 预存款抵 / 其他)
|
||||
- 逾期账单累计金额
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:看 Widget
|
||||
|
||||
每月初(5/1-5/3)看上月数据。
|
||||
|
||||
### 第 2 步:对比历史
|
||||
|
||||
| 月份 | 收款率 | 趋势 |
|
||||
|---|---|---|
|
||||
| 4 月 | 93.2% | 平稳 |
|
||||
| 3 月 | 93.4% | 平稳 |
|
||||
| 2 月 | 98.3% | 高 |
|
||||
| 1 月 | 95.8% | 高 |
|
||||
| **5 月** | **79.2%** | **断崖** |
|
||||
|
||||
5 月异常,需要排查。
|
||||
|
||||
### 第 3 步:排查原因
|
||||
|
||||
可能原因:
|
||||
|
||||
| 原因 | 排查方式 |
|
||||
|---|---|
|
||||
| **逾期账单激增** | 看 `OverdueBillsListWidget`,看 5 月逾期累计 |
|
||||
| **某费用类型收款率低** | 按费用类型下钻 |
|
||||
| **某社区收款率低** | 多社区时按社区下钻 |
|
||||
| **某收款渠道异常** | 看渠道分布,例如微信掉单 |
|
||||
| **季节性**(例如春节后回流缓)| 与去年同月对比 |
|
||||
| **集抄数据异常**(导致账单虚高)| 看 [[../meter/exception-high-consumption]] |
|
||||
| **业务方调整 RatePlan**(账单变高,业户抗拒)| 看 RatePlan 变更日志 |
|
||||
|
||||
### 第 4 步:出月度报告
|
||||
|
||||
给物业总经理 / 财务总监:
|
||||
|
||||
```markdown
|
||||
# 2026 年 5 月 嘉禾花园收款月报
|
||||
|
||||
## 总览
|
||||
- 应收账款生成:¥120,000(300 户)
|
||||
- 实际收款:¥95,000
|
||||
- 收款率:79.2%(同比 -14% / 环比 -14%)
|
||||
|
||||
## 异常分析
|
||||
- 主要原因:5 月物业费 RatePlan 上调(¥3 → ¥3.5),业户抗拒
|
||||
- 60 户拒绝按新价付,只付旧价部分 → Partial 状态激增
|
||||
- 业主大会沟通中,预计 6 月底有结果
|
||||
|
||||
## 已采取措施
|
||||
- 暂停涨价部分的催收(挂起 60 户的差额账单)
|
||||
- 6 月初业主大会重新讨论
|
||||
|
||||
## 预测
|
||||
- 6 月若按新价确认,收款率回升 90%+
|
||||
- 6 月若需妥协 → 走批量作废涨价部分 + 重建按旧价
|
||||
|
||||
## 资金影响
|
||||
- 应收账款余额从 4 月底 ¥35,000 升至 5 月底 ¥60,000
|
||||
- 风险:占用物业现金流 + 6 月人员工资 / 维修 可能紧张
|
||||
```
|
||||
|
||||
## SQL 报表查询
|
||||
|
||||
### 本月生成账单总额
|
||||
|
||||
```sql
|
||||
SELECT SUM(amount) AS billed_total
|
||||
FROM acc_bills
|
||||
WHERE community_id = ?
|
||||
AND billing_period_start BETWEEN '2026-05-01' AND '2026-05-31'
|
||||
AND status != 'void';
|
||||
```
|
||||
|
||||
### 本月收款总额
|
||||
|
||||
```sql
|
||||
SELECT SUM(actual_amount) AS collected_total
|
||||
FROM acc_collection_orders
|
||||
WHERE community_id = ?
|
||||
AND collection_type = 'Bill'
|
||||
AND status = 'completed'
|
||||
AND completed_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
|
||||
```
|
||||
|
||||
### 收款率
|
||||
|
||||
```sql
|
||||
-- 本月生成的账单的本月收款率(注意:可能本月生成的下月才付,本月也可能付上月生成的)
|
||||
-- 标准公式:本月收款总额 / 本月生成总额
|
||||
```
|
||||
|
||||
更精准的"本月生成 → 本月收"对比需要 join,看具体业务定义。
|
||||
|
||||
### 按费用类型分布
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
ft.name AS fee_type,
|
||||
SUM(b.amount) AS billed,
|
||||
SUM(b.paid_amount) AS paid,
|
||||
SUM(b.amount - b.paid_amount) AS outstanding,
|
||||
ROUND(SUM(b.paid_amount) * 100.0 / NULLIF(SUM(b.amount), 0), 2) AS rate_pct
|
||||
FROM acc_bills b
|
||||
JOIN fee_types ft ON b.fee_type_id = ft.id
|
||||
WHERE b.community_id = ?
|
||||
AND b.billing_period_start BETWEEN '2026-05-01' AND '2026-05-31'
|
||||
AND b.status != 'void'
|
||||
GROUP BY ft.name
|
||||
ORDER BY billed DESC;
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
| fee_type | billed | paid | outstanding | rate_pct |
|
||||
|---|---|---|---|---|
|
||||
| 物业费 | 100,000 | 75,000 | 25,000 | 75.0% |
|
||||
| 水费 | 8,000 | 7,500 | 500 | 93.8% |
|
||||
| 电费 | 10,000 | 9,500 | 500 | 95.0% |
|
||||
| 燃气 | 2,000 | 1,950 | 50 | 97.5% |
|
||||
| 杂费 | 0 | 0 | 0 | N/A |
|
||||
|
||||
立刻能看出**物业费是问题**(其他费用收款率 90%+)。
|
||||
|
||||
## 关联 Widgets
|
||||
|
||||
| Widget | 用途 |
|
||||
|---|---|
|
||||
| **`MonthlyBillingVsCollectionChart`**(本场景)| 整体趋势 |
|
||||
| `BillingStatsOverviewWidget` | 总数 + 总额 + 收款率快照 |
|
||||
| `FeeTypeRevenueDistributionChart` | 按费用类型分布(饼图)|
|
||||
| `OverdueBillsListWidget` | 逾期清单 |
|
||||
| `MonthlyRevenueTrendChart` | 月度收入趋势(长期看 12 月)|
|
||||
|
||||
业务人员**月初看一圈**:整体 → 类别 → 逾期 → 长期趋势。
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**不直接看这种报表**。但物业**可能公布**(透明化):
|
||||
|
||||
- 业主大会汇报"上月物业费收款率 X%"
|
||||
- 在小区公告栏公示
|
||||
- 在业主群发月度总结
|
||||
|
||||
收款率高 → 业户感觉"物业团结" 反之有疑虑。
|
||||
|
||||
## 财务视角
|
||||
|
||||
### 财务关心的核心指标
|
||||
|
||||
| 指标 | 目标 | 含义 |
|
||||
|---|---|---|
|
||||
| **收款率** | > 90%(目标 95%+)| 月度收款 / 月度账单 |
|
||||
| **应收账款余额** | < 2 个月账单合计 | 累计欠款,反映现金流压力 |
|
||||
| **逾期账单占比** | < 10% | 长期不付的比例 |
|
||||
| **平均逾期天数** | < 30 | 收款时效 |
|
||||
|
||||
### 与会计核算的衔接
|
||||
|
||||
billing 报表与会计科目映射:
|
||||
|
||||
| 报表项 | 会计科目 |
|
||||
|---|---|
|
||||
| 本月生成账单总额 | "应收账款"(借方)+ "营业收入"(贷方,权责发生制) |
|
||||
| 本月收款总额 | "现金 / 银行存款"(借方)+ "应收账款"(贷方) |
|
||||
| 应收账款余额 | "应收账款"账户余额 |
|
||||
|
||||
报表为财务月度结账提供数据。
|
||||
|
||||
## 趋势分析
|
||||
|
||||
```mermaid
|
||||
xychart-beta
|
||||
title "嘉禾花园 6 个月账单 vs 收款"
|
||||
x-axis [Jan, Feb, Mar, Apr, May]
|
||||
y-axis "金额(千元)" 0 --> 150
|
||||
bar [120, 119, 121, 118, 120]
|
||||
line [115, 117, 113, 110, 95]
|
||||
```
|
||||
|
||||
异常点(5 月)立刻可见。趋势线告诉管理层何时介入。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 收款率多低算异常?
|
||||
> 看历史基线 + 行业标准:
|
||||
> - 健康物业:90%+
|
||||
> - 一般物业:80-90%
|
||||
> - 问题物业:< 80%
|
||||
>
|
||||
> 单月波动 5-10% 正常(春节后 / 大额账单后);连续 2-3 月低于基线 = 严重问题。
|
||||
|
||||
> [!question] 本月生成的账单本月没付完算异常吗?
|
||||
> 不一定。账单 due_at 通常是月底 + 宽限期(到下月中旬)。**本月内付清率 < 100% 是常态**。重要的是**到期前付清率**。
|
||||
|
||||
> [!question] 报表数据与银行账户对账不一致怎么办?
|
||||
> 排查:
|
||||
> - 是否有 CO 状态 = Completed 但银行未到账(在途资金)
|
||||
> - 是否有 CO 创建错(状态 Failed 但 amount 异常)
|
||||
> - 是否有 fund_source=prepaid 的(账面收款但银行没动)
|
||||
> - 是否有手工调账 / tinker 操作
|
||||
|
||||
> [!question] Widget 数据**慢**怎么办?
|
||||
> 大数据量(100k+ 账单)时 SQL 可能慢。优化:
|
||||
> - 加索引(`community_id`, `billing_period_start`, `status`)
|
||||
> - 用物化视图(定时刷新)
|
||||
> - 引入 OLAP 工具(BI dashboard 专门处理)
|
||||
>
|
||||
> 当前数据量不大,Widget 直接 SQL 应能秒级出。
|
||||
|
||||
> [!question] 多社区合并看怎么办?
|
||||
> Widget 通常按当前 Panel 的 community_id 过滤。**多社区合并**需要管理 Panel 角色(无社区限制)+ Widget 显示总览(可下钻到具体社区)。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 收款率低 + 逾期多 → 走 [[exception-overdue-bills|催收]]
|
||||
- 单业户 Partial 多 → [[exception-partial-payment|跟进部分付]]
|
||||
- 收款率异常(数据 bug)→ [[audit-activitylog-trace|查 activitylog]] 排查异常操作
|
||||
- 长期低收款率 → 业务方反思 RatePlan / 服务质量
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[exception-overdue-bills]]
|
||||
- [[exception-partial-payment]]
|
||||
- [[audit-activitylog-trace]]
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[../prepaid/audit-low-balance-and-overdue]]
|
||||
- [[../deposit/audit-monthly-deposit-balance]]
|
||||
267
prop-acc/scenarios/billing/bulk-delete-batch-mistake.md
Normal file
267
prop-acc/scenarios/billing/bulk-delete-batch-mistake.md
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 批量误建,智能 Modal 三档分类清理
|
||||
aliases:
|
||||
- 批量删除账单
|
||||
- BulkDeleteBillsAction 实战
|
||||
- 智能批删
|
||||
- bulk-delete-batch-mistake
|
||||
- 场景-批量删除账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 删除
|
||||
- 批量
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:批量误建,智能 Modal 三档分类清理
|
||||
|
||||
业务人员**误触发批量生成**(例如点了一次"生成 5 月物业费"后忘了又点了一次,导致每户有两张同样账单),需要清理。走 `BulkDeleteBillsAction` 的智能 Modal,自动按状态分三档处理。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 王主管 5 月 1 日上午点了"生成 5 月物业费"批量(已生成 300 张)。下午接电话被打断,**忘了之前已经生成**,又点了一次 → 系统按 `SkipExisting` 策略**全部跳过**(因为已存在)。
|
||||
>
|
||||
> **但**:王主管 Modal 没看清,选了 `Replace` 策略 → 系统 **作废原 300 张 + 重新生成 300 张** → 同业户有重复账单(状态 Void 的旧账单 + Unpaid 的新账单)。
|
||||
>
|
||||
> 业务人员发现后需清理 300 张 Void 状态的旧账单。
|
||||
|
||||
或者:
|
||||
|
||||
> [!example] 真实情境(更典型)
|
||||
> 业务人员手工建了 100 张维修分摊账单,分摊金额算错,需要全部清理重建。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:筛选要清理的账单
|
||||
|
||||
后台 → 账单 → 列表 → 用过滤(按期次 / 状态 / 费用类型 / 生成时间)选出要清理的批。
|
||||
|
||||
例:5 月物业费 + Void 状态 + 创建时间 today → 300 张 Void 旧账单。
|
||||
|
||||
### 第 2 步:选中
|
||||
|
||||
Table 上**勾选**这 300 张(全选 / 多选)。
|
||||
|
||||
### 第 3 步:触发批量删除
|
||||
|
||||
顶部 **"批量删除"** 按钮(`BulkDeleteBillsAction`)。
|
||||
|
||||
> [!warning] 权限守护
|
||||
> 需要 `bill.bulkDelete` **独立高敏权限**(`Policy::deleteAny`)。普通业务人员可能没此权限,需主管 / 财务总监操作。
|
||||
>
|
||||
> 详见 [[smart-bulk-delete-design]]"权限设计"段。
|
||||
|
||||
### 第 4 步:智能 Modal(关键)
|
||||
|
||||
Modal 自动**预检查**所选账单 + 分三档:
|
||||
|
||||
```
|
||||
批量删除账单 (选中 300 张)
|
||||
|
||||
预检查统计:
|
||||
✅ 可删: 200 张 (Unpaid + 无付款关联)
|
||||
⚠️ 需作废: 50 张 (Void 已经 / Partial 已经 = 状态)
|
||||
❌ 跳过: 50 张 (Paid 已付 / 其他异常)
|
||||
|
||||
请选择处理模式:
|
||||
⚪ 仅删除可删的(200 张物理删,100 张跳过)
|
||||
⚫ 删可删 + 作废需作废的(200 删 + 50 作废,50 跳过)
|
||||
|
||||
批量操作原因(必填,审计留痕):
|
||||
[多行输入框:
|
||||
5 月 1 日批量生成时误选 Replace 策略,
|
||||
导致 300 张旧账单 Void。现批量清理。
|
||||
]
|
||||
|
||||
⚠️ 注意:
|
||||
- 物理删除不可恢复(但 activitylog 保留 bill_no)
|
||||
- 作废不可撤销
|
||||
- 跳过的账单不动
|
||||
|
||||
[取消] [确认执行]
|
||||
```
|
||||
|
||||
### 第 5 步:选模式 + 提交
|
||||
|
||||
业务人员看到统计,做决定:
|
||||
|
||||
| 选 | 业务效果 |
|
||||
|---|---|
|
||||
| 仅删可删的 | 200 物理删,100 暂留(包括 50 已作废的 + 50 跳过的)|
|
||||
| 删可删 + 作废需作废 | 200 物理删,50 翻 Void(变作废),50 跳过 |
|
||||
|
||||
填原因 → 提交。
|
||||
|
||||
### 第 6 步:看执行结果
|
||||
|
||||
系统返回:
|
||||
|
||||
```
|
||||
批量操作完成:
|
||||
- 物理删除:200 张
|
||||
- 作废:50 张(若选 DeleteAndVoid)
|
||||
- 跳过:50 张
|
||||
|
||||
详细 bill_no 见 activitylog(本次操作)。
|
||||
```
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Modal
|
||||
participant Action[BulkDeleteBillsAction 业务层]
|
||||
participant Activity
|
||||
participant DB
|
||||
|
||||
业务->>Filament: BillsList → 选 300 张 → 批量删除
|
||||
Filament->>Modal: 预检查(分类三档)
|
||||
Modal->>Filament: 显示 ✅200 ⚠️50 ❌50
|
||||
|
||||
业务->>Modal: 选模式 + 填原因 + 提交
|
||||
Modal->>Action: handle(bills, mode, reason, user)
|
||||
|
||||
Action->>Action: 再次分类(防 UI 缓存过期)
|
||||
|
||||
Action->>DB: 开启事务
|
||||
|
||||
loop 200 可删
|
||||
Action->>DB: bill.delete()
|
||||
end
|
||||
|
||||
alt mode = DeleteAndVoid
|
||||
loop 50 可作废
|
||||
Action->>Action: VoidBillAction.handle(bill, reason, user)
|
||||
Action->>Activity: log voided(单条)
|
||||
end
|
||||
end
|
||||
|
||||
Action->>Activity: log bulk_deleted(总体 + affected_bill_nos 数组)
|
||||
Action->>DB: 提交
|
||||
|
||||
Action-->>Filament: 结果 + 通知
|
||||
Filament-->>业务: 显示"200 删 / 50 作废 / 50 跳过"
|
||||
```
|
||||
|
||||
## activitylog 设计
|
||||
|
||||
详见 [[smart-bulk-delete-design]]"activitylog 设计"段。
|
||||
|
||||
**单条 bulk_deleted log**(批量操作只一条):
|
||||
|
||||
```json
|
||||
{
|
||||
"log_name": "default",
|
||||
"subject_type": null,
|
||||
"subject_id": null,
|
||||
"event": "bulk_deleted",
|
||||
"causer_id": 42, // 王主管 ID
|
||||
"properties": {
|
||||
"mode": "DeleteAndVoid",
|
||||
"reason": "5 月 1 日批量生成时误选 Replace,300 张 Void 清理",
|
||||
"total_selected": 300,
|
||||
"deleted_count": 200,
|
||||
"voided_count": 50,
|
||||
"blocked_count": 50,
|
||||
"affected_bill_nos": [
|
||||
"B-202605-501-001 [DELETED]",
|
||||
"B-202605-502-001 [DELETED]",
|
||||
...
|
||||
"B-202605-501-002 [VOIDED]",
|
||||
...
|
||||
"B-202605-501-003 [SKIPPED]",
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`affected_bill_nos` 是数组,每条标记处理结果。事后可完整还原。
|
||||
|
||||
**同时,每张被作废的 Bill 还各有一条 voided log**(VoidBillAction 内部触发,subject=bill)。
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**通常不感知**:
|
||||
|
||||
- 被物理删的 200 张:业户未付款,通常未通知 → 无感
|
||||
- 被作废的 50 张:业户**可能**收到通知(看物业策略),或无感
|
||||
- 跳过的 50 张:不动,不影响
|
||||
|
||||
**理想**:批量误建及时清理 → 业户全程无感 → 物业体面化解错误。
|
||||
|
||||
**不理想**:清理前业户已经收到推送 + 已付款 → 跳过(Paid 不可删 / 不可作废)→ 需要走单独的"已付作废 + 退款"流程([[void-paid-bill]])。
|
||||
|
||||
## 与单删 / 单作废的对比
|
||||
|
||||
| 维度 | [[delete-bill-unpaid|单删]] | [[void-paid-bill|单作废]] | **智能批删(本)** |
|
||||
|---|---|---|---|
|
||||
| 一次操作多少张 | 1 | 1 | **多张(N)** |
|
||||
| 权限 | `bill.delete` | `bill.void` | **`bill.bulkDelete`(独立高敏)** |
|
||||
| 处理混合状态 | 无(只删 1 张)| 无 | **✅ 三档分类自动** |
|
||||
| activitylog | 1 条单条 | 1 条单条 | **1 条总体 + N 条单条** |
|
||||
| 业务人员效率 | 低(逐张)| 低 | **高(一次)** |
|
||||
| 业务人员风险 | 低 | 低 | **中(影响面大)** |
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 批删的"跳过"档具体包括哪些?
|
||||
> `canBeDeleted=false && canBeVoided=false`:
|
||||
>
|
||||
> - **Paid**(已付清,无法走任何"消除"路径,需走专门退款流程)
|
||||
> - **Void**(已经作废,无需重复)
|
||||
> - **Processing**(罕见,处理中,等回调)
|
||||
>
|
||||
> 跳过的账单不动,业务人员需单独处理。
|
||||
|
||||
> [!question] 实际执行时与 Modal 预检查不一致(其他人在中间改了)?
|
||||
> Action 内部**再次分类**(防 UI 缓存过期),实际按最新状态分。可能与 Modal 显示统计**略有差异**(但极少)。
|
||||
|
||||
> [!question] 批删后想撤销?
|
||||
> - **物理删的**:不可恢复(数据没了)
|
||||
> - **作废的**:不可撤销(Void 终态)
|
||||
> - 唯一办法:重新创建(走 [[create-periodic-property-fee]] 或 [[create-single-bill-manual]])
|
||||
>
|
||||
> 信息可从 activitylog 还原(bill_no / amount 等)。
|
||||
|
||||
> [!question] 批删的影响面太大会有审批吗?
|
||||
> 系统层面**无审批流**。靠**权限控制**(只主管 / 财务总监能批删)+ **必填原因**(留审计)+ **Modal 预检查统计**(让操作者看到影响面后决策)。
|
||||
>
|
||||
> 业务上若需"超过 100 张需双签",需扩展加审批 workflow。
|
||||
|
||||
> [!question] 批删失败怎么办?
|
||||
> 事务内执行,某张失败 → 整批回滚。事务边界是**全成功或全失败**。
|
||||
>
|
||||
> 例外:`Action` 实现可能优化为"个别失败不影响其他",但破坏原子性 → 通常不推荐。
|
||||
|
||||
> [!question] activitylog 表太多 bulk_deleted 怎么管?
|
||||
> 每次批删一条 log(总体 + affected_bill_nos)。即使 100 次批删 = 100 条 log。**不会爆表**。
|
||||
>
|
||||
> 但 voided 的子 log(每张作废一条) = 大量。批删 50 张作废 = 50 条 voided log。归档策略详见 [[smart-bulk-delete-design]]。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 单张删 → [[delete-bill-unpaid]]
|
||||
- 单张作废 → [[void-paid-bill]]
|
||||
- 跳过的 Paid 账单要消除 → [[void-paid-bill|Paid 作废 + 退款]](当前手工)
|
||||
- 批删后想审计 → [[audit-activitylog-trace]]
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[smart-bulk-delete-design]](核心概念)
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[delete-bill-unpaid]]
|
||||
- [[void-paid-bill]]
|
||||
- [[bill-six-state-machine]]
|
||||
- [[audit-activitylog-trace]]
|
||||
257
prop-acc/scenarios/billing/collect-payment-batch.md
Normal file
257
prop-acc/scenarios/billing/collect-payment-batch.md
Normal file
@@ -0,0 +1,257 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 同业户多账单批量收款
|
||||
aliases:
|
||||
- 批量收款
|
||||
- BatchCollectPaymentAction
|
||||
- 一次付多张
|
||||
- collect-payment-batch
|
||||
- 场景-同业户批量收款
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 收款
|
||||
audience:
|
||||
- 业户
|
||||
- 业务人员
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:同业户多账单批量收款
|
||||
|
||||
业户**本月有多张账单**(物业费 + 水电气费 + 其他),想**一次性付清**。业务人员走 `BatchCollectPaymentAction`,**一笔 CollectionOrder 关联多张 Bill**(走 CollectionOrderBill 多对多)。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 张阿姨本月有 4 张账单:
|
||||
>
|
||||
> | 账单 | 金额 |
|
||||
> |---|---|
|
||||
> | 5 月物业费 | ¥800 |
|
||||
> | 5 月水费 | ¥54 |
|
||||
> | 5 月电费 | ¥168 |
|
||||
> | 5 月燃气 | ¥30 |
|
||||
> | **合计** | **¥1,052** |
|
||||
>
|
||||
> 张阿姨到前台:"4 张账单我一次性付清,微信扫码"。业务人员**1 笔操作**完成 4 张账单收款。
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 第 1 步:告诉业务人员要付哪些
|
||||
|
||||
> "我把本月 4 张账单都付了,微信扫码"
|
||||
|
||||
### 第 2 步:确认金额
|
||||
|
||||
业务人员说:"4 张账单合计 ¥1,052,微信扫这个码"。
|
||||
|
||||
### 第 3 步:微信付
|
||||
|
||||
业户扫码 → 输密码 / 指纹 → 付 ¥1,052。
|
||||
|
||||
### 第 4 步:拿收据
|
||||
|
||||
可能是:
|
||||
|
||||
- 一张 Receipt 含 4 行明细(物业费 ¥800 / 水费 ¥54 / ...)
|
||||
- 或 4 张独立 Receipt(每张账单一张)
|
||||
|
||||
具体看实现。**业户体验上前者更好**(一张收据看全部)。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:找业户
|
||||
|
||||
后台 → 业户 → 找到张阿姨 → "她的账单"标签 → 看到 4 张 Unpaid。
|
||||
|
||||
或:后台 → 账单 → 列表 → 过滤业户=张阿姨 + 状态=Unpaid。
|
||||
|
||||
### 第 2 步:选中多张账单
|
||||
|
||||
Table 上勾选 4 张账单 → 顶部 **"批量收款"** 按钮(`BatchCollectPaymentAction`)。
|
||||
|
||||
或:后台 → 业户视图 → "批量收款"(若 UI 支持单业户聚合)。
|
||||
|
||||
### 第 3 步:Modal 表单
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **选中账单数** | 4 张(显示)|
|
||||
| **合计金额** | ¥1,052(自动算)|
|
||||
| **支付方式** | 微信 |
|
||||
| **收款银行账户** | 物业微信账户 |
|
||||
| **备注** | 选填,如 "业户本月全套缴" |
|
||||
|
||||
### 第 4 步:提交
|
||||
|
||||
系统在**一个事务**内:
|
||||
|
||||
1. 校验每张 Bill 可付(`canBePaid()`)
|
||||
2. 建 **1 个 CollectionOrder**(`type=Bill`,`actual_amount=+1052`,`status=Completed`)
|
||||
3. 建 **4 个 CollectionOrderBill**(每张账单一个,各自 `allocated_amount` = 该账单金额)
|
||||
4. 4 张 Bill 各自 `paid_amount = amount`,`status = Paid`
|
||||
5. 触发 `CollectionOrderCompleted` 事件
|
||||
6. Listener 建 Receipt(可能含 4 个 line_items)
|
||||
|
||||
### 第 5 步:给收据
|
||||
|
||||
后台找到 Receipt → 打印 / 微信发。
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业户
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Action[BatchCollectPaymentAction]
|
||||
participant DB
|
||||
|
||||
业务->>Filament: BillsList → 选 4 张 → 批量收款
|
||||
Filament->>Action: handle(bills, channel, bank)
|
||||
Action->>Action: 每张 Bill 校验 canBePaid
|
||||
|
||||
Action->>DB: 开启事务
|
||||
Action->>DB: 1. 建 CollectionOrder(+1052, Completed)
|
||||
|
||||
loop 每张 Bill
|
||||
Action->>DB: 2. 建 CollectionOrderBill(allocated=bill.amount)
|
||||
Action->>DB: 3. Bill.paid_amount = amount, status=Paid
|
||||
end
|
||||
|
||||
Action->>Listener: 4. 触发 CollectionOrderCompleted
|
||||
Listener->>DB: 5. 建 Receipt(line_items × 4)
|
||||
Action->>DB: 提交事务
|
||||
|
||||
Filament-->>业务: 成功通知
|
||||
业务-->>业户: 收据(含 4 项明细)
|
||||
```
|
||||
|
||||
## 数据示例
|
||||
|
||||
收款后:
|
||||
|
||||
### CollectionOrder(1 条)
|
||||
|
||||
```
|
||||
id: 67890
|
||||
collection_type: Bill
|
||||
actual_amount: +1052
|
||||
payment_channel: 微信
|
||||
status: Completed
|
||||
meta.fund_source: external
|
||||
```
|
||||
|
||||
### CollectionOrderBill(4 条)
|
||||
|
||||
| collection_order_id | bill_id | allocated_amount |
|
||||
|---|---|---|
|
||||
| 67890 | 物业费 Bill | 800 |
|
||||
| 67890 | 水费 Bill | 54 |
|
||||
| 67890 | 电费 Bill | 168 |
|
||||
| 67890 | 燃气 Bill | 30 |
|
||||
|
||||
### Bill(4 条更新)
|
||||
|
||||
每张 paid_amount = amount,status = Paid。
|
||||
|
||||
### Receipt(1 条,4 行明细)
|
||||
|
||||
```
|
||||
collection_order_id: 67890
|
||||
amount: +1052
|
||||
line_items: [
|
||||
{ 物业费(5月), 800 },
|
||||
{ 水费(5月), 54 },
|
||||
{ 电费(5月), 168 },
|
||||
{ 燃气(5月), 30 },
|
||||
]
|
||||
```
|
||||
|
||||
## 与单张收款的对比
|
||||
|
||||
| 维度 | [[collect-payment-single|单张]] | **批量(本场景)** |
|
||||
|---|---|---|
|
||||
| Modal 选账单 | 1 张(从 ViewBill 进入)| **多张**(Table 勾选)|
|
||||
| CollectionOrder | 1 个 | 1 个(共用)|
|
||||
| CollectionOrderBill | 1 个 | **N 个**(每张一个)|
|
||||
| 业务人员操作 | 单笔 | 一笔 |
|
||||
| 业户体验 | 一张一张付(慢)| 一次付清(快)|
|
||||
|
||||
**批量收款的好处**:业户体验更好(一次付),业务人员工作量更小。**唯一前提**:业户愿意一次付清。
|
||||
|
||||
## 不同支付方式的分摊
|
||||
|
||||
业户支付的钱**自动按账单原金额比例分摊** 到各 Bill。不需要业务人员手动分配。
|
||||
|
||||
例:¥1,052 微信付 → 4 个 CollectionOrderBill 各自 allocated_amount = 该账单 amount(全额分配)。
|
||||
|
||||
### 如果业户付不够(部分批量付)
|
||||
|
||||
业户只想付 ¥600(不够 ¥1,052)→ 业务人员有几种选择:
|
||||
|
||||
| 策略 | 操作 |
|
||||
|---|---|
|
||||
| **优先付物业费**(默认?)| ¥600 全部分配给物业费 Bill(部分付)|
|
||||
| **按比例分摊** | 600 × 800/1052 = 456 给物业费,600 × 54/1052 = 31 给水费, ... |
|
||||
| **业务人员手动决定** | 给业务人员选哪张账单付多少 |
|
||||
|
||||
当前实现的具体策略看代码。**业务上建议优先付到期早的**(避免逾期)。
|
||||
|
||||
> [!info] 批量收款的"部分付"复杂度
|
||||
> 上述场景比单张部分付更复杂。当前 `BatchCollectPaymentAction` 可能**只支持全额批量**(若金额够付所有选中账单 → 全付;不够 → 走单张部分付逐张操作)。看实现。
|
||||
|
||||
## 业务人员视角:Modal 的预检查
|
||||
|
||||
类似 [[smart-bulk-delete-design|智能批删]] 的预检查思路:
|
||||
|
||||
- 选中 4 张账单 → Modal 显示"总计 ¥1,052"
|
||||
- 选中包含已 Paid 的账单 → Modal 显示"4 选 + 1 已付跳过"
|
||||
- 选中包含 Suspended → Modal 提示 "该账单挂起,无法收款"
|
||||
|
||||
> [!info] 当前实现的成熟度
|
||||
> `BatchCollectPaymentAction` 的智能 Modal 程度看实现。可能比 `BulkDeleteBillsAction` 简单(批删有 issue.md Q6 详细设计,批收款未单独描述)。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 选中跨业户的多张账单能批量收款吗?
|
||||
> 业务上**不应该**(不同业户付的钱不能混)。系统层面应**校验同业户**,否则拒绝。
|
||||
>
|
||||
> 例外:**家属代付**场景(儿子来付父母账单)→ 业务上算同业户的钱。
|
||||
|
||||
> [!question] 批量收款失败一半怎么办?
|
||||
> 事务内**全成功或全失败**。任一 Bill 校验失败(例如某张已 Paid)→ 整笔回滚 → 业户的钱不被收。
|
||||
>
|
||||
> 实施上可能优化为"部分成功"(只失败的跳过),但破坏事务原子性,通常不推荐。
|
||||
|
||||
> [!question] 业户支付的钱比账单合计**多**?
|
||||
> Modal 守护应限制金额 ≤ 合计。如果业务上业户故意多给:
|
||||
>
|
||||
> - 找零给业户(系统层面只收账单的金额)
|
||||
> - **或转入业户预存款账户**(若有此自动逻辑)
|
||||
|
||||
> [!question] 不同期次的账单能一起付吗?
|
||||
> 可以(账单状态都是 Unpaid 即可)。例如付 4 月物业费 + 5 月物业费 + 5 月水电气。
|
||||
|
||||
> [!question] 批量收款的 activitylog 怎么记?
|
||||
> 一条 CollectionOrder 的 activitylog(event=created)+ 每张 Bill 状态变化的 log。可在 SQL 反查 affected_bill_ids。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 单张付 → [[collect-payment-single]]
|
||||
- 部分付 → [[exception-partial-payment]]
|
||||
- 预存款抵 → [[collect-via-prepaid-auto]]
|
||||
- 业户全付不起,挑某张付 → 走 [[collect-payment-single]] 逐张
|
||||
- 收错了想撤 → [[void-paid-bill]](已付作废 + 退款)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[collect-payment-single]]
|
||||
- [[collect-via-prepaid-auto]]
|
||||
- [[exception-partial-payment]]
|
||||
- [[../prepaid/auto-deduction-design]]
|
||||
258
prop-acc/scenarios/billing/collect-payment-single.md
Normal file
258
prop-acc/scenarios/billing/collect-payment-single.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 单张账单收款
|
||||
aliases:
|
||||
- 单张收款
|
||||
- 收款
|
||||
- CollectPaymentAction
|
||||
- collect-payment-single
|
||||
- 场景-单张账单收款
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 收款
|
||||
audience:
|
||||
- 业户
|
||||
- 业务人员
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:单张账单收款
|
||||
|
||||
业户**单张账单付款**(物业费 / 水费 / 电费的某一张),业务人员后台触发 `CollectPaymentAction`。最基础高频的收款场景。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 张阿姨 5 月物业费 ¥800 账单已生成,她下午到物业前台:
|
||||
>
|
||||
> - "我交 5 月物业费"
|
||||
> - 业务人员小李打开张阿姨账户 → 找到 5 月物业费账单 → 收款
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 第 1 步:到前台 / 小程序
|
||||
|
||||
带:
|
||||
|
||||
- 钱(现金 / 微信 / POS 卡)
|
||||
- 房号 / 姓名(身份证)
|
||||
|
||||
### 第 2 步:告诉业务人员要付哪张账单
|
||||
|
||||
> "我交 5 月物业费"
|
||||
|
||||
业务人员从系统找到对应账单。
|
||||
|
||||
### 第 3 步:确认金额 + 付款方式
|
||||
|
||||
业务人员告诉张阿姨:
|
||||
|
||||
> "您 5 月物业费 ¥800,请选付款方式"
|
||||
|
||||
| 付款方式 | 操作 |
|
||||
|---|---|
|
||||
| 现金 | 给钱 → 找零 |
|
||||
| 微信扫码 | 业务人员出示物业收款码 → 业户扫 |
|
||||
| POS 刷卡 | 业户给银行卡 → POS 机刷 |
|
||||
| 银行转账 | 业户给凭证(线下转账已到账)|
|
||||
|
||||
### 第 4 步:拿收据
|
||||
|
||||
- 纸质:**当场打印**
|
||||
- 电子:发到业户微信 / 邮箱
|
||||
|
||||
> [!success] 完成
|
||||
> 账单状态从 Unpaid → Paid。业户带收据离开,5 分钟内搞定。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:找账单
|
||||
|
||||
后台 → 账单 → 列表 → 按业户姓名 / 房号 / 期次过滤 → 找到张阿姨的 5 月物业费(Unpaid,¥800)→ 进 `ViewBill`。
|
||||
|
||||
或者:
|
||||
|
||||
- 后台 → 业户 → 找到张阿姨 → "她的账单"标签 → 看 Unpaid 列表 → 选
|
||||
|
||||
### 第 2 步:点击 `CollectPaymentAction`(标签"收款")
|
||||
|
||||
右上角状态管理组。
|
||||
|
||||
> [!warning] 按钮可见性
|
||||
> `CollectPaymentAction` 守护:`canBePaid()`(Unpaid / Partial)+ `->authorize('collect')`。Paid / Void / Suspended / Processing 灰化。
|
||||
|
||||
Modal 表单:
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **收款金额** | ¥800(默认全额,可改为部分)|
|
||||
| **支付方式(`payment_channel_id`)** | 现金 / 微信 / POS / 银行转账 |
|
||||
| **收款银行账户(`bank_account_id`)** | 微信/POS/转账选对应银行账户;现金可空 |
|
||||
| **收款备注** | 选填,如"业户现场付款" |
|
||||
|
||||
### 第 3 步:提交
|
||||
|
||||
系统在**一个事务**内:
|
||||
|
||||
1. 校验 Bill 可付(`canBePaid` = Unpaid / Partial)
|
||||
2. 校验金额 ≤ Bill 剩余应付(`amount - paid_amount`)
|
||||
3. 建 `CollectionOrder`(`type=Bill`,`actual_amount=+800`,`status=Completed`,`payment_channel`,`meta.fund_source=external`)
|
||||
4. 建 `CollectionOrderBill`(`bill_id`,`collection_order_id`,`allocated_amount=800`)
|
||||
5. 更新 `Bill.paid_amount += 800`
|
||||
6. 更新 `Bill.status`:若 paid_amount = amount → `Paid`;若 < amount → `Partial`
|
||||
7. 触发 `CollectionOrderCompleted` 事件
|
||||
8. Listener 自动建 `Receipt` + `ReceiptItem`(文案"物业费 ¥800")
|
||||
9. 写 activitylog(可选,具体看实现)
|
||||
|
||||
### 第 4 步:给业户收据
|
||||
|
||||
后台找到新生成 Receipt → 打印 / 微信发。
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业户
|
||||
participant 业务[业务人员]
|
||||
participant Filament
|
||||
participant Action[CollectPaymentAction]
|
||||
participant DB
|
||||
participant Listener
|
||||
|
||||
业户->>业务: 付物业费 800
|
||||
业务->>Filament: ViewBill → CollectPaymentAction(modal)
|
||||
Filament->>Action: handle(bill, 800, channel, bank)
|
||||
Action->>Action: canBePaid()? Unpaid=true
|
||||
Action->>Action: 800 ≤ remaining(800)? yes
|
||||
|
||||
Action->>DB: 开启事务
|
||||
Action->>DB: 1. 建 CollectionOrder(type=Bill, +800, Completed)
|
||||
Action->>DB: 2. 建 CollectionOrderBill(allocated=800)
|
||||
Action->>DB: 3. Bill.paid_amount=800
|
||||
Action->>DB: 4. Bill.status=Paid(800=800)
|
||||
Action->>Listener: 5. 触发 CollectionOrderCompleted
|
||||
Listener->>DB: 6. 建 Receipt("物业费 ¥800")
|
||||
Action->>DB: 提交事务
|
||||
|
||||
Filament-->>业务: 成功通知
|
||||
业务-->>业户: 收据
|
||||
```
|
||||
|
||||
## 部分付场景(business 上常见)
|
||||
|
||||
业户只想付 ¥300(全额 ¥800):
|
||||
|
||||
```
|
||||
Modal 表单:
|
||||
- 收款金额:300(手动改)
|
||||
- 支付方式:现金
|
||||
- 备注:"暂时只能付一部分"
|
||||
```
|
||||
|
||||
提交后:
|
||||
|
||||
- Bill.paid_amount = 300
|
||||
- Bill.status: Unpaid → **Partial**
|
||||
- 建 CollectionOrder(+300)+ CollectionOrderBill(allocated=300)
|
||||
|
||||
业户后续补付 ¥500 → 同样走 `CollectPaymentAction` → 第 2 笔 CollectionOrderBill → Bill 收齐 → Paid。
|
||||
|
||||
详见 [[exception-partial-payment]]。
|
||||
|
||||
## 数据示例(完整流水)
|
||||
|
||||
业户付 ¥800 后:
|
||||
|
||||
### Bill 表
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `id` | 12345 |
|
||||
| `amount` | 800 |
|
||||
| `paid_amount` | 800 |
|
||||
| `status` | Paid |
|
||||
|
||||
### CollectionOrderBill 表
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `bill_id` | 12345 |
|
||||
| `collection_order_id` | 67890 |
|
||||
| `allocated_amount` | 800 |
|
||||
|
||||
### CollectionOrder 表
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `id` | 67890 |
|
||||
| `collection_type` | Bill |
|
||||
| `actual_amount` | +800 |
|
||||
| `payment_channel_id` | 微信 |
|
||||
| `status` | Completed |
|
||||
| `meta.fund_source` | external |
|
||||
|
||||
### Receipt 表
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `collection_order_id` | 67890 |
|
||||
| `amount` | +800 |
|
||||
| line_items | [{ 物业费(5月), 800 }] |
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 业户付的钱与账单金额不一致(多付 / 少付)?
|
||||
> | 情况 | 处置 |
|
||||
> |---|---|
|
||||
> | 少付 | 走部分付 Partial 状态;后续补付 |
|
||||
> | **多付** | **不应该**(Modal 守护 `amount ≤ remaining`)。如果业务上业户给的钱多,**找零给业户**(系统层面只收账单的金额) |
|
||||
|
||||
> [!question] 业户付错账单(本想付物业费,付到电费)?
|
||||
> 业务人员可:
|
||||
> - 立即作废这笔 CollectionOrder(走 [[void-paid-bill]] 类似流程)+ 重新对正确账单收款
|
||||
> - 或者**留这笔不动**(资金确实到账)+ 业务人员手工调整 CollectionOrderBill 的 allocated_amount(危险,会破坏审计)
|
||||
>
|
||||
> **推荐第一种**(走作废 + 重收)。
|
||||
|
||||
> [!question] Frozen Bill 能收款吗?
|
||||
> 不能。`canBePaid()` 只允许 Unpaid / Partial。Suspended 状态需先 [[resume-bill|恢复]]。
|
||||
|
||||
> [!question] 已付的 Bill 能再收款吗?
|
||||
> 不能。`canBePaid()` Paid 时返 false。理论上 Bill 已经付清。
|
||||
|
||||
> [!question] 业户预存款够付,业务人员怎么操作?
|
||||
> 看自动 / 手动:
|
||||
> - 自动(待补的 [[../prepaid/auto-deduction-design|预存款自动抵扣 job]]):业务人员不操作,系统自动
|
||||
> - 手动:业务人员在 `ViewBill` → 走 `CollectPaymentAction` 选"预存款抵扣"(或专用 Action)→ 详见 [[collect-via-prepaid-auto]]
|
||||
|
||||
> [!question] 收款时 PaymentChannel 写错(选了微信实际是现金)?
|
||||
> CollectionOrder 一经创建**通常不可改**字段。错了:
|
||||
> - 走作废(详见 [[void-paid-bill]])+ 重新建
|
||||
> - 或 tinker 改字段(运维 + 留审计)
|
||||
>
|
||||
> 预防:Modal 提交前再三确认。
|
||||
|
||||
> [!question] activitylog 记什么?
|
||||
> 详见 [[smart-bulk-delete-design]] activitylog 设计。CollectPayment 通常也记一条 activitylog:event=collected,properties 含 amount / payment_channel / receipt_id。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 部分付场景 → [[exception-partial-payment]]
|
||||
- 批量付(多张账单一起付)→ [[collect-payment-batch]]
|
||||
- 预存款抵 → [[collect-via-prepaid-auto]]
|
||||
- 收款错了想撤 → [[void-paid-bill]]
|
||||
- Bill 挂起中无法收 → [[resume-bill]]
|
||||
- 逾期账单催收 → [[exception-overdue-bills]]
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[exception-partial-payment]]
|
||||
- [[collect-payment-batch]]
|
||||
- [[collect-via-prepaid-auto]]
|
||||
- [[../adhoc/collection-order-and-receipt]]
|
||||
267
prop-acc/scenarios/billing/collect-via-prepaid-auto.md
Normal file
267
prop-acc/scenarios/billing/collect-via-prepaid-auto.md
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 预存款抵扣自动收款
|
||||
aliases:
|
||||
- 预存款抵账单
|
||||
- 自动抵扣收款
|
||||
- collect-via-prepaid-auto
|
||||
- 场景-预存款自动抵扣
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 收款
|
||||
- 跨子模块
|
||||
audience:
|
||||
- 业户
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:预存款抵扣自动收款
|
||||
|
||||
业户**预存款余额够付账单**,系统(理想是自动 job,当前是业务人员手动触发)走 `ConsumeFromPrepaidAccountAction` 抵扣账单。这是 billing × prepaid **两子模块联动**的核心场景。
|
||||
|
||||
> [!info] 本场景跨两个模块
|
||||
> - **billing 视角**:Bill 从 Unpaid → Paid,看似与普通收款一样
|
||||
> - **prepaid 视角**:PrepaidAccount.balance 减,走 consume 流水
|
||||
>
|
||||
> 详见 [[../prepaid/consume-monthly-property-bill]] 完整流程。本场景从 billing 角度补充。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 张阿姨预存款账户余额 ¥3,400,5 月物业费 ¥800。
|
||||
>
|
||||
> 手动模式:王主管在张阿姨预存款账户上点 `ConsumeAction`,选 5 月物业费 → 抵扣完成 → 账单 Paid。
|
||||
>
|
||||
> 自动模式(待补 [[../prepaid/auto-deduction-design]]):月初 1 日凌晨 job 自动跑 → 张阿姨账单当月自动扣 → 早晨业户收到推送"5 月物业费 ¥800 已抵扣,余额 ¥2,600"。
|
||||
|
||||
## billing 视角
|
||||
|
||||
### Bill 的变化
|
||||
|
||||
| 字段 | 变化前 | 变化后 |
|
||||
|---|---|---|
|
||||
| `status` | Unpaid | Paid |
|
||||
| `paid_amount` | 0 | 800 |
|
||||
| 关联的 CollectionOrderBill | 0 个 | 1 个(allocated=800)|
|
||||
| 关联的 CollectionOrder | 0 个 | 1 个(type=Bill, meta.fund_source=prepaid)|
|
||||
| 关联的 Receipt | 0 个 | 1 个("物业费 ¥800") |
|
||||
|
||||
**与普通收款的唯一差异**:CollectionOrder 的 `meta.fund_source = 'prepaid'`(而非默认 `external`)。
|
||||
|
||||
### CollectionOrder 的特殊性
|
||||
|
||||
```yaml
|
||||
id: 67890
|
||||
collection_type: Bill # 仍是 Bill 视角(不是 Prepaid)
|
||||
actual_amount: +800 # 正数
|
||||
payment_channel: null # 不走外部支付渠道
|
||||
status: Completed
|
||||
meta:
|
||||
fund_source: prepaid # 标资金来源
|
||||
prepaid_account_id: 123
|
||||
prepaid_transaction_id: 456
|
||||
```
|
||||
|
||||
详见 [[../prepaid/consume-via-bill-collection-type]]"CollectionOrder.type=Bill 设计"段。
|
||||
|
||||
### Receipt 文案
|
||||
|
||||
与现金 / 微信付的收据**长一样**:
|
||||
|
||||
```
|
||||
物业费(5月)¥800
|
||||
```
|
||||
|
||||
业户**感知不到**资金来源差异。这是有意设计 —— "业户付清账单"的统一感受。
|
||||
|
||||
## prepaid 视角(简述)
|
||||
|
||||
详见 [[../prepaid/consume-monthly-property-bill]] 完整流程。
|
||||
|
||||
- 校验账户 `canOperate()`(Active)
|
||||
- 校验余额够付(`balance >= 800`)
|
||||
- 校验跨社区(Bill 与 Account 同 community)
|
||||
- 建 `PrepaidTransaction(type=consume, amount=800, related_bill_id, balance 3400→2600)`
|
||||
- 更新 `PrepaidAccount.balance = 2600`
|
||||
- 同步 Bill 状态翻 Paid(通过 CollectionOrder + CollectionOrderBill)
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 您会感受到什么
|
||||
|
||||
| 模式 | 感知 |
|
||||
|---|---|
|
||||
| **自动(待补)** | 月初推送"5 月物业费 ¥800 已自动从预存款扣,余额 ¥2,600" |
|
||||
| **手动** | 同上,只是触发时间不固定(看业务人员何时操作)|
|
||||
|
||||
### 与现金付的差异
|
||||
|
||||
| 维度 | 现金付 | **预存款抵** |
|
||||
|---|---|---|
|
||||
| 业户操作 | 到前台 + 给钱 | **无操作**(自动) |
|
||||
| 业户感知 | "我付了" | **"已抵扣"**(被动) |
|
||||
| Receipt | "物业费 ¥800" | **"物业费 ¥800"**(相同) |
|
||||
| 业务人员介入 | 多(收钱 + 录入) | **无**(自动 job)/ 中(手动) |
|
||||
| 时长 | 业户上门 + 5 分钟办理 | 0 秒(自动)/ 5 分钟(手动) |
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 手动模式
|
||||
|
||||
业务人员在 `ViewPrepaidAccount`(预存款账户详情页):
|
||||
|
||||
1. 看到张阿姨账户余额 ¥3,400
|
||||
2. 点击 `ConsumeAction`(预存款上的)
|
||||
3. Modal 选 Bill = "5 月物业费 ¥800"
|
||||
4. 提交 → 系统自动跑完整链路
|
||||
|
||||
> [!info] 这个 Action 不在 billing 模块
|
||||
> `ConsumeAction` 是 prepaid 模块的 Filament Action,在 `PrepaidAccounts/Actions/`。billing 模块的 `CollectPaymentAction` 是普通收款用的(走外部 PaymentChannel)。两者**协作完成**预存款抵扣场景。
|
||||
>
|
||||
> 详见 [[../prepaid/consume-monthly-property-bill]]"业务人员视角"。
|
||||
|
||||
### 自动模式(待补)
|
||||
|
||||
业务人员**几乎不操作**(产品价值的最大体现)。月初看 dashboard 报告:
|
||||
|
||||
```
|
||||
2026 年 6 月 1 日 PrepaidAutoDeductionJob 报告
|
||||
- 候选账户:500
|
||||
- 全抵成功:380(76%)
|
||||
- 部分抵 / 跳过:80(16%)
|
||||
- 账户冻结跳过:8(2%)
|
||||
- 失败:0
|
||||
```
|
||||
|
||||
逐个跟进失败 / 跳过的(走 [[../prepaid/audit-low-balance-and-overdue|低余额预警]] 等)。
|
||||
|
||||
## 系统流程(手动)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant ConsumeAction[Prepaid 的 ConsumeAction]
|
||||
participant ConsumeFromPrepaid[ConsumeFromPrepaidAccountAction]
|
||||
participant Bill
|
||||
participant PrepaidAccount
|
||||
participant DB
|
||||
participant Listener
|
||||
|
||||
业务->>Filament: ViewPrepaidAccount → ConsumeAction(选 Bill, 800)
|
||||
Filament->>ConsumeFromPrepaid: handle(account, bill, 800)
|
||||
ConsumeFromPrepaid->>PrepaidAccount: canOperate() ? Active=true
|
||||
ConsumeFromPrepaid->>PrepaidAccount: community_id match? yes
|
||||
ConsumeFromPrepaid->>PrepaidAccount: balance >= 800? yes
|
||||
|
||||
ConsumeFromPrepaid->>DB: 开启事务
|
||||
ConsumeFromPrepaid->>DB: 1. 建 CollectionOrder(type=Bill, +800, meta.fund_source=prepaid)
|
||||
ConsumeFromPrepaid->>DB: 2. 建 CollectionOrderBill(allocated=800)
|
||||
ConsumeFromPrepaid->>PrepaidAccount: 3. consume(bill, 800)
|
||||
PrepaidAccount->>DB: 建 PrepaidTransaction(consume, 3400→2600, related_bill_id)
|
||||
PrepaidAccount->>DB: 更新 balance=2600
|
||||
ConsumeFromPrepaid->>Bill: 4. recordPayment(800)
|
||||
Bill->>DB: paid_amount=800, status=Paid
|
||||
ConsumeFromPrepaid->>Listener: 5. 触发 CollectionOrderCompleted
|
||||
Listener->>DB: 6. 建 Receipt("物业费 ¥800")
|
||||
ConsumeFromPrepaid->>DB: 提交事务
|
||||
|
||||
Filament-->>业务: 成功
|
||||
```
|
||||
|
||||
## 自动 job 流程(待实现)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Scheduler
|
||||
participant Job[PrepaidAutoDeductionJob]
|
||||
participant Bills
|
||||
participant Action[ConsumeFromPrepaidAccountAction]
|
||||
|
||||
Note over Scheduler: 2026-06-01 00:30
|
||||
|
||||
Scheduler->>Job: dispatch
|
||||
Job->>Bills: SELECT 未付账单(community_id, resident_id 匹配预存款)
|
||||
|
||||
loop 每户
|
||||
Job->>Bills: 按 due_at 升序查未付账单
|
||||
loop 每张账单
|
||||
alt 余额够
|
||||
Job->>Action: handle(account, bill, bill.amount)
|
||||
Note over Action: 同手动模式的链路
|
||||
else 余额不够
|
||||
Job->>Job: 跳过
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Job-->>Scheduler: 完成 + 报告
|
||||
```
|
||||
|
||||
详见 [[../prepaid/auto-deduction-design]] + [[../prepaid/consume-batch-auto-monthly]]。
|
||||
|
||||
## 与单张 / 批量收款的对比
|
||||
|
||||
| 维度 | [[collect-payment-single|单张]] | [[collect-payment-batch|批量]] | **预存款抵(本)** |
|
||||
|---|---|---|---|
|
||||
| 业务人员操作 | 单张点 CollectPayment | 多选 + BatchCollect | 手动 ConsumeAction / 自动 job |
|
||||
| 触发位置 | ViewBill | BillsList | **ViewPrepaidAccount** / 定时任务 |
|
||||
| 资金来源 | 现金 / 微信 / POS / 转账 | 同 | **预存款余额** |
|
||||
| CollectionOrder.meta.fund_source | external | external | **prepaid** |
|
||||
| 业户感知 | 主动付 | 主动付 | **被动收到通知** |
|
||||
| 频率 | 高 | 中 | **未来最高(自动 job)** |
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 业户预存款不够付,但快有了(等到本月底)怎么办?
|
||||
> 业务上不应等。可:
|
||||
>
|
||||
> - 提示业户立即充值预存款([[../prepaid/deposit-additional-topup]])
|
||||
> - 业户用其他方式付(现金 / 微信)
|
||||
> - 部分抵(如果 Bill 支持部分付,见 [[exception-partial-payment]])
|
||||
|
||||
> [!question] 业户希望某些账单不要从预存款扣(例如不接受被自动扣电费)?
|
||||
> 当前自动 job 待实现,实施时**可加白名单 / 黑名单机制**:
|
||||
> - 业户可设置"只允许物业费自动扣"
|
||||
> - 其他费用(水电气)留给业户主动付
|
||||
>
|
||||
> issue.md 未明确需求,看业务方反馈。
|
||||
|
||||
> [!question] 预存款抵扣的 Bill 后续要作废怎么办?
|
||||
> 走 [[void-paid-bill|作废已付账单]] 流程,但**退款方向不同**:
|
||||
> - 不退现金 / 微信
|
||||
> - **退回预存款**(走 PrepaidAccount::deposit 反向充值)
|
||||
> - 详见 [[void-paid-bill]]"已付作废 + 预存款退还"段
|
||||
|
||||
> [!question] 业户跨社区,A 社区有预存款 ¥5000,B 社区欠物业费 ¥800,能跨社区抵吗?
|
||||
> **不能**。详见 [[../prepaid/exception-cross-community-consume]]"跨社区消费防御"段。
|
||||
|
||||
> [!question] 自动 job 跑的时候,业户同时去前台付现金,会重复收款吗?
|
||||
> 看时序 + 锁机制:
|
||||
> - 若 job 已锁 Bill(乐观锁)→ 前台收款失败(Bill 状态可能已变 Paid)
|
||||
> - 若 job 没锁 → 可能并发问题(罕见,需排查具体实现)
|
||||
>
|
||||
> **预防**:业务人员收款前看 Bill 当前状态,Paid 就不收。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 业户预存款不够 → 业务推 [[../prepaid/deposit-additional-topup]] 或走现金
|
||||
- 业户预存款冻结 → [[../prepaid/exception-refund-on-frozen|冻结无法抵]]
|
||||
- 跨社区抵企图 → [[../prepaid/exception-cross-community-consume|系统拦截]]
|
||||
- 抵后想撤 → [[void-paid-bill]] + 预存款回填
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[../prepaid/consume-via-bill-collection-type]]
|
||||
- [[../prepaid/consume-monthly-property-bill]]
|
||||
- [[../prepaid/auto-deduction-design]]
|
||||
- [[../prepaid/consume-batch-auto-monthly]]
|
||||
- [[collect-payment-single]]
|
||||
- [[void-paid-bill]]
|
||||
253
prop-acc/scenarios/billing/create-meter-bill-auto.md
Normal file
253
prop-acc/scenarios/billing/create-meter-bill-auto.md
Normal file
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 抄表自动生成计量账单
|
||||
aliases:
|
||||
- 计量账单生成
|
||||
- 抄表后建账单
|
||||
- create-meter-bill-auto
|
||||
- 场景-计量账单自动生成
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 创建
|
||||
- 计量账单
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 抄表员
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:抄表自动生成计量账单
|
||||
|
||||
抄表数据进入系统后(`MeterReading`),触发 `GenerateBillsFromMeterReadingsAction` 自动建账单。**核心机制在 meter 模块的[bill-generation-pipeline](../concepts/meter/bill-generation-pipeline.md)**,本场景描述 billing 模块的**对接视角**。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 嘉禾花园 5 月底:
|
||||
>
|
||||
> - 1,200 张表的本月抄表全部完成
|
||||
> - 1,160 张走集抄([[../meter/read-via-iot-remote-source]])
|
||||
> - 40 张走手抄([[../meter/read-single-meter-manual]] + [[../meter/read-batch-via-excel-import]])
|
||||
> - 系统在抄表完成后**自动触发**:`GenerateBillsFromMeterReadingsAction`(批量,1,200 张 reading → 1,200 张 Bill)
|
||||
> - 王主管看到的:**1,200 张计量账单已就绪**,无需手工建
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 自动模式(默认)
|
||||
|
||||
业务人员**几乎不操作**:
|
||||
|
||||
1. 抄表完成 → 抄表 Action / Importer 内部触发 `GenerateBillsFromMeterReadingsAction`
|
||||
2. 系统逐张 reading 算金额(走 [[../meter/multiplier-and-tiered-pricing|倍率+阶梯+min/max]] 三层算法)
|
||||
3. 建 Bill,sourceable=MeterReading,bill_type=Meter
|
||||
4. reading.bill_id 回写
|
||||
5. 报告"已生成 1,200 张计量账单"
|
||||
|
||||
### 手动模式(罕见)
|
||||
|
||||
某些情况下业务人员需要**手动触发**:
|
||||
|
||||
- 抄表数据补录后(集抄掉线 + 后续补抄)
|
||||
- 部分 reading 之前生成 Bill 失败后重试
|
||||
- 测试 / 验证
|
||||
|
||||
后台 → 账单 → 列表 → 顶部 "从 reading 生成账单" 按钮(若有 UI)→ 选 reading 范围 → 提交。
|
||||
|
||||
### 第 1 步:核对 reading 完成
|
||||
|
||||
抄表完成后(月底):
|
||||
|
||||
- 看 `MetersNeedingReadingListWidget`:本月待抄数 = 0
|
||||
- 看 `MeterReadingsList`:本月 reading 数 = 1,200
|
||||
|
||||
### 第 2 步:确认账单生成
|
||||
|
||||
抄表 Action 自动触发后:
|
||||
|
||||
- 看 `BillsList`,过滤 `bill_type=Meter` + 本月期次 → 应有 1,200 张
|
||||
- 看每张账单的 `sourceable_id` 指向对应 reading
|
||||
|
||||
### 第 3 步:异常处理
|
||||
|
||||
| 异常 | 处置 |
|
||||
|---|---|
|
||||
| 某 reading 没生成 Bill(状态:`reading.bill_id=null`)| 排查原因(RatePlan 缺失?业户没绑定?)→ 修复 → 重新触发 |
|
||||
| 某 Bill 金额异常(高出预期 10 倍)| 排查 multiplier / 阶梯配置,可能要 [[exception-readings-locked-after-bill|修正流程]] |
|
||||
| 大面积失败(>10%)| 系统级问题,联系运维 |
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**月底/月初**收到水电气账单:
|
||||
|
||||
> 陈先生您好,您的 2026 年 5 月费用账单已生成:
|
||||
>
|
||||
> - 水费:¥54(用水 12 吨)
|
||||
> - 电费:¥168(用电 280 度)
|
||||
> - 燃气费:¥35(用气 15 立方)
|
||||
>
|
||||
> 合计 ¥257,请于 6 月 15 日前付清。
|
||||
|
||||
业户**不知道**这些账单是抄表自动生成的(后端透明)。他看到的就是"几张需要付的账单"。
|
||||
|
||||
## 系统流程(完整链路)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 集抄/抄表员
|
||||
participant MeterAction[抄表 Action / Importer]
|
||||
participant GenBills[GenerateBillsFromMeterReadingsAction]
|
||||
participant Calc[MeterBillCalculator]
|
||||
participant Service[MeterBillGenerationService]
|
||||
participant DB
|
||||
|
||||
Note over 集抄/抄表员: 本月抄表完成
|
||||
|
||||
集抄/抄表员->>MeterAction: 推送 / 录入 reading 数据
|
||||
MeterAction->>DB: 建 MeterReading(bill_id=null)
|
||||
MeterAction->>GenBills: 触发(刚建的 reading 列表)
|
||||
|
||||
loop 每个 reading
|
||||
GenBills->>Service: generateBillForReading(reading)
|
||||
Service->>Calc: calculate(consumption, ratePlan)
|
||||
Calc-->>Service: amount
|
||||
Service->>DB: 建 Bill(bill_type=Meter, sourceable=reading, status=Unpaid)
|
||||
Service->>DB: 更新 reading.bill_id = bill.id
|
||||
end
|
||||
|
||||
GenBills-->>MeterAction: 报告(N 张生成,M 张失败)
|
||||
```
|
||||
|
||||
详细分层见 [[../meter/bill-generation-pipeline]]"Calculator + Service + Action"段。
|
||||
|
||||
## 与 meter 模块的关系
|
||||
|
||||
| 维度 | meter 模块 | **billing 模块(本场景)** |
|
||||
|---|---|---|
|
||||
| 主对象 | Meter + MeterReading | **Bill** |
|
||||
| 责任 | 抄表 + 算用量 | **建账单 + 后续收款** |
|
||||
| 触发 | 抄表 Action / Importer | 由 meter 模块触发 |
|
||||
| 共享 | sourceable_type='MeterReading' | sourceable_id 指向 reading |
|
||||
|
||||
billing 模块对计量账单**没有**自己的创建 Action UI(由 meter 模块的 `GenerateBillsFromMeterReadingsAction` 完全负责)。billing 模块只**接收** sourceable 标记 + 提供后续 [[collect-payment-single|收款]] / [[exception-overdue-bills|催收]] 流程。
|
||||
|
||||
## 计量账单的特殊处理
|
||||
|
||||
### 1. sourceable 关联
|
||||
|
||||
```php
|
||||
// Bill 字段
|
||||
sourceable_type = 'App\Models\MeterReading'
|
||||
sourceable_id = 12345
|
||||
```
|
||||
|
||||
可双向反查:
|
||||
|
||||
```php
|
||||
// 从 Bill 看 reading
|
||||
$bill->sourceable; // → MeterReading 实例
|
||||
|
||||
// 从 reading 看 Bill
|
||||
$reading->bill; // → Bill 实例(通过 reading.bill_id)
|
||||
```
|
||||
|
||||
### 2. bill_type=Meter
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `bill_type` | `Meter`(BillType 枚举) |
|
||||
| 业务分类 | 水费 / 电费 / 燃气费(由 fee_type_id 决定具体)|
|
||||
| 报表分类 | 进"水电气收入"科目 |
|
||||
|
||||
### 3. 期次
|
||||
|
||||
计量账单的 `billing_period_start / end` 通常对应**抄表期次**(例如:本期抄表是 5/26 - 上期 4/28 = 期次 4/29 - 5/26)。具体看实现。
|
||||
|
||||
## 异常分支
|
||||
|
||||
### 异常 1:reading 已生成 Bill,重复触发
|
||||
|
||||
```php
|
||||
// GenerateBillsFromMeterReadingsAction
|
||||
foreach ($readings as $reading) {
|
||||
if ($reading->bill_id !== null) {
|
||||
$skipped[] = ['reading' => $reading, 'reason' => 'already_billed'];
|
||||
continue;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
已有 Bill 的 reading 直接 skip。不会重复建。
|
||||
|
||||
### 异常 2:RatePlan 不存在
|
||||
|
||||
某 fee_type 没配 RatePlan:
|
||||
|
||||
```php
|
||||
$ratePlan = $feeType->currentRatePlan;
|
||||
if (! $ratePlan) {
|
||||
throw new RatePlanNotFoundException();
|
||||
}
|
||||
```
|
||||
|
||||
Service 抛错,Action 捕获后 skip 该 reading + 报告。业务人员需先配 RatePlan 再重试。
|
||||
|
||||
### 异常 3:asset 没绑业户(`community_asset_users` 缺失)
|
||||
|
||||
```php
|
||||
$resident = $this->findCurrentResident($asset);
|
||||
if (! $resident) {
|
||||
// 视设计:抛 / 建匿名 Bill(无 resident_id)/ 跳过
|
||||
}
|
||||
```
|
||||
|
||||
通常**跳过**(看具体实现),业务人员先绑业户再重试。
|
||||
|
||||
### 异常 4:已落账的 reading 数据错
|
||||
|
||||
走 [[../meter/exception-readings-locked-after-bill|修正流程]]:作废 Bill → 改 reading → 重生成 Bill。复杂,需运维介入。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 触发时机:抄表 Action 内部 / 单独定时?
|
||||
> 当前实现:**抄表 Action 完成后立即触发**(同事务或紧接事务)。
|
||||
>
|
||||
> 优点:数据立即一致(reading + Bill 同步出现)。
|
||||
> 缺点:抄表 Action 性能 = 抄表 + 建账单两段时间。
|
||||
>
|
||||
> 替代方案:定时任务(每月固定时间扫所有未生成账单的 reading)。当前未采用。
|
||||
|
||||
> [!question] 业户对计量账单金额有疑问怎么办?
|
||||
> 业务人员可:
|
||||
>
|
||||
> - 给业户看抄表照片(`reading.photo_url`)
|
||||
> - 给业户看 RatePlan(单价配置)
|
||||
> - 重算给业户看(用 Calculator 算法)
|
||||
>
|
||||
> 若证实数据错 → 走 [[../meter/exception-readings-locked-after-bill|修正流程]]。
|
||||
|
||||
> [!question] 与周期账单(物业费)冲突吗?
|
||||
> 不冲突。两者 BillType 不同(`Periodic` vs `Meter`),fee_type 不同(物业费 vs 水电气),sourceable 不同(null vs MeterReading)。各走各的生成路径,各自独立账单。
|
||||
|
||||
> [!question] 业户预存款抵这种账单的优先级?
|
||||
> 看 [[../prepaid/consume-batch-auto-monthly|预存款自动抵扣 job]] 的策略:
|
||||
>
|
||||
> - 按 `due_at` 升序(早到期的先抵)
|
||||
> - 计量账单与物业费的 due_at 通常相近(都是月底 + 宽限期)
|
||||
> - 哪个早抵哪个
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-types-and-sources]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[../meter/bill-generation-pipeline]]
|
||||
- [[../meter/multiplier-and-tiered-pricing]]
|
||||
- [[../meter/exception-readings-locked-after-bill]]
|
||||
- [[create-periodic-property-fee]]
|
||||
- [[create-single-bill-manual]]
|
||||
- [[collect-payment-batch]](业户可能水电气一起付)
|
||||
230
prop-acc/scenarios/billing/create-periodic-property-fee.md
Normal file
230
prop-acc/scenarios/billing/create-periodic-property-fee.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 月度物业费 300 户批量生成
|
||||
aliases:
|
||||
- 批量生成物业费
|
||||
- 月度物业费生成
|
||||
- create-periodic-property-fee
|
||||
- GeneratePeriodicBillsAction 实战
|
||||
- 场景-月度物业费生成
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 创建
|
||||
- 周期账单
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:月度物业费 300 户批量生成
|
||||
|
||||
每月 1 日,物业为社区**所有业户**批量生成本月物业费账单。走 `GeneratePeriodicBillsAction`,默认 `SkipExisting` 策略。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 5 月 1 日上午 9 点,嘉禾花园物业财务王主管打开后台:
|
||||
>
|
||||
> - 300 户业户(住宅 + 商铺)
|
||||
> - 物业费 RatePlan:住宅 ¥3 / m²(平均面积 100 m² → ¥300),商铺 ¥8 / m²
|
||||
> - 王主管:**1 次点击 + 1 个 Modal**,300 张账单全部建好
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:打开 BillsList
|
||||
|
||||
后台 → 账单 → 列表 → 顶部 **"批量生成周期账单"** 按钮(`GeneratePeriodicBillsAction`)。
|
||||
|
||||
### 第 2 步:Modal 填参数
|
||||
|
||||
| 参数 | 填什么 |
|
||||
|---|---|
|
||||
| **期次(billing_period)** | 2026 年 5 月(start: 5/1, end: 5/31)|
|
||||
| **费用类型(fee_type_id)** | 物业费(下拉选)|
|
||||
| **社区范围(community_id)** | 嘉禾花园 |
|
||||
| **业户范围** | 全社区业户(默认)|
|
||||
| **合并策略(merge_strategy)** | `SkipExisting`(默认,详见 [[periodic-bill-generation]])|
|
||||
| 备注 | 选填,如 "5 月例行物业费生成" |
|
||||
| **到期日(due_at)** | 6 月 15 日(本月 + 15 天宽限期)|
|
||||
|
||||
### 第 3 步:提交
|
||||
|
||||
> [!warning] Policy 守护
|
||||
> 按钮 `->authorize('create', Bill::class)`,需要 `bill.create` 权限。
|
||||
|
||||
系统执行(后台逻辑):
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[扫描候选业户清单<br/>SELECT * FROM community_user_profiles<br/>WHERE community_id=? AND status=active] --> B[300 业户]
|
||||
|
||||
B --> C[每户:]
|
||||
C --> D{该户该期次<br/>已有 Bill?}
|
||||
D -->|是| E[Skip(默认策略)]
|
||||
D -->|否| F[查 RatePlan 算金额<br/>3 * 100 = 300]
|
||||
F --> G[建 Bill]
|
||||
G --> H[活动日志]
|
||||
|
||||
E --> I[报告]
|
||||
H --> I
|
||||
```
|
||||
|
||||
### 第 4 步:看结果报告
|
||||
|
||||
```
|
||||
2026 年 5 月物业费生成完成
|
||||
|
||||
✅ 已生成:298 张
|
||||
⏭️ 已跳过:2 张(本月已存在,SkipExisting 策略)
|
||||
❌ 失败:0 张
|
||||
|
||||
总金额:¥120,400
|
||||
平均每户:¥401
|
||||
```
|
||||
|
||||
### 第 5 步:抽样核对
|
||||
|
||||
业务人员抽 2-3 张账单看金额是否对(尤其 RatePlan 改过后)。
|
||||
|
||||
### 第 6 步:推送给业户
|
||||
|
||||
视物业策略:
|
||||
|
||||
- 自动推送(若集成微信公众号 / 小程序)
|
||||
- 或单独触发"通知本月账单"动作
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 王主管
|
||||
participant Filament
|
||||
participant Action[GeneratePeriodicBillsAction]
|
||||
participant DB
|
||||
participant Notify[通知服务]
|
||||
|
||||
王主管->>Filament: 批量生成周期账单 → Modal 填参数 → 提交
|
||||
Filament->>Action: handle(community, feeType, period, strategy)
|
||||
Action->>DB: 扫描候选业户清单
|
||||
|
||||
loop 每户
|
||||
Action->>DB: 查该户该期次是否已有 Bill
|
||||
alt 已有
|
||||
Action->>Action: 按策略处理(默认 Skip)
|
||||
else 无
|
||||
Action->>DB: 查 RatePlan + 算金额
|
||||
Action->>DB: 建 Bill (status=Unpaid, period, due_at)
|
||||
end
|
||||
end
|
||||
|
||||
Action->>Notify: 触发"批量账单已生成"通知(可选)
|
||||
Action-->>Filament: 报告(生成 / 跳过 / 失败统计)
|
||||
Filament-->>王主管: Modal 显示统计
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户 5 月 1 日 / 2 日陆续收到推送:
|
||||
|
||||
> 张阿姨您好,您的 2026 年 5 月物业费 ¥300 已生成,请于 6 月 15 日前付清。
|
||||
>
|
||||
> 可选付款方式:
|
||||
> - 微信小程序付
|
||||
> - 到前台付现金 / 微信 / POS
|
||||
> - 您预存款余额 ¥1,200 足够,可自动抵扣(若开通自动抵扣)
|
||||
|
||||
业户可选:
|
||||
|
||||
- 立即付([[collect-payment-single]] / [[collect-payment-batch]])
|
||||
- 等到期日前付
|
||||
- 不付 → 到期日后变逾期([[exception-overdue-bills]])
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 部分业户失败(RatePlan 缺失)
|
||||
|
||||
| 异常 | 处置 |
|
||||
|---|---|
|
||||
| 某户没分配 RatePlan(新业户)| Action 跳过该户 + 报告标记 → 业务人员手动配 RatePlan 后单独生成 |
|
||||
| 某户面积字段缺失 | 同上 |
|
||||
| 系统级故障(DB / 内存)| Action 抛错 + 部分生成的 Bill 在事务内回滚 |
|
||||
|
||||
### Merge 策略案例(进阶)
|
||||
|
||||
如果业务人员想"同业户的物业费 + 电视费 + 网络费**合并到一张账单**":
|
||||
|
||||
- Modal 选 `merge_strategy = Merge`
|
||||
- 系统找到已生成的物业费 Bill,把电视费 / 网络费**追加进同一张 Bill**(amount 累加)
|
||||
- 业户看到的是一张合并账单 ¥XX(明细几项)
|
||||
|
||||
详见 [[periodic-bill-generation]]"策略 2:Merge"段。
|
||||
|
||||
### Replace 策略案例(罕见)
|
||||
|
||||
业务人员发现 5 月物业费 RatePlan 配错了 → 选 `merge_strategy = Replace`:
|
||||
|
||||
- 找到已有 Bill → **作废**(VoidBillAction)
|
||||
- 按新 RatePlan 重新生成
|
||||
|
||||
> [!warning] Replace 风险
|
||||
> Replace 对**已付的 Bill** 慎用,会让业户已付的钱"卡在 Void 状态",需配套退款流程。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 同一户能否生成两次同期次同费用类型?
|
||||
> 业务上不应(违反一户一月一物业费规则)。系统层面通常有 unique 索引拦截。SkipExisting 策略也避免重复。
|
||||
|
||||
> [!question] 一次生成失败一半怎么办?
|
||||
> Action 内部对单户失败容错(跳过失败户,继续其他),不会因一户失败回滚全部。报告会列出失败户,业务人员单独处理。
|
||||
|
||||
> [!question] 周期账单生成后想推迟到期日?
|
||||
> Bill 创建后 `due_at` 字段**通常可编辑**(若 status=Unpaid)。批量改可能需要专门的"批量推迟到期日" Action(当前没,可手工逐张改或运维 tinker)。
|
||||
|
||||
> [!question] 业务人员忘了月初生成怎么办?
|
||||
> 月中或月底再触发,系统正常生成(due_at 仍按原计划,可能立即逾期)。业户体验不好(账单晚到),需提前通知。
|
||||
|
||||
> [!question] 自动化(Scheduled Job)有吗?
|
||||
> issue.md Q6 未明确(meter 和 prepaid 也都有"自动化待补"的待办)。当前**手动触发**。未来可加:
|
||||
>
|
||||
> - 月初 1 日 00:30 自动触发(类似 prepaid 自动抵扣)
|
||||
> - 触发后立即触发预存款抵扣(无缝)
|
||||
> - 业户次日早收到推送
|
||||
|
||||
> [!question] 业户拒绝接收推送怎么办?
|
||||
> 系统层面不影响(账单仍在,可在小程序 / 后台查)。推送只是触达手段。业户拒收推送 → 物业改用电话 / 短信 / 上门通知。
|
||||
|
||||
> [!question] 不同费用类型(物业费 + 电视费)能一次性都生成吗?
|
||||
> 当前 `GeneratePeriodicBillsAction` 一次只生成**一个费用类型**。批量需触发多次(物业费 1 次 + 电视费 1 次 + ...)。或者业务方反馈后增加"多费用类型批量"功能。
|
||||
|
||||
## 与计量账单生成的对比
|
||||
|
||||
| 维度 | 本场景(周期账单)| [[create-meter-bill-auto|计量账单]] |
|
||||
|---|---|---|
|
||||
| 触发 | 业务人员手动 | 抄表完成后自动 / 批量 |
|
||||
| 金额 | 固定(RatePlan)| 浮动(用量计算)|
|
||||
| 数量 | 每户每期 1 张 | 每抄表 1 张 |
|
||||
| sourceable | null(或周期任务 ID)| MeterReading |
|
||||
| 频率 | 月度 1 次 | 看抄表频率 |
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 计量账单生成 → [[create-meter-bill-auto]]
|
||||
- 临时手动建单 → [[create-single-bill-manual]]
|
||||
- 业务人员要批量删错的 → [[bulk-delete-batch-mistake]]
|
||||
- 业户收到账单要付款 → [[collect-payment-single]]
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[periodic-bill-generation]]
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-types-and-sources]]
|
||||
- [[create-meter-bill-auto]]
|
||||
- [[collect-payment-single]]
|
||||
- [[bulk-delete-batch-mistake]]
|
||||
- [[../meter/bill-generation-pipeline]]
|
||||
- [[../prepaid/auto-deduction-design]](账单生成下游消费)
|
||||
214
prop-acc/scenarios/billing/create-single-bill-manual.md
Normal file
214
prop-acc/scenarios/billing/create-single-bill-manual.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 手动建单(临时收费/调整账单)
|
||||
aliases:
|
||||
- 手动建账单
|
||||
- 临时账单
|
||||
- create-single-bill-manual
|
||||
- 场景-手动建账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 创建
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:手动建单(临时收费/调整账单)
|
||||
|
||||
业务上**不属于周期任务、不属于抄表**的临时收费,通过 `CreateBill` 后台**手工建一张账单**。例如:维修费分摊、特别活动费、单次罚款、跨期补开账单。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境(一):公共维修费分摊
|
||||
> 嘉禾花园 3 单元电梯坏了维修,总成本 ¥15,000。物业公司决定**全单元 30 户业户分摊**(每户 ¥500)。这种"临时性的、不属周期 / 抄表"的费用,业务人员逐户手工建账单(或选定业户清单后批量手工)。
|
||||
|
||||
> [!example] 真实情境(二):个别业户的特别罚款
|
||||
> 张阿姨违反小区车位管理规定(占用应急车位 1 小时),物业罚款 ¥100。业务人员单独给张阿姨建一张账单。
|
||||
|
||||
> [!example] 真实情境(三):跨期补开账单
|
||||
> 业务人员发现陈先生 4 月物业费没生成(系统月初批量时陈先生数据有问题),5 月才发现。需补开 4 月账单。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:打开 CreateBill
|
||||
|
||||
后台 → 账单 → 列表 → 顶部 **"新建"** 按钮 → 进 `CreateBill` 页面。
|
||||
|
||||
### 第 2 步:填表单(`BillForm`)
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **社区(community_id)** | 嘉禾花园(若多社区可选)|
|
||||
| **业户(resident_id)** | 张阿姨(下拉选)|
|
||||
| **房屋(asset_id)** | 12-3-501(自动带入或手选)|
|
||||
| **费用类型(fee_type_id)** | 选合适的 FeeType(物业费 / 杂费 / 罚款 / ...) |
|
||||
| **账单类型(bill_type)** | 通常选 `OneTime` 或 `Adjustment`(看 BillType 枚举)|
|
||||
| **金额(amount)** | ¥500(电梯维修分摊)|
|
||||
| **期次** | 选填(临时账单可不填,或填发生月)|
|
||||
| **到期日(due_at)** | 6 月 15 日 |
|
||||
| **备注(memo)** | **强烈推荐填**,说明缘由,如 "3 单元电梯维修分摊(2026-05),总成本 ¥15,000 ÷ 30 户" |
|
||||
|
||||
> [!warning] Policy 守护
|
||||
> `CreateBill` 需要 `bill.create` 权限。
|
||||
|
||||
### 第 3 步:提交
|
||||
|
||||
系统:
|
||||
|
||||
1. 校验字段(business / asset / fee_type 存在)
|
||||
2. 建 Bill(`status=Unpaid`,`sourceable_type=null` / 或自定义)
|
||||
3. 写 activitylog(单条 Bill 创建,subject=Bill)
|
||||
4. 跳到 `ViewBill` 页面
|
||||
|
||||
### 第 4 步:通知业户
|
||||
|
||||
通常需要业务人员**单独通知**(临时账单不在月度自动推送范围内):
|
||||
|
||||
- 微信 / 短信:"张阿姨,您本月有一笔电梯维修分摊费 ¥500,请于 6 月 15 日前付清"
|
||||
- 附备注说明
|
||||
|
||||
### 第 5 步:批量手工建(电梯维修分摊场景)
|
||||
|
||||
如果是分摊给 30 户业户,逐户建效率低。当前没有"批量手动建" UI,只能:
|
||||
|
||||
- 逐户走 `CreateBill`(30 次)
|
||||
- 或运维 tinker 脚本(SQL 批量 INSERT)
|
||||
- 或要求业务方加"批量手工建"功能
|
||||
|
||||
未来可加:`BulkCreateBillsAction`(给定业户清单 + 模板 → 批量建)。
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务[业务人员]
|
||||
participant Filament
|
||||
participant CreateBill
|
||||
participant Activity[activitylog]
|
||||
participant DB
|
||||
|
||||
业务->>Filament: ListBills → 新建 → CreateBill
|
||||
Filament->>CreateBill: 渲染 form
|
||||
业务->>CreateBill: 填业户 / 金额 / 备注 / 提交
|
||||
CreateBill->>DB: 校验 fields
|
||||
CreateBill->>DB: 建 Bill(status=Unpaid, bill_type=OneTime, sourceable=null)
|
||||
CreateBill->>Activity: log(event=created, subject=Bill)
|
||||
CreateBill-->>Filament: 跳转 ViewBill
|
||||
Filament-->>业务: 显示新账单
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户收到通知:
|
||||
|
||||
> 张阿姨您好,您本月有一笔账单:
|
||||
> - 电梯维修分摊费:¥500
|
||||
> - 备注:3 单元电梯维修分摊
|
||||
> - 到期日:6 月 15 日
|
||||
>
|
||||
> 请通过微信小程序 / 前台 / 预存款 任选方式付款。
|
||||
|
||||
业户:
|
||||
|
||||
- 看明白 → 付([[collect-payment-single]])
|
||||
- 不接受 → 投诉 / 走纠纷流程(可能 [[suspend-bill]] 挂起)
|
||||
|
||||
## 三种典型情境的不同处理
|
||||
|
||||
### 情境 1:电梯维修分摊
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `bill_type` | `OneTime` |
|
||||
| `fee_type_id` | "维修分摊费"(若有此 FeeType,否则用通用"杂费")|
|
||||
| `amount` | ¥500 |
|
||||
| 备注 | 详细说明(分摊依据 + 总成本)|
|
||||
|
||||
业务上还要**备齐资料**:维修发票、户主同意决议(若有业主大会决议)、分摊依据。这些不在系统(物业备查)。
|
||||
|
||||
### 情境 2:违规罚款
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `bill_type` | `OneTime` |
|
||||
| `fee_type_id` | "罚款"(若有此 FeeType)|
|
||||
| `amount` | ¥100 |
|
||||
| 备注 | 违规事由 + 处罚依据(管理规定第 X 条) |
|
||||
|
||||
业务上要 **业户书面确认**(违规事实),否则业户可能投诉 / 拒付。
|
||||
|
||||
### 情境 3:跨期补开
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| `bill_type` | `Periodic`(若是月物业费的补开)|
|
||||
| `period` | **填实际期次**(4 月)|
|
||||
| `amount` | 同正常月物业费 |
|
||||
| 备注 | "4 月物业费补开,原因 XXX" |
|
||||
|
||||
业务人员**需要**:
|
||||
|
||||
- 确认陈先生 4 月确实欠物业费(没有遗漏)
|
||||
- 与陈先生沟通(突然补开他可能困惑)
|
||||
|
||||
## 与周期账单 / 计量账单的对比
|
||||
|
||||
| 维度 | 周期账单([[create-periodic-property-fee]]) | 计量账单([[create-meter-bill-auto]]) | **手动账单(本场景)** |
|
||||
|---|---|---|---|
|
||||
| 触发 | 业务人员批量生成 | 抄表完成自动 | **业务人员单笔手工** |
|
||||
| 数量 | 一次 100-1000 张 | 一次 N 张(对应 reading 数)| **一次 1 张** |
|
||||
| 金额来源 | RatePlan + 房屋参数 | RatePlan + 用量计算 | **业务人员手填** |
|
||||
| 业务场景 | 月度固定收费 | 抄表后变动收费 | **临时收费 / 调整** |
|
||||
| sourceable | null / 周期任务 | MeterReading | **null** |
|
||||
| 频率 | 每月 1 次 | 每月 1 次 | **不定时** |
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 手动建单和周期账单的 fee_type 重复怎么办?
|
||||
> 例如本月已经有"5 月物业费"账单,业务人员又手工建一张同 fee_type 同期次的 → 数据库 unique 约束可能拦截(若有),否则会有两张并存。
|
||||
>
|
||||
> **预防**:手动建单前确认是否已有同类账单。
|
||||
|
||||
> [!question] 手动建单金额没限制吗?
|
||||
> 系统层面无限制。**业务上**应有审核流程(高金额 > X 元 应主管审批,但当前系统不强制此审批流)。
|
||||
|
||||
> [!question] 跨期补开的账单影响月度报表吗?
|
||||
> 看报表的查询:
|
||||
>
|
||||
> - 按 `created_at`(账单创建时间)→ 影响 5 月报表(实际是 5 月创建)
|
||||
> - 按 `billing_period_start`(账单期次)→ 影响 4 月报表
|
||||
>
|
||||
> 不同报表用不同维度。月度对账通常按期次。
|
||||
|
||||
> [!question] 手动建单后业户拒付?
|
||||
> 走标准流程:
|
||||
>
|
||||
> - 沟通 → 协商
|
||||
> - 不接受 → [[suspend-bill|挂起]] 等争议解决
|
||||
> - 调解失败 → 物业法务介入(走线下,系统不参与司法)
|
||||
|
||||
> [!question] 业务人员误建一张账单怎么撤?
|
||||
> 若立即发现:[[delete-bill-unpaid|物理删除]](Unpaid 无付款)。
|
||||
> 若已发出:[[void-paid-bill|作废]](留状态留审计)。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 误建 → 立即删除 [[delete-bill-unpaid]]
|
||||
- 业户拒付 → [[suspend-bill]]
|
||||
- 已发现要修正金额 → 作废 + 新建 [[void-paid-bill]]
|
||||
- 批量手工建 → 待业务方提需求 + 加 BulkCreateBillsAction
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-types-and-sources]]
|
||||
- [[bill-six-state-machine]]
|
||||
- [[create-periodic-property-fee]]
|
||||
- [[create-meter-bill-auto]]
|
||||
- [[delete-bill-unpaid]]
|
||||
- [[suspend-bill]]
|
||||
223
prop-acc/scenarios/billing/delete-bill-unpaid.md
Normal file
223
prop-acc/scenarios/billing/delete-bill-unpaid.md
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 物理删除未付账单(误建立刻删)
|
||||
aliases:
|
||||
- 物理删除账单
|
||||
- 删除未付账单
|
||||
- delete-bill-unpaid
|
||||
- 场景-删除未付账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 删除
|
||||
audience:
|
||||
- 业务人员
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:物理删除未付账单(误建立刻删)
|
||||
|
||||
业务人员**误建**了账单(选错业户 / 写错金额 / 误触发批量生成),**立即发现**且**业户还没付**任何钱 → 走**物理删除**。`canBeDeleted()` 守护:Unpaid + 无付款关联。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 王主管月初手工建账单"5 月物业费 ¥800",**填错业户 ID**(应该是张阿姨,选成了陈先生)。提交后立刻发现错误。
|
||||
>
|
||||
> - 账单状态:Unpaid
|
||||
> - 业户没付过任何钱
|
||||
> - **可走物理删除**(走 [[delete-vs-void-dual-track|双轨制]] 的 delete 路径)
|
||||
> - 再重新建一张正确的(选张阿姨)
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:发现错误
|
||||
|
||||
| 发现时机 | 处置 |
|
||||
|---|---|
|
||||
| **立即**(几秒钟内)| 物理删(本场景)|
|
||||
| **几小时**(可能业户已收到推送)| 仍可删(若 Unpaid + 无付款)|
|
||||
| **几天**(可能业户已付)| **不可删**,走 [[void-paid-bill|作废]] |
|
||||
|
||||
### 第 2 步:打开账单
|
||||
|
||||
后台 → 账单 → 找到误建账单 → 进 `EditBill`(或 `ViewBill`)。
|
||||
|
||||
### 第 3 步:点击 `DeleteAction`(标签"删除")
|
||||
|
||||
> [!warning] 按钮可见性
|
||||
> 守护:`canBeDeleted()` = Unpaid + 无付款 + `->authorize('delete')`(`bill.delete` 权限)。
|
||||
>
|
||||
> 任何其他状态(Partial / Paid / Suspended / Void)或有任何付款关联 → 按钮**灰化**。
|
||||
|
||||
### 第 4 步:Modal 确认
|
||||
|
||||
```
|
||||
确认删除账单 #B-202605-501-001?
|
||||
|
||||
⚠️ 此账单状态为 Unpaid 且无付款关联,可物理删除。
|
||||
删除后无法恢复(但 activitylog 会保留记录)。
|
||||
|
||||
[取消] [确认删除]
|
||||
```
|
||||
|
||||
### 第 5 步:提交
|
||||
|
||||
系统:
|
||||
|
||||
1. 再次校验 `canBeDeleted()`(防 UI 缓存过期)
|
||||
2. 物理删 Bill(从数据库消失)
|
||||
3. 写 activitylog(event=deleted,记 bill_no / amount / 操作员 / 时间)
|
||||
|
||||
> [!info] activitylog 留什么
|
||||
> 即使 Bill 物理删了,activitylog 表还有完整记录:
|
||||
>
|
||||
> ```sql
|
||||
> SELECT * FROM activity_log
|
||||
> WHERE event = 'deleted'
|
||||
> AND properties->>'$.bill_no' = 'B-202605-501-001';
|
||||
> ```
|
||||
>
|
||||
> 可看到"谁在什么时候删了什么账单"。详见 [[smart-bulk-delete-design]]"activitylog 设计"。
|
||||
|
||||
### 第 6 步:重新建正确账单(若需要)
|
||||
|
||||
走 [[create-single-bill-manual|手动建单]] 流程,选正确业户。
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Action[DeleteAction]
|
||||
participant Bill
|
||||
participant Activity
|
||||
participant DB
|
||||
|
||||
业务->>Filament: EditBill → DeleteAction
|
||||
Filament->>Action: handle(bill)
|
||||
Action->>Bill: canBeDeleted()? Unpaid + 无付款 = true
|
||||
Action->>Activity: log(event=deleted, properties={bill_no, amount, ...})
|
||||
Action->>DB: 物理删 Bill
|
||||
Action-->>Filament: 跳回 ListBills
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**无感**:
|
||||
|
||||
- 误建的账单(尤其几秒内删的)业户**根本不知道**它存在过
|
||||
- 即使业户已经收到推送,删除后再次刷新看不到了
|
||||
- 物业可选择给业户发"前述账单作废,系统误建"(可选,看业务策略)
|
||||
|
||||
**最理想场景**:业务人员发现 → 立即删 → 业户从未感知。
|
||||
|
||||
## `canBeDeleted` 详解
|
||||
|
||||
`Bill::canBeDeleted()`:
|
||||
|
||||
```php
|
||||
public function canBeDeleted(): bool
|
||||
{
|
||||
return $this->status === BillStatus::Unpaid
|
||||
&& ! $this->hasAnyPayment();
|
||||
}
|
||||
```
|
||||
|
||||
`hasAnyPayment()`:
|
||||
|
||||
```php
|
||||
public function hasAnyPayment(): bool
|
||||
{
|
||||
return $this->collectionOrderBills()->exists()
|
||||
|| $this->prepaidTransactionsRelatedToBill()->exists();
|
||||
}
|
||||
```
|
||||
|
||||
两条都为真(Unpaid + 无付款关联) → 可删。
|
||||
|
||||
> [!info] 为什么 Partial 不能删?
|
||||
> Partial 意味着业户已经付了一部分 → 有 CollectionOrderBill 关联 → `hasAnyPayment=true` → 拒绝物理删。
|
||||
>
|
||||
> 业户付的钱不能"凭空消失"。Partial 状态要么继续收款 → Paid;要么走 [[void-paid-bill|作废 + 退款]] 流程。
|
||||
|
||||
## 与作废的对比
|
||||
|
||||
| 维度 | **删除(本场景)** | [[void-paid-bill|作废]] |
|
||||
|---|---|---|
|
||||
| 适用 | Unpaid + 无付款 | 非 Paid 非 Void(基本是 Partial / Suspended) |
|
||||
| 数据库 | **从数据库消失** | 留状态 = Void |
|
||||
| activitylog | 留(event=deleted)| 留(event=voided)|
|
||||
| 业户感知 | 通常无感(未付过)| 有(看到状态 Void) |
|
||||
| 可恢复 | ❌ 不可(数据真没了)| ❌ 不可(Void 终态)|
|
||||
| 适用场景 | 误建立刻删 | 任何已付 / 有付款 / 业户已知的 |
|
||||
|
||||
详见 [[delete-vs-void-dual-track]] 双轨制设计哲学。
|
||||
|
||||
## 批量删除(误建一批)
|
||||
|
||||
如果不是单张误建,而是**误触发批量生成**(例如点了一次"生成 5 月物业费"后忘了又点了一次):
|
||||
|
||||
→ 走 [[bulk-delete-batch-mistake|智能批量删除]],走 `BulkDeleteBillsAction` 的预检查 + 三档分类。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 删了后悔了能恢复吗?
|
||||
> 不能(数据真的没了)。**只能重新建**(走 [[create-single-bill-manual]] 或 [[create-periodic-property-fee]])。
|
||||
>
|
||||
> 但**所有信息可从 activitylog 还原**(原 bill_no / amount / 业户 等),重建相对简单。
|
||||
|
||||
> [!question] 业务人员误删大量账单怎么办?
|
||||
> 单删一次一张,影响有限。**批删**才容易误删大量(详见 [[smart-bulk-delete-design]] 的预检查防护)。
|
||||
>
|
||||
> 单张误删的预防:
|
||||
> - Modal 确认(默认有)
|
||||
> - 思考"是否真要删"再点
|
||||
|
||||
> [!question] 删除有审批吗?
|
||||
> 当前**无审批流**(单签批操作)。但**权限层有控制**:`bill.delete` 是普通业务人员权限;`bill.bulkDelete` 是高敏独立权限(详见 [[smart-bulk-delete-design]])。
|
||||
|
||||
> [!question] activitylog 表会爆掉吗?
|
||||
> 长期看会(每次单删 + 批删 + 作废 + 收款都加几条)。需归档策略(详见 [[smart-bulk-delete-design]]"常见问题"段)。
|
||||
|
||||
> [!question] 删除影响 reading.bill_id 吗(若是计量账单)?
|
||||
> 看 cascade 设计:
|
||||
> - 若 reading 的 `bill_id` 是 FK + cascade SET NULL → 自动 nullify
|
||||
> - 若没 cascade → reading.bill_id 仍指向已删 Bill(孤儿 FK,需修复)
|
||||
>
|
||||
> 当前实施看代码。**预防**:计量账单不轻易物理删,走作废更稳妥。
|
||||
|
||||
> [!question] 已删账单的 activitylog 怎么用?
|
||||
> ```sql
|
||||
> -- 某员工某天的全部 delete
|
||||
> SELECT
|
||||
> id, causer_id,
|
||||
> properties->>'$.bill_no' AS bill_no,
|
||||
> properties->>'$.amount' AS amount,
|
||||
> created_at
|
||||
> FROM activity_log
|
||||
> WHERE event = 'deleted'
|
||||
> AND causer_id = ?
|
||||
> AND created_at BETWEEN ? AND ?;
|
||||
> ```
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 误删后想重建 → [[create-single-bill-manual]]
|
||||
- 误删一批 → 走批删的反向(无,只能 1 张张重建)
|
||||
- 不能删(有付款)→ [[void-paid-bill]]
|
||||
- 批量误建 → [[bulk-delete-batch-mistake]]
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[smart-bulk-delete-design]]
|
||||
- [[bill-six-state-machine]]
|
||||
- [[void-paid-bill]]
|
||||
- [[bulk-delete-batch-mistake]]
|
||||
- [[audit-activitylog-trace]]
|
||||
- [[create-single-bill-manual]]
|
||||
242
prop-acc/scenarios/billing/exception-overdue-bills.md
Normal file
242
prop-acc/scenarios/billing/exception-overdue-bills.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 逾期账单清单 + 催收流程
|
||||
aliases:
|
||||
- 逾期账单
|
||||
- 催收
|
||||
- OverdueBillsListWidget
|
||||
- exception-overdue-bills
|
||||
- 场景-逾期账单催收
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 催收
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 业户
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:逾期账单清单 + 催收流程
|
||||
|
||||
业户**到期未付**的账单进入逾期清单(`OverdueBillsListWidget`),业务人员**分级催收**:温和提醒 → 严肃催告 → 法律手段。是物业**应收账款管理**的核心。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 5 月 16 日(物业费 due_at = 5/15 后第 1 天),王主管打开 dashboard:
|
||||
>
|
||||
> - `OverdueBillsListWidget` 显示**当前逾期 25 户**(本月物业费 + 部分上月遗留)
|
||||
> - 合计欠款 ¥18,500
|
||||
> - 平均逾期天数 1-30 天不等
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:打开 Dashboard / Widget
|
||||
|
||||
后台 → Dashboard → `OverdueBillsListWidget`(可能在主 Dashboard 或财务 Dashboard)。
|
||||
|
||||
Widget 显示:
|
||||
|
||||
| 列 | 内容 |
|
||||
|---|---|
|
||||
| 业户 / 房号 | 12-3-501 张阿姨 |
|
||||
| 账单号 | B-202605-501-001 |
|
||||
| 费用类型 | 物业费 / 水费 / ... |
|
||||
| 账单金额 | ¥800 |
|
||||
| 已付金额 | ¥0(Unpaid)/ ¥300(Partial)|
|
||||
| 剩余应付 | ¥800 / ¥500 |
|
||||
| 到期日 | 5/15 |
|
||||
| **逾期天数** | 1 天 / 7 天 / 30 天 / ... |
|
||||
| 状态 | Unpaid / Partial |
|
||||
|
||||
排序通常**按逾期天数降序**(最严重的先)。
|
||||
|
||||
### 第 2 步:分级催收
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[逾期账单清单] --> B{逾期天数}
|
||||
|
||||
B -->|1-7 天<br/>🟢 温和| C[小程序 / 微信 / 短信<br/>友好提醒]
|
||||
B -->|8-30 天<br/>🟡 严肃| D[电话联系 + 上门拜访<br/>面谈了解原因]
|
||||
B -->|31-90 天<br/>🔴 严重| E[正式催告函<br/>+ 加收滞纳金<br/>+ 部分服务受限]
|
||||
B -->|>90 天<br/>⚫ 法律| F[律师函 / 司法起诉<br/>+ 业户失信记录]
|
||||
|
||||
C --> G{业户响应}
|
||||
D --> G
|
||||
E --> G
|
||||
F --> G
|
||||
|
||||
G -->|付款| H[走 collect-payment-single]
|
||||
G -->|协商| I[Suspend Bill + 等协议]
|
||||
G -->|无响应| J[升级催收]
|
||||
G -->|拒付不可调和| K[法律 + 长期 Suspend]
|
||||
```
|
||||
|
||||
### 第 3 步:具体催收动作
|
||||
|
||||
#### 🟢 温和(1-7 天)
|
||||
|
||||
- 自动 推送 / 短信(由系统定时任务,若实现)
|
||||
- "张阿姨您好,您的 5 月物业费 ¥800 已逾期,请尽快付清"
|
||||
|
||||
#### 🟡 严肃(8-30 天)
|
||||
|
||||
- 物业管家电话联系
|
||||
- 上门拜访(若联系不上)
|
||||
- 了解逾期原因 + 协商付款时间表
|
||||
- 若业户有困难 → [[suspend-bill|挂起]] + 协议分期
|
||||
|
||||
#### 🔴 严重(31-90 天)
|
||||
|
||||
- 物业法务部门介入
|
||||
- 出具正式催告函(纸质 + 电子)
|
||||
- 可加滞纳金(看物业合同 / 业主大会决议)
|
||||
- 部分服务限制(如停水电、限制电梯使用,具体看物业政策 + 法律允许度)
|
||||
|
||||
#### ⚫ 法律(>90 天)
|
||||
|
||||
- 委托律师事务所
|
||||
- 律师函
|
||||
- 司法起诉(物业 vs 业户)
|
||||
- 法院判决 → 强制执行
|
||||
|
||||
### 第 4 步:更新跟进记录
|
||||
|
||||
业务人员每次催收**在系统记录**(若有催收日志功能):
|
||||
|
||||
- 催收时间 / 方式 / 业户反馈
|
||||
- 下次跟进时间
|
||||
|
||||
(当前实施可能在 Bill.memo 或单独表,看代码。)
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 您可能收到的
|
||||
|
||||
#### 温和提醒
|
||||
|
||||
> 张阿姨您好,您的 2026 年 5 月物业费 ¥800 已于 5/15 到期,请尽快通过以下方式付清:
|
||||
> - 微信小程序
|
||||
> - 到前台
|
||||
> - 预存款充值后自动扣
|
||||
>
|
||||
> 您预存款余额仅 ¥200,不够付。
|
||||
|
||||
#### 严肃催告
|
||||
|
||||
> 张阿姨,您 5 月物业费 ¥800 已逾期 15 天,请于本周内付清。如有困难请联系物业 XXX 协商。
|
||||
|
||||
#### 严重催告
|
||||
|
||||
> [正式催告函]
|
||||
> 您 2026 年 5 月物业费 ¥800 已严重逾期 60 天,根据物业管理合同第 X 条,我司将:
|
||||
> 1. 加收滞纳金 ¥XX
|
||||
> 2. 限制您的部分物业服务
|
||||
> 3. 若 X 月 X 日前仍未付清,我司将启动法律程序追讨
|
||||
>
|
||||
> 请尽快处理。
|
||||
|
||||
### 您要做什么
|
||||
|
||||
- 立即付款(若能力允许)
|
||||
- 与物业协商(若有困难)
|
||||
- **不要** 不闻不问(代价升级)
|
||||
|
||||
## 滞纳金 / 罚息
|
||||
|
||||
> [!info] 滞纳金的合规边界
|
||||
> 物业能否收滞纳金看:
|
||||
> - 物业管理合同条款(常见日利率 0.05% 或类似)
|
||||
> - 业主大会决议
|
||||
> - 国家 / 地方法规(不能高于法定上限)
|
||||
>
|
||||
> 系统层面**可能不直接管滞纳金**(看实现)。若收滞纳金 → 通常**另开账单**(走 [[create-single-bill-manual]])"滞纳金:¥X(5 月物业费逾期 X 天)"。
|
||||
|
||||
## 部分服务限制的合规
|
||||
|
||||
物业**限制服务**(停水电 / 限电梯)需谨慎:
|
||||
|
||||
| 限制 | 合规性 |
|
||||
|---|---|
|
||||
| 限制小程序业户自助功能(查询 / 报修) | 合规(物业自主决定)|
|
||||
| 拒绝业户业务申请(开停车证等)| 合规 |
|
||||
| 停水(若物业有控制权)| 多数地区**不合规**(基本生活用水有法律保护)|
|
||||
| 停电 | 同上,且通常电网在国家电网,物业无权停 |
|
||||
| 限制电梯使用 | 部分地区合规,部分不合规 |
|
||||
| 公布逾期业户名单 | 部分合规(看公开范围 + 业主大会决议)|
|
||||
|
||||
**严格合规咨询当地法律**。系统**不强制**这些限制,由物业流程决定。
|
||||
|
||||
## 长期逾期的处理
|
||||
|
||||
| 时长 | 处置 |
|
||||
|---|---|
|
||||
| 0-30 天 | 常规催收 |
|
||||
| 30-90 天 | 升级催收 + 必要时 [[suspend-bill|挂起]] |
|
||||
| 90+ 天 | 法律程序 + 长期挂起 |
|
||||
| > 2 年 | 评估**作废 / 走司法判决执行** |
|
||||
| > 5 年(诉讼时效)| 法律时效问题,通常作废 |
|
||||
|
||||
## 与 prepaid 模块的关系
|
||||
|
||||
如果业户**预存款够付** 但因故没自动抵扣(job 没跑 / 业户冻结 / 跨社区) → 账单逾期。**典型案例**:[[../prepaid/audit-low-balance-and-overdue]] 场景中提到。
|
||||
|
||||
业务人员看到逾期账单时,应先查业户预存款余额:
|
||||
|
||||
- 足够付:**手动触发** [[collect-via-prepaid-auto]] 抵扣(快速解决)
|
||||
- 不够付:走标准催收
|
||||
|
||||
## 自动催收 job(待补)
|
||||
|
||||
> [!info] 自动化机会
|
||||
> 当前催收**靠人工**(看 widget + 一一联系)。可加自动化:
|
||||
>
|
||||
> - **定时任务**:每天扫逾期账单 → 按逾期天数分级 → 自动推送 / 短信
|
||||
> - **滞纳金自动计算**:每日跑 → 给逾期账单加滞纳金
|
||||
> - **批量催告函生成**:选中 N 个业户 → 一次生成所有催告函(PDF / 邮件)
|
||||
>
|
||||
> 当前 issue.md 未明确实施。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] Widget 上的逾期天数怎么算?
|
||||
> `(NOW() - bill.due_at).days`(SQL 层算)。若 due_at 还没到 → 不在 widget。
|
||||
|
||||
> [!question] Suspended 状态的账单算逾期吗?
|
||||
> **不算**。Widget 通常过滤 `status IN (Unpaid, Partial)`,Suspended 不在。
|
||||
|
||||
> [!question] 业户付了一部分但仍逾期算不算?
|
||||
> Partial 状态 + 仍欠款 + 过 due_at = 算逾期。Widget 显示"剩余应付"。
|
||||
|
||||
> [!question] 滞纳金怎么记账?
|
||||
> 单独建账单(`bill_type=OneTime`,`fee_type=滞纳金`)。详见 [[create-single-bill-manual]] "情境 2:违规罚款" 模式(滞纳金类似)。
|
||||
|
||||
> [!question] 业户长期失联,催收记录怎么留?
|
||||
> 物业内部催收日志(纸质 / Excel)。系统层面无强制要求(若 issue.md 未实现催收日志功能)。法律纠纷时这些日志是关键证据。
|
||||
|
||||
> [!question] 业主大会决议某些业户免缴怎么办?
|
||||
> 业务上:走 [[void-paid-bill|作废]] 该账单(附决议号)。系统层面不区分"免缴"vs"其他作废"(都是 Void 状态)。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 业户响应付款 → [[collect-payment-single]]
|
||||
- 业户协商分期 → [[exception-partial-payment]]
|
||||
- 业户失联 → [[suspend-bill]]
|
||||
- 法律手段 → 走线下,系统记录 + [[void-paid-bill]]
|
||||
- 业户预存款够付 → [[collect-via-prepaid-auto]] 手动触发
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[exception-partial-payment]]
|
||||
- [[suspend-bill]]
|
||||
- [[void-paid-bill]]
|
||||
- [[collect-payment-single]]
|
||||
- [[collect-via-prepaid-auto]]
|
||||
- [[../prepaid/audit-low-balance-and-overdue]](类似的预警审计场景)
|
||||
279
prop-acc/scenarios/billing/exception-partial-payment.md
Normal file
279
prop-acc/scenarios/billing/exception-partial-payment.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 部分付状态处理(Partial)
|
||||
aliases:
|
||||
- 部分付
|
||||
- Partial 状态
|
||||
- exception-partial-payment
|
||||
- 场景-部分付账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 部分付
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:部分付状态处理(Partial)
|
||||
|
||||
业户**只付了一部分**账单金额,系统状态从 Unpaid 进入 **Partial**(部分付)。后续业务人员需要跟进:**继续催收剩余款** / 提醒业户 / 或视情况调整账单。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 王先生(15-7-203)5 月物业费 ¥800,他到前台说"现在只能付 ¥300,剩下 ¥500 月底再补"。业务人员小李录入收款 ¥300:
|
||||
>
|
||||
> - Bill.paid_amount = 300
|
||||
> - Bill.status:Unpaid → **Partial**
|
||||
> - 建 CollectionOrderBill(allocated=300)+ CollectionOrder(+300)+ Receipt(¥300)
|
||||
>
|
||||
> 后续:王先生 5 月底再付 ¥500 → Bill.paid_amount = 800 → Bill.status: Partial → Paid。
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 部分付场景
|
||||
|
||||
| 业户原因 | 频率 |
|
||||
|---|---|
|
||||
| 现金不够 | 中 |
|
||||
| 短期资金紧张 | 中 |
|
||||
| 对账单部分有异议(剩余部分协商中)| 低 |
|
||||
| 拆分多次付方便记账(罕见)| 低 |
|
||||
| 业户搬走前部分清账 | 低 |
|
||||
|
||||
### 您会感受到什么
|
||||
|
||||
- 收据上显示**实付金额**(¥300,不是账单总额 ¥800)
|
||||
- 推送 / 小程序:"已付 ¥300,剩余 ¥500 待付"
|
||||
- 余额显示 Partial 状态(账单未完全清)
|
||||
- 月底前 reminder(若有催收机制)
|
||||
|
||||
### 您要做什么
|
||||
|
||||
- 在到期日前补齐剩余款
|
||||
- 走 [[collect-payment-single|继续收款]](第 2 笔 ¥500)
|
||||
- 若有困难 → 与物业沟通(协商分期 / 减免 / 挂起)
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 部分付的触发
|
||||
|
||||
走 [[collect-payment-single|单张收款]] Modal,**手动改"收款金额"小于全额**:
|
||||
|
||||
```
|
||||
收款金额:300(改成 300,不是默认的 800)
|
||||
支付方式:现金
|
||||
```
|
||||
|
||||
提交后:
|
||||
|
||||
- Bill.paid_amount: 0 → 300
|
||||
- Bill.status: Unpaid → **Partial**
|
||||
|
||||
### Partial 状态的能力
|
||||
|
||||
| 操作 | Partial 状态 |
|
||||
|---|---|
|
||||
| `CollectPaymentAction`(继续收款)| ✅(canBePaid=true)|
|
||||
| `SuspendBillAction`(挂起)| ✅ |
|
||||
| `VoidBillAction`(作废)| ✅(canBeVoided=true,但需配套退款已付部分)|
|
||||
| `DeleteAction`(物理删)| ❌(canBeDeleted 要求 Unpaid + 无付款)|
|
||||
| `SplitBillAction`(拆账单)| 可能不允许(已付款拆分复杂,见 [[split-bill]])|
|
||||
|
||||
### 第 2 笔收款
|
||||
|
||||
业户来补付时:
|
||||
|
||||
1. 找到 Partial 账单
|
||||
2. 走 `CollectPaymentAction`(状态守护 canBePaid=true,Partial 也允许)
|
||||
3. 收款金额 = ¥500(剩余应付,Modal 应默认带入 remaining)
|
||||
4. 提交 → Bill.paid_amount = 800,status: Partial → Paid
|
||||
|
||||
### 监控 Partial 账单
|
||||
|
||||
业务人员**定期查看** Partial 状态账单:
|
||||
|
||||
```sql
|
||||
SELECT bill_no, resident_id, amount, paid_amount, (amount - paid_amount) AS remaining
|
||||
FROM acc_bills
|
||||
WHERE status = 'partial'
|
||||
AND community_id = ?
|
||||
ORDER BY due_at ASC;
|
||||
```
|
||||
|
||||
或后台 → 账单 → 过滤"状态=Partial"列表。
|
||||
|
||||
业务人员对 Partial 业户:
|
||||
|
||||
- 临近 due_at:发提醒
|
||||
- 已逾期:走 [[exception-overdue-bills|催收]]
|
||||
- 业户失联:走 [[suspend-bill|挂起]]
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业户
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Action[CollectPaymentAction]
|
||||
participant DB
|
||||
|
||||
业户->>业务: 我只付 300(账单 800)
|
||||
业务->>Filament: ViewBill → CollectPayment(modal,改金额=300)
|
||||
Filament->>Action: handle(bill, 300, channel)
|
||||
Action->>DB: 1. 建 CO(+300)+ COBill(allocated=300)
|
||||
Action->>DB: 2. Bill.paid_amount = 300
|
||||
Action->>DB: 3. Bill.status: Unpaid → Partial(因 300 < 800)
|
||||
|
||||
Note over Filament: 几周后业户补付
|
||||
|
||||
业户->>业务: 现在付剩下 500
|
||||
业务->>Filament: ViewBill(Partial)→ CollectPayment(默认带 500)
|
||||
Filament->>Action: handle(bill, 500, channel)
|
||||
Action->>DB: 4. 建 CO(+500)+ COBill(allocated=500)
|
||||
Action->>DB: 5. Bill.paid_amount = 800
|
||||
Action->>DB: 6. Bill.status: Partial → Paid
|
||||
```
|
||||
|
||||
## 数据示例(完整流水)
|
||||
|
||||
业户付 ¥300 后:
|
||||
|
||||
### Bill 表
|
||||
|
||||
```
|
||||
id: 12345
|
||||
amount: 800
|
||||
paid_amount: 300
|
||||
status: Partial
|
||||
```
|
||||
|
||||
### CollectionOrderBill(1 条)
|
||||
|
||||
```
|
||||
bill_id: 12345
|
||||
collection_order_id: 67890
|
||||
allocated_amount: 300
|
||||
```
|
||||
|
||||
### CollectionOrder(1 条)
|
||||
|
||||
```
|
||||
id: 67890
|
||||
actual_amount: +300
|
||||
payment_channel: 现金
|
||||
status: Completed
|
||||
```
|
||||
|
||||
### Receipt(1 条)
|
||||
|
||||
```
|
||||
amount: +300
|
||||
line_items: [{ 物业费(5月)分付, 300 }]
|
||||
```
|
||||
|
||||
业户再付 ¥500 后:
|
||||
|
||||
### Bill 表(更新)
|
||||
|
||||
```
|
||||
paid_amount: 800
|
||||
status: Paid ← 收齐
|
||||
```
|
||||
|
||||
### CollectionOrderBill(2 条总)
|
||||
|
||||
```
|
||||
1: bill_id=12345, collection_order_id=67890, allocated=300
|
||||
2: bill_id=12345, collection_order_id=67891, allocated=500
|
||||
```
|
||||
|
||||
业户两次付,流水**完整保留**。
|
||||
|
||||
## 部分付的几种异常
|
||||
|
||||
### 异常 1:业户长期不补付
|
||||
|
||||
业户付了 ¥300 后**消失**,剩余 ¥500 长期不付:
|
||||
|
||||
| 处置 | 路径 |
|
||||
|---|---|
|
||||
| 临时不催(可能有困难)| 不动,等业户主动 |
|
||||
| 走逾期催收 | [[exception-overdue-bills]] |
|
||||
| 业户失联 | [[suspend-bill|挂起]] |
|
||||
| 协议放弃剩余 | [[void-paid-bill|作废]](但已付 ¥300 不退,业务上协商决定)|
|
||||
|
||||
### 异常 2:业户付错金额
|
||||
|
||||
业户原本想付 ¥300 给物业费,误付到了水费:
|
||||
|
||||
- 物业费 Bill 仍 Unpaid
|
||||
- 水费 Bill 多付了(超过 ¥54)
|
||||
|
||||
处置:看实施细节,可能需手工调整 CollectionOrderBill(危险,破坏审计)。**预防**:Modal 提交前确认 Bill ID。
|
||||
|
||||
### 异常 3:业务人员录错 paid_amount
|
||||
|
||||
业务人员收 ¥500 现金,误录入 ¥300:
|
||||
|
||||
- Bill.paid_amount = 300(应该 500)
|
||||
- 物业账面少记 ¥200 → 账上 vs 银行不一致
|
||||
|
||||
处置:走 [[void-paid-bill|作废]] 原 CollectionOrder + 重录,或运维 tinker 修字段。
|
||||
|
||||
## Partial 的边界
|
||||
|
||||
> [!info] 严格部分付 vs 宽松部分付
|
||||
>
|
||||
> 严格实现(本系统倾向):
|
||||
> - Modal `amount` 校验 `<= remaining`
|
||||
> - 不允许超付(避免凭空多记 paid_amount)
|
||||
>
|
||||
> 宽松实现:
|
||||
> - 允许超付 → 多余部分转入业户预存款 / 留作"未分配收款"
|
||||
>
|
||||
> 当前实现看 `CollectPaymentAction` 代码。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 多次部分付的 Receipt 是合并还是分开?
|
||||
> **分开**(每次收款一张 Receipt)。业户拿到的是两张:
|
||||
>
|
||||
> - Receipt 1:¥300(5/15 现金付)
|
||||
> - Receipt 2:¥500(5/30 现金付)
|
||||
>
|
||||
> 不合并(合并破坏每次收款的独立凭证)。
|
||||
|
||||
> [!question] Partial 账单挂起后能恢复继续收吗?
|
||||
> 可以。走 [[suspend-bill|挂起]] → [[resume-bill|恢复]] → 状态智能判定为 Partial(因有付款)→ 继续收款。
|
||||
|
||||
> [!question] 部分付能预存款抵吗?
|
||||
> 看实施。理论上业户预存款余额 ¥200 + 账单剩余 ¥500 = 预存款抵 ¥200 → 仍 Partial(还差 ¥300)。需要部分抵扣功能(见 [[../prepaid/consume-multiple-bills-priority]] 部分抵讨论)。当前不一定支持。
|
||||
|
||||
> [!question] 业户付的钱比账单总额还多(超付)?
|
||||
> 见上方"严格 vs 宽松"段。严格实现拒绝;宽松实现允许多付转入预存款。
|
||||
|
||||
> [!question] activitylog 怎么追踪 Partial 状态历史?
|
||||
> 每次 CollectPayment 都有 log(event=collected),可看到状态从 Unpaid → Partial 的转变。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 单张全付 → [[collect-payment-single]](标准路径)
|
||||
- 批量付(多张一次)→ [[collect-payment-batch]]
|
||||
- 长期 Partial 无解 → [[suspend-bill]] / [[void-paid-bill]]
|
||||
- 部分付逾期催收 → [[exception-overdue-bills]]
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[collect-payment-single]]
|
||||
- [[suspend-bill]]
|
||||
- [[exception-overdue-bills]]
|
||||
- [[void-paid-bill]]
|
||||
253
prop-acc/scenarios/billing/resume-bill.md
Normal file
253
prop-acc/scenarios/billing/resume-bill.md
Normal 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 同类对比)
|
||||
220
prop-acc/scenarios/billing/split-bill.md
Normal file
220
prop-acc/scenarios/billing/split-bill.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 拆账单(SplitBillAction)
|
||||
aliases:
|
||||
- 拆账单
|
||||
- 账单分摊
|
||||
- SplitBillAction
|
||||
- split-bill
|
||||
- 场景-拆账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 调整
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:拆账单(SplitBillAction)
|
||||
|
||||
业户**多人共住一户**(房东 + 租户 / 合租),物业费 / 水电气**按比例分摊**到各自账户,业务人员走 `SplitBillAction` 把一张账单**拆成多张**。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境
|
||||
> 12-3-501 房屋:
|
||||
>
|
||||
> - 房东陈先生(产权人)
|
||||
> - 租户李先生(2026 年 1 月起租)
|
||||
>
|
||||
> 合同约定:**物业费房东付,水电气租户付**。但系统月初按"业户=陈先生"批量生成的:
|
||||
>
|
||||
> - 5 月物业费 ¥800(应给陈先生)
|
||||
> - 5 月水费 ¥54(实际应租户付)
|
||||
> - 5 月电费 ¥168(实际应租户付)
|
||||
> - 5 月燃气 ¥30(实际应租户付)
|
||||
>
|
||||
> 业务人员王主管要把后 3 张账单**重新分给租户李先生**(从陈先生的账户拆出去)。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:确认拆单需求
|
||||
|
||||
业户来电话 / 合同约定:
|
||||
|
||||
- 房东本人来报备"水电气以后租户付"
|
||||
- 看合同 / 租赁备案
|
||||
- 确认租户身份(可能已在 `community_user_profiles` 注册)
|
||||
|
||||
### 第 2 步:逐张拆单
|
||||
|
||||
**(本场景默认逐张拆,不是一次 SplitAll)**
|
||||
|
||||
后台 → 账单 → 找到 5 月水费 Bill → 进 `ViewBill` → 点 `SplitBillAction`(标签"拆账单")。
|
||||
|
||||
> [!warning] 按钮可见性
|
||||
> 看 `SplitBillAction` 守护(应该是 Unpaid + 业务权限)。Paid / Void 状态可能不允许拆(已付的钱难撤)。
|
||||
|
||||
### 第 3 步:Modal 填参数
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **原账单** | 5 月水费 ¥54(陈先生)|
|
||||
| **拆分方式** | 按比例 / 按金额 / 按目标业户 |
|
||||
| **新业户** | 李先生(下拉选)|
|
||||
| **拆给新业户的金额** | ¥54(全部 → 全转给李先生 = 业户更换;或部分 → 拆成两张)|
|
||||
| **拆分原因** | 必填,如 "房东与租户合同约定:水费由租户付"|
|
||||
|
||||
### 第 4 步:提交
|
||||
|
||||
`SplitBillAction` 业务逻辑(看实现):
|
||||
|
||||
**模式 1:全转给新业户**(本场景常见)
|
||||
|
||||
```
|
||||
- 原 Bill #X(陈先生):状态翻 Void(原账单作废)+ 标 split_to=新 Bill ID
|
||||
- 新建 Bill #Y(李先生):amount=54, status=Unpaid
|
||||
- 关联(meta 记拆分来源)
|
||||
```
|
||||
|
||||
**模式 2:部分拆分**(罕见)
|
||||
|
||||
```
|
||||
- 原 Bill #X(陈先生):amount 改为 30(原 54 - 拆出 24)
|
||||
- 新建 Bill #Y(李先生):amount=24
|
||||
- 两张同存
|
||||
```
|
||||
|
||||
> [!info] 实施细节看代码
|
||||
> `SplitBillAction` 的具体行为(全转 vs 部分拆 vs 按比例)看 `packages/prop-acc/src/Actions/Bills/SplitBillAction.php`。本文按业务场景描述。
|
||||
|
||||
### 第 5 步:通知双方
|
||||
|
||||
- 陈先生:"5 月水费已转给租户李先生付,您不必付了"
|
||||
- 李先生:"您 5 月水费 ¥54 新账单已生成,请于 6 月 15 日前付清"
|
||||
|
||||
### 第 6 步:重复对电费 / 燃气
|
||||
|
||||
逐张拆。3 张全拆完后:
|
||||
|
||||
- 陈先生只欠 5 月物业费 ¥800
|
||||
- 李先生欠 5 月水费 + 电费 + 燃气 ¥252
|
||||
|
||||
## 系统流程(全转模式)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Action[SplitBillAction]
|
||||
participant DB
|
||||
participant Activity[activitylog]
|
||||
|
||||
业务->>Filament: ViewBill(陈先生水费)→ SplitBillAction
|
||||
Filament->>Action: handle(bill=陈先生水费, mode=Full, target=李先生, reason)
|
||||
Action->>Action: 校验 bill.status (Unpaid)
|
||||
Action->>Action: 校验 target_resident 在同 community
|
||||
|
||||
Action->>DB: 开启事务
|
||||
Action->>DB: 1. 原 Bill 翻 Void + meta.split_to=新 ID
|
||||
Action->>DB: 2. 新建 Bill(target=李先生, amount=54, status=Unpaid, meta.split_from=原 ID)
|
||||
Action->>Activity: log(event=split, properties)
|
||||
Action->>DB: 提交
|
||||
|
||||
Filament-->>业务: 跳转新 Bill
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 陈先生(原账单业户)
|
||||
|
||||
收到通知:
|
||||
|
||||
> 陈先生您好,您的 5 月水费 ¥54 已根据合同拆给租户李先生付。您原账单已作废,无需付款。
|
||||
|
||||
### 李先生(新账单业户)
|
||||
|
||||
收到通知:
|
||||
|
||||
> 李先生您好,您 5 月水费 ¥54 账单已生成(由 12-3-501 房屋的合同拆分而来),请于 6 月 15 日前付清。
|
||||
|
||||
## 拆单的几种业务场景
|
||||
|
||||
| 场景 | 频率 | 拆法 |
|
||||
|---|---|---|
|
||||
| **房东 / 租户合同分摊** | 中(本场景)| 按费用类型分,水电气给租户 |
|
||||
| **多业主分摊**(共有产权)| 罕见 | 按比例 |
|
||||
| **公摊费用追溯** | 中(本月才发现某费用应分摊)| 按户数 |
|
||||
| **企业内部子公司分摊**(商铺)| 罕见 | 按合同 |
|
||||
|
||||
## 与"作废 + 重建"的对比
|
||||
|
||||
| 维度 | 拆账单(SplitBillAction)| 作废 + 手动重建 |
|
||||
|---|---|---|
|
||||
| 单一操作 | ✅(一个 Action 完成)| ❌(走 2-3 个 Action)|
|
||||
| 关联追溯 | ✅(meta 标 split_from/to)| ❌(无系统关联)|
|
||||
| 审计 | activitylog event=split | 多条 activitylog(void + create)|
|
||||
| 灵活性 | 受 SplitBillAction 限制 | 更灵活(可改任意字段)|
|
||||
| 推荐场景 | 简单拆分(全转 / 部分按金额)| 复杂拆分 / 拆完还要改其他字段 |
|
||||
|
||||
## 拆单后已付款的复杂情况
|
||||
|
||||
> [!warning] 已付款 Bill 拆单的复杂度
|
||||
>
|
||||
> 如果原账单**已付一部分**(Partial 状态):
|
||||
>
|
||||
> - 原账单作废 → 已付的部分怎么办?
|
||||
> - 退还给原业户 → 走作废 + 退款([[void-paid-bill]] 类似)
|
||||
> - 新账单上记 paid_amount? 不行(原业户付的钱不该算到新业户)
|
||||
>
|
||||
> 实施上 `SplitBillAction` 可能**不允许 Partial 状态拆**(只允许 Unpaid)。看具体实现。
|
||||
>
|
||||
> **推荐**:拆单前先处理已付款(作废 + 退款 / 退现金后)再拆。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 拆给的业户不存在(系统里没注册)怎么办?
|
||||
> 先建业户档案(community 模块的 `community_user_profiles`)→ 然后拆。
|
||||
|
||||
> [!question] 拆错了(应该拆给李先生,选成王先生)?
|
||||
> 看实施:
|
||||
> - 若新 Bill 仍 Unpaid → 走 [[delete-bill-unpaid|删除]] + 重拆
|
||||
> - 若已 Paid → 麻烦,需走作废 + 退款 + 重拆
|
||||
|
||||
> [!question] 按比例自动拆所有账单(房东 50% / 租户 50% 物业费)?
|
||||
> 当前 `SplitBillAction` 应是**逐张操作**。按比例批量拆需要业务方提需求 + 加 `BulkSplitBillsAction`(待实现)。
|
||||
|
||||
> [!question] 拆单的合规性?
|
||||
> 物业要确保:
|
||||
> - 业户合同明确分摊条款
|
||||
> - 双方书面同意拆分
|
||||
> - 物业内部审批留底
|
||||
>
|
||||
> 系统层面只管账单数据,合规由物业流程保障。
|
||||
|
||||
> [!question] 一张账单能拆成 3+ 份吗(多人合租 3 人均摊)?
|
||||
> 看 SplitBillAction 设计。简单实现是**一次拆 2 份**(原账单 + 新账单)。要拆 3 份需 2 次操作:
|
||||
>
|
||||
> 1. 第 1 次:原账单(¥54)→ 拆出 ¥18(给租户 A),原账单剩 ¥36
|
||||
> 2. 第 2 次:原账单(¥36)→ 拆出 ¥18(给租户 B),原账单剩 ¥18(给房东自己)
|
||||
>
|
||||
> 累加得到三份 ¥18 各自归一人。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 拆错了 → 删除 / 作废新 Bill,原 Bill 状态可能要恢复(看实施)
|
||||
- 已付的拆 → 走 [[void-paid-bill]] 流程,复杂
|
||||
- 简单"换业户"(不拆,只改账单的 resident_id)→ 可能用 Edit Bill 直接改(若 Policy 允许),但**强烈不推荐**(破坏追溯)→ 用拆单更标准
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[delete-bill-unpaid]]
|
||||
- [[void-paid-bill]]
|
||||
- [[suspend-bill]]
|
||||
- [[bill-vs-collection-order]]
|
||||
241
prop-acc/scenarios/billing/suspend-bill.md
Normal file
241
prop-acc/scenarios/billing/suspend-bill.md
Normal file
@@ -0,0 +1,241 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 挂起账单(业户失联/纠纷)
|
||||
aliases:
|
||||
- 挂起账单
|
||||
- SuspendBillAction
|
||||
- 暂停收款
|
||||
- suspend-bill
|
||||
- 场景-挂起账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 调整
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:挂起账单(业户失联 / 纠纷)
|
||||
|
||||
业户**与物业有纠纷**或**长期失联**,该账单暂时**不应被收款 / 不应被催收**,但又不能直接作废(纠纷可能解决,业户可能出现)。走 `SuspendBillAction` 把账单挂起,状态 Unpaid → Suspended,后续可 [[resume-bill|恢复]]。
|
||||
|
||||
## 典型情境
|
||||
|
||||
> [!example] 真实情境(一):业户失联
|
||||
> 王先生(15-7-203)三个月没缴物业费(累计 3 张账单 ¥2,400 Unpaid),物业多次联系电话不通、上门无人。物业认为业户可能搬走 / 失联 / 出国,**先把这 3 张账单挂起**,避免:
|
||||
>
|
||||
> - 月度报表"应收账款"虚高(挂着收不回来)
|
||||
> - 催收资源浪费(联系不上的还反复发短信)
|
||||
> - 业户突然回来时账单仍在(不会变作废)
|
||||
|
||||
> [!example] 真实情境(二):纠纷期间
|
||||
> 陈先生认为 5 月物业费 ¥800 不合理(物业服务质量纠纷),拒绝付。物业 / 业主委员会调解中,**挂起该账单**,等调解结果:
|
||||
> - 调解物业胜诉 → [[resume-bill|恢复]] → 业户付
|
||||
> - 调解业户胜诉 → [[void-paid-bill|作废]] → 不收
|
||||
> - 调解妥协 → 走拆账单 / 重新算金额
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 第 1 步:确认挂起场景
|
||||
|
||||
| 场景 | 是否走挂起 |
|
||||
|---|---|
|
||||
| 业户失联 1-3 个月 | ✅ 挂起(等业户出现) |
|
||||
| 业户失联 >6 个月 | 可考虑作废(看物业政策)|
|
||||
| 业户纠纷中 | ✅ 挂起 |
|
||||
| 业户拒不付 | 不挂起,走逾期催收([[exception-overdue-bills]]) |
|
||||
| 业户真的搬走永久不再来 | 走作废 / 法律手段 |
|
||||
|
||||
### 第 2 步:打开账单
|
||||
|
||||
后台 → 账单 → 找到 Unpaid Bill → 进 `ViewBill`。
|
||||
|
||||
### 第 3 步:点击 `SuspendBillAction`(标签"挂起")
|
||||
|
||||
> [!warning] 按钮可见性
|
||||
> 守护:`bill.status === Unpaid || Partial` + Policy `->authorize('suspend')`。Paid / Void / Suspended 状态灰化。
|
||||
|
||||
Modal 表单:
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **挂起原因(reason)** | **必填且详细**,如 "业户失联 3 个月,电话不通 + 上门无人 + 微信无回应" |
|
||||
|
||||
### 第 4 步:提交
|
||||
|
||||
`SuspendBillAction` 业务层逻辑:
|
||||
|
||||
```php
|
||||
class SuspendBillAction
|
||||
{
|
||||
public function handle(Bill $bill, string $reason, User $user): void
|
||||
{
|
||||
if (! in_array($bill->status, [BillStatus::Unpaid, BillStatus::Partial])) {
|
||||
throw new RuntimeException("账单状态不可挂起");
|
||||
}
|
||||
|
||||
$bill->update([
|
||||
'status' => BillStatus::Suspended,
|
||||
'meta' => array_merge($bill->meta ?? [], [
|
||||
'suspend_reason' => $reason,
|
||||
'suspended_at' => now(),
|
||||
'suspended_by' => $user->id,
|
||||
]),
|
||||
]);
|
||||
|
||||
activity()
|
||||
->performedOn($bill)
|
||||
->causedBy($user)
|
||||
->withProperties([
|
||||
'reason' => $reason,
|
||||
'from_status' => $bill->getOriginal('status'),
|
||||
'to_status' => BillStatus::Suspended->value,
|
||||
'bill_no' => $bill->bill_no,
|
||||
])
|
||||
->event('suspended')
|
||||
->log('账单已挂起');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第 5 步:通知业户(可选)
|
||||
|
||||
- 失联场景:不通知(联系不上,无意义)
|
||||
- 纠纷场景:通知"您的账单已挂起,等调解结果"
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Action[SuspendBillAction]
|
||||
participant DB
|
||||
participant Activity
|
||||
|
||||
业务->>Filament: ViewBill → SuspendBillAction(modal)
|
||||
Filament->>Action: handle(bill, reason, user)
|
||||
Action->>Action: 校验 status Unpaid/Partial
|
||||
Action->>DB: 开启事务
|
||||
Action->>DB: 1. bill.status=Suspended
|
||||
Action->>DB: 2. bill.meta.suspend_reason / suspended_at / suspended_by
|
||||
Action->>Activity: 3. log(event=suspended)
|
||||
Action->>DB: 提交
|
||||
|
||||
Filament-->>业务: 成功
|
||||
```
|
||||
|
||||
## 挂起后的能力对照
|
||||
|
||||
| 操作 | Unpaid / Partial | **Suspended** |
|
||||
|---|---|---|
|
||||
| `CollectPaymentAction`(收款)| ✅ | ❌(`canBePaid=false`)|
|
||||
| `CollectPaymentAction`(预存款抵)| ✅ | ❌ |
|
||||
| `SuspendBillAction`(再挂起)| ✅ | ❌(已是 Suspended)|
|
||||
| `ResumeBillAction`(恢复)| ❌ | ✅ |
|
||||
| `VoidBillAction`(作废)| ✅(canBeVoided=true)| ✅ |
|
||||
| 看账单 / 看历史 | ✅ | ✅(只读)|
|
||||
| 出现在 `OverdueBillsListWidget` | ✅(若到期)| ❌(挂起不算逾期)|
|
||||
| 出现在月度账单清单 | ✅ | ✅(标 Suspended)|
|
||||
|
||||
> [!info] Suspended 与 Overdue 的关系
|
||||
> 挂起的账单**不算逾期**(catch up 不会出现在催收清单)。但**期次仍在历史**(归属本月报表)。
|
||||
>
|
||||
> 这是**有意设计**:挂起的目的是停掉催收 + 暂时退出收款流程,但不让账单凭空消失。
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 失联场景(无感)
|
||||
|
||||
业户失联本来就联系不上,业务方决定挂起 → 业户**完全不知道**。等业户出现时:
|
||||
|
||||
- 业户来电话 / 上门
|
||||
- 业务人员说"您有挂起的账单 ¥XXX,我现在帮您恢复 + 收款"
|
||||
- 走 [[resume-bill|恢复]] → 收款
|
||||
|
||||
### 纠纷场景
|
||||
|
||||
业户**可能收到通知**(看物业政策):
|
||||
|
||||
> 陈先生您好,您的 5 月物业费账单已挂起(暂停收款),等纠纷调解结果。预计 X 月 Y 日前出结果。
|
||||
|
||||
业户:
|
||||
|
||||
- 知情 + 等待
|
||||
- 期间不会被催收
|
||||
- 调解后走 [[resume-bill|恢复]] 或 [[void-paid-bill|作废]]
|
||||
|
||||
## 与"作废"的对比
|
||||
|
||||
| 维度 | **挂起(Suspended,本场景)** | [[void-paid-bill|作废 Void]] |
|
||||
|---|---|---|
|
||||
| 是否可恢复 | ✅ 走 [[resume-bill]]| ❌ 终态 |
|
||||
| 业务场景 | 临时暂停(纠纷 / 失联)| 永久消除 |
|
||||
| 报表归属 | 仍在本期(标 Suspended)| 标 Void(可过滤) |
|
||||
| 后续是否能收款 | 恢复后能 | 不能(已 Void)|
|
||||
|
||||
**简单判断**:**不确定后续怎么办 → 挂起**;**确定不再收 → 作废**。
|
||||
|
||||
## 长期 Suspended 的处理
|
||||
|
||||
挂起后**长期没恢复 / 没作废**的账单,业务上需要定期 review:
|
||||
|
||||
| 挂起时长 | 推荐处置 |
|
||||
|---|---|
|
||||
| < 1 月 | 正常等待 |
|
||||
| 1-3 月 | 评估业户情况(再次联系)|
|
||||
| 3-6 月 | 决定:恢复(若有进展)/ 作废(若无希望)|
|
||||
| > 6 月 | 通常作废(或走法律手段)|
|
||||
|
||||
可加 audit 场景:**"长期挂起账单清单"**(类似 [[../deposit/audit-long-pending-accounts]])。当前 issue.md 未明确实施,可作为未来扩展。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 挂起原因填得详细到什么程度?
|
||||
> **越详细越好**。审计要求 + 业户事后查询 + 业务复盘都需要。推荐结构:
|
||||
>
|
||||
> - 触发事件(纠纷 / 失联 / 其他)
|
||||
> - 已采取的联系措施(电话 / 上门 / 微信)
|
||||
> - 业务上的预期(等调解 / 等业户回来 / 等司法)
|
||||
|
||||
> [!question] 挂起的账单已经有部分付(Partial)?
|
||||
> 看实施。SuspendBillAction 可能允许(从 Partial → Suspended)。已付的部分**不退**(只是暂停后续收款)。
|
||||
|
||||
> [!question] 同一业户多张挂起,能批量挂吗?
|
||||
> 当前**无批量挂起 UI**。逐张走 SuspendBillAction,效率低但可控。
|
||||
>
|
||||
> 可加 `BulkSuspendBillsAction`(类似批删的设计),业务方提需求时实施。
|
||||
|
||||
> [!question] 挂起影响 prepaid 自动抵扣 job?
|
||||
> 是。job 应**跳过 Suspended 状态**的账单。设计上看 [[../prepaid/auto-deduction-design]]。
|
||||
|
||||
> [!question] activitylog 怎么查挂起记录?
|
||||
> ```sql
|
||||
> SELECT * FROM activity_log
|
||||
> WHERE event = 'suspended'
|
||||
> AND created_at BETWEEN ? AND ?
|
||||
> ORDER BY created_at DESC;
|
||||
> ```
|
||||
>
|
||||
> 详见 [[audit-activitylog-trace]]。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 业户出现/纠纷解决 → [[resume-bill]]
|
||||
- 确定不再收 → [[void-paid-bill]]
|
||||
- 业务上误挂起 → [[resume-bill]] 撤回(reason 改"误操作")
|
||||
- 长期挂起无解 → 作废 / 法律手段(待业务方明确)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[resume-bill]]
|
||||
- [[void-paid-bill]]
|
||||
- [[exception-overdue-bills]]
|
||||
- [[audit-activitylog-trace]]
|
||||
- [[../deposit/freeze-during-dispute]](类似的"暂停"模式对比)
|
||||
269
prop-acc/scenarios/billing/void-paid-bill.md
Normal file
269
prop-acc/scenarios/billing/void-paid-bill.md
Normal file
@@ -0,0 +1,269 @@
|
||||
---
|
||||
title: prop-acc · billing · 场景 - 作废已付账单(走作废 + 退款)
|
||||
aliases:
|
||||
- 作废账单
|
||||
- VoidBillAction
|
||||
- void-paid-bill
|
||||
- 作废加退款
|
||||
- 场景-作废已付账单
|
||||
tags:
|
||||
- 场景
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 作废
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 场景:作废已付账单(走作废 + 退款)
|
||||
|
||||
业户**已付**的账单需要消除(误开账单业户付了 / 业户事后投诉成功 / 调解结果物业认错)。走**作废 + 退款**组合,留状态留审计 + 退还业户。
|
||||
|
||||
> [!warning] 当前实施状态
|
||||
> `VoidBillAction` 本身**只翻状态 + 留 meta + 写 activitylog**(`canBeVoided=true` for **非 Paid**)。
|
||||
>
|
||||
> **Paid 状态 `canBeVoided=false`**,意味着 `VoidBillAction` 不直接处理 Paid 账单的作废。需要走**类似 meter 的修正流程**:
|
||||
>
|
||||
> - 当前手工 / tinker(运维操作)
|
||||
> - 未来扩展 `VoidPaidBillAction` 自动化(包含退款 + 红字 CO 等)
|
||||
>
|
||||
> 详见 [[delete-vs-void-dual-track]]"`canBeVoided` 的微妙之处"段。
|
||||
|
||||
## 典型情境
|
||||
|
||||
### 情境 1:Partial 状态作废(系统支持)
|
||||
|
||||
> [!example] 真实情境
|
||||
> 陈先生 5 月物业费 ¥800,部分付了 ¥300(状态 Partial)。后续物业承认服务有问题,**双方协议作废账单**,退 ¥300 给业户。
|
||||
>
|
||||
> 业务流程:
|
||||
> 1. 走 `VoidBillAction`(Partial → Void)
|
||||
> 2. **手工配套退款**(给陈先生退 ¥300 现金 / 微信)
|
||||
> 3. (理想)系统自动建红字 CollectionOrder(待扩展)
|
||||
|
||||
### 情境 2:Paid 状态作废(当前需手工)
|
||||
|
||||
> [!example] 真实情境
|
||||
> 张阿姨 5 月物业费 ¥800 已付清(Paid)。物业发现金额算错了(应该 ¥600),**多收 ¥200**。要全额作废 + 退还 ¥800 + 重新建一张 ¥600 账单。
|
||||
>
|
||||
> 当前流程(因 canBeVoided=false for Paid,VoidBillAction 不允许):
|
||||
> 1. **运维 tinker 操作**:把 Bill 状态强制改为 Void + 记 meta
|
||||
> 2. **手工建红字 CollectionOrder**(¥-800)+ Receipt(红字)
|
||||
> 3. **物业线下退款** ¥800 给业户
|
||||
> 4. **手工建新 Bill** ¥600
|
||||
> 5. 业户付 ¥600
|
||||
>
|
||||
> 整个流程**复杂、易出错、无 UI**。issue.md Q6 未明确实施时间,标"待业务方明确"。
|
||||
|
||||
## 业务人员视角(Partial 作废)
|
||||
|
||||
### 第 1 步:确认作废
|
||||
|
||||
- 协议作废(业户与物业书面达成)
|
||||
- 调解 / 司法判决物业方有责
|
||||
- 业务上的特殊情况
|
||||
|
||||
### 第 2 步:打开账单
|
||||
|
||||
后台 → 账单 → 找到 Partial Bill → 进 `ViewBill`。
|
||||
|
||||
### 第 3 步:点击 `VoidBillAction`(标签"作废")
|
||||
|
||||
> [!warning] 按钮可见性
|
||||
> 守护:`canBeVoided()` = 非 Paid 非 Void + `->authorize('void')`(`bill.void` 权限)。
|
||||
|
||||
Modal 表单:
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **作废原因(reason)** | **必填**,如 "调解结果:物业服务质量有问题,账单作废 + 退还已付款" |
|
||||
|
||||
### 第 4 步:提交
|
||||
|
||||
`VoidBillAction` 业务逻辑(详见 [[delete-vs-void-dual-track]]):
|
||||
|
||||
```php
|
||||
$bill->update([
|
||||
'status' => BillStatus::Void,
|
||||
'meta' => array_merge($bill->meta ?? [], [
|
||||
'voided_reason' => $reason,
|
||||
'voided_at' => now(),
|
||||
'voided_by' => $user->id,
|
||||
]),
|
||||
]);
|
||||
|
||||
activity()->performedOn($bill)->causedBy($user)
|
||||
->withProperties([
|
||||
'reason' => $reason,
|
||||
'from_status' => $bill->getOriginal('status'),
|
||||
'to_status' => 'Void',
|
||||
'bill_no' => $bill->bill_no,
|
||||
'amount' => $bill->amount,
|
||||
'paid_amount' => $bill->paid_amount, // 关键:有付款需要后续退
|
||||
])
|
||||
->event('voided')
|
||||
->log('账单已作废');
|
||||
```
|
||||
|
||||
### 第 5 步:手工退款(若 paid_amount > 0)
|
||||
|
||||
VoidBillAction **本身不退钱**。业务人员后续:
|
||||
|
||||
1. 看 activitylog 的 `paid_amount` 字段 = ¥300
|
||||
2. 联系业户确认退款方式(微信 / 现金 / 银行转账)
|
||||
3. 走线下退款(物业财务实际打钱)
|
||||
4. **理想**:建红字 CollectionOrder(`actual_amount=-300`,`type=Bill`,关联到该 Bill)+ Receipt 红字(待扩展自动化)
|
||||
|
||||
> [!info] 当前简化做法
|
||||
> Bill 作废后:
|
||||
> - Bill 状态 = Void
|
||||
> - CollectionOrderBill 关联**不动**(审计需要)
|
||||
> - 业务人员**线下手工退款**给业户
|
||||
> - 系统中没有红字 CO / 红字 Receipt(待 `VoidPaidBillAction` 自动化)
|
||||
>
|
||||
> 财务账面**临时不一致**(Bill 是 Void 但 CO 仍是 Completed),需事后人工对账修复。
|
||||
>
|
||||
> **业务方提需求时,优先级会上来。**
|
||||
|
||||
### 第 6 步:通知业户
|
||||
|
||||
> 陈先生您好,您的 5 月物业费账单已作废,已付的 ¥300 我们将退还您。请确认收款方式。
|
||||
|
||||
## 业务人员视角(Paid 作废,当前 tinker 流程)
|
||||
|
||||
详见上方"情境 2"。当前**没自动化**:
|
||||
|
||||
1. 评估业务必要性(确认无误后操作)
|
||||
2. 联系运维 tinker:
|
||||
```php
|
||||
$bill->update([
|
||||
'status' => BillStatus::Void,
|
||||
'meta' => array_merge($bill->meta ?? [], [
|
||||
'voided_reason' => $reason,
|
||||
'voided_at' => now(),
|
||||
'voided_by' => $user->id,
|
||||
'manual_void' => true, // 标记是 tinker 手工作废
|
||||
]),
|
||||
]);
|
||||
```
|
||||
3. 走完整退款流程(线下退 + 系统记一笔红字 CO/Receipt 若可能)
|
||||
4. 重建账单(走 [[create-single-bill-manual]])
|
||||
5. 业户付新账单
|
||||
|
||||
## 系统流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务
|
||||
participant Filament
|
||||
participant Action[VoidBillAction]
|
||||
participant DB
|
||||
participant Activity
|
||||
|
||||
业务->>Filament: ViewBill(Partial)→ VoidBillAction
|
||||
Filament->>Action: handle(bill, reason, user)
|
||||
Action->>Action: 校验 canBeVoided(非 Paid 非 Void)
|
||||
Action->>DB: 1. bill.status = Void + meta
|
||||
Action->>Activity: 2. log(event=voided, paid_amount=300)
|
||||
Filament-->>业务: 成功
|
||||
|
||||
业务->>业务: 看 activitylog paid_amount=300
|
||||
业务->>业户: 线下退款 + 通知
|
||||
|
||||
Note over Action: 注:不自动建红字 CO/Receipt
|
||||
Note over Action: 待 VoidPaidBillAction 实现
|
||||
```
|
||||
|
||||
## 业户视角
|
||||
|
||||
### 您会感受到什么
|
||||
|
||||
- 收到通知"您的账单 #XXX 已作废,理由 XXX"
|
||||
- 已付的钱**未来几天**收到退款
|
||||
- 收到收据备注(若系统支持)
|
||||
- 小程序账单状态:Partial → Void
|
||||
|
||||
### 您要做什么
|
||||
|
||||
- 确认收款方式
|
||||
- 接收退款(银行 / 微信 / 现金)
|
||||
- 如有疑问联系物业
|
||||
|
||||
## 与 prop-acc 其他作废的对比
|
||||
|
||||
| 模块 | 作废 / void 机制 |
|
||||
|---|---|
|
||||
| **billing(本)** | `VoidBillAction`(非 Paid 非 Void)+ 手工退款配套(Paid 待扩展) |
|
||||
| deposit | 无单独 void;用 [[../deposit/force-close-refund|ForceClose refund]] 等机制 |
|
||||
| prepaid | 无 void;走 [[../prepaid/refund-partial-after-consume|refund]] |
|
||||
| meter | 无 void(reading 不可改,见 [[../meter/exception-readings-locked-after-bill]])|
|
||||
| adhoc | 走 [[../adhoc/cancel-amount-error-redo|VoidAction]](类似 billing 设计)|
|
||||
|
||||
bill 的 void 是**最完整的"账单作废"设计**,但 Paid 的作废**仍待补**(整个 prop-acc 通用问题:已收款的"反向"流程都不够成熟)。
|
||||
|
||||
## 已知限制(issue.md Q6 待补)
|
||||
|
||||
- **VoidBillAction 不处理 Paid 状态**:`canBeVoided=false`,需走专门流程(未实现)
|
||||
- **作废后红字 CO + Receipt 自动生成**:未实现,需手工
|
||||
- **退款金额自动算 / 自动建红字凭证**:未实现
|
||||
- **BulkRefundBillsAction**(批量退款):未实现,issue.md 标优先级低
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 作废后业户已付款怎么记账?
|
||||
> 当前(简版):
|
||||
> - Bill 状态 = Void
|
||||
> - CollectionOrderBill 关联保留(`allocated_amount=300`)
|
||||
> - 物业账面"已收 ¥300"(从 CO 角度)= 实际"应退给业户"
|
||||
>
|
||||
> 财务月度对账时需**人工识别这种情况**(账上有钱但 Bill 已 void = 待退款)。
|
||||
>
|
||||
> 未来自动化后:作废 + 同时建红字 CO(¥-300)抵消原 CO,账面归零。
|
||||
|
||||
> [!question] 作废能撤销吗?
|
||||
> 不能(Void 是终态,详见 [[bill-six-state-machine]])。如需"恢复":新建一张同信息 Bill。
|
||||
|
||||
> [!question] 已付 + 已被预存款抵的 Bill 作废,怎么退到预存款?
|
||||
> 自动化未实现。手工流程:
|
||||
>
|
||||
> 1. tinker 作废 Bill
|
||||
> 2. 给业户预存款手工 deposit(走 [[../prepaid/deposit-additional-topup]] 把 ¥800 充回预存款)
|
||||
> 3. 备注"账单作废退还"
|
||||
>
|
||||
> 系统层面:理想是自动反向(`PrepaidAccount::reverseConsume` 之类),未实现。
|
||||
|
||||
> [!question] activitylog 怎么查作废历史?
|
||||
> ```sql
|
||||
> SELECT * FROM activity_log
|
||||
> WHERE event = 'voided'
|
||||
> AND properties->>'$.bill_no' = ?
|
||||
> ORDER BY created_at DESC;
|
||||
> ```
|
||||
|
||||
> [!question] 业户对作废结果不满意?
|
||||
> 已作废不可逆。业户:
|
||||
> - 走司法 / 仲裁
|
||||
> - 业务方重新协商(可能再建新账单)
|
||||
|
||||
## 异常分支
|
||||
|
||||
- Unpaid 无付款 → [[delete-bill-unpaid|物理删]] 更干净
|
||||
- 批量作废 → [[bulk-delete-batch-mistake]] 选 DeleteAndVoid 模式
|
||||
- 业户拒绝接受作废 → 协商 / 司法
|
||||
- Paid 作废自动化 → 待业务方明确 + 实施 VoidPaidBillAction
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[smart-bulk-delete-design]]
|
||||
- [[bill-six-state-machine]]
|
||||
- [[delete-bill-unpaid]]
|
||||
- [[bulk-delete-batch-mistake]]
|
||||
- [[audit-activitylog-trace]]
|
||||
- [[../meter/exception-readings-locked-after-bill]](类似 已落账修正)
|
||||
- [[../adhoc/cancel-amount-error-redo]](adhoc 同类对比)
|
||||
Reference in New Issue
Block a user