智慧农业微信小程序开发实战:从传感器到屏幕的完整链路

发布时间:2026/9/14 2:04:16
智慧农业微信小程序开发实战:从传感器到屏幕的完整链路 简介一套面向高校计算机相关专业学生的微信小程序智慧农业平台源码集成前端小程序、后端服务与物联网设备数据采集链路适合作为毕业设计、课程设计或项目初期原型。资源包共441个文件、约3.59MB其中js与json为前后端逻辑与数据配置wxss/wxml搭建小程序界面png/jpeg为界面与文档配图tsx/ts/wxs及md文档则包含部分业务组件、TypeScript模块与配置说明结构覆盖从设备数据上报到小程序展示的完整闭环。已有56人学习下载代码经测试可正常运行并附带设计文档可直接部署使用。对需要快速搭建智慧农业演示系统的初学者也可在现有代码基础上二次开发遇到运行问题可寻求远程教学支持。1. 智慧农业微信小程序的复杂度不在页面而在“从传感器到屏幕”的完整链路很多人拿到“微信小程序智慧农业平台前端后端物联网设备集成”这类项目第一反应是先把小程序页面画出来再去写后端接口。实际动手后会发现页面三两天就能搭完最难的部分是让土壤湿度传感器、ESP32 设备、MQTT 消息、后端服务和小程序图表这五层数据环环相扣中间任何一层断掉屏幕上就只剩一个永远转圈的 loading。准备做毕业设计、接私活或公司内部试点的人最需要的是把这条链路从选型到联调一次走通的能力而不是某个文件夹里的全部源码。本文从协议选型、数据模型、设备接入和前端联动四个层面给出一种可以直接复现的搭建思路。2. 前后端分离的通信骨架智慧农业小程序的登录态与请求封装2.1 登录态为何不直接拿 openid 当 tokengetApp() 与 wx.login 的分工微信小程序的鉴权和传统 Web 登录最大的区别在于前端拿不到用户密码只能通过wx.login()获取一个临时 code再把 code 交给后端去微信接口换 openid。常见做法是后端把 openid 映射为自建 token 返回给小程序后续请求通过Authorization: Bearer token携带而不是每次请求都调wx.login()。在智慧农业场景里一个农户账号会同时管理多个大棚设备小程序端需要在app.js的onLaunch里完成登录动作并把 token 存入wx.setStorageSync。之所以不把 openid 直接作为 token是因为 openid 是明文且不可吊销的字符串泄露后无法单独作废自建 token 则可以设置过期时间、绑定角色、按需刷新也方便后端后续做设备绑定关系校验。这是前后端分离项目里非常基础但经常被忽略的一层设计。2.2 request 统一封装baseURL、401 静默刷新与错误码约定智慧农业小程序的页面会反复请求设备列表、实时数据、历史曲线和控制指令如果在每个页面里单独写wx.request后期维护会非常痛苦。我会把所有请求收敛到utils/request.js统一处理三件事拼接域名、附加 token、识别业务错误码。// utils/request.js const BASE_URL https://api.example.com; function request(path, { method GET, data {}, needAuth true } {}) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { Content-Type: application/json, ...(needAuth token ? { Authorization: Bearer ${token} } : {}) }, success(res) { const { code, data, message } res.data || {}; if (res.statusCode 200 code 0) { resolve(data); } else if (res.statusCode 401) { // token 过期静默刷新而不是强制用户退出登录 refreshToken() .then(() request(path, { method, data, needAuth })) .catch(err reject(err)); } else { wx.showToast({ title: message || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常请检查网络, icon: none }); reject(err); } }); }); } function refreshToken() { return new Promise((resolve, reject) { wx.login({ success: (res) { wx.request({ url: BASE_URL /api/auth/login, method: POST, data: { code: res.code }, success: (res2) { wx.setStorageSync(token, res2.data.data.token); resolve(); }, fail: reject }); }, fail: reject }); }); } module.exports { request, BASE_URL };这段封装里最关键的是 401 分支。设备被强制断电重启、或后端 token 过期策略调整时小程序端第一次请求会拿到 401此时用wx.login()重新换 code 刷新 token并重放原请求用户完全无感知。注意refreshToken内部也走wx.request不要加Authorization头否则会死循环。另外uni-app 工程里可以把wx.request换成uni.requestwx.login换成uni.login整体流程一致。后端接口设计按资源划分比较完整的接口清单如下接口路径方法是否需要登录用途/api/auth/loginPOST否code 换 token返回用户信息与绑定大棚列表/api/device/listGET是获取当前用户绑定的设备含在线状态/api/device/{id}/telemetryGET是获取最近 N 条传感器记录用于曲线绘制/api/device/{id}/commandPOST是下发控制指令例如打开水泵、关闭风机/api/alert/listGET是获取阈值告警记录2.3 后端拦截器如何校验 token 并识别“游客 / 农户 / 管理员”后端需要一个拦截器统一校验 token并把用户 ID、角色信息写入ThreadLocal这样 Controller 里不需要重复解析 token。下面的 Java 代码是 Spring Boot 项目里常见写法注意放行OPTIONS预检请求和登录接口本身。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String authorization request.getHeader(Authorization); if (authorization ! null authorization.startsWith(Bearer )) { String token authorization.replace(Bearer , ); String userId tokenService.verify(token); if (userId ! null) { UserContext.set(userId); return true; } } response.setStatus(401); return false; } }这段代码里tokenService.verify()返回 userId 而不是 openid原因在前文已提到业务逻辑只认内部用户 IDopenid 只在登录换 token 时使用一次。前端在 2.2 节的封装中已经处理了 401 刷新所以拦截器只要简单返回 401 即可不需要跳转逻辑。如果项目里需要区分管理员和普通农户可以在 token 里附带角色字段拦截器解析后写入UserContext供后续接口判断“当前用户是否有权控制某台设备”。3. 后端数据模型与设备影子智慧农业平台的表结构、Redis 实时态与控制指令下发3.1 设备表与采集记录表拆开的理由一张表装不下时序数据智慧农业平台里最核心的两张表是设备表和传感器记录表。第一次做这类的项目最容易把“设备当前温度”和“历史温度曲线”混在一张表里导致表越来越大查询越来越慢。合理的做法是把设备基础信息和采集数据拆分设备表只存静态属性和在线状态采集表只存时间序列。设备表device建议至少包含以下字段字段类型说明device_idvarchar(32) 主键设备 SN同时作为 MQTT clientId 后缀farm_idbigint所属农场/大棚 ID绑定用namevarchar(64)设备名称例如“1号大棚”statustinyint0 离线1 在线last_online_atdatetime最近一次与 MQTT broker 心跳的时间created_atdatetime创建时间采集记录表telemetry建议如下设计字段类型说明idbigint 主键自增记录 IDdevice_idvarchar(32)冗余设备 SN方便按设备查询tempdecimal(5,2)空气温度单位摄氏度humidecimal(5,2)空气相对湿度单位百分比soildecimal(5,2)土壤湿度单位百分比created_atdatetime采集时间物联网设备有内部时钟但建议以后端入库时间为准采集表按天、按月做分区或归档是后话初期直接在created_at上建索引就够跑。真正需要提前设计的是写入策略设备每 5 秒上报一条单设备一天约 17000 条如果 10 台设备就是 17 万条全部同步写 MySQL 会拖垮接口。常见做法是设备上报先进 Redis 或消息队列由后端异步批量写入数据库避免高频写库阻塞业务接口。3.2 Redis 设备影子为什么数据库撑不住高频刷新小程序页面上要实时显示“当前温度 26.4℃”如果每次都从 MySQL 里查ORDER BY created_at DESC LIMIT 1数据库压力会非常大。更合理的方案是用 Redis 保存每台设备的“设备影子”即设备最新状态的一份快照。public MapString, Object getDeviceStatus(String deviceId) { String key device:status: deviceId; MapObject, Object entries redisTemplate.opsForHash().entries(key); if (entries.isEmpty()) { Device device deviceMapper.selectById(deviceId); if (device null) { return null; } MapString, Object shadow new HashMap(); shadow.put(online, device.getStatus()); shadow.put(lastOnlineAt, device.getLastOnlineAt()); Telemetry last telemetryMapper.selectLastByDeviceId(deviceId); shadow.put(temp, last ! null ? last.getTemp() : null); shadow.put(humi, last ! null ? last.getHumi() : null); shadow.put(soil, last ! null ? last.getSoil() : null); redisTemplate.opsForHash().putAll(key, shadow); redisTemplate.expire(key, Duration.ofMinutes(5)); return shadow; } return entries.entrySet().stream() .collect(Collectors.toMap(e - e.getKey().toString(), Map.Entry::getValue)); }这段代码在缓存未命中时回源数据库并设置 5 分钟过期。注意device:status:这个 key 的过期时间不是随便设的设置太短热点设备会频繁回源数据库设置太长设备离线状态在页面上更新不及时。5 分钟是一个比较平衡的值如果设备数量超过 100 台可以把过期时间改成 1 分钟并配合订阅消息主动刷新缓存。设备影子还有一个额外的好处后端判断设备是否在线时不需要查设备表可以直接看 Redis 字段里的online和lastOnlineAt页面加载速度会快一个数量级。3.3 控制指令下发流程接口写库 MQTT 发布智能灌溉、风机联动这类功能最终都要落到“后端下发指令给设备”。指令下发接口要解决两个问题一是权限校验二是可靠性。权限层面校验当前用户是否绑定了该设备可靠性层面则需要把指令先写入命令表再通过 MQTT 发布而不是直接发消息不管结果。PostMapping(/device/{id}/command) public ResultVoid sendCommand(PathVariable String id, RequestBody CommandDTO cmd) { // 1. 校验当前用户是否拥有这台设备 deviceService.checkBinding(id, UserContext.getUserId()); // 2. 写入命令表防止 MQTT 发布失败后指令丢失 CommandLog log new CommandLog(); log.setDeviceId(id); log.setType(cmd.getType()); // water / fan / light log.setAction(cmd.getAction()); // on / off log.setStatus(0); // 0 待确认 commandLogMapper.insert(log); // 3. 发布 MQTT 消息payload 里带命令明细 mqttGateway.sendCommand(id, cmd.getType(), cmd.getAction(), log.getId()); return Result.ok(); }命令表command_log在这里扮演了“本地消息表”的角色。设备执行完操作后会上报一条回执后端收到回执后把status改为 1如果超过 30 秒没收到回执定时任务可以把status为 0 的记录重新发布一次。这样即使 MQTT 消息丢失也能通过命令表发现并补偿而不是用户点了按钮没反应却找不到原因。4. 物联网设备接入MQTT 主题规划与 ESP32 端到端上报4.1 交互协议选型MQTT 比 HTTP 轮询更合适的三点理由很多从前后端开发转过来的人第一反应是“设备用 HTTP 上报就行后端 GET 一下不就好了”。对智慧农业这种低功耗、弱网、长连接的场景HTTP 轮询有几个绕不开的问题轮询间隔太短会大量消耗设备电量和运营商流量间隔太长又丧失实时性HTTP 是单向请求平台想主动下发开泵指令时设备必须时刻监听端口NAT 环境里很难做到。MQTT 基于发布订阅模型设备只维持一条长连接平台下发指令时 broker 直接推给设备反向控制不需要设备暴露任何端口。MQTT 的第三个优势是 QoS 机制。QoS 1 保证消息至少送达一次对告警类数据尤其重要QoS 0 则适合高频传感器数据。智慧农业里传感器上报用 QoS 0 或 QoS 1 都行控制指令必须 QoS 1因为“打开水泵”这条消息丢了大棚就浇不上水。4.2 Topic 与 QoS 规划设备上报、平台下发、回执三组主题MQTT 的 Topic 设计决定了后续扩展是否顺畅。我会把主题拆成三组用设备SN做隔离Topic 模板方向QoSpayload 示例说明dev/{deviceId}/up设备→平台0 或 1{t:263,h:41,s:380}传感器数据上报高频低可靠性要求dev/{deviceId}/down平台→设备1{type:water,action:on,cmdId:1024}控制指令下发dev/{deviceId}/ack设备→平台0{cmdId:1024,ts:1699999999}指令执行回执一个小技巧是Payload 里的键名尽量设计得短。设备每 5 秒上报一条数据一年下来流量差异非常可观temp比temperature少 5 个字节在窄带物联网场景里是实打实的成本。后端解析时再映射成业务对象。4.3 ESP32 端采集上报的最小代码框架端侧设备最常见的组合是 ESP32 DHT22 温湿度传感器 土壤湿度传感器通过 ADC 读取。这里给出一个能跑通上报链路的最小框架使用 Arduino IDE 配合 PubSubClient 库。#include WiFi.h #include PubSubClient.h #include DHT.h #define DHTPIN 4 #define DHTTYPE DHT22 #define SOIL_PIN 34 DHT dht(DHTPIN, DHTTYPE); WiFiClient espClient; PubSubClient mqtt(espClient); const char* WIFI_SSID YOUR_SSID; const char* WIFI_PASS YOUR_PASS; const char* MQTT_HOST broker.example.com; const int MQTT_PORT 1883; const char* DEVICE_ID AG0001; const char* CLIENT_ID ESP32-AG0001; void publishSensor() { float h dht.readHumidity(); float t dht.readTemperature(); int soilRaw analogRead(SOIL_PIN); int soilPercent map(soilRaw, 0, 4095, 100, 0); if (isnan(h) || isnan(t)) return; char payload[64]; snprintf(payload, sizeof(payload), {\t\:%d,\h\:%d,\s\:%d}, (int)(t * 10), (int)(h * 10), soilPercent); mqtt.publish(dev/AG0001/up, payload, false); } void callback(char* topic, byte* payload, unsigned int length) { // 下游指令解析示例只处理 on/off控制 GPIO 并回复 ack String msg; for (int i 0; i length; i) { msg (char)payload[i]; } if (msg.indexOf(\action\:\on\) 0) { digitalWrite(2, HIGH); // mqtt.publish(dev/AG0001/ack, {\cmdId\:1024}, false); } } void setup() { Serial.begin(115200); pinMode(2, OUTPUT); dht.begin(); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() ! WL_CONNECTED) { delay(500); } mqtt.setServer(MQTT_HOST, MQTT_PORT); mqtt.setCallback(callback); mqtt.connect(CLIENT_ID); mqtt.subscribe(dev/AG0001/down); } void loop() { if (!mqtt.connected()) { mqtt.connect(CLIENT_ID); mqtt.subscribe(dev/AG0001/down); } mqtt.loop(); static unsigned long lastSend 0; if (millis() - lastSend 5000) { publishSensor(); lastSend millis(); } }map(soilRaw, 0, 4095, 100, 0)是把 ESP32 的 12 位 ADC 原始值映射成湿度百分比注意不同土壤传感器模块的模拟输出范围不一样需要根据实际硬件标定。(int)(t * 10)是在端侧把温度放大 10 倍成整数避免浮点传输的精度损耗。CLIENT_ID必须全局唯一如果两台设备用了同一个 clientIdMQTT broker 会互踢导致设备反复掉线。4.4 后端 MQTT 客户端配置与载荷透传后端需要订阅dev//up和dev//ack两张通配主题。Spring Boot 项目常用 Eclipse Paho 作为客户端配置如下Component public class MqttClientFactory { Value(${mqtt.broker:ssl://broker.example.com:8883}) private String broker; Bean(destroyMethod disconnect) public MqttClient mqttClient() throws MqttException { String clientId backend- UUID.randomUUID(); MemoryPersistence persistence new MemoryPersistence(); MqttClient client new MqttClient(broker, clientId, persistence); MqttConnectOptions opts new MqttConnectOptions(); opts.setCleanSession(true); opts.setAutomaticReconnect(true); opts.setConnectionTimeout(10); opts.setKeepAliveInterval(30); opts.setUserName(agriculture); opts.setPassword(agriculture123.toCharArray()); client.connect(opts); client.subscribe(new String[]{dev//up, dev//ack}, new int[]{1, 0}); return client; } }两个参数需要注意setKeepAliveInterval(30)表示每 30 秒发送一次心跳broker 在 90 秒内没收到心跳会判定设备离线setAutomaticReconnect(true)只是自动重连不代表消息不丢QoS 1 消息与命令补偿仍然依赖业务层的 command_log。生产环境里broker应写成ssl://形式避免明文传输传感器数据与控制指令。后端收到dev//up的 payload 后解析 JSON 并写入 Redis 影子与 MySQL 采集表整个过程保持“设备原样上传、后端统一解析”的原则不在设备端做复杂的数据加工方便后续替换传感器型号。5. 小程序页面的数据联动实时刷新、曲线图表与自动控制5.1 页面数据源轮询与 WebSocket 的取舍智慧农业小程序的主页面通常包含“当前温湿度”“最近 24 小时曲线”“设备在线状态”三块内容。要不要用 WebSocket 实时推送取决于业务对实时性的要求。如果只是 5 秒刷新一次传感器数值轮询完全够用而且实现简单、天然兼容小程序生命周期如果要做告警弹窗或控制指令回执的即时反馈WebSocket 是更合适的选择。使用轮询时注意把定时器挂在onShow/onHide生命周期里而不是onLoad/onUnload。小程序切到后台时onHide会触发但不销毁页面如果此时定时器还在跑会白白消耗用户流量和电量。// pages/device/detail.js const { request } require(../../utils/request); Page({ data: { deviceId: AG0001, telemetry: [], timer: null }, onShow() { this.startPolling(); }, onHide() { this.stopPolling(); }, startPolling() { this.fetchData(); this.timer setInterval(() { this.fetchData(); }, 30000); }, stopPolling() { if (this.timer) { clearInterval(this.timer); this.timer null; } }, async fetchData() { const detail await request(/api/device/${this.data.deviceId}/telemetry, { data: { limit: 50 } }); this.setData({ telemetry: detail.list }); } });这段代码里fetchData在startPolling里先执行一次再启动定时器避免页面首次进入白等 30 秒。limit: 50表示取最近 50 条结合后面的stopPolling切出页面时网络请求也会停止。如果换用 WebSocket则建议在后端通过dev/{deviceId}/up消息触发wx.sendSocketMessage推送并在onSocketError里做断线重连心跳间隔设 30 秒比轮询复杂一到两个量级。5.2 图表组件选型ec-canvas 与 canvas 2d 的兼容性温湿度曲线是智慧农业小程序的标配。社区里常用的ec-canvas是基于 ECharts 的小程序封装能直接画折线图。新版基础库推荐使用 canvas 2d 接口ec-canvas的初始化代码里要把devicePixelRatio传进去否则在部分手机上曲线会模糊。// 图表初始化核心片段 initChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); chart.setOption({ xAxis: { type: category, data: this.data.timeList }, yAxis: { type: value, name: ℃ }, series: [{ type: line, data: this.data.tempList, smooth: true }] }); return chart; }注意devicePixelRatio不能写死为 1否则 iPhone 这类高 DPR 设备上曲线会发虚。图表数据更新时调用chart.setOption而不是整个页面setData刷新否则会明显卡顿。如果项目用了 uniapp可以在模板里用canvas canvas-idchart/canvas配合renderjs处理但原生小程序的 ec-canvas 依然是可靠性最高的方案。5.3 自动浇水是“端侧判断”还是“云端判断”断网可用性才是关键智慧农业里最常见的自动控制是“土壤湿度低于阈值就开泵”。实现上有两条路云端判断和后端判断或者设备端判断。云端判断的优势是策略调整方便小程序上改一个阈值立即生效劣势是设备一旦断网自动控制就失效了对农业场景来说这不能接受。设备端判断的优势是断网还能工作劣势是策略写死在固件里想改阈值得重新刷机。实际项目中通常是两头折中设备端预设一套“保守阈值”作为兜底比如土壤湿度低于 20% 时无条件开泵云端策略作为精细控制例如根据不同作物生长期动态调整目标湿度。阈值一致性用device_config表维护后端把配置下发到设备设备本地保存一份这样平台端可以看到每台设备的当前配置设备端也知道按什么标准执行。表格里可以记录这份配置的字段配置项类型说明threshold_soil_lowint土壤湿度下限低于此值开启灌溉threshold_soil_highint土壤湿度上限高于此值停止灌溉enable_autotinyint0 手动模式1 自动模式push_intervalint传感器上报间隔单位秒6. 线上最容易崩的 3 个环节与智慧农业链路验收命令6.1 链路验收重启设备、观察上抛、后端落库、界面刷新把整个链路串起来验收时不要直接看小程序页面先按“设备→broker→后端→小程序”的顺序逐层确认。设备上电后在电脑上执行下面这条命令订阅该设备的上报主题mosquitto_sub -h broker.example.com -p 1883 -u agriculture -P agriculture123 \ -t dev/AG0001/up -v然后观察串口监视器正常情况下 5 秒内会看到一条带t、h、s字段的 JSON 载荷。如果 mosquitto_sub 能看到消息而数据库没数据问题出在后端订阅或入库逻辑如果 mosquitto_sub 都看不到问题出在设备网络、MQTT 连接或 Topic 不匹配不必急着改小程序代码。6.2 设备集体掉线重连风暴与退避策略几十台设备同时断电再上电会同时向 broker 发起连接形成“重连风暴”轻则 broker 响应变慢重则触发限流导致设备无法连上。MQTT 客户端库自带的自动重连通常没有退避逻辑端侧代码里建议加指数退避function nextReconnectDelay(attempt) { const base 5000; const max 60000; return Math.min(base * Math.pow(2, attempt), max) Math.floor(Math.random() * 1000); }当客户端检测到连接断开时第 1 次等 5 秒第 2 次等 10 秒依此类推超过 60 秒后封顶再加上 0 到 1 秒的随机抖动避免所有设备在同一时刻发起重连。6.3 小程序合法域名与 MQTT clientId 互踢的“隐形故障”小程序真机预览时request的域名必须在小程序后台配置为合法域名开发工具里勾选“不校验合法域名”只能临时绕过体验版和正式版会直接请求失败。注意wx.connectSocket的 URL 必须是wss://开头MQTT over WebSocket 的 broker 需要额外配置 TLS 证书这一点和普通 HTTP 域名配置是两套东西。MQTT 报文中如果clientId重复broker 会把前一个连接踢掉设备会表现为“连上又断开、断开又连上”排查时先看 broker 的日志看有没有Client X already connected之类的记录。最后用 6.1 节的命令确认整条链路已经打通再回到小程序页面刷新数据就能看到传感器数值在图表上一次滑过而不是永远停在初始值上。本文还有配套的精品资源点击获取