From 6bdaa31017648fe9213c81beeb315812a9567721 Mon Sep 17 00:00:00 2001 From: Willie Date: Tue, 26 May 2026 00:28:09 +0800 Subject: [PATCH] vault backup: 2026-05-26 00:28:09 --- .obsidian/workspace.json | 6 +- .../meter/exception-high-consumption.md | 239 +++++++++++++++ .../exception-readings-locked-after-bill.md | 249 ++++++++++++++++ .../meter/generate-bill-min-max-cap.md | 273 ++++++++++++++++++ 4 files changed, 764 insertions(+), 3 deletions(-) create mode 100644 prop-acc/scenarios/meter/exception-high-consumption.md create mode 100644 prop-acc/scenarios/meter/exception-readings-locked-after-bill.md create mode 100644 prop-acc/scenarios/meter/generate-bill-min-max-cap.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 0dcff08..c6414b1 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -197,6 +197,9 @@ }, "active": "849c5ff8936a2b67", "lastOpenFiles": [ + "prop-acc/scenarios/meter/exception-readings-locked-after-bill.md", + "prop-acc/scenarios/meter/exception-high-consumption.md", + "prop-acc/scenarios/meter/generate-bill-min-max-cap.md", "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", @@ -222,9 +225,6 @@ "prop-acc/scenarios/prepaid/close-with-zero-balance-decision.md", "prop-acc/scenarios/prepaid/close-resident-moveout.md", "prop-acc/scenarios/prepaid/unfreeze-after-verification.md", - "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", "prop-acc/concepts/prepaid", "prop-acc/scenarios/deposit", diff --git a/prop-acc/scenarios/meter/exception-high-consumption.md b/prop-acc/scenarios/meter/exception-high-consumption.md new file mode 100644 index 0000000..60d776d --- /dev/null +++ b/prop-acc/scenarios/meter/exception-high-consumption.md @@ -0,0 +1,239 @@ +--- +title: prop-acc · meter · 场景 - 高用量异常(漏水/电器故障) +aliases: + - 高用量预警 + - 漏水告警 + - exception-high-consumption + - HighConsumptionReadingsListWidget + - 场景-高用量异常 +tags: + - 场景 + - prop-acc + - 计量表 + - 异常 +audience: + - 业务人员 + - 业户 + - 抄表员 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:高用量异常(漏水/电器故障) + +业户**用量异常**(漏水 / 电器故障 / 偷电),系统通过 `HighConsumptionReadingsListWidget` 在月度抄表数据出来后**主动预警**,业务人员介入排查,避免账单生成后业户暴击 / 投诉。 + +## 典型情境 + +> [!example] 真实情境 +> 5 月底集抄 / 批量抄表完成,王主管打开 `MeterDashboard`: +> +> - `HighConsumptionReadingsListWidget` 显示 **本月用量 top 20** 的 reading +> - 其中前 3 名: +> +> | 业户 | 表 | 上月 | 本月 | 用量 | 异常? | +> |---|---|---|---|---|---| +> | 张阿姨(12-3-501) | 水表 | 12 吨 | **800 吨** | 800 | 🔴 极度异常(漏水) | +> | 陈先生(12-3-502) | 电表 | 200 度 | **2,800 度** | 2,800 | 🔴 异常高(空调故障 / 偷电) | +> | 商铺一楼餐厅 | 电表 | 1,500 度 | **3,200 度** | 3,200 | 🟡 警告(正常波动?旺季?)| +> +> 王主管立即处理。 + +## 业务人员视角 + +### Widget 显示 + +`HighConsumptionReadingsListWidget`(后台 → MeterDashboard): + +| 列 | 内容 | +|---|---| +| 业户 / 资产 | 房号 + 业户姓名 | +| 表编号 | meter code | +| 上月用量 | previous reading 推出 | +| 本月用量 | consumption | +| 倍数 | 本月 / 上月 | +| 抄表日期 | read_at | +| 操作 | 链接到 reading 详情 / 查看 / 联系业户 | + +排序:**按 consumption 降序 top 20**(简版实现,详见下方"待补")。 + +### 分级处置 + +| 级别 | 触发 | 处置 | +|---|---|---| +| 🔴 **极度异常**(>10× 历史平均) | 漏水 / 大型故障 | 立即联系业户 + 派人现场检查 | +| 🔴 **高异常**(>3× 历史平均) | 设备故障 / 习惯改变 | 联系业户确认 | +| 🟡 **警告**(>1.5× 历史平均) | 正常波动 / 季节性 | 推送提醒 / 标记观察 | +| 🟢 **正常** | < 1.5× | 不处理 | + +### 处理流程(漏水案例) + +```mermaid +flowchart TD + A[Widget 显示张阿姨水 800 吨] --> B[王主管联系张阿姨] + B --> C{业户回应} + + C -->|"我不知道,你来看看"| D[派维修队上门] + C -->|"啊?我不可能用这么多"| D + C -->|"我装修了用水多"| E[业户接受账单] + + D --> F{查到原因?} + F -->|墙内暗水管漏水| G[维修 + 重算账单] + F -->|无可见漏水点| H[换表测试是否表故障] + + H --> I{换表后情况} + I -->|新表用量正常| J[确认旧表故障 + 走修正流程] + I -->|新表用量仍异常| K[排查家电 / 业户习惯] + + G --> L[与业户协商:走 max 封顶/部分减免/重算] + J --> L +``` + +### 修正账单(若证实表故障) + +如果证实是表故障 / 抄表错: + +1. 走 [[replace-broken-meter|换表]]([[decommission-and-locking|退役旧表]]) +2. 旧 reading 已生成 Bill → 走 [[exception-readings-locked-after-bill|作废 Bill]] 流程 +3. 重新算正确用量 → 重生成 Bill +4. 业户付正确金额 + +## `HighConsumptionReadingsListWidget` 实现现状 + +> [!info] 当前实现简版,issue.md Q5 已标待升级 +> 当前:**按 consumption 降序 top 20**,不是统计学意义的异常检测。 +> +> issue.md Q5"待补": +> +> - **3σ 异常**(对比历史 3 个月平均的标准差) +> - **倒走告警**(current < previous) +> - **0 读数告警**(可能表故障) +> +> 当前简版的缺陷: +> - 排名靠前的可能是商铺 / 工业表(正常用量大),不是真异常 +> - 漏掉"中等用量但异常波动"的住户(从 12 吨涨到 50 吨,绝对数小但相对倍数高) +> - 没区分"业户家用电习惯变了"vs"漏水 / 故障" + +业务人员**需自行判断**(看 Widget 内容 + 历史用量对照 + 联系业户)。 + +## 业户视角 + +### 您可能收到的联系 + +物业打电话 / 微信 / 上门: + +> 张阿姨您好,您家本月水量异常高(800 吨,平时 12 吨)。请问您家最近是否有装修 / 漏水 / 大量用水活动?如有疑问,我们可派维修队上门检查。 + +### 您要做的 + +| 情况 | 您要做 | +|---|---| +| 确实在装修 / 大量用水 | 接受账单 + 看是否触发 [[generate-bill-min-max-cap|max 封顶]] 减免 | +| 不知道原因 / 怀疑漏水 | 同意物业派人上门检查 | +| 怀疑表 / 系统错 | 要求看 [[read-with-photo-proof|抄表照片]] + 派人现场再读一次 | + +### 检测到漏水后的减免 + +物业**通常会减免**(看政策): + +- 部分减免(承担一半 / 30%) +- 全免(罕见,看物业宽厚) +- 按"平时月用量"算账(常见) +- max 封顶后业户支付封顶值,差额物业承担 + +具体看物业与业户的协商 + 物业的"漏水维修保险"理赔。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 集抄/抄表员 + participant 系统 + participant Widget[HighConsumptionReadingsListWidget] + participant 王主管 + participant 业户 + + 集抄/抄表员->>系统: 推 / 录入本月 reading + 系统->>系统: 建 MeterReading + 算 consumption + + Note over 系统: 月度数据完成 + + 王主管->>Widget: 打开 MeterDashboard + Widget->>系统: SELECT TOP 20 reading ORDER BY consumption DESC + 系统-->>Widget: 显示 top 20 异常清单 + + 王主管->>王主管: 看清单 → 分级处置 + + loop 每个 🔴 异常 + 王主管->>业户: 联系 + 排查 + alt 漏水 / 故障 + 王主管->>系统: 走修正流程(换表 / 作废 Bill 重算) + else 业户认账 + 王主管->>系统: 接受 + 触发 max 封顶减免(若适用) + end + end +``` + +## 高用量的常见原因清单 + +| 原因 | 业户感知 | 处置 | +|---|---|---| +| **水管漏水**(墙内 / 管井) | 业户不知道,直到账单异常 | 派维修队 + 修管子 + 减免 | +| **马桶漏水**(节流阀坏) | 业户偶尔听到流水声 | 业户自修 / 物业协助 | +| **空调 24h 不关** | 业户习惯 | 业户调整 / 接受账单 | +| **旧冰箱故障**(压缩机一直跑) | 业户不知道 | 业户换冰箱 | +| **电热水器**(储热式漏电 / 一直加热) | 业户不知道 | 业户检查 | +| **业户偷电 / 绕表** | 物业 / 国家电网监管 | 法律责任 | +| **抄表错** | (系统层面)| 走 [[exception-readings-locked-after-bill|修正]] | +| **集抄数据错** | (系统层面)| 同上 | +| **表故障**(乱跳)| 业户长期感觉 | [[replace-broken-meter|换表]] | +| **大型装修 / 大量用水活动** | 业户自知 | 业户接受账单 | + +## 常见问题 + +> [!question] Widget 显示的"top 20"是当月吗? +> 看 Widget 实现。可能是: +> +> - 本月(`read_at` 在当月) +> - 最近 30 天 +> - 所有未结账 reading +> +> 业务上推荐"本月",每月初看一遍 + 月底再看一遍。 + +> [!question] 排查发现是抄表错 + 已经生成 Bill 了怎么办? +> 走 [[exception-readings-locked-after-bill]] 流程:作废 Bill → 修正 reading → 重生成 Bill。复杂,需运维 / 高权限介入。 + +> [!question] 业户漏水但拒不修怎么办? +> 物业**强烈建议** + **法律手段**(漏水可能影响楼下邻居,涉及侵权)。系统层面无法干预。 + +> [!question] 商铺 / 工业用户的"高用量"和住宅"高用量"判断标准应该一样吗? +> **不一样**。商铺正常用量本来就大。Widget 当前简版**无区分**,需业务人员自己判断。 +> +> 升级建议:Widget 按 `asset_type`(住宅 / 商铺 / 工业)分别统计 + 各自的"异常阈值"。 + +> [!question] 排查后没找到原因(业户也说没漏水也没新增电器)? +> 几个可能: +> - 业户家有人偷接电(罕见) +> - 表故障(漂移)→ 换表观察一个月 +> - 集抄 / 抄表系统 bug(同时多户异常?排查系统) + +> [!question] 高用量预警之外,有"用量异常低"(可能业户搬走 / 表故障 0 读数)预警吗? +> 当前**无**(只有高用量 widget)。需求详见 issue.md Q5"待补"。 + +## 异常分支 + +- 排查确认是抄表错 → [[exception-readings-locked-after-bill]] 修正 +- 排查确认是表故障 → [[replace-broken-meter|换表]] +- 业户接受账单 → 走 [[generate-bill-min-max-cap|max 封顶]](若适用) +- 待抄表清单(对偶场景)→ [[audit-meters-needing-reading]] + +## 相关文档 + +- [[multiplier-and-tiered-pricing]] +- [[generate-bill-min-max-cap]] +- [[exception-readings-locked-after-bill]] +- [[replace-broken-meter]] +- [[audit-meters-needing-reading]] +- [[reading-source-and-photo-proof]] diff --git a/prop-acc/scenarios/meter/exception-readings-locked-after-bill.md b/prop-acc/scenarios/meter/exception-readings-locked-after-bill.md new file mode 100644 index 0000000..8feae08 --- /dev/null +++ b/prop-acc/scenarios/meter/exception-readings-locked-after-bill.md @@ -0,0 +1,249 @@ +--- +title: prop-acc · meter · 场景 - 已生成 Bill 的 Reading 锁定,要修正需作废 Bill +aliases: + - Reading 锁定 + - 已落账 reading 修正 + - exception-readings-locked-after-bill + - 作废 Bill 重算 + - 场景-已落账读数修正 +tags: + - 场景 + - prop-acc + - 计量表 + - 异常 + - 数据完整性 +audience: + - 业务人员 + - 架构师 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:已生成 Bill 的 Reading 锁定,要修正需作废 Bill + +`MeterReading` 一经创建**不可改**;**一旦生成 Bill 更不可改 / 不可删**(双锁,见 [[decommission-and-locking]])。如果发现已落账 reading 数据错(抄表录错 / 集抄错传 / 业户质疑成功),需走**复杂修正流程**:**先作废 Bill → 改 reading(实际是建新 reading + 标旧 reading 作废)→ 重生成 Bill**。 + +## 典型情境 + +> [!example] 真实情境 +> 5 月底抄表 + 自动生成账单。陈先生 5 月电费账单 ¥1,200(2,800 度,异常高,触发 [[exception-high-consumption|高用量预警]])。 +> +> 物业派人排查,发现: +> - **抄表员李师傅手抖**:把 1,500 录成 2,500 +> - 实际本月用量 ~1,500 度,账单应 ¥600 左右(不是 1,200) +> +> 陈先生质疑成功,物业要把账单从 1,200 改成 ~600。但: +> - **Reading 不可改**(系统层面双锁) +> - **Bill 已经生成 + 关联 reading** +> +> 需要走**作废 + 重算**的组合流程。 + +## 当前实施状态 + +> [!warning] **当前系统不支持自动化此流程** +> +> issue.md Q5 "待补"段明确记录: +> +> > **"作废已生成 Bill 的 MeterReading"组合流程**:当前 Reading 一旦生成 Bill 即锁定。要修正错误读数需要先**作废 Bill** 再改 Reading 再重新生成。这个组合流程类似 AdHocEvent 的 `VoidAction`(级联废 + 留 voided 审计),需要单独设计。等业务方明确"已收款的 Bill 应该怎么撤销"再做(可能涉及红字 Bill / 退款)。 +> +> **当前替代**:运维 / 高权限人员通过 tinker 手工处理。本场景描述**业务流程层**和**未来的目标态**。 + +## 业务人员视角(当前手工处理) + +### 第 1 步:确认要修正 + +- 业户质疑账单 + 提供合理证据(自家拍照 / 历史数据对照) +- 物业核对:抄表照片 / 集抄数据 / 物理表 +- 内部决定:确实要修正 + +### 第 2 步:看账单是否已付 + +| 账单状态 | 处理路径 | +|---|---| +| **未付**(Unpaid)| 简单:作废 Bill → 重算 | +| **已付**(Paid,业户付现金/微信)| 复杂:作废 Bill → 业户**退款** → 重新出账单 → 业户**重付** | +| **已付**(用 [[../prepaid/consume-monthly-property-bill|预存款抵扣]])| 复杂:作废 Bill → 预存款**反向充值** → 重新出账单 → 重新抵扣 | + +### 第 3 步:走"作废 Bill"流程 + +当前**没专用 UI** —— 联系运维 / 高权限人员: + +```php +// tinker 操作示意(运维) +DB::transaction(function () use ($readingId) { + $reading = MeterReading::find($readingId); + $oldBill = Bill::find($reading->bill_id); + + // 1. 作废 Bill + $oldBill->update([ + 'status' => BillStatus::Voided, + 'voided_at' => now(), + 'voided_reason' => '抄表录错,需重算', + ]); + + // 2. 解锁 reading(把 bill_id 设 null,让它可被处理) + $reading->update(['bill_id' => null]); + // 或者:标 reading 作废,新建一条修正 reading + $reading->update(['voided_at' => now(), 'voided_reason' => '录错']); +}); +``` + +### 第 4 步:建修正 reading + +如果走"建新 reading"路径: + +```php +$correctReading = MeterReading::create([ + 'meter_id' => $oldReading->meter_id, + 'read_at' => $oldReading->read_at, // 同抄表日期 + 'current_reading' => 1500, // 改成正确值 + 'source' => MeterReadingSource::Manual, + 'operated_by' => $currentAdmin->id, + 'memo' => "修正:原 reading #{$oldReading->id} 数值录错(2500 → 1500)", +]); +``` + +### 第 5 步:重生成 Bill + +走 [[bill-generation-pipeline]]:对新 reading 调 `GenerateBillsFromMeterReadingsAction`。 + +### 第 6 步:处理已付款的差额 + +如果旧账单已被业户付了: + +- **业户付现金**:物业现场退差额(¥1,200 - ¥600 = ¥600) +- **微信付**:物业微信退款 +- **预存款抵扣**:走"反向 consume"(技术上是 `PrepaidAccount::deposit` 把钱充回 → 然后从新账单扣) + +### 第 7 步:通知业户 + +完整说明: + +- 旧账单 ¥1,200 已作废(原因:抄表录错) +- 新账单 ¥600 +- 差额已退还(或预存款已回填) + +## 未来目标态(待开发) + +类似 AdHocEvent 的 `VoidAction`(级联废 + 留 voided 审计): + +```mermaid +sequenceDiagram + participant 业务 + participant Filament + participant VoidBillAction[待开发] + participant 数据库 + + 业务->>Filament: 找到要作废的 Bill → VoidBillAction + Filament->>VoidBillAction: handle(bill, reason) + VoidBillAction->>数据库: Bill.status=Voided + voided_reason + voided_at + VoidBillAction->>数据库: Reading.bill_id=null(解锁) + VoidBillAction->>数据库: 若已付:建红字 Receipt + 退款 / 预存款回填 + Filament-->>业务: 完成 + + 业务->>Filament: 建修正 MeterReading(同 read_at) + Filament->>数据库: 新建 reading + Filament->>VoidBillAction[GenerateBills]: handle(new reading) + 数据库->>数据库: 建新 Bill(amount=正确值) +``` + +## 系统视角:双锁的设计意义 + +为什么 Reading 创建后不可改(第 1 锁)+ 有 Bill 更不可改(第 2 锁)? + +| 反例(若允许改)| 后果 | +|---|---| +| 业务人员直接改 Reading 数据 | 历史 Bill 仍是旧金额,Reading 是新数据 → 不一致 | +| 改 Reading 同时改 Bill | 已付的钱怎么办?业户付了 1200 你改成 600 → 600 凭空消失 | +| 业户已付 + 物业改 Reading 改小金额 | 物业账面收入虚高(实际收 1200,账面记 600,差额 600 不知去向)| + +**双锁强制**业务人员走"作废 + 重生成"流程,**留下完整审计痕迹**(旧 Bill voided + 新 Bill 创建 + 退款 / 预存款回填记录)。 + +## 业户视角 + +业户**不直接接触系统层**,只感受: + +- 提出异议 +- 物业核实 +- 收到说明:"经核实,5 月账单 ¥1,200 是抄表录错,实际应付 ¥600。已作废原账单,新账单已发,差额 ¥600 已退到您微信" +- 收到**红字凭证**(若已付,作废 + 退款) +- 收到**新账单** + +整个流程业户感知:"物业认错 + 退钱 + 重发账单",是**正面体验**(物业承认错误并改正)。 + +## 流水台账(完整修正过程) + +| 时间 | 动作 | Reading | Bill | +|---|---|---|---| +| 5/26 | 抄表录错 | #1(current=2500, bill_id=Bill#A) | Bill#A(amount=1200, Unpaid) | +| 5/27 | 业户质疑 + 核实 | (不变) | (不变) | +| 5/30 | 作废 Bill | #1.voided=true, bill_id=null | Bill#A.status=Voided, voided_reason="抄表录错" | +| 5/30 | 建修正 reading | #2(current=1500, bill_id=null) | (无) | +| 5/30 | 重生成 Bill | #2.bill_id=Bill#B | Bill#B(amount=600, Unpaid) | +| 5/31 | 业户付款 | (不变) | Bill#B.status=Paid | + +整条修正记录可审计。 + +## 常见问题 + +> [!question] 如果原账单未付,作废就行,不用退款? +> 是的。未付 → 作废 → 不开账单 → 业户没付任何钱。重生成新账单业户直接付新账单即可。 + +> [!question] 业户已经付了,作废 Bill 之后如何退款? +> 看付款方式: +> - **现金 / 微信 / POS**:物业按渠道退 +> - **预存款抵扣**:`PrepaidAccount::deposit` 反向充值(技术上是建一笔 deposit 流水把钱"还"回预存款余额)→ 然后业户继续用预存款付新账单 +> +> 当前**没自动化**,需运维 / 业务流程操作。 + +> [!question] 已经走过红字流程的 deposit/prepaid 模块,meter 为什么没有? +> meter 模块**直接产 Bill 不直接产 Receipt**,与 deposit/prepaid 直接产 Receipt 的模式不同。"红字 Bill" 的设计需要 Bill 模型支持(增加 `voided_at` / `voided_reason` 字段 + 业务流程),issue.md Q5 标记为"待补"。 + +> [!question] 抄表员经常录错怎么办? +> 系统层面: +> - 强制拍照([[reading-source-and-photo-proof]] + [[read-with-photo-proof]]) +> - Form 上显示 previous 读数 + 异常告警(本月差与上月差比超过 X 倍 → 提示) +> +> 业务层面: +> - 抄表员培训 +> - 关键抄表数据二次审核(双签) +> - 升级集抄([[read-via-iot-remote-source]])减少人工录入 + +> [!question] "修正 reading" 是建新 reading 还是改旧? +> 看实现: +> - **改旧**(直接 update)→ 简单但丢失历史(原数据没了) +> - **建新 + 标旧 voided**(推荐)→ 复杂但完整保留审计 +> +> 当前 issue.md 倾向"建新 + 标旧 voided"模式(类似 AdHocEvent VoidAction)。需要给 Reading 表加 `voided_at` / `voided_reason` 字段。 + +> [!question] 长期不修复这个 gap 有什么风险? +> - 业务人员**每次修正都要联系运维**(慢、不可扩展) +> - 修正没有标准化流程 → 容易出错(漏退款 / 漏作废) +> - 审计困难(运维 tinker 操作的痕迹靠日志,不像 UI 操作那么清晰) +> - 业户体验差(响应慢) +> +> 业务方提需求时优先级会上来。 + +## 相关 issue.md 待补 + +``` +- "作废已生成 Bill 的 MeterReading"组合流程: + 类似 AdHocEvent 的 VoidAction(级联废 + 留 voided 审计),需要单独设计。 + 等业务方明确"已收款的 Bill 应该怎么撤销"再做(可能涉及红字 Bill / 退款) +``` + +## 异常分支 + +- 抄表员录错预防 → [[read-with-photo-proof|拍照存证]] + Form 守护 +- 高用量触发预警 → [[exception-high-consumption]] +- 表故障导致错读 → [[replace-broken-meter|换表]] + 走本场景修正 + +## 相关文档 + +- [[decommission-and-locking]] +- [[bill-generation-pipeline]] +- [[exception-high-consumption]] +- [[replace-broken-meter]] +- [[../adhoc/cancel-amount-error-redo]](adhoc 模块的类似 void 流程参考) diff --git a/prop-acc/scenarios/meter/generate-bill-min-max-cap.md b/prop-acc/scenarios/meter/generate-bill-min-max-cap.md new file mode 100644 index 0000000..650edad --- /dev/null +++ b/prop-acc/scenarios/meter/generate-bill-min-max-cap.md @@ -0,0 +1,273 @@ +--- +title: prop-acc · meter · 场景 - 单笔账单上下限封顶(防异常用量爆账) +aliases: + - min max 封顶 + - 账单封顶 + - generate-bill-min-max-cap + - 场景-账单上下限封顶 +tags: + - 场景 + - prop-acc + - 计量表 + - 账单生成 + - 异常防御 +audience: + - 业务人员 + - 财务 + - 业户 +status: 已发布 +sub_feature: meter +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:单笔账单上下限封顶(防异常用量爆账) + +`RatePlan` 上的 `min_amount` / `max_amount` 字段为单笔账单设置**上下限**: + +- **`max_amount`** 防止极端用量(漏水 / 设备故障)导致离谱账单 → 业户友好,但物业承担差额 +- **`min_amount`** 防止零用量 / 极低用量逃避基础服务费 → 物业兜底,但业户可能不爽 + +本场景演示三种触发情境。 + +## 典型情境 + +### 情境 1:漏水触发 max 封顶 + +> [!example] 真实情境 +> 张阿姨家**水管漏水**(藏在墙里没发现),5 月用水 **800 吨**(平时 12 吨)。按阶梯计价: +> +> ``` +> 0-20 吨段:20 × 3.0 = 60 +> 21-30 吨段:10 × 4.5 = 45 +> 31-800 吨段:770 × 6.0 = 4,620 +> 算出金额:4,725 元 +> ``` +> +> RatePlan 配置 `max_amount = 1,500`,触发封顶: +> +> ``` +> final_amount = min(4725, 1500) = 1500 +> ``` +> +> 账单 ¥1,500,差额 ¥3,225 物业承担(走维修保险 / 业务减免)。 + +### 情境 2:零用量触发 min 兜底 + +> [!example] 真实情境 +> 王先生整月**出差不在家**,水表读数无变化(consumption=0)。按阶梯算 = 0 元。但物业配置 `min_amount = 20`,兜底: +> +> ``` +> final_amount = max(0, 20) = 20 +> ``` +> +> 账单 ¥20,理由"基础服务费 / 管网维护费"。王先生抱怨"我没用水为什么收钱?",物业解释:小区水管 / 表的维护是公共成本,按户分摊。 + +### 情境 3:正常范围,无封顶 + +> [!example] 真实情境 +> 陈先生 5 月用水 35 吨,按阶梯算 135 元。RatePlan 配置 `min_amount=20`, `max_amount=1500`: +> +> ``` +> final_amount = max(20, min(135, 1500)) = 135 +> ``` +> +> 账单 ¥135,封顶规则**不触发**(在合理范围内)。 + +## min / max 算法 + +``` +if (max_amount !== null && calculated > max_amount) calculated = max_amount; +if (min_amount !== null && calculated < min_amount) calculated = min_amount; +final_amount = calculated; +``` + +或等价:`final_amount = max(min_amount ?? 0, min(calculated, max_amount ?? INF))`。 + +详见 [[multiplier-and-tiered-pricing|倍率与阶梯计价]]"第 3 层 min/max 封顶"段。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant Calc[MeterBillCalculator] + participant Service[MeterBillGenerationService] + participant DB + + Calc->>Calc: 阶梯算法 → calculated_amount + Note over Calc: 假设漏水算出 4725 + + Calc->>Calc: max_amount 检查 + alt calculated > max_amount(4725 > 1500) + Calc->>Calc: final = max_amount = 1500 + else 在范围内 + Calc->>Calc: 通过 + end + + Calc->>Calc: min_amount 检查 + alt final < min_amount + Calc->>Calc: final = min_amount + else 在范围内 + Calc->>Calc: 通过 + end + + Calc-->>Service: 1500 + Service->>DB: 建 Bill(amount=1500, sourceable=reading) + Note over DB: 业务上是否记录"封顶减免"?目前 Bill 表无此字段,留作业务备注 +``` + +## 业户视角 + +### Max 封顶(漏水) + +业户收到的账单: + +``` +2026 年 5 月水费账单 + +用水量:800 吨 ⚠️ 用量异常高 +按阶梯算应付:¥4,725.00 +封顶后实付:¥1,500.00 +差额减免:¥3,225.00(由物业 / 维修保险承担) + +应付:¥1,500.00 +``` + +> [!info] 账单展示封顶信息 +> **强烈推荐**账单展示"按阶梯算 X,封顶 Y,差额 Z 由物业承担"。让业户明白封顶的存在 + 物业的友好。 + +业户会**感激物业封顶**,但更会**自查漏水 / 设备故障**。 + +### Min 兜底(零用量) + +业户收到的账单: + +``` +2026 年 5 月水费账单 + +用水量:0 吨 +按阶梯算应付:¥0.00 +基础费(min):¥20.00 + +应付:¥20.00 +``` + +业户可能**不接受**:"我没用水为什么收钱?"。物业要解释: + +- 水管 / 公共部位维护成本 +- 物业服务费的"基础保障"性质(签合同时已告知) +- 法律 / 政策依据(若有) + +## 业务人员视角 + +### 配置 min / max + +后台 → 费率管理 → RatePlan → 编辑 → 填字段: + +| 字段 | 推荐值(参考)| +|---|---| +| `min_amount` | 10-30 元(看物业 + 费用类型)| +| `max_amount` | 1000-5000 元(看业户类型,商铺可以更高)| + +> [!warning] 配置要谨慎 +> +> **max 配低**:正常用量也被封顶 → 物业损失收入。例如 max=300,商铺正常月费 ¥500 → 物业每月被减免 ¥200。**严重 bug**。 +> +> **max 配高**(或不配):无封顶 → 极端用量爆账户,业户投诉 + 法律风险。 +> +> **min 配高**:业户不满 + 投诉。 +> +> **配置后用极端值算例验证**(0 度 / 极少 / 正常 / 极高 各算一遍看是否合理)。 + +### 触发封顶后的业务流程 + +| 触发 | 业务人员动作 | +|---|---| +| max 触发(异常高用量)| 联系业户排查([[exception-high-consumption]]) → 减免数额可能要审批 | +| min 触发(零 / 极低用量)| 通常无需介入,业户接受 min 即可 | +| 频繁 max 触发 | 评估是否表 / 设备有问题(漏水 / 故障)| + +### 封顶减免的会计处理 + +封顶差额(`max_amount` 触发时,实际应付 vs 物业承担)的会计处理: + +| 选项 | 实现 | +|---|---| +| **物业直接承担**(本系统当前简化)| Bill.amount 直接是封顶后金额。账面收入 ¥1,500(实际应是 ¥4,725)→ 物业少收 ¥3,225 | +| **走维修保险**(高大上)| 物业向保险公司报销 ¥3,225,账面通过应收 / 已收 走完整流程 | +| **业户与物业分摊**(罕见)| 部分协议:超过封顶部分 50% 业户 50% 物业 | + +当前**最简单实现**:物业直接承担。其他方案需要业务方提需求。 + +## 财务视角 + +### 月度报表统计封顶情况 + +```sql +-- 本月触发 max 封顶的 reading(假设 Bill 不存"封顶前 amount",我们用 reading 算) +SELECT + r.id AS reading_id, + r.consumption, + -- 重算应付(简化,实际要走 Calculator 逻辑) + -- calculate(consumption, ratePlan) AS expected_amount + b.amount AS billed_amount, + -- (expected - billed) AS reduction + rp.max_amount +FROM acc_meter_readings r +JOIN acc_meters m ON r.meter_id = m.id +JOIN fee_types ft ON m.fee_type_id = ft.id +JOIN rate_plans rp ON ft.current_rate_plan_id = rp.id +JOIN acc_bills b ON r.bill_id = b.id +WHERE b.amount = rp.max_amount -- 简化判断:Bill 金额刚好等于封顶值 = 大概率封顶触发了 + AND b.created_at BETWEEN '2026-05-01' AND '2026-05-31'; +``` + +业务用途:看月度物业因封顶减免多少收入。若太多 → 调整 max 或排查根因(频繁漏水 / 设备故障)。 + +## 常见问题 + +> [!question] min_amount 是 0 / null 时不兜底? +> 是的。若 `min_amount=null`,系统不兜底,零用量账单 = 0 元(可能不开账单)。物业政策决定是否兜底。 + +> [!question] max 触发后业户反悔说"我自查没漏水,你怎么算出我用 800 吨的?" +> 业务人员排查: +> - 看 reading 数据(读数对吗?抄表照片有吗?) +> - 派人现场核对物理表 +> - 找漏水点(物业派维修人员) +> - 若证实表故障 → 走 [[replace-broken-meter|换表]],并重算账单(走 [[exception-readings-locked-after-bill|修正流程]]) +> +> 若所有证据都指向"确实用了 800 吨"(且业户家有漏水迹象)→ 业户认账,封顶后金额是优惠了。 + +> [!question] min 触发后业户拒付怎么办? +> 物业说服 + 法律协议层面要求(物业合同里通常有"基础服务费"条款)。坚决拒付 → 进入逾期催收。 + +> [!question] 不同业户(住宅 vs 商铺)封顶不同可以吗? +> 看 RatePlan 设计。当前可能"每个 FeeType 一份 RatePlan",所以同 FeeType 共用 min/max。如果要区分,可: +> +> - 给商铺单独建 FeeType + RatePlan +> - 或扩展 RatePlan 支持多档 min/max(改 schema) + +> [!question] 跨月用量(忘了抄一个月,两个月用量算一笔)会触发 max 吗? +> 可能会(双月用量翻倍)。**预防**:不要漏抄(走 [[audit-meters-needing-reading|审计]] 监控)。漏抄了发现: +> - 把这笔大账单**人工拆**成两个月(系统不直接支持,业务流程做) +> - 或当作正常账单收 + 与业户沟通 + +> [!question] 封顶后差额怎么入账? +> 当前最简单实现:Bill.amount 直接是封顶后金额,差额不入账(物业默默承担)。 +> +> 严格会计:差额应记入"管理费用 / 维修保险报销 / 服务减免"科目。需扩展 schema 才能精确处理。 + +## 异常分支 + +- 阶梯计价(本场景叠加)→ [[generate-bill-tiered-pricing]] +- 工业表倍率叠加 → [[generate-bill-with-multiplier]] +- 异常高用量(可能触发 max)→ [[exception-high-consumption]] +- 读数错误导致离谱算账 → [[exception-readings-locked-after-bill]] 修正 + +## 相关文档 + +- [[multiplier-and-tiered-pricing]] +- [[bill-generation-pipeline]] +- [[generate-bill-tiered-pricing]] +- [[generate-bill-with-multiplier]] +- [[exception-high-consumption]]