206 lines
7.4 KiB
Markdown
206 lines
7.4 KiB
Markdown
---
|
||
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]]
|