从STM32到微信小程序:智能家居四层架构全链路开发实战
2026/9/16 8:19:47 网站建设 项目流程

简介:这是一套面向STM32、树莓派与微信小程序端的智能家居控制系统完整工程,后端采用Java SpringBoot架构,覆盖嵌入式设备、服务端接口与用户控制端三大层面,适合毕业设计、课程设计、工程实训及学科竞赛等场景。包内共1472个文件,包含C/C++与Java源码、硬件工程配置、Python辅助脚本、JSON/XML配置、前端小程序页面及说明文档等,压缩包约62.72MB,便于对照目录结构快速定位与二次开发。项目代码经严格测试运行,功能完整,可复现复刻,也可借鉴设计思路或扩展更多智能家居功能。当前已有46人学习浏览,适合具备一定嵌入式或Web开发基础、希望快速搭建完整项目的学习者参考使用。

1. 说句实话:这套系统最难的不是「写代码」,而是让四层之间不扯皮

智能家居控制系统的毕设常见套路是 STM32 负责采集和环境控制,微信小程序负责展示,背后再接一个 Java 的 Spring Boot 后端。但真正动手时卡住的地方往往不在驱动或页面,而在链路本身:串口协议定得不对、上报接口没有历史数据、小程序一退后台就收不到设备状态变化。STM32、树莓派、Java 的 Spring Boot 架构和微信小程序,四个组件单独都有成熟教程,放在一起却要解决两个核心问题:数据从单片机到手机经过哪几跳、每跳用什么格式;控制指令从用户点下开关到 GPIO 翻转要多长时间、失败丢在哪一步。下面按「链路设计 → 嵌入式端 → 网关与后端 → 小程序 → 联调」的顺序,把一套能跑通的方案拆开讲,毕设、课设、实训和竞赛都能直接抄,升级到真实物联网平台的思路在最后一章点一下。

2. 技术选型与链路设计:为什么是 STM32、树莓派、Spring Boot 和小程序四层

2.1 四层架构各自的职责边界

先把各层该干什么定死,联调时才能快速定位责任方:

层级承担设备核心职责常见开发环境/语言
感知执行层STM32(F103C8T6 最常见)GPIO 采温湿度、继电器开关、PWM 调光、解析串口指令KEIL MDK / C
边缘网关层树莓派 4B通过 USB 转串口读取 STM32 上报,与后端双向通信Python3 / pyserial
业务后台层云服务器或本机服务器设备数据入库、用户与设备权限、指令下发、WebSocket 推送Java Spring Boot
用户触达层微信小程序登录、仪表盘、开关控制、设备状态实时展示微信开发者工具

最容易被问倒的问题是:为什么中间要放一个 Spring Boot 后端,而不是让树莓派直连小程序?原因有两层。其一,微信小程序对wx.requestwx.connectSocket有域名校验,本地树莓派 IP 不能被配置成合法域名,线下演示时开发者工具可以勾选「不校验合法域名」,但真机预览就会被卡住。其二,树莓派通常在家里或实验室的 NAT 后面,没有公网入口,小程序想主动连它基本不现实。把 Spring Boot 部署到有公网访问能力的服务器上,小程序只和 HTTPS 域名通信,树莓派作为客户端主动维持长连接,整个拓扑才是可长时间运行的。

2.2 两条数据通道的设计:上行遥测与下行控制

我这套系统里最省心的做法,是让上行和下行走不同的协议通道,避免一个地方故障把两条链路同时拖死。

上行链路是 STM32 到小程序的方向。STM32 通过串口按 1Hz 到 2Hz 发送#TEMP:25.3,HUMI:60.1,LED:1\r\n这种行协议,树莓派网关程序解析成 JSON 后,用 HTTP POST 提交给 Spring Boot 的/api/device/report接口。后端把数据写入 MySQL 和 Redis 缓存,再通过 WebSocket 把同一份 JSON 推送向在线的小程序页面。

下行链路是用户操作到 GPIO 的方向。小程序调用POST /api/device/control,Spring Boot 做完鉴权和设备校验后,把指令通过 Redis 发布订阅推给树莓派网关;树莓派把#LED:1\r\n写到串口,STM32 收到后翻转引脚并回一个#LED:ACK\r\n,树莓派再把执行结果回调给后端,状态最终由后端统一维护。

