Files
uniprop-manual/prop-acc/concepts/billing/bill-vs-collection-order.md
2026-05-26 00:43:11 +08:00

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 的关系
Bill vs CollectionOrder
CollectionOrderBill
应收 vs 已收
账单与收款单
概念
prop-acc
账单
核心概念
业务人员
财务
架构师
已发布 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(等于"账单凭空多了钱")

相关文档