搞 Java 后端这几年,帮人改过不少毕业设计代码,也见过太多“从网上下一套源码,跑不起来,Ctrl+CV 之后连项目结构都讲不清”的情况。今天拿一个很典型的题目来聊:SpringBoot智慧农业信息服务平台。这套系统在我这边跑过完整流程,从数据库脚本到前后端联调,再到打 Docker 镜像部署,都验证过,不是那种只能停留在“启动成功”层面的半成品。
这个项目适合谁?如果你正在做 Java 方向的毕业设计,或者想快速搞懂一个真实的 Spring Boot 团队项目长什么样,这篇文章值得你花 20 分钟看完。我会把技术选型、数据库设计、核心接口实现、WebSocket 推送、Docker 部署这些环节全部拆开讲,还会把项目里最常见的版本兼容、大文件上传、资源映射这些坑一并说清楚。
1. 项目整体设计与技术选型拆解
1.1 智慧农业信息服务平台到底解决什么问题
智慧农业这个词听起来大,落到一个真实项目里,核心其实就是三件事:数据采集、状态可视、远程可控。
在农业场景下,大棚里的温湿度、光照强度、土壤水分,水产养殖里的水温、PH 值、溶解氧,这些数据以前是靠人去现场看、拿本子记。这套平台做的,就是把这些数据统一汇总到一个后台系统里,通过传感器定时上报,Spring Boot 后端负责接收和存储,前台通过 Vue 页面和大屏看板把数据展示出来。当某个指标超出阈值,系统自动产生告警记录,并推送给绑定该地块的用户。
这个定位决定了它不是一个大而全的商城或管理系统,而是一个典型的 Iot + 业务管理 的融合项目,这也正是它适合作为毕设的原因:能展示的技术点足够多,又不至于复杂到一个人做不完。
1.2 为什么选 Spring Boot,而不是 SSM 或 Spring Cloud
很多人在写毕设时会纠结框架选择。我的建议很简单:对于单体应用能够承载的场景,Spring Boot 永远是第一选择,SSM 那种 XML 满天飞的开发方式真的没有必要再用了。
Spring Boot 的核心价值是“自动装配”,它把 Spring 生态中原先大量繁琐的配置变成了约定大于配置。你想用一个 Web 组件,加一个 spring-boot-starter-web 依赖即可;想连 MySQL,加 spring-boot-starter-jdbc,数据源配置写在 application.yml 里就完事。相比 SSM 时代要手写 SqlSessionFactory、手写 DispatcherServlet 配置,开发效率不是一个量级。
那为什么不直接上 Spring Cloud?因为那个体系涉及服务注册、配置中心、网关、熔断降级,对于智慧农业系统,设备上报和后端管理本质上还是单服务就能扛住的体量。强行微服务化只会增加部署和调试成本,而且对毕设答辩来说,说不清楚分布式组件的工作原理反而容易露怯。
核心选型建议(实测可靠):
- JDK 1.8 + Spring Boot 2.7.x(最稳组合,下面会展开讲版本问题)
- MyBatis-Plus:单表 CRUD 不用手写 SQL,节省大量代码量
- MySQL 8.0:数据存储主力
- Redis:用于验证码缓存、Token 管理、热点数据缓存
- Vue 2 + Element UI 或 Vue 3 + Element Plus:做管理后台前端
- WebSocket:用于环境数据和告警信息实时推送
- Swagger / Knife4j:接口文档自动生成,答辩时可以现场演示接口调用
注意:如果要用 Spring Boot 3.x,JDK 必须上到 17+,很多云服务器上的部署环境还在用 JDK 8,建议不要在毕设阶段给自己增加版本适配的负担。纯本地跑可以尝鲜,但部署和答辩演示环节很被动。
1.3 包结构与工程目录怎么规划才像“正规军”
很多从网上下载的源码包结构乱七八糟,各种类塞在同一个包下面。一个好的后端工程结构,应该在拿到项目的瞬间就能看出分层意图。这个平台我按下面的方式组织包结构:
text复制com.agriculture.platform
├── controller # 控制层,接收前端请求
├── service # 业务层,接口 + impl
├── mapper # MyBatis-Plus 的 Mapper 接口
├── entity # 数据库实体类
├── dto # 数据传输对象,避免实体类直接暴露给前端
├── vo # 视图对象,封装返回给前端的数据
├── config # 配置类:跨域、WebSocket、MyBatis-Plus分页、Swagger
├── handler # 全局异常处理、公共返回结果封装
├── interceptor # 登录拦截器、JWT 认证拦截器
├── utils # 工具类:JWT、日期、字符串处理
└── task # 定时任务:数据统计、清理过期 token
controller 只做参数接收和结果返回,service 里写业务规则,mapper 层处理数据库操作。这样分工清晰,后续扩展功能时只需要在对应层添加代码即可。包里还放了 CommonResult<T> 这个统一返回类,所有接口的返回格式都是 { code, message, data },前端只需要一个 axios 拦截器就能统一处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块与业务闭环
2.1 系统功能地图:用户端、管理端、大屏端
一套完整的智慧农业平台,从功能上可以分成三个端口来看:
用户端(种植户/养殖户):
- 注册、登录、个人信息维护
- 地块管理(新建地块、绑定设备)
- 查看实时环境数据与历史曲线
- 接收告警消息(WebSocket 实时弹窗、告警记录列表)
- 农事记录:施肥、灌溉、打药等农事操作的登记
管理端(平台运营/管理员):
- 用户管理:用户列表、冻结/解冻、角色分配
- 设备管理:设备注册、所属地块绑定、设备状态监控
- 告警处理:查看告警列表、标记处理状态
- 数据看板:区域维度汇总数据、环境指标统计
大屏端:
- 通过 Vue + ECharts 实现可视化数据面板,展示气温趋势、土壤湿度分布、设备在线率等
整个系统的核心业务闭环是:设备上报 -> 服务端接收 -> 数据入库 -> 实时推送前端 -> 触发告警 -> 用户处理反馈。在做数据库和接口设计时,始终要保证这条链路是通畅的。
2.2 数据库设计:核心表与字段到底怎么定
数据库是一个项目的承重墙。智慧农业平台的核心表我梳理成下面这几张,你可以直接作为表设计参考。
用户表 sys_user:主键、用户名、密码(BCrypt 加密)、真实姓名、手机号、角色 ID、状态(0 禁用 1 正常)、创建时间。角色这块建议做简单的 role_id 字段而不是单独搞一张角色权限表,毕设阶段避免过度设计。
地块信息表 farmland:主键、用户 ID、地块名称、位置描述、面积(亩)、作物类型、设备 ID、状态、创建时间。这块的关联逻辑是“一个用户可以管理多个地块,一个地块至少绑定一个设备”,通过 user_id 外键关联。
设备信息表 device_info:主键、设备编号(唯一)、设备名称、设备类型(传感器/控制器)、地块 ID、状态(在线/离线/禁用)、最后上报时间。设备编号是硬件上报数据时用来标识身份的关键字段,服务端要做唯一性校验。
环境数据表 env_data:主键、设备编号、空气温度、空气湿度、土壤温度、土壤湿度、光照强度、二氧化碳浓度、采集时间。这张表是典型的时序数据表,数据量会比较大,建议按天建立索引,索引字段是 device_no + create_time。
告警记录表 alert_record:主键、设备编号、告警类型(温湿度超限/设备离线/土壤湿度偏低)、告警内容、触发值、标准值、状态(未处理/已处理)、处理人、处理时间。告警不是只存一条历史记录,还承担着用户待办事项的功能,所以“状态”字段很关键。
农事记录表 farm_record:主键、地块 ID、操作类型(施肥/灌溉/打药/除草)、操作时间、操作人、备注。这张表突出的是业务完整性,让系统不只是看数据的工具,而是能围绕农业生产过程做管理。
数据库字符集统一用 utf8mb4,排序规则用 utf8mb4_general_ci,否则前端提交一些特殊符号(比如 emoji 表情备注)时会报“Incorrect string value”的错误。
2.3 从数据采集到业务闭环的实现思路
硬件传感器上报数据是整套系统的“水源”。实际对接时设备一般走两种协议:HTTP POST JSON 或 MQTT。对于毕设项目,我用 HTTP 上报做演示占大多数,因为不用额外搭建 MQTT Broker(EMQX),写一个模拟设备定时上报的脚本就行。
数据接收接口的设计很精简:
java复制@PostMapping("/api/device/data/report")
public CommonResult<?> reportData(@RequestBody EnvDataReportDTO dto) {
// 1. 根据设备编号查询设备信息,确认设备是否注册且启用
// 2. 插入 env_data 表
// 3. 判断当前指标是否超过阈值,超过则写入 alert_record
// 4. 通过 WebSocket 将最新数据推送给前端
// 5. 更新设备最后上报时间与在线状态
return CommonResult.success();
}
这个接口做完,整个系统的“活水”就来了。前端页面可以实时看到数据跳动,后端也能持续充电能。
3. 实操过程与核心环节实现
3.1 从零初始化一个 Spring Boot 工程
这个环节网上教程很多,但实际操练起来总有几个小细节会卡住新人。我按照最稳的路径说一遍。
打开 https://start.spring.io/ ,选择 Maven 工程,Java 版本选 8,Spring Boot 版本选 2.7.x(目前 2.7.18 是 2.x 的最终版本,bug 修复最全)。依赖这里不用纠结太多,先勾选这几项:Spring Web、MySQL Driver、Validation。
生成后用 IDEA 打开,手动往 pom.xml 里补充以下关键依赖:
xml复制<!-- MyBatis-Plus 针对 Spring Boot 2 的版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- JWT 认证 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- WebSocket 实时通信 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 接口文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
application.yml 的核心配置:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/agriculture?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
username: root
password: 123456
redis:
host: localhost
port: 6379
database: 0
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
这里有个特别容易被忽视的点:MySQL 8.0 的驱动类必须是 com.mysql.cj.jdbc.Driver,不是 com.mysql.jdbc.Driver,后者在新版本驱动中已经移除了。url 中 serverTimezone=Asia/Shanghai 必须写上,否则数据库连接会因为时区问题直接报错。
还有一个坑是 MyBatis-Plus 的逻辑删除字段配置。它要求实体里有一个 deleted 字段,数据库表也要有 deleted 列。配置好之后,删除接口执行的是 UPDATE ... SET deleted=1,查询时自动过滤掉已删除记录。如果数据库表结构里没有这个字段但配置又开了,查询会直接报 deleted column not found。
3.2 基于 MyBatis-Plus 快速实现核心业务接口
MyBatis-Plus 最香的地方在于单表 CRUD 完全不用写 SQL。以用户管理为例,实体类加上注解,Mapper 接口继承 BaseMapper<T>,Service 继承 IService<T>,基础方法就全都有了。
java复制@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String nickname;
private String phone;
private Integer roleId;
private Integer status;
private LocalDateTime createTime;
}
java复制@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
java复制public interface SysUserService extends IService<SysUser> {
}
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
}
这样一个用户表的基础操作全部就绪:selectById、selectList、insert、updateById、deleteById。复杂一点的需求再用 QueryWrapper 或 LambdaQueryWrapper 来拼条件,比如分页查询用户列表:
java复制public IPage<SysUserVo> pageUser(int pageNum, int pageSize, String keyword) {
Page<SysUser> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(keyword), SysUser::getUsername, keyword)
.orderByDesc(SysUser::getCreateTime);
// 分页插件需要在 config 里注册,否则分页会失效
IPage<SysUser> result = this.page(page, wrapper);
// 转换为 Vo 并返回
}
分页插件这块要特别注意:如果没有在配置类中注册
MybatisPlusInterceptor并添加PaginationInnerInterceptor,你再怎么调page方法都不会真正执行 LIMIT,而是把全表数据查出来然后在内存里做假分页。数据量少时看不出来,数据量上来后系统会明显变慢。
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
密码加密这块一定要用 BCrypt。很多人图省事用 MD5,答辩时被问到“密码如何安全存储”就现了原形。Spring Security 框架自带 BCryptPasswordEncoder,但为了给项目减负,可以直接引入 spring-security-crypto 这个轻量依赖,单独使用 BCrypt 编码器,不引入整个安全框架。
java复制@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 注册时加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
// 登录时校验
if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
BCrypt 加密后每次生成的密文都不同,但 matches 方法可以校验。这个细节在答辩时提到,是加分项。
3.3 环境数据实时推送:轮询不如 WebSocket
当设备上报数据后,前端页面要立即看到最新数值。最笨的方案是前端定时轮询,比如每 3 秒调一次查询接口。这样做的问题很明显:并发一高,服务器被无意义的请求刷爆,而且实时性有延迟。
正确做法是使用 WebSocket,后端主动往浏览器推数据。Spring Boot 对 WebSocket 的支持很成熟,我用的方案是 TextWebSocketHandler + 一个在线会话管理器。
java复制@Component
public class DataWebSocketHandler extends TextWebSocketHandler {
private static final Map<Long, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
public void sendToUser(Long userId, String message) {
WebSocketSession session = SESSION_MAP.get(userId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(message));
} catch (IOException e) {
// 发送失败记录日志,不需要让业务事务回滚
}
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
Long userId = (Long) session.getAttributes().get("userId");
SESSION_MAP.put(userId, session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
SESSION_MAP.values().remove(session);
}
}
用户需要登录才能建立 WebSocket 连接,这就要在 WebSocket 握手的时候校验身份。实现 HandshakeInterceptor,在拦截器里从请求参数或 Header 中解析出 JWT,解析成功后把 userId 放入 attributes,握手失败则直接拒绝连接。
java复制@Component
public class AuthHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
String token = URLDecoder.decode(request.getURI().getQuery(), StandardCharsets.UTF_8);
// 解析 token 并放入 attributes
Long userId = JwtUtils.getUserId(token);
attributes.put("userId", userId);
return true;
}
}
设备上报数据的接口里,接收完成并入库后,组装一条 JSON 消息,调用 DataWebSocketHandler.sendToUser(userId, message) 推给前端。前端 Vue 页面在进入环境监测页时创建 WebSocket 连接,收到消息后触发数据更新。
3.4 项目部署:本机跑通到 Docker 上线全流程
很多人口中的 Docker 部署就是网上下一个 Dockerfile 复制粘贴,跑不起来也不知道怎么排查。这里我把一套真正可靠、针对 JDK 1.8 打包到 Docker Desktop 的流程完整发出来。
第一步:打包。 本机如果配置了 Maven 环境变量,在项目根目录执行:
bash复制mvn clean package -DskipTests
如果不想装 Maven,IDEA 右侧 Maven 面板里双击 package 也能完成。打包完成后,target 目录下会有一个 agriculture-0.0.1-SNAPSHOT.jar。
注意,Spring Boot 2.x 打的包是可执行 JAR,不能直接用 java -jar 之外的普通方式解压依赖。Dockerfile 里要直接把这一个 JAR 放进去。
第二步:写 Dockerfile。 这里最容易踩的坑是基础镜像选择。很多人用 openjdk:8-jdk-alpine,结果跑起来提示 缺少 fontconfig 或者时间不对。我建议用这个:
dockerfile复制FROM openjdk:8-jdk-alpine
MAINTAINER yourname
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
COPY target/agriculture-0.0.1-SNAPSHOT.jar agriculture.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Xms256m", "-Xmx512m", "-jar", "agriculture.jar"]
-Xms 和 -Xmx 根据服务器内存调,个人开发和演示阶段 256m-512m 足够。如果服务器内存就 1G,不要给 JVM 分配 1G 以上的堆内存,否则容器启动后很容易被系统 OOM Killer 杀掉。
第三步:构建并启动。
bash复制docker build -t agriculture:v1 .
docker run -d --name agriculture-server -p 8080:8080 agriculture:v1
查看日志确认启动成功:
bash复制docker logs -f agriculture-server
看到类似 Started AgricultureApplication in x.xxx seconds 的日志就说明正常运行了。访问 http://localhost:8080/doc.html,如果能看到接口文档页面,说明 Swagger 也配置成功了。
Docker Desktop 环境下如果端口被占用,会报
port is already allocated。用docker ps -a找到旧容器,删掉再重新启动,不要反复用同一个端口硬试。
4. 常见问题与排查技巧实录
4.1 Spring Boot 版本与 JDK 版本冲突
这是最近半年我收到过的最多的提问,也是热词里频繁出现的点。
Spring Boot 2.x 最高支持到 JDK 8,Spring Boot 3.x 要求 JDK 17 起步。如果你本地默认 JDK 是 17 或 21,又建了一个 Spring Boot 2.7 的项目,启动时大概率会遇到这类报错:
text复制java.lang.IllegalArgumentException: Invalid value type for attribute 'factoryBeanObjectType': java.lang.String
原因很简单:Spring Boot 2.x 依赖的 Spring Framework 5.3 在较新 JDK 上存在兼容性问题。解决思路有这么几条:
- 把 JDK 切回 8,并重开 IDEA,让 IDEA 重新索引(最稳妥)
- 项目升级到 Spring Boot 3.x,代码里
javax.*全部改成jakarta.*,同时调整 MyBatis-Plus 到 3.5.5+ 版本(改动面较大) - 如果只是查看源码或调试,降级版本不如降级 JDK 来得干净
另外有个小技巧:IDEA 中切换 JDK 后 pom.xml 里的 <java.version> 也要同步修改,最好改成 1.8,而不是 8,两个写法都能编译,但 1.8 更直观。
4.2 大文件上传与静态资源映射配置
智慧农业平台如果涉及现场照片上传,或者用户导入导出 CSV、Excel 数据表,就会碰到大文件上传设置的坑。
Spring Boot 默认上传大小限制是 1MB,超过就报:
text复制MaxUploadSizeExceededException
在 application.yml 中放开限制:
yaml复制spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
max-file-size 是单文件上限,max-request-size 是请求体总大小上限。如果前端一次传多个文件,后者一定要大于前者,否则总体积略超就报错。
文件上传后保存到服务器磁盘,但这时代码里不能直接用相对路径。我建议把文件存储路径配置在 application.yml 中:
yaml复制file:
upload-dir: /data/agriculture/upload/
然后写一个配置类做静态资源映射,让上传后的文件可以通过 URL 直接访问:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${file.upload-dir}")
private String uploadDir;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**")
.addResourceLocations("file:" + uploadDir);
}
}
这样,上传后的文件保存到 /data/agriculture/upload/xxx.jpg,前端通过 http://localhost:8080/files/xxx.jpg 就能直接访问到图片。存储路径的末尾必须带斜杠,否则 Spring 拼接资源路径时会把最后一个目录名截掉,导致 404。
4.3 前后端联调中的跨域与认证问题
前端工程跑在 5173(Vite)或 8081(开发 server),后端跑在 8080,前端的 axios 请求默认会被浏览器跨域策略拦截。后端的跨域配置是必要的:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
注意,
allowedOriginPatterns和allowCredentials(true)必须配合使用。如果只写allowedOrigins("*")又开了 credentials,浏览器会直接拒绝响应,控制台报错信息还不明显,很容易浪费一堆时间排查。
认证方面,我用了 JWT 方案而不是传统的 Session。登录成功后后端返回一个 token,前端存在 localStorage 里,每次请求在 axios 拦截器中添加 Authorization 头。后端用一个拦截器统一校验:
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行登录接口和文档接口
if (request.getRequestURI().contains("/auth/login") ||
request.getRequestURI().contains("/doc.html")) {
return true;
}
String token = request.getHeader("Authorization");
// 校验失败抛出自定义异常,由全局异常处理器返回 401
Long userId = JwtUtils.parseToken(token);
request.setAttribute("userId", userId);
return true;
}
}
用了 JWT 之后,Session 状态就不存在服务器端了,符合现在前后端分离的主流实践。答辩时讲明白“为什么用 JWT 而不是 Session”这个点,面试官和指导老师一般都比较认可。
4.4 设备数据上报并发与数据延迟问题
模拟设备上报时,如果硬件用轮询方式每 1 秒上报一次,或者测试时用脚本模拟 100 台设备同时上传,数据库写入压力一下就上来了。新手最容易遇到的现象是:MySQL 报 Deadlock found when trying to get lock,或者接口响应时间飙升。
这里给一套务实的优化策略。首先,环境数据表不需要每次都实时刷新前端,MyBatis-Plus 的 insert 批量插入比单条插入性能高很多。设备上报时并不一定来一条插一条,可以先缓存到 Redis 的 List 里,然后定时任务每隔 5 秒批量刷入数据库:
java复制// 上报接口只写 Redis
stringRedisTemplate.opsForList().leftPush("env:data:queue", JSON.toJSONString(dto));
// 定时任务批量落库
@Scheduled(fixedDelay = 5000)
public void batchFlushEnvData() {
List<String> records = stringRedisTemplate.opsForList().rightPop("env:data:queue", 100);
List<EnvData> dataList = records.stream().map(r -> JSON.parseObject(r, EnvData.class)).toList();
envDataService.saveBatch(dataList);
}
这个做法的好处是削峰填谷。设备上报的 QPS 压力由 Redis 扛住,数据库端批量写入能有效避免死锁和连接池打满问题。定时任务记得在启动类加 @EnableScheduling,否则整个定时任务不会生效,这个隐藏坑坑过很多人。
另一个常见问题是数据延迟。定时任务批量刷库可能导致前端看到的数据有 5 秒左右的延迟。由于 WebSocket 推送的是接收时的最新值,这个延迟基本可以接受。如果对实时性要求很高,把批量插入的 fixedDelay 调小到 2 秒即可。
5. 项目做实了之后,还能往哪个方向扩展
如果这个项目不只是为了毕业设计,你还能在现有基础上做很多扩展,从中学到的东西也会远超课程要求。
第一个方向是把硬件真正接进来。现在很多主流开发板都支持 MQTT 协议,可以用 Java 的 Eclipse Paho 客户端直接对接 MQTT Broker。服务端只需要把数据上报接口从 HTTP 换成 MQTT 订阅消费,代码层面多写一个 MQTT 配置类和消息处理类即可。这样整个项目从“模拟数据”变成了“真实物联网系统”,含金量完全不同。
第二个方向是做数据可视化大屏。网上有很多开源的前端大屏模板,配合 ECharts 的折线图、柱状图、地图等组件,把环境数据、设备状态、告警统计做成实时刷新的可视化看板。这个比后台管理表格更有视觉冲击力,答辩演示时也更容易让评委记住。
第三个方向是引入消息队列。设备上报链路加上 RabbitMQ 或 Kafka 后,数据采集、业务处理和告警推送就能完全解耦。这套架构在真实的物联网平台中非常常见,是简历上可以写的一笔。
第四个方向是从单体走向微服务。如果在这个项目的基础上拆出用户服务、设备服务、数据服务、告警服务,用 Spring Cloud Alibaba 的 Nacos + Gateway 组合来做服务治理,那就直接把你从毕设水平拉到了企业初级开发水平。
这几个方向的优先级我按照“投入产出比”排了序。毕设阶段先做 1 和 2 就足够拉开差距,如果时间和精力允许,3 和 4 可以作为亮点写入项目经历。
我在实际做这个项目的过程中最深的体会是:毕设项目的价值不在它有多复杂,而在你能不能把每一步为什么这么做讲清楚、出问题时能不能很快定位修复。拿这套智慧农业平台来说,你真正理解了自己是怎么从零把它搭起来、版本怎么选、数据怎么流转、部署怎么执行之后,再去写论文、做答辩,都会从容得多。
最后分享一个很多同学容易忽略的小技巧:项目的 README 一定要写清楚——怎么建库、怎么改配置、怎么启动、默认账号是什么。这个文件在答辩现场非常有用,指导老师看代码前一般会先看 README,一个能快速跑起来的项目,印象分比什么都重要。
