☰
SpringBoot2+Vue3+MyBatis-Plus垃圾分类管理系统开发实战复盘
2026/10/6 17:25:31 网站建设 项目流程

前阵子在社区里帮朋友做了一套城市垃圾分类管理系统,技术栈选的是 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,前后端分离,代码加文档全部整理好交付。说实话,垃圾分类管理这种系统看起来不复杂,真正动手才发现处处都是细节:垃圾分类怎么动态维护、投放点如何在地图上打点、积分规则怎么设计、后台数据怎么统计,再加上前端权限控制和后端接口配合,一套完整流程跑通并稳定上线,并不是简单把框架搭起来就完事的事情。

这篇就当作我自己的项目复盘帖,把我踩过的坑和最终落地的方案完整写一遍。如果你正准备做一个管理类系统的毕业设计、课设,或者想看看 SpringBoot2 + Vue3 + MyBatis-Plus 这套组合在实际业务中怎么配合,这篇文章应该能给你一套可以照抄的答案。里面所有核心实现、配置代码、数据库表设计,还有问题排查记录,我都会贴出来。

1. 垃圾分类系统到底要做成什么样

1.1 业务场景和用户角色梳理

垃圾分类管理系统的核心使用场景在街道、物业、社区和环卫部门。居民端需要知道某个垃圾属于哪一类、附近哪里有投放点、怎么参与分类获得激励;管理端需要维护垃圾类别信息、投放点信息、积分商品,查看居民反馈和各类统计数据。

所以在动手写代码之前,一定要先把角色理清楚。我把它分成两类用户:普通用户(居民)和系统管理员。居民通过前端页面查看分类知识、查询投放点、提交分类反馈、查看积分;管理员在后台维护所有基础数据、审核反馈、管理兑换商品、查看统计报表。如果后续要扩展上门回收业务,还可以增加回收员角色,但核心一期先做好这两类角色就足够支撑项目跑起来了。

1.2 为什么选 SpringBoot2 + Vue3 这套组合

技术选型是最容易纠结的地方。写这套系统前我对比过几个方案:SpringBoot3 + Vue3、SpringBoot2 + Vue2,甚至考虑过若依这种现成脚手架改一改。

最终我坚持用了 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,理由很实在。第一,SpringBoot2.x 目前在企业项目里存量最大,JDK8 支持完美,各种中间件兼容性文档丰富,遇到问题基本都能搜到答案;SpringBoot3 虽然新,但强制 JDK17,很多老项目依赖和数据库驱动的兼容问题会多一些,对业务系统来说没有必要冒这个险。第二,很多高校和企业面试环节重点问的也是 SpringBoot2,做这套系统拿来学习和面试复习一鱼两吃。第三,Vue3 已经是当前前端主流,组合式 API 写业务代码比 Vue2 选项式更直观,配合 Vite 开发体验也好很多。

MyBatis-Plus 就更不用说了,主流的通用 CRUD 方案,几乎不用写 SQL 就能完成单表操作,项目里基础增删改查全走 ServiceImpl + BaseMapper,省下来的时间全投入到业务逻辑上,效率翻倍。MySQL8.0 是当前主流版本,支持窗口函数、JSON 字段这些新特性,团队里新人也更容易上手。

1.3 功能模块一共拆成几块

先把需求拉成一张模块清单,开发的时候按模块切分,前后端对应着做,心里才有底。

模块用户端功能管理端功能
分类管理查看垃圾所属分类、按关键词查询垃圾类别维护、垃圾条目 CRUD
投放点管理地图查看投放点、地址详情投放点新增、坐标维护、状态管理
积分系统查看积分明细、参与分类反馈积分规则设置、积分审核、兑换商品管理
反馈管理提交文字/图片反馈反馈列表查看、处理状态更新
数据统计个人分类行为统计分类比例、投放点覆盖、反馈趋势图表

这套模块划分覆盖了垃圾分类业务从“居民分类”到“后台运营”的完整闭环。实际写代码时,我会把系统拆成 user、category、point、points、feedback、statistics 几个业务包,每个包内做 controller、service、mapper、entity 四层,逻辑清晰一眼就能看懂。

