vault backup: 2026-05-26 00:43:11
This commit is contained in:
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