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 |
|
|
|
已发布 | 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] 抄表员经常录错怎么办? 系统层面:
- 强制拍照(reading-source-and-photo-proof + read-with-photo-proof)
- Form 上显示 previous 读数 + 异常告警(本月差与上月差比超过 X 倍 → 提示)
业务层面:
- 抄表员培训
- 关键抄表数据二次审核(双签)
- 升级集抄(read-via-iot-remote-source)减少人工录入
[!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 / 退款)
异常分支
- 抄表员录错预防 → read-with-photo-proof + Form 守护
- 高用量触发预警 → exception-high-consumption
- 表故障导致错读 → replace-broken-meter + 走本场景修正