
先说结论这类Spring Boot Vue MySQL的管理系统看标题像是又一个烂大街的课设但垃圾分类回收这个选题和循环经济的政策方向绑得很紧做得好完全可以作为毕业设计、求职项目甚至接真实需求的敲门砖。我前阵子刚完整陪跑了一个类似项目从零到上线从设计表结构到部署到服务器踩了不少坑这篇就把整个项目的核心链路、关键代码和部署调试经验全部摊开讲。整个项目说穿了就是两层面向居民用户的分类查询 预约回收和面向管理方的分类管理 订单调度 积分核销。前端用Vue做单页应用后端用Spring Boot暴露REST接口MySQL负责所有结构化数据的存储三者的关系用一个最简单的比喻就是Vue是餐厅前台Spring Boot是后厨MySQL是仓库。前台点菜后厨根据菜单去仓库取料做完再端出来。垃圾分类回收系统开发实录从表结构设计到服务器部署的完整复盘刚开始动手时我建议你先别急着敲代码而是把这个系统当成一个真实的社区环保基础设施来思考。很多同学做这类项目容易陷入CRUD堆砌表建了五六张接口也写了不少但问起来这个功能为什么这么设计就答不上来。这套系统不一样它的核心优势在于垃圾分类有明确的国家标准和线下业务流程你可以把业务逻辑讲得头头是道答辩或面试时天然有故事可讲。1. 项目定位与功能地图不只是垃圾二字这么简单1.1 从真实痛点反推需求清单垃圾分类在国内很多城市已经强制推行了好几年但你去任何一个小区垃圾投放点蹲十分钟就会发现问题还是有不少人拎着混装垃圾袋直接扔。为什么不是大家不想分而是真的记不住——大棒骨是厨余垃圾还是其他垃圾嚼过的口香糖算什么过期化妆品是有害垃圾吗每一次犹豫都在消耗居民的耐心。所以这个系统的第一刚需是垃圾分类检索用户输入垃圾名称系统立刻告诉你属于哪一类、应该投放到哪个颜色的桶。第二刚需是可回收物预约上门纸箱、旧衣服、家电这类有回收价值的物品用户在线下单回收员接单上门。第三刚需是积分激励正确分类或成功回收后获得积分积分可以兑换垃圾袋、日用品等形成闭环。基于这三个刚需系统功能地图就非常清晰了用户端注册登录、垃圾名称搜索分类、分类知识浏览、预约回收下单、订单状态跟踪、积分查询与兑换管理端用户管理、垃圾分类标准管理类别物品条目、回收订单派单与处理、积分配置与核销、内容管理分类科普文章/公告、数据统计看板1.2 角色权限划分是怎么落地的我不建议在这个项目里引入Spring Security Redis做太复杂的权限模型杀鸡用牛刀还增加部署难度。更稳妥的方案是用JWT 拦截器控制登录态用用户表中role字段区分普通用户和管理员。角色权限三道关卡前端路由守卫未登录跳转登录页非管理员访问/admin路径时直接拦下后端拦截器校验JWT合法性解析出用户角色接口级校验管理端接口统一加RequireAdmin注解或简单的角色判断即使绕过前端也调不通接口这种三级防护对课设和中小型项目来说绰绰有余而且面试被问权限怎么做的时候你能从前端控制体验后端保证安全这个角度回答比死记RBAC模型生动得多。2. 技术选型与版本锁定为什么是这套组合以及版本坑在哪里2.1 前后端分离的技术栈选择逻辑先回答一个我被人问过很多次的问题为什么选Spring Boot而不是SSHStrutsSpringHibernate或SSM因为这个项目要体现快速开发、开箱即用Spring Boot的自动配置、起步依赖和嵌入式Tomcat能省掉大量XML配置让你把精力聚焦在业务逻辑上。后端Spring Boot 2.7.x MyBatis-Plus MySQL 8.0前端Vue 2.6.x Element UI Axios Vue Router认证方案JWTjjwt库构建工具Maven 3.8前端用npm2.2 版本搭配的硬约束这一节是很多人栽跟头的地方我把测试过能稳定运行的一套组合直接贴出来组件版本说明JDK1.8千万别用17部分老依赖会出问题Spring Boot2.7.182.x最终版稳定不会自动升级到3.xMyBatis-Plus3.5.3注意与Spring Boot 2.x兼容MySQL8.0.x8.0以上5.7也能跑但建议直接用8.0Node.js16.x14-18均可Vue 2项目高版本Node编译会报openssl错误Vue CLI4.5.x与Node 16匹配较好Element UI2.15.xVue 2专用UI库网上很多教程直接让你装最新的Spring Boot 3.x JDK17然后跟着走就发现MyBatis-Plus的mybatis-plus-boot-starter报错因为那个starter还是给Spring Boot 2.x用的。如果你是照着课程/毕设模板改的优先把Spring Boot版本锁定在2.7.x能少折腾两天。2.3 数据库和中间件怎么选型MySQL是这套系统的绝对主力不需要额外引入Redis或MQ。有些同学为了显得高级非要在项目里塞一个Redis做缓存——问题是等你打包部署到生产环境光配置Redis就是个新的坑。我的建议是先把纯MySQL版本跑通上线再考虑加缓存提升性能这样你的大文档里也可以写一章性能优化方案引入Redis缓存热点分类数据属于锦上添花而非雪中送炭。3. 数据库设计七张核心表把业务串成一条完整链路3.1 表结构全景数据库是这种管理系统最见功底的部分也是答辩时老师最爱深挖的地方。整个项目我设计了七张核心表user 用户表id, username, password, phone, role, points, create_time garbage_category 垃圾分类类别表id, name, code, icon, description garbage_item 垃圾物品条目表id, category_id, name, keyword, tips recycle_order 回收订单表id, order_no, user_id, address, contact, status, appointment_time, remark, create_time recycle_detail 回收物品明细表id, order_id, item_name, type, weight, quantity points_record 积分流水表id, user_id, change, type, description, create_time article 科普文章表id, title, content, cover, create_time有同学会问garbage_item和garbage_category为什么不合并成一张表直接存垃圾名和分类名原因很简单同一类别的垃圾具有共同的投放属性比如可回收物对应蓝色桶如果合并每个垃圾条目都要重复存一遍分类名冗余不说改分类名还要批量更新这是低版本第一范式都没达到的设计。3.2 垃圾分类的层级划分技巧查阅各地垃圾分类标准后会发现主流分法是四类可回收物、有害垃圾、厨余垃圾、其他垃圾。数据库里我专门建了garbage_category表存储这四大类用code字段做唯一标识RECYCLABLE/HAZARDOUS/KITCHEN/OTHER前端地图配色和图标通过这个code动态渲染。核心的知识库表garbage_item是支撑全文搜索的基石建议设计时加上索引。在建表SQL里keyword字段用于模糊搜索的兜底比如大棒骨可能被用户输入成猪骨头羊骨头这些别名都放进keyword里用逗号分隔。CREATE TABLE garbage_item ( id int NOT NULL AUTO_INCREMENT, category_code varchar(32) NOT NULL COMMENT 所属分类code, name varchar(100) NOT NULL COMMENT 垃圾名称, keyword varchar(500) DEFAULT NULL COMMENT 搜索关键词别名逗号分隔, tips varchar(500) DEFAULT NULL COMMENT 投放提示, PRIMARY KEY (id), KEY idx_name (name), KEY idx_category (category_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.3 订单状态机的设计recycle_order表的核心是status字段我用了数字状态码而不是字符串既省空间又方便比较0 - 待接单用户提交预约 1 - 已接单回收员后台接单 2 - 已完成上门回收完成 3 - 已取消用户或管理员取消为什么非要搞状态机因为前端要显示不同状态下的操作按钮待接单时可以取消已接单后不能取消只能等待完成。后端在更新状态时要校验合法性比如不允许从待接单直接跳到已完成必须经过已接单。我在Service层写了一个状态流转校验方法非法操作直接抛业务异常。3.4 积分配置可扩展的设计比写死更聪明积分规则我做成了一张简单的配置表points_configid, type, value而不是在代码里写if type 1 then points 10。比如完成一次回收得50积分每日首次登录得2积分那么管理员可以直接在后台改数值而不用改代码重新部署。这是很多商业项目里配置化思维的具体体现写进文档里绝对是亮点。4. 后端核心逻辑实现检索、下单、积分兑换三大主链路4.1 垃圾分类检索接口的SQL优化搜索功能是用户第一触点体验必须好。实现非常简单但有几个细节决定了搜索结果质量。我的接口逻辑是public ListGarbageItemVO searchGarbage(String keyword) { // 先精确匹配 LambdaQueryWrapperGarbageItem wrapper new LambdaQueryWrapper(); wrapper.eq(GarbageItem::getName, keyword) .or().like(GarbageItem::getKeyword, keyword) .or().like(GarbageItem::getName, keyword); return garbageItemMapper.selectList(wrapper); }注意这里的查询顺序先查名称精确匹配再查关键词模糊匹配最后才查名称模糊匹配。为什么因为用户输入塑料瓶如果数据库里正好有名字叫塑料瓶的物品优先返回它肯定更精准如果没有精确匹配再用别名搜索和模糊搜索兜底。如果物品数量很大几千条以上LIKE %keyword%会走全表扫描性能会明显下降。优化方案有两种引入Elasticsearch做全文检索——但这明显超出这个项目的复杂度了不适合课设用MySQL全文索引FULLTEXT替代LIKE性能提升明显但中文分词效果一般实际项目的恰当选择是LIKE 热门搜索前置缓存把最常见的几百条垃圾数据塞进内存响应时间控制在10毫秒以内。4.2 预约回收下单的事务处理用户提交回收预约时除了插入recycle_order主表还要插入recycle_detail明细表用户选了哪些可回收物品、预估重量、备注。两条插入操作必须保证原子性要么都成功要么都失败。用Transactional注解搞定。Transactional public Long createOrder(CreateOrderDTO dto) { // 1. 生成订单号当前时间戳 用户ID后四位 随机数 String orderNo String.format(%s%s%03d, new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()), dto.getUserId() % 10000, new Random().nextInt(1000)); // 2. 插入订单主表 RecycleOrder order new RecycleOrder(); order.setOrderNo(orderNo); order.setStatus(0); // 待接单 recycleOrderMapper.insert(order); // 3. 插入明细表 for (RecycleItemDTO item : dto.getItems()) { RecycleDetail detail new RecycleDetail(); detail.setOrderId(order.getId()); detail.setItemName(item.getName()); detail.setWeight(item.getWeight()); recycleDetailMapper.insert(detail); } return order.getId(); }这个接口踩过一个真实的坑订单号重复。一开始我用System.currentTimeMillis()直接当订单号结果测试时连续快速提交两个订单居然撞了。改成上面的时间戳用户ID随机数方案后还是可能重复最稳妥的方案是在数据库给order_no字段建唯一索引如果插入时违反唯一约束就重试一次。事后来看课设阶段用UUID当订单号最省心但展示的时候订单号又长又丑答辩印象分反而不高。4.3 状态流转与积分发放的正确姿势订单状态流转我用了一个简单的状态机校验private static final MapInteger, ListInteger ALLOWED_TRANSITIONS new HashMap(); static { ALLOWED_TRANSITIONS.put(0, Arrays.asList(1, 3)); // 待接单 - 已接单 / 已取消 ALLOWED_TRANSITIONS.put(1, Arrays.asList(2, 3)); // 已接单 - 已完成 / 已取消 }管理员把订单状态改为已完成时系统自动调用积分服务给用户增加积分同时写入积分流水。这里又要考虑事务边界更新订单状态和发放积分必须在同一个事务里否则如果积分发放失败订单状态已经更新用户就白白回收了一次。把积分发放做成独立的Service方法在状态更新的同一事务内调用。4.4 管理端统计查询用聚合函数画数据看板管理后台首页的统计看板是展示项目完成度的加分项三四个数字和两张趋势图立刻让项目显得完整。核心SQL是MyBatis-Plus的聚合查询// 今日新增订单数 QueryWrapperRecycleOrder wrapper new QueryWrapper(); wrapper.select(COUNT(*) as total) .ge(create_time, DateUtil.beginOfDay(new Date())); MapString, Object map recycleOrderMapper.selectMaps(wrapper).get(0);按分类统计可回收物占比、近七日回收趋势这几类图表都是类似的GROUP BY查询前端配合ECharts实现数据格式用JSON返回即可。5. 前端Vue实现从搜索联想到底部导航贴近真实APP的体验5.1 前端工程目录设计Vue项目我推荐用Vue CLI创建目录结构保持清晰到看一眼就懂src/ api/ # 接口请求封装 garbage.js order.js user.js router/ # 路由配置 index.js store/ # Vuex状态管理主要管理用户信息和登录态 index.js views/ # 页面组件 home/ # 首页 search/ # 垃圾分类搜索页 order/ # 预约回收 user/ # 个人中心 admin/ # 管理后台 utils/ # 工具类axios实例、JWT处理等5.2 搜索联想功能防抖与远程搜索垃圾搜索页是用户端最核心的交互页面输入框一敲击就发出请求、展示联想结果、给出分类图标这个体验做好了项目质感直接拉满。实现思路用Element UI的el-autocomplete组件配合自定义远程搜索方法el-autocomplete v-modelkeyword :fetch-suggestionsquerySearch placeholder请输入垃圾名称例如塑料瓶 selecthandleSelect querySearch(query, cb) { if (!query) return cb([]); // 防抖处理避免每敲一个字符就发请求 clearTimeout(this.timer); this.timer setTimeout(() { searchGarbageApi(query).then(res { const suggestions res.data.map(item ({ value: item.name, ...item })); cb(suggestions); }); }, 300); }加防抖的意义在于如果用户输入塑料瓶不防抖的情况下会发塑塑料塑料瓶三次请求防抖后只发最后一次既减轻后端压力又防止页面结果抖动。5.3 用户端两条核心流程的交互细节流程一垃圾分类查询。用户进入首页看到四大分类图标入口点击进入该分类的物品列表页也可以在搜索框直接输入。选中某个垃圾条目后弹出底部气泡展示分类结果大图标文字配合颜色提示可回收物蓝色、有害垃圾红色、厨余垃圾绿色、其他垃圾灰色。屏幕配色建议直接照着交管app的视觉规范来用户一看颜色就知道咋扔。流程二预约回收。进入预约页面后用户依次填写上门地址、联系人和电话、预约时间段、回收物品清单物品名称数量/重量备注提交后生成订单号。前端要做的是表单校验手机号合法性和地址非空。5.4 axios封装请求拦截器统一携带Token前后端分离下JWT必须每次请求都携带。我用axios拦截器全局统一加请求头同时处理后端返回的401状态码自动跳转登录页// 请求拦截器 service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); // 响应拦截器 service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { router.push(/login); } return Promise.reject(error); } );这里有个部署阶段容易翻车的细节后端需要允许Authorization请求头跨域。如果后端用CorsFilter允许跨域默认allowedHeaders(*)没问题但如果用了自定义CORS配置只允许指定header就会看到前端发请求时浏览器直接拦截。太多次在联调阶段排查为什么前端访问不了后端最后发现是这个问题。5.5 管理后台用表格和弹窗撑起完整的运营视角管理端我用Element UI的el-tableel-dialog组合实现分类管理、订单处理、用户管理三大模块。具体来说分类管理左侧显示四大分类卡片右侧是当前分类下的垃圾条目列表支持新增、编辑、删除、批量导入从Excel或JSON模板订单管理列表展示所有回收订单状态不同颜色标签区分。管理员点击接单按钮订单状态改为已接单点击完成状态改为已完成并触发积分发放用户管理查看用户列表和当前积分手动调整积分用于线下补偿场景数据看板用ECharts展示分类占比、回收趋势等图表管理端的页面不需要花哨重点在操作路径短一个订单从上到下处理完不需要超过三次点击。6. 部署调试实录本地跑通是开始部署上线才是真正的技术活6.1 本地开发环境搭建的完整顺序如果你跟着这个项目从零开始推荐安装顺序是JDK 1.8 → Maven → MySQL 8.0 → Node.js 16 → IDEIDEA旗舰版或社区版都行。顺序的重要性在于JDK和Maven是后端编译环境必须先配好MySQL要单独先启动IDE里才能连上测试Node和Vue CLI是前端环境依赖npm下载速度建议配好淘宝镜像源。npm config set registry https://registry.npmmirror.com安装MySQL 8.0时最坑的是默认时区问题。启动Spring Boot项目后如果数据库连上了但时间字段报错或者中文乱码大概率是连接串里没加时区参数和utf8配置。jdbc:mysql://localhost:3306/garbage?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai6.2 前端反向代理本地联调解决跨域的标准姿势开发环境下Vue跑在8080端口Spring Boot跑在8081端口或8080改了端口两者端口不一致浏览器的同源策略就会拦截跨域请求。最标准的方案不是每个控制台都去浏览器装插件而是在vue.config.js里配置代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端代码里请求/api/garbage/search经过代理转发到后端的/garbage/search浏览器看到的始终是同源的8080跨域问题完全消解。把这段配置和原理写进你的部署文档是能独立解决问题的证明。6.3 打包jar后部署Linux服务器从普通打包到注册成系统服务本地跑通后教学意义上的部署其实才刚刚开始。后端打包前要确保一件事application.yml里的数据库地址改成生产环境的IPdev环境的localhost在生产环境不会自动变成服务器自己的内网IP。# 后端打包 mvn clean package -DskipTests # 打出来的jar在 target/ 目录下 # 前端打包 npm run build # 生成 dist/ 目录里面是纯静态文件部署结构我用的是最常见的方案Spring Boot jar直接放服务器用nohup启动日志重定向到文件Vue打包后的dist目录作为静态文件交给Nginx托管Nginx配置路径首页映射到dist目录/api开头的请求反向代理到后端端口这个方案有个好处生产环境不需要解决跨域问题因为前端和后端在同一个Nginx后面所有请求都是同源的/api路径Nginx负责转发。我用的Nginx配置核心片段server { listen 80; server_name your-domain.com; # Vue 静态资源 root /opt/garbage/dist; index index.html; # 前端路由history模式刷新不404的关键配置 location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; } }注意上面location /里的try_files $uri $uri/ /index.html;——不写这行Vue的history路由模式在用户刷新当前路径时比如/order/list会直接404因为服务器上根本没有那个物理文件。这个问题在本地npm run dev时不存在dev server自带fallback一部署就暴露必须提前就在配置里写明白。6.4 部署后必走的冒烟测试清单部署完成后不要急着撒手按下面顺序自测一遍访问首页能正常加载静态资源和登录页注册一个新用户走一遍登录流程搜索塑料瓶确认联想和分类结果正常提交一条预约回收订单确认数据库表有记录管理员登录后台接单、完成订单确认用户积分增加、积分流水表有记录在某个二级路由页面按F5刷新确认不404这套清单也是你写测试报告章节的素材比空泛的系统测试通过有说服力得多。6.5 常见部署报错与排查思路汇总我把自己遇到过的和帮别人排查过的高频问题整理成一张表可以直接抄问题现象 根本原因 解决方案 后端启动报数据库连接失败 连接串的IP/密码/端口配置错误 检查application.yml用mysql客户端连一下 中文乱码 JDBC连接串缺少characterEncodingutf8 连接串补全参数 前端刷新404 Nginx缺try_files配置 补上location /的try_files 上传图片/文件失败 Nginx的client_max_body_size默认1MB 加client_max_body_size 20m 跨域请求被拦截 Nginx代理配置问题 检查proxy_pass末尾的斜杠是否会改变路径 打包后前端资源路径不对 Vue的publicPath配置错误 vue.config.js里设置publicPath: ./ 或绝对路径7. 公开展示与讲解思路源码之外的价值体现在哪里7.1 讲解框架用一个故事把项目串起来当你拿着这个源码文档去演示时别一上来就展示登录注册、增删改查那会把自己讲成普通CRUD程序员。我建议按这个顺序讲痛点导入30秒讲清为什么要做垃圾分类回收系统引用一些垃圾分类数据唤起听众认同业务流程贯穿讲一个完整故事——作为居民我搜索快递纸箱确认可回收预约上门回收管理员接单回收员上门取走我的账户增加积分积分兑换垃圾袋——让听众直观感受系统闭环核心技术亮点把前面四个核心设计表分层、状态机、事务处理、防抖搜索每个用两分钟讲透现场演示按故事线操作一边操作一边提示背后的逻辑部署与运维展示Nginx配置、jar包的启动命令、日志排错方法证明这不是只能运行在本机的玩具7.2 答辩/面试中最容易被追问的十个问题把这些问题提前准备答案能极大提高展示时的从容度为什么选这个课题——结合政策、社区痛点、个人兴趣数据库为什么这样设计——讲清楚每张表的职责和关联关系订单状态怎么管理——讲状态机设计积分发放怎么保证不重复——事务唯一约束垃圾搜索是怎么实现的——讲SQL顺序和索引优化权限是怎么控制的——JWT拦截器路由守卫三级防护如果用户量变大了哪里会成为瓶颈——数据库查询压力如何加缓存、分库分表前端路由的history模式和hash模式有什么区别——部署时为什么刷新会404部署在什么服务器上——云服务器LinuxNginxjar这个系统还能怎么扩展——小程序端、智能识别拍照分类、对接真实的回收企业7.3 项目的可扩展方向让它从课设变成一个能落地的产品如果时间充裕我强烈建议你至少加一个扩展功能因为这让你的项目在同类作品中立刻脱颖而出加一个小程序/H5端或者直接用Vue做响应式适配成移动端风格引入百度AI垃圾分类接口用户拍照识别垃圾这属于一个真正的AI应用场景对接真实的快递回收箱/智能分类终端当然是模拟数据但架构要先预留好接口增加数据可视化大屏把运营数据投到管理大厅的LED屏上我个人最推荐的是拍照识别垃圾因为这是最有话题度也最容易讲清楚的功能前端上传图片后端调用AI识别服务返回垃圾名称和分类。不需要训练模型调用现成的API就行但呈现出的效果是这个项目有AI能力综合印象分拉满。8. 写在最后的几点实在建议保证每次重启后都能自动恢复运行。提示如果服务器内存只有2G建议给JVM设置-Xmx512m否则jar启动后内存占用过大会触发OOM Killer表现为进程莫名其妙消失。日志里看不出明显异常但dmesg能看到内核杀进程的记录。这类经验你在文档里写一段比干巴巴写系统部署在Linux服务器上有价值得多。从技术难度上来讲这个项目并不算难它真正的价值在于完整闭环需求分析、数据库设计、后端API、前端交互、部署上线、讲解演示从0到1走一遍你就掌握了独立交付一个中小型Web系统的全部实践能力。这个能力是你在学校里对着书本学不到但是出来找工作或者自己接项目时最常被考验的底层能力。拿着这套系统当起点后续无论转向Spring Cloud微服务、中间件方向还是转向前端工程化心里都有一条完整业务长什么样的基准线这对后续学习的指导意义可能比项目本身包含的知识点还要大。