这样设计的好处是每层只需要认识一种协议:STM32 只处理串口 ASCII 行,树莓派只做收发和格式转换,后端只看到 REST 请求和 WebSocket 消息,小程序不需要关心底层硬件。排错时先看哪一跳的数据格式不对,直接定位到具体模块,不用把整条链路从头查到尾。

2.3 为什么不用 MQTT 而用「HTTP + WebSocket」组合

很多竞赛项目会直接引入 MQTT broker,让 STM32、树莓派、后端都订阅同一主题。MQTT 在真实生产环境是更优解,但如果这是毕设或实训,我一般建议先别给题目加复杂度:引入 EMQX、配置发布订阅、处理 QoS 和遗嘱消息,每一个都会占掉一整个晚上的联调时间。而 Spring Boot 对spring-boot-starter-websocket的支持非常简洁,小程序原生支持wx.connectSocket,两端事件模型能直接对上。

先跑通 HTTP + WebSocket 这条链路,再去替换协议,整套系统的骨架也不会白做。就算评委在答辩时问「为什么不用 MQTT」,你也能说出两条对比结论:一是设备量在几十台以内时 HTTP 单次请求的开销可以接受;二是 WebSocket 能让小程序端像订阅事件一样接收状态变更,开发成本比维护一套 MQTT over WebSocket 的桥接低得多。等到需要接入更多品牌设备、需要离线消息和遗嘱机制时,再把树莓派网关换成 MQTT Client,后端保留现有 REST 接口,改动可控。

2.4 数据库表设计:一张设备表加一张上报日志表

数据库结构在写后端之前就定死,能省掉后面联调时改字段的麻烦:

