SpringBoot + Vue 社区智能垃圾管理系统毕业设计全解析
简介这是一个基于SpringBoot与Vue前后端分离的社区智能垃圾管理系统完整源码项目适合Java后端与前端开发者用于毕业设计、课程设计或工程实训。项目包含可运行源码、SQL脚本与项目文档覆盖垃圾投放、分类管理、社区信息等业务模块可二次开发或直接作为初期项目基础。压缩包共797个文件以java、vue、js、xml、jpg等为主含186个Java后端类与127个Vue前端组件辅以配置文件、数据库脚本及项目构建脚本包体约40.02MB结构清晰便于导入IDE调试验证。目前已有98人学习下载代码经过调试支持有偿部署遇到问题可与博主沟通。资源整体具备较高借鉴价值适合初学者进阶或作为正式项目参考。1. 社区智能垃圾管理系统SpringBoot Vue 能做出一套什么毕业设计SpringBoot Vue 双端齐全的社区智能垃圾管理系统源码后端是 SpringBoot MyBatis-Plus MySQL前端是 Vue Element UI业务上把「垃圾分类 → 回收下单 → 积分奖励 → 积分兑换 → 清理工单 → 投诉反馈」串成了一条完整闭环不是那种只有登录注册的壳子。管理员后台管理分类、审核回收单、派发清理任务居民端提交回收记录、查积分、换物品跑起来就是一个能直接演示给导师看的完整业务系统。这套资源最适合两类人一类是做毕业设计或课程设计、需要一套能讲清楚业务逻辑的完整源码的人另一类是已经会写 CRUD但想知道「积分流水怎么设计」「回收单状态机怎么流转」「管理员和居民怎么共用一套登录」这类细节的开发者。它能帮你省下从零搭表结构、踩前后端联调坑的时间。我拆完这套项目后最直接的感受是它的难点不在某个单点技术而在「角色权限、状态流转、积分账目」这三件事的耦合处理。后面几章我会按设计、后端、前端、排错、进阶的顺序把关键代码和参数展开讲。2. 技术选型与数据建模为什么这十几张表能撑起整套业务2.1 技术栈选型这套组合是毕业设计的最优解先说服你一件事这套系统没有用 Spring Cloud、没有用 Redis 集群甚至连 Shiro 都没有引但这不是偷工减料反而是这类项目的正确姿势。社区垃圾管理系统的核心是业务闭环不是高并发。单机 SpringBoot 2.7.x 跑一套管理后台加居民端性能完全够用而且部署只用打一个 jar导师验收时你从启动到演示只要两分钟。我一般会建议保持项目里「SpringBoot MyBatis-Plus MySQL Vue2 Element UI」的组合不动。选 MyBatis-Plus 的理由很简单分页插件和 LambdaQueryWrapper 能省掉一半的 XML 手写 SQL尤其是回收单列表这种带多条件筛选的接口用 wrapper 链式拼接比在 XML 里拼if标签清爽得多。Vue2 配合 Element UI 组件库表格、表单、弹窗、分页这些后台管理页面基本就是组装前端开发效率很高。Maven 依赖里需要注意 spring-boot-starter-web 和 mybatis-plus-boot-starter 的版本要匹配常见坑是 MyBatis-Plus 3.5.x 配 SpringBoot 2.4 以下时容易和方法引用冲突。数据库我建议 MySQL 5.7 或 8.0编码统一 utf8mb4这样表情符号和文化字符都不会出乱码。2.2 核心表设计每张表都对应一个业务动作这套系统的表结构我可以直接画出来一共十几张表但核心就看下面这八张表名职责关键字段sys_user用户与管理员username、password、role、unit_idgarbage_type垃圾分类与积分单价name、unit_points、unit_pricerecycle_order回收单user_id、type_id、weight、points、statuspoints_record积分流水user_id、change_type、amount、order_idgoods可兑换商品name、points、stock、coverexchange_order兑换记录user_id、goods_id、points_cost、statuscleaning_task清理任务unit_id、handler、plan_time、statuscomplaint投诉建议user_id、content、reply、status这里最有讲究的是points_record积分流水这张表。它和recycle_order是一对多的关系一条回收单审核通过后会写入一条「收入」流水兑换商品后写入一条「支出」流水。积分余额永远用「收入合计减支出合计」算出来而不是在用户表里存一个points字段。这样做的好处是不会出现账目对不上的问题验收时被问到「积分是怎么算的」你可以直接甩出流水表。status字段我建议全部用 int 而不是字符串0 表示待处理、1 表示通过、2 表示驳回前端用 switch 或者数字映射字典。千万别在代码里写死中文状态后续想加「已取消」这类状态时字符串状态会让你改到想骂人。2.3 权限设计一个 token 复用两种角色这套系统是管理员和居民共用一套登录接口、一套用户表区别只在于sys_user.role字段0 是管理员1 是居民。后端用 JWT 把 userId 和 role 塞进 token过滤器解析出来之后往 ThreadLocal 里放一份接口里随时取。路径规划是/auth/**放登录注册不需要拦截/admin/**开头的接口必须 role0/user/**开头的必须 role1。前端再配合路由守卫做一层跳转控制。这就是典型的「后端兜底校验、前端控制体验」双层设计哪怕有人绕过前端直接调接口后端也会把他挡掉。数据库建表时 user 表里存密码的字段要留足长度因为存的是加盐后的 MD5不是明文也不是无盐 MD5。这个细节是很多答辩老师喜欢问的点能答上来会加分。3. 后端核心模块落地登录鉴权、积分结算与定时任务3.1 登录接口与 JWT 鉴权过滤器登录接口是整套系统的入口写法上中规中矩但有两个细节值得注意一是密码比对在 Service 层做Controller 只接收参数二是 token 的过期时间要可配置不要写死在代码里。下面是核心代码RestController RequestMapping(/auth) public class AuthController { Resource private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { User user userService.login(dto.getUsername(), dto.getPassword()); String token JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok().put(token, token).put(role, user.getRole()); } }这段代码的逻辑很简单login 方法里先按用户名查用户再把传进来的密码加盐做 MD5 和库里的值比对不匹配直接抛业务异常。createToken 会把 userId 和 role 两个字段编码进 JWT我一般设置 24 小时过期答辩当天如果忘记续期也不至于中途被踢出去。接着是鉴权过滤器这是整套系统的安全底线Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); Claims claims JwtUtil.parseToken(token); if (claims null) { throw new BusinessException(401, 未登录或token已过期); } UserContext.setUserId(claims.get(userId, Integer.class)); UserContext.setRole(claims.get(role, Integer.class)); return true; } }参数说明Authorization请求头是前端统一塞的 tokenUserContext是 ThreadLocal 封装请求结束时要在afterCompletion里调用remove()否则线程池复用会导致用户信息串号这是很多人在生产环境才发现的诡异 Bug。拦截器注册时注意addPathPatterns(/**).excludePathPatterns(/auth/**)这样登录接口和静态资源不会被拦。3.2 回收单创建与积分结算审核前后两条路居民提交回收单时前端会上传统计分类、重量和图片。后端拿到数据后要先校验分类是否存在再计算本次应得积分但注意——积分先不写入余额等管理员审核通过才算数。这是这套系统设计里最聪明的一个点能挡住「乱提交刷积分」的漏洞。PostMapping(/user/recycle/order) public Result createOrder(RequestBody RecycleOrder order) { // 1. 校验垃圾分类是否真实存在 GarbageType type garbageTypeService.getById(order.getTypeId()); if (type null) { return Result.error(该垃圾分类不存在); } // 2. 积分 重量(kg) * 该分类的积分单价 BigDecimal points order.getWeight() .multiply(type.getUnitPoints()) .setScale(1, RoundingMode.HALF_UP); order.setPoints(points); order.setStatus(0); // 0 待审核 recycleOrderService.save(order); // 3. 先不写积分流水等管理员审核通过再入账 return Result.ok(order.getId()); }逻辑说明第 2 步的setScale(1, RoundingMode.HALF_UP)是保留一位小数并四舍五入这是积分计算最容易出偏差的地方第 3 步是关键创建订单时只落订单表积分流水留到审核接口里写。管理员审核时调用同一个pointsRecordService.income(order)方法内部把积分流水和订单状态更新放在一个Transactional事务里保证要么都成功、要么都回滚。涉及金额或积分的字段一律用BigDecimal而不是double这是铁律。double算 0.1 加 0.2 会给你 0.30000000000000004账目对不上时根本查不出来。3.3 定时任务每天自动生成清理工单清理工单是这套系统的「智能」所在每天早上 8 点自动给每个小区单元生成一条待执行的清理任务不用人工手动建。SpringBoot 里用Scheduled就能实现不需要引入 Quartz 这种重武器。Component public class CleaningTaskJob { Resource private CleaningTaskService cleaningTaskService; Resource private CommunityUnitService communityUnitService; Scheduled(cron 0 0 8 * * ?) public void generateDailyTask() { ListCommunityUnit units communityUnitService.list(); for (CommunityUnit unit : units) { CleaningTask task new CleaningTask(); task.setUnitId(unit.getId()); task.setPlanTime(LocalDateTime.now()); task.setStatus(0); cleaningTaskService.save(task); } } }cron 表达式里的0 0 8 * * ?拆开看秒 0 分 0 时 8表示每天 8 点整触发?是「不指定星期」不要写成*否则星期和日期会冲突导致表达式报错。主启动类或配置类上别忘了EnableScheduling忘了的话这个任务类写再多也不会执行这是最常见的翻车点。如果你想让配置更灵活可以把 cron 表达式挪到application.yml里用Scheduled(cron ${job.cleaning.cron})读取这样部署到不同环境时改配置就行不用重新编译。4. 前端 Vue 落地路由守卫、axios 封装与统计图表4.1 路由设计与登录守卫前端项目结构我建议按views/admin和views/user分两大块管理员页面和居民页面物理隔离避免路由表混在一起越写越乱。路由配置用 Vue Router 的meta.role标记每个路由需要什么身份配合全局前置守卫做跳转控制const router new VueRouter({ mode: history, routes: [ { path: /login, component: Login }, { path: /admin, component: LayoutAdmin, meta: { role: admin }, children: [ { path: order, component: OrderManage }, { path: stats, component: StatsDashboard } ] }, { path: /user, component: LayoutUser, meta: { role: resident }, children: [ { path: recycle, component: RecycleSubmit }, { path: points, component: MyPoints } ] } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { return next(/login) } const role localStorage.getItem(role) if (to.meta.role to.meta.role ! role) { return next(/403) } next() })逻辑说明token 和 role 存 localStorage 是这套项目的常见做法刷新页面后还能保持登录态。这里有个需要注意的参数细节mode: history会让路由看起来更干净但生产环境如果不做后端 fallback刷新子路由会 404这个坑我在第五章展开讲。如果你不想碰后端配置直接改用mode: hash也能交差URL 里多个#而已。4.2 axios 封装统一注入 token 和拦截 401前端所有请求必须走封装好的 axios 实例不要在页面里裸用$.ajax。封装的核心就两件事请求拦截器注入 token响应拦截器统一处理 401 和业务错误码。import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器每个请求自动带上 token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理错误 service.interceptors.response.use( res res.data, err { if (err.response err.response.status 401) { localStorage.clear() location.href /login } return Promise.reject(err) } ) export default service参数说明baseURL: /api配合开发环境的 devServer 代理把/api转发到localhost:8080这样前端代码里不会出现硬编码 IPtimeout: 10000是 10 秒超时回收单图片上传这种慢接口可以单独放宽。401 拦截里清掉 localStorage 再跳登录页用户刷新一次就能恢复干净状态不用手动清浏览器缓存。4.3 ECharts 图表后端聚合数据前端只管渲染管理员后台的统计面板是答辩时的加分项一般包含三类图月度回收重量柱状图、分类占比饼图、积分发放趋势折线图。数据聚合一定要放后端 SQL 里做前端拿到聚合结果直接渲染。// 月度回收重量柱状图 mounted() { this.$http.get(/admin/stats/monthRecycle).then(res { const chart echarts.init(this.$refs.chartRef) chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: res.data.months }, yAxis: { type: value, name: 重量(kg) }, series: [{ name: 回收重量, type: bar, barWidth: 40%, data: res.data.weights }] }) }) }后端对应 SQL 的思路是SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(weight) FROM recycle_order WHERE status 1 GROUP BY month把「审核通过的订单」按月份求和前端拿回来直接塞进 xAxis 和 series。注意饼图的data要组装成{ name: 可回收物, value: 123 }这种对象数组刚上手的人最容易在[{name: ..., value: ...}]和[{label, value}]之间搞混导致图不渲染。图表容器一定要给固定高度styleheight: 360pxecharts 初始化时容器高度为 0 的话图会缩成一条线。5. 常见问题排查跨域、时间格式与打包的五条血泪经验5.1 刷新子路由 404history 模式的经典坑现象前端 npm run build 之后把 dist 目录丢进 SpringBoot 的 static 目录启动 jar 后首页能打开但一刷新/admin/order就白屏后端报 404。原因Vue Router 用的history模式路由跳转是前端伪路由真实请求到了后端容器SpringBoot 找不到对应的 controller 就返回 404。hash 模式不会有这个问题因为它只改#后面的部分。解决加一个 WebMvc 配置类做 fallback把非/api开头的请求全部转发到 index.htmlConfiguration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:[a-zA-Z0-9._-]}) .setViewName(forward:/index.html); registry.addViewController(/**/{spring:[a-zA-Z0-9._-]}) .setViewName(forward:/index.html); } }如果不改后端最省事的办法是把前端路由改成hash模式重新打包。我自己的习惯是答辩演示项目直接上 hash 模式少一个排查维度。5.2 时间字段返回 2025-01-01T00:00:00.00000:00现象前端展示回收单创建时间页面上一长串英文格式看着非常不像人话。原因Jackson 默认把 LocalDateTime 序列化成 ISO 标准格式时区默认 UTC比北京时间少了 8 小时。解决在 application.yml 里统一配置时间格式和时区spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai配置完重启所有 LocalDateTime 字段都会变成2025-01-01 08:00:00这种正常格式。前端如果再配合 dayjs 格式化一次展示彻底没毛病。5.3 Invalid bound statement (not found)现象启动项目不报错一调用带 SQL 的接口就报Invalid bound statement (not found)看似 Mapper 接口找到了XML 里的 SQL 没找到。原因maven 打包时默认只拷贝 resources 目录下的文件mapper/*.xml放在src/main/java下面时就打不进 target运行时当然找不到。解决在 pom.xml 的 build 节点里加 resource 配置把 xml 和 properties 文件都打进去resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources另外确认启动类上的MapperScan(com.xx.mapper)包路径和 Mapper 接口所在包完全一致大小写都对。5.4 数据库连接报时区错误现象本地 MySQL 连接时抛The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8.x 的驱动对时区要求更严格默认读取系统时区中文乱码时区名直接不认。解决JDBC URL 上显式加时区和编码参数spring: datasource: url: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai顺便把serverTimezoneAsia/Shanghai和上一节 Jackson 的时区配成一致数据库和接口输出才不会出现 8 小时的偏差。5.5 跨域Swagger 能调通页面调不通现象前端页面请求/admin/**报No Access-Control-Allow-Origin header is present浏览器控制台红色一片用 Postman 或 Swagger 直接调后端却是正常的。原因浏览器同源策略只在「浏览器发起的跨域请求」上生效Postman 不受同源约束所以正常。前端 npm run dev 跑在 5173 或 8081后端在 8080协议、域名、端口只要有一个不同就算跨域。解决先确认开发环境用的是 devServer 代理一般配置proxy: { /api: { target: http://localhost:8080, changeOrigin: true } }就够如果前后端直连后端加一个全局 CORS 配置Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); }allowedOriginPattern在 SpringBoot 2.4 用 OriginPattern 代替已弃用的addAllowedOrigin(*)否则带凭据的请求会被拦截。生产环境不建议*全放指定前端的真实域名更安全。6. 进阶改造把轮询换成 WebSocket 实时推送回收提醒这套系统原来的通知方式是前端每 30 秒调一次查询接口拿最新订单状态。功能没问题但体验很迟钝居民提交回收单后管理员审核通过居民端要等到下一次轮询才能看到「审核通过、20 积分」的提示。而且轮询加重了后端无意义的请求压力纯属耗 CPU。我把它改成了 WebSocket 实时推送只动两个文件加几行前端代码工作量不大但演示效果提升明显。后端基于ServerEndpoint思路做一个极简推送器用 ConcurrentHashMap 维护每个用户的连接会话Component ServerEndpoint(/ws/notify) public class NotifyWebSocket { private static final MapLong, Session CLIENTS new ConcurrentHashMap(); OnOpen public void onOpen(Session session, QueryParam(userId) Long userId) { CLIENTS.put(userId, session); } OnClose public void onClose(QueryParam(userId) Long userId) { CLIENTS.remove(userId); } public static void push(Long userId, String message) { Session session CLIENTS.get(userId); if (session ! null session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } }逻辑说明用户在登录后跳转页面时前端通过new WebSocket(/ws/notify?userId userId)建立连接后端把 userId 映射到 Session 句柄。管理员在审核通过接口里调一行NotifyWebSocket.push(order.getUserId(), 您的回收单已审核通过积分 order.getPoints())居民端就立刻收到消息。push是 static 方法就是为了让业务层不注入 Bean 也能直接调用这是这套写法最顺手的地方。前端配合的代码更简单// 登录后初始化连接 connectNotify() { const userId localStorage.getItem(userId) this.ws new WebSocket(ws://${location.host}/ws/notify?userId${userId}) this.ws.onmessage (e) { // 收到审核结果弹提示并刷新列表 this.$message.success(e.data) this.loadOrders() } this.ws.onclose () { // 断线后 5 秒自动重连 setTimeout(() this.connectNotify(), 5000) } }验证这套改造是否生效有个笨但有效的办法开两个浏览器窗口一个登录管理员后台一个登录居民端。管理员在后台把某条回收单点「审核通过」的瞬间居民端页面不用任何刷新就弹出消息提示。我当年验证的时候第一次没弹仔细查是用户 ID 从小写 localStorage 取成了大小写混写断点一看userId是nullSession 没存进去。从那以后我每次做这类前后端分离的毕业设计项目都强制走一遍「双窗口实时联动验证」确认推送延迟在一秒以内才算通过。这套源码本身把权限、状态流、定时任务都铺好了你拿到手先按第五章的坑清单检查一遍再按第六章的方式把实时推送加上演示效果会拉满希望帮到你。本文还有配套的精品资源点击获取