想写这篇文章的念头其实挺实在的——每年到了毕设季后台总有人来问“Spring Boot到底能做什么项目”“校园管理类系统是不是太老套了”。我得说校园绿化管理系统这个选题看起来不花哨但真正做下来你会发现它几乎把 Spring Boot 项目里该踩的坑、该用的招全给你过了一遍CRUD、文件上传、权限控制、数据统计、部署上线……一个都不少。这篇文章我就以这个“基于Spring Boot校园绿化管理系统”为例子把从选题拆分到技术选型、从数据库设计到前后端联动、再到部署答辩的整个思路完整梳理一遍全是实际做项目时总结出来的经验不是网上那种给你贴一堆截图的“云教程”。1. 校园绿化管理系统到底在管什么需求拆解与模块边界先别急着写代码。我见过太多同学拿到题目就开 IDEA 建项目建完发现自己连“绿化管理”四个字都没想清楚。我当时做这个系统的时候第一步就是去搜了一圈真实校园绿化管理员的日常工作内容才发现这类系统的核心根本不是“好看”而是“记录”和“提醒”。一个校园绿化管理系统本质上要解决三类问题校园植被台账不清楚学校到底有多少棵树、多少片绿地、分别种在哪儿全凭老园丁脑子记、养护工作没留痕谁浇的水、谁修的枝、哪天打的药什么都不记录、问题反馈靠吼树木枯死、绿地被踩踏、植被虫害学生想反映找不到入口管理员处理完也没法回复。所以我把整个系统拆成了五个大模块绿化档案管理绿地信息、植物信息、树木点位支持多级分类比如“校区-区域-绿化带-树木”这种层级。养护任务管理浇水、施肥、修剪、打药四类日常任务支持按周期生成任务单任务完成后填写养护记录。巡检与问题上报巡检员扫码或按区域巡检发现问题虫害、枯死、设施损坏登记上报管理员派单处理。物品库存管理化肥、农药、工具等物资的入库、出库、库存预警。系统管理用户管理员、养护员、巡检员、普通师生、角色权限、操作日志。这个模块拆分的逻辑说白了就是“台账-任务-反馈-物资-权限”五条线。做毕设的时候这五个模块已经足够撑起完整的业务闭环又不至于像ERP那样复杂到三个月做不完。还有一个容易被忽视的需求点普通师生的参与感。我后来在系统里加了一个“问题随手拍”入口本校师生登录后可以上传树木异常的照片和位置描述管理员在后台收到后生成工单。这个功能做起来不难但很加分——无论是评阅老师还是答辩评委看到“学生也能参与绿化管理”这个场景都会觉得你的系统有思考不是单纯为了凑模块。2. 技术选型为什么是 Spring Boot Vue 这个组合直接说结论这个系统的技术栈我定为Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Redis Vue 2 Element UI MinIO。下面我把每条选择的原因说明白因为技术选型这部分在论文里写清楚“为什么选”比“选了啥”重要得多。2.1 Spring Boot 版本2.7.x而不是 3.x我知道热搜词里有人问“springboot版本太高”怎么办这个我太有体会了。Spring Boot 3.0 发布之后Java EE 改名 Jakarta EE很多老教程里的依赖坐标都变了javax.*前缀全得改成jakarta.*。如果你照着网上两年前的教程敲代码大概率会碰到包名对不上、自动配置失效的问题。做毕设我要的是稳不是新。Spring Boot 2.7.18 是 2.x 的最后一个版本官方维护期很长几乎所有的开源教程、博客范例都是基于 2.x 写的你遇到报错去搜解决资料也更容易命中。所以除非你的课题明确要求了 Spring Boot 3否则 2.7.18 就是最优解。2.2 持久层MyBatis-Plus不是 JPA也不是原生 MyBatis热搜词里有一条“mybatis的分页插件的用法 springboot”这个直接命中了我项目里的场景。MyBatis-Plus 的分页插件用法如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置好之后业务代码里直接用Page对象接收即可IPagePlant page plantMapper.selectPage( new Page(current, size), new LambdaQueryWrapperPlant() .like(StringUtils.hasText(name), Plant::getName, name) .eq(Plant::getDeleteFlag, 0) );这里有个容易踩的坑只引入 MyBatis-Plus 依赖不配置拦截器分页查询不生效返回的总条数永远是 0。这不是小概率事件而是必定发生的事情。我身边好几个同学都栽在这个上面最后发现是忘了配MybatisPlusInterceptor。别问我为什么印象这么深刻。2.3 缓存Redis 用来存什么校园绿化系统的数据量并不大Redis 缓存我用在两个地方验证码存储登录验证码生成后存 Rediskey是用户会话唯一标识expire设置为 5 分钟。热点字典数据比如绿化区域树形结构、植物类别列表这些数据基本不变但每个页面都要用缓存到 Redis 后接口响应能从几百毫秒降到几十毫秒。缓存这部分的答辩点很清晰不是所有数据都适合用 Redis只有“读多写少、允许短时不一致”的数据才值得缓存。这句话一定要能在答辩时自己说出来。2.4 文件存储MinIO 而不是本地磁盘系统里有树木照片上传、问题随手拍的功能图片总得有地方存。两种方案方案优点缺点本地磁盘存储实现简单保存文件路径到数据库即可服务器重启文件容易丢毕设答辩换机器演示时图片全没MinIO 对象存储独立服务管理文件换环境只需改配置需要多部署一个服务稍微多几个步骤我选了 MinIO。Docker 一行命令就能启动docker run -d \ --name minio \ -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin123 \ minio/minio server /data --console-address :9001后台Java代码里用 MinIO 的 Java SDK 做上传核心就几行public String uploadFile(MultipartFile file) { String fileName UUID.randomUUID() _ file.getOriginalFilename(); minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(fileName) .method(Method.GET) .build()); }注意这里的getPresignedObjectUrl生成的 URL 是有时效的默认 7 天过期。如果你用它作为图片访问地址直接存在数据库里过七天图片就访问不了了。稳妥的做法是存文件名前端展示时后端的接口再把图片解析出来或者生成一个有效期很长的分享链接。我当时就是没注意时效性过了几天前端跟我说图片全裂了。2.5 前后端分离架构与认证方式热搜词里“springboot vue前后端分离”反复出现说明这个架构是当前毕设的主流。前后端分离模式下后端只提供 JSON 接口前端负责页面渲染和数据交互。我用的是 JWTJSON Web Token做登录认证用户登录成功后后端生成一个包含用户ID、角色、过期时间的 token 返回前端。前端把 token 存到 localStorage每次请求在Authorization请求头里带上。后端通过拦截器校验 token 是否有效、是否过期。拦截器里还要做角色权限校验。我实现的时候把“需要管理员权限”的接口路径维护在一个列表中拦截器里比对用户角色不一致直接返回 403。这个逻辑在毕设项目里够用了你要是想做得更精细可以引入PreAuthorize注解配合 Spring Security但说实话对这类管理系统来说 JWT 拦截器是性价比最高的方案实现简单答辩时也讲得清楚。3. 数据库设计六张核心表和一张容易抄错的关联表数据库设计这个环节决定了后面所有开发工作的舒服程度。有些同学上来就建表建完发现字段不够用或者冗余太严重后面改实体类改到崩溃。我习惯先画好 ER 图再动手下面这六张表是绿化管理系统的主干。3.1 绿化区域表CREATE TABLE green_area ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 区域ID, parent_id BIGINT DEFAULT 0 COMMENT 父级区域ID0表示根区域, area_name VARCHAR(50) NOT NULL COMMENT 区域名称, area_type TINYINT NOT NULL COMMENT 区域类型1校区 2区域 3绿化带, area_desc VARCHAR(255) COMMENT 区域描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, delete_flag TINYINT DEFAULT 0 COMMENT 逻辑删除0未删 1已删 );这张表用parent_id实现树形结构区域、绿化带都可以无限层级嵌套。存储区域坐标可选加longitude和latitude字段算是一个亮点可以给以后做地图可视化留个口子。3.2 植物档案表CREATE TABLE plant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, area_id BIGINT NOT NULL COMMENT 所属区域ID, plant_name VARCHAR(50) NOT NULL COMMENT 植物名称, scientific_name VARCHAR(50) COMMENT 学名, plant_type VARCHAR(30) COMMENT 类别乔木/灌木/草本/藤本, status TINYINT DEFAULT 1 COMMENT 生长状态1健康 2一般 3衰弱 4死亡, plant_height DECIMAL(8,2) COMMENT 高度(米), crown_width DECIMAL(8,2) COMMENT 冠幅(米), plant_date DATE COMMENT 种植日期, photo_url VARCHAR(255) COMMENT 照片地址, remark VARCHAR(500) COMMENT 备注, delete_flag TINYINT DEFAULT 0, PRIMARY KEY (id) );这里把area_id作为外键关联至green_area表这就是典型的“一棵树属于某片区域“的关系。有人问为什么不直接存区域名称而是存 ID因为存名称的话一旦区域改名历史数据就全部得改一遍存 ID 才能通过关联查询动态拿到当前名称。3.3 养护记录表CREATE TABLE maintenance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plant_id BIGINT COMMENT 植物ID可选, area_id BIGINT COMMENT 区域ID可选, task_type TINYINT COMMENT 1浇水 2施肥 3修剪 4打药, operator_id BIGINT COMMENT 养护人用户ID, execute_time DATETIME COMMENT 执行时间, detail VARCHAR(500) COMMENT 养护内容描述, image_url VARCHAR(255) COMMENT 养护现场照片, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, delete_flag TINYINT DEFAULT 0 );注意plant_id和area_id我都允许为空。为什么因为有的养护任务是针对整片区域比如喷灌浇水不是针对某一棵树。如果你强制必须关联到具体植物这类区域级任务就插不进去了。做设计的时候给自己留这种灵活性后期会省很多事。3.4 问题上报表很容易设计失误的表CREATE TABLE issue_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, report_user_id BIGINT NOT NULL COMMENT 上报人用户ID, report_type TINYINT COMMENT 1虫害 2枯死 3设施损坏 4绿地践踏 5其他, title VARCHAR(100) NOT NULL, content TEXT COMMENT 问题描述, location_desc VARCHAR(255) COMMENT 位置描述, images VARCHAR(500) COMMENT 图片地址多张用逗号分隔, status TINYINT DEFAULT 0 COMMENT 0待处理 1已派单 2已完成 3已关闭, handler_id BIGINT COMMENT 处理人用户ID, handle_result VARCHAR(500) COMMENT 处理结果, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, delete_flag TINYINT DEFAULT 0 );这张表是“学生随手拍”功能的数据载体。有人可能会疑惑为什么不用独立一张状态流转记录表严格来说流程引擎确实需要但作为毕设项目用status字段枚举四个状态是最务实的做法。如果你加一张issue_log表记录每次状态变更逻辑上是更完整的但这意味着每次状态变更都要插入一条日志工作量多出不少。我的建议是核心业务表满足需求即可状态流转过程可以用一个update_time字段暗示把精力留给更出彩的功能。3.5 物资表与库存记录表material表记录物资信息关键字段有material_name物资名称material_type类别化肥/农药/工具/其他unit单位袋/瓶/件total_stock总库存warning_stock预警库存spec规格说明物资的出入库用一张material_stock_record表记录每个字段material_id关联物资change_type1入库 2出库change_count变更数量operator_id操作人remark备注有人问为什么不直接在material表里改total_stock因为库存的每一次变动都得有留痕后面管理员看到库存不对了必须能从记录表里查出是谁在哪里搞错了。这种“流水表汇总字段”的双表设计是很多管理系统的通用套路答辩的时候被问“库存修改怎么实现”就能答得很从容。3.6 最容易抄错的关联表用户与角色用户表sys_user和角色表sys_role之间是多对多关系需要第三张关联表sys_user_role。这张表就三个字段id、user_id、role_id。但很多新手会犯一个错误——在用户表里直接加一个role_id字段。如果真的只有“一个用户一个身份”的场景那加字段也说得过去但校园绿化系统里一个绿化队队长很可能既是管理员管理任务派发又是养护员自己也参与养护工作。用关联表才能支持“一人多角色”。而且以后想扩展用户绑定多个角色时采用关联表完全不用改动原表结构这点很值得在论文里提一句。4. 后端核心模块实现思路与关键代码片段数据库设计完后端开发就顺畅多了。这里我把几个核心模块的实现思路拆开讲重点不是贴全部代码那太长而是讲清楚每条业务线怎么做、用到了哪些 Spring Boot 特性。4.1 登录认证模块JWT 拦截器 自定义注解登录功能看起来简单但牵扯到安全就值得仔细写。我的实现分三层第一步登录接口生成 token。用户传用户名密码后端校验通过后用jjwt库生成 tokenString token Jwts.builder() .setSubject(user.getId().toString()) .claim(username, user.getUsername()) .claim(role, user.getRoleCode()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();第二步拦截器校验 token。实现HandlerInterceptor的preHandle方法从请求头中取出Authorization尝试解析 token失败则返回 401Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/login)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); request.setAttribute(userId, claims.getSubject()); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } }第三步自定义注解校验权限。我写了一个RequireRole(admin)注解加在需要管理员权限的 Controller 方法上拦截器里读取这个注解判断角色是否匹配if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole requireRole handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole ! null) { String requiredRole requireRole.value(); String currentRole (String) request.getAttribute(role); if (!requiredRole.equals(currentRole)) { response.setStatus(403); return false; } } }这个方案比每次在 Controller 里写 if-else 判断角色优雅得多而且答辩时能讲出“自定义注解 拦截器实现声明式权限控制”好过说“我在每个接口里判断了一下”。这些细节就是区分高分和低分毕设的地方。4.2 植物档案管理的文件上传与回显回头看热搜词里有一条“springboot项目全局过滤器处理上传pdf文件时xss攻击”这个我不展开讲 XSS因为那个场景是 PDF不太一样但文件上传的正规流程值得说清楚。前端用的是el-upload组件提交时会以multipart/form-data格式把文件流发给后端。后端 Controller 这样接收PostMapping(/plant/uploadImage) public ResultString uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件为空); } String fileName fileStorageService.upload(file); return Result.success(fileName); }上传成功拿到文件名后前端再把文件名和其他字段一起提交给plant/save接口。这样页面上即时预览的效果是本地文件上传完成后先用URL.createObjectURL(file)显示临时预览保存到数据库之后下次进入详情页通过后端接口返回图片链接。这里有个经验之谈文件上传和表单提交最好分成两个接口。如果你想把图片和表单一起提交就得用multipart混合请求既要处理 JSON 又要处理文件流解析起来麻烦得多而且出错了不好排查。分开之后前端逻辑也简单先传图拿到文件名拼到表单里一起提交。4.3 养护任务模块的周期生成逻辑养护任务要支持按周期生成比如“春季每月浇水两次”怎么在代码里表达我先定义task_template养护计划表存储养护区域、任务类型、执行周期天为单位、生效日期。然后后端定时任务用 Spring 自带的Scheduled注解每天跑一次Scheduled(cron 0 0 6 * * ?) // 每天早上6点执行 public void generateDailyTasks() { ListTaskTemplate templates taskTemplateMapper.selectList( new LambdaQueryWrapperTaskTemplate() .le(TaskTemplate::getStartDate, LocalDate.now()) .ge(TaskTemplate::getEndDate, LocalDate.now()) ); for (TaskTemplate template : templates) { LocalDate lastExecuteDate getLastExecuteDate(template.getId()); if (lastExecuteDate null || ChronoUnit.DAYS.between(lastExecuteDate, LocalDate.now()) template.getCycleDays()) { createTaskFromTemplate(template); } } }这个逻辑有小细节判断该不该生成任务时不是直接拿当前时间除以周期天数而是对比“上次执行的日期”与“今天”的间隔。否则任务模板调整过开始日期时任务生成节奏会乱掉。这个细节我调了两轮才想明白。Scheduled默认是单线程执行如果以后你还要加别的定时任务比如早上统计昨天的上报问题数量就得注意线程阻塞问题。毕设规模一个定时任务没问题多个任务建议在配置里加一个线程池或者直接用Async注解配合执行器。这个属于锦上添花提一嘴就好。4.4 统计报表的数据组织系统首页需要展示一些统计数字校园绿化总面积、植物总数、待处理上报数、本月养护次数等。这些数字可以一次性从数据库聚合查询出来public DashboardVO getDashboardData() { DashboardVO vo new DashboardVO(); vo.setTotalPlantCount(plantMapper.selectCount(null)); vo.setTotalAreaCount(greenAreaMapper.selectCount(null)); vo.setPendingIssueCount(issueReportMapper.selectCount( new LambdaQueryWrapperIssueReport().eq(IssueReport::getStatus, 0) )); vo.setMonthMaintenanceCount(maintenanceRecordMapper.selectCount( new LambdaQueryWrapperMaintenanceRecord() .ge(MaintenanceRecord::getExecuteTime, LocalDate.now().withDayOfMonth(1)) )); return vo; }再复杂一点的图表数据——比如“各绿化区域植物数量对比”——用 SQL 的GROUP BY就能完成SELECT area_id, COUNT(*) AS cnt FROM plant GROUP BY area_idMyBatis-Plus 里可以用自定义 SQL 写一个 Mapper 方法返回ListMapString, Object前端遍历渲染柱状图。这类聚合统计是毕设里很稳的加分项因为图表一亮相整个系统立刻显得比“纯增删改查”高一个档次。5. 前端页面开发的关键套路先搭框架再填业务前端部分我用的 Vue 2 Element UI Axios。这个组合到现在依然是非常成熟的毕业设计方案组件库丰富遇到问题搜答案也方便。5.1 Axios 封装与统一响应处理前端开发第一步不是写页面而是封 Axios。我在utils/request.js里做了一层封装import axios from axios; import { Message } from element-ui; import router from /router; const service axios.create({ baseURL: /api, timeout: 10000, }); service.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use( (response) { const res response.data; if (res.code 200) { return res; } Message.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg || 请求失败)); }, (error) { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录); localStorage.removeItem(token); router.push(/login); } else { Message.error(error.message || 网络错误); } return Promise.reject(error); } );封装的好处是每个页面不需要自己写错误弹窗和跳转逻辑登录过期全局处理一次所有接口自动统一行为。这个模式在真实企业项目里也是通用的面试或者答辩拿出来讲都有说服力。5.2 表格页面的 80/20 套路做了几个模块之后你会发现管理系统的前端页面 80% 都是“表格 弹窗表单”的结构。以植物档案列表为例套路就是顶部放搜索栏名称、类别、状态下拉框。中间放el-table绑定数据源。右侧放“新增”“编辑”“删除”按钮操作列放“详情”“编辑”。新增和编辑用el-dialog弹窗里面放el-form。删除用el-popconfirm或this.$confirm二次确认。列表页的分页直接用后端返回的total和pages驱动el-pagination组件切换页码时重新请求接口。el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagequeryParams.pageNum :page-sizes[10, 20, 50] :page-sizequeryParams.pageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal /el-pagination这套东西熟练之后一个带增删改查的页面大概一上午就能写完后面全是复制、粘贴改字段。这不是偷懒管理类系统的开发本来就是靠这种规范化的效率。5.3 树形结构在前端的优雅展示绿化区域是树形结构前端用el-tree组件展示。我给后端写了一个listTree接口一次性返回全部区域的树形数据public ListGreenAreaVO listAreaTree() { ListGreenArea allAreas greenAreaMapper.selectList(null); return buildTree(allAreas, 0L); } private ListGreenAreaVO buildTree(ListGreenArea allAreas, Long parentId) { return allAreas.stream() .filter(area - area.getParentId().equals(parentId)) .map(area - { GreenAreaVO vo new GreenAreaVO(); BeanUtils.copyProperties(area, vo); vo.setChildren(buildTree(allAreas, area.getId())); return vo; }) .collect(Collectors.toList()); }前端拿到树形数据下拉选择框可以用el-cascader做层级选择方便用户把植物挂到具体的绿化带节点下。这个树形下拉是整块区域管理模块里最顺手的功能但注意如果区域层级太深比如超过 3 层级联选择器的展示会变挤建议限制最多三级我这边的数据中心根区域占一级校区一级区域一级绿化带一级正好承载四种层级再多就要换成分步选择的交互了。5.4 一个常见问题跨域配置前后端分离开发时前端跑在 8080 端口、后端跑在 8081 端口必然遇到跨域问题。后端统一配置跨域即可Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }同时后端还要处理一个问题如果配置了拦截器OPTIONS 预检请求会被拦截器拦住返回 401。所以拦截器里要放行 OPTIONS 请求if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }这个坑我排查了半小时才找到因为浏览器控制台只会显示 CORS error根本不会告诉你是拦截器拦掉的。只要加了拦截器的项目这句话就是保命句。6. 项目部署与演示从 Spring Boot jar 包到 Docker 容器毕设最终是要演示给评委看的。最尴尬的场面就是你辛辛苦苦开发完答辩当天才发现环境起不来。所以我强烈建议从开发第一天就把部署方式想好最好是 Docker 化部署换机器演示只拉镜像就能跑避免各种“在我电脑上是好的”的悲剧。6.1 打成 jar 包后端项目使用 Maven 打包mvn clean package -DskipTests打包成功后target目录下会出现一个xxx.jar。注意 Spring Boot 2.x 的 fat jar 是自包含的直接可运行java -jar campus-green-admin.jar默认端口是 8081我在application.yml里改了端口启动后访问http://localhost:8081可以看到健康检查接口。6.2 Dockerfile 编写与镜像构建在项目根目录创建DockerfileFROM openjdk:8-jre-alpine COPY target/campus-green-admin.jar app.jar EXPOSE 8081 ENTRYPOINT [java, -jar, /app.jar]构建并启动docker build -t campus-green-admin . docker run -d --name campus-green \ -p 8081:8081 \ --network campus-net \ -e DB_HOSTmysql-service \ -e DB_PORT3306 \ campus-green-admin这里DB_HOST是环境变量你在application.yml里可以用${DB_HOST:localhost}把它读出来。很多同学部署失败是因为数据库连接写的是localhost:3306但容器里的 localhost 指向容器自己根本找不到宿主机上的 MySQL。解决办法有两个用 Docker Compose 把 MySQL 和 Spring Boot 放同一个 network连 MySQL 服务名。如果 MySQL 在宿主机Spring Boot 容器的连接地址要写宿主机 IP比如host.docker.internalWindows/Mac 新版本 Docker 都支持。6.3 初始化数据的准备毕设演示前数据库里一定要有足够的数据。我准备了两个脚本schema.sql建表语句。data.sql基础数据包括管理员账号、几个测试用户、十几条区域和植物数据、几条养护记录。写data.sql的时候我踩了个坑因为有外键关联插入顺序不能乱。必须先插green_area再插plant然后插maintenance_record。如果顺序乱了MySQL 直接报外键约束失败。所以我习惯在 create 语句最后加DROP TABLE IF EXISTS开头每次初始化都是全新环境下执行避免之前残留数据干扰。6.4 演示时的临场准备最后分享一个答辩演示的小技巧把常用的功能入口整理成一个演示脚本比如“用管理员登录—查看首页统计—进入植物档案上传一棵树再删除—处理一条问题上报—查看月度台账报表”。按照这个脚本走完基本能够覆盖系统的所有核心亮点而且不会因为紧张而遗漏功能。不要等到上了讲台再想着“下一步点什么”人类的短期记忆在紧张状态下真的靠不住。再配一个备用的截图文件夹——万一到时网络出问题、Redis 没起来、或者 MinIO 连不上至少截图上还能看到完整效果。投影仪演示这种事多一手准备永远不会错。7. 论文与答辩的几个高频问题和一个反直觉经验到这里系统的开发部署已经讲完了但毕设还有一个大头论文和答辩。很多代码写得不错的同学偏偏挂在讲不明白上非常可惜。我把答辩里被问得最多的几个问题整理出来附带回答要点。7.1 “为什么用 Spring Boot 而不是 SSM”答Spring Boot 是基于 Spring 框架的快速开发脚手架它通过自动配置减少了大量 XML 配置内嵌 Tomcat 服务器可以直接以 jar 包方式运行。对于快速迭代、以业务逻辑为主的校园绿化管理系统来说Spring Boot 能让我把更多精力放在业务功能的实现上而不是花时间调整框架配置文件。同时 Spring Boot 生态成熟社区资料丰富遇到问题能找到大量解决方案。这个回答既讲了技术层面的“自动配置、内嵌容器”也讲了选择它的业务层面的理由。别只背概念要结合项目说。7.2 “JWT 相比传统 Session 有什么优点”答Session 方案需要服务端存储会话状态在前后端分离场景下跨域麻烦服务端水平扩展时 Session 同步也是问题。JWT 是无状态认证token 本身携带用户信息和过期时间服务端只需验证签名不用保存会话数据天然适合前后端分离架构。但 JWT 也有不足会话在过期前无法主动失效如果用户修改密码或封禁账号已签发的 token 仍然有效。因此在实际项目里可以通过维护黑名单或缩短 token 有效期来缓解。这个回答是有结构的两面分析法先讲优点再讲不足和解决方向。导师最喜欢这种“知其然且知其所以然”的答辩人。7.3 “你的项目有哪些不足如果继续做会怎么改进”这是答辩必问题。没经验的同学脱口就说“我的项目没什么不足”这反而是一个很大的减分项。我当时准备了两条真实不足没有做地图可视化目前树木位置是以文字描述的区域 备注存储如果接入 Leaflet 或 ECharts 配合 GIS 数据把每棵树落到地图坐标上会是更直观的 3D 绿化管理平台。后续可以引入地图插件把经纬度和区域绑定让管理员在地图上直接点击维护。权限控制粒度偏粗目前的角色是粗粒度控制的所有管理员拥有相同权限没有办法做到“某些管理员只能管理特定区域”。如果引入更细粒度的权限模型比如数据权限按区域划分会更好。实际上这就是 RBAC 模型天然的限制后续可以考虑按组织架构和数据范围再设一层权限过滤。这两条不但是“缺点”同时暗示了你对系统的思考深度和改进方向比任何“这套系统已经很完善了”的说辞都更有说服力。7.4 一个反直觉的毕设经验别把重点放在“别人没用过的技术”我见过有同学为了突出创新硬塞了 Elasticsearch 做全文检索、ActiveMQ 做消息队列、HanLP 做中文分词——做出来的东西看上去很炸裂但一问细节就露馅。我的经验正好相反有一个小而精的亮点就够了把核心业务做扎实然后把一两个亮点做透比如问题随手拍、MinIO 图片上传、周期养护任务生成、Redis 缓存验证码就已经超过很多“技术堆砌但从头到尾讲不清为什么”的项目了。技术选型的核心逻辑永远是“从业务场景出发”不是“从技术栈出发”。答辩老师最害怕的就是听到“因为我用了它所以它好”最想听到的是“因为这里有这个需求所以我选了它来解决这个问题”。把上面这些经验消化掉你不仅能把绿化管理系统这个题目做出来还能在答辩的时候说出“这个系统每个模块为什么这么设计”的底气。我做这个系统的过程里印象最深的反思是项目管理系统的价值从来不在技术本身而在于让使用者觉得“哦原来数字化之后这么方便”这种成就感比拿高分还实在。