Files
uniprop-manual/prop-acc/concepts/meter/replacement-chain.md
2026-05-25 23:53:01 +08:00

206 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: prop-acc · meter · 表更换链
aliases:
- 表更换链
- Meter Replacement Chain
- replaced_meter_id
- R1 R2 后缀
tags:
- 概念
- prop-acc
- 计量表
- 数据模型
audience:
- 业务人员
- 抄表员
- 架构师
status: 已发布
sub_feature: meter
last_review: 2026-05-25
code_version: 2026-05-22
---
# 表更换链
物理表会**老化、损坏、定期校验**。旧表换新表后,系统通过 **`replaced_meter_id`** 字段把新表指回旧表,**初始读数继承**(避免业户被白嫖一段用量)。新表编号自动加 **`-R1` / `-R2` / ...** 后缀,**整条更换链**(代代相承)在数据库里可追溯。
## 为什么要更换链
> [!info] 真实情境
> 张阿姨家电表(编号 E-501)用了 8 年,2026 年 5 月 物业例行校验发现读数跳变(怀疑表内电路老化),要换新表。
>
> 换表那天:
> - 旧表 E-501 最后读数 = 5,000 度
> - 新表 E-501-R1 出厂 = 0 度,但**初始读数继承 5,000 度**
> - 否则:新表从 0 起 → 业户下个月看着账单(假设用了 50 度)→ 但系统算的 "5050 - 0 = 5050 度"全要业户付 → 灾难
更换链保证**用量计算连续**,业户感知无差异。
## 数据模型
### 字段关系
```mermaid
flowchart LR
A[Meter 旧表<br/>E-501<br/>final_reading=5000<br/>is_active=false<br/>decommissioned_at=2026-05-15<br/>decommission_reason=Replaced] -->|被 replaced_meter_id 指回| B[Meter 新表<br/>E-501-R1<br/>initial_reading=5000<br/>is_active=true<br/>installed_at=2026-05-15<br/>replaced_meter_id=旧表 ID]
```
### 字段语义
| Meter 字段 | 旧表(被换)| 新表(替代)|
|---|---|---|
| `code` | E-501 | **E-501-R1**(`nextReplacementCode()` 自动算)|
| `is_active` | **false** | true |
| `installed_at` | 8 年前 | 2026-05-15(换表当天)|
| `decommissioned_at` | **2026-05-15** | null |
| `decommission_reason` | `Replaced`(枚举,见 [[decommission-and-locking]])| null |
| `final_reading` | **5000**(换表前最后读数)| null |
| `initial_reading` | (历史值,不动)| **5000**(继承自旧表)|
| `replaced_meter_id` | null(无上一代)| **旧表 ID** |
## 换表后的抄表数据
```mermaid
flowchart TD
A[E-501 最后 reading<br/>2026-04-30 → 5000 度] --> B[换表 2026-05-15]
B --> C[E-501-R1 第一次抄表<br/>2026-05-31 → ?]
C --> D[计算 5月用量]
D --> E{用量公式}
E -->|新表上累计读数| F[假设新表读到 50 度<br/>但 initial_reading=5000<br/>所以 current = 5050]
F --> G[consumption = (5050 - 5000) * multiplier = 50 度]
```
**关键**:新表 `initial_reading=5000` **不是**抄表的起点,而是用于计算用量的**基准**。下次抄表时:
```
current_reading新表表头读数 + initial_reading= 50 + 5000 = 5050
previous_reading = 5000继承自旧表最后读数
consumption = (current - previous) × multiplier = (5050 - 5000) × 1 = 50
```
业户付的就是 5 月实际用的 50 度,不是 5050。
> [!warning] 抄表员要注意
> 抄表系统**显示给抄表员的数字是物理表头的数字**(50),系统**内部存的是叠加值**(5050)。如果系统设计不一致(让抄表员录 5050),会让人困惑。当前实现需查 `MeterReadingsRelationManager` / `MeterReadingsImporter` 看具体如何处理。
## 整条链的追溯
一张表可能多次换:
```
E-501 (原生) → E-501-R1 (第一次换) → E-501-R2 (第二次换) → E-501-R3 (第三次换)
```
每张表 `replaced_meter_id` 指上一代。后台 / API 可以:
```php
// 找当前在役表
$current = Meter::where('asset_id', $assetId)
->where('fee_type_id', $feeType)
->where('is_active', true)
->first();
// 顺着链向上追溯
$predecessors = [];
$cursor = $current;
while ($cursor->replaced_meter_id) {
$cursor = Meter::find($cursor->replaced_meter_id);
$predecessors[] = $cursor;
}
// $predecessors 现在是 [R2, R1, 原生] 的逆序数组
```
业务上可用于:
- 业户对历史用量有异议:看哪张表抄出来的
- 表更换历史报表
- 长期累计用量
## `nextReplacementCode()` 实现
代码 `Meter::nextReplacementCode($oldMeterCode)` 算法:
```php
// 伪代码
function nextReplacementCode(string $oldCode): string
{
// E-501 → E-501-R1
// E-501-R1 → E-501-R2
// E-501-R2 → E-501-R3
if (preg_match('/^(.*)-R(\d+)$/', $oldCode, $matches)) {
return $matches[1] . '-R' . ($matches[2] + 1);
}
return $oldCode . '-R1';
}
```
业务人员**不需要手动想**新表编号,系统自动算。
## 操作:`ReplaceMeterAction`
后台 → 计量表 → 找旧表 → 进 `ViewMeter` → 点 `ReplaceMeterAction`
Modal 表单:
| 字段 | 填什么 |
|---|---|
| **旧表最后读数(`final_reading`)** | 现场拍照确认,如 `5000` |
| **退役原因(`decommission_reason`)** | 选 `Replaced`(其他选项见 [[decommission-and-locking]])|
| **退役日期** | 默认今天 |
| **新表编号** | 自动 `E-501-R1`(可改但不推荐)|
| **新表 multiplier** | 默认继承旧表(可改)|
| **新表安装日期** | 默认今天 |
| 备注 | "校验未通过,换新表" |
提交后系统在一个事务内:
1. 旧表 `is_active=false`, `decommissioned_at=今天`, `decommission_reason=Replaced`, `final_reading=5000`
2. 建新表 `is_active=true`, `installed_at=今天`, `replaced_meter_id=旧表.id`, `initial_reading=5000`, `multiplier=继承`
## 常见问题
> [!question] 旧表读数比新表初始低,会发生吗?
> 不会。新表的 `initial_reading` 就是旧表的 `final_reading`,逻辑上必然相等。
> [!question] 换表时业户家正在用电怎么处理?
> 实际换表过程要断电断水短暂时间,业户可感知。系统层面:
>
> - 旧表 `decommissioned_at` 和新表 `installed_at` 都填换表那天
> - 中间用电量(几分钟到几小时)的微小差异通常忽略
> - 严格的物业可在换表说明里告知业户"换表过程几分钟用电不计费"
> [!question] 换表后旧表的历史 MeterReading 还能查吗?
> 能。每条 reading 都关联 `meter_id`(旧表 ID),不会因换表丢失。审计可完整追溯。
> [!question] 误换表(其实不该换)能撤销吗?
> 不能直接撤销(MeterReading 不可变,Meter 状态也不轻易回滚)。要修复:
>
> - 物理上把新表退役(`is_active=false`, `decommission_reason=Removed`)
> - 把旧表重启(`is_active=true`, `decommissioned_at=null`)→ 但这种"复活"操作在 Policy 层可能被守护拒绝([[decommission-and-locking]])
>
> **预防胜于补救**:换表前确认。
> [!question] 同一张表换好多次,链很长怎么办?
> 链长本身不是问题,系统正常处理。如果链过长(>10 代),通常说明该表频繁出问题,业务上应:
>
> - 排查表的型号 / 安装环境
> - 考虑改型号 / 换品牌
> - 长链不影响数据查询性能(关联查询逐级递归,但物业表数量通常不大)
> [!question] 新表 `multiplier` 与旧表不同可以吗?
> 可以,但**强烈不推荐**。如果新换的表倍率不同(例如旧 1x → 新 10x),用量计算公式就变,容易让业户困惑。除非业务上有明确升级原因(从普通家用表换成工业表),否则**继承旧表 multiplier**。
## 异常分支
- 表损坏(非校验)→ 走 [[replace-broken-meter]] 场景(meter_decommission_reason=Damaged)
- 不换表只退役 → [[decommission-without-replacement]]
## 相关文档
- [[meter-vs-meter-reading]]
- [[decommission-and-locking]]
- [[multiplier-and-tiered-pricing]]
- [[replace-broken-meter]]
- [[decommission-without-replacement]]