☰
智能电表远程抄表缴费平台JAVA源码:从建模到闭环落地
2026/9/25 5:43:35 网站建设 项目流程

简介:智能电表远程抄表缴费管理平台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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 List<LadderRule> rules; public LadderPricingStrategy(List<LadderRule> 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 代码挪到数据库配置之后,运营人员改电价就只需要在管理页面维护一张表,不需要碰代码。另一个好处是以后接峰谷分时电价时,只需要新增PeakValleyPricingStrategy,BillingCalculator的 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.6+3.8+ 对镜像和插件更友好
MySQL5.7 或 8.0驱动必须和版本匹配,8.0 用com.mysql.cj.jdbc.Driver
Redis选配只在做缓存、验证码时用,不依赖也能启动

这里我多说一句:很多新手卡在启动阶段,其实是 JDK 环境变量配置出了问题。JAVA_HOME指向 JDK 安装目录,PATH里要把%JAVA_HOME%\bin加到最前面,cmd里执行java -version能输出版本才算配好。如果电脑里装了多个 JDK,Maven 编译时用的版本可能和 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 下的连接串需要带上serverTimezone=Asia/Shanghai,否则 Java 驱动会用 JVM 默认时区,数据库里存的时间可能差 8 小时。

4.3 修改配置并启动:application.yml 里最关键的 5 个参数

拿到源码后第一件事不是直接mvn spring-boot:run,而是先把application.yml改对。下面是精简后的配置:

spring: datasource: url: jdbc:mysql://localhost:3306/smart_meter?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 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: false

useSSL=false是因为本地开发一般没配证书,不开反而省事。characterEncoding=utf8保证中文不会乱码。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.active=dev

启动日志里看到Started Application之后,浏览器访问http://localhost:8080就能进入登录页。如果端口被占用,用--server.port=8081临时换一个,不用改代码。打包时-DskipTests是为了避免测试用例里依赖数据库导致构建失败,等确认环境没问题后再放开测试。

5. 跑 JAVA 源码最常见的 5 个坑:从编译报错到对账不平排查

5.1 表名或字段查询不到:MySQL 大小写敏感在 Windows 和 Linux 表现不一样

现象:源码在本地 Windows 跑得好好的,部署到 Linux 服务器后,登录正常但列表页全是空数据,频繁报Table 'smart_meter.tb_reading' doesn't exist。

原因:Windows 的 MySQL 默认lower_case_table_names=1,表名大小写不敏感;Linux 默认是 0,敏感。如果 SQL 里写的是tb_Reading而建表语句是tb_reading,在 Linux 上直接找不到表。

解决:统一把表名和字段名改成小写,或在 Linux 的my.cnf里配置lower_case_table_names=1后重启 MySQL。我习惯在项目里强制约定:所有表名、字段名全小写,下划线分隔,这样代码迁徙到任何环境都不踩坑。

5.2 电费总是差几分钱:数据库里用了 float 或 double

现象:同一张抄表记录,人工拿计算器算出的电费和系统打印出来的账单差了 0.01 元,一个月两张单子就能差出几角钱。

原因:Java 侧用了BigDecimal可能没问题,但表结构里如果字段是float,从 MySQL 读出来转成BigDecimal时已经带上了浮点误差。常见于账单金额、缴费流水字段类型设计随意。

解决:把金额字段全部改成DECIMAL(10,2),实体类属性用BigDecimal,Java 代码里所有单价相乘后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.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.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() { List<Meter> 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 源码真正值钱的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询