ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

社区智能垃圾管理系统:SpringBoot+Vue全栈设计与避坑指南

社区智能垃圾管理系统:SpringBoot+Vue全栈设计与避坑指南 简介一份基于SpringBoot与Vue前后端分离实现的社区智能垃圾管理系统源码面向准备毕业设计、课程设计或工程实训的Java全栈学习者。系统覆盖垃圾回收、分类监管等社区业务后端SpringBoot整合主流生态前端Vue.js构建交互界面配合MySQL5.7与配套SQL文件可快速运行。资源共797个文件、约40.02MB含186个Java后端逻辑、127个Vue前端组件、SQL初始化脚本及文档并附带bat一键启动脚本开发环境涉及JDK8、Tomcat7与Maven3.3.9。已有98人学习下载。整体结构清晰便于二次开发或扩展智能分类、数据统计等模块。1. 社区智能垃圾管理系统从“满溢报警”到“积分激励”SpringBootVue 能撑起什么社区智能垃圾管理系统听起来像给垃圾桶装个摄像头做自动识别但真正落地时它更像一套“运营管理后台 居民服务端”的多端业务系统社区管理员盯桶位状态、清运工单和分类数据居民则通过小程序或网页查分类、攒积分、兑换物品。很多团队一上来就钻研图像识别结果项目烂在了“积分规则”“桶状态判定”“数据统计”这些不起眼的逻辑上。这套系统用 SpringBoot 做后端服务Vue 做前端界面核心价值是把设备上报数据、居民投放行为和社区管理规则串成一条可运转的闭环。它适合做毕设、中小社区的智慧化改造也适合想快速验证“分类 奖励”模式的团队。下面按我从零搭这类系统的顺序讲不绕弯子直接说设计和踩坑。2. 先拆业务模型再写代码社区垃圾管理系统的表设计与接口边界2.1 核心实体垃圾桶、垃圾类型、投放记录、用户积分做管理系统的第一件事不是写 Controller而是画清楚谁在操作什么。社区智能垃圾管理系统里最少得有五个基础实体社区小区、垃圾桶点位、垃圾类型、投放记录、用户积分账户。如果涉及多社区运营还要再加一个“社区管理员”到“社区”的关联表。我一般这样建表字段能省则省但要留出扩展位。以下是系统里最核心的两张表一张管设备一张管居民行为CREATE TABLE trash_bin ( id bigint(20) NOT NULL AUTO_INCREMENT, community_id bigint(20) DEFAULT NULL COMMENT 社区ID, position varchar(64) DEFAULT NULL COMMENT 点位名称如东门3号, category varchar(16) DEFAULT NULL COMMENT 可回收/厨余/有害/其他, full_level int(11) DEFAULT 0 COMMENT 满溢百分比 0-100, status tinyint(4) DEFAULT 0 COMMENT 0正常 1满溢 2离线, last_report_time datetime DEFAULT NULL COMMENT 最近上报时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT垃圾桶点位表; CREATE TABLE recycle_record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, bin_id bigint(20) NOT NULL COMMENT 垃圾桶ID, category varchar(16) NOT NULL COMMENT 投放分类, weight decimal(10,2) DEFAULT 0.00 COMMENT 投放重量kg, score int(11) DEFAULT 0 COMMENT 本次获得积分, create_time datetime DEFAULT NULL COMMENT 创建时间, PRIMARY KEY (id), KEY idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投放记录表;这里full_level我建议存百分比而不是布尔“满/不满”。因为后面要做趋势图而且不同社区对“满”的阈值不一样有的 80% 就叫满有的 90% 才安排清运存具体百分比灵活得多。category直接用中文字符串比用数字字典更直白接口返回省一次翻译做数据报表时也方便排查。2.2 SpringBoot 项目结构怎么摆controller/service/mapper 与配置不少人照着教程把项目分成controller/service/mapper三层但做带设备上报的管理系统时还缺一个不可忽视的dto/vo层。我的标准结构是这样的src/main/java/com/community/garbage ├── config # 跨域、定时任务、Jackson 配置 ├── controller # REST 接口 ├── service # 业务逻辑、积分规则、设备上报处理 ├── dao # MyBatis-Plus Mapper ├── entity # 数据库实体 ├── dto # 入参对象比如设备上报 DTO └── vo # 返回给前端的对象比如垃圾桶状态 VO配置上我劝你不要用太高的 SpringBoot 版本。SpringBoot 3.x 把javax包换成了jakarta很多老教程和开源代码直接跑不起来尤其是 MyBatis-Plus 的旧版 starter 在 3.x 下经常出兼容问题。如果你照着网上的例子做先锁2.7.x最稳。pom.xml里这样写parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent这个版本的spring-boot-starter-web和mybatis-plus-boot-starter兼容性最好。application.yml里最要紧的三组配置是数据源、MyBatis-Plus 的下划线转驼峰、以及 Jackson 的时间格式。数据库字段是community_idJava 属性是communityId如果不开map-underscore-to-camel-case查询结果会把communityId映射成null这是新手最容易忽略的暗坑。2.3 用 MyBatis-Plus 生成基础 CRUD 的最小代码基础 CRUD 没什么技术含量直接用 MyBatis-Plus 的BaseMapper和ServiceImpl能省下大量手写 SQL 的时间。先定义实体类和 MapperData TableName(trash_bin) public class TrashBin { TableId(type IdType.AUTO) private Long id; private Long communityId; private String position; private String category; private Integer fullLevel; private Integer status; private LocalDateTime lastReportTime; }public interface TrashBinMapper extends BaseMapperTrashBin { }Service public class TrashBinService extends ServiceImplTrashBinMapper, TrashBin { public PageTrashBin pageByCommunity(Long communityId, int page, int size) { LambdaQueryWrapperTrashBin wrapper new LambdaQueryWrapper(); wrapper.eq(TrashBin::getCommunityId, communityId) .orderByDesc(TrashBin::getLastReportTime); return page(new Page(page, size), wrapper); } }LambdaQueryWrapper的好处是编译期能检查字段名避免手写community_id字符串写错。注意ServiceImpl里自带的page方法是无条件的所以这里要自己写一个带条件查询的方法。返回的Page对象里带total总数前端分页组件直接拿就行。Controller 层我给一个最小模板RestController RequestMapping(/api/bin) public class TrashBinController { Autowired private TrashBinService binService; GetMapping(/page) public ResultPageTrashBin page( RequestParam Long communityId, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { return Result.ok(binService.pageByCommunity(communityId, page, size)); } }Result是自己封装的统一返回体里面至少包含code、msg、data三个字段。前端 axios 拦截器只判断code比返回裸对象省事得多。RequestParam(defaultValue 1)一定要加不然前端漏传参数直接 400联调时容易误判成后端错误。3. 后端落地SpringBoot 里做满溢检测、积分规则与定时任务3.1 满溢状态判定用传感器数值还是手动上报先说结论不要真的依赖“智能识别”来做核心状态判断。市面上所谓智能垃圾桶大多通过超声波传感器测距换算成满溢百分比或者通过投递门计数估算。这套系统要对接真实设备最稳妥的做法是接受设备上报的原始值再在后端做判定和过滤。我给设备预留的接口是这样的PostMapping(/api/report) public Result? report(RequestBody DeviceReportDTO dto) { TrashBin bin binService.getById(dto.getBinId()); if (bin null) { return Result.error(bin not found); } Integer fullLevel calcFullLevel(dto.getSensorDistance(), bin.getHeight()); bin.setFullLevel(fullLevel); bin.setStatus(fullLevel fullThreshold ? 1 : 0); bin.setLastReportTime(LocalDateTime.now()); binService.updateById(bin); // 满溢时往消息表插一条待清运工单这里省略 return Result.ok(); }calcFullLevel的换算逻辑很简单(1 - distance / height) * 100。但注意设备上报可能会有瞬时跳变比如垃圾袋突然挡住传感器导致一秒钟内数值从 20% 跳到 95%。我会加一个简单的一阶滤波current last * 0.7 newValue * 0.3避免垃圾桶盖被掀动一下就直接报满。满溢阈值fullThreshold放到application.yml不要写死在代码里不同社区的标准不一样。3.2 积分规则引擎用策略模式代替 if-else积分是社区运营的重点不同垃圾类型给不同积分。可回收垃圾按重量计分厨余按次计分有害垃圾可能单次分高但限制每天只能投一次。如果直接在 Service 里写if (category.equals(可回收))规则多了会变成一团乱麻。我用策略模式来组织。先定义一个策略接口public interface ScoreStrategy { /** 对应垃圾类型 */ String category(); /** 计算本次积分 */ int score(RecycleRecord record); }再写两个具体策略Component public class RecyclableScoreStrategy implements ScoreStrategy { Override public String category() { return 可回收; } Override public int score(RecycleRecord record) { // 可回收按重量给分1kg 10 分 return (int) (record.getWeight() * 10); } } Component public class HazardousScoreStrategy implements ScoreStrategy { Override public String category() { return 有害; } Override public int score(RecycleRecord record) { // 有害垃圾不按重量按次给 20 分 return 20; } }然后在 Service 里把一个ListScoreStrategy转成Map让 Spring 注入所有策略实现Service public class ScoreService { private final MapString, ScoreStrategy strategyMap; public ScoreService(ListScoreStrategy strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap(ScoreStrategy::category, s - s)); } public int grant(RecycleRecord record) { ScoreStrategy strategy strategyMap.get(record.getCategory()); if (strategy null) { return 0; // 未知类型不积分 } return strategy.score(record); } }以后新增一个“玻璃制品”分类只要加一个Component类实现ScoreStrategyService 层不用改。要注意category()返回的 key 必须和数据库里存的值完全一致否则策略匹配不上直接返回 0用户会以为系统吞了积分。3.3 SpringBoot 定时任务扫描垃圾桶状态设备可能长时间不上报但满溢状态不能一直挂着旧值。我会用Scheduled定时扫描所有超过一定时间没上报的桶把它置为“离线”同时把满溢超过 12 小时的桶自动生成清运工单。下面是离线扫描的核心代码Component public class BinStatusTask { Autowired private TrashBinService binService; Scheduled(fixedDelay 60000, initialDelay 5000) public void scanOfflineBins() { ListTrashBin bins binService.list(); for (TrashBin bin : bins) { if (bin.getLastReportTime() null) { continue; } long mins ChronoUnit.MINUTES.between(bin.getLastReportTime(), LocalDateTime.now()); if (mins 30 bin.getStatus() ! 2) { binService.lambdaUpdate() .eq(TrashBin::getId, bin.getId()) .set(TrashBin::getStatus, 2) .update(); } } } }记得在启动类上加EnableScheduling这是一个特别容易漏的坑定时任务写好了但就是不执行控制台也没报错就是因为少了这个注解。fixedDelay 60000表示任务结束 60 秒后再跑下一次initialDelay 5000让服务先起来等 5 秒再跑第一轮防止启动期间数据库连接池还没就绪。这里的扫描逻辑比较简单桶数量超过几千个时建议改成 SQL 批量更新别在内存里全表循环。4. 前端落地Vue 3 Element Plus 搭出管理后台和居民端4.1 Vue 项目结构与路由配置动态路由怎么加前端我选用 Vue 3 Vite Element Plus用npm create vuelatest初始化。项目里src/views按角色拆目录避免管理端和居民端代码混在一起src/views/ ├── admin/ # 管理端垃圾桶管理、清运工单、数据报表 ├── resident/ # 居民端投递记录、积分商城 └── login/热词里经常有人搜“vue路由”和“vue动态路由”这个系统的权限模型很典型管理员和居民看到的菜单不一样所以不能全写死在router/index.js里。我的做法是登录后从后端拿菜单权限再动态addRoute// router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(/views/login/Index.vue) } ] }) export function setupAdminRoutes(router) { const adminRoutes [ { path: /admin, component: () import(/layouts/AdminLayout.vue), children: [ { path: bins, component: () import(/views/admin/BinList.vue) }, { path: orders, component: () import(/views/admin/OrderList.vue) }, { path: report, component: () import(/views/admin/Report.vue) } ] } ] adminRoutes.forEach(route router.addRoute(route)) }注意我用了createWebHistory()这是 HTML5 History 模式URL 干净但打包放到 SpringBoot 后会有刷新 404 的问题具体解决方案在第 5 章。如果不想处理那个问题可以临时改用createWebHashHistory()代价是 URL 里会多一个#。4.2 垃圾分类识别页调后端接口 本地缓存居民端的核心功能是输入垃圾名称或拍照让系统返回分类结果。我不建议真的在 Vue 里跑图像模型成本高且移动端兼容麻烦。常见做法是前端把垃圾名词发给后端后端用分词映射返回分类结果前端只负责展示。前端先封装 axios 实例统一处理请求路径和错误码// src/utils/request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.response.use( response { const res response.data // 后端 Result.code 约定 0 为成功 if (res.code ! 0) { return Promise.reject(new Error(res.msg)) } return res }, error Promise.reject(error) ) export default request然后在页面里调用async function checkGarbage(name) { const res await request.post(/garbage/classify, { name }) answer.value “${name}”属于 ${res.data.category}投递可获 ${res.data.score} 积分 }这里有个容易被忽略的点baseURL设成/api开发环境由 Vite 代理转发到localhost:8080生产环境 SpringBoot 的前端页面和后端接口同域就不存在跨域了。很多新手直接写死http://localhost:8080开发能通打包上线后 IP 或端口一变就废还要到处找替换。4.3 用 ECharts 做垃圾量趋势图管理报表最常用的图是“近 7 天各分类垃圾投放重量趋势”。ECharts 在 Vue 里可以配合vue-echarts使用也可以用原生 echarts 配合onMounted我习惯用原生因为可控性强也方便后续做图表联动。// src/views/admin/Report.vue import * as echarts from echarts import { onMounted, ref, onBeforeUnmount } from vue import request from /utils/request const chartRef ref(null) let chart null onMounted(async () { const res await request.get(/stats/weight-trend, { params: { days: 7 } }) chart echarts.init(chartRef.value) chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: res.data.dates }, yAxis: { type: value, name: 重量(kg) }, series: [{ name: 可回收, type: line, data: res.data.recyclable }, { name: 厨余, type: line, data: res.data.kitchenWaste }] }) }) onBeforeUnmount(() { if (chart) { chart.dispose() } })注意echarts.init(chartRef.value)必须等 DOM 渲染完成后再调用否则chartRef.value是null。如果页面用v-if控制图表容器还得配合nextTick。另外组件销毁时要chart.dispose()不然从管理报表页切到其他页面会内存泄漏时间长了页面会越来越卡。5. 避坑SpringBoot 版本、Vue 打包跨域、时间字段等 4 个常见问题5.1 现象后端接口通了前端却拿不到数据现象前端请求http://localhost:8080/api/bin/page浏览器 Network 里能看到后端返回了但 axios 报跨域错误或者 SpringBoot 控制台频繁出现OPTIONS请求且返回 403。原因浏览器跨域限制。前端地址是http://localhost:5173后端是http://localhost:8080端口不同。浏览器会先发一个OPTIONS预检请求后端没有允许跨域的响应头预检直接失败真实请求也不会发出。解决开发环境用 Vite 代理前端请求写/api在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }如果坚持要用后端解决就在 SpringBoot 里加一个全局CorsFilterConfiguration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }生产环境我建议去掉这个跨域配置让前后端同域部署少一层暴露面。这个坑几乎每个 SpringBoot Vue 项目都会遇到越早统一代理方案越省心。5.2 现象LocalDateTime 序列化出来是数组现象接口返回的createTime变成[2025, 5, 20, 14, 30, 0]数组前端拿到后没法直接展示日期。原因SpringBoot 默认用 Jackson 序列化。Jackson 对LocalDateTime默认处理的输出格式不是字符串而是对象或数组。不同版本表现不一致在 SpringBoot 2.7 下如果不做配置很容易出现数组。解决在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但要注意spring.jackson.date-format只对java.util.Date生效对LocalDateTime不一定有效。最保险的方式是加一个 Jackson 全局配置Configuration public class JacksonConfig { private static final DateTimeFormatter DATE_TIME_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - { builder.serializers(new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); builder.deserializers(new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); }; } }这样所有接口返回的时间字段都是yyyy-MM-dd HH:mm:ss字符串前端不用再做转换。否则每写一个查询接口都要留意时间字段太容易翻车。5.3 现象Vue 打包放进 SpringBoot 后刷新 404现象前端npm run build后把dist目录拷到 SpringBoot 的src/main/resources/static启动后访问首页正常但路由进到/admin/bins之后按 F5 刷新页面 404。原因Vue Router 用的createWebHistory()让浏览器地址栏变成了真实路径/admin/bins。刷新时浏览器向 SpringBoot 发送/admin/bins的请求后端确实没有这个路由就返回 404。解决两种方案。第一改用createWebHashHistory()URL 变成/#/admin/bins刷新始终请求/index.html不会 404。第二保留createWebHistory()在 SpringBoot 加一个 ViewController 转发规则Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:[^\\.]*}) .setViewName(forward:/index.html); } }这个转发会把所有不带点的路径转发给index.html刷新 404 就解决了。但一定要把/api/**排除在外否则前端调接口也会被转发到首页。更严格的做法是加一个拦截器或者把前端静态资源托管到 Nginx由 Nginx 的try_files来处理SpringBoot 只负责 API。我用过几次 Nginx 方案比改后端转发干净但小项目图省事还是用 ViewController 转发。5.4 现象定时任务不执行现象写了Scheduled方法启动项目后没有任何输出也没有报错感觉像代码没跑。原因最常见的是忘在启动类加EnableScheduling另一个原因是Scheduled方法所在的类包路径没被扫到还有可能是fixedDelay时间太长第一轮间隔还没到。解决先在启动类加上EnableScheduling再把定时任务类放到主类所在的包或子包下。排查时在方法里加一行日志看启动后 5 到 10 秒内有没有打印。如果你在定时任务里调用了Thread.sleep会导致任务阻塞下一轮也跟着延迟。这时候建议把fixedDelay调小或者把耗时操作委托给线程池。我用过一个更稳的写法单机场景下用fixedDelay而不是fixedRate因为fixedRate在任务执行时间超过周期时会造成下一个任务挤进来两个任务同时跑同一批数据容易产生脏更新。fixedDelay保证上一轮跑完再等 60 秒适合这种全表扫描的维护型任务。6. 让系统更“智能”对接设备上报与垃圾分类识别的两个进阶方向6.1 不接硬件也能验证满溢链路用脚本模拟设备上报没有真实垃圾桶设备时不要干等硬件联调。我一般用 Python 脚本模拟 HTTP 上报验证满溢判定、积分策略和定时任务是否联动。接口按第 3 章的/api/report设计脚本每 5 秒上报一组随机距离值import requests import time import random url http://localhost:8080/api/report for bin_id in range(1, 11): payload { binId: bin_id, distance: random.randint(10, 80) } resp requests.post(url, jsonpayload) print(bin_id, resp.status_code, resp.text) time.sleep(5)跑起来后去数据库里看trash_bin表的full_level和status变化再把fullThreshold调成 80观察满溢工单是否生成。这一步能帮你提前发现两个问题一是calcFullLevel的换算方向反没反二是设备字段名和后端 DTO 对没对上。如果对接真实设备上报协议很可能是 MQTT 或 TCP 私有协议后端要单独做协议适配层不要把/api/report直接暴露给公网。6.2 垃圾名称匹配用 HanLP 分词才是性价比之王垃圾分类识别的核心是判断用户输入的“塑料瓶”“铝罐”“易拉罐”属于可回收。维护关键词映射表最简单但同义词一多就漏。用 HanLP 做分词再和垃圾类型关键词表做交集能覆盖大多数口语输入。先在pom.xml引入 HanLP 的 portable 版不用下载完整模型dependency groupIdcom.hankcs/groupId artifactIdhanlp/artifactId versionportable-1.8.4/version /dependency然后写一个简单匹配 ServiceService public class GarbageClassifyService { private final MapString, String keywordCategoryMap; public GarbageClassifyService() { keywordCategoryMap new HashMap(); keywordCategoryMap.put(电池, 有害); keywordCategoryMap.put(过期药品, 有害); keywordCategoryMap.put(塑料瓶, 可回收); keywordCategoryMap.put(纸箱, 可回收); keywordCategoryMap.put(剩饭, 厨余); } public String classify(String garbageName) { ListString terms HanLP.segment(garbageName) .stream() .map(term - term.word) .collect(Collectors.toList()); for (String term : terms) { String category keywordCategoryMap.get(term); if (category ! null) { return category; } } return 其他; } }这个方案我实测过对“电池”“过期药品”“剩饭剩菜”这类名词识别非常准对“这个瓶子能扔吗”也能抽取出“瓶子”再用词表命中。但要注意 HanLP 的 portable 包第一次加载会初始化词典启动时稍慢建议做成单例 Bean不要每次请求都重新创建分词器。我自己做这套系统时最大的教训就是先定义好数据字典和接口契约再动手写前后端。很多项目做到一半烂掉不是技术难题而是“桶的状态”“积分规则”“清运工单”这些业务概念没对齐。如果你正在规划这个方向可以从垃圾桶点位表和积分规则表开始设计这两张表稳定了前后端自然就顺畅了。希望帮到你。本文还有配套的精品资源点击获取
返回列表