
简介一份基于Java的智能电网信息系统设计源码面向智能电网信息化管理场景为开发人员提供可借鉴的系统设计与实现思路。资源包共112个文件大小约230KB以66个Java源文件为核心搭配21个类定义文件、14个XML配置、2个YAML配置另有lst、gitignore、iml、txt、properties等辅助文件目录结构清晰。Java源码承担业务逻辑与数据处理类定义体现面向对象模块化思想XML与YAML分别用于配置组件属性和服务参数便于系统部署与维护。从内容预览可见项目包含统一返回结果、全局异常处理、Redis缓存、Swagger接口文档等基础能力通过阅读源码可学习统一响应封装、异常拦截、缓存集成及接口文档生成的具体写法同时理解智能电网信息管理场景下的数据模型与服务分层设计。目前已有319人浏览学习适合正在研究智能电网信息系统或Java企业级应用架构的开发者对照研读也可作为课程设计或毕业设计的参考蓝本。1. 智能电网信息系统设计源码Java 技术栈为什么是这个领域的常青树智能电网信息系统把发、输、变、配、用电的数据从四面八方汇到同一个平台里既要吃得下海量测点又要保证遥控命令不迟到、不倒置。这份基于 Java 的设计源码讲的就是一套完整可运行的电网信息系统落地方案——从数据采集、数据库设计到核心算法和告警处理我把它拆成手把手能复现的工程步骤。适合已经会 Java 基础的开发者和电力行业程序员也适合准备 Java 工程师面试时拿一个真实业务场景做项目复盘。本文不聊空架构而是把关键类、每张表、每个参数都摆出来说说它们为什么这样设计、真正的坑在哪儿。2. 系统架构与核心数据模型先想清楚“四遥”再写第一行代码做智能电网系统第一件事不是写登录而是把“四遥”想明白遥测、遥信、遥控、遥调。这四个词几乎决定了数据模型和接口设计。从头做这套源码工程时我习惯先把四层架构和核心表结构定下来否则后面每加一个功能都可能要改表。2.1 模块划分采集、存储、计算、展示四层模块划分按数据流向切不能按部门切。我一般把这套系统拆成四层采集层负责解析变电站、配电终端、电表等设备上报的报文。常见协议有 IEC 60870-5-101/104、Modbus、DL/T 645。这一层是 Java 里最贴近网络编程的地方需要处理粘包、拆包、帧超时。存储层落地遥测、遥信、告警、操作记录。注意这里不是传统的业务 CRUD而是大量高频写、范围查询所以建表时就要考虑时间分区、索引策略。计算层做越限判断、负荷预测、线损计算、状态估计。它消费采集数据产出结果写回数据库或缓存。展示层给调度员看实时曲线、设备拓扑、告警列表也提供遥控操作入口。后端用 Spring Boot 提供 REST 接口就够了。这里有一个最常见的误区把“四遥”当成一张宽表来设计。遥测测量值和遥信状态值的数据特性差异很大前者是连续值后者是离散布尔量混在一张表里会让存储和查询都很别扭。源码工程里我都是单独建表各算各的。2.2 数据模型遥测、遥信、遥控/遥调表怎么建下面这套表结构是我在多个项目里调过几轮后固定下来的能够兼顾实时写入和后续分析。核心是设备表、测点表、遥测表、遥信表、遥控记录表。-- 设备表 CREATE TABLE t_device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(64) NOT NULL COMMENT 设备编号如站控/间隔/装置, device_name VARCHAR(128) NOT NULL, station_code VARCHAR(64) COMMENT 所属厂站, protocol_type VARCHAR(16) COMMENT 101/104/modbus, address VARCHAR(64) COMMENT 链路地址或RTU地址, enabled TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_device_code (device_code) ) ENGINEInnoDB COMMENT电力设备档案; -- 测点表描述设备有哪些遥测/遥信点 CREATE TABLE t_measure_point ( id BIGINT PRIMARY KEY AUTO_INCREMENT, point_code VARCHAR(128) NOT NULL COMMENT 测点编号如YX_01_TA1, point_name VARCHAR(128), device_id BIGINT NOT NULL, point_type TINYINT COMMENT 1遥测,2遥信,3遥控, unit VARCHAR(16), factor DECIMAL(10,4) DEFAULT 1.0 COMMENT 变比/系数, deadband DECIMAL(10,4) DEFAULT 0.0 COMMENT 死区, last_value BIGINT, last_time DATETIME, UNIQUE KEY uk_point (device_id, point_type, point_code) ) ENGINEInnoDB COMMENT测点档案; -- 遥测历史表按天分表示例只建主表 CREATE TABLE t_telemetry ( id BIGINT PRIMARY KEY AUTO_INCREMENT, point_id BIGINT NOT NULL, value DECIMAL(12,4) NOT NULL, quality TINYINT COMMENT 0正常,1无效,2越限, collect_time DATETIME NOT NULL, receive_time DATETIME NOT NULL, KEY idx_point_time (point_id, collect_time) ) ENGINEInnoDB COMMENT遥测历史数据;这里的关键是collect_time和receive_time分开存。collect_time是设备端的采集时间receive_time是服务器收到报文的时间。很多现场问题排查都靠这两个时间差判断是网络延迟还是设备本身死机。遥信表类似但字段变成布尔状态和变位标志。还需要一张遥控记录表保存每一次下发的全过程CREATE TABLE t_remote_command ( id BIGINT PRIMARY KEY AUTO_INCREMENT, command_id VARCHAR(64) NOT NULL COMMENT 命令全局唯一ID, device_id BIGINT, point_id BIGINT, command_value TINYINT COMMENT 0分,1合, operator VARCHAR(32), create_time DATETIME, ack_time DATETIME, status TINYINT COMMENT 0等待,1成功,2失败,3超时, UNIQUE KEY uk_command_id (command_id) ) ENGINEInnoDB COMMENT遥控/遥调操作记录;这张表是调度系统的审计底线。一旦现场出了开关误动查记录就能还原是谁、在几点、发了什么命令。字段command_value要配合测点表的point_type使用不能让它孤立地解释业务。2.3 技术选型Spring Boot MyBatis、Netty、MySQL 的取舍这个题目的工程源码我选的是 Spring Boot 2.x MyBatis采集层用 Netty缓存用 Redis数据库用 MySQL。为什么这样选Spring Boot生态成熟同一个人写的代码别人接手维护门槛低。调度、权限、监控都有现成 starter。MyBatis电网系统里 SQL 往往需要精细控制尤其是批量写入、时间分区、跨表 join。MyBatis 的 XML 里可以写完整 SQL方便 DBA 审核。Netty处理采集协议时高并发下比 Tomcat 的 BIO 可靠得多而且自带拆包粘包工具类。MySQL大多数地市级电网系统这个数据量几十万测点秒级采集MySQL 配合分区表完全扛得住。不必一上来就上 Oracle 或时序数据库。如果你做省级以上主网调度建议把历史库换成时序数据库ClickHouse、TDengine 都行但核心业务逻辑不用变。源码工程里存储层留着接口就是为了以后替换。建表时用了KEY idx_point_time实际跑起来我发现还是不够按时段查几百万行会变慢。所以工程里加了按月分表方案。常见做法是以日期后缀生成表名例如t_telemetry_202403查询时先算表名再拼 SQL。要注意防止 SQL 注入表名不能直接拼接字符串必须走白名单校验。另外t_device和t_measure_point的关联但业务编号都是字符串后来统一用 BIGINT 代理主键业务编号单列加唯一索引join 性能稳定很多。3. 从零搭建工程Maven 模块划分与 Spring Boot 主工程配置标题里有“源码”那读者最想看的就是拿到代码后怎么把它跑起来。这个工程我按 Maven 多模块拆每个模块都能独立编译真正做到按需部署。3.1 顶层 pom.xml 怎么拆模块我习惯把项目拆成五个模块smart-grid-common常量、工具类、统一异常。smart-grid-daoMyBatis 实体、Mapper、XML。smart-grid-service业务逻辑。smart-grid-collectorNetty 采集服务。smart-grid-webREST 接口、权限、定时任务。父工程的 pom.xml 如下project modelVersion4.0.0/modelVersion groupIdcom.energy/groupId artifactIdsmart-grid/artifactId version1.0.0/version packagingpom/packaging parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent modules modulesmart-grid-common/module modulesmart-grid-dao/module modulesmart-grid-service/module modulesmart-grid-collector/module modulesmart-grid-web/module /modules properties java.version11/java.version maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target /properties dependencyManagement dependencies dependency groupIdcom.energy/groupId artifactIdsmart-grid-common/artifactId version${project.version}/version /dependency /dependencies /dependencyManagement /project注意这里把 Spring Boot 的依赖交给spring-boot-starter-parent管理子模块不必重复写版本号。每个子模块的 pom 里引入spring-boot-starter-web或spring-boot-starter再加上smart-grid-common。模块间依赖方向是单向的web 依赖 serviceservice 依赖 daodao 依赖 common。谁都不允许反过来引。这样做的好处是后期拆微服务时只需要把 web 和 collector 单独打包。3.2 application.yml 里必配的参数连接池、线程池、超时时间采集服务和 web 服务可能需要各自的配置但核心的数据库连接池和 MyBatis 配置是一样的。下面是我常用的配置文件片段spring: datasource: url: jdbc:mysql://192.168.10.20:3306/smart_grid?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: grid_rw password: ******** hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 max-lifetime: 1800000 data: redis: host: 192.168.10.21 port: 6379 password: ******** timeout: 5000 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true call-setters-on-nulls: true grid: collector: netty-port: 2404 boss-threads: 2 worker-threads: 8 max-frame-length: 1024 all-idle-time-seconds: 30 task: pool-size: 8 queue-capacity: 1024重点说几个必须调的参数hikari.maximum-pool-size默认 10 可能不够。电网采集高峰时批量插入和查询会复用连接我一般配 2050。不是越多越好连接数超过数据库 max_connections 会导致应用启动失败。mybatis.map-underscore-to-camel-case必须开否则数据库的device_code映射不到deviceCode要么全 null要么报错。grid.collector.all-idle-time-seconds设 30 秒如果设备连接 30 秒没收到任何帧主动断开重连。太短会误伤慢设备太长会导致死连接堆积。另外boss-threads和worker-threads是 Netty 的线程模型Boss 线程一般 12 个足够Worker 根据 CPU 核数设 2 倍或 4 倍。不要盲目设大线程切换会吃掉性能。3.3 启动类与 MyBatis 扫描路径的一个易错点多模块工程最容易翻车的就是 MyBatis 的 Mapper 接口扫不到。我给出启动类package com.energy.web; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication(scanBasePackages com.energy) MapperScan(basePackages com.energy.dao.mapper) public class SmartGridWebApplication { public static void main(String[] args) { SpringApplication.run(SmartGridWebApplication.class, args); } }注意scanBasePackages必须写成com.energy不然 smart-grid-collector 里的 Netty 服务不会装配。MapperScan指定的是 Mapper 接口所在的包如果配置写成classpath:mapper/*.xmlXML 文件也要放在对应模块的 resources/mapper 下。曾经有一次我漏了MapperScan结果 dao 模块的接口全部报了 Invalid bound statement。那类错误最气人因为编译时一切正常一启动就黑屏。所以我把这条写进项目的 README后来的人再没踩过。4. 核心功能实现采集入库、状态推送与算法计算有了工程骨架接下来就是往里填最关键的代码。我会按“采集 - 解析 - 入库 - 计算”这条链路讲代码能直接放到源码工程的对应模块里。4.1 遥测数据接收接口Netty 解码器与自定义协议工业设备上报往往用私有协议或标准规约比如常见的帧格式可能是起始字 0x68 报文长度 控制域 地址域 用户数据 校验和 结束字 0x16Netty 的解码器负责把字节流切成一帧一帧。下面是一个简单的帧解码器示例package com.energy.collector.codec; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; public class GridFrameDecoder extends ByteToMessageDecoder { private static final byte FRAME_HEAD 0x68; private static final byte FRAME_END 0x16; private static final int MAX_LENGTH 1024; Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, ListObject out) { if (in.readableBytes() 2) { return; } in.markReaderIndex(); byte head in.readByte(); if (head ! FRAME_HEAD) { in.resetReaderIndex(); in.readByte(); return; } if (!in.isReadable()) { in.resetReaderIndex(); return; } int length in.readUnsignedByte(); if (length 0 || length MAX_LENGTH) { return; } if (in.readableBytes() length) { in.resetReaderIndex(); return; } ByteBuf frame in.readRetainedSlice(length); if (frame.getByte(frame.readableBytes() - 1) ! FRAME_END) { frame.release(); in.resetReaderIndex(); in.readByte(); return; } byte[] data new byte[frame.readableBytes()]; frame.readBytes(data); frame.release(); out.add(data); } }逻辑说明解码器先用markReaderIndex记住读指针如果数据不够或帧不完整就resetReaderIndex等待下一轮。长度字段是单字节所以最大帧长不能超过 255否则要换双字节长度。这里的0x68和0x16只是示例实际规约的起始字和结束字可能不同但思路一样。参数MAX_LENGTH要按你的最大帧长调整过大容易把错误数据收入内存过小会误杀合法帧。拿到完整帧后下一步是把字节解析成测点对象。这一步常见做法是用ByteBuffer.wrap()加自定义解析方法。采集模块不要在这里做业务校验只负责拆包和转对象保证高吞吐。4.2 用 MyBatis 批量写入遥测数据的写法电网数据秒级采集如果一条条 insert性能会很难看。源码工程里我优先使用 MyBatis 的批量 insert。Mapper 接口方法public interface TelemetryMapper { int batchInsert(Param(list) ListTelemetryEntity list); }XML 里的写法insert idbatchInsert parameterTypelist INSERT INTO t_telemetry (point_id, value, quality, collect_time, receive_time) VALUES foreach collectionlist itemitem separator, (#{item.pointId}, #{item.value}, #{item.quality}, #{item.collectTime}, #{item.receiveTime}) /foreach /insert调用端要注意分批不能一次塞几万条进去public void saveTelemetry(ListTelemetryEntity data) { int batchSize 500; for (int i 0; i data.size(); i batchSize) { int end Math.min(i batchSize, data.size()); telemetryMapper.batchInsert(new ArrayList(data.subList(i, end))); } }为什么 batchSize 设 500这是我多次压测后折中的结果。小于 200 时网络往返太多大于 1000 时 MySQL 的 rewriteBatchedStatements 开关没打开时反而慢而且生成 SQL 过长容易超过 max_allowed_packet。如果表上有多个索引一次性插入 500 行对行锁的占用也比较温和。另外collect_time和receive_time一定要用DATETIME(3)或TIMESTAMP(3)用于记录毫秒否则高频数据点在同一个秒内会出现时间倒置。4.3 简单负荷预测滑动平均与指数平滑的Java实现采集数据落库后接下来就是计算。负荷预测是智能电网信息系统里最容易体现计算价值的功能。我用最经典的指数平滑方法做短期预测实现简单、可解释性强。public class ExponentialSmoothing { private final double alpha; private Double lastForecast; public ExponentialSmoothing(double alpha) { this.alpha alpha; // 平滑系数0~1 } public double next(double observed) { if (lastForecast null) { lastForecast observed; } else { lastForecast alpha * observed (1 - alpha) * lastForecast; } return lastForecast; } }逻辑说明alpha越大越偏向最近观测值曲线抖动越明显alpha越小历史权重越高曲线越平滑。电网负荷通常有周期性和突发性我一般把alpha设在 0.30.5 之间并用过去 5 分钟的数据做平滑预测未来 5 分钟的值。注意这个类只是单点预测真实工程中要按测点维护多个平滑器最好用缓存存储。除了指数平滑更常用的是滑动平均维护一个固定窗口取均值。窗口大小N我建议设 10因为对 5 秒一颗点的数据来说10 个点正好是半分钟能滤掉瞬时噪声又不至于太滞后。这两种算法都可以下放到采集模块的计算层在内存里算完再写库减少数据库压力。5. 常见问题与踩坑智能电网系统调试里最伤人的 5 个 Bug这部分内容完全来自真实调试中的血泪经验。每一条都是我自己或同事亲手踩过的现象和原因都对得上。5.1 现象数据库里的时间戳全部变成 1970-01-01现象采集上来的遥测数据入库后collect_time全是 1970 年或者时间比北京时间差 8 小时。原因设备上报的时间很多是 Unix 秒或毫秒Java 端SimpleDateFormat使用时区用了默认 UTC或者 MySQL 连接串里的serverTimezone没设成Asia/Shanghai。解决在数据库连接 URL 上明确写serverTimezoneAsia/Shanghai并在解析设备时间戳时统一用ZoneId.of(Asia/Shanghai)。源码工程里我把时间解析工具单独封装所有入口都必须走它禁止直接new Date()。5.2 现象大批量插入时内存和 GC 飙升现象高峰期批量插入 5 万条遥测数据GC 时间飙到几秒应用程序卡顿。原因一次性把全部数据放进一个ArrayList再 MyBatis 链成一条巨型 SQL同时连接池连接不够等待线程积压。解决分批插入每批 500 条连接池maximum-pool-size调到 20foreach里使用subList切割。还要检查数据库max_allowed_packet必要时调到 64M。5.3 现象越限告警发个不停疑似告警风暴现象某条母线电压越限后系统每秒钟发一次告警短信平台被刷爆。原因告警逻辑写成了“值越限就发告警”没有做恢复和去抖处理。同一个测点在越限期间持续触发。解决引入状态机每个测点有 normal / alarm / ack 三种状态越限后进入 alarm 状态并写入一条新告警后续除非值恢复或确认否则不再重复写。实现时用 Redis 存状态TTL 设 5 分钟防止状态丢失。5.4 现象两个厂站的数据串了显示 A 站的功率却来自 B 站现象设备档案里device_code是唯一索引但采集报文里只有站内地址导致映射错位。原因我一开始只依赖protocol_type和address匹配设备而不同厂商的站内地址范围一样比如都用0x01表示主变高压侧。解决改为“链路地址 站内地址 数据地址”三元组定位测点。在解码器里先完整解析地址字段再查测点表。测点表加联合唯一索引(station_code, link_address, data_address)。5.5 现象远程控制命令发下去了现场开关纹丝不动现象调度员遥控断路器界面上显示下发成功但设备没有动作。原因命令报文缺少重传和 ACK 机制。TCP 只保证了报文送达但设备端可能校验失败没有回应。解决遥控流程加“下发—等待确认—超时重发—回报归档”四步。我用一个CommandPendingQueue保存待确认命令收到确认帧后移除超时 5 秒重发最多重发 3 次并记录操作日志。另外遥控命令必须在独立的连接上发送不能和遥测数据共用同一个编解码通道否则会被高频遥测帧堵住。这五条都是非常典型的电网特性问题其他的比如数据库连接泄露也很常见但在通用业务里也能看到这里就不展开。6. 验证与扩展从单机联调到多节点协同的进阶路径源码工程能跑起来只是第一步真正上线要看它扛不扛得住。我习惯先用模拟器把链路打通再做压测最后才谈分布式扩展。6.1 用模拟器灌数据验证系统电网现场不好随便接真设备所以源码工程带了一个模拟终端脚本。它按测点表里的测点编号定时组帧发送到采集端。Java 里可以用Socket或者 Netty 客户端写一个简单模拟器每秒钟向采集端口发送几十帧。验证步骤是启动 smart-grid-web、smart-grid-collector检查 SQL 里是否有新数据入库。用curl调一个查询接口读最新一条遥测值。手工修改模拟器的值确认越限告警能触发且不会刷屏。6.2 压测指标与参数调整压测时我关注三个指标吞吐量帧/秒、入库延迟收到报文到库可见、命令耗时下发到回执。用wrk或JMeter打 REST 接口不够还要直接用 TCP 模拟终端连采集端口。如果发现吞吐上不去先看 Netty worker 线程数量和数据库连接池再看批量 SQL 的 rewriteBatchedStatements 有没有打开。这个参数在 MySQL JDBC URL 里加一行rewriteBatchedStatementstrue批量插入性能能提升一个数量级很多人在这一步卡住。6.3 走向分布式多智能体协同的电网可靠运行单体工程验证完之后如果要扩大规模我会把采集节点和计算节点拆开形成多智能体协同的分布式架构。每个厂站放一个采集节点本地做死区过滤、越限判断只把变化值和告警事件上报中心中心再做全网分析。这个思路和多智能体协同的电网可靠运行是一脉相承的本质是把计算推到数据边上降低核心链路压力。当然分布式涉及一致性、消息队列、注册中心代码量会翻倍。我从实践得到的教训是不要一上来就上微服务先把单体跑好用模拟器压出瓶颈再针对性拆。很多项目死在架构过度设计上。我习惯把每一步验证结果写进项目的 docs 文件夹后面扩展时少走弯路。跑稳定的系统最怕的不是技术难而是没人知道它曾经的边界。这就是我坚持在源码注释里写参数依据的原因。希望这些踩坑记录能帮到你。本文还有配套的精品资源点击获取