SpringBoot2+Vue3+MyBatis-Plus垃圾分类管理系统开发实战复盘

发布时间:2026/10/6 17:25:36
SpringBoot2+Vue3+MyBatis-Plus垃圾分类管理系统开发实战复盘 前阵子在社区里帮朋友做了一套城市垃圾分类管理系统技术栈选的是 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 rootlocalhost 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 从工程结构到通用 CRUD3.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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse 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 里我加了 serverTimezoneAsia/Shanghai 和 useSSLfalse。不加 timezone 后端插入时间会差 8 个小时这个八小时时区问题几乎是每个做 MySQL 的人都会遇到一次。useSSLfalse 是因为本地开发环境没有配置 SSL 证书不加它启动时一堆警告日志刷屏。3.2 基于 MyBatis-Plus 的通用 CRUD 到底怎么写之前看到过一段描述说 MyBatis-Plus 提供的通用 CRUD 服务是“基于工具类实现无状态增删改查”实际用起来就是这个感觉。对单表来说你不需要写一行 SQL只需要定义实体类然后让 Mapper 继承 BaseMapperService 继承 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 BaseMapperWasteCategory { }Service 接口和实现类也一样简洁public interface WasteCategoryService extends IServiceWasteCategory { } Service public class WasteCategoryServiceImpl extends ServiceImplWasteCategoryMapper, 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 实现LambdaQueryWrapperWasteItem wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), WasteItem::getName, keyword) .eq(WasteItem::getStatus, 1); IPageWasteItem 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 vitelatest waste-web -- --template vue cd waste-web npm install接下来按模块装依赖。UI 组件库用的 Element Plus状态管理用的 PiniaVue3 里 Vuex 已经不是最佳选择了路由用的是 Vue Router 4HTTP 请求用的 axios。地图展示我接的是高德地图 JSAPI按投放点经纬度初始化地图然后批量打点即可这个对接逻辑不算复杂。装依赖的时候提醒一句Element Plus 全量引入虽然打包体积大一些但开发省心。如果按需引入需要额外配置 unplugin-auto-import 和 unplugin-vue-components 插件配置不当组件样式会丢失。我项目里为了稳妥直接用了全量引入演示和课设场景完全够用。4.2 状态管理和接口封装告别到处写 fetchVue3 开发管理系统如果把用户信息、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 21 前端打包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 未设置 useSSLfalse连接串加 useSSLfalse后端时间比实际早/晚 8 小时未设置时区或设置不一致URL 加 serverTimezoneAsia/ShanghaiMySQL 认证失败 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: GMT8加完之后前后端传参和显示就完全一致不会再出现前端看到一堆数字的情况。6. 从这套系统还能延伸到哪些方向做完这套系统之后我最大的体会是一个完整的管理系统真正的难点不在单点技术而在把这些技术有机组合起来保证每个环节都能正常运行。垃圾类别管理让 MyBatis-Plus 帮你省事地图打点考验组件的生命周期处理积分流水让你重新审视数据模型设计部署上线又逼着你去理解 Nginx 和网络问题。这套系统做完你对 SpringBoot2 和 Vue3 的理解会进入另一个层次。如果你想继续扩展我建议优先做这几个方向第一对接 uni-app 写一个微信小程序版本地图打点和垃圾查询逻辑基本可以复用一套代码多端运行第二把积分商城扩展成完整的 签到 任务 兑换 闭环加入定时任务统计每日分类数据第三给后端接入 Redis 做缓存热点垃圾类别查询不走数据库性能能明显提升第四用 WebSocket 做实时通知反馈处理完成后主动推送消息给居民端而不是让用户反复刷新页面。我实际做这套系统时个人最深刻的教训就是不要小看数据库设计更不要为了省事把积分这种东西直接存一个字段。前期多花点时间把表结构、中间表、流水表设计合理后面代码少写一半bug 少一大半。这套系统核心围绕“分类数据 投放点 积分激励”打转把这几个链路跑顺了后续加什么功能都顺手。