SpringBoot垃圾分类识别小程序:架构设计、数据库与部署全流程 简介一套面向毕业设计场景的垃圾分类识别小程序项目源码基于SpringBoot框架构建后端使用Java语言实现并附带数据库脚本与部署说明整体覆盖从后台服务到小程序前端展示的完整闭环流程。项目定位清晰适合计算机、通信、人工智能、自动化等相关专业的学生作为课程设计、大作业或毕业设计参考也便于初步接触小程序后端整合的开发者学习。压缩包内共208个文件大小约44.13MB以163个Java核心源码、28个XML配置与映射文件为主另包含SQL数据库脚本、Properties/YAML配置、JAR依赖包及Markdown说明文档目录结构完整各文件用途明确。目前已有97人浏览学习该工程为作者个人毕设代码经过调试测试答辩评审分达98分可确保运行附带数据库初始化脚本与部署说明可减少环境配置与联调阶段的摸索时间。在垃圾分类识别流程、接口封装及页面联调方面都有清晰的实现思路具备较好的学习借鉴与二次开发价值基础较强的使用者可调整功能模块以适配不同场景。1. SpringBoot垃圾分类识别小程序从选题到部署的一条完整链路如果你正在为即将到来的毕业设计找题目又希望这套系统“看得见、摸得着、能当场演示”SpringBoot垃圾分类识别小程序是一个很少出错的选项用户在小程序里拍一张垃圾照片后端用SpringBoot接住图片跑识别逻辑返回可回收、有害、厨余还是其他垃圾同时把记录写进MySQL。技术栈覆盖微信小程序、后端框架、数据库和部署四件事难度刚好卡在“能讲清楚”和“做得完”之间。这篇稿子把从建库到答辩的路径拆开讲怎么设计表、怎么写识别接口、小程序端怎么传图、部署时哪里容易翻车、答辩演示要准备哪些细节。适合还没定题的学生也适合想把手头半成品补成完整作品的人。2. 系统架构与数据库设计图片上传到分类回显的数据链路拿到这套题目我的习惯是先把数据流画清楚再动代码。完整链路是小程序端通过wx.chooseMedia取得本地图片再用wx.uploadFile把图片二进制提交到 SpringBoot 后端的/api/garbage/recognize后端先把图片保存到本地磁盘或对象存储调用识别服务得到分类结果和置信度再把这条识别记录写入数据库最后把结果返回给小程序渲染。用户看到分类名称、置信度和图片缩略图一条闭环就此完成。2.1 三层架构小程序、SpringBoot、MySQL 各司其职三层各自的职责要划清小程序层只负责获取图片、请求接口、渲染结果不碰任何业务计算SpringBoot层负责接收上传、校验文件、执行识别、读写数据库是唯一允许触碰数据源的层MySQL层只做持久化用户信息、垃圾类别字典、识别记录、纠错反馈四类数据都落在里面。为什么强调这一点因为很多第一次做全栈的同学会把识别逻辑写在小程序里、把数据也塞进小程序的storage后端只做一个空壳。开发是快了但答辩时评委一句“你后端做了什么”就答不上来。把链路放在后端既符合真实生产环境答辩也更容易讲出层次。从开发顺序上看我建议先定接口再写页面。后端把/recognize和/history两个接口的定义固定下来返回字段名也先锁死小程序端照着字段写渲染逻辑。两边并行开发时前后端只要守住这份 JSON 结构约定就不会出现联调时互相等对方的尴尬。2.2 四张核心表字段设计与选择理由在垃圾分类这个小业务里表的数量不需要多四张就够。表名业务作用核心字段备注t_user保存微信用户openid、nickname、avatar_url、created_atopenid 唯一小程序 wx.login 后获取t_garbage_category垃圾类别字典code、name、description、sort_order预置可回收、有害、厨余、其他四类t_recognize_record每次识别流水user_id、image_url、category_code、confidence、is_correct、created_at按 (user_id, created_at) 建索引t_feedback用户纠错反馈record_id、user_id、correct_category_code、remark、created_at沉淀错误样本后续可反哺识别字段设计上有一个关键决定类别字段用code字符串而不是自增 id。原因是垃圾分类文案在不同城市并不统一——上海叫干垃圾湿垃圾北京叫厨余垃圾其他垃圾。如果业务表里直接存中文名将来想换城市文案就要改一堆历史数据。存code后前端展示时做一次映射就行数据库不用动。confidence字段用DECIMAL(5,4)范围足够存 0 到 1 之间的置信度is_correct给纠错闭环预留用户如果点了“识别错了”这里记为 0配合 t_feedback 表记录正确类别。还有一点要提醒记录表里别设计一个 JSON 字段把整张图片的 base64 塞进去。image_url存文件路径或外部 URL图片二进制落在磁盘或对象存储否则数据库几个 G 很容易膨胀备份迁移都难受。2.3 建表脚本直接能跑的 DDL 与参数含义CREATE DATABASE IF NOT EXISTS garbage_sort DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE garbage_sort; CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(64) DEFAULT COMMENT 昵称, avatar_url VARCHAR(255) DEFAULT COMMENT 头像地址, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB COMMENT用户表; CREATE TABLE t_garbage_category ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, code VARCHAR(32) NOT NULL COMMENT 类别编码recyclable/harmful/kitchen/residual, name VARCHAR(32) NOT NULL COMMENT 类别名称, description VARCHAR(255) DEFAULT COMMENT 类别描述, sort_order INT DEFAULT 0 COMMENT 展示顺序, UNIQUE KEY uk_code (code) ) ENGINEInnoDB COMMENT垃圾类别字典; CREATE TABLE t_recognize_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, user_id BIGINT NOT NULL COMMENT 用户ID, image_url VARCHAR(255) NOT NULL COMMENT 识别图片URL, category_code VARCHAR(32) NOT NULL COMMENT 识别出的类别编码, confidence DECIMAL(5,4) DEFAULT 0 COMMENT 置信度, is_correct TINYINT DEFAULT NULL COMMENT 用户是否确认正确0否 1是, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 识别时间, KEY idx_user_created (user_id, created_at) ) ENGINEInnoDB COMMENT识别记录表; CREATE TABLE t_feedback ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, record_id BIGINT NOT NULL COMMENT 关联识别记录ID, user_id BIGINT NOT NULL COMMENT 用户ID, correct_category_code VARCHAR(32) NOT NULL COMMENT 用户认为的正确类别, remark VARCHAR(255) DEFAULT COMMENT 备注, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 反馈时间, KEY idx_record (record_id) ) ENGINEInnoDB COMMENT纠错反馈表;脚本里的排序规则用utf8mb4_general_ci就够用。字符集之所以必须选 utf8mb4 而不是 utf8是因为微信用户昵称和垃圾图片备注里会出现 emojiutf8mb4 是 MySQL 5.7 之后能完整存储四字节字符的选择无脑用它不会错。索引只需要两条openid 的唯一索引防止同一用户重复插入识别记录表的(user_id, created_at)联合索引支撑历史记录页按用户倒序查询。其他字段先不加索引等数据量真的上来了再加不为不存在的查询瓶颈买单。3. SpringBoot后端落地识别接口、数据源配置与核心代码后端这部分是把标题里的 springboot框架 真正立起来的地方。我会按工程骨架、配置文件、Controller 与 Service 三层来展开最后说明识别策略怎么选。全程用 JdbcTemplate不引入 MyBatis-Plus因为这个体量的业务用简单 JDBC 就够清楚代码量也更容易让答辩评委看懂。3.1 工程骨架用 2.7.x 打底避开 SpringBoot 3.0 的版本坑搭这类项目SpringBoot 版本我会固定到 2.7.x比如 2.7.18。原因很现实3.x 起把javax包改名成jakartaJDK 最低要求 17而不少机器上装的还是 JDK 8。不是说 3.x 不能用而是 2.7.x 的教程、插件、踩坑问答最多遇到问题搜得到答案。springboot 版本太高确实会让本地环境直接翻车这一节先把这个雷避开。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies依赖里我没放 Lombok。有些同学喜欢用它省去 getter/setter但毕设项目里我建议少一个注解处理就少一层不确定性IDE 的 Lombok 插件没装好时会看到一堆“找不到方法”的编译错误。老老实实手写 DTO 的 getter/setter代码量多不了几行排除故障时更直接。3.2 数据源与上传配置application.yml 的三个关键项springboot 配置集中在application.yml这里有三项必须写对。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/garbage_sort?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB garbage: recognize: provider: localURL 里的三个参数各有用处useUnicode和characterEncodingutf8解决中文写入乱码serverTimezoneAsia/Shanghai解决 MySQL 8.x 的时区报错。driver-class-name必须写com.mysql.cj.jdbc.Driver这是 mysql-connector-j 8.x 的驱动类名写旧的com.mysql.jdbc.Driver会直接启动失败第 5 章会把这个坑再拎出来说。上传大小限制是 creamboard 里最容易忽略的一项。SpringBoot 默认单文件最大 1MB手机上随便一张照片就超了。max-file-size控制单个文件max-request-size控制整个请求体按 10MB/20MB 设置比较宽松。3.3 识别主链路Controller 与 Service 的代码实现接口分层按 Controller → Service 来做Controller 只做参数校验业务逻辑全部进 Service。package com.example.garbage.controller; import com.example.garbage.common.Result; import com.example.garbage.service.GarbageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; RestController RequestMapping(/api/garbage) public class GarbageController { Autowired private GarbageService garbageService; PostMapping(/recognize) public Result recognize(RequestParam(file) MultipartFile file, RequestParam(value userId, required false) Long userId) { if (file null || file.isEmpty()) { return Result.error(400, 图片不能为空); } if (file.getSize() 10 * 1024 * 1024) { return Result.error(400, 图片超过10MB限制); } if (file.getContentType() null || !file.getContentType().startsWith(image/)) { return Result.error(400, 只支持图片文件); } return Result.ok(garbageService.recognize(file, userId)); } GetMapping(/history) public Result history(RequestParam Long userId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { return Result.ok(garbageService.history(userId, page, size)); } }Controller 里做了三层校验文件非空、大小不超限、MIME 类型必须 image/ 开头。这些校验看起来基础但能挡住一大部分异常请求也让答辩现场不会因为传了个 txt 文件就报 500。package com.example.garbage.service; import com.example.garbage.common.Result; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.util.UUID; Service public class GarbageService { private final JdbcTemplate jdbcTemplate; Value(${garbage.recognize.provider:local}) private String provider; public GarbageService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public RecognizeResult recognize(MultipartFile file, Long userId) { String imageUrl saveImage(file); CategoryResult categoryResult; if (cloud.equals(provider)) { categoryResult cloudRecognize(file); } else { categoryResult localRuleRecognize(file.getOriginalFilename()); } String insertSql INSERT INTO t_recognize_record (user_id, image_url, category_code, confidence) VALUES (?, ?, ?, ?); jdbcTemplate.update(insertSql, userId, imageUrl, categoryResult.getCode(), categoryResult.getConfidence()); return RecognizeResult.builder() .imageUrl(imageUrl) .categoryCode(categoryResult.getCode()) .categoryName(CategoryCodeMapper.getName(categoryResult.getCode())) .confidence(categoryResult.getConfidence()) .build(); } private String saveImage(MultipartFile file) { String dir System.getProperty(user.dir) /uploads/; File folder new File(dir); if (!folder.exists()) { folder.mkdirs(); } String original file.getOriginalFilename(); String ext original null ? .jpg : original.substring(original.lastIndexOf(.)); String filename UUID.randomUUID().toString().replace(-, ) ext; File dest new File(folder, filename); try { file.transferTo(dest); } catch (Exception e) { throw new RuntimeException(图片保存失败, e); } return /uploads/ filename; } private CategoryResult localRuleRecognize(String filename) { String name filename null ? : filename.toLowerCase(); if (name.contains(电池) || name.contains(过期药)) { return CategoryResult.of(harmful, 0.90); } if (name.contains(果皮) || name.contains(剩饭)) { return CategoryResult.of(kitchen, 0.90); } if (name.contains(纸箱) || name.contains(塑料瓶)) { return CategoryResult.of(recyclable, 0.90); } return CategoryResult.of(residual, 0.60); } private CategoryResult cloudRecognize(MultipartFile file) { // 接入云厂商图像识别API时在这里完成base64编码与HTTP调用 return localRuleRecognize(file.getOriginalFilename()); } public PageResult history(Long userId, Integer page, Integer size) { String countSql SELECT COUNT(*) FROM t_recognize_record WHERE user_id ?; Integer total jdbcTemplate.queryForObject(countSql, Integer.class, userId); String listSql SELECT id, image_url, category_code, confidence, created_at FROM t_recognize_record WHERE user_id ? ORDER BY created_at DESC LIMIT ? OFFSET ?; ListRecordItem list jdbcTemplate.query(listSql, (rs, rowNum) - RecordItem.builder() .id(rs.getLong(id)) .imageUrl(rs.getString(image_url)) .categoryCode(rs.getString(category_code)) .confidence(rs.getBigDecimal(confidence)) .createdAt(rs.getString(created_at)) .build(), userId, size, (page - 1) * size); return PageResult.of(list, total, page, size); } }saveImage用 UUID 做文件名避免用户上传同名文件互相覆盖后缀从原文件名里截取。目录用user.dir /uploads/是相对路径换服务器不用改配置缺点是所有图片堆在一个目录本次项目量级完全够用。provider参数控制识别策略默认 local。localRuleRecognize 按文件名关键词匹配属于零成本兜底玩法cloudRecognize 的注释位置留给云 API做毕业设计时可以注册云厂商的垃圾分类图像识别服务把图片 base64 编码后 POST 过去拿到候选类别和置信度再映射到自己的 code。用配置项切两种模式的好处是平时演示用 local 不依赖外网答辩要秀真实识别效果时改一个配置重启即可。history方法做了分页。LIMIT ? OFFSET ?的第二个参数是 offset计算方式是(page - 1) * size前端传 1 时要能正确翻到第一页数据。总数查询和列表查询拆开写代码更直白也方便前端在列表底部显示“共 xx 条”的总数统计。4. 小程序端落地拍照、上传、结果渲染与历史记录小程序端我按原生小程序写不用 uniapp。原因很直接这个题目只需要微信一个端原生项目的启动速度最快、调试路径最短遇到问题直接对得上微信官方文档。如果将来想发到支付宝、抖音小程序再抽成 uniapp 也不迟。页面就两页index 识别主页和 history 历史记录页。4.1 项目结构从 app.json 到页面文件组织{ pages: [ pages/index/index, pages/history/history ], window: { navigationBarTitleText: 垃圾分类识别, navigationBarBackgroundColor: #07C160, navigationBarTextStyle: white }, style: v2, sitemapLocation: sitemap.json }pages数组里第一个就是启动页把 index 放最前面。navigationBarTitleText设置顶部导航栏文案垃圾分类识别这个名称直接透出业务主题。小程序端代码总量不大没必要引入分包两页平铺即可。4.2 识别页拍照选图、上传、回显的核心代码先看页面结构view classpage view classpreview image wx:if{{imageUrl}} src{{imageUrl}} modeaspectFit/ text wx:else点击下方按钮拍照或从相册选择垃圾图片/text /view button classaction-btn bindtapchooseImage loading{{loading}}拍照识别/button view wx:if{{result}} classresult-card text classcategory-name{{result.categoryName}}/text text classconfidence置信度{{result.confidence}}/text image classthumb src{{result.imageUrl}} modeaspectFill/ /view /view页面逻辑Page({ data: { imageUrl: , result: null, loading: false }, chooseImage() { wx.chooseMedia({ count: 1, mediaType: [image], sourceType: [camera, album], success: (res) { const tempFilePath res.tempFiles[0].tempFilePath; this.setData({ imageUrl: tempFilePath, result: null }); this.uploadImage(tempFilePath); } }); }, uploadImage(filePath) { this.setData({ loading: true }); wx.showLoading({ title: 识别中 }); wx.uploadFile({ url: http://localhost:8080/api/garbage/recognize, filePath: filePath, name: file, formData: { userId: 1 }, success: (res) { const data JSON.parse(res.data); if (data.code 200) { this.setData({ result: data.data }); } else { wx.showToast({ title: data.msg, icon: none }); } }, fail: () { wx.showToast({ title: 网络请求失败, icon: none }); }, complete: () { wx.hideLoading(); this.setData({ loading: false }); } }); } });wx.chooseMedia是官方推荐的媒体选择 API替代了早期版本里的wx.chooseImage。count限制为 1避免一次选多张sourceType把 camera 放前面答辩演示时点一下直接进入拍照效率最高。wx.uploadFile的name字段必须跟后端RequestParam(file)保持一致formData里的 userId 在真实项目里应该由登录态决定但毕设阶段我习惯先固定一个值顶住把精力留给识别链路。回显时的imageUrl直接用了后端返回的/uploads/xxx.jpg。如果后端没有部署静态资源映射这个地址会 404需要在后端加一个ResourceHandler配置把/uploads/**映射到磁盘目录。这一步比较隐蔽排查时容易误认为是接口写错了。4.3 历史记录页拉取列表与触底加载更多Page({ data: { records: [], page: 1, size: 10, hasMore: true, loading: false }, onLoad() { this.loadHistory(); }, loadHistory() { if (this.data.loading) return; this.setData({ loading: true }); wx.request({ url: http://localhost:8080/api/garbage/history, data: { userId: 1, page: this.data.page, size: this.data.size }, success: (res) { const list res.data.data.list; this.setData({ records: this.data.records.concat(list), page: this.data.page 1, hasMore: list.length this.data.size }); }, complete: () { this.setData({ loading: false }); } }); }, onReachBottom() { if (!this.data.hasMore) return; this.loadHistory(); } });微信小程序页面列表加载更多的标准做法就是 onReachBottom 触底回调。这里有个细节page在成功拿到数据之后才自增避免失败时页码已经前进、重新拉取会跳过一页。hasMore的判定依据是当前页返回数量是否等于请求的 size等于还可能有下一页小于则说明到底了。历史记录列表的数据结构要跟后端PageResult对齐后端返回data.list、data.total前端只管把 list 拼进数组。后端分页逻辑变化时前端不用跟着改。5. 部署与避坑本地联调、服务器上线与 5 个高频问题部署环节直接决定答辩现场能不能把项目跑起来。这里既有“不校验域名”这种本地联调开关也有服务器上nohup跑 jar 的常规操作最后落在一组具体的踩坑记录上。5.1 本地三端联调不校验域名与数据源核对本地把三端跑起来依次做三件事MySQL 执行建表脚本SpringBoot 启动并确认看到 Started 日志小程序开发工具勾选“详情 - 本地设置 - 不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。前两件事不出意外的话很顺利第三件事是本地联调的关键开关。微信小程序默认只允许请求 HTTPS 且域名已备案的接口localhost显然不在白名单里。开发工具里这个开关就是给本地联调用的不勾上的话一刷新就报“不在以下合法域名列表中”。真机预览时还要把小程序请求地址从http://localhost:8080换成电脑的局域网 IP手机和电脑连同一个 Wi-Fi 才能访问。另外我习惯在后端加一个 CORS 配置。小程序原生请求不受浏览器同源策略限制但答辩时老师如果直接用浏览器打开后端地址试探接口跨域会挡住。加这一段开发期能少很多“页面白屏但接口明明有数据”的玄学问题。package com.example.garbage.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST) .allowedHeaders(*); } }CORS 配置里allowedOriginPatterns(*)表示允许所有来源正式上线时应该收紧成你自己的域名。毕业设计阶段放开即可省去反复调试跨域的麻烦。5.2 服务器部署打包、上传、启动一条命令云服务器部署的本质就是把 jar 包和应用依赖的环境放到一台长期运行的机器上。先确认服务器 JDK 是 1.8再执行mvn clean package -DskipTests scp target/garbage-sort-0.0.1-SNAPSHOT.jar rootyour-server:/opt/app/ ssh rootyour-server cd /opt/app nohup java -jar garbage-sort-0.0.1-SNAPSHOT.jar app.log 21 -DskipTests跳过测试毕设项目不用在每个环境的测试上耗时间。nohup让进程脱离当前 SSH 会话继续运行后面加表示后台执行 app.log 21把标准输出和错误输出都重定向到日志文件。这一步不做的话你关掉终端服务就跟着断开这在答辩现场非常致命。启动之后用curl http://127.0.0.1:8080/api/garbage/history?userId1page1size10验证接口能返回 JSON 就说明服务活了。起不来就看app.log日志里定位错误比盲猜有效得多。5.3 五个高频踩坑记录现象、原因、解决**坑一小程序请求报“不在以下合法域名列表中”**现象开发工具里点识别按钮请求直接 failconsole 提示域名不合法。原因微信小程序对 request/uploadFile 的域名有强制校验要求 HTTPS 且已备案本地开发环境不满足。解决开发工具详情页勾选“不校验合法域名”即可真机预览也要在手机端打开调试模式否则同样会被拦。坑二后端启动报 SQLException 驱动类错误现象SpringBoot 启动到数据源初始化时报ClassNotFoundException: com.mysql.jdbc.Driver。原因mysql-connector-j 8.x 驱动类名变成了com.mysql.cj.jdbc.Driver旧类名已经被移除。解决把 application.yml 里的 driver-class-name 改成com.mysql.cj.jdbc.Driver同时确认 pom 里的是 8.0.x 版本而不是 5.x。坑三上传照片报 size exceeds the configured maximum现象小程序选图后上传后端返回 500日志提示文件大小超过限制。原因SpringBoot 默认max-file-size只有 1MB手机照片动辄 3MB 以上。解决在 application.yml 的spring.servlet.multipart节点下把max-file-size和max-request-size调大。坑四SpringBoot 版本太高跟 JDK8 环境不兼容现象项目导入后编译直接报UnsupportedClassVersionError或者大量 jakarta 报错。原因SpringBoot 3.x 强制要求 JDK17毕设电脑和服务器上装的还是 JDK8。解决把 parent 版本回退到 2.7.xMaven 重新刷新依赖。这个坑踩过的人最多也是最没必要的翻车方式。坑五真机预览连不上后端现象真机上打开小程序按钮点了没反应请求一直转圈。原因手机和电脑不在同一局域网或者后端的server.address绑定了 localhost。解决后端不配置server.address或设为0.0.0.0小程序请求地址改成电脑的局域网 IPWindows 防火墙放行 8080 端口最后用手机浏览器打开http://电脑IP:8080/api/garbage/history验证连通性。6. 进阶验证与答辩技巧识别准确率、分页加载与一场稳的演示识别准确率是这个项目能不能打动评委的核心但毕设阶段没必要训练自己的模型。更务实的做法是后端返回三个候选类别而不是一个前端把候选列出来让用户“确认”或“纠正”确认结果写回is_correct纠正结果写进 t_feedback。这样设计之后识别过程从“黑匣子给一个答案”变成“人与系统共同校准”答辩时能清晰讲出数据回流闭环。置信度阈值也要用起来。当接口返回的 confidence 低于 0.6 时前端不要直接展示结论而是弹出一个分类选项让用户手动选择。这个交互细节在演示时很容易被注意到——评委通常会追问“识别错了怎么办”你已经有答案了。答辩演示有三个细节我吃了亏之后才总结出来第一提前准备 3 到 5 张特征明显的垃圾图片电池、香蕉皮、塑料瓶、纸箱在正式答辩前用这套图完整跑一遍确认结果稳定第二演示前清空 t_recognize_record 表让历史记录从零开始跑现场每识别一次列表就增长一条数据变化直观可见第三手机和电脑连同一个 Wi-Fi后端用局域网启动别到了现场才发现真机连不上。我自己最初做这套全栈题目时翻车最狠的一次是纠结 SpringBoot 到底用 2.x 还是 3.x来回改依赖折腾掉一个晚上。后来我给自己定了个习惯毕业设计这类短期项目先用 2.7.x 跑通把精力放在数据流和演示效果上再研究新版本的特性。把版本玄学排除在项目主链路之外你会发现这个垃圾分类识别项目真正值钱的地方不在某个框架版本而在于你把数据的来龙去脉和识别闭环讲圆了。希望帮到你。本文还有配套的精品资源点击获取