CREATE TABLE `device` ( `id` int(11) NOT NULL AUTO_INCREMENT, `device_key` varchar(32) NOT NULL COMMENT '设备唯一标识,如 room1_dht', `device_name` varchar(64) DEFAULT NULL COMMENT '展示名称', `type` tinyint(4) DEFAULT '0' COMMENT '0-传感器 1-开关 2-调光', `last_status` varchar(128) DEFAULT NULL COMMENT '最新状态JSON', `last_report_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_dev_key` (`device_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `device_report_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `device_id` int(11) NOT NULL, `temp` decimal(5,2) DEFAULT NULL, `humi` decimal(5,2) DEFAULT NULL, `status_json` varchar(512) DEFAULT NULL COMMENT '扩展字段,存开关等状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_device_time` (`device_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

device_report_log专门留给趋势曲线和历史查询,last_status只存最新值,画仪表盘时直接读device表,不需要ORDER BY create_time DESC LIMIT 1这种性能不稳定的查询。status_json字段保持一个通用 JSON,等以后加窗帘、空调等设备时不用改表结构。

3. STM32 端的数据采集与控制执行:串口协议写严,后面联调才不遭罪

3.1 最小硬件连接与工程初始化

在 STM32F103C8T6 这颗最常见的芯片上,我用 USART1 做调试日志输出,USART2 通过 PA2(TX)/ PA3(RX)接到外部 USB 转 TTL 模块,树莓派侧再插一个 USB 转串口。DHT11 温湿度传感器接 PB12,继电器控制引脚用 PB13。接线时有个隐藏坑:DHT11 的数据线需要外接 4.7kΩ 上拉电阻,否则读到的数据会偶发跳变;继电器模块要确认是高电平触发还是低电平触发,直接决定引脚初始化时的默认电平,接反了会出现「上电就吸合」的灵异现象。

KEIL MDK 工程配置里,时钟树建议用 8MHz 无源晶振加 PLL 倍频到 72MHz。如果换成 HSI 内部时钟,串口波特率会有偏差,和树莓派对接 115200 时偶发乱码。很多人会搜「stm32 晶振电容计算」,其实 F103 的负载电容按 20pF 左右配对即可,关键是 HSE 起振后在SystemClock_Config里确认HAL_RCC_ClockConfig返回成功,别让芯片跑在异常频率下。

串口接收不要在主循环里用阻塞方式等字节。打开 USART2 的 RXNE 中断,每收到一个字节就存入一个环形缓冲区,主循环只负责检查缓冲区有没有完整的一行:

#include "stm32f1xx_hal.h" #include <stdio.h> #include <string.h> extern UART_HandleTypeDef huart2; uint8_t rx_buf[128]; volatile uint16_t rx_len = 0; void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_RXNE)) { rx_buf[rx_len++] = (uint8_t)(huart2.Instance->DR & 0x1FF); if (rx_len >= sizeof(rx_buf)) { rx_len = 0; } } } void Device_ReportOnce(void) { char line[64]; // 取当前继电器状态,拼成 ASCII 行协议 uint8_t led_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13); snprintf(line, sizeof(line), "#TEMP:%.1f,HUMI:%.1f,LED:%d\r\n", g_dht.temperature, g_dht.humidity, led_state); HAL_UART_Transmit(&huart2, (uint8_t *)line, strlen(line), 100); }

代码逻辑说明:snprintf把温度和湿度格式化成一位小数,LED字段直接读取 PB13 当前电平。协议固定用#开头、逗号分隔、\r\n结尾,是因为中间隔着树莓派,调试时用screenminicom连上串口,人眼能直接读出TEMP:25.3,HUMI:60.1,LED:1,比二进制结构体好排查得多。HAL_UART_Transmit的最后一个参数是超时时间 100ms,工程里如果启用了 RTOS,这里可以换成带信号量的发送接口,保证多任务下不互相打断。

如果 DHT11 某次读取失败,也要把上一次缓存的数据继续上报,让后端至少能看到设备在线。只上报变化值会引入「无变化不上报」的额外判断,毕设阶段没有必要,固定周期上报反而简单可靠。

3.2 下行指令解析:收到#LED:1再动作并回 ACK

下行协议与上行保持一致的风格:#LED:0\r\n代表关闭,#LED:1\r\n代表打开。收到指令后,只执行以#开头且字段名匹配的完整行,避免噪声数据触发误动作:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance != USART2) { return; } if (rx_len >= 2 && rx_buf[rx_len - 2] == '\r' && rx_buf[rx_len - 1] == '\n') { rx_buf[rx_len] = '\0'; if (strncmp((char *)rx_buf, "#LED:", 5) == 0) { uint8_t val = (rx_buf[5] == '1') ? GPIO_PIN_SET : GPIO_PIN_RESET; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, val); char ack[] = "#LED:ACK\r\n"; HAL_UART_Transmit(&huart2, (uint8_t *)ack, strlen(ack), 100); } rx_len = 0; } }

严格来说,串口中断里做字符串比较和 GPIO 翻转不是最佳实践,但这个量级的操作耗时极短,毕设工程里可以接受。更规范的做法是只在这里设置cmd_pending标志位,主循环轮询时再执行指令,避免长指令挤占中断处理时间。回调里直接回 ACK 的好处是快,树莓派端不用额外等待,响应延迟能控制在几毫秒内。

注意 HAL 库的坑:HAL_UART_Receive是阻塞接收,不能直接放在回调里等完整一帧。我用的是HAL_UARTEx_ReceiveToIdle配合回调,或者直接裸操作huart2.Instance->DR读寄存器。如果用中断方式,记得使能UART_IT_RXNE,而不是只调用一次HAL_UART_Receive_IT就完事。

3.3 上报周期、看门狗与掉线判断

上报周期设 1 秒最合适。DHT11 本身分辨率就是秒级,100ms 上报除了给数据库制造无意义写入,没有实际收益。115200 波特率下一帧 30 字节大约只需要 2.6ms,链路瓶颈根本不在串口。树莓派端超时判活设为 5 秒,连续 5 秒没收到有效帧就认为 STM32 离线,后端据此更新设备在线状态。

STM32 端务必开独立看门狗 IWDG,溢出周期设 4 秒,主循环末尾喂狗。这样 DHT11 偶发卡在拉低时序导致任务阻塞时,系统会自动重启,而不是让树莓派误判成永久离线。后端在处理掉线告警时,用 Redis 记录最近活跃时间,3 分钟内重复掉线只告警一次,避免围绕电机抖动、网络闪断刷屏。

4. 树莓派网关与 Spring Boot 后端:把串口数据变成 API

4.1 树莓派上的 Python 网关:pyserial 读串口,requests 上报

树莓派端我是用 Python3 写的,核心依赖只有pyserialrequests。安装前建议先执行sudo apt update,如果下载慢,就先把软件源换成国内镜像,这是树莓派 4B 上 Python 环境配置的老生常谈。STM32 通过 USB 转 TTL 接到树莓派 USB 口后,设备节点一般是/dev/ttyUSB0,可以用ls /dev/ttyUSB*确认。

import serial import requests import time SERIAL_PORT = "/dev/ttyUSB0" BAUD_RATE = 115200 BACKEND_URL = "http://your-springboot-host:8080/api/device/report" DEVICE_KEY = "room1_dht" def parse_line(line: str): # 示例:#TEMP:25.3,HUMI:60.1,LED:1 if not line.startswith("#"): return None segments = line.strip()[1:].split(",") data = {"device_key": DEVICE_KEY} for seg in segments: k, v = seg.split(":") key = k.lower() if key in ("temp", "humi"): data[key] = float(v) elif key == "led": data["led"] = int(v) return data def main(): with serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=2) as ser: while True: line = ser.readline().decode("utf-8", errors="ignore").strip() if not line: continue payload = parse_line(line) if payload: try: resp = requests.post(BACKEND_URL, json=payload, timeout=3) print(resp.status_code, payload) except requests.exceptions.RequestException as e: print("[report error]", e) time.sleep(0.05) if __name__ == "__main__": main()

代码逻辑说明:ser.readline()会一直读到换行符,timeout=2保证没有数据时最多阻塞 2 秒,不会死等。STM32 发来的\r\n经过strip()去掉尾部空白,parse_line再用#和逗号切片。半包或者乱码行不以#开头,直接返回None跳过,下一帧完整数据到达后自然恢复。

requests.post设置 3 秒超时,后端暂时不可用时网关不会卡死。异常只打印日志,继续读下一帧。这个设计隐含了一个重连机制:后端恢复后,下一帧上报自然成功,不需要额外写断线重连逻辑。

有个容易忽略的点:树莓派如果用自带 UART(GPIO14/15)而不是 USB 转串口,需要修改/boot/config.txt开启enable_uart=1,并且停掉系统串口控制台服务,否则/dev/serial0会被系统占用。USB 方案没有这些麻烦,所以我建议毕设优先用 USB 转 TTL 模块。

4.2 Spring Boot 后端:上报接收、指令下发与异步落库

后端我用 Spring Boot 单体应用,启动类加@EnableAsync让上报日志异步写入。用 MyBatis-Plus 还是 Spring Data JPA 都行,关键是接口方法职责单一,下面这个 Controller 足够说明设计:

@RestController @RequestMapping("/api/device") public class DeviceController { @Resource private DeviceService deviceService; @Resource private WsSessionManager sessionManager; @PostMapping("/report") public Result<Void> report(@RequestBody ReportDTO dto) { deviceService.updateStatus(dto); deviceService.saveReportLogAsync(dto); sessionManager.broadcast("device:" + dto.getDeviceKey(), JSON.toJSONString(dto)); return Result.ok(); } @PostMapping("/control") public Result<Void> control(@RequestBody ControlDTO dto) { boolean success = deviceService.sendCommandToGateway(dto); return success ? Result.ok() : Result.error(500, "gateway offline"); } }

updateStatus负责更新device.last_statuslast_report_timesaveReportLogAsync@Async("reportExecutor")隔离线程池,避免上报接口被 MySQL 慢查询拖住。假设 50 台设备每 2 秒上报一帧,每秒也就 25 次写入,一个专用线程池配 2 个核心线程足够。

控制指令下发我用的是「Redis 发布订阅」:Spring Boot 把{deviceKey, cmd, value}写入cmd:queue频道,树莓派网关线程订阅该频道后写串口。网关与后端之间不需要保持 HTTP 轮询,指令延迟在毫秒级,而且树莓派在不同网段时也不需要暴露任何端口。如果想省掉 Redis,可以让树莓派每秒轮询一次待执行指令表,但那样开关灯会有肉眼可见的延迟,不如发布订阅干净。

4.3 WebSocket 服务端:会话池、广播与失效清理

WebSocket 部分用spring-boot-starter-websocket,注册一个TextWebHandler维护在线会话:

@Component public class DeviceWebSocketHandler extends TextWebHandler { private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { String userId = session.getAttributes().get("userId").toString(); SESSIONS.put(userId, session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { SESSIONS.values().remove(session); } public void broadcast(String userId, String message) { WebSocketSession session = SESSIONS.get(userId); if (session != null && session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (Exception e) { SESSIONS.remove(userId); } } } }

会话池必须考虑失效连接。sendMessage在手机切后台或网络切换时会抛异常,不清理的话每次广播都会撞到一个坏 session。上面的broadcast方法捕获异常后顺手移除,防止内存泄漏。握手鉴权通过HandshakeInterceptor校验 URL 上的 token,token 由小程序的wx.login流程换取,后端用 Redis 维护 token 与 userId 的映射。

4.4 提前设好 Spring Boot 配置文件里的 3 个业务参数

有些参数不提前设,联调时会浪费大量时间:

spring: datasource: url: jdbc:mysql://localhost:3306/smarthome?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai hikari: maximum-pool-size: 10 connection-timeout: 30000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 server: port: 8080 tomcat: max-threads: 200 accept-count: 200

参数说明:maximum-pool-size: 10对几十台设备的场景足够,调大反而浪费数据库连接;serverTimezone=Asia/Shanghai必须和 Jackson 的时区一致,否则小程序拿到的上报时间会差 8 小时,这是答辩现场最容易被问出破绽的地方。server.tomcat.accept-count默认只有 100,演示时如果评委手机和小程序同时在刷,一瞬间的并发超过 100 就会被拒绝,改成 200 能让突发流量排队而不是直接报错。

5. 微信小程序控制端:接口封装、WebSocket 与开关状态一致性

5.1 小程序的请求封装与合法域名配置

小程序原生项目的核心目录只有pagesutilsapp.js三块。app.jsglobalData里维护一个backendBaseUrl,所有请求和 WebSocket 连接都从这里取。开发阶段可以填局域网 IP 或云服务器 IP,但要注意wx.request在真机上强制要求 HTTPS 域名,ws://协议不配合法域名也连不上。开发者工具里勾选「不校验合法域名」只能解决本地预览,真机预览前一定要在微信公众平台把 HTTPS 域名加到 request 和 socket 合法域名列表里。

一个够用的请求封装就一个 Promise:

const BASE_URL = getApp().globalData.backendBaseUrl; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, method, data, header: { 'Content-Type': 'application/json' }, timeout: 5000, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); } else { reject(res.data); } }, fail: (err) => reject(err) }); }); } module.exports = { request };

