SpringBoot+Vue城市垃圾分类管理系统开发实战教程

发布时间:2026/10/10 14:42:15
SpringBoot+Vue城市垃圾分类管理系统开发实战教程 前阵子帮一位同学调试基于SpringBootVue的城市垃圾分类管理系统这个项目算是典型Java全栈方向的中型练习项目后端SpringBootMyBatis数据库MySQL前端Vue全家桶还配了一套完整源码。很多人做类似题目时最容易卡住的不是需求而是从零到一落地过程中的各种小细节——表怎么建、登录鉴权怎么拦截、Vue的axios怎么统一管理、部署起来总报错。这篇我把这类系统从需求边界、数据库设计到前后端实现、部署排查的整条链路完整过一遍给准备做毕业设计或练手项目的读者一条可以直接复用的路。说明文中的所有工程、案例均为模拟项目涉及的人物均为虚构代称。代码片段基于Spring Boot 2.7.x和Vue 2.x/Element UI这套组合在同类项目里最稳定教程也最多。1. 先搞清楚这个系统到底要管什么需求边界与功能模块拆解做管理系统之前最忌讳上来就建表写代码。城市垃圾分类管理系统听起来就是“登录、增删改查、统计”三件事但一旦拆开不同角色看到的功能差异非常大。我一般先画业务边界再列功能清单最后才开始设计数据库。1.1 两类角色普通居民与系统管理员用户侧居民端要尽量轻核心场景是“我不知道这个东西算什么垃圾去查一下”。所以居民端必须有一个好用的垃圾条目查询支持按名称搜索、按四分类浏览其次是要能提交投放预约或回收记录比如大件垃圾预约上门再次是积分体系查询积分明细、兑换科普小奖励。积分不是必选但很多评分标准里它属于加分功能而且能串起整条业务链。管理端则完全相反那是一个标准的中后台垃圾类别管理可回收物、有害垃圾、厨余垃圾、其他垃圾、垃圾条目管理给每一条垃圾写分类说明和处理提示、公告管理、居民投放记录的审核与状态流转、积分发放与兑换记录查询以及按周/月/类别维度的统计报表。这几块做出来系统从功能上就完整了。值得注意的是真实项目里“管理端”往往还有二级划分比如超级管理员和普通审核员但作为课程设计或毕业设计两类角色够用了。角色的区分在数据库里就是user表的一个role字段不用单独建角色表。1.2 核心业务流程从居民查询到积分发放的闭环我把系统的核心业务流梳理成一条主链居民登录/注册 → 查询垃圾类别或条目 → 提交投放记录预约大件垃圾回收 → 管理员审核 → 审核通过后自动发放积分 → 居民查看积分明细并兑换奖励。加入积分闭环后这个系统就不是单纯的增删改查了它有了“状态”的概念投放记录从“待审核”到“已通过”或“已驳回”可以由状态字段驱动。这里有两个关键点必须提前想清楚第一个是状态字段的设计。我用status字段0待审核1已通过2已驳回同时配create_time和audit_time审核时间在管理员操作时更新。不要用字符串存状态用tinyint最方便后续统计SQL也简单。第二个是积分发放的幂等控制。审核接口如果被前端重复点击或者后端没有做校验会出现一次审核发两次积分。所以发放积分的动作必须和状态更新放在同一事务里并且通过UPDATE语句的条件判断来兜底比如UPDATE disposal_record SET status1 WHERE id#{id} AND status0影响行数为0说明已经审核过不能再发积分。1.3 模块与页面映射建功能清单表为了避免后面开发时想到哪写哪我在开工前会把功能模块和页面路径列成一张映射表这也是给评分老师看的很好用的文档素材。模块角色核心功能前端路由建议后端接口前缀登录注册居民/管理员用户名密码登录、JWT签发/login/api/auth垃圾查询居民按名称搜索、按四分类浏览/query/api/garbage投放记录居民提交预约、查看个人记录/records/api/record积分中心居民积分明细、兑换记录/points/api/points基础管理管理员类别维护、条目维护/admin/category/api/admin/garbage审核管理管理员投放记录审核、积分发放/admin/audit/api/admin/record统计报表管理员四分类占比、提交趋势/admin/statistics/api/admin/stats这样一列开发顺序立刻就出来了先做认证再做居民端查询再做管理端审核最后做统计。前后端联调时也可以按这个顺序走避免同时铺开一堆接口导致问题不好定位。2. 技术栈选型和数据库设计为什么是SpringBootVueMyBatis表要怎么建2.1 选型逻辑不是赶时髦是匹配业务规模SpringBoot是Java社区做Web服务最主流的框架内嵌Tomcat自动配置省掉大量XML非常适合中小型管理系统。MyBatis的优势是SQL由开发者自己控制特别适合这类需要条件查询、统计报表、复杂SQL的教务/环保/政务系统调试SQL非常直观。Vue配合Element UI做中后台页面效率极高组件现成前后端分离也让前后端可以并行开工。MySQL不必多说免费、稳定、生态好对于这种百人规模的模拟系统完全没有性能压力。这套组合的唯一缺点是工程结构稍显传统但作为课程设计和毕业设计恰恰是老师最熟悉的答辩时不容易被刁钻问题难住。2.2 数据库设计六张核心表搞定全模块数据库是翻车率最高的地方。我见过有人建二十多张表结果字段逻辑混乱也有人只建两张表后面统计接口根本写不下去。合理的做法是先建六张核心表覆盖主体功能后续按需再加表。第一张是用户表user字段包括id、username、password、role、phone、points、create_time。role用数值区分1为管理员2为普通居民。points存积分总额方便展示不需要实时SUM。第二张是垃圾类别表garbage_categoryid、name、code、description。code用字符串比如recyclable、harmful、kitchen、other。注意“厨余垃圾”有些地方也写“湿垃圾”具体命名按你所在城市的分类标准来。第三张是垃圾条目表garbage_itemid、category_id、name、description、tips。category_id逻辑关联类别表不用物理外键省得增删数据时一堆外键约束。垃圾查询主要就是查这张表。第四张是投放记录表disposal_recordid、user_id、garbage_item_id、record_type、status、create_time、audit_time。record_type记录是普通查询还是预约回收status的含义上面说过。第五张是积分明细表points_recordid、user_id、change_points、type、description。type取值例如1审核通过增加、2兑换减少。积分总额和明细分开存是标准做法避免每次都汇总明细表。第六张是公告表announcementid、title、content、create_time。面向居民端展示简单直接。核心建表SQL节选最关键的三张表CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt加密存储, role tinyint(4) DEFAULT 2 COMMENT 1管理员 2居民, points int(11) DEFAULT 0, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE garbage_category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, code varchar(20) NOT NULL, description varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE garbage_item ( id int(11) NOT NULL AUTO_INCREMENT, category_id int(11) NOT NULL, name varchar(100) NOT NULL, description varchar(500) DEFAULT NULL, tips varchar(500) DEFAULT NULL, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个大多数人不会在意但很重要的细节用户名一定要加密存储。不要用明文密码推荐Spring Security自带的BCryptPasswordEncoder。生成验证码时也不要存明文否则数据库一旦泄露所有账号都裸奔这在答辩时也是一个被追问的高频点。3. 后端从0到1认证、业务接口和MyBatis映射的落地细节3.1 基于JWT的登录鉴权怎么落地这类前后端分离系统登录态通常用JWT。思路是用户登录成功后后端生成一个token返回前端在请求头里带Authorization: Bearer token后端通过拦截器解析token得到userId和role。用JWT不用Session主要是无状态、天然适配前后端分离。我用jjwt来实现先封装一个JwtUtil工具类核心方法如下public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 2; public static String generateToken(Integer userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }然后是拦截器。注册WebMvcConfigurer时添加拦截器放行登录注册接口其他接口全部拦截public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(HttpMethod.OPTIONS.name())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }开发环境调试时CORS跨域配置也很关键。Vue跑在8080端口后端跑在8081端口如果不配跨域浏览器会直接拦截响应。我是这样处理的在配置类里实现WebMvcConfigurer的addCorsMappings方法允许所有来源和常用请求头。3.2 垃圾分类查询与投放记录接口的实现垃圾分类查询是居民端点击率最高的接口要求支持模糊搜索。搜索时用户可能输“塑料瓶”也可能输“瓶子”不可能每次命中完全匹配所以SQL采用LIKE匹配名称和描述。这里要小心一件事MyBatis的#{}会自动转义用户输入防SQL注入但如果直接在XML里写LIKE %${name}%就会存在注入风险且无法预编译。推荐用concat函数select idsearchItems resultMapgarbageItemMap SELECT gi.*, gc.name AS category_name FROM garbage_item gi LEFT JOIN garbage_category gc ON gi.category_id gc.id where if testkeyword ! null and keyword ! AND (gi.name LIKE CONCAT(%, #{keyword}, %) OR gi.description LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND gi.category_id #{categoryId} /if /where ORDER BY gi.id DESC /select投放记录提交接口要注意防止重复提交。前端通过loading按钮只能挡掉一部分操作后端还必须做校验比如同一个用户对同一个垃圾条目在当天已经提交过待审核记录就直接拒绝。这个判断用一个count查询就能实现。提交成功后记录初始状态为0待审核。审核接口才需要事务因为“更新状态”和“新增积分明细”必须一起成功或一起失败。我在Service方法上加Transactional先执行UPDATE再用int类型接收影响行数如果返回0说明记录状态已经被改过主动抛出业务异常保证不会重复发放积分。3.3 MyBatis动态SQL与分页的常见坑动态SQL是MyBatis的灵魂但也是最容易掉坑的地方。第一坑是where标签搭配if时很多初学者会在每个if里加AND第一行写成AND name #{name}如果前面没有条件生成的SQL就是WHERE AND name ...直接语法错误。用了where标签后MyBatis会自动去掉多余的AND/OR所以放心地把AND写在每个if开头即可但如果你手写WHERE 11别忘了去掉多余AND。第二坑是PageHelper分页插件。它的原理是在当前线程上下文中拦截下一条查询如果你在PageHelper.startPage()和查询之间又执行了其他查询语句比如先查一次用户信息再查列表第一次查询会被分页插件误处理导致结果错乱。正确写法是PageHelper.startPage(pageNum, pageSize); ListDisposalRecordVO list disposalRecordMapper.selectRecordPage(condition); PageInfoDisposalRecordVO pageInfo new PageInfo(list);在controller里返回pageInfo.getTotal()作为总条数pageInfo.getList()作为当前页数据。注意PageHelper只对紧跟着的第一个查询生效所以一定不要在中间插入其他数据库查询。4. 前端Vue项目页面结构、状态管理和对接口的几种姿势4.1 页面路由与组件划分前端我用Vue 2 Element UI的组合。工程创建好之后最先规划路由。我建议居民端和管理端用不同的layout管理端有侧边栏菜单居民端只有顶部导航这样代码结构清晰。居民端路由登录页/login首页/home垃圾查询/query我的记录/records积分中心/points。管理端路由垃圾类别管理/admin/category垃圾条目管理/admin/item投放审核/admin/audit统计报表/admin/stats。所有管理端路由放在同一个父路由下并设置一个前置守卫判断当前用户的role是否等于1不是就跳回首页。这个导航守卫写起来很简单router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); } else { next(); } });不要在导航守卫里只判断token因为token只是登录凭证角色判断要在进入每个管理端页面时再校验一次。比较偷懒但也有效的做法是在后端管理接口的拦截器里校验role前端即使误入也无权访问数据。4.2 axios封装与token携带每来一个请求都要手动加请求头工作量大且容易漏。正确做法是统一封装一个request模块。我通常会建src/utils/request.jsimport axios from axios; import { Message } from element-ui; import router from /router; const request axios.create({ baseURL: process.env.VUE_APP_BASE_URL || http://localhost:8081/api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); Message.error(登录已过期请重新登录); } else { Message.error(error.response?.data?.message || 请求失败); } return Promise.reject(error); } ); export default request;这里有一个容易被忽略的点response拦截器返回response.data后所有API调用拿到的就是后端返回的数据体不用再在业务代码里反复res.data.data。后端我统一封装了返回结构{code, message, data}前端这边直接解构data字段出来。开发环境跨域问题推荐用proxy代理解决在vue.config.js里配置devServer的proxy把/api代理到后端地址这样浏览器请求的是同源地址不会有跨域报错比后端开启cors配置更干净。4.3 管理端表格与统计图表的实现要点管理端表格是Element UI的绝活。后端分页接口返回{list, total, pageNum, pageSize}前端在el-table中用el-pagination绑定几个属性即可。注意翻页时必须把搜索表单中的keyword、status一并传给后端否则会出现在第二页搜索条件丢失的问题。我通常把搜索表单放一个对象里分页事件里把整个对象序列化传给后端。统计图表推荐ECharts。比如要画“四分类提交记录占比饼图”后端只需要返回一个原始数据组[ {name: 可回收物, value: 120}, {name: 有害垃圾, value: 8}, {name: 厨余垃圾, value: 46}, {name: 其他垃圾, value: 33} ]前端直接chart.setOption({ series: [{ type: pie, data: res.data }] });不用让后端拼好ECharts的完整option因为后端返回原始数据最通用前端可以随意换图表类型以后加需求也不用改后端接口。这个设计思路很多人一开始想不明白但做两三个项目后就会发现后端只需要提供结构化数据前端负责可视化是效率最高的分工。5. 从源码到运行一键部署完整步骤与常见报错排查5.1 环境配置清单版本不匹配是最隐形的拦路虎拿到一套源码先别双击启动先对照环境清单检查。最小可行环境如下组件推荐版本说明JDK1.8多数毕设源码基于JDK8Spring Boot 2.7完全支持Maven3.6.x3.9也能用但仓库源可能要配国内镜像MySQL5.7或8.0注意8.0连接驱动名称与5.7略有差异Node.js14.17Vue 2 Vue CLI 4建议Node 12~16IDEA2023或2024社区版即可支持Maven最容易翻车的组合是用了Spring Boot 3.x源码却装了JDK8或前端项目用Vite构建需要Node 18而你用的是Node 14。解决办法是在动手前看pom.xml里的 和package.json里的scripts确认构建工具版本。5.2 导入源码、建库、启动的详细步骤第一步新建数据库在MySQL里执行CREATE DATABASE garbage CHARACTER SET utf8mb4;然后选择该数据库导入源码中提供的garbage.sql。如果源码里没有sql文件这基本属于残缺源码建议直接换一份。第二步修改后端配置文件。找到application.yml把数据库地址、用户名、密码改为你自己的spring: datasource: url: jdbc:mysql://localhost:3306/garbage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your-password第三步启动后端。用IDEA以Maven项目导入后端文件夹等依赖下载完运行主类。如果不配置镜像第一次下载依赖可能很慢建议在Maven的settings.xml里配置阿里云镜像但这一步属于基础操作不展开。第四步启动前端。进入vue目录依次执行npm install npm run serve如果npm install报错优先查看有没有package-lock.json有就用npm ci代替。启动成功后访问前端地址用管理员预置账号登录。5.3 部署后必看的几个报错与解决办法我总结了一份高频报错对照表都是帮朋友调试时实际遇见过的报错现象可能原因排查与解决Access denied for user rootlocalhost数据库密码错误检查application.yml中username/passwordjava.sql.SQLSyntaxErrorException: Unknown column表和源码版本不匹配重新执行最新sql脚本或看有没有增量更新sqlPort 8080 was already in use8080端口被占用改application.yml中server.port或kill占用进程Failed to load resource: net::ERR_CONNECTION_REFUSED后端没启动或端口不一致确认后端启动确认前端baseURL/proxy指向后端真实端口Uncaught (in promise) invalid tokentoken过期或伪造清localStorage重新登录npm ERR! Missing script: serve项目可能用Vite或工程不完整查看package.jsonscripts定义改用npm run dev这些坑绝大多数不是代码逻辑问题而是环境一致性问题。所以我的习惯是运行前先检查后端端口、前端代理端口、数据库密码三处能省下至少一小时。6. 这个项目还能怎么改可扩展方向与实际开发建议一套基础源码跑通之后很多人会问“这样够了吗”。我的答案是作为课程设计或毕业设计的基础版本够但如果你想让它从“普通管理系统”变成“有亮点的作品”可以在下面几个方向里挑一两个加进去。第一个方向是缓存优化。垃圾分类查询是高频只读操作尤其是热门条目可以引入Redis做一级缓存。每次查询先查Redis没有再查MySQL并回写。这个改动量不大却能在论文里加上“系统性能优化”章节。第二个方向是大屏可视化。很多城市都在推垃圾分类示范小区如果能做一个数据大屏首页在地图上标记投放点用ECharts展示各街道分类趋势视觉效果会非常加分。前端工作量会变大但技术难度不太高适合想在答辩时抓眼球的人。第三个方向是小程序端。把居民端查询和预约提交移植到微信小程序后端接口不变只需要新写一个前端。这也会明显增加工作量建议量力而行至少要留足两周。我个人的开发和调试体会是这类项目最花时间的从来不是CRUD功能而是前后端联调时接口字段名对不上、状态码不统一、跨域拦截、token过期这类琐碎问题。如果后续有人给你一份所谓“完整源码”别急着跑先确认三件事——有没有数据库脚本、后端入口类是否明确、前端代理或baseURL配的是什么。这三件事确认完百分之八十的启动问题都提前消掉了。最后再分享一个小技巧在所有Json响应里统一返回一个包装结构比如{code:200, message:ok, data:xxx}前端axios拦截器只处理code不要直接把整个HttpResponse返回给页面。这能让前后端协作效率提高很多。这个项目我前后迭代过三个版本第一版接口返回乱七八糟第二版统一了后端结构第三版加了积分闭环越到后期越觉得前期把规范和约束定好比多加十个接口都值。照着上面这条路走一遍你拿到源码后从导入到跑通也就一个下午的事。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询