简介:这份资源是面向高校计算机专业学生与Java Web开发学习者的校园垃圾分类管理系统毕业设计文档,采用SSM框架、Java技术与MySQL数据库实现,适合作为课程设计、毕设选题或SSM入门练手项目的参考方案。压缩包内仅含1个docx文档,大小约1.66MB,内容涵盖系统摘要、目录结构、需求分析、功能设计与测试等完整章节。文档围绕网站前台、管理员、学生三类角色展开,前台提供关于我们、公告信息、文明倡导、垃圾袋与商品信息等模块,管理员可管理学生信息、基础信息、扔垃圾记录与商品兑换,学生端则支持个人信息、创意信息及兑换操作,并配有功能、性能与安全测试说明。目前已有196人学习下载,读者可借此快速理解SSM项目分层架构、数据模型设计与论文写作框架,为同类系统开发与答辩准备提供直接参考。
1. 从一份“校园垃圾分类管理系统”需求说起:Java 项目到底该怎么落地
很多同学拿到“基于 Java 校园垃圾分类管理系统设计与实现”这个题目时,第一反应是去搜现成源码,结果下载下来一堆跑不起来的压缩包,数据库连不上、依赖冲突、页面 404,最后只能硬着头皮自己写。我带过几届课程设计和毕设,这个题目的核心其实不是“垃圾分类”四个字有多复杂,而是你能不能把一套典型的 Java Web 业务系统从需求、建模、编码到部署完整走通。它解决的是校园场景下垃圾投放记录、分类积分、违规追溯和统计报表这几件事,适合计算机相关专业做课程设计或毕业设计,也适合刚学完 Java 基础、想找一个完整项目练手的开发者。下面我按实际做项目的顺序,把选型、建表、核心代码和踩过的坑一次讲清楚。
2. 技术选型与数据库设计:别一上来就堆框架
2.1 为什么我建议用 Spring Boot + MyBatis 而不是纯 JSP
这个题目最常见的两种做法:一种是纯 JSP + Servlet + JDBC,另一种是 Spring Boot + MyBatis + Thymeleaf 或前后端分离。如果你只是应付一个简单的课程作业,纯 JSP 确实能跑,但代码会非常散,SQL 写在 JSP 里、事务控制靠手动 commit,后期加一个“积分排行榜”功能就要改五六个文件。我一般会选 Spring Boot 2.7 + MyBatis-Plus + MySQL 8,原因很直接:依赖注入让 Service 层可以单独测试,MyBatis-Plus 的 BaseMapper 省掉大量单表 CRUD 代码,MySQL 8 的窗口函数还能直接算积分排名。
前端如果时间紧,用 Thymeleaf 做服务端渲染最稳,不用处理跨域和 Token 刷新;如果想让简历好看一点,就上 Vue3 + Element Plus,后端只提供 REST 接口。两种我都做过,服务端渲染的调试成本低很多,前后端分离则更贴近企业里的协作方式。这里给一个最小可跑的依赖清单,版本号按你本地 JDK 调整,JDK 8 用 Spring Boot 2.7.x,JDK 17 用 3.x。
<!-- pom.xml 关键依赖,JDK 8 环境 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 层,提供内嵌 Tomcat 和 MVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus,省掉大量单表 SQL --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动,注意 8.x 用 com.mysql.cj.jdbc.Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Lombok,减少 getter/setter 噪音 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这段配置里最容易被忽略的是 MySQL 驱动版本和 JDK 的匹配。JDK 8 配 MySQL 8.0.33 没问题,但如果你把 Spring Boot 升到 3.x,JDK 必须 17 以上,否则启动直接报 UnsupportedClassVersionError。MyBatis-Plus 的版本也要和 Spring Boot 对齐,3.5.3.1 对应 Boot 2.7,Boot 3 要用 3.5.5 以上。
2.2 四张核心表把业务撑起来
校园垃圾分类管理系统的数据模型不复杂,但字段设计要考虑后续统计。我一般会建四张主表:用户表、垃圾类别表、投放记录表、积分流水表。用户表区分学生、督导员、管理员三种角色;垃圾类别表存可回收、有害、厨余、其他四类及其单位积分;投放记录表是核心,记录谁在什么时候投了什么类别、重量多少、是否合格;积分流水表记录每次积分变动,方便对账和排行榜。
-- 用户表:role 用 0 学生 1 督导员 2 管理员 CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '学号/工号', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密存储', `real_name` VARCHAR(50) DEFAULT NULL, `role` TINYINT NOT NULL DEFAULT 0, `points` INT NOT NULL DEFAULT 0 COMMENT '当前积分冗余字段', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 投放记录表:status 0 待审核 1 合格 2 不合格 CREATE TABLE `delivery_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `category_id` INT NOT NULL, `weight` DECIMAL(10,2) NOT NULL COMMENT '单位 kg', `status` TINYINT NOT NULL DEFAULT 0, `inspector_id` BIGINT DEFAULT NULL COMMENT '审核督导员', `delivery_time` DATETIME NOT NULL, `remark` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_time` (`user_id`, `delivery_time`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个设计取舍值得说。第一,user表里冗余了points字段,而不是每次从流水表 sum,因为排行榜查询频率高,实时 sum 在数据量上万后会明显变慢;第二,delivery_record建了idx_user_time联合索引,因为“查某个学生最近一周的投放记录”是最常见的查询。注意weight用 DECIMAL 而不是 FLOAT,积分计算涉及金额和重量时,浮点误差会导致对账对不上,这是血泪经验。
3. 核心功能编码:投放记录、积分结算与审核流
3.1 投放记录提交接口与事务边界
投放记录提交是整个系统的入口,学生扫码或手动选择类别、输入重量,后端要完成三件事:写投放记录、根据类别积分规则算积分、写积分流水并更新用户总积分。这三步必须在同一个事务里,否则会出现记录写了但积分没加的情况。我一般把事务加在 Service 方法上,Controller 只做参数校验。
@Service public class DeliveryService { @Autowired private DeliveryRecordMapper recordMapper; @Autowired private UserMapper userMapper; @Autowired private PointsFlowMapper flowMapper; @Autowired private CategoryMapper categoryMapper; /** * 提交投放记录并结算积分 * @param userId 学生ID * @param categoryId 垃圾类别ID * @param weight 重量 kg */ @Transactional(rollbackFor = Exception.class) public void submit(Long userId, Integer categoryId, BigDecimal weight) { // 1. 查类别,拿到单位积分,比如可回收 10 分/kg Category category = categoryMapper.selectById(categoryId); if (category == null) { throw new BizException("垃圾类别不存在"); } // 2. 写投放记录,初始状态待审核 DeliveryRecord record = new DeliveryRecord(); record.setUserId(userId); record.setCategoryId(categoryId); record.setWeight(weight); record.setStatus(0); record.setDeliveryTime(LocalDateTime.now()); recordMapper.insert(record); // 3. 计算积分:重量 * 单位积分,保留整数 int points = weight.multiply(BigDecimal.valueOf(category.getUnitPoints())) .setScale(0, RoundingMode.HALF_UP).intValue(); // 4. 写流水 PointsFlow flow = new PointsFlow(); flow.setUserId(userId); flow.setRecordId(record.getId()); flow.setPoints(points); flow.setType(1); // 1 表示投放获得 flow.setCreateTime(LocalDateTime.now()); flowMapper.insert(flow); // 5. 更新用户总积分,用 SQL 原子自增避免并发覆盖 userMapper.addPoints(userId, points); } }逻辑说明:第 2 步插入记录后,MyBatis-Plus 会自动回填自增主键到record.getId(),流水表才能关联上。第 5 步没有用select再update,而是在 Mapper 里写UPDATE user SET points = points + #{points} WHERE id = #{userId},这样即使同一学生并发提交,积分也不会丢。参数上,weight建议在 Controller 层限制在 0.01 到 100 之间,超过 100kg 的单次投放大概率是误输入。
3.2 督导员审核与积分回滚
投放记录初始是待审核状态,督导员现场检查后标记合格或不合格。合格则积分保留,不合格要扣回积分并写一条负流水。这里最容易翻车的地方是重复审核:督导员手抖点了两次“不合格”,积分被扣两次。解决办法是在更新记录时加状态条件,只有当前状态为 0 才允许更新。
@Transactional(rollbackFor = Exception.class) public void audit(Long recordId, Long inspectorId, boolean pass) { DeliveryRecord record = recordMapper.selectById(recordId); if (record == null || record.getStatus() != 0) { throw new BizException("记录不存在或已审核"); } // 乐观锁式更新:只有 status=0 时才更新成功 int updated = recordMapper.audit(recordId, inspectorId, pass ? 1 : 2); if (updated == 0) { throw new BizException("该记录已被其他督导员审核"); } if (!pass) { // 不合格,扣回之前加的积分 int points = record.getWeight() .multiply(BigDecimal.valueOf(10)).intValue(); userMapper.addPoints(record.getUserId(), -points); PointsFlow flow = new PointsFlow(); flow.setUserId(record.getUserId()); flow.setRecordId(recordId); flow.setPoints(-points); flow.setType(2); // 2 表示审核扣回 flow.setCreateTime(LocalDateTime.now()); flowMapper.insert(flow); } }recordMapper.audit对应的 SQL 是UPDATE delivery_record SET status=#{status}, inspector_id=#{inspectorId} WHERE id=#{recordId} AND status=0。这个AND status=0就是防重复审核的关键,返回影响行数为 0 就说明已经被处理过。注意扣回积分时单位积分要重新查类别,不能写死 10,否则类别积分规则一改就对不上。
3.3 积分排行榜与统计报表
排行榜用一条 SQL 就能出,按积分倒序取前 20。统计报表按类别分组算总重量和总人次,给管理员看。这里用 MySQL 的DATE_FORMAT按天聚合,方便画折线图。
-- 积分排行榜 SELECT id, real_name, points FROM user WHERE role = 0 ORDER BY points DESC LIMIT 20; -- 近 7 天各类别投放统计 SELECT c.name AS category_name, COUNT(r.id) AS times, IFNULL(SUM(r.weight), 0) AS total_weight FROM category c LEFT JOIN delivery_record r ON r.category_id = c.id AND r.delivery_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND r.status = 1 GROUP BY c.id, c.name;LEFT JOIN保证即使某个类别近 7 天没有合格记录,也会显示 0 而不是消失。IFNULL处理 SUM 返回 NULL 的情况。如果数据量到几十万,delivery_time上的索引要单独建,联合索引idx_user_time对按时间范围的全表统计帮助有限。
4. 避坑与排查:那些让我熬夜的报错
4.1 中文乱码从数据库一路传到页面
现象:学生姓名在页面上显示成问号,或者提交中文类别名后数据库里是乱码。原因通常有三层:数据库连接 URL 没加characterEncoding=utf8、建表时用了latin1、Tomcat 的 URI 编码不是 UTF-8。解决:连接串写成jdbc:mysql://localhost:3306/garbage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,建表统一utf8mb4,Spring Boot 配置文件里加server.servlet.encoding.charset=UTF-8和force=true。三层都对齐才不会漏。
4.2 时间字段差 8 小时
现象:投放时间存进去是 14:00,查出来变成 06:00。原因是 MySQL 驱动 8.x 默认用 UTC 时区,而 JVM 在东八区。解决:连接串加serverTimezone=Asia/Shanghai,实体类时间字段用LocalDateTime而不是java.util.Date,并在application.yml里配spring.jackson.time-zone=GMT+8。如果已经存了错数据,只能写脚本批量加 8 小时修正。
4.3 积分并发提交导致总数对不上
现象:两个督导员同时审核同一条记录,或者学生快速点两次提交,积分流水和用户总积分不一致。原因:没有加状态条件或没有用原子更新。解决:审核 SQL 必须带AND status=0,积分更新必须用points = points + ?而不是先查后改。如果业务允许,还可以在delivery_record上加唯一索引uk_user_time限制同一学生同一秒只能提交一次。
4.4 MyBatis-Plus 逻辑删除和唯一索引冲突
现象:删除了一个用户后,想用同样的学号重新注册,报唯一键冲突。原因:逻辑删除只是把deleted置 1,数据库里那条记录还在,uk_username仍然生效。解决:要么唯一索引改成(username, deleted)联合唯一,要么物理删除。我一般建议学生表用物理删除,因为学号是真实身份标识,逻辑删除反而带来更多麻烦。
4.5 前端传参日期格式导致 400
现象:Vue 页面用new Date()传时间,后端LocalDateTime接收报 400。原因:默认反序列化只认yyyy-MM-dd HH:mm:ss这种格式,ISO 8601 带 T 和 Z 的字符串解析不了。解决:在 DTO 字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),或者前端统一用dayjs格式化后再传。这个坑在前后端分离项目里几乎必踩。
5. 部署上线与一个提效技巧:把“能跑”变成“敢演示”
项目写完只是第一步,答辩或演示时最怕现场启动失败。我一般会做两件事:一是用 Docker Compose 把 MySQL 和应用打包,二是准备一个数据初始化脚本。Docker Compose 的好处是换台电脑也能一键起,不用重新装数据库。
# docker-compose.yml 最小可用版本 version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: garbage ports: - "3306:3306" volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql app: build: . ports: - "8080:8080" depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/garbage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghaiinit.sql放在项目根目录,容器第一次启动会自动执行建表和插入测试数据。注意depends_on只保证启动顺序,不保证 MySQL 已经初始化完成,应用启动时可能连不上。稳妥做法是在应用里加重试,或者用wait-for-it.sh脚本等 MySQL 端口通了再启动。
另一个提效技巧是接口自测。不要每次都用浏览器点,用curl或 Postman 把核心接口跑一遍,尤其是审核和积分结算这种有事务的接口。我习惯在src/test下写几个@SpringBootTest集成测试,用 H2 内存库跑,提交代码前跑一遍,能挡住大部分低级错误。
@SpringBootTest @Transactional class DeliveryServiceTest { @Autowired private DeliveryService deliveryService; @Test void submit_should_add_points() { // 假设初始化数据里 userId=1, categoryId=1 单位积分 10 deliveryService.submit(1L, 1, new BigDecimal("2.5")); // 断言用户积分增加了 25 User user = userMapper.selectById(1L); assertEquals(25, user.getPoints()); } }这个测试用@Transactional保证跑完回滚,不会污染数据库。参数上,2.5kg乘以单位积分 10 得到 25,正好验证了 DECIMAL 计算的正确性。如果你用 FLOAT,这里可能得到 24 或 26,对账时就会出问题。
最后说一个我自己的习惯:每次改完积分规则或审核逻辑,一定手动走一遍“提交—审核不合格—再提交—审核合格”的完整流程,看流水表和用户总积分是否一致。这个习惯帮我省掉了至少三次答辩现场被老师问“为什么积分对不上”的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取