Files
uniprop-manual/prop-acc/scenarios/meter/generate-bill-tiered-pricing.md
2026-05-26 00:23:07 +08:00

7.8 KiB
Raw Permalink 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 · 场景 - 阶梯水电价生成账单(progressive 累进)
阶梯计价账单
tiered pricing 算例
generate-bill-tiered-pricing
场景-阶梯计价生成账单
场景
prop-acc
计量表
账单生成
业务人员
财务
业户
已发布 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] 不同物业 / 不同社区可以有不同阶梯吗? 可以。RatePlancommunity_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

异常分支

相关文档