ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot的城市垃圾分类管理系统设计与实现详解

基于SpringBoot的城市垃圾分类管理系统设计与实现详解 每年到了毕业季就有不少同学私信问我毕设选题的事情。说实话与其跟风做那些重复度极高的商城系统、图书管理系统不如做点既有社会话题性、又能把主流技术栈完整串起来的项目。我最近帮人review过一套“基于SpringBoot的城市垃圾分类管理系统”的源码整体质量不错而且挺适合做毕设的。今天就把这套系统的设计思路、数据库模型、核心实现和部署排错心得掰开揉碎了讲一讲。不管你最后是直接用的开源源码还是准备自己从零敲一个同款这篇文章都能帮你避开不少弯路。1. 项目概述与选题逻辑1.1 为什么垃圾分类管理适合当毕设先聊聊选题。很多同学在毕设选题的时候都卡在“看起来简单”和“技术含量够”之间。垃圾分类管理系统这个题目看起来是给学校答辩看的但打开来看它其实覆盖了一套完整业务系统该有的所有环节用户端、管理端、数据存储、状态流转、甚至还有积分激励体系。从答辩表现的角度来说这个选题有个天然优势——业务模型贴近生活评委老师一看就明白你在做什么不用花大量口舌解释业务背景。更重要的是它的功能扩展空间很大你可以在标准CRUD之外加上垃圾分类查询、预约上门回收、积分兑换、统计报表等功能每一块都能拿出来单独讲技术实现。这套系统的核心价值在于它完成了从“居民不知道垃圾怎么分”到“分完能拿积分换奖品”的完整闭环。用户端解决“怎么分”的疑惑后台解决“分类结果怎么管理”的诉求积分体系则解决了“让用户愿意主动分类”的激励问题。技术栈上则涵盖了SpringBoot、MyBatis Plus、MySQL、Redis、Vue等毕设高频技术属于那种“看起来不炫技但每块都踩在点子上”的项目。1.2 系统功能模块全貌我拿到这套源码第一件事就是画了一张功能脑图。主要角色分三种普通居民用户、小区管理员、平台超级管理员。用户端有注册登录、垃圾分类查询、预约回收、积分明细、个人中心管理端有垃圾类别管理、回收订单管理、积分规则配置、公告发布、数据统计看板。这里需要特别提一下“垃圾类别管理”和“垃圾分类查询”的区别。很多同学一开始把这两个功能混在一起做结果后台配置改了前端查询结果却不更新。这套源码的处理方式是垃圾类别条目独立成一张表用户查询时实时关联后台每改一条数据用户端下一次搜索立刻就能看到最新结果不需要重新构建缓存。这个细节我后面在代码解析部分会展开讲。2. 数据库设计与核心表结构2.1 四分类业务模型怎么落地到表垃圾分类本身有明确的行业标准可回收物、有害垃圾、厨余垃圾、其他垃圾。不过到了系统设计里这个“四分类”不能简单地写成四个硬编码字符串否则以后街道想细化分类比如增加“大件垃圾”“装修垃圾”你就得改代码。这套源码采用了一张字典表 多张业务表的策略。字典表存分类的基本信息比如分类名称、图标、描述、投放要求业务表存具体的垃圾条目比如“塑料瓶”“废电池”“剩菜剩饭”每个条目通过category_id关联到字典表。这样做的好处是数据结构上有主外键约束统计报表按分类聚合时SQL写起来非常顺手。值得留意的是预约回收模块单独建了一张订单表订单里既有用户信息和预约时段也有垃圾的重量估算和回收状态。这个设计把“知识库查询”和“业务流程”完全隔离开不会因为垃圾条目删改影响到订单历史数据。2.2 核心表结构设计与建表参考看源码的时候我习惯先把建表SQL过一遍因为表结构能直接反映开发者的数据建模水平。下面这张表就是核心的用户表表结构设计得比较规范CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, phone varchar(11) DEFAULT NULL COMMENT 手机号, password varchar(128) DEFAULT NULL COMMENT 密码BCrypt加密, nickname varchar(32) DEFAULT NULL COMMENT 昵称, avatar_url varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) DEFAULT 0 COMMENT 角色0普通用户 1管理员, status tinyint(4) DEFAULT 1 COMMENT 状态1正常 0禁用, create_time datetime DEFAULT NULL COMMENT 创建时间, update_time datetime DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户表;这里有两个细节值得抄作业。一是role字段用数字而不是直接用字符串user/admin这样扩展角色的时候不用动表结构。二是密码字段长度给了128因为用的是BCrypt加密加密后的字符串比MD5长不少如果你还是用char(32)后面存密文会直接报错。垃圾条目表的设计也很典型CREATE TABLE garbage_item ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 垃圾名称, category_id bigint(20) NOT NULL COMMENT 分类ID, description varchar(255) DEFAULT NULL COMMENT 详细说明, recycle_method varchar(255) DEFAULT NULL COMMENT 投放/回收方式, status tinyint(4) DEFAULT 1 COMMENT 状态1上架 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category_id), CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES garbage_category (id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT垃圾条目表;外键约束很多人会忽略但我们自己建表最好还是加上。虽然网上有说法说互联网公司为了性能不用外键可毕设场景里外键的存在反而是加分项——它能证明你懂数据完整性而且答辩时老师很可能问“垃圾类目删了订单数据怎么办”这时你可以说使用ON DELETE RESTRICT限制了直接删除必须先把关联数据处理掉这就是一个很好的答辩论据。预约回收订单表我单独拿出来说因为它是整张业务链路的主轴CREATE TABLE recycle_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 下单用户, category_id bigint(20) DEFAULT NULL COMMENT 回收大类, appointment_time datetime DEFAULT NULL COMMENT 预约上门时间, address varchar(255) DEFAULT NULL COMMENT 回收地址, weight_estimate decimal(10,2) DEFAULT NULL COMMENT 预估重量(kg), status tinyint(4) DEFAULT 0 COMMENT 状态0待接单 1已接单 2已完成 3已取消, points_rewarded int(11) DEFAULT 0 COMMENT 奖励积分, remark varchar(255) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT回收订单表;订单号的生成方式在源码里是直接用yyyyMMddHHmmss 随机数这种方案的优点是简单直观不会有并发主键冲突。如果想更严谨一点可以用数据库自增 Redis自增拼接不过对体量不大的系统来说已经够用了。3. 技术栈与工程结构解析3.1 后端技术栈为什么这么选SpringBoot 2.x MyBatis Plus MySQL Redis JWT这套组合在毕设圈里算是“标配中的顶配”。选SpringBoot不奇怪它省掉了Spring MVC时代那一大堆XML配置起步快、社区活跃遇到问题一搜一大把解决方案。MyBatis Plus作为MyBatis的增强工具最大价值就是单表操作不用写SQL。比如删除一个条目只需要调用garbageItemMapper.deleteById(id)。自由组合条件查询可以用它的LambdaQueryWrapper既安全又能避免字符串字段名写错导致运行时才报错的问题。对于毕设交作业这个场景来说MyBatis Plus能让你把时间花在业务逻辑上而不是毫无技术含量的CRUD模板代码里。Redis在这个项目里当了两类角色一类是缓存比如首页的垃圾分类热榜另一类是JWT Token的存储。讲个实际例子后台修改了用户状态为禁用后已经登录的用户理论上还能继续访问接口。这种场景靠前端判断不够可靠后端在拦截器里每次校验Token的同时顺带查一下Redis里的用户状态如果已被禁用就直接拒绝。这个小技巧答辩时讲出来老师是会点头的。前端用的是Vue Element UI典型的后台管理系统组合。因为这套源码把前后端完全分离了所以开发时两边独立跑端口联调阶段靠CORS配置放行不同源的请求。后面我在部署环节会专门说明这个配置细节。3.2 工程目录结构与启动流程SpringBoot项目看结构基本就能猜出作者的代码风格。这套源码的工程结构是标准的单模块分层src/main/java/com/example/garbage ├── config # 配置类跨域、MyBatis Plus分页等 ├── controller # 接口层 ├── service # 业务逻辑层 │ ├── impl # 业务实现 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 请求/响应封装对象 ├── common # 公共类统一返回结果、异常处理、常量 ├── utils # 工具类JWT工具、日期工具 └── GarbageApplication.java启动流程也不复杂三步走先创建数据库并导入garbage.sql然后修改application.yml里的数据库账号密码最后运行GarbageApplication.java的main方法。整个过程没有任何分布式中间件的强依赖单机就能跑起来这也符合毕设演示的实际情况。有点需要提醒的源码里的application.yml有多个配置项比如redis.host、file.upload-path、jwt.secret等别看漏了。有些同学启动报错其实是Redis连不上但日志只给了“连接超时”让人误以为是SpringBoot本身的问题白白排查老半天。4. 核心功能实现与代码细节4.1 登录鉴权JWT方案登录模块看起来简单但真正动手做过的人都知道坑不少。这套源码用的是JWT Redis的解决方案用户登录成功后后端生成一个Token返回给前端前端在请求头里带上Authorization: Bearer token后端写了一个拦截器统一校验。生成Token的代码大致是这样public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }这里有几个点值得细看。一是签发时间setIssuedAt和过期时间setExpiration一定要都设置有些教程只设置过期时间导致生成的Token没有签发时间后面排查问题会很被动。二是密钥secretKey不要硬编码在代码里虽然毕设项目一般不会有人真的攻击你的服务但在课上被老师看到你把密钥写在常量类里印象分还是会打折的我倾向于放到配置文件里引用。拦截器校验的逻辑也不复杂先从请求头里取Token解析成功后再查Redis是否还在有效期内两步都通过才放行。有些同学问为什么Token本身已经包含了过期时间还要查Redis答案其实我前面提过——为了支持“强制下线”这种主动注销场景你不用Redis的话后端根本没有办法提前作废一个还没过期的Token。4.2 垃圾分类查询与知识库用户在前端搜索框输入“塑料瓶”系统返回该垃圾条目以及对应的分类名称、投放建议和回收方式这个功能就是知识库的核心。源码里用的是LambdaQueryWrapper做模糊搜索public ListGarbageItemVO searchGarbage(String keyword) { LambdaQueryWrapperGarbageItem wrapper new LambdaQueryWrapper(); wrapper.like(item - item.getName(), keyword) .and(item - item.getCategoryName() ! null item.getStatus() 1); ListGarbageItem items garbageItemMapper.selectList(wrapper); return convertToVO(items); }like用的是SQL里的%keyword%写法这个做法对中小型数据量完全没压力。但如果垃圾条目表到了几十万条规模%keyword%是走不了索引的那时候就该考虑全文检索方案比如MySQL全文索引或者ES。不过毕设项目把所有条目全量插进去也就几千条用like完全OK答辩时如果老师问性能你把这个扩展思路讲出来比单纯说“能用”要有力得多。搜索接口的响应设计成统一返回结构我用过一次之后发现这个习惯还挺重要{ code: 200, message: success, data: { list: [...], total: 5 } }前端的Vue里对response.code做了判断非200统一弹ElMessage提示错误。这种统一返回结构减少了前后端联调的沟通成本也让接口风格看起来更专业我在自己日常的项目里也一直沿用这个模式。4.3 预约回收与积分闭环预约回收这个环节是区分“课设CRUD”和“更像真实业务系统”的分水岭。用户选择垃圾类别、填写上门时间地址、提交订单后订单状态为“待接单”管理员在后台把订单改成“已接单”然后等实际上门完成后再改成“已完成”。这里面的状态流转源码里不是简单地用if else硬写而是提取了一个订单状态枚举类各个状态之间还做了合法流转校验。这个设计值得学习的地方在于它把订单状态从“字符串魔改”变成了“类型安全”的对象。比如你要从“待接单”直接跳到“已完成”系统会直接拒绝因为枚举里定义的状态机根本不允许这个跳跃。毕设项目能做出这种细节说明你对业务有认真思考。积分发放的逻辑放在订单被标记为“已完成”的时候触发而不是在“已接单”阶段就发放。这个细节很关键因为它保证了用户真正把垃圾交出去之后才能拿到奖励。积分的计算规则是估算重量大于0就按重量 * 权重换算默认情况下每公斤计10分。源码里把权重直接做成了一张配置表方便管理员在后台随时调整。这套配置化的思路也可以套用在签到积分、评论积分等场景里。4.4 管理后台与统计报表管理后台是整个系统的“面子工程”也是答辩老师最喜欢演示的部分。源码里集成了ECharts用几个简单的折线图、柱状图和饼图来展示各小区回收单量的趋势、垃圾类别的占比、用户积分排行等数据。统计接口的实现很有意思不是到业务库里做复杂的多表联查而是单独建了一张daily_statistics表每天定时任务跑一次把昨天的订单数、总重量、分类统计都预处理好。前端查询的时候直接读这张表性能自然快。这个设计在真实企业项目里就是“日报表预处理”在毕设里用上它等于告诉老师你理解“读多写少的数据可以提前聚合”这个优化原则。ECharts前端配置也不复杂Vue里在mounted钩子里调用后端接口拿到数据后直接setOption渲染图表。唯一提醒的是图表容器的宽度高度一定要通过style指定我曾经见过学生把div宽度写成100%结果图表渲染出来只有一条线怎么查都查不出来原因后来发现是父容器没给高度导致ECharts初始化时拿到的尺寸是0。5. 本地部署与踩坑实录5.1 环境准备与配置清单部署这套系统的硬件门槛很低一台8G内存的笔记本电脑就够。软件环境方面建议统一用这些版本能最大程度避免版本兼容问题依赖项推荐版本说明JDK8 或 11Spring Boot 2.x 必须用 8别用21除非你改了全套依赖Maven3.6.x3.8以上也可以但镜像源需要配好MySQL5.7 或 8.0建库用utf8mb4否则中文可能乱码Redis6.x默认端口6379密码可留空Node.js12~16前端Vue项目npm install用的application.yml里最重要的几个配置项如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 jwt: secret: your-secret-key expire-hours: 24有同学反映MySQL8.0连接时提示Public Key Retrieval is not allowed这是因为连接串里少了allowPublicKeyRetrievaltrue。JDBC写法如下url: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue原来是MySQL8默认使用caching_sha2_password认证客户端第一次连接时想拿服务端公钥结果被安全策略挡住。加上这个参数就解决了。5.2 常见部署报错对照表这套系统我在不同电脑上跑过多次也帮人远程看过不少错误日志大部分问题集中在下面这几类报错现象根本原因解决办法Failed to configure a DataSource数据库连接配置错误或驱动缺失检查application.yml的url/账号密码Table garbage_db.xxx doesnt exist建库导表不完整用Navicat或命令行重新执行garbage.sqlERR Client sent AUTH, but no password is setRedis有密码但没配置在配置里加上redis.password或清掉本地Redis密码端口被占用: 8080其他程序占了端口改server.port或查进程杀掉npm run serve报Node Sass/ Sass相关错误Node版本和sass-loader不兼容换成Node14或者重装node-sass端口占用这个问题我多说一句。Windows下可以先netstat -ano | findstr :8080找到PID后到任务管理器结束进程。macOS/Linux用lsof -i:8080。这种情况在实验室电脑上特别常见因为上一届学长可能跑过别的SpringBoot项目没关干净。5.3 前后端联调时的跨域配置前后端分离后前端跑在http://localhost:5173后端跑在http://localhost:8080两个端口不同浏览器默认会拦截非同源的Ajax请求。这时候需要在后端配置CORS。源码里的做法是写一个WebMvcConfigurer配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }要注意的是allowedOriginPatterns(*)和allowCredentials(true)搭配是SpringBoot 2.4以后允许的写法早期的allowedOrigins(*)在这种组合下会报错。前后端跑在一个服务器上部署时这个跨域配置可以去掉改成Nginx统一转发效果更好。6. 从毕设源码到答辩加分拿到一套能跑的源码只是起点真正拉开分差的是你对代码的理解深度。我建议你在答辩前把这几条链路亲手走一遍注册用户 → 搜索垃圾条目 → 提交预约回收 → 管理员后台接单 → 标记完成 → 用户积分变化 → 积分明细展示。这条链路从头到尾能串起来说明你理解了系统的数据流任何一环答不上来都容易露怯。一个比较实用的小技巧把源码里的一些关键表比如recycle_order的ER关系、状态机流转图、系统架构图提前画好打印成纸质版带去答辩现场。老师提问时你直接指着图讲既省时间又显得专业。很多同学只顾着把代码跑起来却忽略“可视化表达”这件事等于白丢了一部分印象分。最后一个建议如果你打算在源码基础上改功能优先改“积分规则配置”和“统计报表”这两个模块。因为这两个地方最容易被老师追问“为什么这样设计”而你也最容易结合自己的改动说出真实思考。比如给积分规则增加一个“按投放次数加权”的参数前端再配合改一个表单工作量不大但讲出来的效果完全不一样。跑通代码只是开始把每条数据流、每个决策的前因后果讲明白才是毕设真正想考察的东西。希望这篇拆解能帮你少走几步弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进