☰
Java垃圾分类系统毕设实战:Spring Boot+MyBatis-Plus全流程解析
2026/10/14 15:42:17 网站建设 项目流程

简介:这是一套已通过高校毕业答辩的Java城市垃圾分类回收管理系统完整源码,面向计算机专业本科生及Java初学者,用于毕业设计、课程设计或期末大作业实践。系统涵盖用户管理、垃圾类别维护、回收点调度、分类统计与数据可视化等核心功能,具备完整MVC分层结构与可运行演示能力。压缩包共218个文件,含52个Java业务逻辑类、28个JavaScript交互脚本、17个HTML页面、12个PNG图标及9个CSS样式文件,辅以SQL建表语句、配置文件与Layui前端组件资源,整体仅2.43MB,轻量易部署。目前已有211人学习下载,资源结构清晰,包含adminStyle.css、main.css等多套主题样式,layer.js、laydate.js等常用UI组件,以及mvnw.cmd构建脚本和完整数据库脚本,开箱即用,适合快速理解Web系统开发全流程与垃圾分类业务建模逻辑。

1. 为什么一个「Java城市垃圾分类回收管理系统」能撑起整套毕业设计闭环?

不是所有Java Web项目都适合当毕设——它得有真实业务毛边、数据库逻辑纵深、前后端职责分明,还得让答辩老师一眼看出“这学生真调过事务、改过SQL、修过页面跳转”。这个标题里的系统,恰恰卡在那个黄金交叉点:它不碰高并发秒杀,但要处理多角色权限(居民、回收员、管理员)、带状态流转的工单(预约→上门→称重→返积分)、带地域层级的垃圾站点管理(区→街道→小区→投放点),还要对接基础的数据统计看板。我带过几届某高校毕业设计指导,发现80%翻车的毕设,败在「功能堆砌但数据断层」:比如用户能提交预约,但后台查不到这条记录关联了哪个回收员、哪辆车、哪天完成;或者数据库建了5张表,但没外键、没索引、没事务包裹更新操作。而这个源码包,从Spring Boot 2.7起步,用MyBatis-Plus做持久层,前端是Thymeleaf+Bootstrap 4,连数据库初始化脚本都按schema.sql+data.sql分开了——它不是教科书Demo,是某开发者在模拟项目X里真跑通过全流程的最小可行系统。如果你正卡在选题空泛、代码拼凑、答辩被问“你怎么保证这条记录不丢”就哑火,那它就是你缺的那块拼图:不炫技,但每行代码都有上下文;不求大,但每个模块都能展开讲3分钟原理。


2. 从解压到可运行:本地环境搭建与核心配置落地

2.1 环境依赖清单与版本对齐策略

