简介:基于Spring Boot与Vue的垃圾分类管理系统,是一套面向高校课程设计与期末大作业的完整前后端分离项目。系统覆盖垃圾类别管理、投放记录、积分规则等典型业务模块,后端采用Java与Spring Boot提供接口,前端由Vue搭建交互页面,适合具备Java或Web基础的学生直接参考运行。压缩包共104个文件,以36个Java源文件、24个Vue组件、11个JavaScript脚本为主,另含SQL数据库脚本、XML配置与项目说明等,总量仅5.44MB,内容组织清晰,便于快速导入IDE并还原环境。目前已有3034人学习下载,代码完整可用,数据库脚本已包含,基本无需额外配置即可启动演示。对于需要短期内完成高质量大作业或期末设计的读者,这套项目提供了可复用的分层架构和基础功能实现,降低从零开发成本,也便于在原有代码上扩展个性化功能。
1. 垃圾分类管理系统:为什么SpringBoot+Vue能撑起95分的大作业
如果你正在为课程设计找题目,或者已经拿到了一份“基于springboot+vue的垃圾分类管理系统源码+数据库”,大概率是被这个组合吸引的。SpringBoot负责后端接口和数据读写,Vue负责页面交互,还带一个MySQL数据库脚本,这是一套非常标准的前后端分离结构。老师看一个课设,无非看三点:功能能不能跑通、数据库设计是不是认真、代码结构是不是能看懂。垃圾分类管理系统恰好能把这三样都占齐,而且业务不复杂,适合作为模板去二次开发。
很多学生拿到源码后反而不知道从哪入手,要么卡在环境上,要么改数据改出问题,最后只能交一个没跑起来的半成品。这篇笔记会按我实际带过的课设经验,把数据库设计、后端接口、前端对接和部署踩坑都拆开讲。目标很直接:让你拿到这份材料后,能自己复现、能讲清楚原理、能在答辩时说出每个模块为什么这么做,配得上95分这个档位。
2. 系统需求与数据库设计:先定表和关系,再碰代码
2.1 角色与功能边界:普通用户和管理员到底能做什么
大部分垃圾分类管理系统的评分核心是登录注册、垃圾信息检索、分类展示、留言反馈和后台管理。普通用户不需要设计复杂权限,管理员能进入后台做增删改查即可。这里一定要把用户、垃圾条目、垃圾类别、留言公告的关系画清楚,否则后期写SQL和前端页面会反复改。
建议把功能模块拆成四个:用户认证模块负责注册、登录、token校验;垃圾检索模块负责按名称或类别查询;内容管理模块负责管理员维护垃圾词库和公告;互动模块负责用户提交留言。这个边界划清楚之后,SpringBoot的Controller和Service就能按模块组织,不会都堆在一个类里。
2.2 数据库表结构:六张表就能覆盖全部功能
我见过很多课设把一张垃圾表放进几十个字段,结果既难维护又难讲。合理的做法是拆成多张表。以最常见的“垃圾分类管理系统”为例,至少需要:用户表、垃圾类别表、垃圾名称表、留言表、公告表,再加上一个简单的角色表或用户角色字段。前四张是关键,公告表用来支撑首页展示,角色直接在用户表里用integer区分。
下面是一份可以直接导入MySQL的建表脚本,注意字符集和自增主键的写法:
CREATE DATABASE IF NOT EXISTS garbage_sort DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE garbage_sort; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(30) NOT NULL, `password` VARCHAR(64) NOT NULL, `nickname` VARCHAR(30) DEFAULT '用户', `role` TINYINT NOT NULL DEFAULT 1 COMMENT '1普通用户 2管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `category` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) NOT NULL, `description` VARCHAR(200) DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `garbage` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `category_id` INT NOT NULL, `sort_code` VARCHAR(10) DEFAULT '' COMMENT '如 可回收物/有害垃圾', PRIMARY KEY (`id`), KEY `idx_name` (`name`), CONSTRAINT `fk_garbage_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `message` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `content` VARCHAR(500) NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表有几个地方容易被扣分:第一,用户名必须唯一,所以用了UNIQUE KEY;第二,垃圾名称的查询频率最高,所以给name加了普通索引,查询“电池”这种关键词时不会全表扫描;第三,外键显式声明了,这样在ER图上能画出关系,答辩时可以说数据引用完整性由数据库层面保证。
2.3 字段类型选型:为什么用TINYINT不用BOOLEAN
许多新手会把表示开关或角色的字段设计成BOOLEAN,但在MySQL里布尔值本身就是TINYINT(1)的别名,可读性反而模糊。上面用的是TINYINT加注释,角色为1或2,状态为0或1,这样在Java里映射为Integer完全没问题。密码字段用VARCHAR(64)是为MD5或BCrypt摘要预留长度,明文密码存32位也不够看。
还要注意时间字段统一用DATETIME,别有的用DATE有的用TIMESTAMP,容易在Jackson序列化时出现格式混乱。数据库同步、连接池之类的问题这里先不提,等部署章再展开。表结构定了之后,后端代码里的实体类和Mapper层基本就是体力活。
3. SpringBoot后端实现:登录鉴权与垃圾识别接口的三个关键段
3.1 项目结构和依赖:先把JWT和MyBatis Plus配好
拿到源码先看pom.xml,这是整个后端能不能跑的地基。一个能拿高分的大作业,依赖应该精简。常见做法是用SpringBoot 2.7.x搭配MyBatis Plus 3.5.x,不用写繁琐的XML Mapper,再加一个jjwt做登录令牌,一个druid或HikariCP作为数据库连接池。下面是我平时配置的依赖片段:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.14</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies>这里最关键的是MyBatis Plus版本要和SpringBoot版本兼容。用3.5.3以上时,分页插件和自动填充都能正常工作。数据库连接池我建议直接使用SpringBoot默认的HikariCP,不需要额外依赖,它能够处理大部分场景,后面避坑章会说调哪些参数。
3.2 登录鉴权:用JWT而不是用Session
课设里最常见的一个扣分点是没有登录拦截,任何人访问后台地址都能看到管理页面。解决办法是登录成功后签发JWT,前端保存token,每次请求在请求头里带上。这里给出一个简化但完整的JWT工具类:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; // 单位毫秒 public String generateToken(String username, Integer role) { Date now = new Date(); Date expireDate = new Date(now.getTime() + expire); return Jwts.builder() .setSubject(username) .claim("role", role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }逻辑说明:@Value读取application.yml中配置的jwt.secret和jwt.expire,secret建议是32位以上的随机字符串,expire我一般设成7200000,也就是两小时。token里只放用户名和角色,不放密码。解析时如果token过期或签名错误,parseToken会抛异常,这个异常要交给全局异常处理器,而不是直接返回500。
登录接口本身很简单,查出用户后比较MD5或BCrypt后的密码。这里要提醒:入库的密码必须是加密后的,绝对不能明文。课设里用BCrypt比MD5更安全,但如果你不熟悉,至少用MD5加盐。答辩时老师问“密码能不能被反查”,你能说出加盐就能过关。
3.3 垃圾识别接口:模糊查询和参数校验
垃圾识别是系统的核心功能,用户输入“塑料袋”或“果皮”,返回对应分类和投放建议。实现上就是一个GET接口,接收name参数,调用MyBatis Plus的LambdaQueryWrapper做模糊匹配。下面是一个Controller和Service的组合写法:
@RestController @RequestMapping("/api/garbage") public class GarbageController { @Autowired private GarbageService garbageService; @GetMapping("/search") public Result search(@RequestParam String keyword) { if (keyword == null || keyword.trim().isEmpty()) { return Result.fail("关键词不能为空"); } List<GarbageVO> list = garbageService.searchList(keyword); return Result.ok(list); } }@Service public class GarbageServiceImpl extends ServiceImpl<GarbageMapper, Garbage> implements GarbageService { @Override public List<GarbageVO> searchList(String keyword) { return baseMapper.selectGarbageWithCategory(keyword); } }参数说明:Controller层只做参数检查和接口返回,真正的SQL在Mapper里。我在这套项目里偏好写一条连表SQL,而不是内存中判断,这样MySQL的索引才能用上。SQL大致是查询garbage表和category表,LIKE条件使用参数绑定#{keyword}防止SQL注入。keyword的长度也需要限制,在Controller里可以用@Size(max = 30),防止用户传入超长字符串拖慢查询。
3.4 统一返回结构和全局异常:让前端不猜后端
很多源码里接口有人返回Map,有人返回JSONObject,前端解析时常常拿到不确定结构。高分项目的大作业,应该有一个统一的Result类,里面至少包含code、message、data三个字段。前端只需要判断code是否为200。配合@RestControllerAdvice,业务异常可以抛成RuntimeException,全局捕获后返回格式统一的错误信息。
@Data public class Result { private Integer code; private String message; private Object data; public static Result ok(Object data) { Result r = new Result(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } }这样做的好处是,前端每一个axios回调都不需要单独处理404、500,只要看code就行。全局异常处理时,注意自定义业务异常和系统异常分开打印日志。这个细节在答辩时讲出来,会显得你考虑过生产环境,而不是只会CRUD。
4. Vue前端实现:从路由守卫到垃圾分类页面的完整对接
4.1 路由与状态管理:token放在哪里,路由参数怎么传
前端拿到源码后的第一件事,是把项目跑起来看效果。Vue项目一般基于Vue CLI或Vite创建。路由设计要划分用户页面和管理员页面,拦截器判断token和用户角色。下面的路由配置使用了Vue Router的参数传递:搜索页接收分类id作为query,用户从首页点击“可回收物”进入列表时,URL会变成/list?categoryId=1。
const router = new VueRouter({ mode: 'history', routes: [ { path: '/login', component: Login, meta: { public: true } }, { path: '/search', component: Search }, { path: '/category/:id', component: CategoryDetail, props: true }, { path: '/admin', component: AdminLayout, meta: { role: 2 } } ] }); router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (!to.meta.public && !token) { next('/login'); } else if (to.meta.role && parseInt(localStorage.getItem('role')) !== to.meta.role) { next('/'); } else { next(); } });路由参数有两种传法:路径参数/category/:id适合详情页,用this.$route.params.id读取;查询参数/list?categoryId=1适合筛选页,用this.$route.query.categoryId读取。很多新手会混淆,导致刷新后参数丢失。这里要注意:如果使用history模式,在部署到服务器时需要配置前端路由重定向到index.html,否则刷新就404。
4.2 登录页与axios拦截器:每次请求自动带上token
登录页通常是最先写的页面,但也是最容易漏掉体验细节的页面。用户登录成功后立即把token和role写入localStorage,同时设置axios默认请求头。为了避免每个组件里都写Authorization: Bearer xxx,用一个拦截器统一处理:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = 'Bearer ' + token; } return config; }); axios.interceptors.response.use( response => { if (response.data.code === 200) return response.data; else return Promise.reject(new Error(response.data.message || '请求失败')); }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } return Promise.reject(error); } );这段代码的逻辑很清晰:请求发出前检查本地token,响应回来后统一判断业务code。特别要注意401状态码的处理,很多大作业只处理了登录,没处理token过期,导致用户停留页面久了之后点击无反应。配上拦截器后,token失效会自动踢回登录页,这个细节能在演示时加分。
4.3 垃圾分类查询页面:双向绑定和防抖
查询页面由一个输入框和列表组成。用户输入关键词后,调用/api/garbage/search接口,把结果显示在卡片里。这里要防止每次键盘事件都发起请求,简单方案是监听输入值,超过一定长度后只发最后一次请求。
<template> <div class="search-container"> <input v-model="keyword" placeholder="请输入垃圾名称,如电池" /> <button @click="doSearch">查询</button> <ul> <li v-for="item in garbageList" :key="item.id"> {{ item.name }}:{{ item.categoryName }} - {{ item.sortCode }} </li> </ul> </div> </template> <script> export default { data() { return { keyword: '', garbageList: [] }; }, methods: { async doSearch() { if (!this.keyword.trim()) { this.garbageList = []; return; } const data = await this.$axios.get('/api/garbage/search', { params: { keyword: this.keyword.trim() } }); this.garbageList = data.data || []; } } }; </script>参数说明:v-model负责输入框和keyword的双向绑定,params属性里的keyword最终会拼接到URL上成为?keyword=xxx。这里故意只用简单的doSearch,是因为课设的演示环境不需要实时自动搜索。如果你想增加亮点,可以加300毫秒防抖,减少无效请求。
4.4 管理员页面:表格、弹窗和表单校验
管理员后台通常包含垃圾分类条目的增删改查,用Element UI或Ant Design Vue能大幅提高效率。需要注意的是,表格里每行要有唯一业务主键,也就是后台返回的id,不要将索引当作key。新增和编辑使用同一个弹窗组件,提交前用rules校验垃圾名称是否为空,分类下拉框的选项从/api/category/list接口加载。这部分代码量较大,但思路一致,把增删改查四个方法对上后端CRUD,就不会有问题。
5. 部署与踩坑:环境、打包、数据库连接的五处翻车现场
5.1 JDK版本与SpringBoot版本冲突,启动就报UnsupportedClassVersionError
现象:使用IDEA启动后端时,控制台立刻报错,提示UnsupportedClassVersionError,后面跟着一串类名。
原因:SpringBoot 2.7默认编译基于Java 8,如果本机JDK是17或更高版本,某些旧版依赖会不识别class文件版本。反过来,如果源码是用SpringBoot 3.x写的,JDK 8又跑不起来。
解决:检查pom.xml中<java.version>标签。如果源码没写,统一安装JDK 8或11,并确认IDEA的Project Structure和Maven的JDK都指向同一版本。我一般会直接在环境变量里配JAVA_HOME,避免Maven和IDEA各自读取到不同版本。这一步能解决90%的SpringBoot启动问题。
5.2 MySQL连接报错:Cannot create PoolableConnectionFactory
现象:后端启动时提示数据库连接失败,具体信息里包含Public Key Retrieval is not allowed或者The server time zone value。
原因:MySQL 8.X的默认认证插件是caching_sha2_password,而连接串里没有允许获取公钥;同时时区没有设置,JDBC驱动无法解析。
解决:修改application.yml中的数据库URL,加上完整的参数:
spring: datasource: url: jdbc:mysql://localhost:3306/garbage_sort?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: your_password参数说明:characterEncoding=utf8保证中文不乱码,serverTimezone必须和本机时区一致,allowPublicKeyRetrieval=true是MySQL 8的坑点,漏掉它就报上面的错。useSSL=false在本地开发时能避免一堆证书警告。
5.3 前端请求跨域,浏览器拦截返回200但页面拿不到数据
现象:Vue页面能打开,但登录时控制台报错Access-Control-Allow-Origin。后端日志显示有请求记录,但浏览器把响应拦截了。
原因:开发环境下前端运行在Vue的8080端口,后端运行在SpringBoot的9090端口,两者端口不同,浏览器遵循同源策略拒绝读取。
解决:最简单的方式是给后端Controller加上@CrossOrigin,或者在SpringBoot中配置一个CorsFilter。更优雅的是使用Vue CLI的代理:在vue.config.js中添加/api的target和changeOrigin:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } };逻辑说明:前端请求/api/xxx时,Vue开发服务器会把请求转发到http://localhost:9090。这样浏览器看到的请求是同源的,就不会触发跨域。注意代理只在开发环境生效,生产部署时需要通过Nginx反向代理或用同端口启动前后端。
5.4 Vue打包后空白或404,资源路径和history配置问题
现象:执行npm run build后,把dist目录丢到服务器,访问index.html页面空白,控制台显示JS文件404。
原因:Vue默认构建时资源路径是绝对路径/js/app.js,如果部署在子目录下,比如http://域名/myproject/,路径就会失效。另外history模式下的路由需要服务器把所有请求重定向到index.html。
解决:在vue.config.js中设置publicPath: './',让打包后的资源使用相对路径。如果使用history路由,需要在Nginx或后端服务器配置try_files规则。
5.5 数据库连接池参数不合理,空闲后首次请求超时
现象:系统每隔一段时间不用,再访问时第一个接口卡住几秒,有时直接报连接超时。
原因:MySQL默认的wait_timeout是8小时,连接池中的连接被MySQL关闭了,但业务侧不知道,拿着死连接去请求。
解决:在application.yml中调整HikariCP的配置项。
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 300000 connection-timeout: 30000 max-lifetime: 2800000参数说明:max-lifetime一定不能大于MySQL的wait_timeout,一般设为2800000毫秒,也就是47分钟;idle-timeout要小于max-lifetime。这套组合能保证连接在被MySQL回收之前就会被连接池主动换掉。答辩时把这一段讲出来,就能证明你遇到过真实运维问题。
6. 从95分到更高分:答辩验证方法、扩展点与参数调优技巧
6.1 答辩前自测:用curl和Postman验证关键接口
演示不应只靠浏览器。我习惯在答辩前一晚用curl跑一遍核心接口,确认登录、搜索和管理员接口返回的code都是200。下面是一组最常用的验证命令:
# 登录,获取token curl -X POST http://localhost:9090/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 携带token查询垃圾分类 curl -X GET "http://localhost:9090/api/garbage/search?keyword=电池" \ -H "Authorization: Bearer <上面返回的token>" # 查看管理员列表 curl -X GET http://localhost:9090/api/admin/garbage/list \ -H "Authorization: Bearer <上面返回的token>"验证时要注意三点:第一,登录接口返回的JSON里如果包含token字段,提取时不要复制到多余空格;第二,搜索接口返回的category是否是中文名称;第三,管理员接口如果用空格token调用,应返回401而不是500。这样答辩时一旦老师提问“权限是怎么控制的”,你可以现场展示拦截器的效果。
6.2 低成本的三个扩展点:图表、导出、Redis缓存
如果你的大作业已经能运行,最高的95分属于谁,往往取决于有没有额外亮点。但我不建议一上来就上Redis和分布式,那种架构放在课设里反而显得突兀。最容易在后期加分的是给首页加垃圾分类统计图,使用ECharts按类别展示垃圾数量;给管理员加一个导出Excel按钮,用Apache POI生成报表;给热点垃圾名称加本地缓存,减少重复查询。
这三个扩展里,ECharts对Vue的适配最友好,只改前端不动后端,两个小时能做完。导出Excel需要加一个POI依赖,后端返回response流时注意设置Content-Disposition文件名编码。缓存方案最省事的是SpringBoot自带的@Cacheable,可以在Service方法上加注解,但注意更新垃圾条目时要记得@CacheEvict,否则数据变了缓存还在。
6.3 答辩时最该讲清楚的三个内部设计
除了演示外,老师通常会抽查几个设计决策。首先是为什么用JWT而不用Session,答“JWT是无状态的,适合前后端分离,服务端不用存session”就够。其次是垃圾名称的模糊搜索性能,你可以说在name字段上加了索引,讲出这个点会显得有自己的思考。最后是密码安全,哪怕项目中其实用的是MD5,你也要说明“项目中密码经过哈希处理,生产环境建议使用BCrypt”。这些回答不需要多长,重点是逻辑自洽。
我在带学生做课设时最深的感受是,很多人死磕代码但不讲功能,答辩时一紧张就把项目讲成了流水账。其实只要把上面三个问题准备好,老师的印象分就会上去。数据库脚本导入、IDEA配置、Maven依赖下载这些基础问题,反而比业务逻辑更容易拖垮你。另外提醒一句:拿到任何源码后,先别着急改功能,先跑通原版再改,否则你都不知道问题出在前端还是后端。希望这篇笔记能帮你在课程设计这条路上少踩点坑,让那份95分的项目真正成为你自己的东西。
本文还有配套的精品资源,点击获取