ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM+MySQL实现作物生长监控系统:从数据采集到预警闭环

SSM+MySQL实现作物生长监控系统:从数据采集到预警闭环 简介这是一份基于Java(SSM)与MySQL实现的Web版作物生长监控系统毕业设计资源适合计算机相关专业学生在课程设计、毕业设计阶段参考也适用于希望学习SSM框架整合和农业物联网Web应用开发的开发者。系统覆盖实时数据推送、站点地图展示、农业新闻爬取、温湿度数据分析、角色权限管理、站点创建等核心模块可帮助理解从需求调研、数据库设计到前后端联调的完整开发流程。资源包共564个文件包含java后端源码、jsp/html/css/js前端页面、xml与properties配置文件、sql数据库脚本、jar依赖库以及war包等压缩包大小约86MB。源码结构完整配有日志与文档资料便于对照运行、排查问题和二次开发。已有127人浏览学习这份资源对于需要快速搭建作物生长监控系统原型或研究SSM项目实践的人来说具备较高参考价值。1. 作物生长监控系统到底在监控什么SSM MySQL 能撑起的最小闭环一个种草莓的大棚最怕的不是虫害是半夜气温跌破 5 度没人知道。作物生长监控系统说白了就是把温度、湿度、光照、土壤水分这些环境参数采集上来存进 MySQL再通过 Web 页面把实时数据和历史曲线摆到管理者面前超阈值就报警。基于 Java(SSM)MySQL 实现这个系统是 Java Web 方向一个非常典型的落地项目Spring 管业务对象SpringMVC 管请求路由MyBatis 管数据库读写MySQL 存采集数据。它不涉及高并发、分布式这些复杂话题核心就是把“采集-存储-展示-预警”这条链路走通。这篇文章写给两类人一是要做课程设计或毕业设计的同学需要快速搭出一个能演示、能答辩的完整系统二是想从增删改查走向“业务闭环”的入门开发者。我会把从建表到预警功能的完整路径拆开讲包括参数怎么定、坑在哪、怎么验证系统是真的能用而不是“能跑”。2. 先立骨架SSM 三层架构与作物监控的项目结构拆解2.1 为什么是这个组合SSM 的职责边界与选型理由作物生长监控系统这类项目选 SSM 而不是 Spring Boot不是因为 Spring Boot 不好而是 SSM 的结构更“透明”。Spring Boot 把大量配置自动完成了新手反而看不明白请求是怎么走进 Controller、又怎么落到数据库的。SSM 把每一层都摆在明面上Spring 是容器负责创建和管理 Service、DAO 这些对象SpringMVC 是 web 层框架负责把浏览器发来的 HTTP 请求映射到 Java 方法上MyBatis 负责把 Java 对象和 SQL 语句之间的映射关系管起来。三层各管一段职责边界非常清楚。对于这个项目SSM 还有一个实际好处MyBatis 手写 SQL 的方式很适合处理传感器数据。作物监控的查询经常带时间范围、阈值条件用注解拼 SQL 很别扭写在 XML 里反而清晰。而且学校里的 Java 课程和大部分 java 面试题问的也还是 SSM 这套东西比如 ssm 常用注解、事务传播行为、动态 SQL。把这个项目做完面试时聊“你怎么设计表、怎么写一个带条件查询的 Mapper”会非常从容。2.2 项目目录怎么摆一个能直接套用的包结构我见过太多 SSM 项目翻车不是代码逻辑错而是包结构从一开始就乱了。Controller 里写 SQL、Service 里输出 JSON、实体类里塞业务逻辑最后改一个功能要动五个文件。做作物监控系统我建议目录就按下面这个方式来分简单、标准、答辨也好讲。src/main/java ├── com.farm.controller # 控制器接收请求返回 JSON 或页面 │ ├── SensorDataController.java │ ├── AlertController.java │ └── PageController.java ├── com.farm.service # 业务层接口 │ ├── SensorDataService.java │ ├── AlertService.java │ └── impl │ ├── SensorDataServiceImpl.java │ └── AlertServiceImpl.java ├── com.farm.mapper # MyBatis Mapper 接口 │ ├── SensorDataMapper.java │ ├── AlertMapper.java │ └── CropMapper.java ├── com.farm.entity # 实体类对应表结构 │ ├── SensorData.java │ ├── AlertRecord.java │ └── Crop.java └── com.farm.common # 工具类、统一返回结果 ├── Result.java └── DateUtil.java注意 Service 层接口和实现类分开放这是 SSM 项目的习惯也是 Spring 面向接口编程的体现。后面如果你要加缓存或者换实现只动 impl 里的代码就够了Controller 不用改。实体类里只放字段和 getter/setter别写业务方法。2.3 三个配置文件把 SSM 串起来applicationContext.xml、spring-mvc.xml、mybatis-config.xmlSSM 项目跑不起来八成是配置文件之间没配合好。需要三个核心配置文件各管一摊事applicationContext.xml 管 Spring 容器扫描 Service 和 Mapperspring-mvc.xml 管 SpringMVC扫描 Controller 并配置视图解析器mybatis-config.xml 管 MyBatis 的全局行为。先看 Spring 的核心配置!-- applicationContext.xml 核心片段 -- context:component-scan base-packagecom.farm.service, com.farm.mapper/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/farm_db?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueroot/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.farm.mapper/ /bean这段配置里有三个关键点。第一context:component-scan的 base-package 必须同时覆盖 service 和 mapper 两个包漏了任何一个都会导致启动时找不到 Bean。第二MySQL 8.0 以上必须用com.mysql.cj.jdbc.Driver并且 url 里要带serverTimezoneAsia/Shanghai不然会报时区错误。第三SqlSessionFactoryBean的mapperLocations指向classpath:mapper/*.xml这要求你的 Mapper 接口和 XML 文件在同一包路径下且同名比如SensorDataMapper.java对应SensorDataMapper.xml。SpringMVC 的配置相对简单核心是开启注解驱动和配置视图解析器!-- spring-mvc.xml 核心片段 -- mvc:annotation-driven/ context:component-scan base-packagecom.farm.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean mvc:resources mapping/static/** location/static//mvc:annotation-driven必须写它负责注册处理 JSON 转换、参数绑定这类请求的默认组件。视图解析器把 Controller 返回的逻辑视图名如dashboard解析成/WEB-INF/views/dashboard.jsp这样做可以防止用户直接通过 URL 访问 jsp 文件算是 Web 项目的安全基本功。mvc:resources用于放行 CSS、JS 和图片不配的话前端页面加载不了静态资源页面会光秃秃的。3. 数据从哪来、存到哪MySQL 表设计与传感器数据入库的四条铁律3.1 五张核心表从作物档案到报警记录作物生长监控系统的数据库设计核心不是“建几张表”而是把“设备-作物-数据-报警”这条业务链理顺。我建议至少设计五张表crop_info存作物基本信息名称、适宜温度区间等sensor_device存传感器设备设备编号、安装位置、状态sensor_data存实时采集数据这是数据量最大的表alert_rule存预警规则哪个参数、上下限多少alert_record存报警历史。用户表sys_user按需加做登录功能才需要。这五张表的关系很直接一个作物可以绑定多个传感器设备一个设备持续产生多条 sensor_data设备按 alert_rule 的规则触发 alert_record。下面给出最关键的sensor_data和alert_record建表语句CREATE TABLE sensor_data ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, device_id varchar(32) NOT NULL COMMENT 设备编号如 DEV001, crop_id int DEFAULT NULL COMMENT 关联作物ID, temperature decimal(5,2) DEFAULT NULL COMMENT 空气温度单位℃, humidity decimal(5,2) DEFAULT NULL COMMENT 空气湿度单位%RH, soil_moisture decimal(5,2) DEFAULT NULL COMMENT 土壤湿度单位%, light_intensity int DEFAULT NULL COMMENT 光照强度单位Lux, collect_time datetime NOT NULL COMMENT 采集时间设备上报时间, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间, PRIMARY KEY (id), KEY idx_device_time (device_id, collect_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT传感器采集数据表; CREATE TABLE alert_record ( id bigint NOT NULL AUTO_INCREMENT, device_id varchar(32) NOT NULL, crop_id int DEFAULT NULL, param_name varchar(20) NOT NULL COMMENT 超限参数temperature/humidity/soil_moisture, alert_value decimal(8,2) NOT NULL COMMENT 触发报警时的实测值, threshold_value decimal(8,2) NOT NULL COMMENT 规则设定的阈值, alert_type tinyint NOT NULL COMMENT 1超上限,2低于下限, status tinyint DEFAULT 0 COMMENT 0未处理,1已确认,2已忽略, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_time (status, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报警记录表;建表时有几个细节必须现在就说清楚。第一采集时间和入库时间是两个字段collect_time是设备上报数据时带的时间create_time是数据库写入时间两者分开才能排查设备延迟上报的问题。第二temperature、humidity这些字段用decimal而不是float因为 float 在 MySQL 里是近似值存 28.6 可能变成 28.599999画曲线时会出现毛刺。第三idx_device_time联合索引非常关键传感器数据最常见的查询是“查某台设备某个时间段的记录”没有这个索引数据量一上去查询就会慢到让人怀疑数据库坏了。第四报警记录要加status字段这是业务需要的不然运营者看到报警也不知道处理了没有。3.2 数据怎么进来模拟数据生成器与真实接入的抉择做这个项目时你首先要回答一个问题传感器数据从哪里来真实硬件方案比如通过 ESP32 内嵌无线模块上报数据效果很好但成本高、调试周期长。课程设计和毕业设计阶段我强烈建议先用模拟数据生成器把链路跑通。这不丢人因为核心业务逻辑和真实接入完全一致只是数据来源不同。模拟数据的实现思路是用一个定时任务每隔 5 秒生成一条符合当前时段特征的记录温度在 18-32 度之间波动湿度在 40%-80% 之间插入sensor_data表。这样做能保证页面和报警功能随时有数据可用演示时不会冷场。下面是模拟数据生成器的核心代码Component public class SensorDataSimulator { Scheduled(fixedRate 5000) public void generateData() { // 模拟三台设备每台设备生成一条记录 String[] devices {DEV001, DEV002, DEV003}; for (String deviceId : devices) { SensorData data new SensorData(); data.setDeviceId(deviceId); // 温度在 18.0~32.0 之间随机保留一位小数 data.setTemperature(Math.round((18 Math.random() * 14) * 10) / 10.0); // 湿度在 40.0~80.0 之间随机 data.setHumidity(Math.round((40 Math.random() * 40) * 10) / 10.0); // 土壤湿度在 20.0~60.0 之间随机 data.setSoilMoisture(Math.round((20 Math.random() * 40) * 10) / 10.0); // 光照强度在 500~5000 之间随机 data.setLightIntensity((int)(500 Math.random() * 4500)); data.setCollectTime(new Date()); // 通过 Service 层写入数据库 sensorDataService.insertData(data); } } }这段代码里有几个值得注意的点。Scheduled(fixedRate 5000)表示每 5 秒执行一次这个注解需要在 spring-mvc.xml 里加task:annotation-driven/才会生效fixedRate和fixedDelay的区别是前者不管上次是否执行完都按固定频率触发后者是上次执行完后再等 5 秒。随机数的范围直接写在代码里方便调。真实场景中设备 ID 应该从sensor_device表读取而不是硬编码但模拟阶段硬编码反而让逻辑更直观。3.3 查询接口怎么设计最近一条、历史区间与平均值聚合数据进了库Web 页面要能查。作物监控页面上有三个高频查询首页大屏要“每台设备的最新一条数据”历史曲线页要“某台设备某段时间内的全部数据”报表页要“某天每小时的均值”。这三个查询分别对应三种 SQL 技巧子查询取最新、范围查询、按时间分组聚合。先看取每台设备最新一条数据的 SQL这是新手最容易写错的地方!-- SensorDataMapper.xml -- select idselectLatestByDevice resultTypecom.farm.entity.SensorData SELECT s.* FROM sensor_data s INNER JOIN ( SELECT device_id, MAX(collect_time) AS max_time FROM sensor_data GROUP BY device_id ) t ON s.device_id t.device_id AND s.collect_time t.max_time /select这段 SQL 的逻辑是先用子查询按设备分组找出每台设备的最大采集时间再关联回原表取完整记录。新手常犯的错误是直接用GROUP BY device_id加上SELECT *这在 MySQL 5.7 之前的版本能跑但结果不对取到的是“每组任意一条”而不是最新一条MySQL 5.7 之后开启ONLY_FULL_GROUP_BY干脆直接报错。所以做“取最新”这类需求子查询 关联是稳妥的做法。历史区间查询相对简单但要特别注意时间字段的边界我用范围查询时习惯把结束时间设为“当天 23:59:59”而不是“当天 00:00:00”select idselectHistory resultTypecom.farm.entity.SensorData SELECT device_id, temperature, humidity, soil_moisture, light_intensity, collect_time FROM sensor_data WHERE device_id #{deviceId} AND collect_time gt; #{startTime} AND collect_time lt; #{endTime} ORDER BY collect_time ASC /select注意 XML 里写大于号、小于号必须用gt;、lt;转义否则 XML 解析直接报错。这个查询的排序一定要用ASC升序ECharts 前端画折线图时X 轴数据必须按时间递增否则曲线折返成乱麻。如果觉得数据太密5 秒一条一小时就 720 条可以改成只查整点数据SQL 里加AND MINUTE(collect_time) 0但要注意这会让索引失效数据量小无所谓数据量大不建议这么写。4. 把功能做出来从 Mapper 到 Controller 再到页面的完整链路4.1 预警功能不是“超过就报警”那么简单预警是作物监控系统里最有业务价值的功能也是答辩时最容易被追问的模块。它不能简单写成“温度大于 35 就报警”因为单条数据的瞬时抖动会导致误报。比如设备传输瞬间的信号干扰可能把 28 度变成 38 度如果系统立刻报警运营者半夜爬起来查看发现是误报下次真报警他就不信了。所以预警要加“连续 N 次超过阈值才触发”的消抖机制。实现消抖的逻辑放在 Service 层核心代码如下Service public class AlertServiceImpl implements AlertService { Autowired private SensorDataMapper sensorDataMapper; Autowired private AlertMapper alertMapper; Override public void checkAlert(String deviceId) { // 1. 查出该设备最近 5 条采集记录 ListSensorData recentList sensorDataMapper.selectRecentByDevice(deviceId, 5); if (recentList.size() 5) { return; // 数据不足 5 条不判定 } // 2. 统计最近 5 条中超过 35 度的条数 long overCount recentList.stream() .filter(d - d.getTemperature() ! null d.getTemperature() 35) .count(); // 3. 连续 5 条全部超限才触发报警 if (overCount 5) { // 查询是否已有未处理的同类报警避免重复插入 int exists alertMapper.countUnhandled(deviceId, temperature, 1); if (exists 0) { AlertRecord record new AlertRecord(); record.setDeviceId(deviceId); record.setParamName(temperature); record.setAlertValue(recentList.get(4).getTemperature()); record.setThresholdValue(new BigDecimal(35)); record.setAlertType((byte) 1); alertMapper.insert(record); } } } }这段代码的逻辑分三步先取最近 5 条记录再统计超限条数最后决定是否插入报警记录。overCount 5意味着连续 5 次全部超限才报警这就是消抖。阈值 35 应该从alert_rule表读取而不是硬编码这里硬编码是为了把逻辑讲清楚实际项目中要换成查表。countUnhandled的含义是查“是否已有同设备、同参数、同类型且未处理的报警”避免每 5 秒触发一次检查就插入一条新报警否则一小时能插入 720 条重复记录。4.2 页面端展示用 ECharts 画实时曲线和最近 24 小时走势后端把数据查出来前端怎么呈现我常用 ECharts 的折线图来展示温湿度变化因为曲线图比数字表格直观得多管理者看一条线往上冲就知道不对劲。前端页面通过 AJAX 请求后端接口拿到 JSON 数据再传给 ECharts。先看后端返回 JSON 的 Controller 代码Controller RequestMapping(/api/sensor) public class SensorDataController { Autowired private SensorDataService sensorDataService; ResponseBody RequestMapping(/history) public Result getHistory(RequestParam String deviceId, RequestParam String startTime, RequestParam String endTime) { ListSensorData list sensorDataService.queryHistory(deviceId, startTime, endTime); return Result.success(list); } }ResponseBody注解的作用是把返回的Result对象序列化成 JSON 字符串写回响应体。SpringMVC 对此依赖mvc:annotation-driven配置和 Jackson 依赖缺了 Jackson 库启动时会报 “HttpMediaTypeNotAcceptable” 之类的错误。Result.success(list)是统一返回格式包含code、message、data三个字段前端拿到后判断code 200再渲染数据。统一返回格式是接手过几个系统后的经验一开始各接口各返回各的前端接得想骂人统一之后加字段、改错误提示都只动一个类。前端页面调用接口并把数据塞给 ECharts 的代码大概是这样的$.ajax({ url: /api/sensor/history, data: { deviceId: DEV001, startTime: 2025-01-10 00:00:00, endTime: 2025-01-10 23:59:59 }, success: function(res) { if (res.code 200) { var times res.data.map(function(item) { return item.collectTime; }); var temps res.data.map(function(item) { return item.temperature; }); chart.setOption({ xAxis: { data: times }, series: [{ name: 温度, type: line, data: temps }] }); } } });这段 JS 里有一个性能问题如果查询一天的数据每 5 秒一条共 17280 条一次性把 1.7 万个点塞给 ECharts浏览器会明显卡顿。常见做法是后端做降采样比如按小时聚合取平均值这样一天只有 24 个点曲线也足够展示趋势。这里的map是 JavaScript 数组方法用了 ES6 语法如果你的项目要兼容 IE 就得换 for 循环但现在的主流 web 项目基本都不管 IE 了。4.3 阈值配置页面让运营者自己改规则预警规则不能写在代码里否则每次改阈值都要重新编译部署。要做一个配置页面让运营者在界面上改每种作物的温湿度上下限。对应的表是alert_rule核心字段包括rule_id、crop_id、param_name、min_value、max_value、is_enabled。后台管理页面对这张表做增删改查前端用表单提交后端校验参数后写入数据库。实现时要注意一个细节校验阈值时最小值必须小于最大值这个校验既要在前端做即时提醒用户也要在后端做防止绕过页面直接调接口。后端校验放在 Service 层只有校验通过才走 Mapper 的 update 方法不通过就抛异常。这是 Web 安全的基本功凡是用户输入的数据都不能信任包括数值的大小关系。5. SSM MySQL 实战避坑环境、事务、时间与并发八个常见问题5.1 环境类坑MySQL 版本、驱动与时区引发的连锁反应现象项目启动时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者 Mapper 执行 SQL 时报Access denied for user。原因MySQL 8.x 的驱动改成了com.mysql.cj.jdbc.Driver且连接 URL 必须显式声明时区。解决url 后拼接?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8同时确认 pom.xml 里 MySQL 依赖的版本和本地数据库版本一致。这个坑踩了的人非常多因为 MySQL 5.7 的老驱动写法在网上流传太广新项目一不小心就复制到旧配置。现象Tomcat 启动时端口被占用报Port 8080 required by Tomcat v9.0 Server is already in use。原因上一次非正常关闭 Tomcat进程还留在后台。解决Windows 下用netstat -ano | findstr 8080找到 PID再用taskkill /PID 进程号 /F杀掉Linux/Mac 下用lsof -i:8080找 PIDkill -9结束进程。这个不算技术难题但每个 SSM 项目新手都会遇到问得多了也就成经典面试场景了。5.2 事务类坑声明了 Transactional 却不生效现象插入一条报警记录后抛了异常但数据还是写进去了事务回滚没有生效。原因排查Transactional注解默认只对 RuntimeException 回滚对受检异常如 SQLException 被包装后抛出不回滚。另一个常见原因是事务方法被同类内部调用绕过了 Spring 的代理对象事务完全没生效。解决在Transactional上显式声明rollbackFor Exception.class并确保事务方法是从外部 Bean 调用的。我在实际项目中一般把事务放在 Service 实现类的方法上Controller 调 Service而不是在 Controller 上加事务。5.3 时间类坑JSON 序列化与 MySQL 时区不一致现象页面显示的采集时间和数据库里的时间差了 8 小时。原因MySQL 连接 URL 没有指定serverTimezoneAsia/Shanghai驱动默认取了系统时区而 Jackson 序列化 Date 时又用了 UTC。解决url 加serverTimezoneAsia/Shanghai同时给实体类的collectTime字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。这个时间问题在作物监控系统里尤其不能忍因为用户看的就是时间序列曲线时间错位会让数据对不上曲线整体平移 8 小时。5.4 业务逻辑坑报警重复插入与历史数据膨胀现象模拟数据跑了几个小时alert_record表里出现几百条一模一样未处理的报警记录。原因定时任务每 5 秒检查一次只要温度连续 5 次超限每次都满足插入条件而数据一直没有被处理countUnhandled也阻止不了因为每次检查后状态仍是 0。解决三种方案结合使用。第一检查逻辑改为“最近一次报警时间距今超过 10 分钟才允许再次插入”。第二报警记录自动过期或标记已处理。第三插入前加唯一索引比如device_id param_name alert_type DATE(create_time)。现象sensor_data表三个月后已经有 150 万条数据页面查询趋势图明显变慢索引也加了但改善有限。原因数据量到了一定级别单表全量扫描即使走索引也很慢特别是范围查询要回表取整行数据。解决按月分表或者至少把旧数据定期导出归档。对于课程设计或中小型项目一个务实的做法是写一个定时任务每天删除 30 天前的数据或者只保留按小时聚合的统计数据。5.5 部署类坑JDK 版本与 Tomcat 版本不匹配带来的报错现象本地 Eclipse 里跑得好好的打成 war 包放到 Tomcat 上启动报UnsupportedClassVersionError。原因本地编译用的 JDK 版本高于 Tomcat 运行时所用的 JRE 版本编译出的 class 文件版本号不兼容。解决在 pom.xml 里显式指定编译版本或者更简单点本地开发时就把 JDK 版本统一成和生产环境一致。血泪经验是不要用 JDK 17 编译一个要跑在 Tomcat 8.5 上的项目除非你把编译 target 改成 1.8。SSM 项目最稳的组合是 JDK 8 Tomcat 8.5 MySQL 5.7 或 8.0。6. 进阶技巧给传感器数据做定时归档和统计项目做完核心功能后我会建议你再补一个能力数据归档与统计。这个功能不仅让系统更完整更重要的是它解决了运行一段时间后必然遇到的性能问题。传感器数据是典型的时序数据如果只增不删表会越来越臃肿。做归档的思路是把sensor_data里超过 30 天的数据按天聚合后存入一张sensor_data_daily汇总表原始明细可以删除或备份这样历史趋势查询走汇总表性能提升非常明显。汇总表的结构比原始表简单device_id、stat_date、avg_temperature、max_temperature、min_temperature、avg_humidity、avg_soil_moisture。用 Spring 的定时任务每天凌晨跑一次SQL 大概长这样INSERT INTO sensor_data_daily (device_id, stat_date, avg_temperature, max_temperature, min_temperature, avg_humidity, avg_soil_moisture) SELECT device_id, DATE(collect_time) AS d, ROUND(AVG(temperature), 2), MAX(temperature), MIN(temperature), ROUND(AVG(humidity), 2), ROUND(AVG(soil_moisture), 2) FROM sensor_data WHERE collect_time DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND collect_time CURDATE() GROUP BY device_id, DATE(collect_time);这段 SQL 拿前一天的数据做聚合DATE_SUB(CURDATE(), INTERVAL 1 DAY)算出昨天的零点GROUP BY device_id, DATE(collect_time)按设备和日期分组。写入汇总表后就可以删除对应的原始明细了。注意执行顺序一定是“先聚合写入再删除原始”且删除前用SELECT COUNT(*)验证汇总表确实有数据。这里我吃过亏当时图省事直接按时间删明细结果定时任务前一天挂了当天明细全没了数据断档画出来的曲线缺了一块补都补不回来。定时任务的执行时间建议放在凌晨三四点因为那个时段几乎没有用户访问系统数据库压力小。此外汇总表也要建索引查询时按device_id stat_date做联合索引日常趋势图直接查这张表就够了MySQL 的负载也能降下来。做完这一步你的作物生长监控系统就从“能跑的课设”变成了“能长期运行的小工程”这个差别在答辩或面试时很容易被识别出来。最后说一个我个人的习惯项目做完我会把 MySQL 的数据导出一份 SQL 备份然后用一个空白数据库重新执行建表脚本和初始数据从零跑一遍启动流程。这个动作能暴露很多“我本地能跑”的错觉——比如忘了配环境变量、少了初始化 SQL、配置文件里写死了本地路径。把这些都修好系统才算真正交付。希望这篇笔记帮你在 SSM 和 MySQL 这条路上少走一段弯路。本文还有配套的精品资源点击获取
返回列表