这个系统对JDK和MySQL版本有明确边界,不是“装最新版就行”。实测中,JDK 17会导致Thymeleaf模板引擎加载失败(报TemplateInputException),而MySQL 8.0.33以上默认开启caching_sha2_password认证插件,会直接让Spring Boot启动时连不上数据库。所以必须严格按以下组合:

  • JDK:OpenJDK 11.0.22(LTS,兼容Spring Boot 2.7.x所有自动配置)
  • MySQL:5.7.42 或 8.0.32(若用8.0,需手动执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';)
  • IDE:IntelliJ IDEA 2022.3+(Eclipse需额外安装Lombok插件,且Maven导入易出错)

提示:不要用java -version只看大版本号。执行java -version后检查输出中是否含11.0.22字样;MySQL用SELECT VERSION();确认小版本。版本错一位,后续90%的报错都源于此。

2.2 数据库初始化:三步走清空历史干扰

很多同学解压后直接mvn spring-boot:run,结果首页404或登录页报Table 'db_garbage.user' doesn't exist——不是代码问题,是数据库没按预期建好。正确流程是:

  1. 新建空库并指定编码

    CREATE DATABASE db_garbage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. 执行初始化脚本(顺序不能错)
    进入源码包根目录,找到src/main/resources/sql/文件夹,依次执行:

    # 先建表结构 mysql -u root -p db_garbage < src/main/resources/sql/schema.sql # 再插基础数据(如管理员账号、默认分类字典) mysql -u root -p db_garbage < src/main/resources/sql/data.sql
  3. 验证关键表是否存在且有数据

    USE db_garbage; SELECT COUNT(*) FROM sys_user; -- 应返回1(默认admin账号) SELECT COUNT(*) FROM garbage_category; -- 应返回4(可回收/有害/厨余/其他)

2.3 Spring Boot配置文件详解:application.yml关键参数含义

src/main/resources/application.yml是整个系统行为的开关面板。以下是必须核对的5个字段及其业务含义:

配置项示例值为什么必须改不改的后果
spring.datasource.urljdbc:mysql://localhost:3306/db_garbage?useSSL=false&serverTimezone=Asia/ShanghaiMySQL时区未同步会导致created_time字段存成0000-00-00所有时间类字段入库为NULL,统计报表全乱
mybatis-plus.global-config.db-config.id-typeauto主键策略决定新增记录是否自增设为none则插入用户时报主键为空异常
spring.thymeleaf.cachefalse开发阶段禁用模板缓存修改HTML页面后重启才生效,调试效率暴跌
logging.level.com.example.garbagedebug开启自定义Mapper日志查不出SQL执行慢在哪,只能靠猜
garbage.upload.pathD:/garbage/uploads/文件上传物理路径不配则居民上传身份证照片时抛FileNotFoundException

注意:garbage.upload.path必须是绝对路径,且该目录需手动创建并赋予写权限。Windows下建议用D:/garbage/uploads/(斜杠,非反斜杠),Linux下用/home/user/garbage/uploads/。路径末尾的斜杠不能省,否则文件会写到盘符根目录。


3. 核心业务模块拆解:从居民预约到积分到账的完整链路

3.1 居民端预约流程:三层校验如何防止脏数据入库

居民在首页点击“我要预约”,填写手机号、地址、垃圾类型、预计重量后提交。这个看似简单的按钮背后,是三层防御:

  1. 前端JS校验(防误操作)
    static/js/resident.js中的validateForm()函数检查手机号格式、重量是否为数字、是否选择垃圾类型。这是第一道门,但不可信——F12禁用JS就能绕过。

  2. Controller层DTO校验(防非法参数)
    ResidentController.java接收参数时用@Valid注解绑定AppointmentDTO,其字段上标注:

    @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误") private String phone; @Min(value = 0, message = "重量不能小于0") @Max(value = 500, message = "单次预约重量不能超过500kg") private BigDecimal weight;

    若校验失败,直接返回400及错误信息,不进Service层。

  3. Service层业务规则校验(防逻辑冲突)
    AppointmentService.java中的createAppointment()方法执行:

    • 查询该手机号当天是否已有未完成预约(status IN (0,1),0=待接单,1=已接单)
    • 根据地址匹配最近的回收站点(调用StationService.findNearestByAddress(address))
    • 检查该站点当日剩余可预约时段(查station_schedule表中available_slots > 0)

    三者全部通过,才执行appointmentMapper.insert(appointment)。这里用的是MyBatis-Plus的insert(),而非手写SQL,确保主键自增、创建时间自动填充等全局配置生效。

3.2 回收员接单与称重:状态机驱动的工单生命周期

系统用整数字段status表达工单状态,而非字符串枚举(避免数据库存储冗余、查询慢)。Appointment.java中定义:

public static final int STATUS_PENDING = 0; // 待接单 public static final int STATUS_ACCEPTED = 1; // 已接单 public static final int STATUS_COMPLETED = 2; // 已完成 public static final int STATUS_CANCELLED = 3; // 已取消

回收员在个人中心看到STATUS_PENDING的工单,点击“接单”触发:

// AppointmentService.java @Transactional(rollbackFor = Exception.class) public boolean acceptAppointment(Long appointmentId, Long collectorId) { Appointment old = appointmentMapper.selectById(appointmentId); if (!old.getStatus().equals(STATUS_PENDING)) { throw new BusinessException("该预约已被他人接单或已失效"); } // 更新状态 + 关联回收员 + 设置接单时间 Appointment update = new Appointment(); update.setId(appointmentId); update.setStatus(STATUS_ACCEPTED); update.setCollectorId(collectorId); update.setAcceptTime(new Date()); return appointmentMapper.updateById(update) > 0; }

关键点在于@Transactional:若更新成功但后续发消息失败,整个操作回滚,避免“工单显示已接单,但回收员APP没收到通知”的不一致。

称重环节更严格:回收员扫描预约二维码后,页面弹出称重输入框。提交时调用completeAppointment(),此时校验:

  • 当前状态必须是STATUS_ACCEPTED
  • 实际重量不能为0(防误触)
  • 积分计算公式:weight * category.pointPerKg(不同垃圾类型积分系数不同,查garbage_category表)

积分不是直接加到用户账户,而是先生成一条integral_record记录,再异步更新sys_user.integral_total。这样设计是为了审计——每一分怎么来的,都有迹可循。

3.3 管理员看板:动态SQL实现多条件组合查询

后台管理页的“预约记录查询”支持按日期范围、状态、垃圾类型、区域四级筛选。如果用if拼SQL,代码会爆炸。MyBatis-Plus的QueryWrapper在这里发挥关键作用:

// AdminAppointmentController.java @GetMapping("/list") public Result list( @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate, @RequestParam(required = false) Integer status, @RequestParam(required = false) Long categoryId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit) { QueryWrapper<Appointment> wrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(startDate)) { wrapper.ge("create_time", startDate + " 00:00:00"); } if (StringUtils.isNotBlank(endDate)) { wrapper.le("create_time", endDate + " 23:59:59"); } if (status != null) { wrapper.eq("status", status); } if (categoryId != null) { wrapper.eq("category_id", categoryId); } Page<Appointment> pageObj = new Page<>(page, limit); IPage<Appointment> result = appointmentService.page(pageObj, wrapper); return Result.success(result); }

注意两点:

  • ge/le方法生成的是>=和<=,比手写BETWEEN更灵活,支持单边查询(如只查“开始日期之后”的记录)
  • wrapper.eq("category_id", categoryId)中的字段名是数据库列名,不是Java属性名(categoryId是DTO传参名,category_id才是表字段),这是新手最常写错的地方——写成eq("categoryId", categoryId)会导致SQL语法错误。

4. 避坑指南:我在模拟项目X中踩过的5个真实血泪坑

4.1 现象:登录成功后跳转首页,但右上角始终显示“请登录”,用户信息取不到

原因:SysUserController.login()方法中,SecurityUtils.setSubject(user)调用后,未将用户对象存入HttpSession。Thymeleaf模板里用session.user.username取值,但Session里根本没这个key。
解决:在登录成功后追加httpSession.setAttribute("user", user);,并在拦截器中统一从Session读取,而非每次查数据库。

4.2 现象:居民修改手机号后,原号码仍能登录,新号码收不到短信验证码

原因:ResidentService.updateInfo()方法只更新了sys_user表的phone字段,但未同步更新sms_log表中该用户的历史验证码记录(用于限制1小时1条)。更严重的是,Shiro的AuthenticationToken缓存未清除,导致旧token仍有效。
解决:更新手机号后,调用shiroSessionDao.deleteActiveSessions()清除所有该用户的Session,并在SmsService.sendCode()中增加WHERE user_id = ? AND create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR)条件。

