266 lines
8.8 KiB
Markdown
266 lines
8.8 KiB
Markdown
---
|
|
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]]
|