所有页面const { request } = require('../../utils/request')后就能用async/await调接口。后端统一返回{code: 0, data: ..., msg: 'ok'},Promise 只 resolve 业务数据,错误统一走 reject,页面的try/catch只处理一种失败形态,不会每写一个接口就复制一套状态码判断。

5.2 仪表盘页面:启动时拉一次,之后靠 WebSocket 增量更新

设备状态页的实时性有两种实现路径:每 2 秒轮询一次设备最新状态,或者启动时拉一次 + WebSocket 推送。轮询代码量更少,但会造成不必要的无效请求,而且开关被远程改动时,页面最多滞后 2 秒。我在实训项目里更推荐后者,能撑起「实时控制」这个亮点:

Page({ data: { temperature: '--', humidity: '--', ledOn: false }, onLoad() { this.loadDeviceStatus(); this.connectWebSocket(); }, onUnload() { if (this.socketTask) { this.socketTask.close({ code: 1000 }); } }, async loadDeviceStatus() { const data = await request('/api/device/room1', 'GET'); this.setData({ temperature: data.temp, humidity: data.humi, ledOn: data.led === 1 }); }, connectWebSocket() { const token = wx.getStorageSync('token'); this.socketTask = wx.connectSocket({ url: `wss://your-backend-host:8080/ws?token=${token}` }); this.socketTask.onMessage((res) => { const msg = JSON.parse(res.data); if (msg.deviceKey === 'room1') { this.setData({ temperature: msg.temp, humidity: msg.humi, ledOn: msg.led === 1 }); } }); this.socketTask.onError(() => { setTimeout(() => this.connectWebSocket(), 3000); }); }, toggleLed(e) { const val = e.detail.value ? 1 : 0; request('/api/device/control', 'POST', { deviceKey: 'room1', cmd: 'led', value: val }).catch(() => { wx.showToast({ title: '指令发送失败', icon: 'none' }); }); } });