2. 数据库设计:MySQL8.0 下的表结构与关键细节

2.1 核心表设计一览

数据库是整个系统最不能将就的部分。分类、投放点、积分、反馈这四个业务域我分别设计了一组表。下面这几张是实际项目里跑起来的核心表。

用户表 sys_user,主要存用户名、密码、昵称、手机号、头像、积分余额、状态字段。密码不存明文,用 BCrypt 加密后存储。垃圾类别表 waste_category,字段包括类别名(可回收物、有害垃圾、厨余垃圾、其他垃圾)、图标、颜色标识、排序号、状态。垃圾明细表 waste_item 是核心表,存具体垃圾名称、所属类别 ID、分类依据说明、投放建议,用户搜索垃圾时主要查这张表。

投放点表 recycle_point 存投放点名称、地址、经纬度、负责人、联系电话、开放时间段、状态。积分记录表 points_record 存用户 ID、变动积分、变动类型(签到、反馈奖励、兑换扣减)、关联业务 ID、创建时间。兑换商品表 points_goods 存商品名称、所需积分、库存、状态。兑换记录表 exchange_record 存用户 ID、商品 ID、兑换积分、收货信息、兑换时间。反馈表 feedback 存用户 ID、垃圾类型、描述、图片地址、处理状态。

2.2 MySQL8.0 建表需要注意的三个点

MySQL8.0 建表时我踩过不少坑,这里专门说一下。字符集一定要在建库时指定 utf8mb4,不然默认字符集在 Linux 环境下可能是 latin1,存中文直接乱码。建库语句我用的是:

CREATE DATABASE waste_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

timestamp 和 datetime 的选择上,业务表我统一用 datetime,因为涉及积分、反馈这些时间记录,datetime 的范围足够用,不涉及时区换算,显示也更直观。如果涉及多时区业务才需要用 timestamp。

第三个要特别注意的地方是 MySQL8.0 的认证插件。我开发环境用 Navicat 链接时遇到过 1251 错误,这是因为 MySQL8.0 默认加密规则是 caching_sha2_password,老客户端不支持。解决办法有两个:要么升级客户端,要么在 MySQL 里执行下面这条:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码'; FLUSH PRIVILEGES;

我实际部署时为了方便维护,直接统一改成了 mysql_native_password,配合新版驱动 com.mysql.cj.jdbc.Driver,从没再出过认证问题。

2.3 积分和兑换业务为什么单独建表

很多新手会把积分余额直接存在用户表一个字段里,消费积分时直接 update 用户表。我第一版也是这么做的,测试一段时间后发现完全不行,因为一旦积分扣错了,根本没有历史记录可查,也不知道这笔积分是签到来的还是反馈奖励来的。

后来我改成积分流水表设计:用户表只冗余一个积分余额字段用于展示,所有积分变动全部落 points_record 表。查询用户积分总流水时直接查流水表,统计某段时间积分产出时也走这张表,数据可追溯性大大提高。这算是我做这个系统最大的重构之一,也符合真实业务系统的设计习惯。

3. 后端实现:SpringBoot2 + MyBatis-Plus 从工程结构到通用 CRUD

3.1 初始化 SpringBoot 工程和依赖配置

工程我直接用 Spring Initializr 生成,JDK 版本选的 1.8,打包方式 jar。核心依赖包括 spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok、hutool 工具包、jwt 认证相关依赖。MyBatis-Plus 我用的 3.5.3.1 版本,不要再往下用老版本,3.5.x 对 SpringBoot2 适配得很好,分页插件 API 也更规范。

配置文件里最重要的还是数据源和 MyBatis-Plus 的设置,实际效果很简单明了:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/waste_management?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root123 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

你看到 URL 里我加了 serverTimezone=Asia/Shanghai 和 useSSL=false。不加 timezone 后端插入时间会差 8 个小时,这个八小时时区问题几乎是每个做 MySQL 的人都会遇到一次。useSSL=false 是因为本地开发环境没有配置 SSL 证书,不加它启动时一堆警告日志刷屏。

