8.8 KiB
title, aliases, tags, audience, status, sub_feature, last_review, code_version
| title | aliases | tags | audience | status | sub_feature | last_review | code_version | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| prop-acc · billing · Bill 与 CollectionOrder 的关系 |
|
|
|
已发布 | billing | 2026-05-26 | 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
完整模型关系图
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(一次性收费) | 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(等于"账单凭空多了钱")