资讯详情

资讯详情

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/recognizePOSTfile 图片文件、text 可选文本识别结果对象小程序拍照/相册上传主入口/api/garbage/recognizeByTextPOSTtext 物品名称识别结果对象文本通道调试和演示用/api/garbage/record/pageGETpage、size分页记录列表历史记录页加载更多数据流是这样的小程序端调用wx.chooseMedia拿到临时文件路径wx.uploadFile以file字段名上传到 SpringBoot 的 ControllerController 收到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 包装类约定code0表示成功非 0 表示业务失败。先看 Controller 层RestController RequestMapping(/api/garbage) public class GarbageRecognizeController { private final GarbageRecognizeService garbageService; public GarbageRecognizeController(GarbageRecognizeService garbageService) { this.garbageService garbageService; } PostMapping(/recognize) public ResultRecognizeVo 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 ResultRecognizeVo 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) { // 从数据库加载映射实际项目可加本地缓存避免每次查库 ListGarbageItem 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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 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: Pss#2024类似地url里的参数值也建议整体用引号包裹。坑二本地浏览器能请求真机预览报「不在以下合法域名列表中」。现象是开发者工具里一切正常手机扫码预览后wx.request直接 fail。原因有两个一是「不校验合法域名」只对开发者工具生效真机上必须配 HTTPS 域名二是后端只监听了127.0.0.1手机访问不到。解决校验收尾阶段用java -jar xxx.jar --server.address0.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)}, timeout30) pred resp.json()[data][typeName] total 1 hit int(pred label) if pred ! label: print(f误判: {path} - 期望 {label} 实际 {pred}) print(fTop1 命中率: {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 就完事答辩老师还专门问了这个设计。这些坑都是我自己反复翻车趟出来的希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →