果树生长管理系统:Spring Boot+Vue全栈农技平台实战
简介本资源是一份面向计算机专业本科生与Java全栈初学者的毕业设计/课程设计完整文档聚焦农业信息化场景提供基于Spring BootVueMySQL的果树生长信息管理系统的设计与实现全过程说明。文档系统阐述了系统架构分层Controller-Service-DAO、技术选型依据Java跨平台性、Spring Boot快速集成、MySQL高并发存储、Tomcat稳定部署及农业管理痛点解决方案涵盖需求分析、模块设计、数据库ER图、核心代码逻辑说明与应用价值总结。资源为单个8.53MB的Word文档.docx内容完整包含摘要、英文摘要、目录、绪论、需求分析、系统设计与实现等标准毕设章节结构规范可直接用于答辩参考或二次开发学习。目前已有63人下载学习适合需要理解前后端分离架构落地、农业类信息系统设计思路及Spring Boot工程化实践的学习者。1. 果树生长信息管理系统不是农业App而是可落地的B/S架构农技管理平台很多人第一眼看到“果树生长信息管理系统”会下意识归类为又一个带点农业情怀的课程设计——直到你打开它的 Vue 前端控制台看到axios正在向/api/tree/growth-record发送 POST 请求点开 Spring Boot 后端的GrowthRecordController.java发现它用Valid RequestBody GrowthRecordDTO接收数据并通过growthRecordService.save()落库到 MySQL 的t_growth_record表。这不是演示项目而是一个具备完整用户角色分离果农/专家/管理员、支持生长周期阶段标记萌芽期→开花期→坐果期→成熟期、内置专家问答异步通知、且数据库字段级支持时间戳自动填充与逻辑删除的真实系统。它解决的不是“有没有”的问题而是“怎么让果农在田间地头用手机拍张病叶照片后30秒内完成上报关联果树ID触发专家待办”的闭环效率问题。系统不依赖安卓/iOS原生能力而是用 Vue 3 Composition API Element Plus 构建响应式 Web 界面适配微信浏览器、Chrome、Safari 甚至老旧安卓平板的 WebView后端用 Spring Boot 2.7.x非最新版但兼容 JDK 8/11规避了 Spring Boot 3.x 对 Jakarta EE 9 的强依赖带来的部署风险MySQL 5.7 表结构中明确区分is_deleted TINYINT(1) DEFAULT 0与delete_time DATETIME NULL为后续审计留出空间。适合中小型果园技术员快速部署、农业合作社统一运维、高校农信专业教学复现——尤其当你需要一份能跑通、能改、能讲清每一层职责的全栈案例时它比“Spring Boot Vue 实现 CRUD”更有纵深感。2. 技术选型拆解为什么是 Spring Boot 2.7.x Vue 3 MySQL 5.7 这个组合2.1 Spring Boot 分层架构的实操约束力Controller/Service/DAO 不是教条而是边界协议系统文档里写的“三层架构”常被新手理解为目录结构分三包但实际约束力体现在接口契约上。以果树生长记录提交为例// src/main/java/com/example/fruit/controller/GrowthRecordController.java PostMapping(/save) public Result save(Valid RequestBody GrowthRecordDTO dto) { growthRecordService.save(dto); // 关键只调 Service 方法不碰 DAO 或 DBConnection return Result.success(); }注意Valid触发 DTO 层校验如NotNull校验果树ID、Future校验观测时间不能早于当前日期失败直接返回 400growthRecordService.save()内部才做业务规则判断如同一果树同一天不可重复提交生长记录这才是 Service 层该干的事——Controller 只负责协议转换与权限拦截Service 才管业务原子性DAO 只管单表 CRUD。对应的 Service 实现强制隔离// src/main/java/com/example/fruit/service/impl/GrowthRecordServiceImpl.java Service public class GrowthRecordServiceImpl implements GrowthRecordService { Resource private GrowthRecordMapper growthRecordMapper; // MyBatis Mapper非 JPA Repository Override public void save(GrowthRecordDTO dto) { // 1. 检查果树是否存在且未被删除 Tree tree treeService.getById(dto.getTreeId()); if (tree null || tree.getIsDeleted() 1) { throw new BusinessException(果树不存在或已停用); } // 2. 检查当日是否已有记录 QueryWrapperGrowthRecord wrapper new QueryWrapper(); wrapper.eq(tree_id, dto.getTreeId()) .eq(DATE(record_time), LocalDate.now()) // MySQL DATE() 函数 .eq(is_deleted, 0); if (growthRecordMapper.selectCount(wrapper) 0) { throw new BusinessException(今日该果树已有生长记录); } // 3. 构建实体并保存 GrowthRecord record new GrowthRecord(); BeanUtils.copyProperties(dto, record); record.setAddTime(LocalDateTime.now()); growthRecordMapper.insert(record); } }逻辑说明treeService.getById()是另一个 Service 的方法体现跨模块调用必须走 Service 层而非直接调 MapperQueryWrapper使用 MyBatis-Plus 的条件构造器避免手写 SQL 注入风险DATE(record_time)是 MySQL 原生函数在application.yml中配置spring.jpa.database-platformorg.hibernate.dialect.MySQL57Dialect确保 Hibernate 不误转为其他方言。这种写法让每个层有明确的“不可越界”行为——Controller 不查库、Service 不写 HTTP 响应、DAO 不做业务判断。2.2 Vue 3 的 Composition API 如何承接农技场景的复杂表单交互果树生长记录页面/src/views/record/GrowthRecordForm.vue需同时处理多图上传病叶/果实特写、生长阶段下拉联动选“坐果期”后自动展开“坐果数量”输入框、专家咨询开关开启则显示提问文本域。若用 Options APIdata、methods、watch散落在不同区块维护成本高。Composition API 将逻辑按功能聚合成可复用函数!-- src/views/record/GrowthRecordForm.vue -- script setup import { ref, reactive, onMounted } from vue import { ElMessage } from element-plus import { uploadImage, saveGrowthRecord } from /api/record const form reactive({ treeId: , stage: , // 萌芽期,开花期,坐果期,成熟期 recordTime: new Date(), images: [], // 上传后的 URL 数组 expertQuestion: , isAskExpert: false }) const stageOptions [ { value: 萌芽期, label: 萌芽期 }, { value: 开花期, label: 开花期 }, { value: 坐果期, label: 坐果期 }, { value: 成熟期, label: 成熟期 } ] // 图片上传逻辑封装为独立函数 const handleImageUpload async (file) { const formData new FormData() formData.append(file, file) try { const res await uploadImage(formData) // 调用 /api/upload/image form.images.push(res.data.url) } catch (err) { ElMessage.error(图片上传失败) } } const submitForm async () { try { await saveGrowthRecord(form) // POST /api/record/save ElMessage.success(提交成功) // 重置表单 form.treeId form.stage form.images [] form.expertQuestion form.isAskExpert false } catch (err) { ElMessage.error(err.response?.data?.msg || 提交失败) } } /script参数说明form.images存储的是后端返回的 CDN 路径如https://cdn.example.com/images/20240520/abc123.jpg非 base64uploadImageAPI 返回{ code: 200, data: { url: ... } }前端只取urlsaveGrowthRecord的请求体是标准 JSON不含文件文件已提前上传避免大文件阻塞主请求。这种写法让“图片上传”和“表单提交”解耦后续扩展视频上传只需新增handleVideoUpload函数不影响主流程。2.3 MySQL 5.7 表结构设计中的农技语义强化系统共 12 张表核心 5 张t_user用户、t_tree果树、t_growth_record生长记录、t_expert_question专家提问、t_farm农场。关键设计不在字段多而在字段如何承载农业逻辑表名字段名类型约束农技语义说明t_treevariety_idBIGINT外键 →t_variety.id果树品种ID非字符串避免“富士苹果”拼写差异t_treeplanting_dateDATENOT NULL种植日期用于计算树龄TIMESTAMPDIFF(YEAR, planting_date, CURDATE())t_growth_recordstage_codeVARCHAR(20)ENUM(SPROUT,FLOWER,FRUIT,MATURE)阶段编码便于程序判断比中文字符串更易索引t_growth_recordweather_conditionVARCHAR(50)DEFAULT 晴当日天气预设值减少输入错误t_expert_questionstatusTINYINT(1)DEFAULT 00待处理,1已回复,2已关闭驱动后台工单流提示t_growth_record表的stage_code使用 ENUM 而非外键关联t_stage表是因为生长阶段固定且极少变更就那4个ENUM 在 MySQL 5.7 中查询性能优于 JOIN且避免因t_stage表误删导致数据异常。实际建表语句中需显式声明CREATE TABLE t_growth_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tree_id BIGINT NOT NULL, stage_code ENUM(SPROUT,FLOWER,FRUIT,MATURE) NOT NULL, record_time DATETIME NOT NULL, weather_condition VARCHAR(50) DEFAULT 晴, is_deleted TINYINT(1) DEFAULT 0, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_tree_stage (tree_id, stage_code), INDEX idx_record_time (record_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;复合索引idx_tree_stage加速“查询某果树所有生长阶段记录”idx_record_time支持按时间范围筛选如导出近3个月数据。3. 全栈联调关键路径从 Vue 提交到 MySQL 落库的 7 个必检节点3.1 前端请求链路验证Axios 拦截器与状态码映射Vue 项目中src/utils/request.js定义全局 Axios 实例必须包含请求/响应拦截器// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, // 代理到后端见 vue.config.js timeout: 10000 }) // 请求拦截添加 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截统一错误处理 request.interceptors.response.use( response { // 成功响应code 200 且 data 存在 if (response.data.code 200) { return response.data } else { ElMessage.error(response.data.msg || 请求失败) return Promise.reject(new Error(response.data.msg)) } }, error { // HTTP 错误码处理 if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login } else if (error.response?.status 403) { ElMessage.error(权限不足) } else { ElMessage.error(网络错误请检查连接) } return Promise.reject(error) } ) export default request验证要点baseURL: /api要求vue.config.js中配置代理否则跨域// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, // Spring Boot 默认端口 changeOrigin: true, pathRewrite: { ^/api: } } } } }Authorization头格式为Bearer tokenSpring Boot 后端需用EnableWebSecurity配置 JWT 解析否则 401response.data.code 200是后端约定的成功码非 HTTP 状态码 200Axios 已将 200~299 视为 success。3.2 后端 Controller 参数绑定与校验失效排查当 Vue 提交{ treeId: 123, stage: 坐果期 }Spring Boot 报400 Bad Request常见原因有三现象检查点修复命令/配置Required request body is missing前端未设Content-Type: application/jsonAxios 默认已设置检查是否手动覆盖Validation failed for argument [0]DTO 缺少RequestBody或字段无NotNull确认GrowthRecordDTO类上有DataLombok字段加NotBlank字符串或NotNull数字JSON parse error: Cannot deserialize instance of ...前端传stage: 坐果期但 DTO 中stage是String而数据库存stage_codeENUM必须修改 DTO 字段名为stageCode并加ApiModelProperty(SPROUT/ FLOWER/FRUIT/MATURE)前端传FRUIT而非坐果期实操验证用 Postman 直接 POST 到http://localhost:8080/api/record/saveBody 选 raw → JSON{ treeId: 1, stageCode: FRUIT, recordTime: 2024-05-20T08:00:00, weatherCondition: 多云 }若返回{code:200,msg:success}证明后端接收正常若报错看控制台Caused by:后的异常类名如MethodArgumentNotValidException表示校验失败HttpMessageNotReadableException表示 JSON 解析失败。3.3 MySQL 数据落库一致性检查事务与时间戳GrowthRecordServiceImpl.save()方法未显式声明Transactional但 MyBatis-Plus 的insert()默认在 Spring 事务管理器下执行。验证事务生效在save()方法开头加int i 1/0;故意抛异常提交表单观察t_growth_record表无新增记录且t_user表如有关联操作也无变更删除int i 1/0;改为在growthRecordMapper.insert(record)后加int j 1/0;提交表单t_growth_record表仍无记录 —— 证明 insert 操作被回滚。时间戳自动填充MySQL 表中add_time DATETIME DEFAULT CURRENT_TIMESTAMP由数据库生成但 Java 实体类GrowthRecord中addTime字段需加TableField(fill FieldFill.INSERT)TableField(fill FieldFill.INSERT) private LocalDateTime addTime;否则 MyBatis-Plus 插入时会传null覆盖数据库默认值。启动项目后执行SELECT * FROM t_growth_record ORDER BY add_time DESC LIMIT 1;确认add_time与record_time时间差在 1 秒内证明数据库自动生成有效。4. 生产部署避坑指南Tomcat MySQL Spring Boot 的 3 类高频故障4.1 Tomcat 部署 WAR 包时的静态资源 404 问题系统打包为 WAR非 JAR部署到 Tomcat 8.5 时Vue 的index.html和assets/文件常返回 404。根本原因是Spring Boot 默认将static/和templates/映射到/但 WAR 模式下 Tomcat 的webapps/ROOT/与 Spring Boot 的资源路径冲突。解决方案在pom.xml中排除内嵌 Tomcat强制使用外部 Tomcatdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope !-- 关键provided 表示由外部容器提供 -- /dependency并在src/main/webapp/WEB-INF/web.xml中声明?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namefruit-system/display-name /web-app验证步骤mvn clean package生成target/fruit-system-1.0.war将 WAR 包复制到$TOMCAT_HOME/webapps/启动 Tomcat$TOMCAT_HOME/bin/startup.sh访问http://localhost:8080/fruit-system/WAR 包名自动成为 context-path浏览器开发者工具 Network 标签页刷新页面确认index.html、assets/js/app.xxx.js状态码为 200。4.2 MySQL 连接池耗尽Druid 配置与慢查询定位系统并发量超 50 时出现com.alibaba.druid.pool.GetConnectionTimeoutException。原因常是 Druid 连接池配置过小或存在慢查询。Druid 基础配置application.ymlspring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 # 关键根据服务器内存调整4GB 内存建议 ≤20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false慢查询定位开启 MySQL 慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; -- 超过1秒记为慢查询 SET GLOBAL log_output TABLE; -- 日志存入 mysql.slow_log 表复现卡顿操作如加载全部果树列表查询慢日志SELECT query_time, sql_text FROM mysql.slow_log WHERE start_time NOW() - INTERVAL 1 HOUR ORDER BY query_time DESC LIMIT 10;若发现SELECT * FROM t_tree WHERE is_deleted0无索引立即添加ALTER TABLE t_tree ADD INDEX idx_deleted (is_deleted);提示max-active: 20意味着最多 20 个连接同时被占用若业务代码中try-with-resources未正确关闭Connection连接会泄漏。用 Druid 监控页面http://localhost:8080/druid查看ActiveCount是否持续增长是则代码必有未关闭连接。4.3 Vue 打包后路由 404Nginx 的 history 模式重写Vue Router 使用history模式URL 无#部署到 Nginx 后直接访问http://domain/record返回 404。这是因为 Nginx 默认只服务物理文件而/record是前端路由需由index.html拦截。Nginx 配置/etc/nginx/conf.d/fruit.confserver { listen 80; server_name fruit.example.com; root /var/www/fruit-system; index index.html; location / { try_files $uri $uri/ /index.html; # 关键所有路径 fallback 到 index.html } location /api { proxy_pass http://localhost:8080; # 代理到 Spring Boot proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }验证方法npm run build生成dist/目录将dist/*复制到/var/www/fruit-system/sudo nginx -t sudo systemctl reload nginx浏览器访问http://fruit.example.com/record确认页面正常加载Network 中index.html状态码 200/api/record/list状态码 200刷新页面URL 保持/record不变且内容正确 —— 证明 history 模式生效。5. 农技场景进阶技巧用 MySQL 窗口函数分析果树生长趋势系统积累数月生长记录后果农最需要的不是单条记录而是“某品种果树坐果期平均坐果数同比去年提升多少”。这需超越单表查询用 MySQL 8.0 窗口函数本系统虽用 5.7但升级后可立即启用。5.1 构建生长趋势视图按品种月份聚合坐果数假设t_growth_record表中stage_codeFRUIT时fruit_count字段存坐果数量单位个创建物化视图MySQL 5.7 用普通视图模拟-- MySQL 5.7 创建视图伪物化 CREATE VIEW v_growth_trend AS SELECT t.variety_name, YEAR(gr.record_time) AS year, MONTH(gr.record_time) AS month, AVG(gr.fruit_count) AS avg_fruit_count, COUNT(*) AS record_count FROM t_growth_record gr JOIN t_tree tr ON gr.tree_id tr.id JOIN t_variety t ON tr.variety_id t.id WHERE gr.stage_code FRUIT AND gr.is_deleted 0 GROUP BY t.variety_name, YEAR(gr.record_time), MONTH(gr.record_time);查询示例查“红富士”2023年 vs 2024年5月坐果数对比SELECT variety_name, CONCAT(year, -, LPAD(month, 2, 0)) AS ym, ROUND(avg_fruit_count, 0) AS avg_count FROM v_growth_trend WHERE variety_name 红富士 AND year IN (2023, 2024) AND month 5 ORDER BY year;返回variety_nameymavg_count红富士2023-05125红富士2024-051425.2 Spring Boot 动态 SQL 实现趋势图表数据接口前端 ECharts 需要 JSON 格式的时间序列数据后端用 MyBatis-Plus 的QueryWrapper动态构建// src/main/java/com/example/fruit/mapper/TrendMapper.java Mapper public interface TrendMapper { Select(SELECT YEAR(record_time) as year, MONTH(record_time) as month, AVG(fruit_count) as avg_count FROM t_growth_record WHERE stage_code FRUIT AND tree_id IN (SELECT id FROM t_tree WHERE variety_id #{varietyId}) AND record_time DATE_SUB(NOW(), INTERVAL #{months} MONTH) GROUP BY YEAR(record_time), MONTH(record_time) ORDER BY year, month) ListTrendData selectTrendByVariety(Param(varietyId) Long varietyId, Param(months) Integer months); } Data public class TrendData { private Integer year; private Integer month; private BigDecimal avgCount; // 注意AVG 返回 DECIMAL }Controller 调用GetMapping(/trend) public ResultListTrendData getTrend( RequestParam Long varietyId, RequestParam(defaultValue 12) Integer months) { ListTrendData data trendMapper.selectTrendByVariety(varietyId, months); return Result.success(data); }前端传GET /api/trend?varietyId1months12后端返回最近12个月的月均坐果数ECharts 直接渲染折线图。此接口避免了在 Java 层做List聚合压力全在 MySQL符合 OLAP 场景优化原则。最后一步在t_growth_record表的record_time字段上建立INDEX idx_record_time_stage (record_time, stage_code)使WHERE stage_code FRUIT AND record_time ...查询走索引10万行数据查询控制在 50ms 内。本文还有配套的精品资源点击获取