3.2 基于 MyBatis-Plus 的通用 CRUD 到底怎么写

之前看到过一段描述,说 MyBatis-Plus 提供的通用 CRUD 服务是“基于工具类实现无状态增删改查”,实际用起来就是这个感觉。对单表来说,你不需要写一行 SQL,只需要定义实体类,然后让 Mapper 继承 BaseMapper,Service 继承 IService,实现类继承 ServiceImpl,增删改查接口就全部齐了。

以垃圾类别管理为例,核心代码量少得惊人:

@Data @TableName("waste_category") public class WasteCategory { @TableId(type = IdType.AUTO) private Long id; private String name; private String icon; private String color; private Integer sort; private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

Mapper 接口:

@Mapper public interface WasteCategoryMapper extends BaseMapper<WasteCategory> { }

Service 接口和实现类也一样简洁:

public interface WasteCategoryService extends IService<WasteCategory> { } @Service public class WasteCategoryServiceImpl extends ServiceImpl<WasteCategoryMapper, WasteCategory> implements WasteCategoryService { }

从 Controller 层调用的时候,新增、修改、分页查询、删除全都能直接走 IService 暴露的方法,单表业务几乎不用写 SQL。通用 CRUD 省下的时间,全花在业务规则上,这才是 MyBatis-Plus 存在的意义。

3.3 分页、自动填充和逻辑删除的细节配置

MyBatis-Plus 很多功能不是加了依赖就能用,必须在代码里显式配置。首先是分页插件。如果只引入依赖不配置 PaginationInnerInterceptor,调用 page 方法返回的数据里 total 是 0,我最初就栽在这里。完整配置类如下:

@Configuration @MapperScan("com.waste.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }

我同时还加了 OptimisticLockerInnerInterceptor,给积分流水表设了一个 version 字段。因为积分增减是高并发场景,两个请求同时改同一个用户积分,不加乐观锁版本号极容易把余额覆盖成旧值。加上之后,更新时自动带上 version 条件,失败就重试,数据一致性有了保障。

自动填充 createTime 和 updateTime 也建议统一处理。我写了一个 MetaObjectHandler 实现类,在 insert 和 update 时自动给实体类的 createTime、updateTime 注入值,这样每张表都不用手动维护时间字段。

逻辑删除我用了全局配置里的 logic-delete-field 配置,实体类里加一个 deleted 字段,删除操作自动变成 update 语句。这个对投放点、垃圾类别这种需要保留历史数据的表特别重要,用户误删后管理员还能恢复。

3.4 非单表查询:垃圾名称模糊搜索和投放点附近查询

虽然通用 CRUD 能覆盖大部分操作,但真正业务里还是有几个场景需要自定义 SQL。第一个场景是用户端垃圾查询。用户在搜索框输入“纸箱”“玻璃瓶”“过期药品”,后端要带分页、支持模糊查询、关联垃圾类别显示名称。我直接用 MyBatis-Plus 的 LambdaQueryWrapper 实现:

LambdaQueryWrapper<WasteItem> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), WasteItem::getName, keyword) .eq(WasteItem::getStatus, 1); IPage<WasteItem> page = wasteItemService.page(new Page<>(current, size), wrapper);

第二个场景是投放点按距离排序。MySQL8.0 可以直接用 ST_Distance_Sphere 函数计算经纬度距离,SQL 里直接按距离排序,省去后端逐条计算的开销:

