简介:基于JAVA构建的智能电表远程抄表缴费管理平台源码,面向物业、房东、写字楼等用电管理场景,解决人工抄表效率低、缴费不便等问题,可兼容正泰、人民、天正、许继等主流电表品牌。代码涵盖远程抄表、数据处理与分析、线上缴费、用户管理、报警通知、报表生成及开放API等完整模块,其中wwby-worker-ammeter组件负责定时采集与任务处理,便于开发者理解物联网通信与业务逻辑。资源包共86个文件,以77个Java源码文件为主,附8个XML配置文件和1个IDEA模块文件,整体仅67KB,目录包含src/main等标准结构,体量精简、便于快速浏览核心代码。已有2181人学习下载;对物联网开发者而言,可以借助这套代码掌握远程通信(GPRS/LoRa/NB-IoT)集成思路、支付接口对接方式以及二次开发扩展方法,具有较高参考价值。
1. 智能电表远程抄表缴费管理平台:从“挨户抄表”到“自动读取”的Java完整实现
做物业管理的朋友应该懂这种痛:每个月 1 号到 5 号,要安排两个人挨栋楼去抄电表,一层一层走,一个表一个表记。碰到租户不在家、表箱锁了,还得回头补抄。回来以后把几千条数据手敲进 Excel,再算阶梯电费、做账单、催缴费,一整套流程下来,一个 800 户的园区要折腾整整一周。这套智能电表远程抄表缴费管理平台,解决的就是这件事。它用 Java 实现了从电表数据采集、远程抄读、自动算费、账单生成到在线缴费的完整闭环——集中器通过 RS485 采集电表数据,平台通过 Netty 通信服务定时抄读,按区域、按电价方案自动生成电费账单,住户在小程序或公众号里就能缴费。对二次开发者来说,它不只是一堆源码,而是一个把「通信协议解析 + 定时任务调度 + 计费引擎 + 支付回调」全部串起来的标准 IoT 业务系统,拿来改一改,就能套到水表、气表、园区能耗监控等场景。这也是我拆完这份源码后最直观的感受:它不炫技,但每一层都在解决真实项目里的实际问题。
2. 系统架构与工程结构:从电表脉冲到 MySQL 落库的完整数据链路
2.1 三层物理架构:主站、采集终端、智能电表之间怎么通信
整套系统的物理模型分三层。最底层是智能电表,分布在各个住户和商铺里,支持 DL/T645-2007 和 Modbus 协议;中间层是采集终端(集中器),一个集中器下挂最多 256 块电表,通过 RS485 总线轮询读取电表数据;最上层是平台主站,也就是这份 Java 源码构成的服务器系统。
主站和集中器之间的通信方式是这套系统选型的核心。常见做法是主站主动发起 TCP 连接,用类 FTP 的 05 规约或者直接走 HTTP 轮询,让集中器上报数据。而这份源码采用的是 Netty 服务端长连接方式:集中器主动连接到主站的 9100 端口,连接保持,主站下发抄表指令,集中器返回报文。这样做的好处是实时性好,告警数据(比如电表异常、停电事件)能秒级上报,不用等轮询周期。
数据流整体是这样:电表 → RS485 → 集中器 → TCP/Netty → 协议解析服务 → MySQL → 计费任务 → 账单 → 支付接口。每一层解耦得比较干净,通信层只管收发报文,业务层只管处理解析后的结构化数据。这一点在做二次开发时非常重要——你想换一种电表协议,只需要替换报文解析那一层,上层业务代码完全不用动。
2.2 后端工程目录:一个 Maven 多模块项目的标准切法
打开源码包,后端是标准的 Maven 多模块结构,分成了六个子模块。我整理了一下,每个模块的职责从包名上就能看出来:
- smart-meter-common:公共模块,放统一返回结果、异常处理、工具类、常量定义
- smart-meter-netty:通信模块,Netty 服务端、报文编解码、协议解析、会话管理
- smart-meter-collect:采集调度模块,抄表任务、定时策略、数据补抄
- smart-meter-business:核心业务模块,用户管理、电价方案、计费引擎、账单生成、缴费订单
- smart-meter-admin:后台管理接口,给运营人员用的 Web 管理端对接 API
- smart-meter-job:定时任务模块,基于 XXL-Job 的分布式任务调度
这种切法最直接的好处是依赖关系单向清晰:business 依赖 collect,collect 依赖 netty,common 被所有人依赖,不会出现循环依赖。做二次开发时,如果你想替换通信层,直接重写 netty 模块的接口实现即可,业务模块不受牵连。
2.3 数据模型设计:抄表记录、电费账单、缴费订单三张核心表
这套系统的数据库设计比较务实,核心就围着三张表转。第一张是 meter_read_record,也就是抄表记录表,每一条对应一块表的一次抄读结果,字段包括电表编号、止码、上次止码、倍率、抄读时间、数据来源(自动/人工/补抄)。第二张是 electricity_bill,电费账单表,每次计费任务跑完以后生成,记录账单周期、用户编号、表编号、电量、电价方案、电费金额、账单状态。第三张是 payment_order,缴费订单表,记录用户缴费的每一笔流水,包含订单号、支付渠道、支付金额、回调状态。
三张表通过 user_id 和 meter_no 串联起来,形成一条完整的关系链。这里有一个设计细节值得注意:抄表记录表里存的是「止码」而不是「电量」,电量是在计费时用本次止码减去上次止码算出来的。这个设计是因为电表可能补抄、追补,如果直接存电量,一旦上次抄表数据修正,后续全部要重算,而存止码则可以从源头重算,逻辑更安全,这也是做过计量系统的人才会注意到的点。
2.4 技术选型:为什么是 Spring Boot + Netty + XXL-Job,而不是 Spring Cloud 全家桶
技术栈选得很务实,没有为了追新而上微服务那一套。后端基础框架是 Spring Boot 2.x + MyBatis-Plus,数据库 MySQL,缓存 Redis,权限认证用的 Sa-Token,定时任务用 XXL-Job,通信层是 Netty,前端管理端是 Vue 2 + Element UI。对于单园区几百台集中器、几万块电表的规模,这套组合完全够用,而且部署运维成本低很多。
有几个选型点我觉得挺有参考价值。通信层没有用 Spring 自带的 WebSocket 或者单纯的 HTTP 轮询,而是专门用 Netty 起了一个独立的 TCP 长连接服务,这是为了应对集中器数量多、连接保持时间长的场景,Netty 在这方面的性能和稳定性都经过大规模验证。定时任务没有用 Spring 自带的 @Scheduled,而是接了 XXL-Job,因为抄表任务往往需要分布式部署,多台机器同时跑任务时要避免重复抄读,XXL-Job 的分片广播和任务锁能直接解决这个问题。总的来说,这套选型属于那种「在真实生产里摸过一遍坑之后沉淀下来的组合」,朴实但扛得住。
3. 核心代码拆解:一次远程抄表从指令下发到电费账单生成的完整链路
3.1 通信层实现:用 Netty 手写一个 DL/T645-2007 报文解析器
通信层是整个系统里技术含量最高的一块。集中器主动连上来以后,主站要能解析它的登录报文、心跳报文和主动上报的告警报文,同时要能下发抄表指令。DL/T645-2007 是我国多功能电能表通信协议标准,报文格式是固定的:起始符 68H + 地址域(6 字节表号)+ 控制码 + 数据长度 + 数据域 + 校验和 + 结束符 16H。
Netty 这边最核心的是报文解码器的实现。我用伪代码还原一下核心逻辑:
public class Dl645Decoder extends ByteToMessageDecoder { // 最小报文长度:起始符1 + 地址域6 + 控制码1 + 长度1 + 校验1 + 结束符1 = 11字节 private static final int MIN_FRAME_LENGTH = 11; @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // 等待积累到足够长度再解析 if (in.readableBytes() < MIN_FRAME_LENGTH) { return; } // 标记当前读取位置,解析失败时回退 in.markReaderIndex(); if (in.readByte() != 0x68) { // 不是报文起始符,丢弃一个字节继续找 in.resetReaderIndex(); in.skipBytes(1); return; } // 读表号地址域,6字节 byte[] address = new byte[6]; in.readBytes(address); byte controlCode = in.readByte(); // 数据域长度,DL/T645中长度包括控制码和数据域 int dataLength = in.readUnsignedByte(); // 校验数据域长度是否合法 if (dataLength < 2 || dataLength > 255) { in.resetReaderIndex(); in.skipBytes(1); return; } // 读取数据域和校验码 byte[] data = new byte[dataLength]; in.readBytes(data); byte checkCode = in.readByte(); if (in.readByte() != 0x16) { in.resetReaderIndex(); in.skipBytes(1); return; } // 封装成协议对象交给上层处理器 Dl645Frame frame = new Dl645Frame(address, controlCode, data); out.add(frame); } }这块代码的逻辑是典型的「读一个字节验证一个字节」的流式解析。先等缓冲区凑够最小长度,再检查起始符,不匹配就跳过一字节继续找,避免因为粘包导致错位。地址域读出来后,根据长度字段确认整个帧的边界,最后校验结束符。这样粘包和半包的问题都能处理掉:半包时 readableBytes 不够,直接 return 等下一次 channelRead;粘包时解析完一个完整的帧丢出去,剩余字节留在缓冲区里继续解析。
解析出来的 Dl645Frame 对象,会接着被送到业务层的报文处理器。这里我一般会注意一个参数:集中器下挂电表多的时候,一次上报可能带几十块表的数据,数据域长度可能超过 200 字节,解码器里的 dataLength 上限要结合集中器型号去调,别用默认值硬扛大数据帧,容易出现 CRC 校验对不上导致报文被丢弃的玄学问题。
3.2 采集调度:定时抄读任务如何避免重复执行
采集调度模块解决的问题是怎么在正确的时间抄到正确的表。平台支持两种抄读模式:定时自动抄读和手工补抄。定时任务跑在 XXL-Job 上,配置一个任务处理器,按配置的 cron 表达式周期性触发。
@Component public class MeterReadJob { // 抄表任务执行入口,由XXL-Job调度触发 public ReturnT<String> execute(String params) { // 1. 查询所有需要参与本次抄表的集中器和电表 List<CollectorDevice> collectors = collectorMapper.selectOnlineList(); // 2. 按集中器分组,每个集中器单独下发一轮抄读指令 for (CollectorDevice collector : collectors) { // 3. 先对集中器下发广播校时,保证时间基准一致 nettyClient.sendTimeSync(collector.getChannelId()); // 4. 遍历集中器下挂的电表,逐块下发抄读指令 for (MeterDevice meter : collector.getMeterList()) { SendResult result = nettyClient.sendReadCommand( collector.getChannelId(), meter.getMeterNo(), ReadCommand.ACTIVE_ENERGY); // 5. 异步等待电表响应,5秒超时 MeterReadRecord record = result.getFuture() .get(5, TimeUnit.SECONDS); // 6. 记录抄读结果,失败则写入补抄队列 saveReadRecord(record); } } // 返回执行结果给调度中心 return ReturnT.SUCCESS; } }这段逻辑有几处是生产环境必须考虑的。首先,串行逐表抄读对 256 块表的大集中器来说太慢,实际改造时我会把每块表的抄读指令改成异步发送,用 Future 等待结果,或者干脆每块表丢到一个独立线程池里并发抄,抄读时间能从 5 分钟压缩到 20 秒。其次,sendTimeSync 广播校时不能省,电表内部时钟跑偏会导致费率时段切换不准确,阶梯电价算错就麻烦。最后,超时和失败的电表一定要写进补抄队列,否则月底算费时漏一块表,整个园区的账单都对不上,这是血泪经验。
任务调度上,XXL-Job 这里有一个天然优势。如果平台拆成多节点部署,普通 @Scheduled 会出现在每台机器上同时执行抄表的问题,而 XXL-Job 的任务路由策略可以指定「第一个机器执行」或者「分片广播」,从框架层面避免重复抄读。这个点上我踩过坑:之前自己用 Redis 分布式锁控制 @Scheduled 任务,锁过期时间设短了导致任务重叠,设长了又担心服务宕机锁不释放,换成 XXL-Job 后这些全不用管了。
3.3 计费引擎:阶梯电价、峰谷电价怎么算才不出错
抄表数据到位以后,真正的重头戏是算费。电费计算不是简单地用电量乘以单价,而是要处理阶梯电价、峰谷分时电价、力率调整、基本电费等多种方案。这套平台的计费引擎被设计成可配置的,价格方案存数据库,算费时先用规则引擎匹配方案,再逐项计算。
public class BillingEngine { // 计算单个用户的电费账单 public Bill calculate(BillingContext context) { // 1. 取阶梯方案和用户当前累计用电量 TariffPlan plan = context.getTariffPlan(); // 2. 计算本次计费周期的用电量 BigDecimal currentReading = context.getCurrentReading(); BigDecimal previousReading = context.getPreviousReading(); BigDecimal energy = currentReading.subtract(previousReading); // 3. 根据累计用电量判断当前所处的阶梯档位 List<TariffStep> steps = plan.getSteps(); Bill bill = new Bill(); bill.setEnergy(energy); // 4. 遍历阶梯,拆分区间计算电费 BigDecimal remaining = energy; BigDecimal totalAmount = BigDecimal.ZERO; for (TariffStep step : steps) { // 当前档位的上限区间 BigDecimal stepLimit = step.getUpperLimit(); BigDecimal stepStart = step.getLowerLimit(); // 累计用电量落在本档位内的部分 if (context.getCumulativeEnergy().compareTo(stepLimit) >= 0) { continue; // 累计电量已超过本档上限,进入下一档 } BigDecimal usable = stepLimit.subtract(context.getCumulativeEnergy()); BigDecimal consume = remaining.min(usable); // 本档电价 × 用电量 totalAmount = totalAmount.add( consume.multiply(step.getPrice())); remaining = remaining.subtract(consume); // 更新累计用电量 context.setCumulativeEnergy( context.getCumulativeEnergy().add(consume)); if (remaining.compareTo(BigDecimal.ZERO) == 0) { break; } } bill.setTotalAmount(totalAmount); return bill; } }这段算法的关键是「先判档位区间,再拆用电量」。比如某地区阶梯电价分三档:年用电 3000 度以内每度 0.5 元,3001-5000 度每度 0.6 元,5000 度以上每度 0.8 元。用户在 8 月份算费时已经累计用了 2800 度,这个月用了 800 度,那其中 200 度按第一档 0.5 元算,600 度按第二档 0.6 元算。循环里每次取「本档剩余可用量」和「待算电量」的较小值,保证电量拆分准确。
这里有一个必须提醒的细节:金额和电量计算绝对不能使用 double,一定要用 BigDecimal,否则小数点后精度丢失,月底对账时会差出几分钱,积少成多就是大问题。另外,阶梯方案的上下限参数建议冗余存到账单表里,因为电价政策三年两头变,账单必须能追溯当时用的什么方案,只存一个方案 ID 的话,改方案后历史账单就说不清了。
3.4 缴费回调:支付结果的幂等处理是最后一道防线
算完费生成账单,用户在小程序里发起缴费,平台生成支付订单,跳转微信支付或者支付宝收银台。用户支付成功后,支付平台会向回调地址发一个异步通知。这个回调处理得好不好,直接决定账能不能对上。支付回调这块的代码看起来简单,但坑特别多:
@PostMapping("/api/payment/notify/{channel}") public String handleNotify(@PathVariable String channel, @RequestBody String notifyData) { // 1. 验签:用支付平台公钥验证通知签名 boolean valid = payService.verifySign(channel, notifyData); if (!valid) { return "fail"; } // 2. 解析通知内容,提取商户订单号 Map<String, String> data = payService.parseNotify(channel, notifyData); String orderNo = data.get("out_trade_no"); String tradeNo = data.get("trade_no"); String totalAmount = data.get("total_amount"); // 3. 查询本地订单,核对订单状态和金额 PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo); if (order == null) { return "fail"; } // 4. 状态机校验:只有待支付状态才能更新为已支付 if (!PaymentStatus.WAIT_PAY.equals(order.getStatus())) { // 重复通知或已处理过,直接返回成功 return "success"; } // 5. 金额比对:防止黑客伪造回调 if (order.getAmount().compareTo(new BigDecimal(totalAmount)) != 0) { return "fail"; } // 6. 更新订单状态,财务入账,通知业务系统 paymentOrderMapper.updateStatus(orderNo, PaymentStatus.PAID); billService.markPaid(order.getBillId()); return "success"; }回调处理的核心就一句话:收到通知先验签,再查订单,再看状态,最后改状态。幂等靠的是第四步的状态机校验——只有 WAIT_PAY 状态才能流转到 PAID,重复回调来了发现状态已不是 WAIT_PAY,直接返回 success,不重复入账。
这里我要重点说一个之前翻车的细节:支付平台的异步通知可能会在几分钟内重复发送多次,而且顺序没有保证。如果代码里没有状态机校验,同一笔订单被回调两次,就会给用户记两笔缴费,财务对账直接炸。另外,金额比对一定要做,因为回调接口拿到的 total_amount 是支付平台的实付金额,有些平台支持用户用优惠券导致实付比订单金额少,这时要么直接拒单,要么按实付金额生成不同的处理逻辑,千万别直接拿通知金额覆盖订单金额。
4. 跑通与避坑记录:从源码下载到自动抄表上线
4.1 环境准备:JDK 1.8、MySQL 5.7、Redis、XXL-Job 一个都不能少
把源码在本地跑起来,需要准备的东西很明确。JDK 必须 1.8 版本,用 11 或 17 跑 Spring Boot 2.x 会出现一些依赖兼容问题。MySQL 用 5.7 或 8.0 都行,建库时注意选择 utf8mb4 字符集,否则账单备注里的特殊字符存不进去。Redis 用来存集中器的会话状态和验证码,版本没有硬性要求。XXL-Job 需要单独下载调度中心并启动,同时要在 admin 后台配置一个执行器,名字要和源码里 application.yml 配置的 appname 一致。
依赖服务准备完以后,先别急着启动项目,把配置文件捋一遍。数据库连接、Redis 地址、XXL-Job 地址这三处是必改的,我都写一个统一的说明。然后是 Netty 的监听端口,默认 9100,如果服务器上被占用就换一个。最后是支付回调地址,本地调试时没有公网 IP,需要借助内网穿透工具把回调地址映射到本机,这一步跳过的话缴费流程就跑不通。
4.2 数据库初始化:建库脚本和初始数据怎么处理
源码包里带了 sql 目录,里面有两类脚本。一类是 schema.sql,建表语句,直接执行就能把所有表建好。另一类是 data.sql,初始化数据,里面包含管理员账号、电价方案示例、区域和楼栋的基础数据。有几个表是运行前必须保证有数据的,比如电价方案表、集中器型号表、计费周期配置表——这些表的初始数据不能删,否则系统启动后连计费任务都创建不了。
执行数据脚本时要留意一个问题:data.sql 里的集中器和电表数据是模拟的,它对应的是源码里附带的模拟终端。你在启动 Netty 服务之前,建议先把模拟终端也启动起来,让模拟集中器主动连接主站。我这里用的方式是:先在数据库里查集中器列表,确认集中器编号和端口,再启动模拟程序时传入对应的集中器编号,这样主站能看到设备上线,后续手动抄表才有设备可选。排序规则和权限方面,管理员账号初始数据里的密码是经过 MD5 加密的,登录后记得先在后台改掉。
4.3 启动流程:正确的启动顺序能省一半排查时间
启动顺序有讲究,按依赖方向来才不会出幺蛾子。第一步启动 Redis;第二步启动 XXL-Job 调度中心;第三步启动 Netty 通信服务模块,这个模块单独有 main 方法,跑起来后监听 9100 端口;第四步启动核心业务模块;最后启动定时任务模块。如果顺序反了,业务模块在启动时会去连 Netty 服务,连不上就会报连接异常,搞不清楚的人会以为代码坏了,其实只是没启动对应的依赖服务。
全部启动以后,验证方式分三步走。第一步看日志,Netty 模块日志里出现「Server started on port 9100」说明通信服务正常。第二步看设备上线,模拟集中器连上来以后,后台管理界面设备管理页里能看到集中器状态为在线,这块电表状态为正常。第三步手动触发一次抄读,在下发抄读指令前,可以先检查电表是否有读数返回,返回正常说明整个链路已经打通。这些验证都过了,平台的基本运行就没有问题了。
4.4 避坑记录:五个高频翻车现场的排查与修复
现象一:模拟终端启动后,主站后台看不到设备上线。原因通常是集中器编号的大小写不一致——模拟终端配置里写的是小写编号,数据库存储的是大写,或者反过来,导致设备鉴权失败被拒。解决方式是统一集中器编号的大小写规则,建议全部用大写,数据库和配置文件都保持一致。
现象二:定时抄表任务到了整点没触发。原因多半是 XXL-Job 执行器的 appname 和源码配置文件里的不一致,调度中心找不到执行器,任务永远停在「等待调度」。解决方式是登录调度中心后台,查看执行器管理页面,确认执行器的 AppName 和源码里配置的 appname 完全一致,然后重启任务。
现象三:手动抄读电表,一直提示超时。原因大概率是模拟终端没有针对这块电表配置应答数据,或者是电表的通信地址和集中器下挂表号对不上。解决方式是检查抄读指令里的表号,和模拟终端中配置的电表地址逐位比对,特别要注意 DL/T645 协议中通信地址是反序传输的,所以在界面上填表号时,要确认前后是否颠倒。
现象四:账单金额和手工计算不一致。原因基本上是阶梯电价的计费周期没有配置对,或者最后一次抄表止码和上次止码之间出现了负数(电表清零换表),负用电量会导致倒扣,整个账单数据就乱了。解决方式是查看电表操作日志,确认是否发生过换表操作,如果有,需要先做换表流程:旧表止码锁定,新表底码初始化,再重新算费。
现象五:支付回调成功了,订单状态没变。原因一般是回调地址配置的域名没映射到本机,或者回调请求被主站的验证逻辑拦截了。解决方式是先看业务模块的访问日志,确认回调请求有没有到达,如果没有,检查内网穿透的域名配置;如果到了但返回 fail,按上面的逻辑一步步排查验签和金额比对。
5. 二次开发指南:把抄表平台改造成适合自己业务场景的三个方向
5.1 替换协议解析层:把 DL/T645 换成 Modbus 协议
如果你接的不是国网标准的智能电表,而是工业现场常见的 Modbus-RTU 电表,改造点集中在通信模块。DL/T645 的帧格式是 68H 开头、16H 结尾,而 Modbus-RTU 是地址码 + 功能码 + 数据 + CRC16 校验,格式完全不同。做法是保留 Netty 通信框架不变,新增一个 ModbusDecoder 解码器,替换原来的 Dl645Decoder。采集逻辑里原来发的是「读有功电量」的 DL/T645 指令,改成 Modbus 功能码 03H 读保持寄存器,寄存器地址对应电量、电压、电流等参数。业务层的抄表记录、计费、缴费完全不用动,因为上层拿到的是解析后的结构化数据,不感知底层协议差异。
这里面真正的工程点是电表地址的映射。DL/T645 用 6 字节表号,Modbus 用 1 字节从站地址,集中器下挂的表数量上限也从 256 变成 247。改造时建议在集中器表里加一个 slave_address 字段,专门存 Modbus 从站地址,抄表指令下发时用这个字段拼报文。我改造时遇到的坑是 CRC16 校验算法写错,Modbus 报文对不上,调试了两天才发现是 CRC 查表法的表生成不对,后来直接用网上验证过的 CRC16-Modbus 实现替换,一次通过。
5.2 数据可视化:给平台加一个能耗分析大屏
这套源码里的管理后台虽然功能完整,但界面偏传统,数据展示都是表格。二次开发时最出效果的方向,是加一个能耗分析大屏,把抄表数据可视化。具体做法是在 business 模块里新增能耗统计接口,按小时、按天、按楼栋聚合用电量,前端用 Vue + ECharts 画柱状图、折线图和热力图。
聚合查询的核心是 to_days 函数配合 group by:
SELECT DATE_FORMAT(read_time, '%Y-%m-%d') AS day, building_no, SUM(energy) AS total_energy FROM meter_read_record WHERE read_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY day, building_no ORDER BY day;这个查询的关键是让 MySQL 在 read_time 上走索引,如果数据量超过百万行,建议加一个 building_no + read_time 的联合索引,否则大屏加载会明显变慢。展示层我用 ECharts 做折线图对比不同楼栋的日用电量,用电异常偏高的楼栋自动标红,物业一眼就能看出哪栋楼用电异常。这一步做完,整个平台的观感提升非常大,直接可以用作项目演示的首页。
5.3 扩展多费率计费:从单一电价到峰谷分时计费
电价政策在地域间差异很大,有些地方实行峰谷分时电价,高峰时段电价上浮、低谷时段下浮。我建议在计费引擎里加一个费率时段配置表,字段包括时段名称、开始时间、结束时间、电价倍率。算费时先判断读数发生在哪个时段,再乘以对应倍率。
时段的判断逻辑放在 BillingEngine 里新增一行判断:根据电表的费率时段配置,取出抄表时间对应的时段倍率,然后叠加到阶梯电价上。具体实现时要注意跨天时段的问题,比如峰段从 8 点到 22 点,谷段从 22 点到次日 8 点,跨天的时间段要单独处理。实际改造时,我一般会用一个时段判断工具类,把开始时间和结束时间都转成分钟数,判断当前时间是否落在区间内,跨天的区间用「结束时间 < 开始时间则加 24 小时」的逻辑解决。
5.4 我的三个验证习惯
改完代码以后,我习惯按下面三个步骤快速验证。第一步是新加模块的单测,比如电价计算,我写一个测试类,手动构造抄表数据、阶梯方案和预期金额,跑一次对比结果。第二步是本地模拟全流程,从模拟终端抄数据,到计费任务生成账单,再到手动支付回调,全链路走一遍,熟悉整套源码的人会习惯用这种方式确认没有遗漏关联表。第三步是数据核对,拿上个月实际账单和这套平台生成的结果逐笔对比,任何一笔不一致都要查清楚原因再上线。从那以后我每次改动核心计费逻辑,都强制自己走完这三步,哪怕只是改了一个折扣参数。希望这些记录能帮你少走一些弯路。
本文还有配套的精品资源,点击获取