关于开关状态的一致性,我刻意没有在toggleLed里立即翻转ledOn。原因很简单:如果点了开关后端没送达或者 STM32 执行失败,页面先画成 ON,下一次 WebSocket 推送又跳回 OFF,用户会认为系统不稳定。先等指令执行结果通过 WebSocket 回推再更新 UI,虽然按钮少了「立刻响应」的手感,但状态绝对可信。如果评委纠结这一点,你可以补一句「生产级 App 会用乐观更新加超时回滚」,这就是很好的加分话术。

5.3 多用户与场景模式:从「能跑」到「能答辩」的进阶点

如果还想让项目更像产品,可以在设备表加owner字段,把wx.login换来的 openid 绑定到设备,后端在control接口里校验当前用户是否有操作权限。场景模式则可以抽象成「设备 + 动作」的列表,小程序发{scene: 'home', actions: [{deviceKey: 'led', value: 1}, {deviceKey: 'ac', value: 1}]},后端逐条执行。这里不要一次性把所有指令并发下发,因为 STM32 串口是单通道,多设备同时动作时指令会排队,场景执行结果要等所有 ACK 都回来再提示用户。

6. 联调验证:从 STM32 串口帧到 Spring Boot 接口的 3 个高频坑与一条 SQL 曲线

6.1 三段式联调:先看串口,再看接口,最后看推送

