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

250 lines
9.6 KiB
Markdown

---
title: prop-acc · meter · 场景 - 已生成 Bill 的 Reading 锁定,要修正需作废 Bill
aliases:
- Reading 锁定
- 已落账 reading 修正
- exception-readings-locked-after-bill
- 作废 Bill 重算
- 场景-已落账读数修正
tags:
- 场景
- prop-acc
- 计量表
- 异常
- 数据完整性
audience:
- 业务人员
- 架构师
status: 已发布
sub_feature: meter
last_review: 2026-05-26
code_version: 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|预存款抵扣]])| 复杂:作废 Bill → 预存款**反向充值** → 重新出账单 → 重新抵扣 |
### 第 3 步:走"作废 Bill"流程
当前**没专用 UI** —— 联系运维 / 高权限人员:
```php
// 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"路径:
```php
$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 审计):
```mermaid
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|换表]] + 走本场景修正
## 相关文档
- [[decommission-and-locking]]
- [[bill-generation-pipeline]]
- [[exception-high-consumption]]
- [[replace-broken-meter]]
- [[../adhoc/cancel-amount-error-redo]](adhoc 模块的类似 void 流程参考)