news 2026/10/7 10:26:00

Java垃圾分类管理系统毕业设计:Spring Boot+MyBatis规则引擎与积分策略实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java垃圾分类管理系统毕业设计:Spring Boot+MyBatis规则引擎与积分策略实战

简介:这份资源是面向高校计算机相关专业学生与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 原子更新”,如果是前者,立刻改成后者。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 10:25:59

SpringBoot+Vue二手滑板交易系统:从数据库设计到部署实战

滑板圈子里有个很实在的现象&#xff1a;装备的流通速度比大多数运动器材都快。原因不复杂——动作练到一定程度&#xff0c;板面磨穿了要换&#xff0c;桥和轮子的损耗程度不一样要拆开来出&#xff0c;新手入坑又想先收一套成色好的练手&#xff0c;二手市场就这么被需求撑起…

作者头像 李华
网站建设 2026/10/7 10:25:56

差分数组经典应用:从“最高的牛”理解区间更新与前缀和

说实话&#xff0c;第一次拿到这题的时候&#xff0c;我盯着题目愣了好一会儿。题目描述绕来绕去的&#xff0c;又是"最高的牛"又是"互相看见"&#xff0c;乍一看跟差分数组八竿子打不着。但等我把条件翻译完&#xff0c;才发现这就是差分的一个标准模板题…

作者头像 李华
网站建设 2026/10/7 10:25:50

Modbus RTU单报文收发:协议边界与CRC校验实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 10:25:47

UE编辑器工具开发:用HighlightPickedActors实现视口点选高亮

做自定义编辑器工具时&#xff0c;最常遇到的一个需求就是&#xff1a;让用户在关卡视口里点一下某个物件&#xff0c;工具立刻把这个物件高亮出来&#xff0c;然后拿着这个物件去干后续的活——批量改材质、收集资产信息、检查贴图尺寸&#xff0c;诸如此类。 这个动作在运行…

作者头像 李华
网站建设 2026/10/7 10:24:29

Hot 100普通数组刷题笔记:六道高频面试题的边界与复杂度解析

如果你准备面试或者正在刷题&#xff0c;LeetCode Hot 100应该是绕不开的一份清单。这份榜单把高频面试题按数据结构分成了十几个分区&#xff0c;其中“普通数组”这一栏很不起眼&#xff0c;题量不大&#xff0c;也不涉及链表、树、图这些复杂结构&#xff0c;但它是我刷了三…

作者头像 李华
网站建设 2026/10/7 10:23:21

从Docker到Kubernetes:容器化部署到集群运维的实战排错指南

如果你已经能熟练地写 Dockerfile、能跑通docker-compose up -d&#xff0c;甚至习惯了把 MySQL、Redis 都塞进容器里跑&#xff0c;那说实话&#xff0c;单机容器化这一关你已经过了。但"阶段二"的挑战&#xff0c;恰好是从你试图把这些经验搬到 Kubernetes 集群里那…

作者头像 李华