vault backup: 2026-05-26 00:43:11
This commit is contained in:
10
.obsidian/workspace.json
vendored
10
.obsidian/workspace.json
vendored
@@ -197,6 +197,10 @@
|
||||
},
|
||||
"active": "849c5ff8936a2b67",
|
||||
"lastOpenFiles": [
|
||||
"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",
|
||||
@@ -222,16 +226,12 @@
|
||||
"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/exception-cross-community-consume.md",
|
||||
"prop-acc/scenarios/prepaid/close-with-zero-balance-decision.md",
|
||||
"prop-acc/scenarios/prepaid/close-resident-moveout.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",
|
||||
"resident-portal/reference"
|
||||
"resident-portal/scenarios"
|
||||
]
|
||||
}
|
||||
222
prop-acc/concepts/billing/bill-six-state-machine.md
Normal file
222
prop-acc/concepts/billing/bill-six-state-machine.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
title: prop-acc · billing · 账单六状态机
|
||||
aliases:
|
||||
- 账单六状态机
|
||||
- Bill 状态机
|
||||
- BillStatus
|
||||
- 6 个账单状态
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 状态机
|
||||
- 核心概念
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 账单六状态机
|
||||
|
||||
`Bill` 是 prop-acc 状态机**最复杂**的子模块,有 **6 个状态**:**Unpaid / Partial / Paid / Suspended / Processing / Void**。比 deposit / prepaid / meter 的 3-4 态都细,反映账单从生成到关闭的全生命周期 + 异常处置路径。
|
||||
|
||||
## 6 状态速查
|
||||
|
||||
| 状态 | 中文 | 何时进入 | 能做什么 |
|
||||
|---|---|---|---|
|
||||
| `Unpaid` | 未付 | 账单刚创建 | 收款 / 拆单 / 挂起 / 删 / 作废 |
|
||||
| `Partial` | 部分付 | 收到一部分钱(小于账单金额) | 继续收款 / 作废(已收的需退) |
|
||||
| `Paid` | 已付 | 收齐全部金额 | 作废(走退款) |
|
||||
| `Suspended` | 挂起 | 业务人员主动暂停(纠纷 / 失联) | 恢复 / 作废 |
|
||||
| `Processing` | 处理中 | 收款流程中(罕见,如银行扣款排队)| 等回调 |
|
||||
| `Void` | 作废 | 主动 void | 只读,审计可查 |
|
||||
|
||||
## 状态机图
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Unpaid : 创建
|
||||
|
||||
Unpaid --> Partial : 部分收款
|
||||
Unpaid --> Paid : 全额收款
|
||||
Unpaid --> Suspended : suspend(纠纷)
|
||||
Unpaid --> Void : void
|
||||
Unpaid --> [*] : 物理删除(无付款关联)
|
||||
|
||||
Partial --> Partial : 继续收款(仍未付清)
|
||||
Partial --> Paid : 收齐
|
||||
Partial --> Suspended : suspend
|
||||
Partial --> Void : void(已收部分需退)
|
||||
|
||||
Paid --> Void : void(已收全额需退)
|
||||
|
||||
Suspended --> Unpaid : resume(纠纷解决)
|
||||
Suspended --> Partial : resume(回到部分付状态,若之前有付款)
|
||||
Suspended --> Void : void
|
||||
|
||||
Processing --> Paid : 收款回调成功
|
||||
Processing --> Unpaid : 收款回调失败
|
||||
|
||||
Void --> [*]
|
||||
```
|
||||
|
||||
## 守护方法(模型层)
|
||||
|
||||
`Bill` 模型上的辅助方法:
|
||||
|
||||
| 方法 | 返回 true 的状态 | 用途 |
|
||||
|---|---|---|
|
||||
| `canBePaid()` | Unpaid / Partial | CollectPaymentAction 准入 |
|
||||
| `canBeIssued()` | (用于"发出账单"流程,看实现) | — |
|
||||
| `isVoid()` | Void | UI 灰化判断 |
|
||||
| `isPaid()` | Paid | 报表 / 收款率统计 |
|
||||
| `hasAnyPayment()` | 有任何 CollectionOrderBill / prepaid_transaction 关联 | 决定能否物理删 |
|
||||
| `canBeDeleted()` | Unpaid + 无任何付款关联 | DeleteAction 守护 |
|
||||
| `canBeVoided()` | 非 Paid 非 Void | VoidBillAction 守护 |
|
||||
|
||||
> [!warning] `canBeVoided` 的微妙之处
|
||||
> Paid 状态**也可以走 void**(已付全额作废 → 退款),但 `canBeVoided()` 返回 false。原因:
|
||||
> - 已付账单的作废需要**走退款流程**,不是单纯改状态
|
||||
> - 简单的 `VoidBillAction` 只翻状态 + 留 meta,**不处理钱**
|
||||
> - 已付的作废应该走专门的"作废 + 退款"组合流程(类似 [[../meter/exception-readings-locked-after-bill|计量账单修正]])
|
||||
>
|
||||
> 即:`canBeVoided()` 检查的是"能否走简单作废",不是"能否所有方式作废"。
|
||||
|
||||
## 状态转换详解
|
||||
|
||||
### Unpaid → Partial(部分收款)
|
||||
|
||||
业户付了 ¥300 给 ¥800 的账单:
|
||||
|
||||
```
|
||||
- Bill.status: Unpaid → Partial
|
||||
- 建 CollectionOrderBill(allocated_amount=300)
|
||||
- 关联 CollectionOrder(actual_amount=+300)
|
||||
- Bill.paid_amount(若有此字段) = 300
|
||||
- 余 500 待付
|
||||
```
|
||||
|
||||
### Partial → Paid(收齐)
|
||||
|
||||
业户再付 ¥500:
|
||||
|
||||
```
|
||||
- Bill.status: Partial → Paid
|
||||
- 建第 2 条 CollectionOrderBill(allocated_amount=500)
|
||||
- Bill.paid_amount = 800(=账单金额)
|
||||
- 收齐
|
||||
```
|
||||
|
||||
### Unpaid → Suspended(挂起)
|
||||
|
||||
业户与物业纠纷 / 业户失联:
|
||||
|
||||
```
|
||||
- Bill.status: Unpaid → Suspended
|
||||
- 业务人员走 SuspendBillAction
|
||||
- 记 suspend_reason + suspended_at(meta)
|
||||
- 后续 CollectPaymentAction 守护拒绝(canBePaid=false)
|
||||
```
|
||||
|
||||
### Suspended → Unpaid(恢复)
|
||||
|
||||
纠纷解决:
|
||||
|
||||
```
|
||||
- Bill.status: Suspended → Unpaid(或回到 Partial,若之前有付款)
|
||||
- 业务人员走 ResumeBillAction
|
||||
- 记 resume_reason + resumed_at
|
||||
- 后续 CollectPaymentAction 重新可用
|
||||
```
|
||||
|
||||
### Unpaid → Void(作废)
|
||||
|
||||
误开的账单,业户没付:
|
||||
|
||||
```
|
||||
- Bill.status: Unpaid → Void
|
||||
- VoidBillAction 守护通过(canBeVoided=true)
|
||||
- 记 voided_reason + voided_at + voided_by
|
||||
- 业户无影响(没付钱)
|
||||
```
|
||||
|
||||
### Paid → Void(已付作废)
|
||||
|
||||
走专门的"作废 + 退款"流程(未来扩展):
|
||||
|
||||
```
|
||||
- 当前 VoidBillAction **不允许**(canBeVoided=false for Paid)
|
||||
- 需要走类似 meter 的修正流程
|
||||
- 后续若实施 RefundBillAction:作废 + 退款一次性
|
||||
```
|
||||
|
||||
### Unpaid → 物理删除(误建立刻删)
|
||||
|
||||
```
|
||||
- 状态层面消失(从数据库删除)
|
||||
- 走 DeleteAction
|
||||
- 守护:canBeDeleted = Unpaid + 无任何付款关联
|
||||
- 留 activitylog
|
||||
```
|
||||
|
||||
详见 [[delete-vs-void-dual-track]]。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
后台账户列表的状态列对应这 6 个值:
|
||||
|
||||
- 🟢 **Unpaid**:绿色,可操作(收款 / 拆 / 挂起 / 删 / 作废)
|
||||
- 🟡 **Partial**:黄色,部分付,可继续收款
|
||||
- ✅ **Paid**:绿色对号,已完成,无操作
|
||||
- 🧊 **Suspended**:灰色雪花,挂起,只能恢复 / 作废
|
||||
- ⏳ **Processing**:旋转图标,处理中(等回调)
|
||||
- ❌ **Void**:红色叉,已作废,只读
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户感受到的:
|
||||
|
||||
| 状态 | 业户感知 |
|
||||
|---|---|
|
||||
| Unpaid | 收到"账单 ¥800 待付"通知 |
|
||||
| Partial | 看到"已付 ¥300,还差 ¥500"|
|
||||
| Paid | 收到"已付清"通知 + 收据 |
|
||||
| Suspended | (通常通知)"账单挂起,事由 XXX,请联系物业" |
|
||||
| Processing | 几乎不感知(过渡态)|
|
||||
| Void | 收到"账单已作废,理由 XXX" |
|
||||
|
||||
## 与其他模块状态机的对比
|
||||
|
||||
| 模块 | 状态数 | 主要状态 |
|
||||
|---|---|---|
|
||||
| deposit | 3 | Active / Frozen / Closed |
|
||||
| prepaid | 3 | Active / Frozen / Closed |
|
||||
| meter(`is_active`)| 2 | active / inactive |
|
||||
| **bill** | **6** | **Unpaid / Partial / Paid / Suspended / Processing / Void** |
|
||||
|
||||
bill 的复杂度反映**收款全流程**:从应收到已收,中间有挂起、部分付、回调中等多种异常路径。
|
||||
|
||||
## 历史:Policy 修复
|
||||
|
||||
> [!info] issue.md Q6 第一轮发现的漏洞
|
||||
> 原 `BillPolicy` 几乎是空壳(只有 `getPermissionPrefix`),所有授权全靠 `AbstractPolicy` 默认(只查权限,**不查状态**)。
|
||||
>
|
||||
> 修复后补 7 个方法:`update` / `delete` / `deleteAny` / `void` / `collect` / `suspend` / `resume`。每个方法都加了**状态守护**(canBeDeleted / canBeVoided 等)。
|
||||
>
|
||||
> 详见 [[delete-vs-void-dual-track]] 和 [[smart-bulk-delete-design]]。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-types-and-sources]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[smart-bulk-delete-design]]
|
||||
- [[collect-payment-single]]
|
||||
- [[suspend-bill]]
|
||||
- [[void-paid-bill]]
|
||||
- [[../meter/decommission-and-locking]](类似的"状态机+守护"对比)
|
||||
220
prop-acc/concepts/billing/bill-types-and-sources.md
Normal file
220
prop-acc/concepts/billing/bill-types-and-sources.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
title: prop-acc · billing · 账单类型与来源
|
||||
aliases:
|
||||
- 账单类型
|
||||
- 账单来源
|
||||
- BillType
|
||||
- sourceable polymorphism
|
||||
- 周期账单 vs 计量账单
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 数据模型
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 账单类型与来源
|
||||
|
||||
`Bill` 可以由**多种业务源**产生:周期任务(月度物业费)、抄表事件(水电费)、手工建单(临时收费)。系统通过 **`sourceable` 多态关联**统一存储,通过 **`BillType` 枚举**区分类型。
|
||||
|
||||
## 三种主要账单类型
|
||||
|
||||
### 1. 周期账单(Periodic)
|
||||
|
||||
**业务**:按固定周期(月 / 季 / 年)自动生成的账单。最典型:物业费、停车费、有线电视费。
|
||||
|
||||
**生成方式**:`GeneratePeriodicBillsAction`(批量,详见 [[periodic-bill-generation]])。
|
||||
|
||||
**源**:无直接 sourceable(由周期任务批量生成,不指向具体 reading / event)。
|
||||
|
||||
**特点**:
|
||||
- 金额固定(由 RatePlan + 房屋面积 / 停车位等参数算)
|
||||
- 期次明确(`billing_period_start / end`)
|
||||
- 批量生成(一户一张,或合并)
|
||||
|
||||
### 2. 计量账单(Meter-based)
|
||||
|
||||
**业务**:抄表产生的账单。最典型:水费、电费、燃气费。
|
||||
|
||||
**生成方式**:`GenerateBillsFromMeterReadingsAction`([[../meter/bill-generation-pipeline]])。
|
||||
|
||||
**源**:`sourceable_type = MeterReading`,`sourceable_id = reading.id`。
|
||||
|
||||
**特点**:
|
||||
- 金额浮动(按用量算)
|
||||
- 一抄表 → 一 Bill 行
|
||||
- 由 [[../meter/multiplier-and-tiered-pricing|阶梯+倍率+min/max]] 三层算法决定金额
|
||||
|
||||
### 3. 临时账单(Manual / Ad-hoc)
|
||||
|
||||
**业务**:不定期的临时收费,业务人员手工建单。例如:维修费分摊、特别活动费、单次罚款。
|
||||
|
||||
**生成方式**:`CreateBill` Filament 页面手工建。
|
||||
|
||||
**源**:无 sourceable(或自定义 sourceable_type)。
|
||||
|
||||
**特点**:
|
||||
- 金额手填
|
||||
- 单次建单
|
||||
- 适合"系统不知道怎么算"的特殊场景
|
||||
|
||||
## `BillType` 枚举
|
||||
|
||||
> [!info] 实际 BillType 枚举值看代码
|
||||
> 当前 `packages/prop-acc/src/Enums/BillType.php` 的具体枚举值需查代码。常见可能:
|
||||
>
|
||||
> - `Periodic`(周期)
|
||||
> - `Meter`(计量)
|
||||
> - `OneTime`(一次性 / 临时)
|
||||
> - `Adjustment`(调整,罕见)
|
||||
>
|
||||
> 类型字段用于:报表分类、UI 过滤、业务逻辑分支(例如周期账单的合并策略只对 Periodic 类型生效)。
|
||||
|
||||
## Sourceable Polymorphism
|
||||
|
||||
Bill 表用 Laravel 多态关联存"源":
|
||||
|
||||
```
|
||||
Bill
|
||||
├── sourceable_type: enum 'App\Models\MeterReading' / 'App\Jobs\GeneratePeriodicBills' / null
|
||||
├── sourceable_id: 关联记录的 ID
|
||||
└── (other fields)
|
||||
```
|
||||
|
||||
### 类型对照
|
||||
|
||||
| Bill.sourceable_type | Bill.sourceable_id | 业务来源 |
|
||||
|---|---|---|
|
||||
| `MeterReading` | reading.id | 计量账单 |
|
||||
| `PeriodicBillingTask`(假设) | task.id | 周期账单(若设计为指向任务记录)|
|
||||
| `null` | null | 手工建单 / 周期任务但不挂任务表 |
|
||||
| (其他业务可扩展) | (其他)| 未来新业务可加 |
|
||||
|
||||
### 多态的好处
|
||||
|
||||
| 好处 | 说明 |
|
||||
|---|---|
|
||||
| **可追溯** | Bill 上可以查到"这条 Bill 是哪条 reading 生成的" |
|
||||
| **可反查** | reading 上有 `bill_id`,Bill 上有 `sourceable_*`,双向 |
|
||||
| **统一接口** | 各业务源都用同样的 Bill 表,不用为每种源单独建表 |
|
||||
| **扩展友好** | 未来新业务(如"维修工单产生账单")只需:写新 Action + 设 `sourceable_type='WorkOrder'`,不改 Bill 表结构 |
|
||||
|
||||
### 多态的代价
|
||||
|
||||
- ORM 查询稍复杂(需 morphTo 关系)
|
||||
- 关联完整性靠应用层(数据库无强制外键到具体表)
|
||||
- 调试时要看 sourceable_type 才知道是哪种业务
|
||||
|
||||
## 账单核心字段(简表)
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `id` | 账单 ID |
|
||||
| `bill_no` | 账单编号(对外显示,如 `B-202605-501-001`)|
|
||||
| `community_id` | 所属物业项目 |
|
||||
| `asset_id` | 关联房屋 |
|
||||
| `resident_id` | 账单收方业户 |
|
||||
| `fee_type_id` | 费用类型(物业费 / 水费 / 电费 / ...)|
|
||||
| **`bill_type`** | **BillType 枚举(Periodic / Meter / ...)** |
|
||||
| `amount` | 账单金额 |
|
||||
| `paid_amount` | 已付金额(可能字段名不同)|
|
||||
| **`status`** | **BillStatus(6 种,见 [[bill-six-state-machine]])** |
|
||||
| `billing_period_start` / `billing_period_end` | 期次(周期账单用)|
|
||||
| `due_at` | 到期日 |
|
||||
| **`sourceable_type`** / **`sourceable_id`** | **多态源关联** |
|
||||
| `meta` | JSON 扩展(suspend_reason / voided_reason 等审计字段)|
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
后台账单列表(`BillsTable`)列:
|
||||
|
||||
| 列 | 内容 |
|
||||
|---|---|
|
||||
| 账单号 | bill_no |
|
||||
| 业户 | resident.name |
|
||||
| 房号 | asset.code |
|
||||
| 费用类型 | fee_type.name(物业费 / 水费 / ...)|
|
||||
| **类型(BillType)** | 周期 / 计量 / 临时 |
|
||||
| 期次 | period_start ~ period_end |
|
||||
| 金额 | amount |
|
||||
| 状态 | status(6 状态图标)|
|
||||
| 操作 | 收款 / 拆 / 挂起 / 作废 / 删 |
|
||||
|
||||
可按 BillType 过滤:看本月所有"周期账单"或所有"计量账单"。
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**通常不关心账单类型**,只看到:
|
||||
|
||||
```
|
||||
我的账单
|
||||
|
||||
5月 物业费 ¥800 ✅ 已付
|
||||
5月 水费 ¥54 待付
|
||||
5月 电费 ¥168 待付
|
||||
5月 燃气 ¥30 待付
|
||||
```
|
||||
|
||||
每条对应一个 Bill,业户不关心是周期还是计量,只关心"该付多少 / 付了没"。
|
||||
|
||||
## 与 CollectionOrder 的关系
|
||||
|
||||
Bill 是**应收应付**(应该收的钱),CollectionOrder 是**已收**(实际收到的钱)。两者多对多关联(CollectionOrderBill 中间表)。
|
||||
|
||||
详见 [[bill-vs-collection-order]]。
|
||||
|
||||
## 与其他子模块的协作
|
||||
|
||||
| 业务模块 | 与 billing 的关系 |
|
||||
|---|---|
|
||||
| **meter** | 抄表 → 生成计量 Bill(sourceable=MeterReading)|
|
||||
| **prepaid** | 业户预存款抵 Bill(consume,Bill 状态 Unpaid → Paid)|
|
||||
| **deposit** | 通常不抵 Bill(押金是代管资金,不主动抵)|
|
||||
| **adhoc** | AdHocEvent 与 Bill 是兄弟概念(都产 CollectionOrder),但不互相关联 |
|
||||
|
||||
billing 是 prop-acc 的**收款中枢** —— 各种"该收的钱"都先变成 Bill,然后通过 CollectionOrder 完成收款。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 周期账单和计量账单的 BillType 一定区分吗?
|
||||
> 业务上**应该区分**。理由:
|
||||
>
|
||||
> - 报表分类(物业费收入 vs 水电费收入)
|
||||
> - 业务逻辑(周期账单可合并,计量账单不合并)
|
||||
> - 业户感知(账单上区分类型,业户更明白)
|
||||
|
||||
> [!question] sourceable 为 null 的 Bill 怎么追溯来源?
|
||||
> 看 `created_by`(操作员)+ `created_at`(时间)+ `meta`(备注)。手工建单可在 memo 写清缘由。
|
||||
|
||||
> [!question] 同一抄表 reading 多次生成 Bill 会怎样?
|
||||
> 不会发生(理论上):
|
||||
> - Reading 上有 `bill_id` 字段,有值表示已关联 Bill
|
||||
> - `GenerateBillsFromMeterReadingsAction` 校验 `bill_id === null`(已有 Bill 跳过)
|
||||
>
|
||||
> 但若数据手工乱改 → 可能 Bill 重复 → reading 的 bill_id 会被新建的覆盖,留下"孤儿 Bill"。需运维清理。
|
||||
|
||||
> [!question] 一个 Bill 关联多个 reading 可以吗?
|
||||
> 当前 `sourceable_*` 是 1:1(一个 Bill 对应一个 source)。如果业务上一户家有 2 张水表合并出账(罕见):需要扩展(新建中间表 BillReading,或者改 Bill schema 加 child reading 字段)。issue.md 未明确,可作为未来扩展点。
|
||||
|
||||
> [!question] 临时账单的 sourceable_type 应该是什么?
|
||||
> `null` 或自定义类型。当前实现倾向 `null`(简单)。如果未来要"临时账单"也有追溯链(例如关联到具体维修工单),可以扩展为 `sourceable_type='WorkOrder'`。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-vs-collection-order]]
|
||||
- [[periodic-bill-generation]]
|
||||
- [[delete-vs-void-dual-track]]
|
||||
- [[create-periodic-property-fee]]
|
||||
- [[create-meter-bill-auto]]
|
||||
- [[create-single-bill-manual]]
|
||||
- [[../meter/bill-generation-pipeline]]
|
||||
- [[../prepaid/consume-via-bill-collection-type]]
|
||||
265
prop-acc/concepts/billing/bill-vs-collection-order.md
Normal file
265
prop-acc/concepts/billing/bill-vs-collection-order.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
title: prop-acc · billing · Bill 与 CollectionOrder 的关系
|
||||
aliases:
|
||||
- Bill vs CollectionOrder
|
||||
- CollectionOrderBill
|
||||
- 应收 vs 已收
|
||||
- 账单与收款单
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 账单
|
||||
- 核心概念
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: billing
|
||||
last_review: 2026-05-26
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# Bill 与 CollectionOrder 的关系
|
||||
|
||||
`Bill`(应收 / 应付)和 `CollectionOrder`(已收)是 prop-acc 的**两个核心对象**:
|
||||
|
||||
- **Bill** = "这是业户该付的钱"(应收账款)
|
||||
- **CollectionOrder** = "业户实际付了多少 / 怎么付的"(已收记录)
|
||||
|
||||
两者通过 **`CollectionOrderBill` 中间表多对多关联** —— 一张 Bill 可能由多笔 CollectionOrder 凑齐(部分付);一笔 CollectionOrder 可能付多张 Bill(批量收款)。
|
||||
|
||||
## 一对多 vs 多对多
|
||||
|
||||
### 错例:一对一
|
||||
|
||||
```
|
||||
Bill ─── CollectionOrder
|
||||
```
|
||||
|
||||
业务上**不够用**:
|
||||
- 业户付一半 → 怎么记?
|
||||
- 业户一次付 3 张账单 → 怎么记?
|
||||
- 一张大账单分两次付 → 怎么记?
|
||||
|
||||
### 正例:多对多(本设计)
|
||||
|
||||
```
|
||||
Bill ─── CollectionOrderBill ─── CollectionOrder
|
||||
M ───────── N ───────── M ───────── N
|
||||
```
|
||||
|
||||
- 1 个 Bill 可对应多个 CollectionOrderBill(一张账单分两次付)
|
||||
- 1 个 CollectionOrder 可对应多个 CollectionOrderBill(一次收款付多张账单)
|
||||
- `CollectionOrderBill` 中间表存**分配金额**(`allocated_amount`)
|
||||
|
||||
## `CollectionOrderBill` 中间表
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `id` | 主键 |
|
||||
| `collection_order_id` | 关联收款单 |
|
||||
| `bill_id` | 关联账单 |
|
||||
| **`allocated_amount`** | **本次分配给该账单的金额**(可以小于账单金额 = 部分付)|
|
||||
| `created_at` | 关联时间 |
|
||||
|
||||
> [!info] allocated_amount 是关键
|
||||
> 这个字段决定"这笔收款付了这张账单多少",而不是"账单全付了"。例如:
|
||||
>
|
||||
> - 业户付 ¥1,000 想分摊到 3 张账单(¥300 + ¥400 + ¥300)
|
||||
> - 建 1 个 CollectionOrder(¥1,000)
|
||||
> - 建 3 个 CollectionOrderBill(分别 allocated_amount 300/400/300,关联各自的 Bill)
|
||||
> - 3 张 Bill 的 paid_amount 累加各自的 allocated_amount
|
||||
|
||||
## 完整模型关系图
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
Bill ||--o{ CollectionOrderBill : "1:N"
|
||||
CollectionOrderBill }o--|| CollectionOrder : "N:1"
|
||||
|
||||
Bill {
|
||||
id PK
|
||||
bill_no
|
||||
amount
|
||||
paid_amount
|
||||
status
|
||||
}
|
||||
|
||||
CollectionOrderBill {
|
||||
id PK
|
||||
bill_id FK
|
||||
collection_order_id FK
|
||||
allocated_amount
|
||||
}
|
||||
|
||||
CollectionOrder {
|
||||
id PK
|
||||
collection_type
|
||||
actual_amount
|
||||
payment_channel_id
|
||||
status
|
||||
}
|
||||
```
|
||||
|
||||
## 业务场景对照
|
||||
|
||||
### 场景 1:简单单笔收款
|
||||
|
||||
业户付 ¥800 物业费,一笔现金:
|
||||
|
||||
```
|
||||
Bill #B-001 (amount=800, status=Unpaid)
|
||||
│
|
||||
└── CollectionOrderBill #1 (allocated_amount=800)
|
||||
│
|
||||
└── CollectionOrder #CO-001 (type=Bill, actual_amount=+800, payment=现金)
|
||||
│
|
||||
└── Receipt #R-001 ("物业费 ¥800")
|
||||
```
|
||||
|
||||
Bill 状态翻 Paid,完成。
|
||||
|
||||
### 场景 2:同业户批量收款(本月所有账单一起付)
|
||||
|
||||
业户付 ¥1,082 = 物业费 800 + 水费 54 + 电费 168 + 燃气 60:
|
||||
|
||||
```
|
||||
Bill #B-001 物业费 (amount=800) ──┐
|
||||
Bill #B-002 水费 (amount=54) ──┤
|
||||
Bill #B-003 电费 (amount=168) ──┤
|
||||
Bill #B-004 燃气 (amount=60) ──┤
|
||||
│
|
||||
4 个 CollectionOrderBill ──┘
|
||||
│
|
||||
└── 1 个 CollectionOrder (actual_amount=+1082, payment=微信)
|
||||
│
|
||||
└── 1 个 Receipt(可能列出 4 个明细)
|
||||
```
|
||||
|
||||
走 `BatchCollectPaymentAction`,详见 [[collect-payment-batch]]。
|
||||
|
||||
### 场景 3:部分付(Partial)
|
||||
|
||||
业户付 ¥300 给 ¥800 物业费(欠 ¥500):
|
||||
|
||||
```
|
||||
Bill #B-001 物业费 (amount=800, status=Partial)
|
||||
│
|
||||
└── CollectionOrderBill #1 (allocated_amount=300)
|
||||
│
|
||||
└── CollectionOrder #CO-001 (actual_amount=+300, payment=现金)
|
||||
```
|
||||
|
||||
Bill 状态:**Partial**(详见 [[bill-six-state-machine]])。后续业户补付 ¥500:
|
||||
|
||||
```
|
||||
Bill #B-001 (amount=800, status=Paid) ← 收齐变 Paid
|
||||
│
|
||||
├── CollectionOrderBill #1 (allocated_amount=300, 关联 CO-001)
|
||||
└── CollectionOrderBill #2 (allocated_amount=500, 关联 CO-002)
|
||||
│
|
||||
└── CollectionOrder #CO-002 (+500)
|
||||
```
|
||||
|
||||
### 场景 4:预存款抵扣
|
||||
|
||||
业户预存款余额 ¥5,000,自动抵 ¥800 物业费:
|
||||
|
||||
```
|
||||
Bill #B-001 (amount=800, status=Unpaid → Paid)
|
||||
│
|
||||
└── CollectionOrderBill #1 (allocated_amount=800)
|
||||
│
|
||||
└── CollectionOrder #CO-001 (type=Bill, actual_amount=+800,
|
||||
meta.fund_source=prepaid)
|
||||
│
|
||||
└── PrepaidTransaction (type=consume, amount=800)
|
||||
```
|
||||
|
||||
详见 [[../prepaid/consume-via-bill-collection-type]]。**关键**:CollectionOrder.type 仍是 `Bill`(账单视角),fund_source 标 prepaid。
|
||||
|
||||
## CollectionOrderBill 是只读管理
|
||||
|
||||
`CollectionAllocationsRelationManager`(Bill 详情页的子表)**完全只读**(无任何 Action,不可改 / 不可删 / 不可加)。理由:
|
||||
|
||||
- 分配是收款 Action 内事务一次性写入,业务上不需要"事后调整"
|
||||
- 改了会导致 Bill.paid_amount 与 allocations 累加值不一致 → 数据脱节
|
||||
- 误删等于"业户付的钱凭空消失" → 灾难
|
||||
|
||||
issue.md Q6 明确:**"`CollectionAllocationsRelationManager` 完美保持不动:作为只读管理器的最佳示范"**。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
后台 Bill 详情(`ViewBill`)的子表"收款分配":
|
||||
|
||||
| 列 | 内容 |
|
||||
|---|---|
|
||||
| 收款单号 | CO-XXX |
|
||||
| 分配金额 | allocated_amount |
|
||||
| 付款方式 | CO 的 payment_channel |
|
||||
| 付款时间 | CO 的 created_at |
|
||||
|
||||
业务人员看明白:"这张账单的 ¥800 是 5/15 微信付的"。
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**通常感受不到**这两个对象的复杂关系。看到的是:
|
||||
|
||||
- 账单状态(Unpaid / Partial / Paid / ...)
|
||||
- 已付金额 / 未付金额
|
||||
- 收款明细(几次付、什么时候付、用什么方式付)
|
||||
|
||||
整体感知:**"我付了 ¥800 物业费"**,而不是"我建了一个 CollectionOrder 关联到 Bill 通过 CollectionOrderBill"。
|
||||
|
||||
## 与 adhoc 模块的对比
|
||||
|
||||
[[../adhoc/collection-order-and-receipt|adhoc 的 CollectionOrder 与 Receipt]] 概念:
|
||||
|
||||
| 维度 | adhoc(一次性收费)| **billing(账单)** |
|
||||
|---|---|---|
|
||||
| 主对象 | `AdHocEvent` | `Bill` |
|
||||
| 与 CollectionOrder 关系 | 1:1(一次买卖一个 CO)| **多对多(中间表)** |
|
||||
| 是否支持部分付 | ❌(一次性付清)| **✅ Partial 状态** |
|
||||
| 是否支持批量付 | ❌(每单独立)| **✅ BatchCollectPayment** |
|
||||
| 收款类型 | `CollectionType=AdHoc` | `CollectionType=Bill` |
|
||||
|
||||
bill 的多对多设计让**批量收款 + 部分付**成为可能,这是周期账单业务的核心需求。
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 为什么不直接在 Bill 上记 paid_amount,不要中间表?
|
||||
> 因为要记**每笔付款的细节**(什么时候付、什么方式、多少金额)。中间表才能存这些。
|
||||
>
|
||||
> 单纯 `paid_amount` 字段只能表达"总付了多少",无法回答"业户上次付的时候用的什么方式"。
|
||||
|
||||
> [!question] 一个 CollectionOrder 关联到一个 Bill 但 allocated_amount < CO.actual_amount 怎么办?
|
||||
> 业务上不应该发生(每笔 CO 应该被完全分配)。如果发生,差额部分是"未分配收款" → 需要后续补建 CollectionOrderBill 关联其他 Bill(或留作业户预付,看业务设计)。
|
||||
|
||||
> [!question] Bill.paid_amount 字段从哪算?
|
||||
> 系统应自动维护:`paid_amount = SUM(collectionOrderBills.allocated_amount)`。每次新建 CollectionOrderBill 时回填 Bill.paid_amount。
|
||||
>
|
||||
> 若数据库直接改 collectionOrderBills 没回填(理论上不应该,但若有 bug)→ Bill.paid_amount 与实际不符 → 需修复 + 审计。
|
||||
|
||||
> [!question] 已付账单作废后,关联的 CollectionOrderBill 怎么办?
|
||||
> 看实现:
|
||||
> - **不动**(保留历史关联)+ 走"作废 + 退款"组合(建红字 CO 退给业户)
|
||||
> - **解除关联**(allocated_amount 退还,但这种做法破坏审计)
|
||||
>
|
||||
> 推荐**前者**(保留历史 + 走退款)。详见 [[void-paid-bill]]。
|
||||
|
||||
> [!question] CollectionOrderBill 的允许的 allocated_amount 大于 Bill.amount 吗?
|
||||
> 业务上不应该(超额付了无意义)。如果业户付多了:
|
||||
> - 多余部分**应该退**(或转入预存款)
|
||||
> - 不应该让 CollectionOrderBill.allocated_amount > Bill.amount(等于"账单凭空多了钱")
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[bill-six-state-machine]]
|
||||
- [[bill-types-and-sources]]
|
||||
- [[collect-payment-single]]
|
||||
- [[collect-payment-batch]]
|
||||
- [[collect-via-prepaid-auto]]
|
||||
- [[exception-partial-payment]]
|
||||
- [[../adhoc/collection-order-and-receipt]]
|
||||
- [[../prepaid/consume-via-bill-collection-type]]
|
||||
Reference in New Issue
Block a user