尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot2+Vue3全栈垃圾分类管理系统:从权限设计到积分闭环实战

SpringBoot2+Vue3全栈垃圾分类管理系统:从权限设计到积分闭环实战 先说结论这是一套能直接跑、能讲清业务闭环、也能拆开来自学的 Java Web 全栈管理系统。后端基于 SpringBoot2 MyBatis-Plus MySQL8.0前端是 Vue3 Vite Element Plus源码里还带了数据库脚本和设计文档。城市垃圾分类这个话题听起来很“政务”但真正落到系统上其实就是“信息管理 流程审批 积分激励”三件事非常适合拿来练全栈基本功。什么人适合看这篇三类人一是准备毕业设计、课程设计的学生刚拿到同类源码不知道怎么验收二是初级 Java 开发想补全前后端联动、数据库设计、权限控制这些实际能力三是想自己从头搭一个管理系统的朋友哪怕不写垃圾分类后面讲的设计思路和踩坑记录也能直接迁移到别的业务上。我会按自己实际重构这套项目的顺序来写业务拆解、模块设计、数据库和接口实现、前端要点、以及每一步真正容易翻车的地方包括我在 MySQL8.0、MyBatis-Plus、Vue3 三个环节里踩过的坑。1. 这个垃圾分类管理系统到底做了什么1.1 业务原型不是“给垃圾桶拍照”而是“让管理闭环”我最早接触这类项目时也以为核心是图像识别判断垃圾是什么类别。真做起来才发现市区里落地的一套系统重点根本不在识别算法而在于三个环节教居民正确分类、记录居民真实参与、用积分把参与固化下来。看不懂的都能用知识库去查投了垃圾的要有记录和审核参与之后要有积分、兑换、排名这样居民才愿意持续使用。所以整个系统被拆成三大块居民端垃圾分类查询、投放登记、个人积分流水、积分商城兑换。社区端投放点管理、投放记录审核、本社区参与率统计。管理端用户管理、分类知识库维护、积分规则配置、全量数据看板。这套划分对应了垃圾分类落地的三个痛点“不会分”用知识库解决“不愿分”用积分和排行榜解决“不好管”用社区维度的审核和数据看板解决。做项目答辩或者面试时能把业务痛点和技术功能对应起来讲比单纯背功能列表有用得多。1.2 一套源码三端角色的权限设计权限上我没有做得很复杂但保持了标准的“菜单权限 数据权限”两层超级管理员拥有全部菜单权限能看到所有社区的数据。社区管理员只能管理自己社区的投放点、审核自己社区的投放记录。居民用户只能看到自己的积分、自己的记录。菜单权限本质是“用户有哪些按钮能点”数据权限本质是“查询的时候 SQL 自动带上哪些过滤条件”。后者很多人叫它行级权限意思是不同用户查同一张表返回的行不一样。比如社区管理员查投放记录时Service 层会自动追加community_id 当前管理员所属社区这个条件这是系统里比较容易被忽略但很能体现设计水平的地方。后面讲 MyBatis-Plus 实现时我会给具体代码。1.3 为什么偏偏是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0技术栈不是拍脑袋选的每个选择背后都有原因技术选它的原因实际限定条件SpringBoot 2.7.x生态稳定JDK8 即可运行教程和问题资料最多不必追 3.xBoot3 对 JDK17 和部分 starter 有兼容要求Vue3 ViteComposition API 更适合中后台业务复用Vite 启动快注意script setup别再写 Vue2 老语法MyBatis-Plus单表 CRUD 零 SQLLambdaQueryWrapper 写条件很顺手别忘了装分页插件后面有坑MySQL8.0utf8mb4、窗口函数、JSON 支持都比 5.7 好连接串参数必须写对否则连不上我重构这个项目时特意没有换 SpringBoot3不是因为新的不好而是 3.x 要求 JDK17MyBatis-Plus 相关的 starter 和部分兼容性插件在老版本上踩坑成本高。这类偏向“快速交付一套可复现系统”的项目稳定组合比最新组合更值钱。2. 核心业务模块拆解从分类知识库到积分闭环2.1 四分类知识库搜索不是甩一条 like 完事垃圾分类知识库是整个系统里使用频率最高的模块。居民打开小程序或后台页面输入“电池”“果皮”“外卖盒”要立刻知道属于哪一类。国内生活垃圾普遍是四分法可回收物、有害垃圾、厨余垃圾、其他垃圾。数据库里我建了两张表waste_type存大类的名称和图标waste_item存具体物品。去做这个设计的时候最关键的细节在别名字段。举例物品名称别名用户可能会搜所属大类锂电池电池、充电宝、纽扣电池有害垃圾剩菜剩饭剩饭、菜叶、骨头厨余垃圾纸箱快递盒、纸壳可回收物陶瓷碎片碗、花瓶碎片其他垃圾用户输入“快递盒”时name匹配不到但alias里存了“快递盒”一样能查出来。搜索 SQL 用 MyBatis-Plus 写是这样public ListWasteItem search(String keyword) { if (StringUtils.isBlank(keyword)) { return new ArrayList(); } return baseMapper.selectList(new LambdaQueryWrapperWasteItem() .and(w - w.like(WasteItem::getName, keyword) .or() .like(WasteItem::getAlias, keyword)) .eq(WasteItem::getStatus, 1) .last(limit 20)); }这里有个新手极易犯的错like和or如果不包进.and(...)里很容易和后面的eq(status, 1)错乱拼接成name like ? or alias like ? and status 1导致禁用物品也被查出来。所以多条件里的or一定要用括号包住LambdaQueryWrapper 里对应的就是.and包裹。2.2 投放登记与积分防刷、幂等、事务缺一不可知识库解决“不会分”积分模块解决“不愿分”。业务流是这样的居民在投放点把垃圾投进对应垃圾桶然后在系统登记一条投放记录选择垃圾大类、拍照或填重量社区管理员审核通过后系统给居民加积分积分可以在商城兑换垃圾袋、日用品。后端设计上积分模块有三个关键点幂等一条投放记录只能加一次积分。我建了point_log表并对(user_id, record_id)加了唯一索引即使审核接口被重复调用第二次插入会因为唯一键冲突而失败。防刷同一用户每天投递次数、每次最大积分上限都配置在数据库里由后台动态调整不写死在代码里。事务审核通过 更新delivery_record状态 写入point_log 累加用户积分三件事必须在同一个数据库事务里完成否则会出现“状态变了但积分没加”的脏数据。接口层我用Transactional把整个方法包裹住任何一步失败都会回滚。表面看这只是三行注解实际面试时把“为什么必须事务”讲透就是加分项。2.3 数据看板与社区排名聚合查询比循环统计快一个量级管理后台需要展示总投放量、各分类占比、各社区参与率、近七日变化趋势这类统计。很多初学者会把明细数据全查出来然后在 Java 里循环计数这种做法数据量小时没问题量一大就废了。正确做法是让数据库直接完成聚合。比如查各类别投放占比SELECT waste_type_id, COUNT(*) AS total FROM delivery_record WHERE create_time #{startTime} GROUP BY waste_type_id;要用 MyBatis-Plus 的QueryWrapper写select(waste_type_id, count(*) as total).groupBy(waste_type_id)就能拿到结果列表再转换成前端要的结构。社区排名则是按community_id分组按参与人数倒序。前端用 ECharts 画饼图和柱状图后端只出聚合数据网络传输量小、前端渲染也快。这也是我在项目里比较满意的一个点宁可 SQL 多写一行也不要在内存里做无谓的循环。3. 数据库设计与后端落地MySQL8.0 表结构 MyBatis-Plus 实操3.1 MySQL8.0 表结构把所有“枚举”做成表别写死在代码里有人喜欢把垃圾大类直接写成一个 Java 枚举但这样后台什么都改不了。我更倾向于把这类“业务枚举”做成表一行数据一个大类后台可以增删改。核心表一共八张sys_user、sys_role、waste_type、waste_item、community、delivery_point、delivery_record、point_log外加exchange_product和exchange_order完成积分闭环。关键表结构以投放记录为例CREATE TABLE delivery_record ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, community_id bigint DEFAULT NULL COMMENT 所属社区ID, point_id bigint DEFAULT NULL COMMENT 投放点ID, waste_type_id bigint NOT NULL COMMENT 垃圾大类ID, weight_kg decimal(6,2) DEFAULT 0 COMMENT 登记重量(kg), verify_status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, point int NOT NULL DEFAULT 0 COMMENT 本次获得积分, remark varchar(255) DEFAULT NULL, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_community (community_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投放记录;设计这套表时有几个通用的原则换个业务也一样适用所有表都用utf8mb4否则用户留言里的 emoji 会变成乱码。金额、重量这类字段用decimal别用float和double避免精度漂移。每张表加create_time重要的表加update_time审计和排查问题都靠它。常用查询条件都建索引比如user_id、community_id但不要盲目给所有列加索引写入会变慢。point_log表的唯一索引尤其关键CREATE TABLE point_log ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, record_id bigint NOT NULL COMMENT 投放记录ID, point int NOT NULL COMMENT 变动积分, change_type tinyint(1) NOT NULL DEFAULT 1 COMMENT 1获得 2扣除, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_record (user_id, record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT积分流水;3.2 SpringBoot2 工程的关键配置连接串里的每个参数都有用我拿到这类项目源码的第一件事永远是检查application.yml。MySQL8.0 和旧版本在连接方式上有区别连接串写不对后端起得再快也白搭。一份能跑起来的配置长这样spring: datasource: url: jdbc:mysql://localhost:3306/city_waste?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 hikari: maximum-pool-size: 10 minimum-idle: 2这里的参数逐个说清楚characterEncodingutf8保证中文传输不乱码配合数据库的utf8mb4才能完整支持中文和特殊字符。serverTimezoneAsia/Shanghai不写这个MySQL8 经常报“The server time zone value is unrecognized”这类时区错误。useSSLfalse本地开发不需要 SSL 加密链路少一层握手更快。allowPublicKeyRetrievaltrueMySQL8 默认认证插件是caching_sha2_password一些版本连接时会提示 “Public Key Retrieval is not allowed”加上这个参数能直接绕开。pom.xml 里最核心的依赖就是三个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java。MyBatis-Plus 我用的 3.5.x 版本对应的 MySQL 驱动版本 8.0.33 以上注意别混用 5.x 的驱动驱动换了类名都不同。3.3 通用 CRUD 与行级权限用 MyBatis-Plus 把代码量减一半MyBatis-Plus 最大的价值是单表 CRUD 几乎不用写 SQL实体类继承Model或者 service 继承ServiceImpl之后save、getById、list、page全都现成。我甚至不写传统 Controller 里的增删改查而是直接复用ServiceImpl提供的方法比如public interface WasteTypeService extends IServiceWasteType { } Service public class WasteTypeServiceImpl extends ServiceImplWasteTypeMapper, WasteType implements WasteTypeService { }然后 Controller 里直接调用wasteTypeService.list()、wasteTypeService.getById(id)、wasteTypeService.saveOrUpdate(entity)。不是所有模块都要建一堆自定义方法基础增删改查就这样几行搞定。如果你用的是 MyBatis-Plus 3.5.4 以上版本还有一个Db静态工具类可以直接在任意 Service 里静态调用完成无状态 CRUDimport com.baomidou.mybatisplus.extension.toolkit.Db; ListUser users Db.list(new LambdaQueryWrapperUser() .eq(User::getCommunityId, currentCommunityId));这个Db工具类没有内部状态不需要注入任何东西特别适合在工具类、监听器、非 Spring 管理的代码里使用。行级权限我也是用的类似思路public ListDeliveryRecord pageForCurrentUser(PageDeliveryRecord page, Long currentUserId) { LambdaQueryWrapperDeliveryRecord wrapper new LambdaQueryWrapper(); // 假设通过上下文得到当前用户的社区ID Long communityId SecurityContextHolder.getCommunityId(); if (communityId ! null) { wrapper.eq(DeliveryRecord::getCommunityId, communityId); } else { // 超级管理员不加过滤条件 } return this.page(page, wrapper).getRecords(); }关于分页还有一个必须做的事注册分页插件。这个很容易忘忘了之后调用page方法不会报错但会把全表数据查出来自己分页性能直接崩。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }4. Vue3 前端实现要点Composition API 的工程化组织4.1 Composition API 和 Options API实战到底怎么选前端我一开始也纠结过要不要用 Options API毕竟教程多、习惯熟悉。后来重构成script setup Composition API原因是这个项目里功能模块多按业务把逻辑拆到自定义 hooks 里代码复用度高很多。举个最直接的对比用户信息和积分的获取逻辑Options API 要写进createdComposition API 里写成一个usePoints函数哪个页面需要就 import 哪个。基本页面结构我会严格执行“视图和逻辑分离”src/ ├── api/ # 后端接口封装 ├── stores/ # Pinia 状态管理 ├── router/ # 路由配置 ├── views/ # 页面级组件 ├── components/ # 通用组件 ├── utils/request.js # axios 封装 └── hooks/ # 业务逻辑复用页面里最常见的写法就是 ref 接口调用 表格绑定const list refRecordItem[]([]) const loading ref(false) async function getList() { loading.value true try { const res await getRecordList({ pageNum: 1, pageSize: 10 }) list.value res.rows } finally { loading.value false } }这里有一个 Vue3 的经典坑很多从 Vue2 转过来的人都会踩reactive包裹的数组如果直接整体赋值会丢失响应式。也就是const state reactive({ list: [] })然后写state.list res.rows页面不会重新渲染。正确做法是给list单独用ref或者用Object.assign(state.list, res.rows)。我在项目里统一用ref管理列表数据避免这种隐性问题。4.2 登录态、路由守卫与按钮级权限的整套方案前端登录态我用 Pinia 管理核心字段是 token 和用户信息。登录成功后把 token 存到 localStorage同时写入 Pinia state。axios 请求拦截器统一在请求头里带 tokenrequest.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config })路由守卫负责拦截未登录访问router.beforeEach((to) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { return { path: /login } } })按钮级权限我用的是自定义指令比如v-permissionwaste:item:add没有权限的按钮直接移除。这样后端接口有权限校验前端页面也同步隐藏入口双保险。4.3 前后端联调代理、拦截器与接口约定开发环境我强烈建议用 Vite 代理绕开跨域不要在后端代码里写全局 CORS 放行。后端的 CORS 配置在本地开发能用一旦上线部署前后端不同域名时会冒出各种安全问题。vite.config.js里配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/wasteItem/list代理会自动转发到http://localhost:8080/api/wasteItem/list。前提是后端 Controller 的 mapping 也保持/api前缀两边的约定一致后面联调会省很多事。响应拦截器里我统一处理业务码后端所有接口返回{ code, msg, data }这种统一结构前端只要code 200就取数据401 统一跳转登录页。约定大于配置这个原则前后端都遵守项目维护起来会舒服很多。5. 踩过的坑与排查实录MySQL8.0 / MyBatis-Plus / Vue35.1 MySQL8.0 从安装到连接一份问题速查表我在 Windows 上装过 MySQL8也用 Docker 拉过镜像。日常开发用 Docker 最快一条命令搞定docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0启动容器后再进容器执行 SQL 脚本docker exec -it mysql8 mysql -uroot -p123456如果电脑上已经装了 MySQL 5.7再来一个 8.0 容器会提示端口占用把宿主端口改成3307映射即可连接串同步改端口。数据库导入时我统一先建库再指定字符集CREATE DATABASE IF NOT EXISTS city_waste DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;MySQL8.0 的坑我整理成一张速查表基本覆盖能遇到的九成场景报错现象原因解决方式Access denied for user rootlocalhost密码错或认证插件问题检查密码确认 root 允许本地连接Public Key Retrieval is not allowedcaching_sha2_password 认证URL 加allowPublicKeyRetrievaltrueThe server time zone value is unrecognized时区未配置URL 加serverTimezoneAsia/ShanghaiCommunications link failureMySQL 服务没启动或端口不对docker ps看容器状态检查 3306/3307 映射Unknown character set utf8mb4驱动版本太旧升级 mysql-connector-java 到 8.x中文乱码连接串缺编码参数URL 加characterEncodingutf8确认库表字符集5.2 MyBatis-Plus 常见翻车点分页、逻辑删除、条件拼接MyBatis-Plus 好用但默认行为不熟悉会踩几个大坑。第一个就是分页。我前面已经强调过分页插件必须注册这里再说一个细节PaginationInnerInterceptor的数据库类型要按实际数据库设置我写的是DbType.MYSQL。如果你复制别人的代码没改类型分页方言可能不对导致 limit 语法异常。第二个是逻辑删除和唯一索引的坑。我给大部分表加了deleted字段做逻辑删除然后在实际业务字段上建了唯一索引比如用户表的用户名。逻辑删除只是把deleted置 1但 MySQL 唯一索引还是会把已删除的旧数据也算进去导致你重新创建一个同名用户时直接报唯一键冲突。我的处理方案是要么唯一索引在业务上不接受改名回收要么用deleted这个值来参与唯一约束比如唯一索引改成(username, deleted)删除时把deleted设为自增唯一值。第三个是条件拼接里的or错乱这个在 2.1 已经提过属于高频问题排查方法也很简单打印 MyBatis-Plus 生成的 SQL看到WHERE子句括号位置不对基本都是or没包好。还有一个容易忽略的点是自动填充。如果想让create_time、update_time自动写入需要实现MetaObjectHandlerComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }同时在实体字段上标注TableField(fill FieldFill.INSERT) private LocalDateTime createTime;不配置这个你会发现几乎所有实体表的时间字段都是 null后面统计按天分组时会出尽洋相。5.3 Vue3 开发高频坑响应式丢失、表单校验与路由 404Vue3 的坑比 Vue2 更隐蔽我挑三个最影响上手的。响应式丢失。除了reactive数组整体赋值的问题还有props解构问题。script setup里如果直接写const { visible } props那这个visible就失去了响应性父组件改值子组件没反应。处理方式是const { visible } toRefs(props)或者统一用props.visible读取。很多人用 Vue3 的 TypeScript 模板项目时遇到“值更新了但视图不刷新”八成是这个原因。表单校验不生效。Element Plus 里写el-form :modelform :rulesrules refformRef el-form-item label分类名称 proptypeName el-input v-modelform.typeName / /el-form-item /el-form校验半天不弹提示最常见的三个原因prop没写、v-model绑定的字段在form对象里没初始化、或者rules里的字段名和prop不一致。Element Plus 要求form对象先有字段哪怕初始值为空字符串都行否则它找不到校验目标。打包后刷新 404。Vue Router 默认用 history 模式打包后直接放到 nginx刷新一个二级路由页面大概率 404。原因很简单服务器不知道history模式下的路径要回退到index.html。处理方式有两种后端部署时配置location / { try_files $uri $uri/ /index.html; }或者干脆把路由改成 hash 模式。如果项目是给学校演示用、不追求美观的 URLhash 模式省很多事。6. 源码到手后的第一步不是运行而是“验收”我每次拿到一套源码第一件事不是急着npm run dev或者启动后端而是先过一遍交付物清单这套项目的标准验收流程值得直接抄作业检查配套文档里是否包含数据库脚本和需求说明缺 SQL 脚本的项目基本没法完整跑通。确认 MySQL 版本。脚本里如果用了 MySQL8 的语法而你本地是 5.7建表大概率报错。建库导数据启动后端。后端启动日志里看到Tomcat started on port(s): 8080再停这之前不用碰前端。前端npm install如果报 node-sass 这类老依赖错误把依赖换成sass或sass-embedded顺便检查本机 Node 版本和 Vite 是否匹配。用三个角色账号分别登录一遍重点看社区管理员的“数据范围”是否真的被限制住。这套跑下来不光是验证“能动”更是逼自己把每个环节的为什么讲清楚。我重构时最大的体会就是这项目表面是垃圾分类管理系统实际上考验的是事务、索引、权限、接口约定这些通用后端能力。你把积分幂等和行级权限这两块吃透再换一个“社区团购管理系统”“校园二手交易系统”的皮核心代码都能平移过去因为它们的底子就是一套同样的用户、订单、积分、统计模型。最后再分享一个小技巧接手旧代码前先去target/generated-sources或汇编结果里看一遍 MyBatis 的 Mapper XML 和实体类注解比在业务代码里逐行摸排快得多。很多源码运行不动不是业务代码有 bug而是环境变量、数据库连接串、驱动版本三件套里有一个没对齐。把这三件事按顺序排查完剩下的事基本都是你熟悉业务和扩展功能的时间了。
返回列表