7.8 KiB
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 · 场景 - 阶梯水电价生成账单(progressive 累进) |
|
|
|
已发布 | meter | 2026-05-26 | 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,业户收到的账单准确。
系统流程
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() 算法(伪代码)
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:
-- 本月各阶梯段用量分布(简化版,真实算法更复杂)
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
- 异常用量触发 min/max → generate-bill-min-max-cap
- 用量异常高(漏水)→ exception-high-consumption