Files
uniprop-manual/prop-acc/scenarios/meter/exception-readings-locked-after-bill.md
2026-05-26 00:28:09 +08:00

9.6 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 · meter · 场景 - 已生成 Bill 的 Reading 锁定,要修正需作废 Bill
Reading 锁定
已落账 reading 修正
exception-readings-locked-after-bill
作废 Bill 重算
场景-已落账读数修正
场景
prop-acc
计量表
异常
数据完整性
业务人员
架构师
已发布 meter 2026-05-26 2026-05-22

场景:已生成 Bill 的 Reading 锁定,要修正需作废 Bill

MeterReading 一经创建不可改;一旦生成 Bill 更不可改 / 不可删(双锁,见 decommission-and-locking)。如果发现已落账 reading 数据错(抄表录错 / 集抄错传 / 业户质疑成功),需走复杂修正流程:先作废 Bill → 改 reading(实际是建新 reading + 标旧 reading 作废)→ 重生成 Bill

典型情境

[!example] 真实情境 5 月底抄表 + 自动生成账单。陈先生 5 月电费账单 ¥1,200(2,800 度,异常高,触发 exception-high-consumption)。

物业派人排查,发现:

  • 抄表员李师傅手抖:把 1,500 录成 2,500
  • 实际本月用量 ~1,500 度,账单应 ¥600 左右(不是 1,200)

陈先生质疑成功,物业要把账单从 1,200 改成 ~600。但:

  • Reading 不可改(系统层面双锁)
  • Bill 已经生成 + 关联 reading

需要走作废 + 重算的组合流程。

当前实施状态

[!warning] 当前系统不支持自动化此流程

issue.md Q5 "待补"段明确记录:

"作废已生成 Bill 的 MeterReading"组合流程:当前 Reading 一旦生成 Bill 即锁定。要修正错误读数需要先作废 Bill 再改 Reading 再重新生成。这个组合流程类似 AdHocEvent 的 VoidAction(级联废 + 留 voided 审计),需要单独设计。等业务方明确"已收款的 Bill 应该怎么撤销"再做(可能涉及红字 Bill / 退款)。

当前替代:运维 / 高权限人员通过 tinker 手工处理。本场景描述业务流程层未来的目标态

业务人员视角(当前手工处理)

第 1 步:确认要修正

  • 业户质疑账单 + 提供合理证据(自家拍照 / 历史数据对照)
  • 物业核对:抄表照片 / 集抄数据 / 物理表
  • 内部决定:确实要修正

第 2 步:看账单是否已付

账单状态 处理路径
未付(Unpaid) 简单:作废 Bill → 重算
已付(Paid,业户付现金/微信) 复杂:作废 Bill → 业户退款 → 重新出账单 → 业户重付
已付(用 [[../prepaid/consume-monthly-property-bill 预存款抵扣]])

第 3 步:走"作废 Bill"流程

当前没专用 UI —— 联系运维 / 高权限人员:

// tinker 操作示意(运维)
DB::transaction(function () use ($readingId) {
    $reading = MeterReading::find($readingId);
    $oldBill = Bill::find($reading->bill_id);

    // 1. 作废 Bill
    $oldBill->update([
        'status' => BillStatus::Voided,
        'voided_at' => now(),
        'voided_reason' => '抄表录错,需重算',
    ]);

    // 2. 解锁 reading(把 bill_id 设 null,让它可被处理)
    $reading->update(['bill_id' => null]);
    // 或者:标 reading 作废,新建一条修正 reading
    $reading->update(['voided_at' => now(), 'voided_reason' => '录错']);
});

第 4 步:建修正 reading

如果走"建新 reading"路径:

$correctReading = MeterReading::create([
    'meter_id' => $oldReading->meter_id,
    'read_at' => $oldReading->read_at,  // 同抄表日期
    'current_reading' => 1500,           // 改成正确值
    'source' => MeterReadingSource::Manual,
    'operated_by' => $currentAdmin->id,
    'memo' => "修正:原 reading #{$oldReading->id} 数值录错(2500 → 1500)",
]);

第 5 步:重生成 Bill

bill-generation-pipeline:对新 reading 调 GenerateBillsFromMeterReadingsAction

第 6 步:处理已付款的差额

如果旧账单已被业户付了:

  • 业户付现金:物业现场退差额(¥1,200 - ¥600 = ¥600)
  • 微信付:物业微信退款
  • 预存款抵扣:走"反向 consume"(技术上是 PrepaidAccount::deposit 把钱充回 → 然后从新账单扣)

第 7 步:通知业户

完整说明:

  • 旧账单 ¥1,200 已作废(原因:抄表录错)
  • 新账单 ¥600
  • 差额已退还(或预存款已回填)

未来目标态(待开发)

类似 AdHocEvent 的 VoidAction(级联废 + 留 voided 审计):