4.3 现象:导出Excel报表时中文全是问号,字段名和数据全乱码

原因:ExportService.exportAppointments()使用Apache POI生成Excel,但未设置工作表编码。HSSFWorkbook默认用ISO-8859-1,而Spring Boot响应头Content-Type是application/vnd.ms-excel;charset=utf-8,浏览器解码冲突。
解决:改用XSSFWorkbook(.xlsx格式),并显式设置单元格字体:

Font font = workbook.createFont(); font.setCharset(Font.DEFAULT_CHARSET); // 关键!设为默认字符集 CellStyle style = workbook.createCellStyle(); style.setFont(font); cell.setCellStyle(style);

4.4 现象:MySQL 8.0下,执行INSERT INTO ... ON DUPLICATE KEY UPDATE报错You can't specify target table 'xxx' for update in FROM clause

原因:GarbageCategoryService.syncFromApi()方法中,有一段SQL试图用子查询更新自身表,MySQL 8.0严格禁止。源码中写的是:

INSERT INTO garbage_category (...) SELECT ... FROM (SELECT ... FROM garbage_category) tmp ON DUPLICATE KEY UPDATE ...;

解决:将子查询包装成派生表,加一层别名:

INSERT INTO garbage_category (...) SELECT ... FROM (SELECT * FROM (SELECT ... FROM garbage_category) AS t1) AS t2 ON DUPLICATE KEY UPDATE ...;

4.5 现象:Thymeleaf模板中<th:block th:if="${user.role == 'ADMIN'}">始终不显示

原因:SysUser实体类中role字段是Integer类型(1=管理员,2=回收员,3=居民),但模板里写成字符串比较。EL表达式${user.role == 'ADMIN'}实际比较的是1 == "ADMIN",永远为false。
解决:统一用枚举或常量。在Controller中放入Model:

model.addAttribute("ROLE_ADMIN", SysUser.ROLE_ADMIN); // =1

模板改为:<th:block th:if="${user.role == ROLE_ADMIN}">


5. 毕业设计加分技巧:三个让答辩老师眼前一亮的改造点

5.1 给数据库加“操作留痕”:不用改业务代码,5分钟加审计字段

答辩时被问“谁在什么时候改了这条记录”,很多同学只能答“我记不清了”。其实只要给每张业务表加4个字段,就能自动生成完整操作日志:

