简介:面向电力行业设备管理与信息化开发场景的参考型源码包,适合计算机、电气自动化相关专业学生及具备一定 C++/MFC 基础的开发者,用于理解设备全生命周期管理系统的模块划分与实现思路。内容围绕设备资产、状态监测、维护检修计划、工单流转、备件库存、报表分析、安全权限及供应商合同等业务展开,可辅助课程设计、毕业设计或企业原型参考。压缩包共 65 个文件,约 1.47MB,以 h 头文件、cpp 源文件为主,辅以 bmp/ico 界面位图与图标、txt 说明、rc 资源脚本及工程配置和数据库文件,能够较完整地呈现一个桌面端管理系统的工程结构。已有 226 人学习,便于读者从主框架、视图、数据库记录集、表格控件、登录验证等实现中梳理业务与界面组织方式,也可作为电力设备管理功能拆解和二次开发的参考资料。
1. 从一张填了六年的巡检表说起:电力设备管理系统能替你干哪些活
某开发者在运维部门见过一张 Excel,六年来每次设备巡检都往里填,配电柜、变压器、开关柜、避雷器的编号、上次巡检日期、缺陷描述全挤在几十列里,谁改过、改了什么完全看不出来,年底要做设备健康度分析时得先花两天清洗数据。这个电力设备管理系统参考包,干的就是把这套流程搬到线上:设备台账建档、巡检计划按周期自动生成任务、巡检出缺陷自动转告警、告警闭环留痕、按设备维度出统计。它适合两类人,一类是接手了现场设备管理需求、要从零搭一套后端系统的开发者,另一类是手里有一堆台账数据、想先跑个能演示的原型出来对齐需求的实施同学。系统本身不复杂,但设备对象、周期计划、状态流转这几块该有的骨架都留出来了,拿来当二次开发的地基比较省事。
2. 项目全貌与技术选型:三层结构怎么切、数据往哪存
2.1 系统边界:从设备台账到缺陷闭环的五件事
先把功能范围框死,这不是一个要做成大而全的资产系统,核心就盯着“设备—计划—任务—缺陷—告警”这条主线。设备台账保存每台设备的基本属性和位置信息,是后面所有业务的挂载点;巡检计划描述“哪些设备、多久一次、由谁负责”;定时器按计划滚动生成当天的巡检任务;巡检员提交表单后如果勾了缺陷,系统自动建一条缺陷记录并按等级决定要不要转成告警;告警有未处理、处理中、已关闭三个状态,关掉之前必须填处理结论。这条链路跑通,一个地市级单位的小型设备管理场景就够用了,剩下的备品备件、检修工单、报表中心都是在这条主线上横向扩展出来的。
反过来说,哪些东西这个参考包故意没做深,也要清楚。权限模型只做到角色菜单这一层,没有做数据权限到部门的下钻;设备台账的字段扩展没有做成动态表单,新增一种设备类型需要改代码;告警通知只留了站内消息,没有对接短信网关和第三方推送。这些不是缺陷,是留白,因为真正落地时每家单位的要求都不一样,写死了反而不好改。你拿到手之后第一件事应该是拿自己的需求清单去对这条主线,看缺口有多大,再决定是改造它还是另起炉灶。
判断一个设备管理系统好不好用,一个朴素的指标是:现场人员能不能在三步之内完成一次巡检提交。这个项目的表单设计基本贴着这个目标走,扫码进来带出设备编号,勾选正常或异常,异常再选缺陷等级加点照片,提交就结束。复杂表单在现场是灾难,尤其是防爆区里戴着手套操作手机的时候。
2.2 技术栈与分层:为什么选 Spring Boot + MyBatis-Plus + Vue
后端上的是 Spring Boot 打底,配 MyBatis-Plus 做持久层,MySQL 存主数据,Redis 放登录态和当天任务的防重复标记,定时调度用 Spring 自带的@Scheduled加一个分布式锁兜底。前端是 Vue 配 Element 系组件库,页面结构走经典的后台管理布局:左侧菜单、顶部面包屑、主体是查询条件区加数据表格加分页。
选 MyBatis-Plus 而不是原生 MyBatis,主要是看中两件事:一是BaseMapper把 90% 的单表增删改查都省了,这个系统里设备台账、巡检记录、缺陷列表全是标准的单表操作;二是逻辑删除和自动填充字段这种横切逻辑配一次全局生效,不用每个 XML 里都写一遍。代价是复杂关联查询还是得乖乖写 XML,比如“查某人名下所有超期未完成的巡检任务”这种带多表联结和聚合条件的,别指望条件构造器能拼得优雅。
提示:如果你打算把 MyBatis-Plus 升级到更高的大版本,条件构造器的部分方法签名有变动,尤其
selectPage和LambdaQueryWrapper的链式调用顺序,升级前先跑一遍巡检列表和缺陷列表两个查询。
分层是标准的 controller → service → mapper 三层,中间夹了一个convert包做 DO 和 VO 的互转。这个convert层很容易被新手忽略,但它是有价值的:接口返回给前端的字段和数据库字段从来不是一一对应的,比如设备台账里存的是status数字,前端要显示成“运行/停役/检修”的中文标签,这个映射放在转换层比塞进 SQL 里干净得多。我一般会把这类枚举映射集中到一个DeviceStatusEnum里,转换层只负责调用,避免同一种状态在三个地方各写一份 if-else。
2.3 目录结构:前后端的文件落点
目录划分按职责切,不按技术名词堆。后端大致是这样:
pe-server/ ├── pe-common/ 公共模块:统一返回体、异常、工具类 │ └── src/main/java/.../common/ │ ├── result/ R<T> 统一返回结构 │ ├── exception/ BizException + 全局异常处理 │ └── util/ 日期、编号生成 ├── pe-admin/ 启动模块 + 接口层 │ └── src/main/java/.../admin/ │ ├── controller/ EquipmentController、TaskController、AlarmController │ ├── config/ MybatisPlusConfig、RedisConfig、WebMvcConfig │ └── PeApplication.java ├── pe-system/ 业务模块 │ └── src/main/java/.../system/ │ ├── domain/ Equipment、InspectionTask、Alarm 等 DO │ ├── mapper/ 对应 Mapper 接口 │ ├── service/ 业务实现 │ ├── convert/ DO ↔ VO 转换 │ └── schedule/ 定时任务入口 └── pe-server.sql 建库脚本 + 初始化数据前端是一个标准的 Vue 工程,src/api下按模块拆接口文件,src/views下按业务拆页面,src/utils/request.js里封装了请求拦截和错误提示。这个结构没什么花活,好处是你找任何一个功能的代码路径都是确定的:想知道设备列表怎么查,从页面views/equipment/index.vue找到api/equipment.js里的方法名,再去后端EquipmentController里搜同名方法,三跳之内能定位。
我一般建议接手一个新项目时先看两样东西:一是application.yml,所有环境相关的秘密都在里面;二是建库脚本,表结构能告诉你作者对业务的理解到什么深度。这个项目的建库脚本里设备表有install_date、commission_date、rated_voltage、rated_current这些字段,说明作者确实接触过真实的台账,不是凭空捏的表。
3. 核心模块拆解:设备台账、巡检任务与告警的完整链路
3.1 台账表结构:字段设计决定后面能改多深
设备台账是整个系统的基础数据表,它的字段设计直接决定了后面巡检、缺陷、报表能做到什么程度。核心看下面几个字段:
CREATE TABLE `eq_equipment` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `eq_code` VARCHAR(32) NOT NULL COMMENT '设备编号,现场贴码,全局唯一', `eq_name` VARCHAR(64) NOT NULL COMMENT '设备名称', `eq_type` VARCHAR(32) NOT NULL COMMENT '设备类型:TRANSFORMER/SWITCHGEAR/ARRESTER', `location_id` BIGINT NOT NULL COMMENT '所属位置(站房/楼层)', `rated_voltage` VARCHAR(32) DEFAULT NULL COMMENT '额定电压,带单位字符串,如 10kV', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1运行 2停役 3检修 4报废', `install_date` DATE DEFAULT NULL COMMENT '投运日期', `next_check_at` DATETIME DEFAULT NULL COMMENT '下次巡检时间,由计划推算', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_eq_code` (`eq_code`, `deleted`), KEY `idx_next_check` (`next_check_at`) ) COMMENT '设备台账';几个地方的取舍值得说一下。eq_code上的唯一索引带上了deleted字段,因为用了逻辑删除,如果只对eq_code建唯一索引,一台设备被“删除”之后再新建同编号设备就会撞键,这是新手最容易翻车的地方之一,后面避坑章节还会展开。rated_voltage用字符串而不是数值,是因为现场电压等级的写法太杂,“10kV”“10KV”“10000V”都有人填,强行数值化反而要写一堆清洗逻辑,不如存原样加一个枚举字典做规范化展示。next_check_at单独冗余了一列并建索引,是为了让“今天该巡检哪些设备”这个高频查询能走索引,不用每次都去关联计划表算一遍。
status用TINYINT存枚举值而不是字符串,这个选择在查询效率上有优势,但要注意前端展示和接口文档的对应关系一定要维护住,我通常会在枚举类里把 code 和 label 写死,并且让 Swagger 注释引用同一个枚举,避免出现“数据库是 3、文档说 2 是检修”这种事故。
3.2 巡检任务生成:定时任务与设备类型的挂钩
任务生成是这个系统里唯一有“自动化”味道的部分,也是最容易出问题的地方。逻辑是每天凌晨扫一遍巡检计划,把当天该执行的计划实例化成具体任务,一台设备一条任务记录。先看实体和 Mapper 的定义:
@Data @TableName("eq_inspection_task") public class InspectionTask { @TableId(type = IdType.ASSIGN_ID) private Long id; private Long planId; // 来源计划 private Long equipmentId; // 目标设备 private Long inspectorId; // 负责人 private LocalDate planDate; // 计划执行日期 private Integer status; // 1待执行 2已完成 3超期 4已跳过 private LocalDateTime finishTime; @TableLogic private Integer deleted; }planDate用LocalDate而不是DATETIME,因为任务是以“天”为粒度的,带上时分秒反而会让唯一性判断变复杂。生成逻辑写在 service 里,定时器只负责触发:
@Component @RequiredArgsConstructor public class InspectionTaskJob { private final InspectionPlanService planService; private final InspectionTaskService taskService; /** 每天 00:10 生成当天任务,错开整点避免和其他任务抢资源 */ @Scheduled(cron = "0 10 0 * * ?") public void generateDailyTask() { LocalDate today = LocalDate.now(); // 1. 取出今天生效的计划(已启用、且在有效期内、周期匹配今天) List<InspectionPlan> plans = planService.listActivePlans(today); for (InspectionPlan plan : plans) { // 2. 按计划关联的设备列表展开,逐台生成任务 List<Long> eqIds = planService.listEquipmentIds(plan.getId()); for (Long eqId : eqIds) { // 3. 幂等:同一计划同一设备同一天只生成一条 if (taskService.exists(plan.getId(), eqId, today)) { continue; } taskService.createTask(plan, eqId, today); } } } }逐段说下关键点。第一步listActivePlans里要处理周期匹配,计划表上有cycle_type(按天/按周/按月)和cycle_value(比如每 7 天一次),判断“今天是否需要执行”的常见做法是拿计划的start_date和今天做天数差,对cycle_value取模为 0 才算命中,而不是简单地判断星期几,否则“每 10 天一次”这种周期就表达不了。第二步的设备列表展开,建议在计划表之外单独建一张eq_plan_equipment关联表,而不是在计划记录里存逗号拼接的设备 ID 串,后者在“查某台设备被哪些计划覆盖”时几乎没法查。
第三步的幂等判断是整个方法最要紧的一行。哪怕只有一个实例在跑,定时任务因为异常重启、手工重跑等原因也可能执行两次,没有这个判断就会生成重复任务,巡检员打开手机看到两条一模一样的待办,体验直接崩。更稳的做法是靠数据库唯一索引兜底,在eq_inspection_task上建(plan_id, equipment_id, plan_date, deleted)的唯一键,插入时捕获重复键异常当作成功处理。代码里判断加数据库约束双保险,这属于吃过亏之后的肌肉记忆。
注意:
@Scheduled默认是单线程串行执行所有定时任务,如果项目里还有别的定时任务,一个卡住会拖累全部。启动类上加@EnableScheduling之后,记得配一个ThreadPoolTaskScheduler,核心线程数给到 4 到 8,别用默认值。
3.3 巡检提交与缺陷自动转告警
巡检员提交表单时,服务层要做三件事:写巡检记录、更新任务状态、判断是否要建缺陷和告警。这三步必须在同一个事务里,否则会出现“任务已完成但没有巡检记录”的脏数据。
@Service @RequiredArgsConstructor public class InspectionRecordService { private final InspectionRecordMapper recordMapper; private final InspectionTaskMapper taskMapper; private final DefectService defectService; @Transactional(rollbackFor = Exception.class) public void submit(RecordSubmitDTO dto) { // 1. 校验任务状态,防止重复提交(并发下靠 update ... where status=1 兜底) InspectionTask task = taskMapper.selectById(dto.getTaskId()); if (task == null || task.getStatus() != TaskStatus.PENDING.getCode()) { throw new BizException("任务不存在或已处理"); } // 2. 落巡检记录 InspectionRecord record = new InspectionRecord(); record.setTaskId(dto.getTaskId()); record.setEquipmentId(task.getEquipmentId()); record.setResult(dto.getResult()); // 1正常 2异常 record.setRemark(dto.getRemark()); record.setCheckTime(LocalDateTime.now()); recordMapper.insert(record); // 3. 乐观更新任务状态,影响行数为 0 说明被别的请求抢先处理了 int rows = taskMapper.finishTask(dto.getTaskId(), record.getCheckTime()); if (rows == 0) { throw new BizException("任务已被处理,请刷新"); } // 4. 异常结果自动建缺陷,缺陷等级决定是否升格为告警 if (dto.getResult() == 2) { defectService.createFromRecord(record, dto.getDefectLevel()); } } }第二步的乐观更新是防重复提交的关键,对应的 SQL 是UPDATE eq_inspection_task SET status = 2, finish_time = #{time} WHERE id = #{id} AND status = 1,把状态判断下推到数据库,靠行锁保证原子性。如果只靠第一步的select判断,两个请求同时进来都会读到status = 1,都能往下走。
第四步的缺陷转告警,我建议把规则做成可配置的而不是写死在 if 里。常见做法是给缺陷等级定义三个档:一般、严重、危急,只有严重和危急才生成告警记录并推送站内消息,一般缺陷只进缺陷池等定期处理。这套阈值放在配置表里,运维人员自己就能调,不用每次都找开发改代码重新发版。
3.4 接口层与前端调用
控制层没什么花样,保持薄,只做参数校验和结果包装:
@RestController @RequestMapping("/api/equipment") @RequiredArgsConstructor public class EquipmentController { private final EquipmentService equipmentService; /** 分页查询设备台账,支持按类型、状态、关键字过滤 */ @GetMapping("/page") public R<PageResult<EquipmentVO>> page(EquipmentQuery query) { return R.ok(equipmentService.page(query)); } /** 扫码后按设备编号查详情,现场高频调用,加了缓存 */ @GetMapping("/code/{eqCode}") public R<EquipmentVO> getByCode(@PathVariable String eqCode) { return R.ok(equipmentService.getByCodeWithCache(eqCode)); } }前端那边接口文件保持和 controller 一一对应,请求封装里统一处理 token 和错误码:
// src/api/equipment.js import request from '@/utils/request' // 设备分页列表,参数对象直接透传给后端 Query 对象 export function pageEquipment(params) { return request({ url: '/api/equipment/page', method: 'get', params }) } // 扫码查询,结果做 5 分钟本地缓存,避免重复扫同一台设备时打接口 export function getByCode(eqCode) { return request({ url: `/api/equipment/code/${eqCode}`, method: 'get' }) }注意R<PageResult<EquipmentVO>>这层包装,不要图省事直接返回 MyBatis-Plus 的IPage对象。IPage里带了orders、optimizeCountSql这些前端根本用不上的字段,而且一旦框架升级,序列化结构可能变,前端就跟着崩。用自己定义的PageResult只暴露list、total、pageNum、pageSize四个字段,接口契约就稳了。
4. 跑起来:从空库到登录首页的完整步骤
4.1 环境准备:JDK、MySQL、Redis、Node
动手之前把版本对齐,这类管理系统的依赖不算刁钻,但版本错配照样起不来。JDK 建议 8 或 11,Spring Boot 2.x 系列两个都吃得下;MySQL 用 5.7 或 8.0 都行,注意 8.0 的连接串要加时区和useSSL参数;Redis 起一个单机实例够用,主要拿来存登录态和任务防重标记;Node 建议 16 以上,太老的版本装依赖会卡在node-sass编译上。
# 先确认基础环境都在,缺哪个补哪个 java -version # 期望 1.8 或 11 mysql --version # 期望 5.7 / 8.0 redis-cli ping # 期望返回 PONG node -v # 期望 v16+如果机器上还没装 MySQL,用容器起一个最省事:
docker run -d --name pe-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e MYSQL_DATABASE=pe_server \ mysql:8.0 --default-authentication-plugin=mysql_native_password--default-authentication-plugin=mysql_native_password这个参数别省,MySQL 8 默认的caching_sha2_password认证方式老版本的 JDBC 驱动不认,会报Public Key Retrieval is not allowed,第一次配的时候这个报错能卡人半小时。
4.2 建库与配置:三个必须改的地方
建库脚本一般在仓库根目录,导入之前先看一眼字符集,utf8mb4是必须的,设备名称里出现生僻字或者备注里带表情符号时,utf8会截断报错。
# 建库 + 导入结构和初始数据 mysql -uroot -p123456 -e "CREATE DATABASE IF NOT EXISTS pe_server DEFAULT CHARSET utf8mb4;" mysql -uroot -p123456 pe_server < pe-server.sql配置文件里必须动手改的是三块:数据源、Redis、文件上传路径。数据源除了地址账号密码,serverTimezone一定要显式指定为Asia/Shanghai,否则时间字段写进去和读出来能差 8 小时,这个坑在避坑章节细说。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/pe_server?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root123 redis: host: 127.0.0.1 port: 6379 database: 0 servlet: multipart: max-file-size: 10MB # 巡检照片单张上限,按现场手机像素调 pe: upload: path: /data/pe/upload # 照片落盘目录,Linux 下记得给写权限第三块是文件上传目录,Windows 上写成D:/pe/upload就行,部署到 Linux 服务器上要确认运行用户对该目录有写权限,不然后台上传巡检照片会静默失败——接口返回成功但文件没落盘,因为异常被吞在了 IO 流关闭的地方。
4.3 启动后端与接口自测
后端是标准的 Spring Boot 入口,用 Maven 打包或直接在 IDE 里跑都行。第一次建议 IDE 里跑,报错看得清楚。
# 打包跳过测试,第一次跑先保证能起来 mvn clean package -DskipTests # 运行,指定生产之外的配置文件 java -jar pe-admin/target/pe-admin.jar --spring.profiles.active=dev看到日志里打印出 Tomcat 监听端口和启动耗时,基本就成了。接着拿一个只读接口验证链路通不通,登录接口一般不需要鉴权,设备列表接口需要带 token,所以先用登录接口拿 token:
# 1. 登录拿 token,初始账号一般在初始化 SQL 的 sys_user 表里 curl -X POST http://127.0.0.1:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}' # 2. 拿返回的 token 调设备分页接口 curl "http://127.0.0.1:8080/api/equipment/page?pageNum=1&pageSize=10" \ -H "Authorization: Bearer <上一步返回的token>"如果第二步返回 401,先别急着怀疑代码,八成是拦截器里 token 的解析前缀和登录接口返回的格式对不上,有的项目返回裸 token,有的返回Bearer xxx,两边对一下就知道。
4.4 前端联调与打包
前端进目录装依赖,注意package-lock.json有没有提交,有的话用npm ci比npm install稳,不会因为依赖小版本漂移导致构建出不一样的产物。
cd pe-web npm ci # 有 lock 文件时优先用它 npm run serve # 本地开发,默认 8081 之类,看 vue.config.js npm run build # 产物在 dist 目录开发环境下vue.config.js里的proxy会把/api转发到后端,所以本地不用配跨域。打包部署到 Nginx 之后,要配try_files $uri $uri/ /index.html做前端路由回退,同时把/api反向代理到后端端口,这两条配置缺一条都会让页面白屏或者接口 404。
5. 避坑与常见问题排查:这几个坑我踩过
5.1 集群部署后巡检单每天生成两份
现象是巡检员手机上同一个设备出现两条内容一样的待办任务,只在双机部署的环境里出现,单机测试怎么都复现不了。
原因很直接,@Scheduled在每个实例上都会触发,两台机器同时扫计划、同时插入,幂等判断在并发窗口里失效了,两个请求都查到“不存在”然后各插一条。
解决方法是加分布式锁,锁的粒度可以是全局的(保证同一时刻只有一个实例在执行任务生成),也可以是按计划维度。全局锁用 Redis 的SET key value NX PX 300000实现最省事:
public void generateDailyTask() { String lockKey = "pe:job:inspection:generate:" + LocalDate.now(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofMinutes(10)); if (Boolean.FALSE.equals(locked)) { return; // 别的实例已经在跑了,直接退出 } try { doGenerate(); } finally { // 不要在这里删锁,让它自然过期,避免任务没跑完锁就被释放 } }锁的过期时间给足,任务量大的时候生成过程可能跑几分钟,锁提前过期等于没加。数据库唯一索引依然建议保留,作为最后一道防线。
5.2 时间字段查出来差 8 小时
现象是数据库里存的是2024-05-20 09:00:00,接口返回给前端变成了2024-05-20 17:00:00,或者反过来。
原因有两处可能,一是 JDBC 连接串没写serverTimezone=Asia/Shanghai,驱动按 UTC 解析;二是实体类里用了java.util.Date配合@JsonFormat却没指定时区,序列化时按 JVM 默认时区转了一遍。
解决方法是统一标准:连接串显式写时区,所有时间字段改用java.time.LocalDateTime,全局配一个 Jackson 的时间序列化器指定GMT+8,三个地方对齐之后就不会再漂。改完拿一条新增数据从写库到接口返回走一遍,比对三个环节的时间值,别只看数据库。
5.3 逻辑删除和唯一索引打架
现象是删掉一台设备之后想重新建档同编号的设备,插入直接报唯一键冲突。
原因是唯一索引建在eq_code单列上,逻辑删除只是把deleted置 1,记录还在表里,唯一约束依然生效。
解决方法是把唯一索引改成(eq_code, deleted)联合索引。但这里还有个小坑,如果同一编号被删两次,两条记录的deleted都是 1,又撞了。更稳的做法是把deleted设计成存删除时间戳或者主键 ID 的字段,删除时写入当前时间,这样每次删除的值都不同,唯一索引天然不冲突。改索引的时候记得先清一遍历史脏数据,不然加索引那一步就会失败。
5.4 打包后刷新页面 404,接口全部跨域
现象是本地开发一切正常,丢到 Nginx 上之后,首页能打开,点进二级页面一刷新就 404,接口请求控制台一片红色跨域报错。
原因分别是:前端用的是 history 路由模式,Nginx 找不到对应路径的文件就直接返回 404;跨域报错是因为 Nginx 没有配置/api的反向代理,请求打到了前端静态资源服务器的根路径上。
解决方法是 Nginx 配置里加两段,一段是location / { try_files $uri $uri/ /index.html; }处理路由回退,另一段是location /api/ { proxy_pass http://127.0.0.1:8080/api/; }把接口转给后端。反向代理配好之后,前端代码里的接口 baseURL 要改成相对路径,别写死http://ip:端口,否则换环境又得改一遍代码重新打包。
5.5 导出设备台账把服务拖挂
现象是运维点了“导出全部设备”,页面转圈几十秒之后服务无响应,日志里一堆OutOfMemoryError。
原因是导出方法用selectList把几万条记录一次性捞进内存,再交给 POI 生成 Excel,堆内存直接爆掉。
解决方法是分批查、分批写。用 MyBatis-Plus 的分页或者游标查询,每批 1000 条,写出用 EasyExcel 的SXSSFWorkbook模式(或者 EasyExcel 自带的流式写出),数据边读边写临时文件,内存占用能压到几十兆以内。导出这种功能不要贪图代码简短,一次性查完看着清爽,上线就是事故。
6. 二次开发:加一种设备类型要动哪些文件
拿到这套代码之后最常见的改造需求就是扩设备类型。假设现场新增了环网柜这一类,改动路径是可以列清楚的,按下面这张表走一遍基本不会漏:
| 改动位置 | 具体文件 | 要做的事 |
|---|---|---|
| 枚举字典 | DeviceTypeEnum | 新增RING_MAIN_UNIT及中文标签 |
| 数据库 | sys_dict表 | 补一条类型字典数据,前端下拉框才有选项 |
| 台账表单 | views/equipment/form.vue | 若该类设备有专属字段,加条件渲染区块 |
| 计划生成 | InspectionPlanService | 若巡检周期与类型相关,在周期计算里加分支 |
| 缺陷规则 | DefectRuleConfig | 配置该类设备的缺陷等级映射 |
如果专属字段不多,不建议给台账表加列,改成在eq_equipment上加一个ext_json字段存扩展属性更灵活。代价是查询和统计时要解析 JSON,MySQL 5.7 以上支持 JSON 函数,简单的等值查询还能走函数索引,复杂聚合就不太行了。字段超过十个再考虑拆子表,别一上来就搞 EAV 模型,那玩意儿查起来能让人怀疑人生。
第二个改造点是告警阈值别写死。项目里判断设备是否超期未巡检用的是固定的 7 天,实际现场不同设备差别很大,变压器可能 30 天一次,避雷器要 90 天。建议把阈值抽到eq_threshold配置表,按设备类型加优先级的维度存,服务层查配置时按“设备专属 > 类型默认 > 全局默认”三级回退。这样运维自己就能调,不用每次改代码。写这段逻辑时注意加缓存,配置表读得频繁但改得少,本地缓存加个 5 分钟过期足够。
验证改动是否生效,我习惯按三步走:先在eq_inspection_task里手工插一条明天到期的任务,把系统时间调一下或者直接改plan_date,跑一次定时任务的方法(可以用单元测试直接调 service,不用等 cron),看生成结果对不对;再走一遍完整的巡检提交接口,检查缺陷和告警有没有按新规则生成;最后打开统计页面看数据是否一致。这三步都过,改动基本就算稳了。
说个自己的教训。早些年改这类周期计算,我图快只测了“每 7 天”这一种情况,上线之后现场反馈“每月 1 号”的计划从来不发任务,排查半天发现是取模的分母算错了,跨月的天数差算出来是负数。从那以后我每次动周期、阈值、状态流转这类逻辑,都强制自己把边界值列出来跑一遍:第一天、最后一天、闰年 2 月、跨年,四个场景一个不落,全部通过才提交。多花二十分钟,省掉一次半夜被叫起来的时间,太值了。希望这套东西到你手里能少走点弯路。
本文还有配套的精品资源,点击获取