联调的第一现场是树莓派终端,执行screen /dev/ttyUSB0 115200,能看到#TEMP:25.3,HUMI:60.1,LED:1一行行滚动,说明 STM32 到树莓派这段物理链路通了。如果屏幕无输出,优先检查 USB 转串口驱动和 TX/RX 是否交叉,以及两端是否共地;杜邦线超过 20 厘米时,高速率下偶发乱码是正常现象,可以先降到 9600 波特率验证通路,再回到 115200 验收。

串口链路正常后,用 curl 模拟树莓派网关直接测后端,不依赖任何硬件:

curl -X POST http://localhost:8080/api/device/report \ -H "Content-Type: application/json" \ -d '{"deviceKey":"room1","temp":26.1,"humi":58.0,"led":1}'

返回{"code":0}后,查日志表确认落库:mysql -e "select * from device_report_log order by id desc limit 5;" smarthome。这个 curl 还能在硬件还没到的时候,先把小程序页面和后端所有功能跑起来,非常值得养成习惯。

6.2 Spring Boot 三个高频问题的快速定位

java.net.BindException: Address already in use说明上一次进程没退干净,lsof -i:8080找到 PID 杀掉即可;Communications link failure是 MySQL 的wait_timeout默认 8 小时回收了空闲连接,Hikari 连接池需要探活,在配置里加connection-test-query: SELECT 1能解决;WebSocket 握手出现 403 时,优先查HandshakeInterceptor里对 token 的校验逻辑,而不是怀疑跨域,小程序 WebSocket 不受浏览器同源策略限制,这个 403 绝大多数是业务拦截器返回的。如果小程序端想确认连接细节,可以在开发者工具的 Network 面板看 WS 帧,不必额外抓包。

6.3 一条 SQL 画出 24 小时温湿度曲线

让答辩更有说服力的技巧,是把上报链路沉淀成可视化数据。按小时聚合最近 24 小时的平均温湿度:

SELECT DATE_FORMAT(create_time, '%H:00') AS hour_point, ROUND(AVG(temp), 1) AS avg_temp, ROUND(AVG(humi), 1) AS avg_humi FROM device_report_log WHERE device_id = 1 AND create_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d %H:00') ORDER BY hour_point;

小程序拿到结果后用 canvas 画折线图。这条 SQL 的价值在于同时验证了上报链路的连续性:如果设备中途掉线,曲线会出现断档,比任何日志都直观。注意AVG在某一小时没有数据时返回 NULL,绘图前把空值替换为前一个小时的值,避免画布出现缺口;也可以把这个聚合查询做成@Scheduled定时任务,每 5 分钟把结果缓存到 Redis,小程序读缓存,接口响应会更快,也更接近工业监控系统的常见做法。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询