基于SSM+VUE的楼宇电能综合管控系统设计与实践解析
简介一份基于SSM与Vue.js的楼宇电能综合管控系统毕业设计论文文档面向计算机科学与技术相关专业本科毕业生、需要完成类似选题的开发者。文档以“需求分析—技术方案—系统架构—前后端实现—测试评价”为主线完整覆盖楼宇能源监控、能耗统计、异常报警与节能策略等核心模块全文约万字并已完成降重处理出自本科毕业论文场景。压缩包内共1个docx文件包体仅29KB包含中英文摘要、完整目录、章节正文及参考文献框架适合直接作为论文模板或设计参考。已有153人学习浏览。文档重点展示了SSMSpring、SpringMVC、MyBatis后端与Vue.js前端配合的工程化思路并给出数据库表结构、RESTful API接口设计、功能测试与性能测试要点对理解前后端分离开发、快速搭建同类能源管理系统具有实用价值。 我在这套系统上前后花了三个多月从需求梳理到数据库设计再到前后端联调踩过的坑比写过的代码还多。最近整理文档的时候翻到最终交付的那版“基于SSMVUE框架的楼宇电能综合管控系统的设计及实现”想着把整个项目的设计思路、核心代码、还有那些文档里不会写的坑都梳理一遍给后面要做同类系统的朋友做个参考。这套系统本质上解决的是大型楼宇“电费说不清、能耗看不见、异常发现慢”的问题。物业管理人员只能看到每个月总电表的一个总数至于哪层楼耗电最高、哪个时段是峰值、哪台设备在偷偷跑电基本靠猜。所以系统的核心目标就是三件事把每一块电表的数据实时采集上来、用可视化方式呈现给管理者、在异常时及时预警。适合来看这篇文章的人我猜大概率是这两类一类是准备做SSM或者VUE相关的毕业设计、课程项目的学生另一类是公司里要搭内部能耗管理平台、需要一个完整参考方案的开发人员。这篇内容我会尽量把架构选型、表结构设计、前后端关键实现、以及联调阶段最折磨人的几个问题都讲透。1. 项目整体设计与架构思路1.1 系统定位与核心需求拆解楼宇电能综合管控系统说白了就是一个给楼宇管理者用的“用电驾驶舱”。我在做需求分析的时候把用户划分为三类角色每一类关注的维度完全不同超级管理员关注整栋楼的用电总量、电费总支出、各楼层占比还要能管理所有用户的账号权限。楼层/区域管理员关注自己所辖区域的用电趋势、设备运行状态、费用分摊明细。巡检/运维人员关注设备是否离线、电压电流是否越限、有没有告警需要处理。这三类需求叠加起来整个系统的功能地图就出来了。首先是基础的用户与权限管理这是所有系统的地基然后是楼宇档案管理楼栋、楼层、房间、电表设备这些基础数据必须有一个清晰的组织结构再往上走是核心的能量采集与监控包括实时数据展示和历史曲线查询接着是用能分析与费用管理要能按时间段、按区域统计用电量结合电价规则自动计算费用最后是告警管理用短信或站内信把异常通知到责任人形成闭环。这里有一个容易被忽略的点楼宇电能管控系统中设备的层级关系极其重要。一块电表挂在哪个配电箱、配电箱在哪个楼层、楼层属于哪个区域这些关系如果不在设计初期理清楚后面做费用分摊和权限隔离的时候会非常痛苦。我在建模的时候直接把组织架构作为了一棵树来设计具体后面讲表结构的时候展开。1.2 为什么是SSMVUE而不是其他组合技术选型是这个项目最早定下来的事。后端用SSM也就是Spring、SpringMVC、MyBatis三个框架的组合前端用VUE。这套组合在2024年的今天看不能说新潮但它在中小型管理系统里的地位依然稳固。先看SSM这套后端组合。Spring负责依赖注入和事务管理它是整个应用的骨架SpringMVC负责接收HTTP请求、参数绑定、返回视图或JSON数据是Web层的入口MyBatis负责数据库操作把SQL写在XML文件里可控性非常强。三个框架各司其职配合度极高。相比SpringBootSSM在配置上确实麻烦一些需要手动处理web.xml、Spring配置和SpringMVC配置之间的引用关系但对于理解Java Web运行机制的帮助非常大。做这套系统的时候我明显感觉到如果一上来就用SpringBoot很多东西被自动配置包裹住反而不容易理解请求从URL到数据库再返回的完整链条。前端选VUE的原因则要实际得多。楼宇电能管理系统本质上是一个数据密集型应用页面上的表格、图表、表单交互非常多。VUE的双向数据绑定机制让数据和视图保持同步修改一个数据对象页面上所有引用它的地方自动更新。加上VUE生态里的Element UI组件库和ECharts图表库做后台管理界面和信息可视化大屏的效率非常高。用原生JS写这种体量的系统代码量和维护成本会是VUE方案的三倍以上。还有一点是面试和答辩时经常被问到的为什么不用前后端不分离的传统JSP方案。JSP方案在服务端渲染HTML每次页面刷新都要重新请求整个页面数据实时更新的体验很差。而VUE在浏览器端维护DOM和数据状态与后端只通过纯JSON数据交互是标准的“前后端分离”架构。这种架构的好处一目了然后端只提供接口、不关心页面长什么样前端专注于交互和展示两者可以并行开发。2. 后端核心设计与实现2.1 数据库模型设计一张好的表结构能省一半的返工时间这个系统的数据库一共设计了9张核心表我最想重点讲的是组织架构相关的三张表和电表数据表因为它们直接决定了系统的扩展能力。楼宇组织架构这块我设计了building、floor、room三张表分别代表楼栋、楼层、房间。每张表都带有parent_id字段形成一个可以无限扩展的树形结构。building表不设parent_id它是根节点floor表的building_id指向所属楼栋room表的floor_id指向所属楼层。再加上一个device表用来挂电表设备device表里包含room_id字段这样从一栋楼到一块电表一条链路就完整串起来了。在MySQL里建树形结构的表关键是要合理使用外键和索引。虽然很多团队不推荐物理外键但我个人在项目初期还是保留了FOREIGN KEY约束因为它能在数据写入时就拦截掉非法关联。等系统上线跑稳了再换掉也不迟。电表采集数据表meter_data是数据量最大的表我设计它的核心字段包括device_id电表设备ID、voltage电压、current电流、power功率、energy电量、collect_time采集时间。这张表几乎是按分钟级别写入的如果不做处理单表数据量会飞速膨胀。我在设计时就确定了按月份自动分表的方案每个月生成一张meter_data_202410这样的表用MyBatis的${tableName}动态传入表名进行查询。费用相关我设计了electricity_price和cost_record两张表。electricity_price存储不同时段的电价字段包括time_period时段名称、start_time、end_time、price。cost_record则用于存储每次结算生成的费用记录包含room_id、energy、amount、billing_month避免每次查看历史账单都要重新计算。还要重点提到warning_log表这个表记录每一次告警事件。字段有device_id、warning_type过压/欠压/过流/功率越限、warning_desc、status已处理/未处理、create_time。做巡检页面的时候运维人员直接查这张表按状态筛选即可。2.2 SSM框架整合要点与关键配置SSM整合是所有基于这套框架的系统的地基。配置顺序必须是Spring容器先启动然后SpringMVC子容器再启动两者通过ContextLoaderListener和DispatcherServlet挂载到同一个Web应用中。这个顺序搞反了最典型的症状就是Service注入到Controller时报空指针。我自己用的整合方式是applicationContext.xml只负责Spring相关的配置包括组件扫描排除Controller、数据源、事务管理器。spring-mvc.xml单独负责SpringMVC的配置包括Controller扫描、注解驱动、JSON消息转换器、静态资源映射和视图解析器。MyBatis的配置一般放在mybatis-config.xml里然后在applicationContext.xml中用SqlSessionFactoryBean引入。要特别注意的是Mapper扫描的配置必须放在Spring配置里否则MyBatis无法自动生成Mapper代理bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.mapper/ /bean数据源方面我用的是Druid连接池。除了基本的连接参数initialSize、minIdle、maxActive这几个核心参数一定要设置合理。真正生产环境下还要开启Druid的监控页面这样能看到SQL执行耗时、连接池使用情况排查慢查询的时候特别好用。监控页面配置在Web.xml里加一个Servlet即可servlet servlet-nameDruidStatView/servlet-name servlet-classcom.alibaba.druid.support.http.StatViewServlet/servlet-class /servlet servlet-mapping servlet-nameDruidStatView/servlet-name url-pattern/druid/*/url-pattern /servlet-mapping事务管理这块我用tx:annotation-driven/开启注解事务然后在Service层的写方法上标注Transactional。电能采集数据量大、写入频繁但单条写入失败并不影响大局所以采集服务的方法我特意用了REQUIRES_NEW传播行为确保即使事务回滚也不会影响主流程。2.3 定时采集任务与告警判断逻辑电能数据的时效性决定了系统价值。如果数据延迟一个小时那管理者看到的就只是一堆历史数据谈不上“管控”。我用Spring自带的Scheduled注解实现了定时采集任务这比引入Quartz要轻量得多。采集调度配置相当简单在Spring配置文件中加一行task:annotation-driven/然后在Service类的方法上标注Scheduled(cron 0 0/1 * * * ?) public void collectMeterData() { ListDevice devices deviceMapper.selectAllOnline(); for (Device device : devices) { MeterData data modbusClient.readData(device.getIp(), device.getPort()); meterDataMapper.insert(data); } }定时任务在实际项目中有一个非常关键的坑默认情况下Spring的定时任务是单线程串行执行的。如果某个任务的执行时间超过了任务间隔后续任务就会排队等待造成数据采集延迟。我的解决方案是配置一个线程池让不同的任务并行执行task:annotation-driven schedulertaskScheduler/ bean idtaskScheduler classorg.springframework.scheduling.concurrent.ThreadPoolTaskScheduler property namepoolSize value10/ /bean告警判断逻辑我放在采集任务内同步执行每次拿到最新数据就跟阈值对比。规则包括电压超过额定电压的±10%、电流超过载流量上限、功率因数值低于0.8、电表连续一段时间无数据上报。命中规则后先查同设备在最近5分钟内是否已有同类告警如果有就跳过避免重复报警刷屏如果没有才插入告警记录。这一步也要考虑实际采集时Modbus协议的兼容性。不同厂家的电表寄存器地址并不完全一致通用的做法是在device表里单独加两个字段存电压和电量的寄存器地址代码里根据设备型号动态读取而不是在每个设备里去写死地址。3. 前端VUE项目搭建与页面实现3.1 VUE环境配置与项目初始化VUE项目的搭建很多新手会卡在环境上。我建议用VUE CLI来初始化项目它会帮你把Webpack配置全部封装好省去大量配置文件折腾的时间。Node.js版本需要特别注意VUE CLI 5要求Node版本在12以上但也不能太新我用的是Node 16 LTS版本。太新的Node版本有时会给Webpack4项目带来兼容性问题那种报错密密麻麻的看半天看不出原因最后发现是Node版本太高不兼容非常浪费时间。项目初始化后用npm安装Element UI和ECharts两个核心依赖npm install element-ui -S npm install echarts -S npm install axios -SVUE项目的目录结构我习惯按功能模块拆分而不是按文件类型拆分。在src/views下按页面功能建目录比如dashboard大屏、device设备管理、warning告警管理、cost费用管理。每个目录下放对应页面的vue文件相关的组件放到src/components里。这样多人协作时每个人负责一个模块目录git冲突会少很多。路由配置使用vue-router因为系统里的告警管理、设备管理等页面需要携带ID跳转我用的是动态路由传参方式。比如点击某条告警记录进入详情页路由可以写成{ path: /warning/detail/:id, name: WarningDetail, component: () import(../views/warning/WarningDetail.vue) }在跳转时用this.$router.push({ name: WarningDetail, params: { id: row.id } })。这种方式的优点是URL直观、刷新页面参数不丢失缺点是需要单独处理参数变化时的数据刷新逻辑这个后面会讲。3.2 大屏可视化与ECharts图表接入首页大屏是整个系统的门面领导看系统首先看就是大屏。我的设计思路是顶部放全楼的总用电量、总电费、今日峰值功率这几个核心KPI指标中间主体区域放按小时统计的24小时用电趋势曲线左右两侧分别放各楼层用电占比饼图和设备运行状态列表。ECharts在VUE里的接入方式并不复杂核心步骤是先在mounted钩子里初始化图表实例然后通过Axios请求后端接口拿数据用setOption更新图表选项。有一个坑必须提醒ECharts实例在组件销毁时一定要手动销毁否则切换路由时会报“Cannot read properties of undefined”的错误。我的做法是在beforeDestroy生命周期中调用this.chart.dispose()。大屏数据实时刷新的逻辑我用了定时器加重新请求接口的方式。每10秒调用一次刷新方法获取最新24小时数据和设备状态然后更新图表的series.data。这里要注意一个细节ECharts的setOption方法默认是合并模式对于曲线图直接更新series.data是有效的但对于饼图最好把series整个替换掉否则有时会出现动画闪烁的问题。前端还有一个细节容易被忽略就是数据为空时的页面状态。很多机器在刚通电或者电表离线时接口返回的是空数组这个时候ECharts画出来是一片空白。我在表格组件中设置了empty-text暂无数据在图表容器上加了空数据判断信息明确总比白屏让人困惑强。3.3 登录认证与Axios拦截器前后端分离架构下登录认证是绕不开的课题。我用的是基于Session的方式后端在登录成功后把用户对象存进Session前端每次请求都携带Cookie后端通过拦截器验证Session中的用户是否合法。这种方式比JWT要传统但在内网管理系统这种单机部署的场景下反而简单可靠。Axios封装是前端请求的核心。我在src/utils/request.js里创建了一个Axios实例统一配置了baseURL和超时时间。然后添加了请求拦截器和响应拦截器。请求拦截器主要用来在请求头里加token或Cookie相关配置响应拦截器最重要的作用是统一处理HTTP错误码。比如后端返回401表示未登录或会话过期直接在拦截器里跳转到登录页并弹出提示这样每一个页面都没有必要单独写判断逻辑。跨域问题是前后端分离项目绕不开的话题。开发阶段VUE项目跑在8080端口后端跑在8081端口浏览器的同源策略会直接拦下所有请求。我在后端写了一个CORS配置类允许指定的前端地址跨域访问。要注意的是SpringMVC的CORS配置和Spring Security的CORS配置容易互相覆盖如果两个都配置了就可能出现请求被正确发出但响应被拦截的情况。4. 关键功能模块的实现细节4.1 电价策略与费用自动计算电价计算模块是整个业务逻辑中最能体现“综合管控”价值的部分。楼宇用电的特点是不同时段电价不同峰时电价可能是谷时的三倍有经验的物业管理团队会通过错峰用电来降低成本。系统的费用计算模块必须支持多时段尖峰平谷电价的灵活配置而不是简单地把总电量乘以一个固定单价。我在实现时设计了一个CostCalculateService核心逻辑是public BigDecimal calculateCost(Long roomId, String month) { ListPriceConfig priceConfigs priceMapper.selectAll(); ListMeterData monthData meterDataMapper.selectByRoomAndMonth(roomId, month); BigDecimal totalCost BigDecimal.ZERO; for (MeterData data : monthData) { PriceConfig config findMatchPrice(priceConfigs, data.getCollectTime()); BigDecimal energy data.getEnergy().subtract(lastEnergy); totalCost totalCost.add(energy.multiply(config.getPrice())); } return totalCost; }这个逻辑说起来简单但要处理一个边界情况电价配置的生效时间有重叠怎么办。我的方案是findMatchPrice按优先级排序优先匹配time_period为“尖峰”的配置再匹配“峰”、“平”、“谷”保证不会一个时间点同时匹配到两个价格。前端费用页面我做了一个月份选择器选择哪个月就查询哪个月的费用记录。页面上的费用数据默认从cost_record表读取只有在用户点击“重新结算”按钮时才触发重新计算的接口。这么设计的原因是电表的读数可能存在补录、修正的情况每次都全量重新汇总计算在数据量大的时候可能会卡顿。4.2 告警推送与处理闭环告警模块是楼宇电能管控里最有直接价值的模块。管理者不可能每时每刻盯着大屏勤快的物业可能要每隔几个小时巡检一次电能异常如果在几小时内都没人发现损失和价值就不好估了。我实现的告警流程是采集任务发现异常写入warning_log表状态为“未处理”前端告警页面通过轮询方式每30秒获取一次最新未处理告警列表当有新的未处理告警时页面顶部红色气泡数字加1同时弹出一个Toast提示运维人员点击“处理”按钮后填写处理说明状态变为“已处理”。告警轮询的前端实现setInterval(() { request.get(/warning/unhandled).then(res { this.unhandledCount res.data.length; if (res.data.length this.previousCount) { this.$notify({ title: 新的告警, message: 检测到新的用电异常请及时处理, type: warning }); } this.previousCount res.data.length; }); }, 30000);为了不让告警列表越积越多系统还提供了一个“批量处理”功能。运维人员可以勾选多条同类型的告警统一标记为“已处理”并填写一条统一的处理说明。之所以要加这个功能是因为在实际使用中经常会出现某层楼集中跳闸一次性产生上百条告警挨个点处理会把人逼疯。4.3 用户权限与菜单的动态渲染在楼宇电能管控系统中不同角色的用户登录后看到的菜单和页面必须不一样。超级管理员能看到全部菜单楼层管理员只能看到自己管辖楼层的数据巡检人员只能看到设备状态和告警页面。菜单权限这块我采用了最简单也最可控的方案后端登录接口返回该用户拥有权限的菜单列表前端根据这个列表动态生成侧边栏菜单同时用vue-router的addRoutes方法动态注册路由。后端存储菜单与角色的关联表role_menu菜单表menu中维护菜单名称、路由路径、图标等字段。管理员在“用户管理”页面给用户分配角色在“角色管理”页面勾选角色拥有的菜单权限权限变更后用户下一次登录即生效。我本来想用WebSocket做权限变更实时推送后来想了想让用户重新登录一次也不算什么负担还能避免权限变更时前端状态不同步的问题。后端的拦截器同样需要做权限校验不能只依赖前端隐藏菜单API层面也要拦截无权限的请求。5. 常见问题与排查技巧实录5.1 前端请求跨域问题跨域问题是前后端分离项目中最常见的拦路虎光我帮同事排查这个问题就不下十次。最典型的报错是浏览器控制台出现“Access-Control-Allow-Origin”相关的错误。后端的CORS配置确保响应头中带上正确信息同时要注意allowedHeaders不能只给默认值否则前端自定义的请求头会被拒绝。后端CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }如果加了登录拦截器还要注意在拦截器中放行OPTIONS请求。因为浏览器在发送跨域请求前会先发一个预检请求这个请求不带业务参数如果被拦截器拦下来返回401那真正的业务请求就永远不会发出。5.2 MyBatis动态SQL与表名参数注入在分表场景下MyBatis的${}和#{}的区别如果不搞清楚很容易踩坑。#{}是预编译参数能有效防止SQL注入${}是字符串拼接注入风险很高。但在分表查询的场景下表名只能通过${}传入因为#{}给表名加引号会直接报错。正确写法是select idselectByDeviceAndTime resultTypeMeterData SELECT * FROM ${tableName} WHERE device_id #{deviceId} AND collect_time BETWEEN #{startTime} AND #{endTime} /select在Mapper接口里#{deviceId}是有序的预编译参数${tableName}是直接拼接表名。这里的规范是${}只用来传表名或排序字段这种无法预编译的内容其他所有用户输入全部用#{}绝对不要为了图省事把参数拼到SQL里。5.3 前端时间格式化与时区问题电能数据的时间字段是后端生成的默认是LocalDateTime类型转成JSON后通常是“2024-10-15T14:30:00”这种ISO格式。这种格式直接显示在页面上很难看而且在不同时区的浏览器上解析结果可能不一致。我处理的方式是后端在把数据转JSON时通过Jackson配置统一格式化Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); }同时前端在展示时也做了兜底处理写了一个全局过滤函数formatDate在模板里统一使用filters: { formatDate(value) { if (!value) return --; return value.replace(T, ).substring(0, 19); } }这个问题看似小但如果不在开发阶段就统一好格式等到了上线前联调阶段再来统一修改牵涉的页面至少有十几个工作量会陡增。5.4 定时任务与线程池的吞吐量项目刚把采集频率调到每30秒一次的时候出现了一个诡异的现象页面上的实时数据更新正常但是部分楼房的数据偶尔会缺失。排查后发现是定时任务串行执行导致的问题。采集任务每隔30秒触发一次但单次采集几百台电表数据需要40秒任务还没跑完下一次触发就排队等待数据自然就漏了。解决方案就是前面提到的线程池。同时我还把采集任务按楼栋拆成了多个方法分别标注不同的Scheduled(cron)表达式错开执行时间。这样做还有一个好处第3栋楼的电表采集出了问题只会影响第3栋的数据不会拖垮整个采集流程。5.5 前端下拉框数据量过大时的性能优化设备管理页面中有一个“选择设备”的下拉框理论上会把所有电表设备一次性加载进来。在设备数量少的时候感觉不到问题等到设备扩展到上百台甚至更多时下拉框打开时会明显卡顿。我的解决方案是改用远程搜索模式输入关键字才去后端查询避免了全量加载el-select v-modelform.deviceId filterable remote :remote-methodsearchDevices placeholder请输入设备编号搜索 el-option v-foritem in deviceOptions :keyitem.id :labelitem.deviceName :valueitem.id /el-option /el-select配套的后端接口是一个模糊查询接口接收关键字参数返回前20条匹配记录。这个优化看似简单但对于长时间运行的项目来说非常关键数据量增长后不至于让用户抱怨“页面打不开”。5.6 从开发到部署Linux服务器上的一次成功上线最后记录一下部署环节。整个项目前端是纯静态文件构建后生成dist目录里面是打包好的JS、CSS和HTML。后端是标准的Maven工程打包成War包。我部署时用的是Nginx Tomcat的组合Nginx负责托管前端静态资源并转发API请求到TomcatTomcat负责运行后端应用。Nginx的关键配置server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }用try_files将前端路由全部指向index.html这是一个非常关键的配置。VUE项目使用了前端路由如果这里不配置刷新页面时Nginx会返回404这可能是SPA部署中最容易遇到的问题。部署完成后我习惯用curl测试几个关键接口比如登录接口、采集接口确认状态码和返回内容正常然后访问首页看大屏数据是否正常刷新。整套流程跑通系统才算真正上线。6. 写在最后的个人体会这套系统做完至今我自己回看的时候最大的体会是系统的架构选型和技术框架只是手段真正让项目落地的反而是对业务需求的理解深度。比如电价策略如果一开始没有考虑到峰值平谷的分时计价后期再加这个功能就要改动表结构和计算逻辑工作量会成倍增加。如果你也在做类似的项目我的建议是先把需求列表里的非功能性需求也想清楚数据量大概多大、并发量有多少、运维人员的技术水平如何。这些因素会直接影响你的分表策略、定时任务配置和前端组件选型。这套系统后续还可以扩展的方向很多比如接入更丰富的设备类型、增加用水用气数据、引入机器学习做用电负荷预测、生成更精细化的能耗分析报告。架构上只要把设备接入层抽象好扩展这些功能都不需要推倒重来。至于什么时候扩展、扩展哪一块就看楼宇管理的实际需求了。本文还有配套的精品资源点击获取