--- 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]]