vault backup: 2026-05-26 00:28:09
This commit is contained in:
249
prop-acc/scenarios/meter/exception-readings-locked-after-bill.md
Normal file
249
prop-acc/scenarios/meter/exception-readings-locked-after-bill.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
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 流程参考)
|
||||
Reference in New Issue
Block a user