8.0 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 · 场景 - 抄表自动生成计量账单 |
|
|
|
已发布 | billing | 2026-05-26 | 2026-05-22 |
场景:抄表自动生成计量账单
抄表数据进入系统后(MeterReading),触发 GenerateBillsFromMeterReadingsAction 自动建账单。核心机制在 meter 模块的bill-generation-pipeline,本场景描述 billing 模块的对接视角。
典型情境
[!example] 真实情境 嘉禾花园 5 月底:
- 1,200 张表的本月抄表全部完成
- 系统在抄表完成后自动触发:
GenerateBillsFromMeterReadingsAction(批量,1,200 张 reading → 1,200 张 Bill)- 王主管看到的:1,200 张计量账单已就绪,无需手工建
业务人员视角
自动模式(默认)
业务人员几乎不操作:
- 抄表完成 → 抄表 Action / Importer 内部触发
GenerateBillsFromMeterReadingsAction - 系统逐张 reading 算金额(走 ../meter/multiplier-and-tiered-pricing 三层算法)
- 建 Bill,sourceable=MeterReading,bill_type=Meter
- reading.bill_id 回写
- 报告"已生成 1,200 张计量账单"
手动模式(罕见)
某些情况下业务人员需要手动触发:
- 抄表数据补录后(集抄掉线 + 后续补抄)
- 部分 reading 之前生成 Bill 失败后重试
- 测试 / 验证
后台 → 账单 → 列表 → 顶部 "从 reading 生成账单" 按钮(若有 UI)→ 选 reading 范围 → 提交。
第 1 步:核对 reading 完成
抄表完成后(月底):
- 看
MetersNeedingReadingListWidget:本月待抄数 = 0 - 看
MeterReadingsList:本月 reading 数 = 1,200
第 2 步:确认账单生成
抄表 Action 自动触发后:
- 看
BillsList,过滤bill_type=Meter+ 本月期次 → 应有 1,200 张 - 看每张账单的
sourceable_id指向对应 reading
第 3 步:异常处理
| 异常 | 处置 |
|---|---|
某 reading 没生成 Bill(状态:reading.bill_id=null) |
排查原因(RatePlan 缺失?业户没绑定?)→ 修复 → 重新触发 |
| 某 Bill 金额异常(高出预期 10 倍) | 排查 multiplier / 阶梯配置,可能要 [[exception-readings-locked-after-bill |
| 大面积失败(>10%) | 系统级问题,联系运维 |
业户视角
业户月底/月初收到水电气账单:
陈先生您好,您的 2026 年 5 月费用账单已生成:
- 水费:¥54(用水 12 吨)
- 电费:¥168(用电 280 度)
- 燃气费:¥35(用气 15 立方)
合计 ¥257,请于 6 月 15 日前付清。
业户不知道这些账单是抄表自动生成的(后端透明)。他看到的就是"几张需要付的账单"。
系统流程(完整链路)
sequenceDiagram
participant 集抄/抄表员
participant MeterAction[抄表 Action / Importer]
participant GenBills[GenerateBillsFromMeterReadingsAction]
participant Calc[MeterBillCalculator]
participant Service[MeterBillGenerationService]
participant DB
Note over 集抄/抄表员: 本月抄表完成
集抄/抄表员->>MeterAction: 推送 / 录入 reading 数据
MeterAction->>DB: 建 MeterReading(bill_id=null)
MeterAction->>GenBills: 触发(刚建的 reading 列表)
loop 每个 reading
GenBills->>Service: generateBillForReading(reading)
Service->>Calc: calculate(consumption, ratePlan)
Calc-->>Service: amount
Service->>DB: 建 Bill(bill_type=Meter, sourceable=reading, status=Unpaid)
Service->>DB: 更新 reading.bill_id = bill.id
end
GenBills-->>MeterAction: 报告(N 张生成,M 张失败)
详细分层见 ../meter/bill-generation-pipeline"Calculator + Service + Action"段。
与 meter 模块的关系
| 维度 | meter 模块 | billing 模块(本场景) |
|---|---|---|
| 主对象 | Meter + MeterReading | Bill |
| 责任 | 抄表 + 算用量 | 建账单 + 后续收款 |
| 触发 | 抄表 Action / Importer | 由 meter 模块触发 |
| 共享 | sourceable_type='MeterReading' | sourceable_id 指向 reading |
billing 模块对计量账单没有自己的创建 Action UI(由 meter 模块的 GenerateBillsFromMeterReadingsAction 完全负责)。billing 模块只接收 sourceable 标记 + 提供后续 collect-payment-single / exception-overdue-bills 流程。
计量账单的特殊处理
1. sourceable 关联
// Bill 字段
sourceable_type = 'App\Models\MeterReading'
sourceable_id = 12345
可双向反查:
// 从 Bill 看 reading
$bill->sourceable; // → MeterReading 实例
// 从 reading 看 Bill
$reading->bill; // → Bill 实例(通过 reading.bill_id)
2. bill_type=Meter
| 字段 | 值 |
|---|---|
bill_type |
Meter(BillType 枚举) |
| 业务分类 | 水费 / 电费 / 燃气费(由 fee_type_id 决定具体) |
| 报表分类 | 进"水电气收入"科目 |
3. 期次
计量账单的 billing_period_start / end 通常对应抄表期次(例如:本期抄表是 5/26 - 上期 4/28 = 期次 4/29 - 5/26)。具体看实现。
异常分支
异常 1:reading 已生成 Bill,重复触发
// GenerateBillsFromMeterReadingsAction
foreach ($readings as $reading) {
if ($reading->bill_id !== null) {
$skipped[] = ['reading' => $reading, 'reason' => 'already_billed'];
continue;
}
// ...
}
已有 Bill 的 reading 直接 skip。不会重复建。
异常 2:RatePlan 不存在
某 fee_type 没配 RatePlan:
$ratePlan = $feeType->currentRatePlan;
if (! $ratePlan) {
throw new RatePlanNotFoundException();
}
Service 抛错,Action 捕获后 skip 该 reading + 报告。业务人员需先配 RatePlan 再重试。
异常 3:asset 没绑业户(community_asset_users 缺失)
$resident = $this->findCurrentResident($asset);
if (! $resident) {
// 视设计:抛 / 建匿名 Bill(无 resident_id)/ 跳过
}
通常跳过(看具体实现),业务人员先绑业户再重试。
异常 4:已落账的 reading 数据错
走 ../meter/exception-readings-locked-after-bill:作废 Bill → 改 reading → 重生成 Bill。复杂,需运维介入。
常见问题
[!question] 触发时机:抄表 Action 内部 / 单独定时? 当前实现:抄表 Action 完成后立即触发(同事务或紧接事务)。
优点:数据立即一致(reading + Bill 同步出现)。 缺点:抄表 Action 性能 = 抄表 + 建账单两段时间。
替代方案:定时任务(每月固定时间扫所有未生成账单的 reading)。当前未采用。
[!question] 业户对计量账单金额有疑问怎么办? 业务人员可:
- 给业户看抄表照片(
reading.photo_url)- 给业户看 RatePlan(单价配置)
- 重算给业户看(用 Calculator 算法)
若证实数据错 → 走 ../meter/exception-readings-locked-after-bill。
[!question] 与周期账单(物业费)冲突吗? 不冲突。两者 BillType 不同(
PeriodicvsMeter),fee_type 不同(物业费 vs 水电气),sourceable 不同(null vs MeterReading)。各走各的生成路径,各自独立账单。
[!question] 业户预存款抵这种账单的优先级? 看 ../prepaid/consume-batch-auto-monthly 的策略:
- 按
due_at升序(早到期的先抵)- 计量账单与物业费的 due_at 通常相近(都是月底 + 宽限期)
- 哪个早抵哪个