From 495caa278018b3dbf3b2ffa88db559eeee703af6 Mon Sep 17 00:00:00 2001 From: Willie Date: Tue, 26 May 2026 00:23:07 +0800 Subject: [PATCH] vault backup: 2026-05-26 00:23:07 --- .obsidian/workspace.json | 8 +- .../meter/generate-bill-tiered-pricing.md | 246 ++++++++++++++++++ .../meter/generate-bill-with-multiplier.md | 234 +++++++++++++++++ .../meter/read-via-iot-remote-source.md | 246 ++++++++++++++++++ .../scenarios/meter/read-with-photo-proof.md | 229 ++++++++++++++++ 5 files changed, 959 insertions(+), 4 deletions(-) create mode 100644 prop-acc/scenarios/meter/generate-bill-tiered-pricing.md create mode 100644 prop-acc/scenarios/meter/generate-bill-with-multiplier.md create mode 100644 prop-acc/scenarios/meter/read-via-iot-remote-source.md create mode 100644 prop-acc/scenarios/meter/read-with-photo-proof.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 358fb93..0dcff08 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -197,6 +197,10 @@ }, "active": "849c5ff8936a2b67", "lastOpenFiles": [ + "prop-acc/scenarios/meter/generate-bill-with-multiplier.md", + "prop-acc/scenarios/meter/generate-bill-tiered-pricing.md", + "prop-acc/scenarios/meter/read-with-photo-proof.md", + "prop-acc/scenarios/meter/read-via-iot-remote-source.md", "prop-acc/scenarios/meter/read-batch-via-excel-import.md", "prop-acc/scenarios/meter/read-single-meter-manual.md", "prop-acc/scenarios/meter/decommission-without-replacement.md", @@ -221,10 +225,6 @@ "prop-acc/scenarios/prepaid/freeze-suspected-fraud.md", "prop-acc/scenarios/prepaid/refund-partial-after-consume.md", "prop-acc/scenarios/prepaid/refund-full-resident-moveout.md", - "prop-acc/scenarios/prepaid/consume-batch-auto-monthly.md", - "prop-acc/scenarios/prepaid/consume-meter-bill.md", - "prop-acc/scenarios/prepaid/consume-multiple-bills-priority.md", - "prop-acc/scenarios/prepaid/consume-monthly-property-bill.md", "prop-acc/scenarios/prepaid", "prop-acc/concepts/prepaid", "prop-acc/scenarios/deposit", diff --git a/prop-acc/scenarios/meter/generate-bill-tiered-pricing.md b/prop-acc/scenarios/meter/generate-bill-tiered-pricing.md new file mode 100644 index 0000000..b86be30 --- /dev/null +++ b/prop-acc/scenarios/meter/generate-bill-tiered-pricing.md @@ -0,0 +1,246 @@ +--- +title: prop-acc · meter · 场景 - 阶梯水电价生成账单(progressive 累进) +aliases: + - 阶梯计价账单 + - tiered pricing 算例 + - generate-bill-tiered-pricing + - 场景-阶梯计价生成账单 +tags: + - 场景 + - prop-acc + - 计量表 + - 账单生成 +audience: + - 业务人员 + - 财务 + - 业户 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:阶梯水电价生成账单(progressive 累进) + +业户**用量超过低阶梯**进入更高阶梯时,系统按 **progressive 累进** 算账单(每段用量按各自单价,**不是**整月按最高阶梯)。本场景完整演示算例。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨 5 月用水 35 吨(平时 12-15 吨,本月浇花 + 装修),嘉禾花园的水费阶梯如下: +> +> | 阶梯 | 月用水(吨)| 单价(元/吨)| +> |---|---|---| +> | 第一阶梯 | 0-20 | 3.0 | +> | 第二阶梯 | 21-30 | 4.5 | +> | 第三阶梯 | 30+ | 6.0 | +> +> 张阿姨 5 月用 35 吨,本月水费应是多少? + +**Progressive 累进算法**(本系统采用): + +``` +段 1(0-20 吨):20 × 3.0 = 60 元 +段 2(21-30 吨):10 × 4.5 = 45 元 +段 3(31-35 吨): 5 × 6.0 = 30 元 +合计: 135 元 ✅ +``` + +**Full-tier 简陋实现**(错!本系统未采用): + +``` +35 吨 × 6.0(整月按最高阶梯) = 210 元 ❌ +``` + +差额 ¥75。简陋实现对业户极不公平,**是市场上劣质系统的常见 bug**。本系统 `MeterBillCalculator::calculateTiered()` 实现的是 progressive,业户收到的账单准确。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 抄表完成 + participant Action[GenerateBillsFromMeterReadingsAction] + participant Service[MeterBillGenerationService] + participant Calc[MeterBillCalculator] + participant 数据库 + + 抄表完成->>Action: handle([5月 reading,consumption=35]) + Action->>Service: generateBillForReading(reading) + Service->>数据库: 查 meter.fee_type=水费 → 查 RatePlan + RateTier(3 个 tier) + Service->>Calc: calculate(consumption=35, ratePlan, min, max) + Calc->>Calc: calculateTiered(35, [tier1, tier2, tier3]) + Note over Calc: 段 1:min(35,20)-0=20 → 20*3=60 + Note over Calc: 段 2:min(35,30)-20=10 → 10*4.5=45 + Note over Calc: 段 3:35-30=5 → 5*6=30 + Note over Calc: 总和 135 + Calc-->>Service: 135 + Service->>Service: clamp(135, min, max) = 135 + Service->>数据库: 建 Bill(amount=135, sourceable=reading) + Service->>数据库: 更新 reading.bill_id + Service-->>Action: ok +``` + +## `calculateTiered()` 算法(伪代码) + +```php +function calculateTiered(float $consumption, Collection $tiers): float +{ + $amount = 0.0; + $remaining = $consumption; + + foreach ($tiers as $tier) { + if ($remaining <= 0) break; + + $tierRange = $tier->upper ? $tier->upper - $tier->lower : INF; + $consumedInTier = min($remaining, $tierRange); + + $amount += $consumedInTier * $tier->unit_price; + $remaining -= $consumedInTier; + } + + return $amount; +} +``` + +详见 [[multiplier-and-tiered-pricing|倍率与阶梯计价]] 概念。 + +## 业户视角 + +### 您收到的账单(简化版) + +``` +5 月水费账单 + +用水量:35 吨 + +明细: + 0-20 吨段:20 × 3.0 = 60 元 + 21-30 吨段:10 × 4.5 = 45 元 + 31+ 吨段: 5 × 6.0 = 30 元 +合计:135 元 + +应付:¥135.00 +``` + +> [!info] 账单明细的展示 +> 当前 Bill / Receipt 是否展示阶梯明细取决于模板设计。**强烈推荐展示**: +> +> - 业户能看清"为什么收 135 不是 105" +> - 政策合规(国家阶梯水电价要求公开透明) +> - 减少业户疑问 + +### 用得越多越贵的教育意义 + +阶梯计价**鼓励节约**: + +| 用量 | 总水费 | 平均单价 | +|---|---|---| +| 15 吨(低用)| 45 | 3.0 | +| 35 吨(本场景)| 135 | 3.86 | +| 60 吨(浪费)| 270 | 4.5 | + +业户**用越多平均单价越高**,符合"超额消费多付费"的政策导向。 + +## 业务人员视角 + +### 配置阶梯 + +阶梯定义在 `RatePlan` + `RateTier`(不在 meter 子模块,通常运营 / 财务总监配): + +- 后台 → 费率管理 → 选水费 → 编辑 RatePlan +- 加 RateTier:`tier=1, lower=0, upper=20, unit_price=3.0` +- 加 RateTier:`tier=2, lower=20, upper=30, unit_price=4.5` +- 加 RateTier:`tier=3, lower=30, upper=null, unit_price=6.0`(upper=null 表示无上限) + +> [!warning] 阶梯配置要严谨 +> 配置错的常见症状: +> - 段不连续(`tier1 upper=20`, `tier2 lower=22`)→ 21 吨用量无法分配 +> - 段重叠(`tier1 upper=20`, `tier2 lower=18`)→ 18-20 吨段算两次 +> - 缺最高段(没有 `tier_max`)→ 超过最高阶梯的用量无单价 +> +> 业务人员配置完后**用极端值算例**验证(0 / 1 / 20 / 21 / 30 / 31 / 100 / 1000 吨各算一遍看是否合理)。 + +### 月度账单生成 + +抄表完成 → 业务人员触发 `GenerateBillsFromMeterReadingsAction`(或自动)→ 系统调 `MeterBillCalculator` → 每张表算金额 → 建 Bill。 + +### 异常处理 + +阶梯计价的常见异常: + +| 异常 | 处置 | +|---|---| +| 业户用量极高(> 100 吨) | [[exception-high-consumption|高用量预警]] → 排查是否漏水 / 设备故障 | +| 业户用量极低(0 吨)| `min_amount` 兜底,详见 [[generate-bill-min-max-cap]] | +| 业户用量倒走(reading 错)| 走 [[exception-readings-locked-after-bill|修正流程]] | + +## 财务视角 + +### 账面会计 + +阶梯账单的**总金额**仍归"水费收入"科目。无需按阶梯拆分入账。 + +阶梯只影响**业户感知**(单价不同)和**业户行为引导**(鼓励节约),不影响会计核算。 + +### 报表统计 + +业务可能想看"本月各阶梯段用量分布"(政策报告 / 节约成效),需要单独的报表 SQL: + +```sql +-- 本月各阶梯段用量分布(简化版,真实算法更复杂) +SELECT + SUM(LEAST(consumption, 20)) AS tier1_volume, + SUM(GREATEST(LEAST(consumption, 30) - 20, 0)) AS tier2_volume, + SUM(GREATEST(consumption - 30, 0)) AS tier3_volume +FROM acc_meter_readings +WHERE meter_id IN (SELECT id FROM acc_meters WHERE community_id=? AND fee_type_id=水费) + AND read_at BETWEEN '2026-05-01' AND '2026-05-31'; +``` + +## 常见问题 + +> [!question] 阶梯按月 / 按年? +> 看物业政策: +> - **按月**(常见):每月 reset,从段 1 开始算 +> - **按年**(部分地区):全年累计,跨段更慢 +> +> 当前系统**按月**(单次抄表 = 单段计价)。按年的话需要不同算法(累加去年 12 月以来的用量,再分阶梯)。 + +> [!question] 不同物业 / 不同社区可以有不同阶梯吗? +> 可以。`RatePlan` 按 `community_id` + `fee_type_id` 隔离。每个社区独立配置。 + +> [!question] 阶梯改了,历史 Bill 怎么办? +> 历史 Bill 不变(`Bill.amount` 是当时算出的,不动态查 RatePlan)。新 Bill 按新阶梯算。 +> +> 这是正确做法(已发账单不应因配置变化追溯改金额)。 + +> [!question] 业户对算法有疑问怎么解释? +> 给业户看明细(段 1 + 段 2 + 段 3)+ 阶梯单价表。绝大多数业户看懂后接受。 + +> [!question] progressive 算法的边界用量(如 20 吨整)算哪段? +> 看实现细节: +> - `consumption=20`:段 1 全部(20 × 3 = 60),段 2 / 3 不进入 +> - `consumption=20.01`:段 1(20 × 3 = 60)+ 段 2(0.01 × 4.5 ≈ 0.045) +> - 边界是 inclusive 还是 exclusive 看 RateTier 配置(`lower`/`upper` 字段语义) + +> [!question] 阶梯计价对工业表(multiplier > 1)的影响? +> 倍率 + 阶梯叠加:`consumption = (current - previous) × multiplier`,然后这个 consumption 走阶梯。例如三相工业表 multiplier=10: +> +> - 物理表头读数差 28 度 → consumption = 280 度 +> - 280 度走阶梯计算 +> +> 详见 [[generate-bill-with-multiplier]]。 + +## 异常分支 + +- 工业表倍率参与 → [[generate-bill-with-multiplier]] +- 异常用量触发 min/max → [[generate-bill-min-max-cap]] +- 用量异常高(漏水)→ [[exception-high-consumption]] + +## 相关文档 + +- [[multiplier-and-tiered-pricing]] +- [[bill-generation-pipeline]] +- [[meter-vs-meter-reading]] +- [[generate-bill-with-multiplier]] +- [[generate-bill-min-max-cap]] diff --git a/prop-acc/scenarios/meter/generate-bill-with-multiplier.md b/prop-acc/scenarios/meter/generate-bill-with-multiplier.md new file mode 100644 index 0000000..ec3615d --- /dev/null +++ b/prop-acc/scenarios/meter/generate-bill-with-multiplier.md @@ -0,0 +1,234 @@ +--- +title: prop-acc · meter · 场景 - 工业表 10x 倍率生成账单 +aliases: + - 工业表账单 + - multiplier 计算 + - generate-bill-with-multiplier + - 场景-倍率表生成账单 +tags: + - 场景 + - prop-acc + - 计量表 + - 账单生成 + - 倍率 +audience: + - 业务人员 + - 财务 + - 抄表员 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:工业表 10x 倍率生成账单 + +商铺 / 集团表 / 三相工业表的物理表头**只显示用量的 1/10 或 1/100**,需要乘以倍率(multiplier)才是实际用量。系统在 `consumption = (current - previous) × multiplier` 公式中自动处理。 + +## 典型情境 + +> [!example] 真实情境 +> 嘉禾花园 1 楼商铺(中式餐厅)装的是**三相工业电表**(multiplier=10)。本月抄表: +> +> - 上月 reading.current_reading = 280 +> - 本月 reading.current_reading = 308 +> - 物理表头读数差 = 28 +> +> **实际用电量** = 28 × **10** = **280 度**(不是 28 度!) +> +> 按电费阶梯(0-200 度 0.8 元,200+ 度 1.0 元)算: +> +> ``` +> 段 1:200 × 0.8 = 160 元 +> 段 2:80 × 1.0 = 80 元 +> 合计: 240 元 +> ``` +> +> 商铺老板看到账单 ¥240 / 280 度。 + +如果**没有 multiplier**: + +``` +consumption = (308 - 280) × 1 = 28 度 +amount = 28 × 0.8 = 22.4 元 ❌ +``` + +少收 ¥217.6,商铺白用 252 度电。**物业必须用倍率算账,这是工业表的天然属性**。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 抄表员 + participant Filament + participant Calc[MeterBillCalculator] + participant 数据库 + + 抄表员->>Filament: 录入 reading current=308(物理表头) + Filament->>Filament: 查 meter.multiplier=10 + Filament->>Filament: 查 previous_reading=280(上月) + Filament->>Filament: consumption = (308 - 280) × 10 = 280 度 + Filament->>数据库: 建 MeterReading(consumption=280) + + Note over 数据库: 后续 GenerateBills + + Filament->>Calc: calculate(consumption=280, ratePlan) + Calc->>Calc: 阶梯算法 200*0.8 + 80*1 = 240 + Calc-->>Filament: 240 + Filament->>数据库: 建 Bill(amount=240) +``` + +## 抄表员视角(李师傅) + +### 抄表录入 + +抄表员看到物理表头是 **308**(不是 3080),录入时填 **308**。 + +> [!warning] 抄表员不应自己乘倍率 +> +> **错误做法**:抄表员看到 308 + 知道倍率 10 → 录入 3080 +> +> **后果**:系统再乘一次 10 → consumption = (3080 - 2800) × 10 = 2800 度 → 收业户 2800 度电费 → 业户疯狂投诉 +> +> **正确做法**:**严格录入物理表头数字**(308),倍率由系统自动处理。 + +Form 上应显示 meter 的 multiplier 提醒抄表员: + +``` +表编号:E-COMMERCIAL-1 +倍率:10x(工业表) +上次读数:280 + +本次读数:[___] (请录入物理表头数字) +``` + +### 与家用表的对比 + +| 维度 | 家用表(multiplier=1)| 工业表(multiplier=10/100/...)| +|---|---|---| +| 抄表员录入 | 表头数字 = 实际用量 | 表头数字 ≠ 实际用量 | +| 是否需要换算 | 不需要 | 需要(系统自动)| +| 抄表员错误风险 | 低 | **中**(可能录乘 / 不乘 10) | +| 业务培训 | 简单 | **需培训**(说清楚"录原始数字")| + +## 业务人员视角 + +### 配置倍率 + +建表时 `MeterForm.multiplier` 字段填: + +| 表类型 | 推荐 multiplier | +|---|---| +| 家用单相电表 | 1 | +| 家用水表 | 1 | +| 三相工业电表 | 10(常见)| +| 大型工业电表(高压侧) | 100 / 1000 | +| 工业大流量水表 | 10 / 100 | + +具体看表的物理铭牌 + 设计图纸。 + +### 倍率改了如何处理 + +> [!warning] 倍率改动影响极大 +> `multiplier` 改了不会重算历史 reading 的 consumption(那些数据已经存了)。但**会影响后续抄表**。 +> +> **何时可改 multiplier**: +> - 表是 `is_active=true`(退役表不可改,见 [[decommission-and-locking]]) +> - 表**没有任何已生成 Bill 的 reading**(若有,改 multiplier 让历史 vs 现在的算法不一致,审计困难) +> +> **推荐做法**:倍率配错 → 退役旧表 → 建新表用正确 multiplier(不走更换链,因为不是物理换表)。详见 [[decommission-without-replacement]]。 + +## 业户视角(商铺老板) + +### 您看到的账单 + +``` +2026 年 5 月电费账单 + +用电量:280 度 + (本月表头读数 308,上月 280,差 28 × 倍率 10) + +明细: + 0-200 度段:200 × 0.8 = 160 元 + 201+ 度段: 80 × 1.0 = 80 元 +合计:240 元 +``` + +> [!info] 账单展示倍率信息 +> 强烈推荐账单 / Receipt 展示"原始读数 + 倍率 + 计算公式",让业户看明白: +> +> - 业户看到差 28 度,但收 240 元,会疑惑(单价怎么算?) +> - 展示"28 × 倍率 10 = 280 度,按 280 算"业户秒懂 + +### 商铺老板的特殊关注 + +| 业户疑问 | 应答 | +|---|---| +| "为什么我家是工业表不是家用表?" | 商铺用电量大,法规要求工业表 | +| "倍率是物业定的吗?" | 不是,是物理表的属性,出厂时定 | +| "可以换成家用表吗?" | 不能,商铺合规上必须用工业表 | + +## 倍率与阶梯的叠加 + +倍率**先算**,得到 consumption;然后 consumption 走阶梯。两者完全独立、按顺序应用。 + +完整公式: + +``` +consumption = (current - previous) × multiplier # 第 1 层 +amount = sum(段 i 用量 × 段 i 单价) # 第 2 层(阶梯) +amount = clamp(amount, min, max) # 第 3 层(min/max 封顶) +``` + +详见 [[multiplier-and-tiered-pricing]] 完整说明。 + +## 不同 multiplier 表的算例 + +| 表类型 | multiplier | 表头读数差 | consumption | 单价 0.8 | 账单 | +|---|---|---|---|---|---| +| 家用 | 1 | 280 | 280 | 224 | 224 | +| 三相工业 | 10 | 28 | 280 | 224 | 224 | +| 大工业(高压侧) | 100 | 2.8 | 280 | 224 | 224 | + +**同样 280 度,账单相同**,只是物理表头数字大小不同。系统的 multiplier 字段把这层差异隐藏在算法里。 + +## 常见问题 + +> [!question] multiplier 必须是整数吗? +> 不必须。decimal(10,4) 精度,支持 0.5 / 1.25 等小数。但 1.0 / 10.0 / 100.0 是市场常见的"整数倍率"。 + +> [!question] 同一表的 multiplier 会随时间变吗? +> 物理上**不会**。表的物理参数(变压比)是出厂时确定的,使用过程中不变。 +> +> 系统层面**可改字段**(若 `is_active=true` 且无 Bill,Policy 允许),但**不推荐**改 —— 历史与现在的算法不一致,审计困难。 + +> [!question] 抄表员录入时如何区分"录物理读数"vs"录乘倍率后读数"? +> Form 上**明确显示倍率**+ 标注"请录入物理表头数字"。培训抄表员**严格遵守**。系统统一存物理表头数字。 + +> [!question] 集抄系统推过来的数据是物理读数还是乘了倍率? +> 看集抄运营商。**通常是物理读数**(IoT 设备读表头,不知道倍率)。本系统接收时按 `meter.multiplier` 算 consumption。 +> +> 如果集抄推已乘倍率的数据 → 集抄端**降级处理**(本系统 multiplier 应该设为 1,避免重复乘)。需对接时讲清楚。 + +> [!question] 配置错倍率(应该 10 配成 1)会怎样? +> 系统按 multiplier=1 算 → consumption 缩小 10 倍 → 业户账单缩小 10 倍 → 物业损失收入(直到发现)。 +> +> 发现后修复: +> - 改 multiplier=10(若 Policy 允许) +> - 修复历史已欠收(走"补开账单"业务流程,系统不直接支持) +> - 通知业户 + 退还 / 补收差额 + +## 异常分支 + +- 阶梯计价(本场景叠加)→ [[generate-bill-tiered-pricing]] +- min/max 封顶 → [[generate-bill-min-max-cap]] +- 倍率配错想改 → 复杂,通常退役旧表新建([[decommission-and-locking]]) +- 抄表员录错乘倍率 → [[exception-readings-locked-after-bill]] 修正 + +## 相关文档 + +- [[multiplier-and-tiered-pricing]] +- [[bill-generation-pipeline]] +- [[meter-vs-meter-reading]] +- [[generate-bill-tiered-pricing]] +- [[generate-bill-min-max-cap]] diff --git a/prop-acc/scenarios/meter/read-via-iot-remote-source.md b/prop-acc/scenarios/meter/read-via-iot-remote-source.md new file mode 100644 index 0000000..d25a838 --- /dev/null +++ b/prop-acc/scenarios/meter/read-via-iot-remote-source.md @@ -0,0 +1,246 @@ +--- +title: prop-acc · meter · 场景 - 集抄系统自动推送(source=remote) +aliases: + - 集抄系统 + - IoT 抄表 + - read-via-iot-remote-source + - 场景-集抄自动抄表 +tags: + - 场景 + - prop-acc + - 计量表 + - 抄表 + - IoT +audience: + - 业务人员 + - 架构师 + - 抄表员 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:集抄系统自动推送(source=remote) + +集抄系统(IoT)通过 RS485 / NB-IoT / LoRa 等技术**远程读取**物理表读数,定时(每天 / 每小时 / 每月)推送给本系统。本系统收到后建 `MeterReading(source=remote)`,**完全自动,无人工介入**。是现代化物业的标配。 + +## 典型情境 + +> [!example] 真实情境 +> 嘉禾花园 2 年前升级了集抄系统,1,200 张水电气表全部接入 IoT 网关。集抄运营商每月 1 日凌晨**统一上传上月用量数据**到本系统。 +> +> 物业财务王主管早上来上班,集抄数据已经全部到位 + 账单已经自动生成。她只需要: +> - 看 dashboard 异常告警(高用量 / 集抄掉线表) +> - 手工处理少数异常情况 + +## 系统流程 + +```mermaid +sequenceDiagram + participant Meter[物理表] + participant Gateway[IoT 网关] + participant Vendor[集抄运营商平台] + participant API[本系统集抄 API] + participant 数据库 + participant GenerateBills + + Note over Meter: 物理表每月 1 日凌晨自报 + + Meter->>Gateway: 读数推送(RS485 / NB-IoT) + Gateway->>Vendor: 上传(GSM / 4G / 有线) + Vendor->>Vendor: 整合 + 校验 + 归档 + + Note over Vendor: 月度批量推送(可能是每天 / 每月) + + Vendor->>API: HTTP POST(批量推 readings) + API->>API: 校验签名 + 防重放 + API->>API: 校验 meter 是否存在 + 是否 active + + loop 每条 reading + API->>数据库: 建 MeterReading(source=remote, operated_by=null, photo_url=null) + end + + API->>GenerateBills: handle(刚建的 readings) + GenerateBills->>数据库: 批量建 Bill + 回写 reading.bill_id + + API-->>Vendor: 响应(成功 N,失败 M) +``` + +## 业务人员视角 + +### 日常(集抄正常运行) + +业务人员**几乎不操作**: + +- 看 `MeterDashboard`(自动跳出异常告警) +- 月度对账(`audit-meters-needing-reading.md` 类似审计) +- 不需要手动抄表 / 录入 + +### 月初(集抄推数完成后) + +通常上午到岗时: + +- 所有 reading 已经在 `MeterReading` 表里 +- 所有 Bill 已经生成 +- Dashboard 显示:本月已抄 N 张 / 应抄 M 张 +- 看 `HighConsumptionReadingsListWidget` 异常告警 → 处理([[exception-high-consumption]]) +- 看 `MetersNeedingReadingListWidget` 显示掉线 / 漏抄表(本场景的兜底:走 [[read-single-meter-manual]] 个别补抄) + +### 异常(集抄掉线) + +| 掉线规模 | 处置 | +|---|---| +| 个别表(< 5%)| 抄表员补抄([[read-single-meter-manual]])| +| 大面积(网关故障)| 联系集抄运营商修复 + 物业短期手抄兜底 | +| 长期掉线(> 2 周)| 重新评估集抄设备的可靠性 | + +## 集抄 API 设计(待文档化 / 实现细节) + +> [!info] 当前实施状态 +> 集抄 API 的**具体实现细节**(endpoint、签名、数据格式、防重放)需要看代码或与集抄运营商对接文档,本场景描述**业务流程层**。 + +### 接口要求 + +| 要求 | 说明 | +|---|---| +| 接收端 | 本系统提供 HTTP POST endpoint | +| 数据格式 | JSON 数组,每条 reading 字段:meter_code / read_at / current_reading / community_id / vendor_signature | +| 签名校验 | 防伪造,通常 HMAC-SHA256 或非对称签名 | +| 防重放 | 同 meter + 同 read_at 不重复建(unique index)| +| 错误响应 | 详细告知哪条失败、原因 | +| 失败重试 | 集抄运营商按响应决定重试逻辑 | + +### MeterReading 字段对照 + +集抄推送的 reading,数据库存的 MeterReading: + +| 字段 | 集抄数据 | 系统存 | +|---|---|---| +| `meter_id` | 集抄推 meter_code → 系统查 | meter ID | +| `read_at` | 集抄推时间(通常表自身上报) | datetime | +| `current_reading` | 物理表读数 | decimal(12,2) | +| `previous_reading` | (系统自动取上次)| decimal | +| `consumption` | (系统自动算)| decimal | +| **`source`** | (系统设)| **`remote`** | +| `operated_by` | null(集抄无人)| null | +| `photo_url` | null(集抄无照片)| null | +| `bill_id` | null(初始)→ 后续 GenerateBills 回写 | 可空 | +| `memo` | 选填(集抄可能写型号 / 网关 ID)| text | + +## 集抄 vs 手抄 数据差异 + +| 维度 | manual(手抄) | **remote(集抄)** | +|---|---|---| +| 录入速度 | 慢(几天到几周)| **几小时全社区** | +| 准确性 | 中(手抖 / 看错)| **高**(机器直读)| +| 拍照存证 | 强烈推荐 | 无(IoT 设备自身就是凭证)| +| operated_by | 抄表员 ID | null | +| 业户感知 | 抄表员上门 | **无感**(无人上门)| +| 异常处理 | 抄表员现场判断 | 系统后台告警 → 人工介入 | +| 成本 | 抄表员工资 | IoT 设备 + 网关 + 月度服务费 | +| 业户家在不在影响 | 影响(开门才能抄) | 无影响 | + +## 业户视角 + +业户**完全无感** —— 集抄系统的存在业户可能都不知道(除非物业告知)。 + +唯一感知: + +- 账单出来更准时(每月固定日期到账) +- 没人上门抄表(隐私感更好) +- 用量明细可能更详细(集抄能支持小时 / 天级别 granularity,虽然账单仍是月度) + +## 集抄设备与本系统的关系 + +```mermaid +flowchart TB + subgraph "物理层" + M1[家用水表] + M2[家用电表] + M3[工业表] + end + + subgraph "网关层" + G1[楼栋网关
RS485 → 4G] + end + + subgraph "运营商层(第三方)" + V[集抄运营商平台
设备管理 / 数据归档 / API 输出] + end + + subgraph "本系统(prop-acc)" + API[集抄 API endpoint] + DB[(MeterReading)] + GenerateBills[GenerateBillsFromMeterReadingsAction] + Bill[(Bill)] + end + + M1 --> G1 + M2 --> G1 + M3 --> G1 + G1 --> V + V -->|每月推送| API + API --> DB + DB --> GenerateBills + GenerateBills --> Bill +``` + +**本系统只**管接收 + 存 + 生成账单。**不管**物理设备、网关、信号质量。这些都是集抄运营商的事。 + +## 常见问题 + +> [!question] 集抄数据准确吗?会不会比手抄更容易出错? +> 集抄数据**理论上更准**(机器直读,没手抖)。但: +> +> - IoT 设备本身可能故障(读数漂移、传输错) +> - 信号不好可能漏传(掉线) +> - 集抄运营商平台可能 bug(数据传错) +> +> 准确性强烈依赖**集抄运营商的可靠性**。选大牌运营商 + 完善 SLA。 + +> [!question] 集抄推过来的数据能改吗? +> 不能(MeterReading 不可变,见 [[decommission-and-locking]])。如果集抄推错: +> +> - 在系统侧建错误 reading +> - 走作废 Bill → 删 reading(若 Policy 允许)→ 集抄重推 +> - 或运维 tinker 介入 + +> [!question] 集抄运营商收费贵不贵? +> 看市场,通常: +> - 设备 + 安装一次性几十到几百每点 +> - 月度服务费 几元 / 表 / 月 +> +> 中型社区(1,000 表)月度成本可能几千到几万。物业自行评估"省抄表员工资 vs 集抄费用" ROI。 + +> [!question] 集抄系统对接需要多久? +> 看运营商: +> - 大牌(华为 / 海康)平台标准 API → 1-2 周(主要是数据格式映射 + 测试) +> - 小厂自研协议 → 1-3 个月(需自定义对接代码) + +> [!question] 集抄推数据时本系统有问题(数据库挂 / 网络断)? +> 集抄运营商应有重试机制(看 SLA)。本系统应: +> - API 幂等(同 meter + 同 read_at 不重复建) +> - 失败响应详细(让运营商知道哪条没收到) +> - 重要数据有补传机制(运营商手动重推) + +> [!question] 集抄数据与业户预存款自动扣的关系? +> 集抄推数 → 生成 Bill → 触发 [[../prepaid/auto-deduction-design|预存款自动抵扣 job]](待实现)→ 业户预存款余额自动扣 + Bill 翻 Paid。 +> +> 整条链**完全无人介入**,业户次日推送:"5 月电费 ¥168 已自动扣,余额 ¥X"。 + +## 异常分支 + +- 集抄掉线个别表 → [[read-single-meter-manual]] 兜底 +- 集抄数据异常用量 → [[exception-high-consumption]] +- 集抄推错数据需修正 → [[exception-readings-locked-after-bill]] +- 集抄 API 故障 → 运维 / 集抄运营商支持 + +## 相关文档 + +- [[meter-vs-meter-reading]] +- [[reading-source-and-photo-proof]] +- [[bill-generation-pipeline]] +- [[read-single-meter-manual]] +- [[read-batch-via-excel-import]] +- [[exception-high-consumption]] diff --git a/prop-acc/scenarios/meter/read-with-photo-proof.md b/prop-acc/scenarios/meter/read-with-photo-proof.md new file mode 100644 index 0000000..080002b --- /dev/null +++ b/prop-acc/scenarios/meter/read-with-photo-proof.md @@ -0,0 +1,229 @@ +--- +title: prop-acc · meter · 场景 - 抄表拍照存证(物理表头照片) +aliases: + - 抄表拍照 + - 照片存证 + - read-with-photo-proof + - 场景-抄表拍照存证 +tags: + - 场景 + - prop-acc + - 计量表 + - 抄表 + - 存证 +audience: + - 业户 + - 业务人员 + - 抄表员 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:抄表拍照存证(物理表头照片) + +抄表员**录入读数同时上传现场拍的物理表头照片**(`photo_url`),作为**业户事后质疑账单时的关键凭证**。配合 [[read-single-meter-manual|单录]] 流程使用。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨 6 月初收到 5 月电费账单 ¥168(280 度),她声称"我家平时就 100 多度,不可能这么多": +> +> - 王主管打开后台 → 找 E-501 的 5 月 reading → 看 `photo_url` +> - 照片清晰显示:**表头数字 5,280**(日期戳 2026-05-26,环境信息可识别为张阿姨家配电箱) +> - 拿照片给张阿姨看 → **业户无话可说**(确实读数是 5,280) +> - 王主管协助分析:可能近期家里某电器异常耗电(空调 24 小时开?旧冰箱故障?) +> - 业户回去自查 → 发现儿子用了电烤箱 + 空气炸锅一个月 → 接受账单 +> +> 拍照存证**避免了一次纠纷**。 + +## 抄表员视角(李师傅) + +### 拍照要点 + +每张表抄表时,**拍 1-2 张照片**: + +| 角度 | 内容 | +|---|---| +| **正面特写** | 表头数字清晰可读(关键!)| +| **环境照** | 表 + 周围环境(配电箱 / 房号牌),证明是哪家的表 | + +照片要素: + +- **清晰**(对焦,光线足够) +- **包含表头读数全部位数**(不要遮挡) +- **时间戳**(手机 EXIF 元数据自动带) +- **GPS**(若手机 App 支持,更好) + +### 录入 + +后台 / 抄表 App 上的 reading 录入 form 含 `photo_url` 字段: + +| 字段 | 填什么 | +|---|---| +| 当前读数 | 5280 | +| 拍照 | 上传刚才的照片(或 App 直接拍照上传)| +| 备注 | 选填,如"电表正常,环境无异常" | + +上传后照片存储到对象存储(S3 / OSS),`photo_url` 字段存对外可访问 URL。 + +> [!info] 强制度看物业政策 +> - **严格物业**:Form 上 `photo` 字段 `->required()`,无照片不能提交 +> - **建议物业**:Form 可空,UI 上有"建议拍照"提示 +> - **宽松物业**:Form 可空,无提示 +> +> 当前实现看 `MeterReadingsRelationManager` 的 form 配置。**生产环境强烈推荐严格模式**。 + +## 业务人员视角 + +### 查照片 + +后台 → 计量表 → ViewMeter → Reading 列表(`MeterReadingsRelationManager`): + +- 每条 reading 显示"照片"图标(若 photo_url 不为空) +- 点击图标 → 弹出照片预览 +- 可下载原图 + +### 业户争议处理 + +业户来电话 / 上门质疑账单: + +1. 王主管打开对应 reading +2. 调出 photo_url 照片 +3. 与业户当面 / 视频核对 +4. 业户接受 → 案件了结 +5. 业户仍质疑 → + - 物业派人**现场再读一次**(若表还在) + - 若现场读数与照片一致 → 业户更难反驳 + - 若现场读数与照片不一致 → 表可能故障 / 数据有问题,走 [[replace-broken-meter|换表]] / [[exception-readings-locked-after-bill|修正流程]] + +## 拍照流程图 + +```mermaid +sequenceDiagram + participant 李师傅 + participant 物理表 + participant 手机App + participant ObjectStorage[对象存储 S3/OSS] + participant 数据库 + + 李师傅->>物理表: 现场观察读数 + 李师傅->>物理表: 拍照 + 李师傅->>手机App: 录入读数 + 选刚拍的照片 + 手机App->>ObjectStorage: 上传照片 + ObjectStorage-->>手机App: 返回 URL + 手机App->>数据库: 建 MeterReading + photo_url +``` + +## 业户视角 + +### 您会感受到什么 + +通常**不感知** —— 抄表员拍照是物业内部流程。 + +唯一影响: + +- 抄表员上门时可能要求"打开配电箱看表"(配合) +- 极端情况:业户怀疑抄表员拍假照(看清照片是否真的是自己家表 = 看背景环境) + +### 您的权利 + +对账单有异议时**有权要求**: + +- 看抄表照片 +- 派人现场再读一次 +- 提交业户自己拍的反证照片(若业户当时也拍了) + +## 拍照的额外用途 + +除业户争议外,照片还有其他价值: + +| 用途 | 怎么用 | +|---|---| +| **审计追溯**(年审 / 监管)| 抽查某月某些 reading 的 photo_url,验证抄表真实性 | +| **抄表员考核** | 检查抄表员是否真去现场(对比 GPS / 时间 / 环境)| +| **培训新人** | 给新抄表员看老抄表员的照片范例 | +| **争议预防** | 业户知道有照片在,主动质疑减少 | + +## 拍照的成本 + +| 成本 | 说明 | +|---|---| +| 抄表员时间 | 每张表 +5-10 秒 → 1,000 张表 +1-2 小时 | +| 手机存储 | 每张照片 1-5 MB → 1,000 张 = 几 GB / 月 | +| 对象存储 | 几 GB / 月 ≈ 几元 / 月(阿里云 OSS / S3) | +| 长期累积 | 5 年 = TB 级,需归档策略 | +| 培训 | 抄表员要学会拍合格照片(对焦、清晰、含表头全部数字)| + +整体**成本可控**,但需要持续投入。 + +## 拍照的法律意义 + +> [!info] 业户法律纠纷时 +> +> 在物业 vs 业户法律纠纷中,拍照照片是**关键物证**: +> +> - 照片有**时间戳 + GPS + 环境元数据**(EXIF) → 可证明真实性 +> - 照片清晰显示读数 + 表号 → 可证明读数正确 +> - 法院通常**采信** 这类专业现场记录 +> +> 物业**完全没有拍照** → 只有业户口供 vs 物业账面数据 → 物业举证困难。 + +## 集抄(remote)与拍照的关系 + +集抄 reading(`source=remote`)**通常无拍照**: + +- IoT 设备直接传数据,无相机 +- IoT 设备本身就是凭证(机器读数 → 通常比人工准) +- 集抄掉线的兜底 [[read-single-meter-manual]] 仍要拍照 + +详见 [[reading-source-and-photo-proof]]"拍照存证"段。 + +## 常见问题 + +> [!question] 业户拒绝抄表员进屋拍照怎么办? +> 公共部位的表(楼道电表、燃气表)通常装在外面,不用进屋。 +> +> 入户表(水表常在厨房 / 卫生间)若业户拒绝: +> - 协商上门时间(业户在家时) +> - 业户**自己拍照**发给物业(部分物业接受) +> - 长期拒绝 → 物业政策决定(警告 / 法律手段 / 估算用量) + +> [!question] 照片上传失败怎么办? +> 网络问题 / 对象存储故障: +> - App 应有**本地缓存**机制(离线时存手机,联网时上传) +> - 上传失败的 reading 应**仍能提交**(photo_url 为空但有备注"上传失败") +> - 后续重传(若设计支持) + +> [!question] 照片存多久? +> 法律 / 业务规定: +> - 中国通常 3-5 年(看物业法规) +> - 与账单同周期(账单存多久,照片存多久) +> - 长期归档(冷存储,如阿里云 OSS Archive)成本极低 + +> [!question] 一张照片能证明哪张表是谁家的? +> 照片本身不能(看不出房号)。需配合: +> - **环境识别**(房号牌、门口装饰、邻居环境) +> - **抄表员手写标注**(若 App 支持) +> - **GPS 坐标**(若 App 支持) +> - **数据库 meter_id 关联**(reading 关联 meter,meter 关联 asset) + +> [!question] 业户对照片质疑"那不是我家的表"怎么办? +> 调取**原始 EXIF 数据**(时间戳、GPS、设备型号)→ 证明照片真实性。物业可派人**现场对照**确认表号物理一致。 + +## 异常分支 + +- 单录(本场景前置)→ [[read-single-meter-manual]] +- 批量导入(无拍照)→ [[read-batch-via-excel-import]] +- 集抄(无拍照)→ [[read-via-iot-remote-source]] +- 拍照后业户仍质疑高用量 → [[exception-high-consumption]] + +## 相关文档 + +- [[reading-source-and-photo-proof]] +- [[read-single-meter-manual]] +- [[read-batch-via-excel-import]] +- [[read-via-iot-remote-source]] +- [[exception-high-consumption]] +- [[decommission-and-locking]]