
简介基于 SpringBoot MyBatis MySQL Redis 的 Java 物联网智能家居项目资源包覆盖灯、插座、门磁、传感器、无线按钮、透传设备、四路无线开关与空调等设备的统一接入与控制适合 Java 后端开发者、物联网初学者及相关毕设选题人群参考学习。压缩包为 rar 格式内含 994 个文件以 224 个 Java 源码、217 个 XML 配置为主同时包含 YML/INI 配置文件、JS/HTML 前端页面、少量可执行脚本和图片素材整体约 196.99MB。资源已吸引 1297 人学习下载工程结构清晰按设备类型划分功能模块并提供数据库与中间件配置、前后端交互示例及部署辅助文件。借助这份资源可快速搭建一套可运行的智能家居系统深入理解设备接入、指令下发、数据持久化与 Redis 缓存应用等关键环节也便于在此基础上做二次开发与功能扩展。1. Java 物联网智能家居项目八类设备控制链路是怎么跑通的Java 物联网智能家居项目并不只是一套 SpringBoot 增删改查后台。把灯、插座、门磁、传感器、无线按钮、透传设备、四路无线开关、空调这八类设备放进同一个系统后真正的技术难点集中在设备建模、指令下发、状态回传、离线判断和缓存降级这几条链路上。许多 Java 开发者拿到源码后按常规管理系统的思路启动服务结果设备连不上或者控制后状态对不上问题多半出在表结构设计和指令回执处理而不是 MQTT 或 TCP 传输本身。这篇文章把我拆解该项目时沉淀下来的设计取舍、代码路径和排错经验完整过一遍适合正在做物联网毕业设计、智能家居实训项目或者准备深入 Java 物联网方向的工程师。2. 设备建模与指令表设计MyBatis 映射前先定数据结构2.1 device 主表 device_property 扩展属性不做八张表刚开始容易给灯建 lamp 表、插座建 socket 表、门磁建 door_sensor 表但对照八类设备的字段结构你会发现八成以上的属性是重合的设备编号、设备类型、所属房间、在线状态、最后上报时间、创建时间。差异只是在少量能力字段比如灯的亮度、空调的目标温度、透传设备的波特率。所以我在项目里采用 device 主表 device_property 扩展表的结构避免八张业务表来回维护。CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL UNIQUE COMMENT 设备唯一标识全局唯一, device_type TINYINT NOT NULL COMMENT 1灯 2插座 3门磁 4传感器 5无线按钮 6透传设备 7四路无线开关 8空调, name VARCHAR(64) DEFAULT , room_id BIGINT DEFAULT NULL COMMENT 所属房间, online TINYINT NOT NULL DEFAULT 0 COMMENT 0离线 1在线, last_seen DATETIME DEFAULT NULL COMMENT 最后上报时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_type_room (device_type, room_id), KEY idx_last_seen (last_seen) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备主表;device 表把公共字段全部收拢其中 device_id 是设备端固件烧录的编号不用自增主键关联这样上报和控制时都能直接用这个编号定位。room_id 关联房间表查询“客厅所有灯”时走idx_type_room索引即可。online 字段不直接依赖设备上报的在线布尔值而是根据 last_seen 与心跳周期计算出来的这样设备异常断电时不会留下一个永久在线状态。设备类型对应的差异字段用一个属性表承载CREATE TABLE device_property ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, prop_key VARCHAR(32) NOT NULL, prop_value VARCHAR(128) DEFAULT NULL, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_device_prop (device_id, prop_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备扩展属性表;灯可以存 brightness80空调可以存 target_temp26、modecool透传设备可以存 baud_rate9600。后端读设备详情时先查 device 主表再按 device_id 批量查属性组装成 Map。新增设备类型时不需要改表结构新的 prop_key 就是新能力这也是这套设计在多个设备类型下仍然能保持简洁的原因。用一张表对比八类设备的特点更直观device_type设备核心上报字段特有属性示例1灯status, brightnessbrightness2插座status, voltage, currentpower3门磁status(open/close)delay4传感器temperature, humidityilluminance5无线按钮action(1/2/3)key code6透传设备payload(hex)baud_rate7四路无线开关channel actionchannel_num8空调mode, target_temp, fan_speedswing2.2 设备类型枚举与控制动作映射设备类型在数据库里用 TINYINT 编码在 Java 里必须对应稳定的枚举。直接拿字符串“lamp”“socket”做判断会让代码里散落各种魔法值而且类型编码一旦定下来就不能再复用。我在项目里定义 DeviceType 枚举public enum DeviceType { LIGHT(1, 灯), SOCKET(2, 插座), DOOR_SENSOR(3, 门磁), SENSOR(4, 传感器), WIRELESS_BUTTON(5, 无线按钮), PASSTHROUGH(6, 透传设备), FOUR_WAY_SWITCH(7, 四路无线开关), AIR_CONDITIONER(8, 空调); private final Integer code; private final String desc; DeviceType(Integer code, String desc) { this.code code; this.desc desc; } public Integer getCode() { return code; } public static DeviceType fromCode(Integer code) { for (DeviceType type : values()) { if (type.code.equals(code)) { return type; } } throw new IllegalArgumentException(未知设备类型编码: code); } }控制动作是另一套维度。灯支持 on/off/setBrightness/query插座支持 on/off/query空调支持 on/off/setMode/setTargetTemp传感器基本只上报不控制。建议在服务启动时预加载一张“设备类型 → 支持动作”映射表private static final MapInteger, SetString SUPPORTED_ACTIONS new HashMap(); static { SUPPORTED_ACTIONS.put(1, new HashSet(Arrays.asList(on, off, setBrightness, query))); SUPPORTED_ACTIONS.put(2, new HashSet(Arrays.asList(on, off, query))); SUPPORTED_ACTIONS.put(8, new HashSet(Arrays.asList(on, off, setMode, setTargetTemp))); }做校验时直接用SUPPORTED_ACTIONS.get(deviceType).contains(action)不支持的返回 400。很多教材项目忽略这一步给空调下发 setBrightness 不报错等设备回执失败才暴露协议不支持问题定位周期太长。2.3 device_command 指令表控制不能直接 update 状态设备控制不能直接修改 device.status必须经历“新增指令 → 下发通道 → 设备回执 → 更新状态”的链路。指令表还有一个作用当用户反馈“灯没亮”先看 device_command 表里最后一次指令的 status 是几1 表示已发送没回执3 表示超时4 表示设备离线被拦截问题能快速定位到发送端还是设备端。CREATE TABLE device_command ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_id VARCHAR(64) NOT NULL COMMENT 指令唯一ID用于回执匹配, device_id VARCHAR(64) NOT NULL, action VARCHAR(32) NOT NULL, params_json VARCHAR(512) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待下发 1已下发 2设备确认 3超时 4失败, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_device_time (device_id, create_time), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT指令记录表;灯和插座的开关指令在 params_json 里可以传空对象或者带{channel:relay1}空调则带上温度和模式。request_id 由后端生成并下发给设备设备回执时原样带回来。status 的每一次变化都对应一次网络交互也是排查设备问题的凭证。超时扫描任务每 30 秒跑一次把 status1 且早于当前时间 10 秒的记录更新为 3避免和回执 update 互相覆盖。3. 灯、插座、门磁、传感器的控制与上报实现3.1 统一控制接口一个 Controller 处理所有设备类型拆分控制接口的第一版容易为每类设备建 Controller后面发现代码重复率太高。这里我改成统一入口RestController RequestMapping(/api/device) public class DeviceController { Resource private DeviceQueryService deviceQueryService; Resource private DeviceCommandService commandService; PostMapping(/{deviceId}/command) public ResultVoid sendCommand(PathVariable String deviceId, RequestBody CommandRequest request) { DeviceType type deviceQueryService.getDeviceType(deviceId); commandService.validateAction(type, request.getAction()); commandService.dispatch(deviceId, type, request); return Result.success(); } }CommandRequest 里的 action 是 on/off/setBrightnessparams 是一个 Map具体内容由设备类型决定。门磁和传感器一般不会走到这个接口因为它们的动作集合是空的validateAction 阶段直接拦截。这个设计让前端对接成本最低也方便后续接入新设备。3.2 指令处理 Service命令先入库再下发离线状态优先判断DeviceCommandService 是控制链路的重点。它要兼顾“指令必须保留”、“离线快速提示”和“回执可靠更新”三个目标流程如下Service public class DeviceCommandService { Resource private DeviceCommandMapper commandMapper; Resource private RedisTemplateString, String redisTemplate; Resource private DeviceGateway deviceGateway; public void dispatch(String deviceId, DeviceType type, CommandRequest request) { DeviceCommand command new DeviceCommand(); command.setDeviceId(deviceId); command.setAction(request.getAction()); command.setParamsJson(JSON.toJSONString(request.getParams())); command.setStatus(0); String requestId UUID.randomUUID().toString().replace(-, ); command.setRequestId(requestId); commandMapper.insert(command); Boolean online redisTemplate.hasKey(device:online: deviceId); if (!Boolean.TRUE.equals(online)) { commandMapper.updateStatus(requestId, 4); throw new BizException(设备离线指令未下发); } deviceGateway.publish(deviceId, command.toPayload()); commandMapper.updateStatus(requestId, 1); } }为什么不先判断在线再入库因为设备可能刚好在判断完成之后离线如果先入库至少能保留这次控制请求方便后续补发或追溯。在线判断用 Redis不使用 MySQL 查询因为在线状态是高频变化数据Redis 的 TTL 能自然处理心跳过期逻辑。deviceGateway.publish 是网关抽象在项目里具体可能是 MQTT 发布、TCP 下发或串口写入代码不动。回执更新单独做public boolean ack(String requestId) { int count commandMapper.updateStatusByRequestId(requestId, 2); return count 1; }对应的 MyBatis Mapper 接口定义如下Mapper public interface DeviceCommandMapper { int insert(DeviceCommand command); int updateStatusByRequestId(Param(requestId) String requestId, Param(targetStatus) Integer targetStatus); ListDeviceCommand listPending(Param(seconds) long seconds); }超时扫描的 SQL 用 create_time 做时间窗口查询select idlistPending resultTypeDeviceCommand SELECT * FROM device_command WHERE status 1 AND create_time lt; DATE_SUB(NOW(), INTERVAL #{seconds} SECOND) LIMIT 500 /selectSQL 里没加AND status 1的回执更新是为了避免回执晚到时被超时状态误拦。真实项目中我会在更新前再比较 create_time 是否超过超时窗口没超过才允许更新为 2。这个细节在大多数博客里不会提但现场环境很容易出现“指令显示超时设备实际已经执行”的状态错乱。3.3 门磁和传感器上报型设备只信设备上报不主动推状态门磁和传感器与灯、插座不同。灯和插座是“被动控制型”门磁传感器是“主动上报型”。门磁开关状态永远以门磁上报的 open/close 为准传感器温湿度则以周期上报数据为准后端不做主动控制。处理入口统一放到消息上报回调里public void onDeviceReport(String deviceId, MapString, Object payload) { Integer deviceType deviceQueryService.getDeviceTypeCode(deviceId); if (deviceType 3) { String doorStatus (String) payload.get(status); deviceMapper.updateStatus(deviceId, doorStatus); eventMapper.insert(DeviceEvent.builder() .deviceId(deviceId) .eventType(door) .eventValue(doorStatus) .build()); } else if (deviceType 4) { double temperature Double.parseDouble(payload.get(temperature).toString()); double humidity Double.parseDouble(payload.get(humidity).toString()); sensorMapper.insertSample(deviceId, temperature, humidity); checkSensorAlert(deviceId, temperature, humidity); } }门磁每次状态变化都记录一条事件用于回放“几点开门、几点关门”同时支持告警比如门长时间未关。传感器的采样数据不写 device_property因为那是当前状态不是历史数据。我单独建 sensor_sample 表按时间维度存分页查询和聚合统计都走这张表。上报数据的格式不一定都是 JSON。某些传感器会直接上报01 03 02 01 2C这样的 hex 报文后端需要先按项目协议拆帧把数据部分转成结构化字段再走上面的逻辑。拆帧代码在透传场景里更常见下一章会展开。这里总结一下控制型设备和上报型设备的处理差异设备类型控制方式上报方式主要处理链路灯主动控制状态上报Controller → Service插座主动控制计量上报Controller → Service门磁无控制状态漂变上报ReportHandler → 事件表传感器无控制周期采样上报ReportHandler → 采样表4. 无线按钮、四路无线开关、透传设备和空调的接入方式4.1 无线按钮和四路无线开关事件型设备只存事件无线按钮和四路无线开关都是事件型设备没有持续状态。无线按钮上报 1、2、3 表示单击、双击、长按四路无线开关会带 channel 编号比如 channel1 表示第一路开关被触发。如果按普通设备的思路去“更新开关状态”会得到一个意义不明的结果因为这些设备没有“当前状态”只有“按下时间”和“按下次数”。事件型设备在后端统一记录到 device_event 表CREATE TABLE device_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, event_type VARCHAR(32) NOT NULL, event_value VARCHAR(64) NOT NULL, request_id VARCHAR(64) DEFAULT NULL COMMENT 上报去重ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_device_time (device_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备事件表;事件落库后去匹配场景规则。比如四路无线开关的第 2 路绑定空调按下后联动指令“制冷 26 度”。联动目标指令依然走 device_command 表因此第 2 章的指令状态机完全复用。场景规则我一般放 scene_rule 表不在代码里硬编码这样才能让用户在前端自由配置场景。去重防抖是无线按钮模块最典型的坑。设备按键抖动可能让一条单击事件重复上报两三次如果直接落库并触发场景灯就会被快速开关多次。我用 RedisSETNX按 request_id 去重设备端上报时带上由时间和计数组合成的 request_id后端 1 秒内重复请求直接丢弃。4.2 透传设备原样留痕不解析不改写透传设备在智能家居项目里常指没有统一协议的 485 设备、串口服务器或者自定义传感器节点。它的接入原则是不透传、不解析、不截断。后端收到的 hex 报文原样写入日志表再按需转发给上层应用或上位机。CREATE TABLE passthrough_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, channel_id INT DEFAULT 0, data_type VARCHAR(16) DEFAULT hex, payload VARCHAR(1024) NOT NULL, process_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待消费 1已消费, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_device_time (device_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT透传日志表;透传数据的下游推送可以通过 MQTT topic 完成比如passthrough/{deviceId}/data。如果上位机需要原始数据订阅该 topic 就能实时拿到。payload 字段一定不要截断很多故障排查依赖原始报文对比截断会丢关键信息。透传设备的“下发”动作在控制表里 action 固定为passthroughparams_json 中放 payload 字段网关把它原样发到目标串口或网络端口。4.3 空调控制参数组合与红外指令编码空调的操控是八类设备里对协议最敏感的一类。红外空调不接收单独的温度指令它需要接收到一个包含模式、温度、风速的完整控制帧。所以空调控制接口的入参不能只有一个 temperature 字段。我一般把空调控制参数固定为三个字段并与 action 组合成这样的请求体{ action: setMode, params: { mode: cool, targetTemp: 26, fanSpeed: auto } }参数范围可以整理成一张表方便做入参校验参数名含义示例取值mode运行模式cool / heat / autotargetTemp目标温度16 - 30fanSpeed风速auto / low / high后端拿到参数后要转成空调协议帧。这段代码我统一收敛到一个方法里避免散落在控制服务各处public String buildAirCommand(String mode, int temp, String fan) { String modeCode; switch (mode) { case cool: modeCode 01; break; case heat: modeCode 02; break; case auto: modeCode 03; break; default: throw new IllegalStateException(unsupported mode); } String tempCode String.format(%02X, temp); String fanCode auto.equals(fan) ? A0 : B0; return modeCode tempCode fanCode; }注意这套编码只是结构示范实际红外码需按空调型号对照表替换。重要的是把“业务参数”和“协议编码”分离换品牌空调时只改 buildAirCommand 中的一个映射表。空调的当前状态如果读不到红外反馈通常用房间内的环境传感器间接判断传感器按 room_id 关联空调设备控制效果用温度变化曲线评估。5. Redis 缓存与上线前排错离线判定和指令超时怎么处理5.1 Redis 缓存设备在线状态TTL 就是离线计时器前面提到控制前要查在线状态这里把实现讲清楚。设备每次心跳或上报时后端执行一次带过期时间的写入redisTemplate.opsForValue().set( device:online: deviceId, 1, Duration.ofSeconds(90) );TTL 设置为心跳周期的 2 到 3 倍比如设备 30 秒上报一次90 秒内没有收到任何心跳Redis 自动过期在线状态就变成离线。这个设计避免了定时任务去批量更新 device.online 字段Redis 天然承担了状态缓存和自动清理两件事。在线查询用一条命令完成redis-cli EXISTS device:online:device123如果 key 存在说明在线不存在则需要回源查 MySQL 的 last_seen 作兜底。注意不要每次查询失败都把 online 字段刷成 0高频心跳下这种写库操作会拖垮主库。一千台设备按 30 秒心跳计算每秒约 33 次上报上万台就是三百多次只有 Redis 能抗住这种量级MySQL 需要留给核心业务。5.2 指令超时、协议粘包、缓存穿透的排查路径指令超时排查按三步展开。第一查 device_command 表看 statusSELECT device_id, action, status, create_time FROM device_command WHERE request_id 需要排查的指令ID;status1 表示已下发但未确认下一步抓设备端日志看是否收到了 MQTT 或 TCP 报文同时确认上报回执是否带有同一个 request_idstatus3 表示超时检查网络延迟和设备响应速度必要时把超时时间从 10 秒调整到 15 秒status4 表示离线拦截不是超时问题检查心跳。透传设备协议粘包的解法是先攒缓冲再按消息长度截断。把收到的 hex 按数据头找帧长度不够就保留到下次上报再拼避免把两帧数据当成一帧解析。缓存穿透发生在有人用不存在的 device_id 反复请求时解法是查询失败时对 key 缓存空值 30 秒再加一层布隆过滤器兜底。这几条路径覆盖了我排查这个项目时遇到的绝大多数线上问题。本文还有配套的精品资源点击获取