这两年只要打开招聘网站的后台,就能看到“垃圾分类回收”相关的系统需求一直没断过。无论是学校里的课程设计、毕业设计,还是社区、园区、校园里的真实回收运营项目,背后几乎长一个样:一个用来给居民下单预约回收的前端,一个给回收员、管理员处理订单的后端,中间再连一套数据库和文件存储。这类项目的技术栈高度集中在两个词上:SpringBoot 和 Vue。
我这次想聊的,就是基于 SpringBoot + Vue 做的一个垃圾分类回收网站。它不是那种只写“欢迎页+登录+增删改查”的演示项目,而是真正把这套业务常见的问题都碰了一遍:垃圾品类怎么维护、预约订单的状态怎么流转、图片怎么传、前端路由怎么搭、项目怎么打包部署。文章里会穿插我实际开发时的取舍、踩过的坑,以及一些直接可以“抄作业”的代码和配置。
不管你是准备拿它当毕设,还是真的想给某个小区做一套回收系统,这篇内容都值得看完。我会尽量把关键设计讲透——不是简单把代码贴一遍,而是告诉你每一步为什么要这么做。
1. 业务场景与功能模块设计
1.1 核心需求解析:垃圾分类回收网站解决什么问题
先想清楚一个事:这种网站到底是给谁用的,解决什么痛点。很多新手上来就写代码,结果功能堆了一堆,但用户路径完全说不通。
垃圾分类回收网站的核心场景其实是这样的:居民手里有废纸、塑料瓶、旧家电、旧衣服,不想直接扔掉,或者社区要求分类投放但没有专人管理。这时候需要一个平台,让居民在线预约,回收员接单上门,或者居民自己拿到回收点,通过系统登记、称重、结算。整个过程涉及三类人:居民(前端用户)、回收员(接单执行者)、管理员(系统运营者)。
所以功能模块天然分成两端:
- 前端用户端(Vue 页面):垃圾分类查询、在线预约回收、订单查看、积分或金额结算、个人中心。
- 后台管理端(SpringBoot 接口 + 管理页面):垃圾类别管理、预约订单管理、回收员管理、用户管理、公告管理、数据统计。
我做这套系统的时候,没有一上来就写代码,而是先列了一张问题清单:居民怎么知道哪个垃圾属于哪类?答案需要一个“垃圾词典”库,支持输入关键词查询。预约回收怎么提交?答案需要一个表单:地址、电话、垃圾类型、期望时间。后台怎么知道订单到了哪一步?答案需要一个订单状态机:待接单、已接单、已上门、已完成、已取消。
这些问题清单就是整个项目的骨架。骨架对了,后面填代码只是时间问题。
1.2 用户角色与核心流程梳理
系统里最常见的角色有四个:普通用户、回收员、管理员,还有一个容易被忽略的——访客。我建议把访客也考虑进来,因为垃圾查询的功能应该允许游客先用,降低使用门槛,等真要下单时再引导注册登录。
核心流程我梳理成两条主线:
预约回收主线:用户登录 → 选择垃圾类别 → 填地址和时间 → 提交订单 → 管理员或回收员接单 → 上门回收 → 确认完成 → 用户获得积分/金额。
垃圾分类查询主线:用户输入垃圾名称 → 系统返回分类结果 → 如果结果不准确,用户反馈 → 管理员后台维护词库。
这两条线一个偏交易流程,一个偏知识库查询,技术上关注的侧重点不一样。前者考验的是订单状态管理和数据一致性,后者考验的是检索效率和词库的设计。我自己在开发时先用状态机把预约主线走通,再回头补分类查询的细节,这样主流程能尽早跑起来调试。
1.3 页面与接口清单规划:先画页面再写代码
很多新手容易犯一个错误——直接打开 IDEA 开始建实体类。我的习惯是先在纸上画一遍页面流转:首页、分类查询页、预约表单页、订单列表页、订单详情页、个人中心页、后台管理各子页面。每画一个页面,就在旁边写下这个页面需要哪些接口。
这一步做完之后,接口清单基本就出来了:
- 首页:轮播图列表、热门品类、回收价格公示
- 分类查询:关键词搜索、分类列表、反馈提交
- 预约订单:创建订单、订单支付(可选)、订单列表、订单详情、取消订单
- 用户中心:登录注册、个人信息、积分记录
- 管理后台:订单分派、品类管理、词库管理、用户管理、统计报表
我实际开发中把接口设计成了 RESTful 风格,统一返回Result<T>包装对象,里面包含 code、message、data 三个字段。这样做的好处是前端 axios 拦截器可以统一处理错误码,不需要每个接口单独写异常判断。
2. 技术选型与系统架构
2.1 为什么是 SpringBoot + Vue,而不是其他方案
聊到技术选型,我得说说我为什么每次都推荐 SpringBoot + Vue,而不是 Spring Cloud、或者用 Thymeleaf 做服务端渲染,再或者用 React。
SpringBoot 的价值在于“约定大于配置”。对一个单体管理网站来说,它内置了 Tomcat、自动配置了数据源、整合了 MyBatis-Plus 之后连基本 CRUD 的 SQL 都不用手写,非常适合快速交付。这不是说别的框架不行,而是这个业务场景下,SpringBoot 能最大程度减少“环境折腾”,让你把精力花在业务逻辑上。
Vue 这边,最大的优势是组件化和响应式。回收网站的页面虽然多,但很多是“列表+表单+详情”的变体,用 Vue 组件抽出来之后,复用率非常高。比如订单卡片组件,在用户订单页、回收员接单页、后台管理页都能用,只是数据来源和操作按钮不同。如果用 Thymeleaf 服务端渲染,这部分的复用成本会高很多。
如果你问我是不是一定非它们不可,我的答案是不一定。但如果你想要一个学习资料多、招人容易、社区问答丰富、踩坑还能搜到解决方案的栈,SpringBoot + Vue 确实是目前最稳妥的答案。我自己面试时也经常被问到这个组合的考点,后面专门有一节讲。
2.2 核心依赖与组件选择(MySQL、Redis、MinIO、MyBatis-Plus)
选完大框架,就是选具体组件。我先把这套系统用到的核心依赖列出来,都是实际验证过的组合。
| 组件 | 版本建议 | 作用 |
|---|---|---|
| Spring Boot | 2.7.x | 基础框架,别一上来用 3.x,会有很多兼容坑 |
| MyBatis-Plus | 3.5.x | 简化 CRUD,分页查询很顺手 |
| MySQL | 5.7 或 8.0 | 主数据库,存业务数据 |
| Redis | 任意稳定版 | 缓存分类词库、存验证码、JWT 黑名单 |
| MinIO | 8.5.x | 图片文件存储,替代本地磁盘存储 |
| JWT | jjwt 0.11.x | 登录令牌,保持会话状态 |
| Hutool | 最新版 | 工具类库,生成验证码、Bean 拷贝 |
为什么要用 MinIO?因为垃圾分类网站里一定有图片场景:用户上传垃圾照片、管理员上传轮播图、回收员上传称重凭证。如果直接存本地磁盘,项目一迁移数据就丢,而且 Nginx 暴露静态目录也不够规范。MinIO 是开源的对象存储方案,接口兼容 S3,部署一个实例大概两分钟,开发环境用起来比阿里云 OSS 更灵活,而且不用绑银行卡。
Redis 在这套系统里不是必须的,但加了之后体验好很多。比如分类词库,几千条数据如果每次搜索都打 MySQL,高峰期会有点顶不住。把热门搜索词和查询结果缓存到 Redis,TTL 设置成 30 分钟,接口响应能从 200ms 降到 20ms 以内。另外验证码、短信验证码(如果有)也是 Redis 的经典场景。
2.3 前后端分离架构与接口通信机制
前后端分离的核心是:前端工程和后端工程完全独立,后端只提供 JSON 接口,前端只负责页面渲染和数据交互。开发时前端跑在自己的开发服务器上(默认 8080),通过 Vite 的代理把/api开头的请求转发到后端(端口 8081)。生产环境则把前端打包成静态文件,由 Nginx 托管,反向代理到后端服务。
接口通信机制里最需要注意的是跨域问题。开发环境下 Vite 代理能解决,但生产环境如果 Nginx 配置不对,前端静态页面请求后端接口时会直接报 CORS 错误。我在项目里同时做了两手准备:Nginx 层配置/api/反向代理,后端也配置了全局跨域过滤器。顺便说一句,后端配置allowedOriginPatterns("*")时要配合allowedMethods和allowedHeaders一起写,否则预检请求(OPTIONS)会被拦下来。
还有一个细节:后端接口统一加/api前缀,前端所有 axios 请求的 baseURL 都是/api。这样以后如果要拆微服务,网关层完全可以按前缀做转发,不需要改前端代码。
3. 数据库设计与核心表结构
3.1 五大核心表的设计思路
数据库是整个系统最不能省功夫的部分。我用的表不算多,但每一张都经过了几轮调整。整套系统最核心的五张表是:
- user:用户表,存放居民、回收员、管理员账号,用 role 字段区分。
- garbage_category:垃圾类别表,比如可回收物、有害垃圾、厨余垃圾、其他垃圾。
- garbage_item:垃圾词条表,对应具体垃圾名称,比如“矿泉水瓶”、“旧书本”,带 category_id 外键。
- recycle_order:预约订单表,记录谁、在哪、约了什么时间、什么垃圾、什么状态。
- recycle_apply(可选):回收员接单申请表,有些系统直接把接单人写在订单表里,看你的流程复杂度。
表与表之间的关系不算复杂,基本上是用户一对多订单、类别一对多词条。真正考验设计的是订单表字段的完整性,我后面单独讲。
用户表的 role 字段我用的是 Integer 类型,0 表示普通用户,1 表示回收员,2 表示管理员。这样做比字符串枚举更省空间,查询也快。密码字段存的是 BCrypt 加密后的字符串,长度 60 左右,所以字段类型用 varchar(100) 留足余量。
3.2 表字段细节与索引设计
关键表字段我直接列出来,附带类型和注释,你们做参考的时候可以直接拿去用。
用户表(user):
- id:bigint 主键,自增
- username:varchar(50) 唯一索引
- password:varchar(100) BCrypt 加密
- phone:varchar(20)
- role:int 默认 0
- points:int 积分
- create_time:datetime
词条表(garbage_item):
- id:bigint
- name:varchar(100),普通索引
- category_id:bigint,普通索引
- status:int(1 正常 0 停用)
- remark:varchar(255) 备注
这里有个心得:name字段一定要加索引,垃圾分类查询是系统里访问频率最高的接口,全表扫描在词条数量过万之后会明显变慢。另外category_id加索引是因为后台管理列表需要按类别筛选词条。
订单表(recycle_order)字段要细说,这是整个项目里最重要的设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号,唯一索引 |
| user_id | bigint | 下单用户 |
| category_id | bigint | 垃圾类别 |
| address | varchar(255) | 回收地址 |
| contact_name | varchar(50) | 联系人 |
| contact_phone | varchar(20) | 联系电话 |
| appoint_time | datetime | 预约时间 |
| worker_id | bigint | 接单回收员 |
| status | int | 0 待接单 1 已接单 2 已完成 3 已取消 |
| weight | decimal(10,2) | 实际重量,可以为空 |
| amount | decimal(10,2) | 结算金额 |
| remark | varchar(255) | 备注 |
| create_time | datetime | 下单时间 |
订单号不能直接用自增 id,原因很简单:你不想让用户通过订单号猜测平台每天的单量。我推荐生成规则:日期 + 6 位随机数,比如20250612103025这种格式,加唯一索引防止并发冲突。
3.3 垃圾分类词库设计的特别之处
垃圾分类网站和普通商城网站最大的区别,在于它有一个知识图谱性质的词库。同一个东西叫法可能完全不同,比如“矿泉水瓶”有人搜“塑料瓶”,有人搜“饮料瓶”,还有人搜“PET瓶”。如果只做简单的精确匹配,那这个查询接口基本是废的。
我的方案是三层结构:第一层精确匹配,第二层模糊查询(LIKE),第三层就靠管理员后台维护同义词了。比如词条表里再加一个alias字段,存同义词组,用逗号分隔。查询时先看name匹配,再看alias匹配,都没有就返回一个“未识别”的分类,引导用户反馈。
另一个细节是:词条要支持“可回收”“不可回收”这种用户口语化的状态描述。所以在garbage_category表里我加了code和description字段,前端展示时除了名称还能显示分类说明,比如“可回收物——包括适宜回收利用和资源化利用的生活废弃物”。这对用户理解分类规则很有帮助。
4. 核心功能实现与关键技术点
4.1 垃圾分类查询接口的实现(含 HanLP 分词方案)
垃圾分类查询接口是我觉得最有意思的一块,因为它不完全是“查数据库”,还牵涉到用户输入的真实场景。
最简单的实现方式:一个接口接收关键词,先用garbage_item表做精确匹配,没有结果再走alias模糊匹配。但这种方式在遇到“旧衣服能不能回收”这种句子时会直接失败,因为用户输入的不是词,而是一句话。
如果想把查询做得更智能,可以引入 HanLP 分词。热词里也提到过“hanlp分词在springboot”,这个方案非常适合用到当前场景。具体做法是:用户输入一句话 → HanLP 分词提取名词 → 拿关键词去词库匹配。比如“旧衣服能不能回收”分完词后提取出“旧衣服”,再去词库匹配就成功了。
引入 HanLP 需要注意一个问题:它的词典加载比较耗时,第一次调用可能有一秒延迟。我的解决办法是项目启动时把分词器初始化成单例,提前热身调用一次,后续请求延迟就能降到几十毫秒。另外 HanLP 的依赖体积不小,如果用不到完整功能,可以只用标准分词包,别把所有的模型都引进来。
接口返回结构我设计成了:
{ "code": 200, "data": { "name": "旧衣服", "category": "可回收物", "description": "适宜回收利用和资源化利用的生活废弃物", "tips": "请保持衣物干燥整洁,避免污染" } }这个结构简单清晰,前端可以直接展示卡片。如果匹配不到,category 返回“未识别”,同时把用户输入记录下来,方便管理员后台扩充词库。
4.2 预约回收订单流程与状态机设计
预约订单是整个网站的交易核心,状态流转我花了不少时间设计,因为这里面有一个很容易被忽略的问题:回收员接单后的“已上门”状态要不要单独存在。
我的最终方案是四状态:待接单(0)→ 已接单(1)→ 已完成(2),外加一个已取消(3)。上门回收和称重结算都发生在“已接单”状态里,通过子操作区分,这样状态机不至于过于复杂,对前端展示也友好。用户能看到的就是“正在等师傅接单”“师傅已接单,预计今天上门”“订单完成,到账 3.5 元”这种清晰的提示。
状态机的流转控制我放在 Service 层,不允许直接 UPDATE status。每个操作对应一个方法:acceptOrder()、completeOrder()、cancelOrder()。每个方法里先校验当前状态是否合法,再执行更新。这样做的好处是,以后就算前端把状态参数改了,后台也不会被带偏,因为非法状态流转会被拦截。
这里还有一个并发问题:同一笔订单如果两个回收员同时点“接单”,会怎样?如果不做控制,可能就是两个人都接单成功,实际只有一个能上门,体验非常差。我的方案是用乐观锁,在订单表加一个version字段,UPDATE ... SET status=1, version=version+1 WHERE id=? AND version=?。更新结果影响行数为 0 说明已经被抢了。
4.3 Vue 前端关键页面与路由设计
前端这块,我用 Vue 3 + Vite + Element Plus 搭的。Vue 3 的组合式 API 配合<script setup>写起来很顺手,Element Plus 的组件库也让后台管理页面开发快了不少。
路由设计上,用了动态路由的思路。传统写法是把所有路由写死在router/index.js里,但这套系统有前端用户和管理后台两大块,权限不同,路由应该分开。我的方案是:前端用户部分用静态路由,管理后台部分用动态路由。用户登录后,后端根据角色返回菜单权限,前端用router.addRoute()动态添加路由。
一个实操细节:动态路由添加后,直接刷新页面会遇到“路由匹配不到”的白屏问题。原因很简单,刷新时 Vue 重新执行了路由初始化,但动态路由还没来得及加载。解决办法是在路由守卫里加一个“路由是否已动态添加”的标记,每次刷新检查一次,没有就重新请求后端菜单数据并添加路由,再next({ ...to, replace: true })。
页面组件方面,我抽了三个核心组件:OrderCard.vue(订单卡片)、CategorySelect.vue(分类选择器)、SearchBar.vue(搜索栏)。这三个组件在首页、预约页、订单页都有使用。组件之间数据传递用 props 和 emit,比较复杂的状态引入 Pinia 管理,比如用户登录信息、购物车(如果有积分商城的话)。
4.4 MinIO 图片上传与访问路径处理
文件上传这块,我的路径是:前端选图片 → axios 传给后端 → 后端转存到 MinIO → 拿到访问 URL → 把 URL 存到数据库。MinIO 的接口代码不长,但有几个坑必须提醒你。
第一个坑是桶的权限。如果桶是 private,前端通过 URL 直接访问图片会被拒绝。垃圾分类网站的图片不算特别敏感,我一般把桶设置成 public,方便前端直接引用。如果你担心安全问题,可以改用 private + 预签名 URL 的方式,但是预签名 URL 有效期要处理好,不然过一段时间图片就裂了。
第二个坑是文件名。直接用原始文件名上传会有两个问题:文件名可能存在中文或特殊字符,导致 URL 解析出错;同名文件会互相覆盖。我的做法是用 UUID 重新生成文件名,保留原文件扩展名。比如fw8d3k9s.jpg。
第三个坑是上传大小限制。SpringBoot 默认单文件上传大小是 1MB,如果你上传的垃圾照片动不动就 3-5MB,会被直接拦截。需要在配置文件里调大:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MBMinIO 的 Java 客户端核心代码大致是这个样子:
@Autowired private MinioClient minioClient; public String upload(MultipartFile file, String bucket) { String filename = UUID.randomUUID().toString().replace("-", "") + "." + FilenameUtils.getExtension(file.getOriginalFilename()); // 检查桶是否存在 boolean exists = minioClient.bucketExists( BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { minioClient.makeBucket( MakeBucketArgs.builder().bucket(bucket).build()); } minioClient.putObject( PutObjectArgs.builder() .bucket(bucket) .object(filename) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucket) .object(filename) .build()); }我实际生产用的不是预签名 URL 而是直接拼接http://ip:9000/bucket/filename,因为桶是 public 的。
4.5 积分与结算模块设计要点
积分不是垃圾分类网站的核心必需功能,但我建议加,因为它是用户黏性的来源。我的设计很简单:预约回收完成后,系统根据重量计算积分,比如 1 公斤可回收物 = 10 积分。积分记录单独建表points_record,存user_id、change_type(增加/减少)、change_value、reason、create_time。
这里有个数据一致性问题:订单完成时既要改订单状态,又要给用户加积分,两步操作如果分开执行,中间服务挂了,用户就白回收了。我当时是在同一个 Service 方法里用@Transactional注解包起来的,两个操作放在一个事务里,任何一个失败都会整体回滚。
如果订单里涉及金额结算(比如废纸按斤计价),那就还要考虑支付环节。我的方案是先不接入真实支付,而是采用“账户余额”的形式。用户订单完成后金额进入余额,可以提现或下次消费抵扣。这样既可以把流程完整走通,又避免了繁琐的支付对接。
5. 前后端联调与部署上线
5.1 Vue 打包与 SpringBoot 集成部署的两种方式
项目做完之后总要部署给别人看。Vue 和 SpringBoot 的部署方式有两种,我都实际用过,这里把区别讲清楚。
方式一:完全分离部署。前端打包后扔给 Nginx,后端 jar 包单独跑在某个端口。Nginx 配置里把/api/反向代理到后端地址。这种方式的好处是前后端互不影响,后端重启时前端还在跑。公司里正规项目基本都是这么干的。
方式二:Vue 打包后放进 SpringBoot。把前端构建生成的 dist 目录复制到 SpringBoot 的src/main/resources/static/下,后端启动时直接访问 8080 端口就能看到前端页面。这种方式适合个人项目快速演示,一个人一台服务器搞定。
我个人更推荐方式一,因为热词里也提到了“vue打包放进springboot中”,但我想说这种方案有坑:后端接口路径和前端页面路由会冲突。如果你用 Vue Router 的 history 模式,刷新页面时 SpringBoot 会把/order/detail当成后端接口处理,返回 404。解决方法是后端加一个控制器,把不匹配的路径转发到 index.html。而 Nginx 就不用担心这个问题,因为它有try_files指令。
Nginx 配置核心片段:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里注意一个细节:proxy_pass http://127.0.0.1:8081/结尾的斜杠很关键,它会自动去掉前缀/api,把请求转发到后端对应的接口路径。如果这个斜杠忘了写,后端收到的 URL 会带上/api,容易 404。
5.2 Docker 部署的配置与实战
如果你不想在服务器上手动装 JDK、MySQL、Redis、MinIO,用 Docker Compose 一把梭是最高效的。我直接分享一套我用的 docker-compose 配置,包含前端 Nginx、后端 jar、MySQL、Redis、MinIO 五个容器。
version: '3.8' services: mysql: image: mysql:8.0 container_name: recycle-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: recycle_db ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:7-alpine container_name: recycle-redis ports: - "6379:6379" minio: image: minio/minio container_name: recycle-minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 ports: - "9000:9000" - "9001:9001" volumes: - ./minio-data:/data backend: image: openjdk:11-jre-slim container_name: recycle-backend depends_on: - mysql - redis - minio volumes: - ./app.jar:/app/app.jar ports: - "8081:8081" command: ["java", "-jar", "/app/app.jar"] frontend: image: nginx:alpine container_name: recycle-frontend depends_on: - backend volumes: - ./dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf ports: - "80:80"这里提醒一下:depends_on只是控制启动顺序,不能保证 MySQL 已经初始化完成。后端启动时如果连不上 MySQL 会直接报错退出,解决办法是加一个健康检查,或者在启动命令里加--spring.datasource.url等参数配合重试策略。我实际用的方案是等 30 秒再起后端容器,省事但有用。
5.3 Maven 构建与多环境配置文件管理
后端打包用的是 Maven,命令很简单:
mvn clean package -DskipTests但我建议你把环境配置拆开。开发环境连本地 MySQL,生产环境连 Docker 里的 MySQL,地址和账号都不一样。SpringBoot 支持application.yml+ 多环境 profile,我分了三个:
application-dev.yml:本地开发配置application-prod.yml:生产配置application.yml:公共配置
启动时通过参数指定环境:
java -jar app.jar --spring.profiles.active=prod这里有个非常重要的坑:如果你的配置里有数据库密码、MinIO 密码,千万别提交到 Git 仓库。就算项目是私有仓库,也养成好习惯,用${DB_PASSWORD}这类环境变量占位,部署时在 Docker 容器里注入真实值。
5.4 前端部署后的浏览器缓存 문제
这个不算 bug,但特别折腾人。前端打包后,文件名带了 hash 值,比如index.b3f2a.js,按理说浏览器会自动加载新文件。但有些用户会发现“页面还是旧的”,这是因为浏览器缓存的 index.html 没有及时刷新。
解决办法是 Nginx 对 index.html 设置不缓存:
location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; }同时给带 hash 的静态资源设置长缓存,这样既能保证页面及时更新,又能让静态资源充分利用缓存加速。
6. 常见问题排查与技术面试要点
6.1 从热词看高频开发难题:跨域、端口、热更新
我搜了下目前关于 SpringBoot 和 Vue 的高频搜索词,发现大家最容易卡住的点集中在这几个地方。
跨域问题。前后端分离开发时必遇。前端报错Access-Control-Allow-Origin,或者浏览器 console 里红色的 CORS 提示。解决办法就是在后端加全局配置类实现WebMvcConfigurer,重写addCorsMappings方法。记住要允许 OPTIONS 请求。
端口冲突。SpringBoot 默认 8080,Vue 开发服务器默认 5173。但如果 8080 被别的进程占了,启动直接报错。解决办法是换端口,但前后端代理路径也要同步改。我习惯把后端端口固定为 8081,前端 Vite 代理指向它,这样默认 8080 可以留给其他项目用。
前端热更新失效。Vite 项目改完代码页面不刷新,大概率是server.host配置问题,或者代理配置正则匹配有问题。如果你用了局域网 IP 访问,要把server.host改成0.0.0.0,而且 Vite 的 proxy 配置里changeOrigin要设成true。
后端热更新。SpringBoot 改了代码要重启这件事确实烦。热词里提到“springboot thymeleaf 热更新”,虽然我们不用 Thymeleaf,但可以借助 DevTools 实现:引入spring-boot-devtools依赖,修改代码后 IDEA 会自动重启应用,接口调试效率会提升很多。配合 IDEA 的Build → Build Project快捷键,几秒钟就能完成一次重启。
6.2 数据库与 Redis 常见问题:连接不上、缓存穿透
MySQL 连接不上,最笨但最快的方法:先命令行测telnet 127.0.0.1 3306,端口通不通。然后看看配置文件里的database名是不是提前建好了,SpringBoot 不会自动建库。如果用的是 Docker 容器,还要确认端口映射正确。
缓存穿透问题。垃圾词库用 Redis 做缓存后,如果有人恶意刷一条不存在的词,比如“外星人垃圾”,每次请求都会穿透到 MySQL。解决办法有两个:第一,对空结果也缓存,TTL 设置 60 秒;第二,用布隆过滤器拦截不存在的 key,但那个对你这套系统来说有点重了,空值缓存就够用。
缓存与数据库数据不一致。后台管理员修改了某条垃圾词条,但 Redis 里旧数据还在有效期。解决办法是管理员修改词条时主动删除 Redis 对应 key,下次查询自然回源加载最新数据。
6.3 SpringBoot 自动装配与 Vue 路由高频面试题梳理
用这套项目做毕设或者面试项目的话,有几道高频题你肯定会碰上,我直接梳理一下回答思路。
“SpringBoot 自动装配的原理是什么?”
回答思路:SpringBoot 通过@EnableAutoConfiguration注解启动自动装配,核心是spring.factories文件里注册的自动配置类。每个自动配置类用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解判断生效时机。比如 RedisAutoConfiguration,当 classpath 下存在 RedisTemplate 相关类,并且用户没有自定义连接工厂时,才会自动创建配置。面试时能举一个具体的自动配置类例子,就会显得调研过源码。
“Vue Router 的动态路由怎么实现?”
回答思路:核心是router.addRoute()方法。用户登录后,前端拿到后端返回的菜单或权限数据,遍历后逐条调用addRoute加入路由表。同时配合路由守卫beforeEach验证 token,如果当前路由不在静态表中且动态路由没注册完,就先去获取菜单数据再放行。这道题我上面也提到了,多数人答不到“刷新后动态路由丢失”这个坑,你能说出来就加分。
“项目里见过哪些设计模式?”
这道题是我面试时很喜欢反问的。垃圾分类项目里其实用到了不少:Service 层用模板方法模式统一处理订单状态流转的前置校验;策略模式可以用来处理不同垃圾类型的积分计算规则;外观模式体现在 Controller 层统一调 Service,不让 Controller 直接操作多个组件。回答这类问题不要背理论,直接讲“我在订单状态的积分计算里用了策略模式,因为有四类垃圾,计分规则不同,我用一个 Map 把策略类注入进去,新增品类不用改老代码”,面试官一听就懂你是有实战经验的。
7. 写在最后:一点实在的建议
做这个垃圾分类回收网站,说起来技术点并不复杂,但真正把它做成一个“完整可用”的系统,远比想象中琐碎。设计词库、画状态机、调跨域、配 Nginx、解决刷新白屏和浏览器缓存,每一件单独拿出来都不难,但全部串起来才是一个真实的项目。
我自己做的时候感触最深的一点是:刚开始很容易陷入“想把所有功能做得很全”的冲动,但实际开发中一定要先走通主链路——用户注册、提交订单、回收员接单、完成结算,这条线通了,其他功能都是在这个骨架上生长出来的。如果一上来就研究积分商城、数据大屏、智能识别,项目很可能卡在中途就推不动了。
如果你正打算做类似的系统,我的建议是:先花一个下午把页面流转和数据库表设计出来,再动手写代码。这个时间省不得,后面返工的成本会高出好几倍。当然,如果真的在某个环节卡住了,比如 Vue 动态路由刷新白屏、MinIO 上传图片签名过期、或者 SpringBoot 跨域配置无效,你大可以把报错信息记下来,静下心先判断是前端问题、后端问题还是环境问题。百分之八十的“疑难杂症”其实都能用一个简单的curl命令定位出来:先直接请求后端接口看通不通,再经过 Nginx 代理看通不通,一层层缩小范围,比满屏搜索高效得多。
我最后还想补充一个容易被忽略的小问题:如果你准备把项目部署到服务器上给导师或者客户演示,记得把 MySQL 的时区配置成Asia/Shanghai,否则订单里的create_time显示的时间会差 8 个小时。这个坑我踩过,不在某个深夜排查过的人不会懂。希望这些内容能帮你把项目从“能跑”做到“能看、能讲、能拿得出手”。