
简介智能电表远程抄表缴费管理平台是一套基于Java构建的物联网应用源码面向物业管理者、房东及物联网开发人员用于替代人工抄表、实现远程数据采集与在线缴费。平台兼容正泰、人民、天正、许继等主流品牌电表覆盖远程抄表、数据处理、支付接口、用户权限、报警通知及报表生成等核心模块。压缩包共86个文件以77个Java源码文件为主辅以8个XML配置和1个工程文件整体仅67KB代码结构凝练便于快速定位业务逻辑。已有2181人学习参考适合具备一定Java基础、希望掌握物联网设备接入与能耗管理实践的开发者。通过研读其中负责定时采集、任务调度等工作组件可弄清数据流转机制同时了解如何对接多厂商协议、开放API及嵌入缴费功能为二次开发或同类项目落地提供可复用的技术参照。1. 智能电表远程抄表缴费管理平台从采集到销账的完整闭环智能电表远程抄表缴费管理平台外行看到的是自动抄表省了人工内行看到的是一整套计费与资金闭环——采集读数、阶梯计费、生成账单、用户缴费、支付回调、销账对账每一步都牵扯状态一致性和幂等处理。这套 JAVA 源码不是玩具 demo而是把设备管理、抄表任务、计费引擎、缴费流水和能耗报表串在一起的工程骨架适合需要做课题设计的学生也适合要快速搭建能耗平台原型的从业者。重点在后端业务逻辑定时采集怎么不丢数据、账单怎么算不错、支付回调重复到达怎么只入账一次这些是平台最值钱的部分。2. 系统架构与模块拆解Spring Boot 单体如何撑起抄表业务很多人在打开源码之前第一反应是现在不都流行微服务吗但电表抄表平台的业务特点是设备接入量大、链路固定、状态一致性要求高单体架构反而是更稳妥的选择。真正难的不是服务拆分而是采集任务的时序控制和资金链路的数据一致性。这套源码用经典 Spring Boot MyBatis 单体结构职责边界清晰适合直接拿来改业务也适合初学者快速看懂完整项目。2.1 技术选型理由为什么单体架构在这个场景更合适我拆过的几个能耗类项目里用微服务的反而把自己坑了。电表数据采集是典型的定时批处理场景凌晨统一拉取、白天零星补采服务拆开之后分布式事务成本远大于收益。Spring Boot 单体加上数据库事务足以应付几千块表的并发写。技术栈选了 Java 8 以上的 Spring Boot 2.x 体系持久层用 MyBatis权限部分用 Shiro 或同类框架做认证授权。这套组合在中小型后台管理系统里非常成熟社区资料多遇到问题搜索成本低。如果换成 Spring Cloud 那套光服务注册、配置中心、链路追踪就够折腾半个月对抄表平台来说是过度设计。2.2 核心模块清单与职责边界源码里的模块划分大致如下模块职责关键实体设备管理电表档案、通信参数、在线状态MeterDevice采集调度定时下发读取指令、处理上报数据ReadTask, ReadRecord计费引擎阶梯电价、分时电价计算BillRule, Bill缴费管理余额充值、账单支付、退款RechargeOrder, PaymentFlow用户权限管理员、操作员、住户角色SysUser, Role能耗报表日/月用电量统计、费用汇总ReportQuery模块之间通过数据库表耦合而不是通过服务调用这是单体架构天然的优点。比如计费引擎读账单表生成 Bill 记录时不需要关心采集模块怎么拿到读数的只要采集模块把读数写进 reading_record 表计费任务自然会扫到。2.3 数据库设计账单表必须有的三个字段看源码先看表结构。账单表bill有三个字段容易被忽略但非常重要bill_no、status、version。CREATE TABLE bill ( id bigint(20) NOT NULL AUTO_INCREMENT, bill_no varchar(32) NOT NULL COMMENT 账单号全局唯一, meter_id bigint(20) NOT NULL COMMENT 电表ID, user_id bigint(20) NOT NULL COMMENT 用户ID, period_start date NOT NULL COMMENT 计费周期开始, period_end date NOT NULL COMMENT 计费周期结束, start_reading decimal(12,4) NOT NULL COMMENT 期初读数, end_reading decimal(12,4) NOT NULL COMMENT 期末读数, amount decimal(12,2) NOT NULL COMMENT 账单金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已销账 3已作废, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_bill_no (bill_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;bill_no 必须有唯一约束这是幂等的基础——同一周期生成账单时如果重复插入会被数据库拦住。status 是状态机字段从待支付到已支付再到已销账每一步流转都要在代码里检查前置状态。version 是乐观锁字段支付回调更新账单时要用它防止并发覆盖。这三个字段缺一个后面对账的时候就会翻车。3. 远程抄表链路指令下发、报文解析与并发控制抄表是整个平台的数据源头读数不准、缺失、重复后面计费全错。这块源码实现的核心是两种采集模式主站主动拉取和表端主动上报。平台同时兼容这两种方式业务上互为备份。3.1 两种主流采集模式怎么选主动拉取模式下平台在固定时间向电表发送读取指令电表返回数据帧平台解析后入库。适合集中式小区、工厂等表计通信稳定的场景。主动上报模式下电表按自身周期把读数推送到平台的接收端常见于分散的农村台区或使用窄带物联网的表计。源码里两个模式都做了抽象上层定时任务统一调用采集服务接口底层通过适配器区分不同通信协议。如果你要接自己的硬件重点改适配层就行不需要动任务调度代码。这也是我拆源码时最先确认的部分——通信协议和业务逻辑是解耦的。3.2 定时采集任务的实现与参数设置采集任务的核心代码在 MeterReadTask 里用 Spring 的 Scheduled 注解驱动Service public class MeterReadTask { private static final Logger log LoggerFactory.getLogger(MeterReadTask.class); Autowired private MeterDeviceMapper meterDeviceMapper; Autowired private ReadTaskService readTaskService; Scheduled(cron 0 30 1 * * ?) public void dailyCollect() { // 1. 查询所有在线电表 ListMeterDevice meters meterDeviceMapper.selectOnlineList(); log.info(开始抄表在线设备数量{}, meters.size()); // 2. 提交到线程池批量执行 readTaskService.submitBatchRead(meters); } }cron 表达式0 30 1 * * ?表示每天凌晨 1 点 30 分执行。我一般不建议用 0 点整因为很多系统的数据库备份、日切任务都集中在 0 点抄表任务再挤进去容易和备份抢数据库连接。选 1 点 30 分是为了避开这些窗口。注意这里只是提交任务真正的读取逻辑在 ReadTaskService 里。把任务提交和实际执行分开是为了让定时触发线程快速返回避免 Scheduled 线程被长时间占用导致下一次触发堆积。3.3 线程池参数采集并发写库的调优细节参数推荐值说明corePoolSize8保底常驻线程maxPoolSize32采集高峰期最大并发queueCapacity200等待队列上限rejectedExecutionHandlerCallerRunsPolicy队列满时由主线程执行防止丢任务线程池配置在 yml 文件里核心参数是上面几个。queueCapacity 不能设太大否则几百块表同时排队内存压力不小也不能太小否则触发拒绝策略直接丢任务。CallerRunsPolicy 是比较安全的兜底任务队列满了就让提交任务的线程自己执行宁慢勿丢。这块的血泪经验来自一个能耗平台项目最初用 AbortPolicy队列一满直接抛异常结果凌晨有一批表没采到数据第二天账单全部偏低对账对了一天。后来改成 CallerRunsPolicy 加上补采任务才算稳定下来。3.4 漏抄补采不能让账单一直错下去采集不可能永远成功通信模块断电、信号干扰、电表离线都会导致漏抄。源码里处理漏抄的方式是标记法采集成功的读数记录状态为成功失败的记录为失败。计费任务只统计成功状态的记录失败的会生成补采任务在下一个时段自动重试。补采逻辑我建议三个重试梯度30 分钟后补一次2 小时后补一次次日凌晨再补一次。三次都失败就标为异常推送人工处理。这一步直接决定月度账单的准确性宁可让运维去看一眼表也不能让系统用估算值去计费。4. 计费与缴费流程状态机、流水和幂等是命门采集拿到读数之后真正的业务核心才开始。这套源码里最值得反复读的部分不是页面前端而是计费引擎和缴费回调的状态机设计。简单说就是一个账单从生成到销账每个状态变更都必须有迹可循每个支付回调都必须只生效一次。4.1 阶梯电价与分时电价怎么落到引擎里国内民用电大多执行阶梯电价商业用户可能执行峰谷分时。源码的计费引擎没有把价格硬编码在 Java 代码里而是放在数据库配置表中改价格不用重新编译发版。CREATE TABLE bill_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, rule_type tinyint(4) NOT NULL COMMENT 1阶梯 2分时, level_no int(11) DEFAULT NULL COMMENT 阶梯档位, start_value decimal(12,4) DEFAULT NULL COMMENT 档位起始值, end_value decimal(12,4) DEFAULT NULL COMMENT 档位结束值, price decimal(12,4) NOT NULL COMMENT 单价, effective_date date NOT NULL COMMENT 生效日期, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;阶梯计费时引擎先把周期内总用电量算出来然后按档位逐段累加。比如第一档 0 到 200 度单价 0.5第二档 200 到 400 度单价 0.6某户用了 250 度公式就是 200 乘 0.5 加 50 乘 0.6。源码的引擎在计算时用 BigDecimal 而不是 double这个细节很关键后面避坑章会细说。4.2 缴费回调和销账支付平台重复通知的坑用户在线缴费之后支付平台会通过回调接口通知平台入账。回调接口的特点是可能重复调用很多次网络超时会重试平台重启也会重推。如果不做幂等防护同一个订单会被入账两次余额直接翻倍。Transactional(rollbackFor Exception.class) public void handleRechargeCallback(RechargeCallbackVO vo) { // 1. 按订单号查本地订单 RechargeOrder order rechargeOrderMapper.selectByOrderNo(vo.getOrderNo()); if (order null || order.getStatus() RechargeStatus.SUCCESS) { // 已处理过或订单不存在直接返回 return; } // 2. 加锁防止并发重复入账 RechargeOrder locked rechargeOrderMapper.selectForUpdate(order.getId()); // 3. 更新余额 写资金流水 更新订单状态 accountMapper.increaseBalance(locked.getUserId(), locked.getAmount()); paymentFlowMapper.insert(buildFlow(locked)); rechargeOrderMapper.updateStatus(locked.getId(), RechargeStatus.SUCCESS); // 4. 自动销账把该用户待支付账单按时间顺序抵扣 billService.settlePendingBills(locked.getUserId()); }// Mapper 中的行锁查询示例 Select(SELECT * FROM recharge_order WHERE id #{id} FOR UPDATE) RechargeOrder selectForUpdate(Param(id) Long id);这里的每一步都有讲究。先查一遍订单状态是为了拦截大多数重复回调不走无谓的锁。selectForUpdate 是数据库行锁同一时间只有一个事务能更新这个订单防止并发回调同时入账。更新余额和写流水放在同一个事务里要么都成功要么都回滚不会出现余额变了流水没记的怪象。最后调 settlePendingBills 把待支付账单按时间顺序用余额抵扣这一步就是销账。4.3 账单状态机每一步流转都要检查前置状态账单状态设计成单向流转待支付到已支付已支付到已销账已支付到退款作废。状态机最关键的一条规则是禁止跳变和回退。比如已销账的账单不能被退回到待支付否则用户余额和账单状态就失去对应关系了。源码里用的是简单的状态检查加乐观锁更新更新语句带状态条件类似于UPDATE bill SET status #{newStatus}, version version 1 WHERE id #{id} AND status #{expectedStatus}。如果影响行数为 0说明当前状态不是预期状态说明有并发操作抢在前面了这时候抛异常或者重试。5. 部署、测试与排查五个容易翻车的踩坑记录源码拆解是一回事真正部署运行是另一回事。这一章说几个我在实际部署和测试中遇到的坑每一条都是花钱买来的经验。如果你是第一次跑这套平台照着这个清单提前检查能省下不少时间。5.1 数据库连接参数的时区问题现象采集到的读数时间比实际时间早了 8 个小时电费报表的日统计全部错位。原因服务器默认使用 UTC 时区而业务数据用的是北京时间。解决在 JDBC 连接串里明确指定时区。spring: datasource: url: jdbc:mysql://localhost:3306/meter_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai这条参数看起来不起眼但漏掉之后所有基于时间的统计都会跟着错。别问我怎么知道的某次部署我排查了整整一个下午才发现是连接串少了 serverTimezone。5.2 金额计算用 double 导致精度丢失现象某户账单金额显示为 88.19999999999999 元。原因代码里用了 double 做金额运算。解决全链路改用 BigDecimal数据库字段用 decimal比较大小用 compareTo 而不是 equals。金额和读数相关的所有字段从实体类到 VO 再到数据库统一 BigDecimal 和 decimal不要在中间环节转成浮点型。5.3 集群部署时定时任务重复执行现象两块电表的数据被采集了两遍账单金额翻倍。原因平台部署在两个节点上Scheduled 在每个节点都会触发。解决给采集任务加分布式锁。常见做法是使用 Redis 的 setnx 命令抢锁抢到锁的节点才执行采集任务执行完释放锁另一个节点直接跳过。如果不想引入 Redis也可以借助数据库的SELECT ... FOR UPDATE或唯一任务标识做分布式锁。5.4 重启后未完成的采集任务全部丢失现象平台夜间重启凌晨的抄表任务没有补跑当天部分电表无读数。原因任务只存在内存里没有持久化。解决建议在采集任务开始时把批次信息写入任务表状态标记为执行中重启后扫描未完成任务并重新提交。这套源码里带了任务表的雏形但没有实现断点续跑这属于需要自己加固的部分。5.5 回调处理抛异常导致事务回滚回调方不断重试现象支付回调接口偶发 500支付平台反复推送同一订单。原因代码里某个非关键步骤抛了异常导致整个事务回滚。解决回调处理的主流程里只有余额变更和流水写入必须在事务内其余操作如通知、短信、日志挪到事务提交后再执行。我是用事务边界拆分解决的先入账成功了再发通知通知失败不影响入账结果。这样可以避免非关键步骤拖垮资金主链路。6. 把源码跑起来之后模拟电表与二次开发验证前面几章讲了架构和逻辑这一章说说怎么验证这套东西真的能跑通。我一般会先起一个模拟电表的脚本伪造读数然后走一遍完整的采集、计费、缴费流程确认每个环节的状态都符合预期才敢接真实硬件。6.1 用脚本模拟电表数据import random import time meter_id METER001 base_reading 1000.0 while True: # 模拟电表每隔几秒上报一次当前读数 delta random.uniform(0.1, 0.5) base_reading delta payload f{meter_id},{round(base_reading, 4)},{int(time.time())} print(payload, flushTrue) time.sleep(5)这是一个极其简单的脚本每次循环产生一个递增的读数按设备号,读数,时间戳的格式输出。实际使用中可以把 print 替换成 HTTP 上报或写入 MQTT 主题。关键在于它能持续产生有效数据让平台的采集接口有东西可收。6.2 验证闭环的四个步骤第一步启动平台在设备管理里手动添加一块表表号对应脚本里的 meter_id。第二步运行模拟脚本观察日志里是否出现读数入库记录数据库里 reading_record 表是否持续增加数据。第三步手动触发一次计费任务确认账单金额与预期一致——手动算一遍总用电量乘单价。第四步模拟一次支付回调确认余额增加、流水写入、账单状态从待支付变成已销账。这四步走完等于把平台的采集、计费、缴费三条核心链路都验证了一遍。任何一步状态不对都能在日志里找到线索切记别跳过。6.3 从电表到水表、燃气表改造边界这套平台的设计思路其实不限于电表。水表、燃气表的抄表计费链路高度相似定时采集读数、按价格规则计费、生成账单、用户缴费。改造的边界主要在适配层——通信协议和读数格式不一样其他模块基本可以复用。把设备类型从电表换成水表需要改的是报文解析部分、表档案字段和计费规则配置而采集调度、账单状态机、支付回调幂等处理这些核心逻辑原封不动就能跑。有一个经验值得分享某次我接一个热力表项目直接把电表逻辑拿过来套用结果发现热力表的计费还涉及温度补偿系数只在规则表里加字段不够还要改计算引擎。所以二次开发前先列清楚业务差异点再决定哪些部分复用、哪些部分重写不要急着动手。从那以后我每接手一个源码项目都会强制自己先跑一遍模拟数据验证链路完整性再考虑改造。习惯这东西养成了就能少踩很多坑。希望帮到你。本文还有配套的精品资源点击获取