
拿到“基于微信的垃圾分类小程序”这个选题的时候我第一反应是这不就是把垃圾分类表塞进小程序里加个搜索框吗真做完才发现从 SpringBoot 后端到微信小程序前端再到部署上线每层都有不少细节坑尤其是“分类识别”这个核心功能方案选不好轻则准确率被答辩老师当场问倒重则整个项目变成套壳 demo。这篇文章把我从选题到最后部署、准备答辩的完整过程写下来包括数据库设计、接口实现、小程序页面开发、图片识别方案取舍以及那些文档里不会写的踩坑经验给同样在做垃圾分类小程序、或者准备用 SpringBoot 做毕设的同学一个能直接参照的实操版本。1. 接到“垃圾分类”这个题目时我先想清楚了这些事先说明一下项目背景这是一个典型的课程设计 / 毕业设计选题要求是微信小程序端 SpringBoot 后端需要提供完整文档和源码。很多人一听“垃圾分类”就往 AI 图像识别方向冲觉得一定要拍照识别才够“高级”。我当时的判断是核心亮点可以放在 AI但主流程必须是一个可靠、快速、覆盖常见垃圾的查询系统。所以整体定位是微信小程序做交互入口SpringBoot 做后端服务MySQL 存储垃圾词汇库和用户行为数据真正的卖点是“搜索准、响应快、有数据沉淀”图像识别作为加分项而不是唯一依赖。1.1 它本质是个查询工具别一上来就想 AI 算法垃圾分类的难点在哪不在“可回收物、有害垃圾、厨余垃圾、其他垃圾”这四个大类本身而在具体物品的归属。比如“椰子壳”属于厨余垃圾还是其他垃圾各地标准还不完全一样。再比如“口红”是其他垃圾还是有害垃圾一般人真的分不清。所以项目的核心价值是建立一个足够大、足够准确的垃圾名词库让用户搜到就能查到。我见过很多类似的毕设把搜狗、百度那套“垃圾分类词库”直接导进数据库一查有几千条数据看着很充实但用户输入“塑料瓶”查得出来输入“矿泉水瓶”就查不出来。为什么因为缺少别名处理。这个后面会重点讲。所以第一步我没有急着写代码而是先把数据的来源、结构、覆盖范围规划好。这里的数据来源我分了三块一是网上公开的垃圾分类词库比如各地城管发布的分类指南二是用户在小程序里通过“纠错反馈”提交的补充词条三是自己日常积累的高频垃圾、易混垃圾。1.2 为什么是微信小程序 SpringBoot不是别的组合选微信小程序核心就一句话触达成本低不用装 App扫码即用而且毕设答辩现场只需要手机演示不需要带电脑跑原生工程。微信生态还自带用户身份openid、分享、客服等能力不用自己从零搭一套账户体系。SpringBoot 这边理由也很直接它在 Java 后端里最省事内嵌 Tomcat打一个 jar 包扔服务器上就能跑不需要单独装 Web 容器。对于毕设场景SpringBoot MyBatis-Plus MySQL 几乎是“标准套餐”资料多、踩坑少答辩老师也熟悉不容易被追问到无法招架。相比传统的 SSMSpring SpringMVC MyBatisSpringBoot 自动化配置多写接口效率高尤其是注解那一套省了 XML 配置的大量成本。如果你将来还想扩展用户积分、物品识别记录、管理后台SpringBoot 的生态也撑得住。2. 数据库和业务表设计四分类背后要沉多少数据这章是整个项目的地基。表结构没设计好后面写接口和小程序页面会各种别扭。2.1 先建表还是先定接口我的顺序是接口驱动我的习惯是先用简单的接口文档反推表结构。比如小程序端需要这么几个功能用户首次登录用 wx.login 拿 code 换 openid并且把用户信息落库首页搜索框输入垃圾名称返回分类结果、分类图标、投放提示分类浏览页按四大类列出代表性垃圾热搜榜返回搜索次数靠前的垃圾词用户纠错反馈用户发现某个垃圾归类不对时提交“正确分类 备注”。顺着这些接口表结构就清晰了。我这里最终是 5 张表user用户表字段包括id、openid、nickname、avatar_url、create_time。openid 唯一索引因为它是小程序里识别用户的唯一凭证。category分类表存四个大类字段包括id、name、icon、description、create_time。icon 这里我建议直接存小程序云存储或线上图片地址不要在数据库里存 base64。waste垃圾词条表这是核心表。字段包括id、name、category_id、alias、tips投放提示比如“请倒掉液体后投入可回收物”、heat搜索热度、create_time。其中alias是别名用于匹配同义词。search_record搜索记录表字段包括id、user_id、keyword、waste_id、create_time。这张表除了做热搜排行也是答辩时展示“系统有真实用户行为数据”的论据。feedback反馈表字段包括id、user_id、content、suggest_category_id、status0 待处理、1 已处理、create_time。另外如果你做了“答题赚积分”之类的功能还要加一张integral_record积分流水表。这个看时间是否充足属于加分项。2.2 词汇表和别名的设计决定了搜索的“聪明”程度很多人的词库设计只有id、name、category_id三个字段然后前端传一个 keyword后端where name like %keyword%一查就完事。这样能跑通 demo但用户体验很差。举个真实例子用户搜“塑料瓶”数据库存的是“矿泉水瓶”like %塑料瓶%查不到。搜“创口贴”和“创可贴”明明是同一个东西数据库存了其中一个另一个就匹配不上。这类问题靠 like 解决不了必须引入别名机制。我的做法是关键词匹配时先查name再查alias。alias字段用逗号分隔存储多个别名比如“矿泉水瓶”这一条记录alias可以写成“塑料瓶,饮料瓶,水瓶”。查询时用where name #{keyword} or FIND_IN_SET(#{keyword}, alias)命中后返回name那条记录的完整信息。这里要注意一个性能小坑FIND_IN_SET在大数据量下没法走索引。垃圾分类词库一般几千条到几万条问题不大但如果将来数据量大就要拆一张waste_alias表用 waste_id 关联。我当时守住了“几千数据量直接 FIND_IN_SET 够用”的原则答辩时还主动提了这个优化思路老师反而印象不错。还有一类特殊词条需要单独处理一词多义。比如“苹果”既是厨余垃圾水果果核类又要考虑是不是电子品牌在垃圾分类场景里直接归厨余垃圾没问题但“电池”要区分普通干电池和锂电池普通干电池现在很多地方归其他垃圾充电宝里的锂电池反而要专门回收。这些易混词我在tips字段里写了详细说明搜索出来后用户能看到投放引导比只显示分类名称更有说服力。3. SpringBoot 后端核心接口实现和真实踩坑记录后端用什么框架不是重点重点是你提供的接口是不是能真正支撑小程序的流畅体验。这里我把核心代码思路和遇到的三个大坑写下来。3.1 目录结构和核心依赖配置我建的是一个标准 SpringBoot 工程包结构分成controller、service、mapper、entity、config、common六层。没有用 Security因为小程序端根本不需要复杂登录流程用 openid 做身份标识就够了。关键依赖也就这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependencyapplication.yml里除了常规的数据源配置有两个容易被忽略的点。第一是时区连接 MySQL 要在 JDBC URL 后面加serverTimezoneAsia/Shanghai否则报时差错误。第二是开启 MyBatis-Plus 的驼峰映射否则数据库字段category_id映射不到实体类的categoryId默认其实没问题但如果你改了配置就要注意。3.2 垃圾查询接口的完整实现逻辑搜索接口是整个系统最核心的接口我把它做成了三个层次的匹配GetMapping(/search) public Result search(RequestParam String keyword) { // 1. 精确匹配名称完全一致或别名命中 // 2. 模糊匹配名称中包含关键词用于用户输入不完整的情况 // 3. 兜底返回如果都没有命中提示用户“未找到请尝试相近词语或提交反馈” }第一层用精确匹配返回结果最快也最准。第二层模糊匹配返回列表供用户点击选择。这里有一个很实际的问题如果用户输入“垃圾袋”但库里只有“塑料袋”怎么办我的模糊匹配策略是先用 name 做like %keyword%再用 keyword 做反向匹配——也就是name like %塑料袋%能不能命中“垃圾袋”不能。这就暴露了词库的覆盖度问题。解决思路有两个一个是扩大别名体系把“垃圾袋”也写进“塑料袋”的别名里另一个是记录未命中词条后台定期整理合并。我当时在小程序的反馈功能里加了一个隐藏入口搜索无结果时自动记录 keyword 到一张search_miss表然后定期人工处理把它合并进主词库或建立别名。这个机制做出来之后答辩时可以作为“数据闭环”的亮点。另外搜索接口要处理空字符串和白名单过滤。微信小程序端如果用户输入了空格keyword是 后端trim()之后直接返回空列表否则数据库会做一次无意义的全表扫描。3.3 跨域、LocalDateTime、打包后找不到文件这三个坑这三个坑我踩得记忆深刻逐个说。第一跨域。小程序端wx.request本质上是一个浏览器层面的 HTTP 请求如果你的后端接口没做跨域配置真机调试时大概率报 “URL 域名不合法” 或 CORS 错误。我在config包里写了一个CorsFilter放行所有跨域请求。注意这个“放行所有”只适合开发期上线前应该限定白名单域名。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }第二LocalDateTime 序列化。MyBatis-Plus 实体里用了 Java 8 的LocalDateTime不加处理返回给前端的 JSON 是2024-06-01T12:00:00虽然前端能解析但不美观。我直接添加了 Jackson 的全局配置统一格式为yyyy-MM-dd HH:mm:ss。第三打包后找不到静态资源。我把分类图标、默认头像放在了src/main/resources/static目录下本地开发访问没问题打成 jar 包部署到服务器后发现图片全部 404。原因是 SpringBoot 对 jar 包内的静态资源路径处理跟本地文件系统不一样。最后我的方案是把所有图片资源放到服务器独立目录通过 Nginx 做静态资源映射后端接口只返回图片 URL。4. 小程序前端让用户在 10 秒内完成一次查询小程序端我讲四个部分页面结构、请求封装、组件实践、以及附加功能。4.1 页面怎么拆首页、分类、个人中心小程序页面我用的是典型的三个 tab首页负责搜索 热搜 快捷分类入口分类页按四大类展示代表性物品个人中心展示用户信息、搜索历史、反馈入口和积分记录如果做了。首页的搜索框我用了微信官方的input组件设置confirm-typesearch这样用户在手机键盘上点击“搜索”时能直接触发search事件体验比多一个按钮舒服得多。搜索结果的展示我建议用卡片列表每条结果显示分类名称、分类图标、物品名称和投放提示。分类图标我用的是 emoji 对应方案但考虑到文章里不能用这些表情符号建议你自己在项目里用本地图标。热搜词排行从后端search_record表统计输出用一个limit 10的列表。这个接口做的时候不要每次都 count 全表我是定期把热度冗余到了waste.heat字段排序直接按这个字段查就行。4.2 wx.request 统一封装与加载态处理小程序的wx.request是一个很底层的 API每写一个页面都手写一遍会非常痛苦。我封装了一个request.js统一处理 baseURL、请求头、超时时间、错误提示和加载动画。这个小程序端的关键代码如下const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.showLoading({ title: 加载中... }) wx.request({ url: baseUrl url, method: method, data: data, header: { Content-Type: application/json }, success(res) { if (res.statusCode 200) { resolve(res.data) } else { wx.showToast({ title: 请求失败, icon: none }) reject(res) } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) }, complete() { wx.hideLoading() } }) }) }这里有个细节我一定要提醒开发环境调试时要在微信开发者工具的“详情-本地设置”勾选“不校验合法域名”否则http://localhost:8080这个地址根本调不通。但千万记得上线前必须取消这个勾选配置正式的 HTTPS 域名。4.3 微信登录、客服、分享与动态设置标题微信小程序的登录不像是网页的账号密码登录而是wx.login拿一个临时 code传给后端后端拿 code 去微信服务器换 openid。如果你没有部署后端还可以用微信云开发的cloud.openid一行搞定。我当时是自己后端实现的controller 里加了一个/login接口流程是小程序调wx.login()拿 code小程序调后端POST /login传 code后端用 code 调微信接口换 openid查询用户表有没有记录没有就插入新用户后端把 openid 返回给前端存在本地缓存里后续请求带上作为用户标识。这里有一个安全考虑openid 不应该暴露给前端作为 token 使用因为任何人拿到 openid 都可以冒充用户。正确做法是后端生成一个随机的 session token存到数据库或者 Redis然后前端每次请求带这个 token。毕设阶段很多人会忽略但如果答辩老师问“你这个登录安全吗”你能答上这一层会非常加分。时间允许建议做不允许则至少要能说出这个改进方向。附加功能方面客服按钮直接写button open-typecontact不需要后端参与。分享功能则在onShareAppMessage里配置小程序卡片标题和路径。还有一个容易忽略的小功能动态设置小程序头部标题。比如搜索“苹果”后把当前页面标题改成“苹果属于厨余垃圾”用wx.setNavigationBarTitle实现即可这个小细节会让演示效果显得非常完整。5. “识别”功能的落地第三方 API 本地词库兜底垃圾分类小程序如果只做文字搜索无论系统做得多完善总感觉缺一点“技术含量”。所以我加了拍照识别功能。这章主要讲路线选择和落地实现。5.1 三条路线对比云接口、自训练模型、词库兜底我调研了一圈建模型这个方向是最快被排除的——垃圾图像分类没有现成的公共数据集可以拿来直接用自己标注训练不仅耗时训练出来的准确率也很难看。就算用教程里的垃圾分类数据集那些图片都是单物体、纯色背景拍的实际使用中用户对着垃圾桶拍照、光线昏暗、物体遮挡效果会大打折扣。第二条路线是接第三方 API比如百度 AI 开放平台的图像识别接口。这类接口通常能识别出物体名称比如“可乐瓶”“苹果”然后我再拿识别出的“物体名称”去词库里查分类。优势是识别通用物体能力强、接入简单劣势是免费额度有限到了答辩演示的关键时刻可能额度耗尽或者把物体识别成“金属”这种太抽象的词词库里根本没有。我最终采用的是第三条主识别走第三方 API识别结果再走本地词库做二次映射如果词库没有就用一个兜底规则返回。这是一个两级流程下面详细说。5.2 我的两级识别流程和容错设计第一级用户在小程序端拍一张照片通过wx.uploadFile上传到后端/recognize接口。后端拿到图片后调第三方识别 API 获取物体标签列表从里面挑一个置信度最高且词库存活的标签返回分类结果。第二级如果 API 识别出来的标签在词库里查不到比如识别出“塑料”而库里只有“塑料瓶”就用字符串匹配看标签和词库有没有包含关系比如“塑料”包含在“塑料瓶”里就直接返回“塑料瓶”的分类和投放提示。如果这两级都没命中那就回退到一个规则按垃圾桶颜色兜底。前端拍照时弹出一个选项“你拍的是哪种垃圾桶绿 / 蓝 / 红 / 黑”然后把用户的选择作为主要分类返回。这个设计有两个好处一是兜底逻辑简单可靠二是把“识别失败”从技术事故变成了业务交互演示效果反而显得很人性化。还要说一句后端调用第三方 API 的 key 一定不要写死在小程序前端代码里否则别人反编译你的小程序包就能偷走你的 API 额度。正确方式是把 key 配在后端的 application.yml 里前端只上传图片由后端统一去调用第三方接口。6. 从 jar 包到线上可用部署和备案的那些事最后说说部署。很多同学做毕设的时候项目在本地跑得风生水起一上服务器就各种问题。这里我把从打包到线上运行的全过程捋一遍。6.1 服务器、数据库和 HTTPS 证书的准备我用的是一台云服务器2核4G 的配置跑 SpringBoot MySQL 完全没有压力。系统选了 Ubuntu装了 JDK 8这里注意SpringBoot 2.x 用 JDK 8 最稳如果你用 SpringBoot 3.x 就必须 JDK 17 以上、MySQL 5.7、Nginx。数据库这步有个常见坑云服务器的 MySQL 默认只允许 localhost 连接如果你用 Navicat 想远程连上去导数据会报Host xxx is not allowed to connect to this MySQL server。处理方法是执行一条授权 SqlGRANT ALL PRIVILEGES ON *.* TO root% IDENTIFIED BY 你的密码 WITH GRANT OPTION; FLUSH PRIVILEGES;但上线后记得把 root 的远程权限收回来只给业务账号开最小权限。这个点也能体现安全意识。HTTPS 证书方面小程序要求所有 request 请求域名必须是 HTTPS且域名需要备案。我在云服务商那里申请了免费证书下载 Nginx 版证书文件放到服务器目录然后在 Nginx 配置里引入证书。注意证书文件更新会过期编不过去。建议在手机日历里设置到期提醒提前一个月换新证书否则用户打开小程序直接白屏这是真实会发生的线上事故。6.2 Nginx 反向代理和域名校验的完整配置SpringBoot 默认跑在 8080 端口小程序要求用 HTTPS 443 访问所以需要 Nginx 做一次反向代理。我的 nginx 配置关键部分长这样server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }配好之后nginx -t测试配置文件通过后systemctl reload nginx。小程序端的 request baseURL 从http://localhost:8080改成https://yourdomain.com在微信公众平台后台的“开发管理-服务器域名”里添加这一行。这个域名校验有一个特点不加端口号只要域名能正常响应 HTTPS 请求即可。我记得第一次配置的时候一直报 “request:fail 无法连接服务器”排查了一圈发现是 Nginx 配置里没加proxy_set_header Host导致后端接收请求时拿不到正确的 Host微信小程序那边判断服务器不可用。加上之后就正常了。这种问题没有捷径只能一步一步看日志排查。6.3 答辩高频问题与我的回答思路这里列几个我被问到的问题和我的回答框架供参考“你这个分类凭什么这么准”我的回答思路是数据先由各地官方分类指南支撑再通过用户反馈持续纠偏词库是一个动态更新的结构不是一次性导入。把feedback表调出来给老师看用户提交了哪些纠错、系统怎么处理。“搜索功能如果用户输入错别字怎么办” 正常回答是“基于别名和模糊匹配兜底”更进阶的回答是引入编辑距离算法做纠错提示比如用户搜“创口贴”但库里没有可以提示“你是不是想找创可贴”。这是一个方案升级方向能不能做完看时间但回答方向必须清晰。“为什么不用云开发” 因为项目要求用 SpringBoot 做后端相当于一个独立服务器对数据表结构的掌控、接口设计、部署运维的理解都更完整也能体现 Java 后端的实战能力。这是从学习目的出发的选择不需要回避“云开发更简单”的事实。“你的系统有多少用户” 如果只有你在测试就如实说这是演示环境有搜索记录表可以展示行为数据同时说明系统设计了热搜榜、反馈闭环具备扩展到真实用户的基础。整个答辩过程我觉得最有说服力的不是代码写得多优雅而是你能说出每一个设计背后的理由以及你踩过什么坑、怎么解决的。这些真实经历远比任何套话都更打动老师。个人体会是做这类“文档源码”的完整项目最忌讳的就是前期一头扎进代码。先把数据源理清把匹配逻辑定好把部署链路想明白后面反而是体力活。垃圾分类小程序真正难的部分从来不是小程序 UI也不是 SpringBoot 接口而是那个看起来不起眼的词库——词库做好了这个系统才算真正“可用”。