SELECT id, name, address, longitude, latitude, ST_Distance_Sphere(point(longitude, latitude), point(#{lng}, #{lat})) AS distance FROM recycle_point WHERE status = 1 ORDER BY distance ASC LIMIT 0, 20;

这个函数返回的是米,配合前端地图打点,用户能直接看到离自己最近的 5 个投放点,体验比那种全量返回让前端算距离的方案好太多。

3.5 接口设计:统一返回结构 + JWT 登录认证

前端调用后端接口时,最忌讳每个接口返回结构不一样。我定义了一个 R 类统一返回 code、message、data。所有 Controller 返回类型都写成 R,这样前端 axios 拦截器只用判断 code 是否为 200,就能统一处理成功和失败。

登录认证这套系统我用的是 JWT。用户登录成功后,后端返回 token,前端存到 localStorage 并放入请求头。后端写一个 JwtInterceptor 拦截器,校验 token 中携带的用户 ID 是否正确、是否过期。管理员和普通用户的路由权限在前端控制,后端接口只做最基础的登录校验。如果要更严谨,可以在接口上增加 @RequireRole 注解做角色校验,这个后续扩展再加也不迟。

4. 前端实现:Vue3 + Vite + Element Plus 从搭项目到页面交互

4.1 Vue3 工程初始化和配套依赖选择

前端我用的 Vite 构建,创建命令很简单:

npm create vite@latest waste-web -- --template vue cd waste-web npm install

接下来按模块装依赖。UI 组件库用的 Element Plus,状态管理用的 Pinia(Vue3 里 Vuex 已经不是最佳选择了),路由用的是 Vue Router 4,HTTP 请求用的 axios。地图展示我接的是高德地图 JSAPI,按投放点经纬度初始化地图然后批量打点即可,这个对接逻辑不算复杂。

装依赖的时候提醒一句,Element Plus 全量引入虽然打包体积大一些,但开发省心。如果按需引入,需要额外配置 unplugin-auto-import 和 unplugin-vue-components 插件,配置不当组件样式会丢失。我项目里为了稳妥,直接用了全量引入,演示和课设场景完全够用。

4.2 状态管理和接口封装:告别到处写 fetch

Vue3 开发管理系统,如果把用户信息、token 散落在各个组件里,维护起来就是灾难。我统一放到 Pinia 的 store 里管理。store 里存了 token、用户信息、侧边栏折叠状态。登录成功后调接口拿到用户信息,setUser 存进 store;退出登录时清空 store 和 localStorage 里的数据。

axios 封装我单独成 request.js 文件,设置了 baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器里从 localStorage 取出 token 并放到 Authorization 头里;响应拦截器里统一处理后端返回的 code,如果不是 200,直接弹出 ElMessage 提示。这样每个页面调用接口时,两行代码就能拿到数据,不会出现一处一个样的问题。

4.3 几个核心页面:垃圾查询、地图打点、积分商城

垃圾查询页面是用户端使用频率最高的一页。顶部放搜索输入框,下面是分页列表,卡片展示垃圾名称、类别标识、分类依据说明。搜索时防抖处理,用户输入停顿 500 毫秒再发请求,避免每敲一个字母就请求一次后端。

投放点地图页用的是高德地图。页面初始化时通过 navigator.geolocation 获取浏览器定位,然后请求后端接口拿到投放点列表,循环用 AMap.Marker 打点。点开标记弹窗显示投放点名称、地址、开放时间。这里有个细节,地图组件初始化一定要等 DOM 渲染完成后再执行,我在 onMounted 里用了 nextTick,不然有时地图容器宽度是 0,地图渲染出来一片灰。

积分商城页面分两块:左侧展示用户当前积分余额,右侧是商品卡片列表。兑换按钮点击后调后端兑换接口,后端判断积分是否足够、库存是否充足,通过校验就扣积分、减库存、写兑换记录。前端收到成功结果后重新拉取用户积分和商品列表。

4.4 前端路由权限控制怎么做

路由我分成两类:公共路由和需要登录的路由。登录验证用路由守卫判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })

需要登录才能访问的页面在路由配置里加 meta: { requiresAuth: true }。管理员页面额外加 meta: { roles: ['admin'] },打开页面时校验当前用户的角色,没有权限直接跳转到首页并提示无权限。这套方案虽然不能防住真正的安全攻击,但对业务系统来说足够清晰、易扩展。

5. 部署联调与常见问题排查实录

5.1 本地开发和前端联调时的跨域问题

我在前端开发环境用的 Vite dev server,默认端口是 5173,后端跑在 8080,跨域问题根本绕不开。前端 vite.config.js 里配置一下 proxy 就能解决:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }

