简介:智慧农场小程序完整版面向农业信息化开发者、课程设计学生及有二次开发需求的农业企业,提供一套可运行的微信小程序源码与配套后台素材,用于快速理解并搭建种植管理、环境监测与远程控制类应用。压缩包共1646个文件、约28.41MB,以PHP后台文件、WXML/WXSS/JS小程序页面、JSON配置、HTML页面及PNG/JPG图片资源为主,另含dat数据文件与少量脚本、证书和说明文档,前后端结构与静态素材相对齐全。已有4732人学习下载,具备一定参考热度。读者可从中梳理小程序页面组织、接口调用、数据库交互、角色权限与后台管理模块的实现方式,并围绕农业物联网数据展示、农田可视化、告警通知等场景做定制扩展,适合作为毕业设计、功能原型或二次开发底稿。
1. 从大棚门口那台只亮灯的网关说起
去年冬天去看一个草莓棚,老板指着墙上的网关说:这东西装上就一直在闪灯,数据也能在小程序里看,可我还是每天早晚各跑一趟棚里掀帘子。他手机里那个“智慧农场小程序”界面做得挺漂亮,土壤湿度、空气温湿度、光照曲线一应俱全,但没有一条数据变成过一个动作。
这就是“完整版”三个字最容易被误解的地方。完整不是功能列表长,而是设备、数据、任务、人这条链子通了:土壤含水率掉到阈值以下要能推一条带地块编号的提醒,而不是让人自己去翻曲线;卷帘机该放风的时候要能下发一条指令并拿到回执,而不是只能看不能动。这篇讲的就是把这个闭环从硬件接入一路做到小程序端,适合正在接农业物联网项目的开发者,也适合手里有一堆传感器却不知道怎么串起来的团队。
2. 端边云怎么切:设备接入层与小程序端的技术选型
农业场景的网络条件决定了架构不能照搬消费物联网那一套。棚区往往在郊区,4G 信号两格,WiFi 覆盖靠几个室外 AP 硬撑,断网半小时是常态。所以整条链路的切分原则只有一条:边缘能自己活,云端负责记账。
常见的四层切法是:传感器与执行机构(RS485/Modbus RTU、继电器、脉冲水表)→ 边缘网关(轮询采集、标定换算、本地缓存、断网补传)→ 云端接入层(MQTT Broker、规则引擎、时序库、业务库)→ 小程序端(订阅、展示、下发、告警)。中间任何一层短暂掉线,其余层要能降级运行,而不是一起崩。
2.1 一条数据从土壤里到小程序页面要过几道手
先看采集侧。土壤墒情、EC、pH 大多是 Modbus RTU 从机挂在同一条 485 总线上,网关做主机轮询。这一步最常见的翻车点不是协议,是寄存器地址偏移和字节序。厂家手册写“湿度寄存器地址 0x0000”,实际读的时候要区分是保持寄存器还是输入寄存器,功能码 0x03 和 0x04 不能混;32 位浮点数还涉及 ABCD/CDAB 字序,读出来是 65535 或者 0.00 基本都是这个原因。
下面是我一般会给网关写的轮询采集骨架,跑在边缘侧,用 Python 加一个轻量 Modbus 库,采完直接拼成统一报文发 MQTT。
# edge_gateway.py —— 边缘网关轮询采集 + MQTT 上报骨架 import time, json, struct from pymodbus.client import ModbusSerialClient import paho.mqtt.client as mqtt SLAVE_ID = 1 PORT = "/dev/ttyS1" # 485 转 USB 或板载串口 BAUD = 9600 # 传感器默认多为 9600 8N1 FARM_ID, DEVICE_ID = "f1024", "dev_soil_01" # 点位表:addr 为寄存器起始地址,scale 为换算系数,kind 为数据类型 POINTS = [ {"key": "soil_temp", "addr": 0, "kind": "i16", "scale": 0.1, "unit": "C"}, {"key": "soil_moist", "addr": 1, "kind": "u16", "scale": 0.1, "unit": "%"}, {"key": "soil_ec", "addr": 2, "kind": "f32", "scale": 1.0, "unit": "us/cm"}, ] def decode(raw, kind): if kind == "i16": v = raw[0] return v - 65536 if v > 32767 else v if kind == "u16": return raw[0] if kind == "f32": # 字序按 CDAB 处理,很多国产变送器是这个序,AB CD 时改为 raw[1], raw[0] buf = struct.pack(">HH", raw[1], raw[0]) return struct.unpack(">f", buf)[0] raise ValueError(kind) def poll(client): out = {} for p in POINTS: rr = client.read_holding_registers(p["addr"], 2 if p["kind"] == "f32" else 1, slave=SLAVE_ID) if rr.isError(): out[p["key"]] = None # 单点失败不拉挂整轮 continue val = decode(rr.registers, p["kind"]) * p["scale"] out[p["key"]] = round(val, 3) return out def main(): client = ModbusSerialClient(port=PORT, baudrate=BAUD, timeout=1, retries=2) client.connect() mq = mqtt.Client(client_id=DEVICE_ID) mq.connect("broker.example.internal", 1883, keepalive=60) mq.loop_start() while True: data = poll(client) payload = { "deviceId": DEVICE_ID, "farmId": FARM_ID, "ts": int(time.time() * 1000), # 统一用毫秒时间戳,禁用设备本地时间 "seq": int(time.time()), # 用于断网补传去重 "values": data, } mq.publish(f"farm/{FARM_ID}/device/{DEVICE_ID}/telemetry", json.dumps(payload), qos=1) time.sleep(30) # 土壤量变化慢,30s 足够,省流量也省电 if __name__ == "__main__": main()这段代码有三个地方值得展开。一是ts用网关的系统时间而不是设备时间,设备断电重启后时钟基本不可信;二是seq字段,断网时把报文写本地环形队列,恢复后按 seq 补传,云端靠它做幂等写入,不然一次断网能给你塞进去几万条重复;三是qos=1,至少一次投递,重复是必然的,幂等逻辑必须站在服务端。轮询间隔 30 秒是经验和功耗的折中,土壤参数分钟级变化,采太密除了让时序库膨胀没有别的收益。
2.2 小程序端为什么不该直连 MQTT Broker
很多方案图省事,让小程序直接连 Broker 的 WebSocket 端口,topic 权限按设备号下发。这在一台设备的 Demo 里能跑通,但设备一多就会出问题,原因有三条。
第一,小程序端只能用平台的 WebSocket API(wx.connectSocket),它给你的是裸 socket,MQTT 协议要自己在 JS 层实现,包解析、重连、心跳全靠手写,包体还不小。第二,鉴权凭证落到客户端,topic 通配符farm/+/device/#一旦被改成订阅全场,别人的数据就全看见了,这类越权在农业项目里是硬伤。第三,手机切后台、锁屏、地铁里信号跳变,连接状态根本不可控,Broker 侧会积压一堆半死连接。
所以我的做法是在后端加一个薄薄的 WebSocket 网关:小程序连网关,网关内部连 Broker 并按登录态过滤 topic,图片和大屏数据走 HTTP,指令下行走独立的请求-响应通道。小程序端只需要处理“连上、断开、重连”三件事。
// ws-client.js —— 小程序端长连接封装,只管重连与去重 const WS_URL = "wss://api.example.internal/ws/telemetry"; let socket = null, retry = 0, heartbeat = null, closedByUser = false; function connect(onMessage) { socket = wx.connectSocket({ url: `${WS_URL}?token=${wx.getStorageSync("token")}` }); socket.onOpen(() => { retry = 0; heartbeat = setInterval(() => socket.send({ data: '{"type":"ping"}' }), 25000); }); socket.onMessage((res) => { const msg = JSON.parse(res.data); if (msg.type === "ping") return; onMessage(msg); // 业务层只关心 telemetry / alarm / cmdAck }); socket.onClose(() => { clearInterval(heartbeat); if (closedByUser) return; const delay = Math.min(1000 * Math.pow(2, retry++), 30000); // 指数退避,封顶 30s setTimeout(() => connect(onMessage), delay); }); socket.onError(() => socket.close()); }token放在 query 里而不是首帧里,是因为小程序 WebSocket 不支持自定义 header;服务端拿到 token 校验后把用户和农场绑定关系写进连接上下文,后续推什么由服务端决定,客户端不传订阅参数,从根上杜绝越权订阅。心跳 25 秒是配合多数网关 60 秒空闲断连的保守值,指数退避封顶 30 秒是为了避免信号恢复瞬间几百台手机同时重连把网关打穿。
2.3 业务库和时序库分家:MySQL 与 TDengine 的边界
把遥测数据塞进 MySQL 单表是最常见的起步做法,也是三个月后最想删库重来的做法。50 个设备、10 个点位、30 秒一次,一天就是 144 万行,一个月后SELECT一条曲线要三秒,小程序端直接白屏。
边界我一般这么划:MySQL 存有生命周期、要事务、要关联查询的东西——农场、地块、设备档案、任务、投入品、批次、告警记录;时序库存只追加、按时间范围聚合的遥测量,TDengine 或 InfluxDB 都行,前者写入吞吐好、建表按超级表+子表,后者生态更熟。Redis 存每个设备的最新值,小程序首页那几张卡片不该去查时序库。
时序库建模的关键是标签设计:farm_id、device_id、point_key做标签,ts做时间戳,value做量测。标签基数要克制,别把批次号、任务号这种高频变化的东西塞进标签,否则子表数量爆炸,写入直接卡住。查询侧必须带时间范围和降采样,小程序端画 7 天曲线,服务端返回 288 个点就够,多出来的部分除了让 canvas 卡顿没有任何意义。
3. 设备接入与配网:从裸传感器到可远程控制的最小闭环
设备能不能被远程控制,分水岭不在继电器本身,而在于这台设备在系统里有没有一个稳定身份。身份不稳,指令就发错棚;身份稳了,剩下的都是幂等和超时问题。
3.1 物模型三张表:产品、设备、点位
我习惯把设备抽象成三层:产品(一类硬件的模板,定义有哪些点位、量程、单位)、设备(具体一台,绑定产品并持有序列号与密钥)、点位(产品下的字段定义,含数据类型、换算、上下限)。这样同一个型号的 200 台墒情仪只维护一份点位定义,改量程不用逐台改。
-- schema.sql —— 设备物模型三张核心表 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(64) NOT NULL UNIQUE, -- 型号编码,如 SOIL_4IN1 name VARCHAR(64) NOT NULL, category TINYINT NOT NULL, -- 1传感器 2执行器 3网关 4摄像头 protocol VARCHAR(16) NOT NULL DEFAULT 'modbus' ); CREATE TABLE point_def ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, point_key VARCHAR(32) NOT NULL, -- soil_moist unit VARCHAR(16), data_type VARCHAR(8) NOT NULL, -- i16/u16/f32/bool scale DECIMAL(10,4) NOT NULL DEFAULT 1, min_val DECIMAL(12,4), -- 低于此值判为异常,不参与告警 max_val DECIMAL(12,4), UNIQUE KEY uk_prod_key (product_id, point_key) ); CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sn VARCHAR(64) NOT NULL UNIQUE, -- 出厂序列号,配网时写入 product_id BIGINT NOT NULL, farm_id BIGINT NOT NULL, plot_id BIGINT, -- 归属地块,允许为空(网关类设备) secret CHAR(32) NOT NULL, -- 设备级密钥,MQTT 鉴权用 online TINYINT NOT NULL DEFAULT 0, last_seen_at DATETIME(3), KEY idx_farm (farm_id, plot_id) );三张表的分工要守住:point_def里存的是定义,不是值;值只在时序库里,最新值在 Redis 里。见过把最新值回写到device表的设计,几十个点位横向加列,加一个传感器就要改表结构,维护成本高得离谱。min_val/max_val这一对字段看着不起眼,实际是数据质量的第一道闸门,传感器坏了输出 85.0 或者 0.0,先在这里被拦下来,不会一路冲到告警模块。
3.2 蓝牙配网:20 字节 MTU 与分包写入
农户手里的设备要连 WiFi,最现实的方案是小程序蓝牙配网:手机连设备的热点或 BLE,把 WiFi 的 SSID 和密码写进去,设备自己连云。这条路上翻车最多的是分包。
BLE 默认 MTU 是 23 字节,一次writeBLECharacteristicValue有效载荷只有 20 字节。一个 16 位 SSID 加 12 位密码再加 JSON 外壳,轻松超过 40 字节,一次写完的结果就是设备收到半截密码,连云失败,农户觉得是设备坏了。安卓可以用wx.setBLEMTU提到 247,iOS 不支持这个接口,所以稳妥做法是不管平台一律按 20 字节切片,设备端按序号拼包。
// ble-provision.js —— 蓝牙配网分包写入 const CHUNK = 20; async function provision({ deviceId, serviceId, charId, ssid, password }) { // 1. 统一成定长帧:cmd(1) + seq(1) + total(1) + len(1) + payload const body = JSON.stringify({ s: ssid, p: password }); const bytes = new Uint8Array(new TextEncoder().encode(body)); const total = Math.ceil(bytes.length / CHUNK); for (let seq = 0; seq < total; seq++) { const slice = bytes.slice(seq * CHUNK, (seq + 1) * CHUNK); const frame = new Uint8Array(4 + slice.length); frame[0] = 0x01; // 命令字:写入 WiFi 配置 frame[1] = seq; // 包序号,设备端据此排序 frame[2] = total; // 总包数,设备端校验完整性 frame[3] = slice.length; // 本包有效长度 frame.set(slice, 4); await new Promise((resolve, reject) => { wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: charId, value: frame.buffer, success: resolve, fail: reject, }); }); await new Promise((r) => setTimeout(r, 30)); // 包间留 30ms,别把设备缓冲打爆 } // 2. 发完等设备回执,回执里带联网结果,别在这里直接弹"配网成功" }两个参数值得说:包间 30 毫秒不是拍脑袋,很多国产 BLE 模组的串口缓冲只有几百字节,连续高速写入会丢包,30ms 是实测比较稳的值;配网成功的判定一定要等设备回执,不能 write 成功就提示成功——write 成功只代表手机发出去了,设备连云失败是另一回事。另外安卓搜不到设备,八成是定位权限没给,BLE 扫描在多数安卓机型上被归到位置权限组里。
3.3 下行控制:指令 ID、超时与幂等
下行比上行危险得多。上行数据错了只是曲线难看,下行指令重发一次可能就把卷帘机放下来两次,或者把水泵开了不停。
我的做法是每条指令带一个客户端生成的cmdId(UUID),服务端落库为pending,通过 MQTT 发到farm/{farmId}/device/{deviceId}/cmd,QoS 1,同时起一个 15 秒的定时任务;设备执行完往cmd_ack上报cmdId和结果,服务端改成success/failed;超时没回执改成timeout,并在小程序端展示“未确认”,而不是直接判失败重发。
重发策略上有个细节:设备端必须按cmdId做去重,收到相同cmdId直接回上次的执行结果,不重复动作。这条比服务端的幂等更重要,因为网络抖动导致的重发,服务端不可能知道设备到底收没收到。农业现场的继电器控制还要加一个物理约束——同一个执行器两次动作之间强制 30 秒冷却,防止小程序上连点两下水泵。
4. 业务闭环:地块、农事任务、投入品与溯源
数据能看、设备能控,还差最后一环:这些能力要落到农事流程上。农户不会为了看曲线打开小程序,但会为了“今天该给 3 号棚追肥”打开。业务层做对了,设备层的数据才有出口。
4.1 地块多边形:面积别用 ST_Area 直接算
地块要在小程序地图上画出来,农户点几个点围成一块地,系统算亩数。这里有个几乎人人踩的坑:MySQL 的ST_Area在 SRID 4326 下返回的单位是平方度,不是平方米。1 平方度在赤道附近约 1.2 万平方公里,在纬度 40 度附近又不一样,直接拿去乘系数,算出来的面积能差出两三成。
正确做法是把经纬度当球面多边形算,用球面多边形面积公式(或者用等积投影转平面再算)。我一般在小程序端画完点就把坐标数组传到服务端,服务端算面积并回写,前端只负责展示,避免两端算法不一致。
# area.py —— 球面多边形面积,单位平方米 import math R = 6378137.0 # WGS84 长半轴 def polygon_area_m2(lnglat): """lnglat: [(lng, lat), ...] 首尾不必闭合""" if len(lnglat) < 3: return 0.0 total = 0.0 n = len(lnglat) for i in range(n): lng1, lat1 = map(math.radians, lnglat[i]) lng2, lat2 = map(math.radians, lnglat[(i + 1) % n]) total += (lng2 - lng1) * (2 + math.sin(lat1) + math.sin(lat2)) return abs(total * R * R / 2.0) if __name__ == "__main__": plot = [(116.3012, 39.8811), (116.3045, 39.8813), (116.3048, 39.8792), (116.3015, 39.8790)] m2 = polygon_area_m2(plot) print(round(m2, 1), "m2 =", round(m2 / 666.67, 2), "亩")换算系数 666.67 是 1 亩 = 666.67 平方米,别用 1 公顷 = 15 亩去绕,容易在小数位上出岔子。另外多边形顶点顺序会影响符号,取绝对值就行;如果地块跨了 180 度经线(国内场景基本不会),这套公式要另做处理。还有一个业务上的小要求:面积只用于展示和投入品用量估算,不要拿它做结算依据,农户手画的边界误差本来就大。
4.2 农事任务状态机与提醒落地
农事任务不能做成一个“待办列表”,要有状态机,否则“做完了没做”永远说不清。我一般用四态:pending(待执行)→doing(进行中)→done(已完成)→skipped(跳过/作废),只允许正向流转和pending→skipped,done之后禁止回退,要改就新开一条。每次流转写一条操作日志,带上操作人、时间、坐标(可选,用于确认人到过地块)。
提醒的落地方式决定留存。小程序的订阅消息是一次性的,用户点一次授权只能推一条,所以推送策略要算:任务到期前 1 天推一次,当天早上 7 点推一次,超过 48 小时未处理推到农场管理员而不是执行人。这里必须做合并推送——同一农户名下的 5 条任务要合成一条“您有 5 项农事待处理”,逐条推的结果就是用户第一周就把通知关了,这条经验我交过学费。
4.3 溯源档案:一个批次要存哪些字段
溯源不是给监管看的装饰,它的核心是“出了问题能倒查”。一个批次的最小档案集我固定为这几项:批次号(播种时生成,格式建议 农场码+地块码+日期+序号)、品种、播种/移栽时间、采收时间、每个环节的农事任务记录(谁在什么时候做了什么)、投入品使用记录(名称、剂型、用量、安全间隔期、施用地块)、环境数据摘要(生长期内的温度区间、累计光照)、检测报告图片。
关键在于投入品和安全间隔期这一项。采收时系统要自动校验:距离最后一次施药是否满足安全间隔期,不满足就在采收任务上标红并阻止生成溯源二维码。这个校验规则要存在配置表里,不同药剂间隔期不同,硬编码在代码里,换一个品种就得改代码发版,非常被动。溯源页面的二维码内容用短链,不要直接把整段 JSON 编进二维码,字段一多二维码密度就上去,农户用旧手机扫不出来。
5. 上线前翻车的 5 个坑:从分包超限到告警风暴
下面这五条都是真实跑出来的,按“现象→原因→解决”给,看到相似现象可以直接对号。
5.1 开发版正常,体验版白屏:主包 2MB 撑爆
现象:开发者工具里一切正常,传到体验版打开就白屏,控制台报加载分包失败。
原因:地图组件、图表库、几张小程序里的大图全塞在主包里,主包体积超了平台 2MB 上限。很多人不知道开发者工具默认不严格校验,真机才暴露。
解决:按业务域拆分包,把“地块地图”“设备详情”“溯源展示”各做一个分包,首页只留登录和农场概览;图表用轻量 canvas 手绘或者按需引入,别整包拉进来;所有图片走 CDN 加尺寸参数,本地只留图标。改完在开发者工具里开“上传时压缩代码”和“分包异步化”再验证一遍。
5.2 安卓能配网,iOS 搜不到设备
现象:同一个蓝牙模组,安卓机列表里能看到设备名,iPhone 上扫半天是空的。
原因:模组广播包里没带kCBAdvDataLocalName,iOS 扫描时按名称过滤就抓不到;另外部分机型要求先授权定位才允许 BLE 扫描。
解决:让硬件侧在广播包中带上设备名前缀,扫描时改成按 serviceUUID 过滤而不是按名称;小程序侧在扫描前先调权限接口,安卓被拒后引导用户去系统设置开位置权限。这类问题一半在硬件固件,一半在权限,排查时先用第三方蓝牙调试工具确认真实广播内容,再回来改代码,能省一半时间。
5.3 曲线越拉越慢,最后直接白屏
现象:上线两周后用户反馈“设备详情打不开”,越用越卡。
原因:前端一次请求 7 天原始数据,30 秒一个点共 2 万个点,接口返回 3MB,小程序渲染层直接跪。后端还在用WHERE ts BETWEEN全表扫 MySQL。
解决:查询接口强制带interval参数,服务端按时间范围决定降采样粒度(1 小时看 1 分钟粒度,7 天看 1 小时聚合),返回点数上限 300;数据源切到时序库;前端曲线做分页加载,先画最近 24 小时,用户左滑再拉更早的。改完之后同一个页面首屏从 4 秒降到 400 毫秒。
5.4 一个传感器抖动,推了 200 条告警
现象:某天凌晨农户被连续 200 多条告警吵醒,第二天直接把小程序的通知权限关了。
原因:土壤湿度探头接触不良,读数在阈值上下反复跳变,告警规则是“超过阈值即推送”,没有去抖窗口,也没有抑制。这是典型的告警风暴。
解决:三层处理。第一层去抖,连续 3 个采样点越界才判为有效告警;第二层抑制,同一设备同一指标 30 分钟内只推一次,恢复通知也要推;第三层分级,P0(水泵空转、棚温超限)立即推,P1(电量低、信号弱)合并到日报。改完之后日均推送量降了八成,反而没人关通知了。
5.5 断网补传后数据乱序、重复、时间穿越
现象:棚区停电两小时,恢复后时序库里出现一堆时间戳重复的记录,曲线画出锯齿,日统计翻倍。
原因:网关本地缓存直接按恢复后的系统时间重打时间戳,丢了原始采集时间;同时 QoS 1 的重复投递没有做幂等,同一个seq写了两遍。
解决:缓存时把原始ts和seq一起存,补传时原样带上,服务端按(device_id, seq)建唯一索引做 upsert;补传限速,每秒不超过 50 条,避免恢复瞬间打满 Broker;服务端对超过 24 小时的补传数据只入库不触发告警,防止半夜收到两小时前的越界通知。这条做完,停电再也没造成过数据事故。
6. 让告警少一半:数据质量校验与降噪的几个具体手法
把告警做准,比把功能做全更影响这套系统能不能活下去。我现在的习惯是在告警规则前面加三道闸门,任何一条数据想触发告警,得先过这三关。
第一道是量程闸门,直接用point_def里的min_val/max_val,超出范围的直接标记为无效数据,落库但打上quality=0标签,不参与告警也不参与统计。第二道是变化率闸门,相邻两点变化超过物理可能的速率就判为异常,比如棚温 30 秒内跳 15 度,基本可以断定是探头故障而不是真的升温。第三道是持续性闸门,滑动窗口内连续 N 个点越界才升级为告警,N 按指标定,温度取 3,土壤含水率取 6,因为后者变化本来就慢。
# quality.py —— 采集值入告警前的三道校验 def check(point_def, prev, cur, window): if cur is None: return "invalid", "空值" if not (point_def["min_val"] <= cur <= point_def["max_val"]): return "invalid", "超出量程" if prev is not None: rate = abs(cur - prev) / 30.0 # 每秒变化率 if rate > point_def.get("max_rate", 1e9): return "suspect", "变化率异常" hit = sum(1 for v in window if v is not None and v > point_def["alarm_high"]) if hit >= point_def.get("debounce", 3): return "alarm", "持续越界" return "ok", ""参数上max_rate和debounce要跟着点位走,别全表一个值:空气温度max_rate给 0.5 度/秒已经宽松,土壤 EC 给 0.05 就够;debounce越大误报越少但响应越慢,P0 级别的点位宁可按 2 来算,误报一次总比漏掉水泵空转强。
还有一条是跨设备交叉校验,成本低效果好:同一个棚里空气温湿度和土壤温度三个探头,如果只有一个异常另外两个正常,多半是那一个坏了;如果三个同时异常,才更可能是环境真的出问题。这套规则我用一张配置表驱动,不写死在代码里,现场调参不用发版。
最后说个我自己的教训:早期做这类项目,我总想着把功能做全,地图、曲线、报表、溯源一个不落,结果上线三个月,农户日常用的只有“今日任务”和“告警提醒”两个入口,剩下全是给验收看的。后来我改成先只做这两条链路,把推送准确率和任务闭环做到位,再按真实使用数据往上加功能,留存反而好得多。设备在棚里能稳定跑一年,比在小程序里多画十条曲线难得多,也值钱得多。希望帮到你。
本文还有配套的精品资源,点击获取