简介:这是一套面向Java初学者与全栈开发学习者的基于SpringBoot的智能家居管理系统完整源码,适用于课程设计、毕业设计及Web全栈项目实践。资源涵盖前后端一体化实现,后端采用SpringBoot+MyBatisPlus+MySQL 5.7,前端基于Vue+ElementUI+Ajax,配套详细文档目录(含绪论、技术介绍、系统分析等章节)与可直接运行的构建脚本(build.bat/run.bat/install.bat),降低环境配置门槛。压缩包共368个文件,包含77个Java核心业务类、46个Vue组件页面、161个SVG图标资源及图片/视频/样式/配置等辅助文件,整体大小15.22MB,结构清晰、模块分明,便于按功能模块快速定位与二次开发。目前已有695人学习下载,提供开箱即用的用户管理、素材上传(图片/视频)、设备控制模拟等基础智能家居场景功能,是理解B/S架构下IoT应用系统设计逻辑的优质教学级参考项目。
1. 从“能跑的Demo”到“能住的家”:这个智能家居系统到底做了什么
先说个挺常见的现象。很多Java学习者做到Spring Boot项目时,翻来覆去就是“图书管理”“学生选课”“订单系统”这老三样。不是说这些项目不好,而是做多了之后你会明显感觉到:它们练的是CRUD熟练度,但涉及真实业务场景里的状态同步、并发上报、规则命中、长连接通信这些硬骨头,完全碰不到。
我这次用Spring Boot完整实现了一套智能家居管理系统,不是那种只有一个设备列表的演示品,而是从设备接入、状态上报、远程控制、自动化场景到数据统计都能跑通的完整闭环。一句话概括:通过这套系统,你可以用手机页面远程开关家里的灯、空调、窗帘,查看温湿度传感器和空气质量数据,还能设置“回家自动开灯”“温度高于28度自动开空调”这类联动规则。
技术栈上以Spring Boot 2.7为底座,配合MyBatis-Plus操作MySQL存储设备与日志数据,用WebSocket建立服务端和智能网关之间的双向通道,前端用Vue 3 + Element Plus搭建管理界面。整个项目代码结构清晰,完全可以作为毕业设计、个人作品集或者中小企业做轻量级智能家居平台的基础骨架。
如果你现在处于这几个阶段,这篇内容会特别合适:
- 已经学过Java基础和Spring Boot基本使用,但想做一个“有技术深度、面试拿得出手”的综合项目
- 正在准备软件方向的毕业设计或简历项目,希望代码规范和业务完整度都经得起细看
- 对物联网后端设计有兴趣,想知道设备上报、指令下发、自动化规则这些经典场景在真实代码里是怎么落地的
这套系统在设计之初就有一个明确原则:不依赖任何商用的云端平台,所有设备接入、通信、规则引擎自己实现。这样你拿到源码后,部署在自己服务器上就是一个真正自主可控的智能家居控制中枢,也能把整个链路看透。
2. 为什么我选Spring Boot + WebSocket,而不是MQTT那一套
2.1 网关上云,先分清“设备直连”和“网关代理”两种模型
做智能家居后端,第一个绕不开的决策是设备如何接入。当前主流有两套思路:
第一套是设备直连云,典型就是涂鸦智能、小米米家这种商业方案。家里的WiFi插座、灯泡内置了WiFi模块,它们自己通过加密协议连到云端,云平台直接管理每台设备。这种模式下,设备端协议五花八门,Zigbee、蓝牙Mesh、WiFi各有各的适配层,后端要把这块全部消化掉,工程量巨大。
第二套是网关代理,也就是我在这套系统里采用的方案。家里部署一个智能网关,类似一个小型服务器,它通过Zigbee或蓝牙Mesh与各个传感器、开关通信,而网关本身主动连接到我的Spring Boot服务端。服务端只需和网关维持一条稳定的长连接,不需要关心底层每一种设备的私有协议。
网关代理的好处非常明显:后端对设备屏蔽了差异,通信模型从“N对N”简化成了“N对1”。网关侧框架也成熟,像开源社区常见的Home Assistant、Node-RED都可以扮演这个角色。就算你暂时没有硬件,也可以写一个模拟网关的Java客户端脚本,把设备状态用JSON往服务端推,开发调试完全不受限制。
2.2 为什么不用MQTT:协议选型要看维护成本
你可能马上会问:物联网通信标准不都是MQTT吗,Spring Boot集成一个MQTT Broker(比如EMQX)也很方便,为什么要用WebSocket?
答案是:这套系统的定位决定了WebSocket更合适。
MQTT是发布订阅模型,适合大规模、跨网络、弱网环境下的设备通信。它需要额外部署Broker服务,增加了一整个独立组件。而我们的场景是家庭内部网关通过宽带接入服务端,网络环境相对稳定,服务端连的设备数量也就是几十到几百台,完全不需要消息中间件来削峰填谷。
用WebSocket直连方案,省掉了一层代理。网关认证通过后,服务端就可以直接向网关下发指令,网关也能第一时间把设备状态变化推上来,不需要维护主题订阅关系,调试的时候用浏览器开发者工具都能看到实时帧。
这不是说MQTT不好。如果你未来要做一个工厂级的物联网平台,接入几万台设备,MQTT几乎是必选。但在家庭场景、单体应用、学习项目这个复杂度量级上,Spring Boot + WebSocket是运维成本更低、理解成本也更低的组合。
两类方案的对比,我整理在下面:
| 对比维度 | WebSocket直连 | MQTT + Broker |
|---|---|---|
| 额外组件 | 无(Spring Boot自带支持) | 需要部署Broker |
| 通信模型 | 全双工长连接 | 发布订阅 |
| 设备规模 | 几千台以内 | 十万台级别 |
| 弱网鲁棒性 | 一般,依赖TCP保活 | 强,协议天然支持 |
| 调试难度 | 低,可直观看到帧 | 中,要理解Topic规则 |
| 最佳场景 | 家庭/园区级系统 | 广域物联网平台 |
如果你还是想用MQTT折腾一下,Spring Boot集成也简单,加依赖配个客户端就行。但作为本项目的核心通信链路,WebSocket是清晰度和可控性最优的方案。
2.3 Spring Boot版本和Java版本的搭配问题
做这个项目时Spring Boot 3.x已经发布,为什么我反而用2.7?这是很多初学者容易忽略的坑——版本选型直接影响后续开发的顺畅度。
Spring Boot 3.0基于Jakarta EE规范,把javax.包全部换成了jakarta.,而很多老牌的第三方库(尤其是一些硬件SDK、早期版本的MyBatis相关组件)还没有跟上这个变化。一旦你要在Spring Boot 3.x里集成这些库,要么升级库版本,要么自己改源码,处理起来非常头大。
2.7是Spring Boot 2.x的最后一个大版本,也是我踩过无数坑之后认为最稳的版本。它兼容JDK 8和JDK 11,大部分中间件都有成熟适配,MyBatis-Plus、Druid、WebSocket相关的文档和解决方案在网上也非常齐全。真正要做项目,稳定性永远比追求新版本重要。
3. 数据库设计的核心:设备状态不是简单存一张表就完事
智能家居系统数据库设计的关键,不是设计多少张表,而是想清楚两个核心问题:设备状态怎么存、指令与状态的关系怎么处理。这是我二次重构后最想分享的部分。
3.1 核心表结构:六个表把业务串起来
整个系统的业务表我最终收敛成了六张:
用户与家庭空间
user:用户账号,包括用户名、密码(BCrypt加密)、手机号、角色home:家庭空间,层级结构是 用户 -> 家庭 -> 房间 -> 设备
设备管理
device:设备静态信息,包括设备名、类型、所属房间、网关ID、品牌型号device_status:设备实时状态,存储设备的当前值(开关状态、温度数值、亮度等)device_log:设备历史日志,记录每一次状态变化
自动化规则
automation_rule:联动规则表,比如“当温度大于28度时打开空调”这种条件,包括触发器、执行动作、启用状态
3.2 为什么把device和device_status拆成两张表
很多初学者会把设备基础信息和当前状态放在同一张表里,用的时候一条SQL查出来。听起来没毛病,但真实场景下有两个绕不开的痛点:
第一,设备状态更新极其频繁。温度传感器可能每十秒就上报一次,如果每次都去UPDATE设备表,行锁竞争会让数据库压力陡增。而设备基础信息(名称、类型、房间)几乎不变。
第二,设备表的基础信息需要在界面上展示,如果状态更新频繁触发搜索引擎或ORM的缓存失效,会拖慢整个系统的热点查询。
拆分成两张表后,设备基础表只需要在添加设备或修改配置时写入,设备状态表则专职处理高频写入。状态表可以选择只保留最新状态,也可以保留一个短窗口的变更序列,再配合定时任务把历史数据归档到device_log表去做统计。这样读写分离,性能问题天然被化解了一大半。
3.3 设备状态表设计:map结构比啰嗦的字段更实用
智能家居设备类型千奇百怪:一个灯泡的状态是“开/关”,一个空调的状态是“模式+温度+风速”,一个传感器的状态是一个数值。如果每种设备都设计对应字段,设备表会无限膨胀,加一种新设备就要改表结构。
我的解法是:device_status表设计成设备ID + 状态JSON的结构。类似这样:
CREATE TABLE `device_status` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `device_id` bigint(20) NOT NULL COMMENT '设备ID', `status_value` json DEFAULT NULL COMMENT '状态JSON', `update_time` datetime DEFAULT NULL COMMENT '状态更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_device_id` (`device_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status_value字段存的是类似这样的JSON:
{ "power": "on", "brightness": 80, "color": "#FFFFFF" }这样一来,无论接入什么新设备,后端只需要解析JSON,不需要改表结构。前端根据设备类型,动态渲染对应的控制面板即可。这是物模型思想的一个简化版实现,也是商业物联网平台普遍采用的做法。
3.4 设备日志表:不做聚合查询的日志就是灾难
一开始我图省事,设备日志表只存了设备ID、状态快照、时间。结果设备一多、上报频率一上来,想查“今天客厅空调开了多久”这种问题,SQL会把几百万行历史数据全扫一遍,慢到怀疑人生。
后来我加了两个维度来优化:
- 设备日志按天分表(
device_log_20250601这种形式),或者至少加date分区 - 核心统计指标用定时任务提前聚合到
device_daily_stat表,查询直接读聚合结果
对于成长型项目来说,日志表的设计一开始就要考虑数据量级,千万别等到慢了再重构,那时候改表的成本非常高。
4. 从网关认证到指令下发:通信模块的完整实现
通信模块是整个系统的技术核心,也是面试时最能讲出东西的部分。这里的每个设计点几乎都踩过坑,我按一条完整链路来说。
4.1 网关接入:首次注册拿到唯一凭证
智能网关首次上电时,并不知道服务端地址,也不知道自己的身份。我在这里设计了一个最简单的引导流程:
网关第一次连接时,使用出厂预置的序列号(SN)调用/gateway/register接口,服务端收到请求后校验SN合法性,为该网关生成一个随机盐值(salt),并将SN + 盐值进行组合加密生成access_token,保存在网关表里返回给网关。
之后网关每次连接WebSocket,都需要在URL上携带这个token:
ws://your-server:8080/ws/gateway?token=xxxx服务端在WebSocket握手阶段拦截token,校验通过则建立连接,不通过直接拒绝。这个方案比“先建连接再认证”更安全,也省去了一次无效的连接周期。
4.2 WebSocket连接管理:在线状态和心跳缺一不可
WebSocket是长连接,但网络环境复杂,TCP可能断掉但连接还没被感知。因此心跳机制必须有。
我设计的通信帧格式(JSON)统一如下:
{ "type": "COMMAND" | "REPORT" | "HEARTBEAT" | "REGISTER" | "ACK", "deviceId": "xxx", "data": {} }网关每30秒发送一个HEARTBEAT帧。服务端通过一个定时任务,每60秒检查一次所有网关的最后心跳时间,如果超过90秒没有收到心跳,就判定这个网关离线,更新网关在线状态,并清除在线WebSocket会话。
这里有个细节值得注意:APP端控制设备状态和网关上报设备状态是两条方向相反的消息。我用了一个CommandService和ReportService分别处理下发指令和设备上报,职责很清楚。
服务端下发指令给网关的核心代码(节选):
@Component public class GatewaySessionManager { private final ConcurrentHashMap<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>(); public void addSession(String gatewayId, WebSocketSession session) { sessionMap.put(gatewayId, session); } public void sendCommand(String gatewayId, CommandMessage command) { WebSocketSession session = sessionMap.get(gatewayId); if (session != null && session.isOpen()) { try { session.sendMessage(new TextMessage(JSON.toJSONString(command))); } catch (IOException e) { log.error("下发指令失败,gatewayId: {}", gatewayId, e); // 标记网关离线,触发离线处理 offline(gatewayId); } } else { // 网关不在线,把指令缓存起来等它上线再发(离线消息) pendingCommandService.save(gatewayId, command); } } }这个ConcurrentHashMap + WebSocketSession的组合比Spring自带的SimpMessagingTemplate更直观、控制力更强。因为智能家居场景下,消息不是广播,而是精确到某个网关的点对点下发,自己维护会话集合是最简单有效的方式。
4.3 设备上报:状态入库和规则触发怎么配合
网关上报的设备状态,处理顺序非常重要。我先更新设备状态表,然后异步去触发自动化规则引擎。为什么异步?因为规则引擎里面可能挂了好几条复杂的联动逻辑,如果同步执行,网关上报一次状态可能要等几百毫秒才收到ACK,网络不好的时候客户端会严重超时。
上报链路我用CompletableFuture做了异步化:
public void handleDeviceReport(DeviceReportMessage report) { // 1. 更新设备最新状态 deviceStatusService.updateStatus(report.getDeviceId(), report.getStatus()); // 2. 记录设备日志 deviceLogService.recordLog(report.getDeviceId(), report.getStatus()); // 3. 异步触发规则引擎 CompletableFuture.runAsync(() -> { ruleEngine.evaluate(report.getDeviceId(), report.getStatus()); }, ruleExecutor); }这里还要考虑一个问题:设备上报频率高时,** 同一设备同一秒的重复状态**是否需要入库。我的做法是在device_log表插入前做一个简短判断,如果状态和上一条完全一样,跳过记录。这个简单的幂等处理能把日志数据量降低60%以上。
4.4 自动化规则引擎:温度高了就开空调,这行代码是怎么写的
规则引擎是智能家居“智能”二字的灵魂。我的实现思路借鉴了开源规则引擎的简化模型,不引入Drools这种重型组件,而是自己设计一个轻量级的RuleEngine。
规则表结构设计如下:
CREATE TABLE `automation_rule` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) DEFAULT NULL COMMENT '规则名称', `home_id` bigint(20) DEFAULT NULL COMMENT '所属家庭', `trigger_type` varchar(20) DEFAULT NULL COMMENT '触发类型:DEVICE_DEVICE/TIMER', `condition_device_id` bigint(20) DEFAULT NULL COMMENT '条件设备ID', `condition_field` varchar(50) DEFAULT NULL COMMENT '条件字段,如temperature', `condition_operator` varchar(10) DEFAULT NULL COMMENT '条件运算符:GT/LT/EQ', `condition_value` varchar(50) DEFAULT NULL COMMENT '条件值', `action_device_id` bigint(20) DEFAULT NULL COMMENT '执行设备ID', `action_command` json DEFAULT NULL COMMENT '执行指令,如{"power":"on"}', `enabled` tinyint(1) DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;规则引擎核心代码:
public void evaluate(String deviceId, Map<String, Object> status) { List<AutomationRule> rules = ruleMapper.findByConditionDeviceId(deviceId); for (AutomationRule rule : rules) { if (!rule.getEnabled()) continue; Object actualValue = status.get(rule.getConditionField()); if (actualValue == null) continue; if (compare(actualValue, rule.getConditionOperator(), rule.getConditionValue())) { // 条件满足,下发执行指令 CommandMessage command = new CommandMessage(); command.setDeviceId(rule.getActionDeviceId()); command.setData(JSON.parseObject(rule.getActionCommand())); gatewaySessionManager.sendCommand(getGatewayIdByDevice(rule.getActionDeviceId()), command); } } }这个compare方法支持GT、LT、EQ、NE等常见操作符,核心逻辑二十几行就能写完。如果你想让规则更强大,可以加一个AND/OR的嵌套语法,甚至用Aviator表达式引擎来解析规则表达式,效果也很好。
我实际测试中,从传感器上报温度到空调收到开机指令,中间延迟大概在300毫秒以内,完全满足家居场景的实时性要求。
5. 设备管理接口:一个规范的Controller是怎么设计出来的
5.1 RESTful接口设计:动词交给HTTP,名词交给URL
后端接口我严格按照RESTful风格来设计,这样做的好处是前后端对接时,光看URL和方法就能猜到语义,不用来回翻文档。
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/device/list | 查询当前家庭下的设备列表 |
| GET | /api/device/{deviceId} | 查询设备详情和最新状态 |
| POST | /api/device | 添加新设备 |
| PUT | /api/device/{deviceId} | 修改设备信息(名称、所属房间等) |
| DELETE | /api/device/{deviceId} | 删除设备 |
| POST | /api/device/{deviceId}/command | 下发控制指令 |
| GET | /api/log/list | 分页查询设备历史日志 |
一个典型的控制指令接口实现:
@RestController @RequestMapping("/api/device") public class DeviceController { @Resource private DeviceCommandService commandService; @PostMapping("/{deviceId}/command") public Result sendCommand(@PathVariable Long deviceId, @RequestBody CommandRequest request) { // 校验设备是否存在且属于当前用户 Device device = deviceService.checkDevicePermission(deviceId); // 构造下发指令,写入命令流水表 commandService.send(device, request.getCommand()); return Result.success("指令下发成功"); } }这里有个非常容易被忽略的细节:checkDevicePermission这一步不能省。如果接口不做归属校验,任意登录用户遍历ID就能控制别人家的设备,这是物联网项目的严重安全事故。我在每个涉及设备操作的接口都做了当前用户和设备的归属关系校验。
5.2 统一返回体和全局异常:别让客户端自己猜错误
项目一旦前后端分离,接口返回格式必须统一。我定义了标准的Result对象:
@Data public class Result { private Integer code; private String message; private Object data; public static Result success(Object data) { Result result = new Result(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static Result error(Integer code, String message) { Result result = new Result(); result.setCode(code); result.setMessage(message); return result; } }再加上@RestControllerAdvice全局异常处理器,把业务异常、参数校验异常、未知异常统一包装成这个格式输出。这样前端拿到响应后,只需要判断code是不是200,不需要为每一种错误单独写解析逻辑。
5.3 分页查询的正确姿势:不要让数据全量加载
设备日志分页查询我用MyBatis-Plus的Page对象,同时规定了单页最大条数不能超过100。为什么有这个限制?因为日志表数据量很容易膨胀,如果接口支持客户端传任意大的pageSize,一次全量查询会把数据库IO打满。
public PageResult<DeviceLogVO> queryLogPage(LogQuery query) { // 强制校验:每页最大100条 if (query.getPageSize() > 100) { query.setPageSize(100); } Page<DeviceLog> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<DeviceLog> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(DeviceLog::getDeviceId, query.getDeviceId()) .orderByDesc(DeviceLog::getCreateTime); Page<DeviceLog> result = deviceLogMapper.selectPage(page, wrapper); // 转VO返回,不直接暴露实体类 return convertToPageResult(result); }这里又有一个细节:转VO返回,不直接暴露实体类。数据库实体类中的字段(比如网关ID、逻辑删除标记)不适合直接交给前端,通过VO对象做一层字段过滤,既安全又灵活。
6. 权限控制和多用户家庭:不是登录一下就叫权限系统
6.1 JWT登录认证流程
用户认证我选用JWT而不是Session,主要是为了前后端分离和无状态扩展。流程如下:
- 用户提交用户名密码
- 服务端校验密码(BCrypt加密对比)
- 生成JWT令牌(有效期设定为24小时)
- 前后端约定:请求头
Authorization: Bearer <token> - 拦截器校验token,解析用户信息放入
ThreadLocal上下文
核心拦截器:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { String jwt = token.substring(7); try { Claims claims = JwtUtil.parseToken(jwt); Long userId = claims.get("userId", Long.class); // 存入ThreadLocal,方便Controller层获取 UserContext.set(userId); return true; } catch (Exception e) { // token无效,返回401 } } response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } }6.2 家庭权限模型:家人和设备之间的绑定关系
这套系统的权限模型比单用户体系要复杂一层,因为智能家居天然是家庭级的应用。爸爸、妈妈、孩子可能都有自己的账号,但控制的是同一套设备。
我的模型是:一个用户有一个主家庭(也可以创建多个家庭),用户通过home_member表和家庭绑定。设备通过home_id归属到家庭。操作设备时,先校验该设备所属家庭是否包含当前用户。
SQL层面做个简单的exists判断:
SELECT COUNT(*) FROM device d WHERE d.id = #{deviceId} AND d.home_id IN ( SELECT home_id FROM home_member WHERE user_id = #{userId} )这个模型虽然简单,但已经能覆盖绝大多数家庭场景。要做更强权限控制的,还可以在home_member表加一个role字段,区分管理员和普通成员,管理员能添加/删除设备,普通成员只能控制设备。
6.3 密码存储和防暴破
用户密码绝对不能明文存储,我用Spring Security的BCryptPasswordEncoder做加密。BCrypt的特点是有内置盐,每次加密结果不同,同一个密码存出来的hash都不一样,暴力破解的成本非常高。
登录接口我还加了简单的防爆破逻辑:连续5次密码错误后,账号锁定15分钟。这个功能在真实项目里非常刚需,别等到被人扫了再补。
7. 定时任务与数据统计:让系统自己“过日子”
7.1 Spring Boot定时任务的使用边界
设备状态的统计报表(家庭设备在线时长、电量统计、设备操作次数)靠实时查询设备日志来做,根本扛不住。我的方案是Spring Boot自带的@Scheduled定时任务,每天晚上凌晨1点批处理前一天的数据。
@Component public class StatisticsJob { @Resource private DeviceLogMapper deviceLogMapper; @Resource private DeviceDailyStatMapper dailyStatMapper; @Scheduled(cron = "0 0 1 * * ?") public void calculateDailyStats() { // 查前一天日志 List<DeviceLog> yesterdayLogs = deviceLogMapper.selectLastDay(); // 按设备+小时聚合 Map<String, Long> stats = yesterdayLogs.stream() .collect(Collectors.groupingBy( e -> e.getDeviceId() + "_" + hourOf(e.getCreateTime()), Collectors.counting() )); // 写入统计表 dailyStatMapper.batchInsert(stats); } }这里想重点提一个Spring Boot定时任务的坑:单线程陷阱。默认情况下,Spring Boot的定时任务只有一个线程,如果你定义了好几个@Scheduled方法,它们会排队执行。一个任务跑得慢会导致后面的任务全部延迟。解决方法是配置异步任务线程池:
@Configuration public class ScheduledConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }这就避免了“一个任务卡住,全家任务拖死”的尴尬。
7.2 设备在线时长统计的业务价值
设备在线时长不只是给用户看个数据,它本身是有分析价值的。比如一个空气净化器每天只运行2小时,但客厅PM2.5指数一直超标,说明用户的净化器摆放位置或者使用习惯可能有问题。这些数据如果设计得好,可以直接转化成产品迭代建议和用户服务方案。
8. 前端的控制台和可视化页面
8.1 前端框架选型:Vue 3 + Element Plus
后端数据结构和接口设计好了,前端要能直观地把数据用起来。我选择了Vue 3 + Element Plus,原因很简单:组件库丰富、文档完善、上手快。整个前端项目用Vite构建,工程结构清晰。
核心页面包括四块:
- 登录/注册页
- 设备总览页(卡片形式展示所有设备和当前状态)
- 设备控制台(根据设备类型动态渲染控制面板)
- 自动化规则页(可视化配置联动规则)
8.2 设备控制面板的动态渲染
设备控制面板是整个前端最需要动脑子的一部分。灯泡需要开关和亮度滑块,空调需要温度调节和模式切换,传感器只需要显示数值。怎么统一处理?
我的做法是前后端约定一套设备类型协议。后端返回设备时带上deviceType字段,前端维护一个组件映射表:
const deviceComponents = { 'light': LightController, 'air_conditioner': AirConditionerController, 'curtain': CurtainController, 'sensor_temperature': TemperatureSensorCard, 'sensor_humidity': HumiditySensorCard, 'sensor_air_quality': AirQualityCard }渲染时通过<component :is="deviceComponents[device.deviceType]" :device="device" />动态绑定组件。以后新增一种设备类型,不需要改路由和页面结构,只要增加一个对应的控制组件,并注册到映射表里就行。这个方案扩展性极好,是我前端的核心设计之一。
8.3 前后端联调:跨域问题和接口鉴权实践
前端开发服务器地址是localhost:5173,后端是localhost:8080,跨域是绕不开的问题。我在后端配置了全局跨域支持:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里有一个安全取舍:开发阶段允许所有来源跨域没问题,但生产环境必须把allowedOriginPatterns收紧成真实的域名,否则任何网站都能通过浏览器向你的接口发起请求,配合JWT被窃取,后果不堪设想。
9. 我踩过的几个比较大的坑,提前给你排掉
9.1 WebSocket session并发写导致的异常
最初我在网关上报处理中直接调用session.sendMessage(),但WebSocketSession并不是线程安全的。当多个业务线程同时往同一个连接写消息时,会抛出IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING]。
解决方法是给每个连接加一个写锁,或者在发送前用synchronized修饰发送方法。我在GatewaySessionManager里为每一个session封装了一个独立的写队列,把发送任务串行化:
public void sendCommand(String gatewayId, CommandMessage command) { WebSocketSession session = sessionMap.get(gatewayId); if (session != null && session.isOpen()) { // 通过netty或自建队列确保同一session的写串行 ConcurrentLinkedQueue<CommandMessage> queue = writeQueues.computeIfAbsent(gatewayId, k -> new ConcurrentLinkedQueue<>()); queue.offer(command); // 触发发送,确保同一session同时只有一个发送线程 trySessionSend(gatewayId); } }这是一个隐蔽但一旦出现就极其痛苦的并发问题,你如果做WebSocket通信,早晚会撞上,提前了解能省下大量排查时间。
9.2 MyBatis-Plus的JSON字段映射问题
MyBatis-Plus在实体类中映射MySQL的JSON字段时,默认不支持自动转换。前端传过来的状态JSON无法直接写入数据库,查出结果也无法直接转为Map对象。
解决方法是自定义一个JacksonTypeHandler,在实体字段上显式声明:
@TableField(value = "status_value", typeHandler = JacksonTypeHandler.class) private Map<String, Object> statusValue;另外记住一条容易踩的坑:使用@TableName(autoResultMap = true)开启自动结果映射,否则查询时typeHandler不生效。
9.3 Spring Boot版本太高导致的循环依赖报错
Spring Boot 2.6之后默认禁止了Bean的循环依赖。很多人写的Service互相引用在旧版本能跑,升级到新版以后直接启动失败。我实际项目里遇到过DeviceService依赖RuleEngineService,而RuleEngineService又依赖DeviceService的场景。
解决办法有两个:
- 从设计上就避免循环依赖,把公共方法抽到单独的组件里
- 如果你确实受限于旧代码,在配置文件中加一行
spring.main.allow-circular-references=true(但不推荐)
我更推荐第一个。循环依赖往往是代码结构出了问题,拆开之后反而更清晰。
9.4 设备控制响应的时延问题:必须异步化
在设备控制时,最初我实现了同步等待网关ACK。网关收到指令后处理设备,设备真正执行完动作才返回ACK,整个链路可能耗时1~2秒。如果这时候用户连续点击开关,前端会累积好几个请求,体验非常糟糕。
优化方案:指令下发接口收到网关的ACK消息后只更新设备状态,不等待设备底层执行结果,前端直接根据操作结果做状态乐观更新(先改UI,再等后端确认)。如果5秒内没有同步到新的设备状态,则回滚UI并提示失败。这种“先响应、后对账”的模式在很多物联网场景都很实用。
10. 项目跑起来以后:部署、调试和验收的完整建议
10.1 本地完整启动环境清单
如果你从源码开始跑,下面这些环境务必先准备齐,否则很容易在起项目阶段就被劝退:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8 / 11 | Spring Boot 2.7稳定支持 |
| Maven | 3.6+ | 项目管理工具 |
| MySQL | 5.7 / 8.0 | 业务数据存储 |
| Redis | 5.0+(可选) | 用于JWT黑名单和网关心跳缓存 |
| Node.js | 16+ | 前端构建环境 |
启动后端:导入Maven项目后,修改application.yml里的数据库连接信息,执行mvn spring-boot:run即可。
启动前端:进入前端目录,依次执行npm install和npm run dev。
10.2 没有真实硬件怎么办:模拟网关工具
很多朋友拿到项目后问的第一句话是:我没有智能硬件,这套系统能玩起来吗?当然能。
我在项目里附带了一个模拟网关的Java客户端,它会做三件事:
- 向服务端注册并建立WebSocket连接
- 定时上报随机温度、湿度数据(模拟传感器)
- 接收服务端下发的指令,打印并模拟设备响应
你只需要启动这个模拟器,就能在管理后台看到设备上线、数据上报、指令下发、联动规则触发这些效果。这个模拟器本身就是理解整个通信协议最好的学习材料。
10.3 生产部署:服务器上跑起来的最小配置
如果你要把项目部署到云服务器上,一个最简但完整的部署架构如下:
- 安装JDK 11 + MySQL 8.0 + Nginx
- 将后端打成jar包,用
nohup java -jar smart-home.jar &启动 - 前端打包成静态文件,通过Nginx托管
- Nginx配置反向代理,把
/api和/ws路径转发到后端服务 - 在安全组开放80端口(WebSocket通过HTTP协议进行升级,不需要额外开放8080)
Nginx配置节选:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }这里特别提醒:WebSocket的代理必须设置Upgrade和Connection头,否则前端WebSocket连接会一直握手失败,报101切换协议错误。
10.4 验收清单:怎么判断系统是不是真的“做完”了
把系统跑起来之后,不要只看界面能不能打开。我给自己定的验收标准是这样的,供你参考:
- 模拟网关启动后,后台设备列表能在几秒内显示在线状态
- 设备上报的温湿度数据能实时刷新到图表中
- 创建一条“温度超过28度开空调”的规则,通过模拟器把温度改到29度,空调设备能自动收到开机指令
- 控制台点击关闭空调,空调状态能在1秒内同步为关闭
- 用另一个账号登录,无法操作不属于该家庭的设备
- 设备日志按照时间筛选能正确分页展示
如果这几条都通过了,恭喜你,这个项目已经不只是“能跑”,而是“真正能用”了。
项目源码结构速览
最后看一眼我整理好的项目工程结构,拿到源码后你会更快定位自己想要的内容:
smart-home-system ├── backend │ ├── src/main/java/com/smarthome │ │ ├── config # WebSocket、跨域、定时任务配置 │ │ ├── controller # 对外REST接口 │ │ ├── service # 业务逻辑层 │ │ ├── mapper # MyBatis-Plus数据访问层 │ │ ├── entity # 数据库实体 │ │ ├── dto # 前端交互对象 │ │ ├── ws # WebSocket握手、消息分发 │ │ ├── rule # 规则引擎核心 │ │ └── common # 统一返回、异常处理、工具类 │ └── src/main/resources # application.yml、SQL初始化脚本 ├── frontend │ ├── src/views # 前端页面组件 │ ├── src/api # 接口访问封装 │ └── src/router # 路由配置 ├── simulator # 模拟网关客户端 └── sql/smart_home.sql # 数据库初始化脚本如果你是要应付毕业设计答辩或者面试项目讲解,我最真诚的建议是:把源码吃透,尤其是WebSocket通信、规则引擎和权限控制这三块。你能亲手讲清楚这三个模块的来龙去脉,这个项目的含金量会远超一堆只写了CRUD的“项目经验”。当时做这套系统的时候,光是WebSocket并发发送和状态上报幂等这两个问题就各花了一个晚上去排查,但这些坑踩下来之后,后续再做任何长连接相关的系统,我都觉得心里非常有底。这就是一个真实项目能带给你的最大收获。
本文还有配套的精品资源,点击获取