简介:这份资源是面向高校计算机相关专业学生与Java初学者的一套垃圾分类管理系统完整项目,可直接用于毕业设计、课程作业或自学练手。项目采用前后端分离思路,客户端覆盖登录注册、垃圾名称查询与分类介绍、活动参与获取积分、积分商城兑换、用户信息与密码修改、积分转赠等模块;服务端则负责用户信息、活动、积分商城物品管理及历史兑换记录查看,业务闭环完整,适合作为综合实践参考。压缩包共548个文件,约3.99MB,以xml配置、java源码、png与jpg图片、js脚本、json数据、vue组件为主,另含scss、wxss、wxml等小程序样式与结构文件,以及sql建库脚本和md说明,便于快速还原运行环境。目前已有287人学习下载,读者可从中获取完整功能实现、数据库设计、前后端交互逻辑与目录组织方式,为毕业设计选题和答辩准备提供直接参考。
1. 从一份能跑通的 Java 垃圾分类管理系统说起
很多同学做毕业设计时,选题卡在“垃圾分类”上,觉得业务太简单,怕答辩被问“这有什么技术含量”。但真正动手写起来才发现,问题不在业务复杂度,而在数据模型和分类逻辑的耦合方式。这份基于 Java 的垃圾分类管理系统,核心解决的是“垃圾条目—分类规则—投放记录—积分反馈”这条链路的闭环管理,适合软件工程、计算机专业的本科毕业设计或课程作业,也适合需要一套完整 CRUD + 业务规则引擎练手的 Java 初学者。它不是一个玩具 Demo,而是把垃圾分类这件小事拆成了可配置的规则表、可追溯的投放流水和可扩展的积分策略,让你在答辩时有东西可讲,在代码层面有边界可守。
2. 技术选型与数据模型:为什么用 Spring Boot + MyBatis 而不是裸 Servlet
2.1 分层架构的取舍理由
拿到这个题目,第一反应可能是用 JSP + Servlet 直接写,毕竟“垃圾分类”听起来就是几张表的增删改查。但真写到积分计算和分类规则匹配时,裸 Servlet 的弊端就暴露了:请求参数散落在各个 doGet/doPost 里,事务控制要手动开 JDBC,前端页面和 Java 代码混在一起,改一个字段要动三四个文件。常见做法是上 Spring Boot + MyBatis,把 Controller、Service、Mapper 三层分清楚,分类规则用配置表驱动,积分策略用策略模式解耦。
我一般会这样定分层:Controller 只负责接收参数和返回 JSON;Service 层处理业务规则,比如“判断垃圾属于哪一类”和“计算本次投放积分”;Mapper 层只做单表 CRUD 和简单关联查询。这样做的直接好处是,答辩时老师问“你的分类逻辑写在哪”,你可以明确说在 Service 的classifyGarbage方法里,而不是在某个 JSP 的 scriptlet 里。
2.2 核心表结构设计
垃圾分类管理系统的表不用多,但字段要经得起追问。下面是我建议的最小可用表结构,共 5 张表:
| 表名 | 作用 | 关键字段 |
|---|---|---|
garbage_category | 垃圾分类大类(可回收、有害、厨余、其他) | id, name, code, description |
garbage_item | 具体垃圾条目 | id, name, category_id, keywords, status |
classification_rule | 分类匹配规则 | id, item_id, match_type, match_value, priority |
disposal_record | 投放记录 | id, user_id, item_id, weight, points, create_time |
user_points | 用户积分汇总 | id, user_id, total_points, update_time |
其中classification_rule是容易被忽略但答辩时很加分的设计。比如“电池”这个条目,可以配置多条规则:关键词包含“电池”且不是“锂电池”时归为有害垃圾,优先级设为 10;而“锂电池”单独一条规则,优先级设为 20。这样当老师问“如果用户输入‘废电池’怎么办”,你可以说规则表里配了模糊匹配,match_type设为LIKE,match_value设为%电池%,系统按优先级从高到低匹配。
2.3 分类匹配的代码实现
下面这段代码是 Service 层分类匹配的核心逻辑,用 Java 8 的 Stream 做规则排序和过滤:
@Service public class ClassificationService { @Autowired private ClassificationRuleMapper ruleMapper; /** * 根据用户输入的垃圾名称匹配分类 * @param inputName 用户输入的垃圾名称 * @return 匹配到的垃圾条目,未匹配返回 null */ public GarbageItem classify(String inputName) { // 1. 查询所有启用的规则,按优先级降序排列 List<ClassificationRule> rules = ruleMapper.selectEnabledRulesOrderByPriorityDesc(); // 2. 遍历规则,找到第一条匹配的 for (ClassificationRule rule : rules) { if (matchRule(inputName, rule)) { return rule.getGarbageItem(); } } // 3. 没有匹配到任何规则,返回 null,由上层处理为“其他垃圾” return null; } private boolean matchRule(String input, ClassificationRule rule) { String matchValue = rule.getMatchValue(); switch (rule.getMatchType()) { case "EXACT": return input.equals(matchValue); case "LIKE": return input.contains(matchValue.replace("%", "")); case "REGEX": return input.matches(matchValue); default: return false; } } }逻辑说明:先按优先级降序取规则,保证“锂电池”这种特殊规则排在“电池”通用规则前面。matchRule方法支持精确、模糊、正则三种匹配方式,LIKE类型里我把%去掉再用contains判断,是为了兼容数据库 LIKE 语法但避免 SQL 注入风险。参数方面,priority字段建议用整数,越大越优先,初始数据里通用规则给 10,特殊规则给 20 或 30。
提示:规则表一定要加
status字段做软删除,否则后期想停用某条规则只能物理删除,投放记录里的外键会断掉。
3. 从零搭建可运行环境:JDK、Maven 与数据库配置
3.1 环境版本与依赖清单
这个项目对版本不挑,但为了避免玄学问题,我建议用 JDK 8 或 JDK 11,Spring Boot 2.7.x,MyBatis 2.3.x,MySQL 5.7 或 8.0。前端如果不想写 Vue,用 Thymeleaf 模板引擎也能跑,但既然关键词里带了 Spring Boot + Vue,那前后端分离更符合当前毕业设计的“工作量”要求。
pom.xml里必须显式声明的依赖:
<dependencies> <!-- Web 层 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Lombok 简化实体类 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>参数说明:mybatis-spring-boot-starter的版本不要用 3.x,3.x 对应 Spring Boot 3 和 JDK 17,如果你本地是 JDK 8,启动会直接报UnsupportedClassVersionError。MySQL 驱动 8.x 的类名是com.mysql.cj.jdbc.Driver,URL 里要加serverTimezone=Asia/Shanghai,否则插入时间会差 8 小时。
3.2 数据库初始化脚本
建库建表用下面这段 SQL,直接复制到 Navicat 或 MySQL 命令行执行:
CREATE DATABASE garbage_sort DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE garbage_sort; CREATE TABLE garbage_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT '分类名称', code VARCHAR(16) NOT NULL COMMENT '分类编码', description VARCHAR(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE garbage_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '垃圾名称', category_id BIGINT NOT NULL, keywords VARCHAR(255) DEFAULT NULL COMMENT '辅助关键词,逗号分隔', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', INDEX idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE classification_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, item_id BIGINT NOT NULL, match_type VARCHAR(16) NOT NULL COMMENT 'EXACT/LIKE/REGEX', match_value VARCHAR(128) NOT NULL, priority INT DEFAULT 10 COMMENT '越大越优先', status TINYINT DEFAULT 1, INDEX idx_item (item_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:garbage_item和classification_rule是一对多关系,一个垃圾条目可以有多条匹配规则。keywords字段是冗余设计,方便后台做快速搜索,但实际分类匹配还是走规则表。字符集统一用utf8mb4,避免用户输入生僻字或 emoji 时插入失败。
3.3 application.yml 关键配置
spring: datasource: url: jdbc:mysql://localhost:3306/garbage_sort?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.garbage.entity configuration: map-underscore-to-camel-case: true参数说明:map-underscore-to-camel-case必须开,否则数据库的category_id映射不到 Java 的categoryId,查询结果里这个字段永远是 null。mapper-locations指向 XML 文件目录,如果你用纯注解方式写 Mapper,这行可以去掉,但复杂关联查询还是 XML 更清晰。
注意:
serverTimezone不要写成UTC,否则前端展示的投放时间会比实际时间少 8 小时,答辩演示时会被一眼看穿。
4. 避坑与排查:分类匹配和积分计算里的血泪经验
4.1 规则优先级相同导致匹配结果随机
现象:用户输入“塑料瓶”,有时匹配到“可回收物”,有时匹配到“其他垃圾”,刷新几次结果还不一样。
原因:classification_rule表里两条规则的priority都是默认值 10,MySQL 在ORDER BY priority DESC时对相同值的行返回顺序不确定,导致每次查询拿到的第一条规则可能不同。
解决:给priority加唯一性约束不现实,但可以在ORDER BY priority DESC, id ASC里加第二排序字段,保证相同优先级时按 id 升序取第一条。更稳妥的做法是初始化数据时就把优先级拉开,通用规则 10,特殊规则 20,兜底规则 5。
4.2 积分计算出现小数精度丢失
现象:用户投放 0.5kg 厨余垃圾,应得 5 积分,但数据库里存的是 4.999999。
原因:积分字段用了FLOAT或DOUBLE,Java 里用float做乘法。二进制浮点数无法精确表示 0.1 这类十进制小数。
解决:积分字段用DECIMAL(10,2),Java 实体类用BigDecimal,乘法用BigDecimal.multiply()并指定RoundingMode.HALF_UP。如果已经用了double,至少在前端展示时用String.format("%.0f", points)做四舍五入。
4.3 模糊匹配把“电池”和“锂电池”混为一谈
现象:用户输入“锂电池”,系统匹配到了“电池”的通用规则,归为有害垃圾,但实际锂电池在部分城市标准里属于可回收物或单独类别。
原因:规则表里只有一条LIKE %电池%的规则,没有为“锂电池”配置更高优先级的精确规则。
解决:在classification_rule里插入一条match_type=EXACT, match_value=锂电池, priority=30的规则,确保它排在通用规则前面。同时把通用规则的match_value改成%电池%但排除“锂电池”,这需要在代码里加一个excludeKeywords字段,或者用正则^(?!.*锂电池).*电池.*$。
4.4 投放记录表数据量大了之后查询变慢
现象:系统跑了一个月,disposal_record表有几十万条数据,用户查自己的投放历史要等好几秒。
原因:user_id和create_time上没有索引,每次查询都是全表扫描。
解决:加联合索引ALTER TABLE disposal_record ADD INDEX idx_user_time (user_id, create_time DESC);。如果数据量继续增长,考虑按月份分表,或者把历史记录归档到disposal_record_history表,主表只保留最近三个月。
4.5 前端传参中文乱码导致分类失败
现象:用户在搜索框输入“废纸箱”,后端接收到的却是“废纸箱”,分类匹配直接返回 null。
原因:前端请求头没有设置Content-Type: application/json;charset=UTF-8,或者后端application.yml里没有配置server.servlet.encoding.charset=UTF-8。
解决:Spring Boot 2.x 默认已经配了 UTF-8,但如果你用了自定义过滤器或拦截器,可能把编码覆盖了。检查server.servlet.encoding.enabled=true和force=true是否生效。前端用 axios 时,确保headers: { 'Content-Type': 'application/json;charset=UTF-8' }。
5. 进阶技巧:用策略模式替换 if-else 积分计算
5.1 为什么要把积分规则抽出来
初始版本里,积分计算大概率是这么写的:
if ("可回收物".equals(category)) { points = weight * 10; } else if ("厨余垃圾".equals(category)) { points = weight * 5; } else if ("有害垃圾".equals(category)) { points = weight * 20; } else { points = 0; }这段代码能跑,但答辩时老师问“如果我想调整可回收物的积分倍率,或者增加一个‘投放次数奖励’,你要改几个地方”,你就只能指着这段 if-else 说“改这里”。更麻烦的是,如果不同城市的积分规则不同,这段代码会膨胀成几十个分支。
5.2 策略模式的具体实现
定义一个积分策略接口,每个分类对应一个实现类:
public interface PointsStrategy { BigDecimal calculate(BigDecimal weight); String getCategoryCode(); } @Component public class RecyclablePointsStrategy implements PointsStrategy { @Override public BigDecimal calculate(BigDecimal weight) { return weight.multiply(new BigDecimal("10")).setScale(0, RoundingMode.HALF_UP); } @Override public String getCategoryCode() { return "RECYCLABLE"; } } @Component public class HarmfulPointsStrategy implements PointsStrategy { @Override public BigDecimal calculate(BigDecimal weight) { return weight.multiply(new BigDecimal("20")).setScale(0, RoundingMode.HALF_UP); } @Override public String getCategoryCode() { return "HARMFUL"; } }然后在 Service 里注入所有策略,用 Map 做路由:
@Service public class PointsService { private final Map<String, PointsStrategy> strategyMap = new HashMap<>(); @Autowired public PointsService(List<PointsStrategy> strategies) { for (PointsStrategy strategy : strategies) { strategyMap.put(strategy.getCategoryCode(), strategy); } } public BigDecimal calculatePoints(String categoryCode, BigDecimal weight) { PointsStrategy strategy = strategyMap.get(categoryCode); if (strategy == null) { return BigDecimal.ZERO; } return strategy.calculate(weight); } }逻辑说明:Spring 启动时会自动把所有PointsStrategy的实现类注入到List里,构造函数里遍历并放入 Map。这样新增一个分类的积分规则,只需要新建一个实现类,不用改PointsService的任何代码。参数方面,weight用BigDecimal接收,避免浮点误差;setScale(0, RoundingMode.HALF_UP)表示保留整数位并四舍五入。
5.3 验证策略是否生效
写完策略后,用单元测试验证一下:
@SpringBootTest class PointsServiceTest { @Autowired private PointsService pointsService; @Test void testRecyclablePoints() { BigDecimal points = pointsService.calculatePoints("RECYCLABLE", new BigDecimal("1.5")); assertEquals(new BigDecimal("15"), points); } @Test void testUnknownCategory() { BigDecimal points = pointsService.calculatePoints("UNKNOWN", new BigDecimal("1.0")); assertEquals(BigDecimal.ZERO, points); } }跑通这两个测试,说明策略路由和兜底逻辑都没问题。答辩时如果老师问“怎么保证新增分类不影响旧代码”,你可以直接把这个测试类和策略接口展示出来。
5.4 一个容易被忽略的细节:积分汇总的并发问题
用户投放垃圾后,user_points表的total_points需要累加。如果两个请求同时到达,先查后加会导致积分丢失。常见做法是用 SQL 的原子更新:
UPDATE user_points SET total_points = total_points + #{points}, update_time = NOW() WHERE user_id = #{userId};而不是在 Java 里int total = userPoints.getTotalPoints() + points;再update。后者在并发场景下就是典型的读-改-写竞态。如果user_points表里没有该用户的记录,先用INSERT ... ON DUPLICATE KEY UPDATE做插入或更新。
从那以后我每次做积分或库存类功能,都会先问一句“这个字段是读出来改再写,还是直接 SQL 原子更新”,如果是前者,立刻改成后者。希望帮到你。
本文还有配套的精品资源,点击获取