这样就相当于把前端发起的 /api 请求代理到后端,不带 CORS 配置也能联调。如果你非要用跨域而不是代理,那就在后端加一个 CorsFilter 允许所有来源,但生产环境建议用代理方式,更安全也更快。

5.2 上线部署环境搭建与打包配置

服务器环境我用的是最常规的方案:后端 jar 包 + Nginx 静态站点。先在服务器装好 JDK8 和 MySQL8,导入数据库脚本后修改数据源连接,然后打包:

mvn clean package -DskipTests nohup java -jar waste-server.jar > server.log 2>&1 &

前端打包:

npm run build

打包产物在 dist 目录,把它拷到 Nginx 的 html 目录下,再加一个反向代理指向后端接口:

server { listen 80; server_name yourdomain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

注意 try_files 这一行必须写。因为前端用的是 history 路由,不配 try_files 时刷新首页没问题,但刷新二级页面会直接 404,这是 Vue3 部署最经典的坑。

5.3 实际运行中的高频问题速查表

我把项目开发和部署过程中遇到的高频问题整理成了一张表,方便你以后排查。这些问题基本每个 SpringBoot2 + Vue3 项目都会撞见。

问题现象原因分析解决方案
MySQL 连接失败,报 SSL 错误URL 未设置 useSSL=false连接串加 useSSL=false
后端时间比实际早/晚 8 小时未设置时区或设置不一致URL 加 serverTimezone=Asia/Shanghai
MySQL 认证失败 1251MySQL8 默认认证插件不兼容改 mysql_native_password 或升级客户端
MyBatis-Plus 分页 total 为 0未配置分页插件注册 PaginationInnerInterceptor
前端打包后刷新 404未配置 try_filesNginx 加 try_files 回退
Vue3 响应式数据页面不更新用了赋值方式替换整个 reactive 对象改用 ref 或对对象逐字段赋值
前后端联调跨域请求失败端口不同未做代理Vite proxy 或后端加 CORS
上传图片后访问 404上传目录未做静态资源映射自定义 WebMvcConfigurer 映射上传路径

5.4 两个容易忽略的编码细节

Vue3 写业务时最常被坑的是响应式。reactive 对象如果用Object.assign或直接给整个对象赋值,响应式会丢失。解决办法是推荐用 ref 来存整体对象,取值时加 .value,修改时直接替换 value。我在写用户信息更新接口时就改了好几次才彻底弄清楚这个边界。

后端还有一个容易被忽略的点是 Jackson 时间序列化。默认情况下 LocalDateTime 会被序列化成一大串数组,前端接收后没法直接展示。我统一在配置文件里加了 Jackson 时间格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

加完之后前后端传参和显示就完全一致,不会再出现前端看到一堆数字的情况。

6. 从这套系统还能延伸到哪些方向

做完这套系统之后,我最大的体会是:一个完整的管理系统,真正的难点不在单点技术,而在把这些技术有机组合起来,保证每个环节都能正常运行。垃圾类别管理让 MyBatis-Plus 帮你省事,地图打点考验组件的生命周期处理,积分流水让你重新审视数据模型设计,部署上线又逼着你去理解 Nginx 和网络问题。这套系统做完,你对 SpringBoot2 和 Vue3 的理解会进入另一个层次。

如果你想继续扩展,我建议优先做这几个方向:第一,对接 uni-app 写一个微信小程序版本,地图打点和垃圾查询逻辑基本可以复用,一套代码多端运行;第二,把积分商城扩展成完整的 签到 + 任务 + 兑换 闭环,加入定时任务统计每日分类数据;第三,给后端接入 Redis 做缓存,热点垃圾类别查询不走数据库,性能能明显提升;第四,用 WebSocket 做实时通知,反馈处理完成后主动推送消息给居民端,而不是让用户反复刷新页面。

我实际做这套系统时,个人最深刻的教训就是:不要小看数据库设计,更不要为了省事把积分这种东西直接存一个字段。前期多花点时间把表结构、中间表、流水表设计合理,后面代码少写一半,bug 少一大半。这套系统核心围绕“分类数据 + 投放点 + 积分激励”打转,把这几个链路跑顺了,后续加什么功能都顺手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询