Files
uniprop-manual/prop-acc/scenarios/billing/create-meter-bill-auto.md
2026-05-26 00:58:14 +08:00

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 · 场景 - 抄表自动生成计量账单
计量账单生成
抄表后建账单
create-meter-bill-auto
场景-计量账单自动生成
场景
prop-acc
账单
创建
计量账单
业务人员
财务
抄表员
已发布 billing 2026-05-26 2026-05-22

场景:抄表自动生成计量账单

抄表数据进入系统后(MeterReading),触发 GenerateBillsFromMeterReadingsAction 自动建账单。核心机制在 meter 模块的bill-generation-pipeline,本场景描述 billing 模块的对接视角

典型情境

[!example] 真实情境 嘉禾花园 5 月底:

业务人员视角

自动模式(默认)

业务人员几乎不操作:

  1. 抄表完成 → 抄表 Action / Importer 内部触发 GenerateBillsFromMeterReadingsAction
  2. 系统逐张 reading 算金额(走 ../meter/multiplier-and-tiered-pricing 三层算法)
  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 日前付清。

业户不知道这些账单是抄表自动生成的(后端透明)。他看到的就是"几张需要付的账单"。

系统流程(完整链路)

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 不同(Periodic vs Meter),fee_type 不同(物业费 vs 水电气),sourceable 不同(null vs MeterReading)。各走各的生成路径,各自独立账单。

[!question] 业户预存款抵这种账单的优先级? 看 ../prepaid/consume-batch-auto-monthly 的策略:

  • due_at 升序(早到期的先抵)
  • 计量账单与物业费的 due_at 通常相近(都是月底 + 宽限期)
  • 哪个早抵哪个

相关文档