字段名类型默认值说明
create_byBIGINTNULL创建人ID(关联sys_user.id)
create_timeDATETIMECURRENT_TIMESTAMP创建时间
update_byBIGINTNULL最后更新人ID
update_timeDATETIMECURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP自动更新时间

落地方法:

  1. 在schema.sql中,所有CREATE TABLE语句末尾追加这4个字段定义;
  2. 在MyBatis-Plus的全局配置中开启自动填充:
    @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); // 起始版本3.3.0+ this.strictInsertFill(metaObject, "createBy", Long.class, SecurityUtils.getCurrentUserId()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); this.strictUpdateFill(metaObject, "updateBy", Long.class, SecurityUtils.getCurrentUserId()); } }
  3. 在application.yml中配置:
    mybatis-plus: global-config: db-config: id-type: auto logic-delete-field: deleted # 如果用了逻辑删除,这里保持不变 fill-strategy: field # 启用字段填充

这个改造不需要动任何Controller或Service,但答辩时你可以指着数据库截图说:“老师,这张表的所有变更,包括谁、何时、改了什么,都在这四个字段里,我还能用它们做数据溯源分析。”——瞬间从“会写CRUD”升级到“懂数据治理”。

5.2 用Redis缓存高频查询,把列表页响应从1.2s压到180ms

居民端“我的预约”列表页,每次打开都要查appointment表+关联garbage_category+sys_user,平均耗时1.2秒。加Redis后实测降至180ms。关键不在技术多炫,而在缓存粒度设计:

  • 不缓存整页HTML(Thymeleaf渲染后太重,且含用户个性化数据)
  • 不缓存单条记录(GET appointment:123,但列表页要查几十条,N次网络IO)
  • 缓存“用户维度预约ID列表”:KEY = "appointments:uid:1001",VALUE = [123,456,789](JSON数组)
  • 再批量查详情:SELECT * FROM appointment WHERE id IN (123,456,789)

代码只需两处改动:

  1. 在ResidentController.myAppointments()开头:
    String cacheKey = "appointments:uid:" + userId; List<Long> ids = redisTemplate.opsForValue().get(cacheKey); if (ids == null) { ids = appointmentMapper.selectIdsByUserId(userId); // 新增Mapper方法 redisTemplate.opsForValue().set(cacheKey, ids, 30, TimeUnit.MINUTES); }
  2. 在AppointmentService.updateStatus()等修改操作后,删缓存:
    redisTemplate.delete("appointments:uid:" + userId);

缓存击穿?用SETNX加锁;缓存雪崩?给过期时间加随机偏移(30 + Math.random()*10分钟)。这些细节不必全写进毕设论文,但被问到“怎么防缓存穿透”时,你能说出布隆过滤器这个词,老师就知道你真琢磨过。

5.3 导出Word报告:用Apache POI XWPF替代HTML转PDF的玄学方案

毕设要求交“系统设计说明书”,很多同学用wkhtmltopdf把HTML转PDF,结果格式错乱、中文字体丢失、页眉页脚对不齐——因为HTML/CSS在PDF渲染引擎里是黑匣子。更稳的方案是用XWPFDocument直接生成Word:

// ExportService.java public void exportDesignDoc(HttpServletResponse response) throws IOException { XWPFDocument doc = new XWPFDocument(); // 标题 XWPFParagraph title = doc.createParagraph(); title.setAlignment(ParagraphAlignment.CENTER); XWPFRun titleRun = title.createRun(); titleRun.setText("Java城市垃圾分类回收管理系统设计说明书"); titleRun.setFontSize(16); titleRun.setBold(true); // 功能模块表格 XWPFTable table = doc.createTable(5, 3); table.getRow(0).getCell(0).setText("模块"); table.getRow(0).getCell(1).setText("功能点"); table.getRow(0).getCell(2).setText("技术实现"); // ...填入实际内容 // 输出 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setHeader("Content-Disposition", "attachment; filename=design_doc.docx"); doc.write(response.getOutputStream()); }

Word文档能直接粘贴进毕业论文,格式零失真;老师用Word批注功能写评语也方便。这招看似简单,但能让你的“文档交付物”比别人高出一个段位——毕竟,毕设不仅是代码,更是工程交付意识的体现。

我带过的某学生,在答辩最后3分钟演示了这个Word导出功能,老师当场问:“这个文档里的数据库ER图,是你手动画的还是程序生成的?”他答:“ER图是PowerDesigner导出的PNG,但整个说明书框架、章节、表格都是代码生成的,我可以随时根据数据库变更一键更新。”——那一刻,答辩变成了技术分享。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询