SpringBoot垃圾分类识别小程序:从架构到部署的实战避坑指南
简介:基于SpringBoot框架的垃圾分类识别小程序项目源码,面向计算机、通信、人工智能、自动化等专业的学生与从业者,适合用作毕业设计、期末课程设计或课程大作业的完整参照。项目提供完整源码、数据库脚本和部署说明,代码经调试测试,确保可正常运行。资源包共208个文件,以163个Java源文件为主体,配合28个XML配置、2个YAML配置、3个properties属性文件,以及SQL数据库脚本、依赖JAR包和Markdown说明等,整体44.13MB,目录结构清晰,便于按模块定位代码。目前已有97人学习下载,具备较高的学习借鉴价值。内容涵盖小程序端、后台管理、核心识别逻辑等部分,从环境配置到运行部署均有说明,适合小白逐步上手;基础扎实者还可在此基础上二次开发,扩展分类识别功能或调整业务流程,灵活适配不同场景。
1. 基于SpringBoot的垃圾分类识别小程序:毕业设计里最容易翻车的不是算法,是联调
拿到一份「基于SpringBoot的垃圾分类识别小程序」的毕业设计源码包,很多人第一反应是去看识别算法写得高不高级,实际上这套项目能不能在答辩前跑通,九成卡在数据库连接串、小程序域名校验、文件上传大小这三个最不起眼的环节。标题里的「源码+数据库+部署说明」恰恰点明了它的交付形态:SpringBoot 负责接口,小程序负责拍照和展示,MySQL 存分类知识和识别记录,部署说明负责把你从零带到能演示。适合三类人:Java 基础停留在 SSM 阶段、小程序只写过页面跳转、以及想在答辩时把「技术选型」讲出层次感的人。识别本身用「规则表兜底、模型可插拔」的策略,先把闭环跑通,再谈准确率,这比一上来就训练模型务实得多。
2. 先把架构立住:从拍一张垃圾照片到返回分类结果的数据流
2.1 为什么是 SpringBoot + 微信小程序 + MySQL 这套组合
毕业设计选技术栈,第一原则不是「新」,而是「每层都有东西可讲、每层你都能接得住评委追问」。SpringBoot 相比 SSM 少写大量 XML 配置,自动装配加内嵌 Tomcat,本地启动只需要一条mvn spring-boot:run,环境搭建成本低;微信小程序不用装 APK,评委用微信扫码就能预览,省去安卓和 iOS 双端适配的麻烦;MySQL 是面试必问的关系型数据库,建表语句、索引、联表查询都能在答辩时直接展示。
我一般会建议把 Redis 缓存、消息队列这类看起来加分、实际经不起追问的组件从主链路里拿掉,放到「未来展望」里一句话带过。原因是评委一旦追问「缓存和数据库一致性怎么保证」「消息丢失怎么处理」,没有真实生产经验很难圆回来。这套项目的主链路保持简单:小程序 → SpringBoot → MySQL,最多加一个 OCR 辅助服务,前后端走 HTTP + JSON,第一版连 Nginx 都不需要。
关于前端选型还有一个常见做法:原生微信小程序、uniapp、Taro 三者都能做。原生小程序对毕设最稳妥,因为微信开发者工具直接导入就能跑,不需要额外构建链;如果你以后想跨端复用,再考虑用 uniapp 重写,构建产物还能以 H5 形式嵌入 SpringBoot 的 static 目录实现单端口部署,这一点放到最后一章展开。
2.2 一条识别请求的完整链路:接口先行
动手写代码之前,先把接口定下来。这套项目通常需要三个核心接口:
| 接口路径 | 方法 | 入参 | 出参 | 用途 |
|---|---|---|---|---|
| /api/garbage/recognize | POST | file 图片文件、text 可选文本 | 识别结果对象 | 小程序拍照/相册上传主入口 |
| /api/garbage/recognizeByText | POST | text 物品名称 | 识别结果对象 | 文本通道,调试和演示用 |
| /api/garbage/record/page | GET | page、size | 分页记录列表 | 历史记录页加载更多 |
数据流是这样的:小程序端调用wx.chooseMedia拿到临时文件路径,wx.uploadFile以file字段名上传到 SpringBoot 的 Controller;Controller 收到MultipartFile后交给 Service;如果带了文本参数,直接走规则表匹配,没有文本则先调 OCR 服务抽取出物品关键词,再走规则表;拿到分类结果后写入garbage_record表,最后把 JSON 返回给小程序渲染结果页。
这个双通道设计是我强烈建议保留的:recognizeByText这个文本接口让你在没有 GPU、OCR 服务没起时也能完整演示「输入物品名 → 返回分类」,而图片接口负责展示完整链路。注意wx.uploadFile里的name字段必须和后端@RequestParam("file")保持一致,这是前后端联调时最容易挂的一环。
2.3 识别策略:为什么先用规则表,而不是训练模型
明确一个反直觉结论:纯规则表 + OCR 是这个课题性价比最高的起步方案。图像分类模型不是不能做,而是训练一个能稳定四分类的模型需要标注数据集、GPU 训练时间和反复调参,毕业设计周期里很容易变成烂尾工程。常见做法是两阶段:先用 OCR 把图片里的文字抽出来,再用一张「物品名称 → 垃圾类别」的映射表做匹配,表里查不到的返回低置信度兜底结果。
规则表的优点是可解释、可运营、零训练成本。答辩时被问「为什么不用深度学习」,你可以回答:第一,垃圾分类场景的难点不在视觉特征而在语义理解,比如同样形状的瓶子,材质不同分类就不同;第二,规则表支持运营后台增量维护,新物品加一条记录就能识别,而模型每次更新都要重新训练。缺点是语义盲区,比如「那个装外卖的盒子」这种口语描述就匹配不上,所以实际使用时要在小程序端提示用户拍清楚标签或输入品牌名。
3. 后端识别核心:用 SpringBoot 把「垃圾图片分类」做成可调接口
3.1 接收图片与文本的 Controller 写法
后端入口建议用一个统一的 Result 包装类,约定code=0表示成功,非 0 表示业务失败。先看 Controller 层:
@RestController @RequestMapping("/api/garbage") public class GarbageRecognizeController { private final GarbageRecognizeService garbageService; public GarbageRecognizeController(GarbageRecognizeService garbageService) { this.garbageService = garbageService; } @PostMapping("/recognize") public Result<RecognizeVo> recognize( @RequestParam("file") MultipartFile file, @RequestParam(value = "text", required = false) String text) throws Exception { // 双通道:带 text 直接走文本识别,避免每次图片都触发 OCR 冷启动 if (StringUtils.hasText(text)) { return Result.ok(garbageService.recognizeByText(text)); } return Result.ok(garbageService.recognizeImage(file)); } @PostMapping("/recognizeByText") public Result<RecognizeVo> recognizeByText(@RequestParam("text") String text) { return Result.ok(garbageService.recognizeByText(text)); } }逻辑说明:@RequestParam("file")对应小程序wx.uploadFile的name: 'file',两个名字不一致会直接收不到文件;required = false的text参数实现了双通道入口,调试时你在浏览器里拼一个?text=矿泉水瓶就能测规则表,不需要每次走完整 OCR 链路。
参数说明:如果你在浏览器里用 fetch 调试该接口,需要后端配置 CORS,但小程序wx.request本身不受浏览器同源策略限制,所以不要被网页开发经验误导去白加@CrossOrigin。另外 Controller 里不要写业务逻辑,识别、落库、异常处理都放到 Service,保持这一层瘦一点,答辩时分层讲起来也清晰。
3.2 规则匹配 Service:查数据库而不是硬编码
很多毕设会把分类映射写死在 HashMap 里,这是第一版最容易犯的错。把物品表挪到 MySQL 后,运营想加一个新物品只需要插一条 SQL,不用改代码重新打包。核心匹配逻辑如下:
@Service public class GarbageRecognizeServiceImpl implements GarbageRecognizeService { private final GarbageItemMapper garbageItemMapper; public GarbageRecognizeServiceImpl(GarbageItemMapper garbageItemMapper) { this.garbageItemMapper = garbageItemMapper; } @Override public RecognizeVo recognizeByText(String text) { // 从数据库加载映射,实际项目可加本地缓存避免每次查库 List<GarbageItem> items = garbageItemMapper.loadAll(); for (GarbageItem item : items) { // contains 比 equals 容错高,能匹配「XX矿泉水瓶」这类带修饰语的输入 if (text.contains(item.getItemName()) || item.getAliases().stream().anyMatch(text::contains)) { return buildVo(item, 0.9); } } return RecognizeVo.unknown("未识别到物品,试试补充牌子或材质,例如:农夫山泉矿泉水瓶"); } }逻辑说明:loadAll()从garbage_item表加载全部物品映射,aliases字段存同义词,例如「易拉罐」和「铝罐」指向同一分类。这里用contains而不是equals,因为用户可能输入「500ml矿泉水瓶」「用过的纸巾」这类带修饰语的描述。buildVo(item, 0.9)里 0.9 是规则匹配的固定置信度,表示「按规则表确认」;完全没命中时返回unknown,并附带一句引导用户补充信息的提示。
这里有个实操细节:文字识别对「印刷体品牌名」最稳,对「手写」「异形包装」识别率很差,所以小程序端要提示用户拍标签正面。另外 OCR 服务建议单独封装一个OcrClient接口,第一版可以用本地开源 OCR 的 HTTP 服务,后续可以无痛替换成云端 OCR,调用方不需要改代码。
3.3 返回结构、失败语义与超时设置
接口返回格式统一如下,小程序端只需要判断code就能决定走成功分支还是错误提示:
{ "code": 0, "msg": "ok", "data": { "typeCode": 2, "typeName": "可回收物", "confidence": 0.9, "matchedItem": "矿泉水瓶", "tip": "请压扁后投入可回收垃圾桶" } }业务失败约定:40001表示未识别到物品,40002表示参数错误,500表示服务异常。typeCode用 1 到 4 对应四分类:1 可回收物、2 有害垃圾、3 厨余垃圾、4 其他垃圾,前端按这个数字映射图标和颜色。
超时参数也要提前约定。OCR 服务首次调用有冷启动,动辄两三秒,所以小程序端wx.uploadFile的timeout建议设 30000;后端的 OCR 客户端 HTTP 连接超时设 5000、读超时设 10000,避免下游 OCR 卡死时把整个接口拖住。如果 OCR 异常,Service 里要 try-catch 降级:返回「识别服务暂不可用」,而不是让前端收到一个 500 白屏。
4. 小程序端联调:识别页、结果页与历史记录的加载更多
4.1 拍照选图与上传:先压缩再上传
小程序端的主入口逻辑集中在识别页,代码结构如下:
async function chooseAndUpload() { const res = await wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], // 真机上务必用压缩图,原图容易触发后端大小限制 }); const tempFilePath = res.tempFiles[0].tempFilePath; wx.uploadFile({ url: `${getApp().globalData.baseUrl}/api/garbage/recognize`, filePath: tempFilePath, name: 'file', timeout: 30000, success: (resp) => { const data = JSON.parse(resp.data); if (data.code === 0) { wx.setStorageSync('lastResult', Object.assign({}, data.data, { imagePath: tempFilePath })); wx.navigateTo({ url: '/pages/result/result' }); } else { wx.showToast({ title: data.msg || '识别失败', icon: 'none' }); } }, fail: () => { wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }); }, }); }逻辑说明:sourceType同时传相册和相机,真机上会弹出选择菜单;sizeType: ['compressed']是关键,压缩图通常只有几百 KB,能避开后端默认 1MB 单文件限制。wx.uploadFile的name字段必须和后端@RequestParam("file")对应,这里错了会直接报「Required part 'file' is not present」。
参数说明:成功回调里先把识别结果存入本地缓存再跳转结果页,这样结果页不会出现短暂白屏。联调阶段后端还没就绪时,建议先在本地写一份 mock 数据放在data里渲染页面,把样式定完再切真实请求,这套「页面先行、接口后接」的流程能省下不少排队等后端的时间。
4.2 结果页:动态标题与四分类视觉反馈
结果页拿到数据后,第一件事是用wx.setNavigationBarTitle动态设置标题,让用户一眼看到「识别结果:可回收物」:
Page({ data: { result: null, typeMeta: { 1: { name: '可回收物', color: '#1E88E5', icon: '/assets/recyclable.png' }, 2: { name: '有害垃圾', color: '#E53935', icon: '/assets/harmful.png' }, 3: { name: '厨余垃圾', color: '#43A047', icon: '/assets/kitchen.png' }, 4: { name: '其他垃圾', color: '#757575', icon: '/assets/other.png' }, }, }, onLoad(options) { const result = wx.getStorageSync('lastResult'); this.setData({ result }); wx.setNavigationBarTitle({ title: `识别结果:${result.typeName}` }); }, });四分类配色用行业约定:可回收物蓝色、有害垃圾红色、厨余垃圾绿色、其他垃圾灰色。页面布局建议是顶部大图展示上传的原始照片,中间是分类结果卡片,下方是投放建议tip。这个卡片是答辩时最容易出视觉效果的模块,值得多花时间调样式,让分类图标足够大、配色足够鲜明。
4.3 历史记录页:微信小程序页面列表加载更多
历史记录页不要一次性把全部数据拉回来,用分页加载更符合移动端习惯,也对应数据库的分页查询。onReachBottom是微信小程序触底加载的标准入口:
Page({ data: { list: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadPage(1, true); }, onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.loadPage(this.data.page + 1, false); }, loadPage(page, isReset) { this.setData({ loading: true }); wx.request({ url: `${getApp().globalData.baseUrl}/api/garbage/record/page`, data: { page, size: this.data.pageSize }, success: (resp) => { const records = resp.data.data.records; this.setData({ list: isReset ? records : this.data.list.concat(records), page, hasMore: records.length === this.data.pageSize, loading: false, }); }, }); }, });逻辑说明:hasMore的判定用「本次返回条数是否等于 pageSize」,等于说明还有货,小于说明到底了;loading锁是防止 iOS 上onReachBottom连续触发导致重复请求;concat追加而不是覆盖,保证滚动位置不跳。后端对应的是LIMIT #{size} OFFSET #{page}这类分页 SQL,或者直接用 MyBatis PageHelper,注意 PageHelper 的页码是从 1 开始的,跟前端的 page 语义对齐就不会出现「第一页数据丢了」的问题。
5. 数据库与部署常见问题排查:从建表到跑通的五条踩坑记录
5.1 三张表的最小表结构
这套项目的数据模型建议拆成三张表:用户表、垃圾知识表、识别记录表。知识表单列出来,方便运营维护:
CREATE TABLE garbage_item ( id INT PRIMARY KEY AUTO_INCREMENT, item_name VARCHAR(64) NOT NULL COMMENT '物品名称,如矿泉水瓶', aliases VARCHAR(255) DEFAULT '' COMMENT '同义词,用逗号分隔:易拉罐,铝罐', garbage_type_code TINYINT NOT NULL COMMENT '1可回收 2有害 3厨余 4其他', tip VARCHAR(255) DEFAULT '' COMMENT '投放建议,如压扁后投放', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_type (garbage_type_code) ) COMMENT '垃圾分类知识表'; CREATE TABLE garbage_record ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT '微信用户标识', image_url VARCHAR(255) DEFAULT '' COMMENT '上传图片保存路径', matched_item VARCHAR(64) DEFAULT '' COMMENT '命中的物品词条,用于复盘', garbage_type_code TINYINT NOT NULL, confidence DECIMAL(4,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_openid_time (openid, create_time) ) COMMENT '识别记录表'; CREATE TABLE wx_user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE, nickname VARCHAR(64) DEFAULT '', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '小程序用户表';garbage_item单独建表而不是写死在代码里,这是整个设计里最值得在答辩时强调的一点:知识库与代码解耦,新增一个「奶茶杯」只需要一条 INSERT,线上生效不需要重启服务。garbage_record里的matched_item字段容易被忽略,但它对复盘「哪些物品经常识别失败」非常重要,后续做模型训练也需要这份数据做标注参考。
5.2 本地部署四步走:先库、后服务、再联调
部署顺序不能乱,我习惯的节奏是「数据库先行」。第一步建库导表:
mysql -uroot -p < garbage_design.sql第二步改配置,src/main/resources/application.yml里重点看数据库连接和上传大小:
spring: datasource: url: jdbc:mysql://localhost:3306/garbage_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: 'your_password' driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB server: port: 8080第三步启动后端,两种方式任选:
mvn spring-boot:run # 或者打包后运行 mvn clean package -DskipTests java -jar target/garbage-0.0.1-SNAPSHOT.jar第四步是微信开发者工具导入小程序目录,改app.js里的baseUrl为http://localhost:8080,并到「详情 - 本地设置」勾选「不校验合法域名」。四步全部完成后,先在浏览器验证文本通道:直接访问http://localhost:8080/api/garbage/recognizeByText?text=矿泉水瓶,返回typeName=可回收物就说明后端链路通了,再回小程序测图片上传。
5.3 五条高频踩坑记录:现象、原因、解决
坑一:后端启动报 Failed to configure a DataSource。现象是 SpringBoot 启动日志出现Failed to configure a DataSource: 'url' attribute is not specified,或数据库密码相关的认证错误。原因往往不是配置缺失,而是application.yml里密码包含@、#、:这类 YAML 特殊字符,解析时被截断。解决:给密码加单引号,password: 'P@ss#2024',类似地url里的参数值也建议整体用引号包裹。
坑二:本地浏览器能请求,真机预览报「不在以下合法域名列表中」。现象是开发者工具里一切正常,手机扫码预览后wx.request直接 fail。原因有两个:一是「不校验合法域名」只对开发者工具生效,真机上必须配 HTTPS 域名;二是后端只监听了127.0.0.1,手机访问不到。解决:校验收尾阶段用java -jar xxx.jar --server.address=0.0.0.0绑定所有网卡,手机和电脑连同一 WiFi 后用局域网 IP 拼baseUrl;要真机联网演示就必须提前准备备案域名和 HTTPS 证书,这是微信的硬性要求。
坑三:SpringBoot 版本太高,项目怎么都起不来。现象是mvn spring-boot:run编译能过,启动瞬间报UnsupportedClassVersionError或一堆 bean 创建失败。原因很直接:SpringBoot 3.x 要求 JDK17,而你电脑上是 JDK8。解决:如果想留在 JDK8,把pom.xml里的版本降到 2.7.x 系列;如果坚持用 3.x,就去装 JDK17 并改 IDEA 的 Project Structure。这个问题在验收季出现频率极高,因为很多同学直接去 start.spring.io 生成了最新版。
坑四:上传图片提示 MaxUploadSizeExceededException。现象是手机相册拍一张 3MB 的原图传上去,后端直接抛异常。原因是 SpringBoot 默认单文件限制 1MB,原图轻松超限。解决:application.yml里按上面那样把max-file-size提到 10MB、max-request-size提到 20MB,同时小程序端在chooseMedia时用sizeType: ['compressed']做前端压缩,双保险。
坑五:文本通道正常,图片通道永远返回未识别。现象是同一个物品,输入「矿泉水瓶」能识别,拍照上传却提示「未识别到物品」。原因大概率是 OCR 服务没启动,或者 SpringBoot 调用 OCR 超时后没有降级处理,直接落到了 unknown 分支。解决:检查 OCR 服务进程是否存活;在后端加上超时处理和降级逻辑,OCR 不可用时返回「识别服务暂时不可用」,前端也能给出更明确的提示,而不是让用户以为自己的照片有问题。
6. 验证与进阶:本地压力自测和识别准确率的四种提升路径
部署跑通之后,下一步不是急着加功能,而是先建立一套「能证明它有效」的验证方式。我习惯用一个小脚本做批量回归,把测试图片按真实分类建目录,目录名就是期望结果:
import os import requests image_exts = (".jpg", ".jpeg", ".png", ".webp") base_url = "http://localhost:8080/api/garbage/recognize" total = 0 hit = 0 for root, _, files in os.walk("./test_imgs"): for f in files: if not f.lower().endswith(image_exts): continue label = os.path.basename(root) # 目录名当作真实类别 path = os.path.join(root, f) resp = requests.post(base_url, files={"file": open(path, "rb")}, timeout=30) pred = resp.json()["data"]["typeName"] total += 1 hit += int(pred == label) if pred != label: print(f"误判: {path} -> 期望 {label} 实际 {pred}") print(f"Top1 命中率: {hit}/{total} = {hit / max(total, 1):.2%}")这个脚本能帮你量化每一次改动的影响。我一般要求每类垃圾至少准备 20 张真实拍摄的图片,四类共 80 张,跑一轮拿到基线,后面每改一次规则表或 OCR 参数都重新跑一遍,避免「修好了一个误判、坏掉了另外三个」的回归问题。
识别准确率想往上走,有四条路径,按性价比排序:第一,扩充garbage_item知识表,把同义词和品牌词铺开,这是投入最小收益最直接的方式;第二,优化 OCR 输入质量,上传前对图片做 resize 和对比度增强,文字区域越大识别越稳;第三,把规则表改成后台管理,做一个简单的增删改查页面,让知识库可运营;第四,如果确实想上模型,用 MobileNetV3 做四分类,导出 ONNX 后用 onnxruntime 在 SpringBoot 里推理,作为 OCR 匹配失败的兜底。
如果前期用了 uniapp 开发,打包时可以把 H5 构建产物放进src/main/resources/static目录,SpringBoot 会直接托管静态资源,实现前后端同端口访问,这也是「vue 打包放进 springboot 中」那类做法的本质——一个端口解决所有请求。我带人做这个课题到最后悔的一件事,就是第一版把分类映射写死在 HashMap 里,运营想加一个「奶茶杯」还得求我改代码重新打包;后来改成数据库表,加一条 SQL 就完事,答辩老师还专门问了这个设计。这些坑都是我自己反复翻车趟出来的,希望帮到你。
本文还有配套的精品资源,点击获取