sequenceDiagram
    participant 业务
    participant Filament
    participant VoidBillAction[待开发]
    participant 数据库

    业务->>Filament: 找到要作废的 Bill → VoidBillAction
    Filament->>VoidBillAction: handle(bill, reason)
    VoidBillAction->>数据库: Bill.status=Voided + voided_reason + voided_at
    VoidBillAction->>数据库: Reading.bill_id=null(解锁)
    VoidBillAction->>数据库: 若已付:建红字 Receipt + 退款 / 预存款回填
    Filament-->>业务: 完成

    业务->>Filament: 建修正 MeterReading(同 read_at)
    Filament->>数据库: 新建 reading
    Filament->>VoidBillAction[GenerateBills]: handle(new reading)
    数据库->>数据库: 建新 Bill(amount=正确值)

系统视角:双锁的设计意义

为什么 Reading 创建后不可改(第 1 锁)+ 有 Bill 更不可改(第 2 锁)?

反例(若允许改) 后果
业务人员直接改 Reading 数据 历史 Bill 仍是旧金额,Reading 是新数据 → 不一致
改 Reading 同时改 Bill 已付的钱怎么办?业户付了 1200 你改成 600 → 600 凭空消失
业户已付 + 物业改 Reading 改小金额 物业账面收入虚高(实际收 1200,账面记 600,差额 600 不知去向)

双锁强制业务人员走"作废 + 重生成"流程,留下完整审计痕迹(旧 Bill voided + 新 Bill 创建 + 退款 / 预存款回填记录)。

业户视角

业户不直接接触系统层,只感受:

  • 提出异议
  • 物业核实
  • 收到说明:"经核实,5 月账单 ¥1,200 是抄表录错,实际应付 ¥600。已作废原账单,新账单已发,差额 ¥600 已退到您微信"
  • 收到红字凭证(若已付,作废 + 退款)
  • 收到新账单

整个流程业户感知:"物业认错 + 退钱 + 重发账单",是正面体验(物业承认错误并改正)。

流水台账(完整修正过程)

时间 动作 Reading Bill
5/26 抄表录错 #1(current=2500, bill_id=Bill#A) Bill#A(amount=1200, Unpaid)
5/27 业户质疑 + 核实 (不变) (不变)
5/30 作废 Bill #1.voided=true, bill_id=null Bill#A.status=Voided, voided_reason="抄表录错"
5/30 建修正 reading #2(current=1500, bill_id=null) (无)
5/30 重生成 Bill #2.bill_id=Bill#B Bill#B(amount=600, Unpaid)
5/31 业户付款 (不变) Bill#B.status=Paid

整条修正记录可审计。

常见问题

[!question] 如果原账单未付,作废就行,不用退款? 是的。未付 → 作废 → 不开账单 → 业户没付任何钱。重生成新账单业户直接付新账单即可。

[!question] 业户已经付了,作废 Bill 之后如何退款? 看付款方式:

  • 现金 / 微信 / POS:物业按渠道退
  • 预存款抵扣:PrepaidAccount::deposit 反向充值(技术上是建一笔 deposit 流水把钱"还"回预存款余额)→ 然后业户继续用预存款付新账单

当前没自动化,需运维 / 业务流程操作。

[!question] 已经走过红字流程的 deposit/prepaid 模块,meter 为什么没有? meter 模块直接产 Bill 不直接产 Receipt,与 deposit/prepaid 直接产 Receipt 的模式不同。"红字 Bill" 的设计需要 Bill 模型支持(增加 voided_at / voided_reason 字段 + 业务流程),issue.md Q5 标记为"待补"。

[!question] 抄表员经常录错怎么办? 系统层面:

业务层面:

[!question] "修正 reading" 是建新 reading 还是改旧? 看实现:

  • 改旧(直接 update)→ 简单但丢失历史(原数据没了)
  • 建新 + 标旧 voided(推荐)→ 复杂但完整保留审计

当前 issue.md 倾向"建新 + 标旧 voided"模式(类似 AdHocEvent VoidAction)。需要给 Reading 表加 voided_at / voided_reason 字段。

[!question] 长期不修复这个 gap 有什么风险?

  • 业务人员每次修正都要联系运维(慢、不可扩展)
  • 修正没有标准化流程 → 容易出错(漏退款 / 漏作废)
  • 审计困难(运维 tinker 操作的痕迹靠日志,不像 UI 操作那么清晰)
  • 业户体验差(响应慢)

业务方提需求时优先级会上来。

相关 issue.md 待补

- "作废已生成 Bill 的 MeterReading"组合流程:
  类似 AdHocEvent 的 VoidAction(级联废 + 留 voided 审计),需要单独设计。
  等业务方明确"已收款的 Bill 应该怎么撤销"再做(可能涉及红字 Bill / 退款)

异常分支

相关文档