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

资讯详情

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

基于SpringBoot+Vue的社区智能垃圾管理系统设计与实现

基于SpringBoot+Vue的社区智能垃圾管理系统设计与实现 1. 垃圾管理系统先别急着写代码这个项目真正在解决什么问题做这个系统的起因其实很朴素——很多社区现在还在用人工巡检 电话调度的方式管垃圾清运保洁员每天要跑遍几十个垃圾投放点谁满了谁没满全靠肉眼判断居民对垃圾分类的参与度也基本靠自觉扔得对不对、扔了多少没有任何数据沉淀。等你真正把需求捋一遍就会发现这类管理系统的本质并不是做一个花哨的界面而是要把一条完整的业务闭环——居民投递垃圾 → 系统识别分类与计重 → 积分激励 → 箱体满溢预警 → 生成清运任务 → 数据统计回溯——用软件的方式固化下来。标题里智能两个字很容易让人误解成AI识别垃圾但实际上在SpringBoot单体会话式架构下真正能落地且稳定的智能体现在三块投递行为的规则化判断分类是否正确、投递频次是否异常垃圾箱满溢状态的自动感知与任务联动推送基于投递记录的数据分析辅助物业调整清运排班和垃圾分类宣导策略这套系统适合谁如果你是正在做Java课程设计、毕业设计的学生或者刚入职想练手SpringBoot全家桶的初级开发这个项目的技术难度曲线很友好不涉及分布式、不涉及消息中间件但把SpringBoot MyBatis-Plus MySQL Vue这套主流Web开发栈里的核心环节都覆盖到了——RBAC权限、业务状态机、定时任务、文件上传、接口统一封装、ECharts可视化报表。做完这一个你对一个完整的业务系统长什么样会有比刷十套面试题更直观的认知。下面我就按这个系统从需求到落地的完整链路把我实际开发过程中的设计决策、表结构、关键代码逻辑和踩坑点都摊开讲。如果你打算从零复刻这套系统这篇文章可以直接当设计文档用。2. 需求拆解先行三个角色、五条主线、一套闭环2.1 角色权限设计居民、清运员、管理员各管一摊社区智能垃圾管理系统的角色划分我最终定下来的是三种角色而不是常见的四种五种。因为角色越多权限配置越复杂但在真实社区场景里物业经理和系统管理员往往是同一个人没必要强行拆开。角色核心诉求关键操作居民普通用户查分类、查积分、查投递记录在线查看垃圾投放指南、浏览公告、输入/扫码登记投递、查询个人积分与明细、兑换预告清运员知道哪里满了、去哪儿清运查看待处理任务、接收清运指令、上报箱体状态、填写清运记录系统管理员掌握全局、配置规则管理垃圾箱点位、管理用户、配置分类与积分规则、审核积分异常、查看统计报表与系统公告权限这块我用的是Spring Security 自定义注解 拦截器的方式没有引入重型权限框架。核心是定义了一个RequireRole注解标注在Controller方法上拦截器里从当前登录用户的安全上下文中取角色编码做比对命中就放行不命中返回统一错误码。这套轻量方案的好处是整个权限逻辑只有一百来行代码评审和答辩时很容易讲清楚设计动机。2.2 业务主流程一条投递记录引发的连锁反应系统里最核心的业务流转路径是这样走的居民在垃圾箱点位扫码或者手动输入箱体编号选择垃圾类别可回收、有害、厨余、其他系统校验该箱体状态如果当前容量已满则直接拦截并给出提示投递信息落库生成一条投递记录同时按积分规则计算本次积分积分累加到用户账户同时写一条积分明细方便日后对账和异常追溯数据库更新垃圾箱当前容量如果超过满溢阈值我设置的是90%箱体状态自动从运行中翻转为已满系统检测到箱体满溢后自动在清运任务表里生成一条待办任务并推送通知给清运员账号清运员完成任务后更新任务状态箱体容量归零满溢状态解除这套流程初看没什么但真正实现了之后你会发现表与表之间的状态联动才是代码量的大头。如果你用事务把投递记录 积分流水 箱体容量更新 满溢状态翻转这套操作串起来必须考虑好异常回滚的边界比如积分算错了要不要影响投递记录的入库我的处理方案是投递记录单独一个事务先落库积分和箱体容量在同一个事务里处理——这样做虽然多了一步但避免了投递成功但积分没到账这种用户感知最强的问题。2.3 非功能性需求这类项目中容易被忽略但答辩经常被问的点除了功能需求我在设计阶段就给自己定了几个非功能约束并发量社区场景单点位并发极低QPS按个位数设计即可不需要引入Redis缓存热点数据单机MySQL完全扛得住数据一致性积分扣减和投递记录必须严格一致采用事务 乐观锁双保险垃圾箱容量更新时如果没有并发写同一个箱体的场景普通update就行可维护性所有枚举值投递类型、任务状态、箱体状态不散落硬编码在业务代码里统一收口到枚举类方便维护可演示性系统内置一个数据初始化脚本自动生成近30天的模拟投递数据方便查看ECharts统计图表的动态效果这个对答辩演示帮助极大明确这些之后写代码的效率会高很多。很多初学者一上手就建表、写Controller做到后面发现逻辑混乱大多是因为跳过了需求边界这一步。3. 技术选型不用追新为什么这套组合最省心3.1 后端SpringBoot 2.7.x MyBatis-Plus 3.5.xSpringBoot版本我特意锁定在2.7.x没有跟风上SpringBoot 3.x。原因很简单SpringBoot 3基于Jakarta EE 9底层用Java 17很多老的兼容方案比如某些代码生成器、老版本Swagger在3.x下会暴雷而目前主流教材、公司内部脚手架、面试考点基本都以SpringBoot 2.7.x为基准。用这个版本踩坑成本最低。ORM框架我的选择是MyBatis-Plus压过Spring Data JPA一头。理由是这类管理系统90%以上的数据操作是单表CRUD和简单联表查询MyBatis-Plus的BaseMapper内置方法直接搞定复杂统计用Select注解写SQL也很顺手不需要JPA那种隐含的级联关系带来的心智负担。网上流传的Spring Boot MyBatis-Plus根据Java实体类生成建表SQL这个骚操作其实本质就是利用MyBatis-Plus内置的代码生成器反推表结构——这恰好说明它的实体和表映射足够简单直接适合这种管理系统的开发节奏。3.2 前端Vue2 ElementUI ECharts前端我选的是Vue2 ElementUI的组合很多初学者会纠结要不要上Vue3。我的建议是如果目标是快速稳定地完成系统Vue2 ElementUI的成熟度无可比拟——组件全、报错都被搜索引擎踩平了、网上资料一抓一大把。Vue3 Element Plus确实新但你要花大量时间适配坑对做业务系统的进度来说没有本质提升。接口联调用Axios统一封装后端返回统一结构体前端用拦截器统一处理登录失效和业务错误码这样代码干净、排错也清晰。3.3 数据库与工具链MySQL 8.0 Redis可选数据库选MySQL 8.0。如果你本机是5.7同样没问题唯一要注意的是utf8mb4字符集必须设置到位否则存表情字符会报错。Redis在这个系统里不是必需品——有的题目描述里会要求热点数据缓存但我们社区场景根本没有高并发我用本地Caffeine做了公告和分类知识库的短暂缓存5分钟过期够用且少维护一套中间件。开发工具链我统一用IDEA Navicat Git接口调试用Apifox这些不展开讲顺手就行。3.4 关于SpringBoot整合Flink这类热搜词别被带偏网上很多热搜词比如SpringBoot整合Flink、SpringBoot整合ActiveMQ看着热闹但和这个项目的技术定位完全不在一个层级。垃圾管理系统的数据量级决定了对流处理和消息队列没有任何硬性需求硬要引入Flink只能说明你没理解技术选型的第一性原则——为业务服务不为技术表演。如果你的目标是把这个项目包装得更有深度优先把定时统计报表用SpringScheduled实现每日投递量聚合和异步通知用Spring事件机制实现研究透性价比高得多。4. 数据库设计六张核心表从第一版到最终版经历了什么4.1 表结构总览与设计动机我最终的数据库一共六张核心表外加一张关联辅助表为了存储过程和触发器没有使用它——这类系统的业务规则放在Service层更灵活也更好解释逻辑表名用途关键字段user系统用户基表三个角色共用id, username, password(Bcrypt加密), real_name, phone, role_code, points_total, statusgarbage_box垃圾箱点位表id, location, longitude, latitude, box_type, max_capacity, current_capacity, status(运行中/已满/维护)delivery_record投递记录表id, user_id, box_id, category_id, weight_kg, points, photo_url, delivery_timepoints_record积分明细流水表id, user_id, change_type(收入/支出), change_value, balance_after, related_record_id, create_timeclean_task清运任务表id, box_id, task_status(待处理/进行中/已完成), assignee_id, admin_id, create_time, finish_time, remarkcategory_info垃圾分类知识表id, category_name, description, points_rule, accepted_items, icon_urlnotice公告信息表选用id, title, content, publish_time, publisher_id4.2 关键设计决策的思考过程第一user表为什么把三类角色统一不建多张表这是个设计取舍。传统企业开发习惯把用户和角色拆成多张表但在这类课程设计/毕设规模中一张表 角色字段足够用了。好处有三登录鉴权时只需要一次主键查询三个角色之间的数据关联比如清运员和居民后续可能需要互评不需要跨表外键报表统计用户数时直接按role_code分组就行。代价是如果未来要扩展四五种完全异构的角色表字段会冗余但那不是现在要操心的事。第二积分为什么不直接存在user表还要一个points_record明细表这来源于一个实际的坑我第一版只在user表放了points_total字段后来做积分清零、兑换功能时发现根本没有历史轨迹用户来质问我上周还有500分去哪了没有任何流水可以追溯。加上明细表后points_total和points_record形成类似余额 流水的关系查询当前余额走用户表查历史走流水表异常追查时两边一对照就清楚了。同时流水表里我加了change_type和related_record_id两个字段——related_record_id用于关联投递记录或兑换记录一旦积分计算有误能精准定位到是哪一笔投递产生的这个细节在答辩问数据一致性时很加分。第三垃圾箱为什么单独维护max_capacity和current_capacity有人可能觉得箱体容量是硬件设备上报的字段应该接入物联网。但实际上很多社区的智能垃圾箱就是普通垃圾桶加上二维码牌智能体现在软件的逻辑。我这边用容量字段做满溢判断配合投递时的重量预估前端让用户输入大致重量或按本次投放标准重量计不需要任何硬件成本。这里容量单位我统一用标准袋数而非升——便于居民理解也便于清运员核对。第四昵称、头像、手机号这些字段要不要建索引user表的username和phone建了唯一索引其他不建。delivery_record表因为要频繁按用户和时间范围做统计查询建了复合索引(user_id, delivery_time)这个索引对后续报表查询优化非常关键。4.3 数据库初始化脚本里的小心机我在sql/init_data.sql里放了约200条模拟投递记录分布在最近30天内日期按随机偏移生成分类的占比故意设置成可回收25%、厨余50%、有害5%、其他20%——这样出来的ECharts图表一周趋势、分类占比、箱体使用率演示效果特别生动。这是我踩过坑之后的经验第一次演示用的是真实空数据统计图全是一片空白评委看了毫无感觉。初始化数据这种为演示服务的细节往往比某个接口写得漂亮更影响答辩印象分。5. 核心业务实现从投递计分到满溢预警的代码逻辑5.1 统一返回体与包装层所有接口的交通规则所有Controller的返回类型统一为ResultT结构如下public class ResultT { private Integer code; // 业务码200成功其他为失败 private String message; // 提示信息 private T data; // 载荷数据 }辅助一个ResultUtil静态工厂方法返回成功/失败配合全局异常处理器RestControllerAdvice把参数校验异常、业务异常、空指针异常统一转成这种结构。前端Axios拦截器里判断code来决定走成功回调还是弹出错误提示。这种约定带来的收益要等接口数量超过一百个时才能体会——如果每个方法都随手MapString, Object返回写到后面你自己都不知道哪些字段名是可缺省的联调会变成灾难。5.2 投递计分核心流程事务边界是最容易写错的地方投递接口的Service层代码逻辑是这套系统的重头戏我贴一段关键逻辑来解释事务边界为什么这样切Transactional(rollbackFor Exception.class) public DeliveryResult submitDelivery(DeliveryReqDTO dto) { // 1. 校验箱体状态 GarbageBox box garbageBoxMapper.selectById(dto.getBoxId()); if (box null || BoxStatus.MAINTENANCE.getCode().equals(box.getStatus())) { throw new BizException(ErrorCode.BOX_NOT_AVAILABLE); } if (box.getCurrentCapacity() box.getMaxCapacity()) { throw new BizException(ErrorCode.BOX_FULL); } // 2. 写入投递记录 DeliveryRecord record new DeliveryRecord(); record.setUserId(LoginUtil.getCurrentUserId()); record.setBoxId(box.getId()); record.setCategoryId(dto.getCategoryId()); record.setDeliveryTime(LocalDateTime.now()); // 重量与积分计算交给单独方法 BigDecimal points calculator.calculate(dto.getCategoryId(), dto.getWeightKg()); record.setPoints(points); deliveryRecordMapper.insert(record); // 3. 更新垃圾箱容量并判断满溢 box.setCurrentCapacity(box.getCurrentCapacity() dto.getStandardBags()); if (box.getCurrentCapacity() (int)(box.getMaxCapacity() * 0.9)) { box.setStatus(BoxStatus.FULL.getCode()); } garbageBoxMapper.updateById(box); // 4. 积分入账 流水记录 userMapper.increasePoints(LoginUtil.getCurrentUserId(), points); pointsRecordMapper.insert(buildPointsRecord(points, record.getId())); // 5. 如果满了异步生成清运任务跨事务 if (BoxStatus.FULL.getCode().equals(box.getStatus())) { applicationEventPublisher.publishEvent(new BoxFullEvent(box.getId())); } return DeliveryResult.build(record.getId(), points); }注意最后一步用applicationEventPublisher发事件来生成清运任务没有写在同一事务里。原因很实际如果和主事务同进同出那么箱体满溢检测失败会导致整笔投递回滚——但投递本身是成功的用户不该为任务生成失败买单。事件监听器通过TransactionalEventListener的phase TransactionPhase.AFTER_COMMIT确保只在主事务成功提交后再执行任务生成。这个点你要是能在答辩时说清楚技术深度分直接拉满。5.3 防刷积分策略业务规则远比技术手段重要积分系统的天然漏洞是同一人反复投小额垃圾刷积分。我实现的防刷策略从两层夹击业务规则层设置同一用户对同一垃圾箱的投递间隔最短为10分钟间隔内的重复请求直接拒绝并提示请勿频繁投递每日投递上限20次超过后需管理员审批开通。技术层表里加delivery_time和复合索引(user_id, box_id, delivery_time)查询最近一条投递记录时间比较间隔是否达标。这层逻辑没有用Redis的SETNX因为社区场景没有并发压力数据库层面就把问题消解了。但我在代码注释里保留了说明如果是面向公共区域的无人值守箱体以后要接多个点位并发投递方案自然要升级成Redis分布式锁。这种留好演进接口的思路比不管三七二十一直接上Redis更成熟。5.4 定时任务与报表统计数据报表我用的是Spring的Scheduled ECharts。实现思路每天凌晨2点跑一次聚合任务把过去24小时的投递量按分类、按时段聚合写入一张stats_daily表第六张实际存在的表查询接口直接读聚合表前端ECharts拉接口渲染折线图、柱状图、饼图聚合任务本身要处理数据延迟补偿比如凌晨任务跑的时候还有一部分前一天晚上的投递未入库所以在聚合查询里加了时间窗口容错确保今天看到的昨天数据是完整的这个读聚合表而非实时统计原始表的做法是从生产环境大数据报表里学来的思路——虽然我们这个数据量实时查也不卡但聚合表让查询接口的复杂度从边查边算变成纯取数对接口响应时间是一种结构性的优化。5.5 满溢事件与清运任务的状态机清运任务从生成到完成包含四个状态待处理 → 进行中 → 已完成 → 已取消。状态转移规则待处理状态可以由管理员手动取消比如箱体其实没满、是误报进行中状态由清运员点击开始清运触发系统记录开始时间已完成状态由清运员点击完成触发要求填写本次清运袋数完成后垃圾箱容量归零、状态恢复为运行中状态字段我是通过status字段维护的并配合时间戳记录create_time、start_time、finish_time。我踩过一个坑最初没有start_time后来想统计清运平均响应时长发现没有任何依据。所以建议这类管理任务表跟时间相关的字段尽量预留全宁可前期多写两个字段也不要后期补数据的时候无从下手。6. 前后端联调与部署落地从本地能跑到演示不翻车6.1 接口规范Swagger文档和Postman/ Apifox集合都要有在前后端联调阶段我维护了一份Apifox接口集合把每个接口的请求/响应示例固化成文档前端同学或你自己写Vue时的自己照着文档调接口基本不用来回问。SpringDoc集成后Swagger UI页面在开发环境直接可访问方便随时调试。这里有个小技巧springdoc的接口文档路径前缀要和后端路由前缀保持一致比如统一/api/v1开头前端代理也按此配置后面部署的时候少走弯路。6.2 Vue项目打包进SpringBoot的两种姿势这个话题是热搜词里很经典的一个——vue打包放进springboot中。做单体演示项目最省事的部署方式就是把前端构建产物交给SpringBoot统一托管。两种姿势我都试过方式一简单推荐Vuenpm run build生成dist目录直接把dist下的内容整体拷贝到后端src/main/resources/static目录。启动SpringBoot后访问http://localhost:8080/就是前端页面接口走同源/api/v1路径不存在跨域问题。这种方式最简单但每次改前端都要手动拷贝。方式二自动化在前端项目pom.xml中引入frontend-maven-pluginMaven打包时自动执行npm install和npm run build把产物复制到target/classes/static。好处是mvn package打出的是一个完整可运行的jar一次构建后直接丢到服务器即可。坏处是构建时间变长如果前端依赖安装慢耐心要够。我最终选了方式二jar包单文件部署不管什么环境只要装了JDK就能java -jar xxx.jar跑起来演示前不用再折腾前端构建。6.3 部署用宝塔反而比纯命令行省心服务器上我用的是宝塔面板 Docker 部署SpringBoot这是我对热搜词宝塔docker部署springboot的真实实践经验。具体链路把 jar 包和Dockerfile上传到服务器特定目录Dockerfile里基础镜像用openjdk:8-jre-alpine项目如果是JDK8或openjdk:17-jre-slimJDK17不要用自带完整JDK的大镜像能小一半体积构建镜像docker build -t community-waste .启动容器docker run -d --name waste-app -p 8080:8080 -v /opt/waste/config:/app/config community-wasteMySQL 如果用Docker跑容器间要通过自定义桥接网络互联或者后端配置直接用宿主机IP访问在application-prod.yml里把数据库地址指向Docker容器对应的DB端口、用户名密码用环境变量注入而不是写死在配置文件里。记得mysql的时区参数要配serverTimezoneAsia/Shanghai不然存时间差8小时就是从这个环节开始的。6.4 部署前强制检查清单每次部署前我会逐项过一遍这个检查清单帮我避掉了至少三次演示事故是否关闭了开发环境专属的Swagger通过spring.profiles.activeprod切掉数据库初始化脚本是否已按生产环境账号执行init_data.sql是否带了幂等判断重复执行不报错文件上传目录是否存在且具备写权限比如upload/文件夹SD卡/服务器路径别用相对路径建议统一配置绝对路径跨域配置是否只在开发环境放开生产环境同源部署应关闭CorsFilter端口是否被占用、防火墙是否放行、Nginx是否转发非必须可直连端口用宝塔的最大的好处是监控面板上直接能看到CPU、内存、磁盘演示前瞄一眼资源使用率心里有数。7. 踩过的四个坑每一个都能让第二天的演示社死7.1 坑一LocalDateTime 序列化格式没配置前端拿到一堆1988-03-04T12:30:00SpringBoot默认的Jackson对LocalDateTime序列化输出的是ISO格式带T字母前端显示很不友好如果你用JsonFormat每个字段加又累赘且容易漏。正确做法是在application.yml中统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但注意这个配置只对java.util.Date生效对LocalDateTime依然要配合jackson-datatype-jsr310的配置最省心的方案是定义一个全局的Jackson自定义配置类Jackson2ObjectMapperBuilderCustomizer统一给LocalDateTime和LocalDate设置序列化格式。这是我第一次联调时前后端扯皮半小时的血泪教训。7.2 坑二MyBatis-Plus 分页查询没配拦截器全表数据被一页返回MyBatis-Plus 3.x 的分页功能依赖PaginationInnerInterceptor如果你只引入了依赖却没有在配置类里添加拦截器分页方法是不会生效的——查询结果全量返回前端分页变成了假的。这个坑非常隐蔽因为你不看SQL日志根本发现不了。排查链路前端只显示第一页的数据但每条记录索引却是真实数据的全局位置 → 打印SQL发现只有一条select *没有limit→ 检查配置类发现MybatisPlusInterceptor压根没注入。加上这个拦截器问题立刻消失Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }7.3 坑三SpringBoot版本升级引发的查无此类方法连环报错有一次为了尝鲜把项目升到SpringBoot 3.2结果启动时报ClassNotFoundException: javax.annotation.Resource因为Jakarta改名后javax变成jakarta又因为MyBatis-Plus版本太老不兼容启动时各种反射异常。搜索引擎一搜全是相似的问题。这次经历之后我彻底想明白一件事课程设计级别项目的核心是逻辑稳定和功能完整不是版本最新。我后来的做法是初始化项目时直接参照Spring Initializr默认推荐一个稳定组合比如2.7.18 JDK8所有依赖版本用Maven的dependencyManagement锁定不再因为排名靠前就追新。7.4 坑四统计报表数据不对原来时区差8小时有段时间我做的最近7天投递趋势图数据总是对不上今天的记录跑到了昨天的分组里。查到最后是MySQL连接串里缺少serverTimezoneAsia/Shanghai导致驱动的会话时区用了UTCJava写入的LocalDateTime存入MySQL时被转换偏移读取时再偏一次——一来一去整整8小时。修复链接串后数据正常。这类数据歪了但报错不明显的问题排错的第一原则是先看数据库实际存的原始值而不是盯着前端图表猜。我的排查链路前端图表不对 → 查真正的表数据 → 发现时间晚8小时 → 检查数据库连接串 → 补serverTimezone→ 重跑初始化脚本验证正常。8. 锦上添花的几个扩展点做完核心功能后还能往上加什么如果你的时间还有富余我按性价比从高到低列了几个扩展方向都亲测过可行积分兑换功能把积分从单纯的数字变成可消费的资产用户可用积分兑换垃圾袋、毛巾等小礼品。实现时只需新增redeem_record表和对应Controller事务边界要注意扣减积分和生成兑换记录要放在同一事务避免扣了分但没生成记录。微信小程序端如果毕设要求有移动端可以基于SpringBoot接口直接适配小程序端把扫码投递和积分查询搬到微信生态内。核心接口完全复用只需额外处理code换session_key的登录逻辑。垃圾分类知识库 模糊查询在category_info表里维护一份常见物品清单前端输入香烟盒后端分词匹配返回可回收垃圾体验感很强。这个用MySQLLIKE %关键词%就能实现量不大不值得上Elasticsearch。箱体GPS地图展示接入高德地图 JS API把garbage_box表的经纬度数据用Marker标注在地图上满溢箱体用红色标记清运员打开地图能直观点位。耗时不大视觉冲击力强。这些扩展点的核心思路是一致的都用现有的表结构和接口能力不需要引入新的技术栈但你做完后系统完整度会有质的提升。回到项目本身系统做到最后你会发现技术实现反而只是整件事里最顺的一环。真正花时间的是想清楚箱子满了之后应该触发什么操作积分异常由谁审核报表指标到底是什么口径这类业务边界的定义。建议不管你是做课程设计还是面试写Demo项目都先花一个下午把业务规则画成流程图再动工写表结构。方向对了后面只是敲键盘的问题了。
返回列表