--- 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]]