简介:这是一份基于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.xml
SSM 项目跑不起来,八成是配置文件之间没配合好。需要三个核心配置文件,各管一摊事:applicationContext.xml 管 Spring 容器,扫描 Service 和 Mapper;spring-mvc.xml 管 SpringMVC,扫描 Controller 并配置视图解析器;mybatis-config.xml 管 MyBatis 的全局行为。先看 Spring 的核心配置:
<!-- applicationContext.xml 核心片段 --> <context:component-scan base-package="com.farm.service, com.farm.mapper"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/farm_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.farm.mapper"/> </bean>这段配置里有三个关键点。第一,context:component-scan的 base-package 必须同时覆盖 service 和 mapper 两个包,漏了任何一个都会导致启动时找不到 Bean。第二,MySQL 8.0 以上必须用com.mysql.cj.jdbc.Driver,并且 url 里要带serverTimezone=Asia/Shanghai,不然会报时区错误。第三,SqlSessionFactoryBean的mapperLocations指向classpath:mapper/*.xml,这要求你的 Mapper 接口和 XML 文件在同一包路径下且同名,比如SensorDataMapper.java对应SensorDataMapper.xml。
SpringMVC 的配置相对简单,核心是开启注解驱动和配置视图解析器:
<!-- spring-mvc.xml 核心片段 --> <mvc:annotation-driven/> <context:component-scan base-package="com.farm.controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 id="selectLatestByDevice" resultType="com.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 id="selectHistory" resultType="com.farm.entity.SensorData"> SELECT device_id, temperature, humidity, soil_moisture, light_intensity, collect_time FROM sensor_data WHERE device_id = #{deviceId} AND collect_time >= #{startTime} AND collect_time <= #{endTime} ORDER BY collect_time ASC </select>注意 XML 里写大于号、小于号必须用>、<转义,否则 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 条采集记录 List<SensorData> 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) { List<SensorData> 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 后拼接?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8,同时确认 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找 PID,kill -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 没有指定serverTimezone=Asia/Shanghai,驱动默认取了系统时区,而 Jackson 序列化 Date 时又用了 UTC。解决:url 加serverTimezone=Asia/Shanghai,同时给实体类的collectTime字段加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。这个时间问题在作物监控系统里尤其不能忍,因为用户看的就是时间序列曲线,时间错位会让数据对不上,曲线整体平移 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 这条路上少走一段弯路。
本文还有配套的精品资源,点击获取