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

7.4 KiB
Raw Blame History

title, aliases, tags, audience, status, sub_feature, last_review, code_version
title aliases tags audience status sub_feature last_review code_version
prop-acc · meter · 表更换链
表更换链
Meter Replacement Chain
replaced_meter_id
R1 R2 后缀
概念
prop-acc
计量表
数据模型
业务人员
抄表员
架构师
已发布 meter 2026-05-25 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 度"全要业户付 → 灾难

更换链保证用量计算连续,业户感知无差异。

数据模型

字段关系

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

换表后的抄表数据

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 可以:

// 找当前在役表
$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) 算法:

// 伪代码
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

异常分支

相关文档