智能电表远程抄表缴费平台JAVA源码:从建模到闭环落地

📅 发布时间:2026/9/25 5:43:44
智能电表远程抄表缴费平台JAVA源码:从建模到闭环落地
简介智能电表远程抄表缴费管理平台JAVA源码是一套基于Java技术的物联网应用面向物业、房东及写字楼管理者用于解决人工抄表效率低、缴费不便等痛点。资源包共86个文件主体为77个Java代码文件辅以8个XML配置文件和1个IML工程模块描述整体压缩为67KB的RAR包目录结构清晰便于导入开发工具直接阅读。平台功能覆盖远程抄表、能耗数据处理、线上缴费、用户权限管理、异常报警和自动报表等模块并可对接正泰、人民、天正、许继等主流品牌电表。目前已有2174人学习下载。对开发者而言研读这份源码能理解物联网通信集成如GPRS/LoRa/NB-IoT、定时采集任务设计、支付接口对接及前后端交互等关键实现尤其适合希望掌握物联网应用全链路开发的初中级工程师为二次开发或构建同类能源管理项目提供直接参考。1. 一套 JAVA 源码先把“远程”放一边闭环才是智能电表平台的命门单看“智能电表远程抄表缴费管理平台 JAVA 源码”这个名字很多人第一反应是通讯协议、采集终端和 4G 模块。真把这套源码下载下来跑一遍就会发现难点根本不在“远程”而在抄表、计费、缴费这三步有没有被数据模型和状态机锁死。这个平台要解决的是物业和园区长期对不上的电费账不再上门抄表、手工算费而是把表计档案、月度读数、阶梯电价计算、在线缴费和流水对账全放到同一个 Web 页面上闭环。适合有 Java 基础的学生拿来做课程设计或毕业设计也适合中小型收费系统做一套能落地的代码底座。2. 先拆业务再建表抄表、计费、缴费凭什么不打架2.1 三个角色一条链路开发前先把账算清楚一个能真正运行的智能电表远程抄表缴费平台角色至少分三档管理员负责建电表档案、配电价规则、分配账号权限抄表员负责录入表底数、复核异常数据财务或收费员负责生成账单、处理缴费和冲正。如果客户还要自己登录查账单、在线缴费就需要再拆出独立的用户角色。角色不拆开后面所有统计口径都会乱这是我在看很多 JAVA 源码项目时最先挑的毛病。动手写第一行 Java 代码之前我会先把业务链路走一遍表计台账初始化 → 周期抄表人工录入或远程采集→ 计算用电量 → 按电价规则生成应收账单 → 发起缴费 → 支付回调更新账单 → 财务对账。这条链路上抄表记录是底稿账单是应收缴费流水是实收三个数字必须能闭环勾稽。这里也是这类“JAVA 源码”和普通增删改查课程设计的分水岭。远程抄表在源码里通常只是抽象出一个采集器接口表具可能走 DL/T 645、Modbus 或厂家私有协议但平台侧真正关心的是拿到读数之后的数据如何流转。我一般会保留一个模拟采集器让项目在没有真实电表的情况下也能把演示跑通真实设备接入时再替换实现类。2.2 表计、抄表、账单、缴费四张核心表怎么建才不埋雷建表是这套平台最值得抄作业的部分。核心设计原则是一个物理电表对应一个表计台账一个表计每个月只有一条抄表记录一条抄表记录经过计费生成一张账单一张账单挂多笔缴费流水。下面这是精简版的 DDL字段按真实项目常用口径缩减过。-- 表计台账一个物理电表对应一条档案 CREATE TABLE tb_meter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_no VARCHAR(32) NOT NULL COMMENT 电表编号现场唯一, customer_name VARCHAR(64) COMMENT 业主/房间名称, meter_model VARCHAR(32) COMMENT 电表型号, initial_reading DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 初装表底, last_reading DECIMAL(12,2) NOT NULL COMMENT 最近一次已复核表底, tariff_type TINYINT NOT NULL COMMENT 1居民阶梯 2商业单一 3峰谷, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, UNIQUE KEY uk_meter_no (meter_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT表计台账;-- 抄表记录一个表计一个月只允许一条 CREATE TABLE tb_reading ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_id BIGINT NOT NULL, read_month CHAR(6) NOT NULL COMMENT 格式YYYYMM, last_reading DECIMAL(12,2) NOT NULL COMMENT 上次表底冗余自tb_meter, current_reading DECIMAL(12,2) NOT NULL COMMENT 本次表底, usage_amount DECIMAL(12,2) NOT NULL COMMENT 本月用电量, read_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待复核 1已复核 2已计费, reader_user_id BIGINT COMMENT 抄表员, read_time DATETIME COMMENT 抄表时间, remark VARCHAR(255), UNIQUE KEY uk_meter_read_month (meter_id, read_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT抄表记录;-- 账单一个抄表记录只生成一张应收单 CREATE TABLE tb_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(32) NOT NULL COMMENT 账单编号, reading_id BIGINT NOT NULL COMMENT 关联抄表记录, total_amount DECIMAL(10,2) NOT NULL COMMENT 应收金额, paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 已收金额, bill_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1部分支付 2已缴清, UNIQUE KEY uk_bill_no (bill_no), UNIQUE KEY uk_reading_id (reading_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT电费账单;-- 缴费流水一单支付只允许入账一次 CREATE TABLE tb_payment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, out_trade_no VARCHAR(64) NOT NULL COMMENT 支付渠道订单号幂等键, bill_id BIGINT NOT NULL, pay_amount DECIMAL(10,2) NOT NULL, pay_type TINYINT COMMENT 1微信 2支付宝 3现金 4预存, pay_time DATETIME NOT NULL, UNIQUE KEY uk_out_trade_no (out_trade_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT缴费流水;这里有几个参数值得说明tb_reading的唯一键(meter_id, read_month)是防重抄的第一道防线单纯靠 Java 代码校验不靠谱并发请求下很容易插进去两条同月记录。tb_bill的reading_id也做唯一键避免同一抄表记录被计费两次。金额字段一律用DECIMAL(12,2)绝不能图省事用float或double否则后面算电费会出现“差几分钱”这种很难给业主解释的问题。2.3 状态字段代替物理删除账务逻辑不能心软这套平台里的每张核心表都带状态字段这是刻意的。抄表记录用read_status从 0 走到 2账单用bill_status从 0 走到 2支付流水一旦写入就不允许修改或删除。很多新手拿到源码后会习惯性用delete来“作废订单”这在电费系统里是致命的因为财务对账需要完整留痕。我一般会约定三条规则。第一抄表数据录错了不做删除而是新增一条“冲正记录”把错误读数通过状态位标记为作废。第二账单生成后如果需要重新计费必须走“冲红”流程生成负数金额账单和原账单冲抵而不是直接改原单。第三缴费回调如果发现渠道重复通知直接按幂等键返回成功不让账单金额发生二次变化。这样设计之后SELECT sum()查出来的数字永远等于业务真实发生值不会出现账表对不上的玄学问题。-- 冲正示例对已支付流水做负数入账保留原流水 INSERT INTO tb_payment (out_trade_no, bill_id, pay_amount, pay_type, pay_time) VALUES (REFUND_20250101_001, 1001, -50.00, 4, NOW());负金额流水听起来反直觉但它是财务系统里最常见的做法查询历史流水时一目了然。至于并发安全Java 侧可以靠Transactional保证同一事务内更新账单和插入流水要么全成功要么全失败数据库侧再靠唯一约束兜底两层都别省。3. 把核心流程写成 JAVA 代码抄表校验、阶梯计费和缴费幂等3.1 抄表录入服务唯一约束兜底业务校验先行抄表模块是整个平台的入口代码不复杂但校验逻辑必须写全。下面这段是典型的ReadingService实现Service public class ReadingServiceImpl implements ReadingService { Autowired private ReadingMapper readingMapper; Autowired private MeterMapper meterMapper; Transactional(rollbackFor Exception.class) public Long addReading(ReadingDTO dto) { Meter meter meterMapper.selectById(dto.getMeterId()); if (meter null) { throw new BizException(表计档案不存在); } // 本次读数小于上次表底直接拒绝 if (dto.getCurrentReading().compareTo(meter.getLastReading()) 0) { throw new BizException(本次表底不能小于上次表底); } Reading reading new Reading(); reading.setMeterId(meter.getId()); reading.setReadMonth(dto.getReadMonth()); reading.setLastReading(meter.getLastReading()); reading.setCurrentReading(dto.getCurrentReading()); // 用电量 本次 - 上次 reading.setUsageAmount(dto.getCurrentReading().subtract(meter.getLastReading())); reading.setReadStatus(ReadingStatus.PENDING_REVIEW.getCode()); readingMapper.insert(reading); // 更新台账上的最近表底但只更新时间条不覆盖历史 meterMapper.updateLastReading(meter.getId(), dto.getCurrentReading()); return reading.getId(); } }这段代码有三个关键点。第一比较表底数必须用BigDecimal.compareTo不能用equals因为1.0和1.00的 scale 不同equals会返回 false这在 Java 面试题里也常被拿来当陷阱。第二Transactional(rollbackFor Exception.class)指定了遇到运行时异常也回滚否则默认只在RuntimeException上回滚某些 checked exception 会导致流水写了一半。第三last_reading冗余在台账表里是为了减少每次抄表都要去翻上一张记录的查询成本但必须保证和抄表流水一致所以更新操作要和插入抄表记录放在同一个事务里。3.2 阶梯电价计算策略模式替掉 if-else别把规则写死在 service 里很多课程设计源码会把阶梯电价写成if (usage 200) { ... }这是最容易翻车的地方。电价规则会随政策调整硬编码意味着每次改规则都要重新打包上线。常见做法是定义一个PricingStrategy接口把电价规则做成可配置的策略类再用Map按tariffType分派。下面抽两段核心代码public interface PricingStrategy { // 返回当前策略支持的电价类型 TariffType type(); // 根据用电量计算电费 BigDecimal calc(BigDecimal usage); }Component public class LadderPricingStrategy implements PricingStrategy { private final ListLadderRule rules; public LadderPricingStrategy(ListLadderRule rules) { this.rules rules; } Override public TariffType type() { return TariffType.LADDER; } Override public BigDecimal calc(BigDecimal usage) { BigDecimal total BigDecimal.ZERO; BigDecimal remain usage; // rules 按阶梯上限从小到大排序例如 [0-200, 201-400, 401] for (LadderRule rule : rules) { if (remain.compareTo(BigDecimal.ZERO) 0) { break; } BigDecimal block rule.getMaxUsage() .subtract(rule.getMinUsage()); BigDecimal usedInBlock remain.min(block); total total.add(usedInBlock.multiply(rule.getPrice())); remain remain.subtract(usedInBlock); } return total.setScale(2, RoundingMode.HALF_UP); } }LadderRule表结构至少要包含min_usage、max_usage、price、effective_month四个字段加载时按生效月份过滤。把规则从 Java 代码挪到数据库配置之后运营人员改电价就只需要在管理页面维护一张表不需要碰代码。另一个好处是以后接峰谷分时电价时只需要新增PeakValleyPricingStrategyBillingCalculator的 Map 通过 Spring 注入策略列表自动注册代码零改动。3.3 缴费回调幂等同一个支付通知来了两次流水绝不入账两次缴费模块是资金相关逻辑最怕重复入账。支付渠道回调偶尔会重发如果代码不做幂等一张 100 元的账单可能被入账两次平台账面上立刻出现 100 元差额。这里的关键是“先查本地订单号再决定是否写流水”。Transactional(rollbackFor Exception.class) public void confirmPayment(PaymentConfirmDTO dto) { Payment exist paymentMapper.selectByOutTradeNo(dto.getOutTradeNo()); if (exist ! null) { // 已入账直接返回避免重复记账 return; } Bill bill billMapper.selectByIdForUpdate(dto.getBillId()); if (bill null) { throw new BizException(账单不存在); } // 用条件更新防止并发下账单金额加重复 int changed billMapper.increasePaidAmount( bill.getId(), dto.getPayAmount(), bill.getPaidAmount()); if (changed 0) { throw new BizException(账单状态已变更请刷新后重试); } Payment payment new Payment(); payment.setOutTradeNo(dto.getOutTradeNo()); payment.setBillId(bill.getId()); payment.setPayAmount(dto.getPayAmount()); payment.setPayTime(dto.getPayTime()); paymentMapper.insert(payment); }这段代码有两个细节容易被忽略。selectByIdForUpdate用数据库行锁把 bill 锁住避免两个线程同时读到同一个paid_amount然后各加一遍。increasePaidAmount的 SQL 里要带WHERE paid_amount #{oldPaidAmount}这是乐观锁语义一旦发现已经被别人改过就直接失败让上层决定重试还是人工干预。传入的dto里如果有用户 ID 或操作员 ID也建议一并写到流水表方便以后查“谁在什么时候动了这笔账”。4. 把源码跑起来从 JDK 环境变量配置到数据库初始化和打包4.1 技术栈和版本选型JDK、MySQL、Maven 怎么配最稳这类 JAVA 源码最常见的组合是 Spring Boot 2.x MyBatis-Plus MySQL 8.0 Maven前端用 Vue 2 或 Layui 打包进静态资源目录。选型理由很简单Spring Boot 让开发和部署成本低MyBatis-Plus 的selectById、updateById能省掉大量单表 CRUD 代码MySQL 8 在事务和 JSON 支持上都够用。组件推荐版本说明JDK1.8 或 11如果是 JDK 17部分旧依赖会有源发行版 17编译问题Maven3.63.8 对镜像和插件更友好MySQL5.7 或 8.0驱动必须和版本匹配8.0 用com.mysql.cj.jdbc.DriverRedis选配只在做缓存、验证码时用不依赖也能启动这里我多说一句很多新手卡在启动阶段其实是 JDK 环境变量配置出了问题。JAVA_HOME指向 JDK 安装目录PATH里要把%JAVA_HOME%\bin加到最前面cmd里执行java -version能输出版本才算配好。如果电脑里装了多个 JDKMaven 编译时用的版本可能和 IDE 显示的不一致优先看mvn -version输出里的Java version字段。4.2 初始化数据库建库、导表、灌数据的执行顺序别乱数据库初始化是这套平台最容易出错的一步。源码包里一般会有sql目录我的建议是按下面顺序执行不要图省事只导一个合并文件。# 第一步建库设置 utf8mb4 mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS smart_meter DEFAULT CHARSET utf8mb4; # 第二步导入表结构 mysql -uroot -p smart_meter sql/init_schema.sql # 第三步导入基础数据 mysql -uroot -p smart_meter sql/init_data.sql # 第四步验证是否导成功 mysql -uroot -p smart_meter -e SHOW TABLES;为什么要分两步init_schema.sql只建表结构init_data.sql负责灌入管理员账号、电价规则、模拟表计等基础数据。如果合成一个大文件一旦中间的模拟数据出错很难定位是结构问题还是数据问题。导入后至少看到tb_meter、tb_reading、tb_bill、tb_payment四张表都在再继续下一步。MySQL 8 下的连接串需要带上serverTimezoneAsia/Shanghai否则 Java 驱动会用 JVM 默认时区数据库里存的时间可能差 8 小时。4.3 修改配置并启动application.yml 里最关键的 5 个参数拿到源码后第一件事不是直接mvn spring-boot:run而是先把application.yml改对。下面是精简后的配置spring: datasource: url: jdbc:mysql://localhost:3306/smart_meter?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true server: port: 8080 meter: collector: enabled: falseuseSSLfalse是因为本地开发一般没配证书不开反而省事。characterEncodingutf8保证中文不会乱码。map-underscore-to-camel-case: true是 MyBatis-Plus 自动把meter_no映射成meterNo的开关这个不开大量查询结果会是 null。meter.collector.enabled是模拟远程采集的开关先在false模式下跑通等接真实表具再打开。然后执行打包和启动# 打包跳过测试 mvn clean package -DskipTests # 启动 java -jar target/smart-meter-0.0.1-SNAPSHOT.jar --spring.profiles.activedev启动日志里看到Started Application之后浏览器访问http://localhost:8080就能进入登录页。如果端口被占用用--server.port8081临时换一个不用改代码。打包时-DskipTests是为了避免测试用例里依赖数据库导致构建失败等确认环境没问题后再放开测试。5. 跑 JAVA 源码最常见的 5 个坑从编译报错到对账不平排查5.1 表名或字段查询不到MySQL 大小写敏感在 Windows 和 Linux 表现不一样现象源码在本地 Windows 跑得好好的部署到 Linux 服务器后登录正常但列表页全是空数据频繁报Table smart_meter.tb_reading doesnt exist。原因Windows 的 MySQL 默认lower_case_table_names1表名大小写不敏感Linux 默认是 0敏感。如果 SQL 里写的是tb_Reading而建表语句是tb_reading在 Linux 上直接找不到表。解决统一把表名和字段名改成小写或在 Linux 的my.cnf里配置lower_case_table_names1后重启 MySQL。我习惯在项目里强制约定所有表名、字段名全小写下划线分隔这样代码迁徙到任何环境都不踩坑。5.2 电费总是差几分钱数据库里用了 float 或 double现象同一张抄表记录人工拿计算器算出的电费和系统打印出来的账单差了 0.01 元一个月两张单子就能差出几角钱。原因Java 侧用了BigDecimal可能没问题但表结构里如果字段是float从 MySQL 读出来转成BigDecimal时已经带上了浮点误差。常见于账单金额、缴费流水字段类型设计随意。解决把金额字段全部改成DECIMAL(10,2)实体类属性用BigDecimalJava 代码里所有单价相乘后setScale(2, RoundingMode.HALF_UP)。这属于数据模型基础问题改完基本一劳永逸。5.3 远程采集接口超时整单抄表事务失败现象接上真实电表采集器后点一次远程抄表要等十几秒偶尔超时报错结果这一条读数没存进去台账底数也没更新。原因设备通信被放进了和数据库操作同一个事务里。网络 IO 不稳定一旦超时事务回滚把已写好的读数也一起回滚了。解决把采集动作和业务落地拆成两步。第一步先调采集器接口拿读数拿到后保存在缓存表或消息队列第二步再启动本地事务写入tb_reading和更新台账。失败的采集任务丢给定时重试补偿任务只读缓存表不重新发起网络请求。没有消息队列时用一个tb_collect_task任务表也能实现同样效果。5.4 Maven 编译报“源发行版 17 需要目标发行版 17”现象mvn clean package时直接失败提示java: 警告: 源发行版 17 需要目标发行版 17或者报无效的目标发行版。原因项目里的pom.xml没指定java.version而本机 Maven 默认用了 JDK 17。如果源码是按 JDK 8 写的某些 API 在 17 下行为也会变化。解决在pom.xml的properties里明确指定properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties改完后重新mvn clean再打包。如果你是项目开发者强烈建议在 README 里写清楚“只能用 JDK 8/11”能省掉一半的环境类提问。5.5 缴费回调重复入账账单已缴清但收款多了一笔现象业主付款后说显示扣了两次钱后台账单状态是已缴清但支付流水表里有两条相同金额记录。原因支付回调没有做幂等也没有在流水表上建唯一约束。渠道短时间内连续通知两次第二次进来时又走了一遍入账逻辑。解决落实第 3 章的幂等方案tb_payment.out_trade_no必须有唯一索引Java 侧先查后插不够必须靠唯一约束兜底。另外bill_status变更要放在同一个事务里用条件更新确认“当前金额 本次金额”不超过应收总额超过直接拒绝并告警。6. 从 demo 到能上线三个验证动作让这套 JAVA 源码真正可信6.1 用定时任务代替手工点“远程抄表”项目能跑只是第一步能持续跑才是验证源码质量的关键。我会给采集器接口套一层定时任务让它每天凌晨自动生成模拟读数这样就不用每次回归测试都手动去点按钮。Spring Boot 里加一个Scheduled即可Component public class MeterCollectJob { Scheduled(cron 0 30 3 * * ?) public void autoCollect() { ListMeter meters meterMapper.selectAllNormal(); for (Meter meter : meters) { // 随机生成递增读数真实系统在这里替换成表具通信协议 BigDecimal current meter.getLastReading() .add(BigDecimal.valueOf(ThreadLocalRandom.current().nextInt(1, 50))); readingService.addReading(buildDTO(meter, current)); } } }跑一周下来如果账单生成、缴费入账、报表统计都能对上这套源码的核心链路才算经得起验证。cron表达式里0 30 3 * * ?表示每天 3 点 30 分执行避开白天业务高峰。注意EnableScheduling要加到启动类上不加这个注解定时任务是静默失效的这是新手最容易忽略的坑。6.2 一张对账 SQL 把三张表之间的数字核平我交付前一定会跑一遍“三表对账”抄表用电量合计应该等于账单应收合计账单实收合计应该等于支付流水合计。下面这条 SQL 可以把问题一次性捞出来SELECT DATE_FORMAT(r.read_month, %Y-%m) AS month, SUM(r.usage_amount) AS total_usage, SUM(b.total_amount) AS total_bill, SUM(p.pay_amount) AS total_payment, SUM(b.paid_amount) - SUM(p.pay_amount) AS diff FROM tb_reading r LEFT JOIN tb_bill b ON b.reading_id r.id LEFT JOIN tb_payment p ON p.bill_id b.id GROUP BY month;diff不为 0 时优先查tb_payment里是否有负金额冲正记录再查是否存在paid_amount大于应收的脏数据。这套 SQL 建议直接固化成一个报表页面让运营每周自己跑比出问题后再翻代码高效得多。6.3 造一年数据做边界回归别只在 demo 数据上自嗨很多源码自带的初始数据只有两三个表计根本测不出问题。我会用存储过程或一段 Python 脚本批量生成 12 个月、上百个表计的读数每月每个表计生成一条记录用电量按正态分布模拟。等数据量上来后再验证三个边界连续多个月未抄表的表计账单是否会积压全部表计同一天抄表是否撑得住阶梯电价跨档时金额是否平滑递增。我自己的习惯是把这些验证脚本放在项目scripts/目录里和源码一起保存每次改完计费逻辑就跑一遍完整回归。一套能落地的智能电表远程抄表缴费管理平台不是把页面做出来就够了账能对上、规则能配置、异常能追溯才是这套 JAVA 源码真正值钱的地方。希望帮到你。本文还有配套的精品资源点击获取