---
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 旧表
E-501
final_reading=5000
is_active=false
decommissioned_at=2026-05-15
decommission_reason=Replaced] -->|被 replaced_meter_id 指回| B[Meter 新表
E-501-R1
initial_reading=5000
is_active=true
installed_at=2026-05-15
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
2026-04-30 → 5000 度] --> B[换表 2026-05-15]
B --> C[E-501-R1 第一次抄表
2026-05-31 → ?]
C --> D[计算 5月用量]
D --> E{用量公式}
E -->|新表上累计读数| F[假设新表读到 50 度
但 initial_reading=5000
所以 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]]