254 lines
8.0 KiB
Markdown
254 lines
8.0 KiB
Markdown
|
|
---
|
||
|
|
title: prop-acc · billing · 场景 - 抄表自动生成计量账单
|
||
|
|
aliases:
|
||
|
|
- 计量账单生成
|
||
|
|
- 抄表后建账单
|
||
|
|
- create-meter-bill-auto
|
||
|
|
- 场景-计量账单自动生成
|
||
|
|
tags:
|
||
|
|
- 场景
|
||
|
|
- prop-acc
|
||
|
|
- 账单
|
||
|
|
- 创建
|
||
|
|
- 计量账单
|
||
|
|
audience:
|
||
|
|
- 业务人员
|
||
|
|
- 财务
|
||
|
|
- 抄表员
|
||
|
|
status: 已发布
|
||
|
|
sub_feature: billing
|
||
|
|
last_review: 2026-05-26
|
||
|
|
code_version: 2026-05-22
|
||
|
|
---
|
||
|
|
|
||
|
|
# 场景:抄表自动生成计量账单
|
||
|
|
|
||
|
|
抄表数据进入系统后(`MeterReading`),触发 `GenerateBillsFromMeterReadingsAction` 自动建账单。**核心机制在 meter 模块的[bill-generation-pipeline](../concepts/meter/bill-generation-pipeline.md)**,本场景描述 billing 模块的**对接视角**。
|
||
|
|
|
||
|
|
## 典型情境
|
||
|
|
|
||
|
|
> [!example] 真实情境
|
||
|
|
> 嘉禾花园 5 月底:
|
||
|
|
>
|
||
|
|
> - 1,200 张表的本月抄表全部完成
|
||
|
|
> - 1,160 张走集抄([[../meter/read-via-iot-remote-source]])
|
||
|
|
> - 40 张走手抄([[../meter/read-single-meter-manual]] + [[../meter/read-batch-via-excel-import]])
|
||
|
|
> - 系统在抄表完成后**自动触发**:`GenerateBillsFromMeterReadingsAction`(批量,1,200 张 reading → 1,200 张 Bill)
|
||
|
|
> - 王主管看到的:**1,200 张计量账单已就绪**,无需手工建
|
||
|
|
|
||
|
|
## 业务人员视角
|
||
|
|
|
||
|
|
### 自动模式(默认)
|
||
|
|
|
||
|
|
业务人员**几乎不操作**:
|
||
|
|
|
||
|
|
1. 抄表完成 → 抄表 Action / Importer 内部触发 `GenerateBillsFromMeterReadingsAction`
|
||
|
|
2. 系统逐张 reading 算金额(走 [[../meter/multiplier-and-tiered-pricing|倍率+阶梯+min/max]] 三层算法)
|
||
|
|
3. 建 Bill,sourceable=MeterReading,bill_type=Meter
|
||
|
|
4. reading.bill_id 回写
|
||
|
|
5. 报告"已生成 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 日前付清。
|
||
|
|
|
||
|
|
业户**不知道**这些账单是抄表自动生成的(后端透明)。他看到的就是"几张需要付的账单"。
|
||
|
|
|
||
|
|
## 系统流程(完整链路)
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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 关联
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Bill 字段
|
||
|
|
sourceable_type = 'App\Models\MeterReading'
|
||
|
|
sourceable_id = 12345
|
||
|
|
```
|
||
|
|
|
||
|
|
可双向反查:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 从 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,重复触发
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 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:
|
||
|
|
|
||
|
|
```php
|
||
|
|
$ratePlan = $feeType->currentRatePlan;
|
||
|
|
if (! $ratePlan) {
|
||
|
|
throw new RatePlanNotFoundException();
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Service 抛错,Action 捕获后 skip 该 reading + 报告。业务人员需先配 RatePlan 再重试。
|
||
|
|
|
||
|
|
### 异常 3:asset 没绑业户(`community_asset_users` 缺失)
|
||
|
|
|
||
|
|
```php
|
||
|
|
$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 不同(`Periodic` vs `Meter`),fee_type 不同(物业费 vs 水电气),sourceable 不同(null vs MeterReading)。各走各的生成路径,各自独立账单。
|
||
|
|
|
||
|
|
> [!question] 业户预存款抵这种账单的优先级?
|
||
|
|
> 看 [[../prepaid/consume-batch-auto-monthly|预存款自动抵扣 job]] 的策略:
|
||
|
|
>
|
||
|
|
> - 按 `due_at` 升序(早到期的先抵)
|
||
|
|
> - 计量账单与物业费的 due_at 通常相近(都是月底 + 宽限期)
|
||
|
|
> - 哪个早抵哪个
|
||
|
|
|
||
|
|
## 相关文档
|
||
|
|
|
||
|
|
- [[bill-types-and-sources]]
|
||
|
|
- [[bill-vs-collection-order]]
|
||
|
|
- [[../meter/bill-generation-pipeline]]
|
||
|
|
- [[../meter/multiplier-and-tiered-pricing]]
|
||
|
|
- [[../meter/exception-readings-locked-after-bill]]
|
||
|
|
- [[create-periodic-property-fee]]
|
||
|
|
- [[create-single-bill-manual]]
|
||
|
|
- [[collect-payment-batch]](业户可能水电气一起付)
|