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