资讯动态

Spring Boot垃圾分类管理系统实战:数据库设计与核心功能代码拆解

发布时间:2026/10/7 5:21:28 来源:尧图企业网站定制
去年帮人做了个城市垃圾分类管理系统用的就是 Spring Boot 这套技术栈。说实话这个课题在近两年高校毕业设计和课程设计里出场率相当高原因很简单业务场景贴近生活、功能边界清晰、技术栈主流而且很容易讲出“社会价值”。我当时做完之后顺手把源码和文档一起整理好交付了项目本身很完整。这篇就把这套系统从业务设计到数据库表结构再到几个核心功能的代码实现完整拆开讲一遍给正在做同类系统、或者想快速了解 Spring Boot 项目完整链路的朋友一份可以直接参考的实战笔记。这套系统解决的核心痛点很明确居民面对垃圾分类时搞不清“这是什么垃圾”社区管理人员也缺少一个能够承载宣教、激励和回收预约的数字化抓手。系统面向两类使用者——普通用户和管理员前者用来查分类、学知识、预约回收、攒积分后者用来维护垃圾字典、处理预约、配置规则和看数据报表。如果你打算拿它做课程设计、毕业设计或者想练手 Spring Boot 全家桶那这篇可以说是手把手级别的拆解了。1. 项目整体设计与需求拆解1.1 垃圾分类的业务痛点与系统切入点垃圾分类这个场景听起来简单实际落地时最大的拦路虎是“知识盲区”和“激励缺失”。我问过身边不少人大棒骨是什么垃圾榴莲壳呢干电池呢答案五花八门。很多人不是不想分类是真的不知道手里的东西该往哪个桶里扔。这种识别成本一旦变高大家就倾向于随手一扔分类工作就形同虚设。所以这个系统第一要务不是“炫技”而是把“垃圾是什么、属于哪一类、怎么处理”这件事做到足够简单。用户打开系统输入垃圾名称立刻就能得到分类结果和处理建议。同时再叠加一层激励——积分。查一次分类给积分预约回收给积分每日签到给积分。积分能兑换小礼品。这样一来用户的每次“正确分类”都被量化、被奖励系统才能真正推动行为改变。我当时对需求的理解就一句话做给老百姓用的工具而不是做给管理员看的后台。整个功能设计都围绕这句话展开。1.2 用户角色与功能边界划分系统按角色天然分成两个端但千万注意这两个端不要做成两套独立系统而是放到同一个 Spring Boot 工程里通过登录后的角色权限去区分功能菜单。这也是大多数课程设计和企业小项目的通用做法。用户端功能垃圾名称检索输入“塑料袋”“过期药品”“剩饭”等关键词返回分类结果、分类依据、投放建议。分类知识库维护垃圾分类常识文章支持按分类筛选。预约回收填写家庭地址、预约时间、垃圾类型和预估重量生成回收单。积分中心查看积分流水、每日签到、积分兑换。管理端功能垃圾字典维护增删改查垃圾条目和分类标准。回收预约管理接单、确认完成、取消订单。用户管理查看用户列表、冻结异常账号。积分规则配置设置每次查询、签到、回收的积分奖励数值。数据统计按垃圾分类统计查询热度按小区统计参与人数和回收量。我当时画功能清单就画了大半页但真正落到数据库里其实也就六到八张核心表。设计阶段最忌讳一上来就写代码先把角色、功能、数据流捋清楚后面写起来会快很多。1.3 技术选型为什么是 Spring Boot MyBatis-Plus MySQLSpring Boot 在这类管理系统里几乎是标准答案。它最大的价值不是“性能有多强”而是“让开发者少操心环境问题”。自动配置、起步依赖、内嵌 Tomcat这些特性让项目从编码到部署都比传统 SSM 省掉大量配置文件。尤其是一个人开发一个管理系统精力应该集中在业务逻辑上而不是折腾 XML 配置。所以当时选型的时候我根本没犹豫。数据访问层我选了 MyBatis-Plus 而不是原生 MyBatis。原因也很实在这个系统的单表 CRUD 操作占了七成以上MyBatis-Plus 的 BaseMapper 和 LambdaQueryWrapper 能把简单的 SQL 全省掉只写那些真正复杂的统计查询。数据库用 MySQL 5.7免费、稳定、资料多碰到问题随便搜都能找到答案。前端的话如果纯课程设计可以直接用 Thymeleaf 模板加 Bootstrap省去前后端联调的麻烦如果想让项目更有亮点就用 Vue 做单页应用打包后放进 Spring Boot 的静态资源目录这样最终交付的仍然是一个可执行的 jar 包。我的做法是 Vue 前端 Spring Boot 后端但把前端构建产物直接集成进工程这样部署时只需要跑一个 Java 程序。这里补充说明一下选型时的一个判断标准如果你的项目周期在两周到一个月优先选择你熟悉的技术而不是最新最火的技术。Spring Boot 的版本也不用追求最新稳定可运行比版本号好看重要得多。2. 数据库设计——先把表结构想清楚再动手写代码2.1 垃圾分类标准表类别与条目分离设计垃圾字典是这个系统的心脏所以表结构设计上我做了严格的“分类表 条目表 别名表”三层拆分。很多新手只建一张表把分类名称和垃圾名称塞在一起当时看着方便后面维护别名、扩展属性的时候就非常痛苦。t_category 分类表结构字段名类型说明idbigint主键category_namevarchar(50)分类名称可回收物、有害垃圾等category_codevarchar(20)分类编码RECYCLABLE、HAZARDOUS 等colorvarchar(20)对应垃圾桶颜色标识descriptionvarchar(500)分类描述与投放要求create_timedatetime创建时间t_garbage_item 垃圾条目表字段名类型说明idbigint主键category_idbigint所属分类ID关联 t_categorynamevarchar(100)标准垃圾名称alias_namevarchar(255)别名逗号分隔比如“可乐瓶”对应“塑料瓶”suggestionvarchar(500)投放建议比如“清洗后压扁投放”disable_flagtinyint是否禁用0正常 1禁用这里有个很重要的设计细节t_garbage_item 里加了一个 alias_name 字段。因为同一件东西用户嘴里的叫法五花八门——“塑料瓶”有人叫“可乐瓶”有人叫“饮料瓶”如果只按精准名称匹配用户查不到就会觉得系统很蠢。用别名表加一个预处理逻辑检索时会先拿用户输入去匹配标准名匹配不到就匹配别名大大提升命中率。这个设计其实不复杂但正是这些细节让一个管理系统从“能跑”变成了“好用”。2.2 用户、积分与兑换记录表激励闭环的数据支撑用户表 t_user 不用太花哨核心字段包括 id、username、passwordBCrypt 加密存储、nickname、phone、community_id、address、points当前积分、status正常/禁用、create_time。这里强烈建议不要偷懒只存一个总积分字段一定要同时建积分流水表。t_points_record 积分流水表字段名类型说明idbigint主键user_idbigint用户IDpointsint变动值正数增加、负数扣减source_typevarchar(30)积分来源QUERY、SIGN、RECYCLE、EXCHANGEsource_idbigint来源业务IDremarkvarchar(200)备注create_timedatetime变动时间积分流水表的作用远超想象。首先管理员能通过流水表看到积分是哪里来的用户申诉积分有问题时可以直接溯源其次流水表记录了用户的真实行为轨迹后面做统计报表都能用上。举个例子你可以统计“每天有多少次查询分类”来判断系统活跃度也可以统计“哪个小区的回收预约最多”来评估试点效果这些数据都是从流水表里来的。只有总积分没有流水这些分析就全都做不了。积分兑换这块我建议做成商品表 t_exchange_goods 兑换记录表 t_exchange_record。用户用积分兑换礼品扣除积分并记录流水。如果嫌麻烦简化成直接在后台配置几条兑换规则也行但至少要保持“用户点击兑换 - 扣积分 - 记录流水 - 管理员线下核销”这条链路完整。2.3 预约回收与内容管理表业务流程的数据落点预约回收是整个系统里业务流程最长的一个模块涉及用户下单、管理接单、完成回收、积分结算四个环节。我用 t_recycle_order 一张表把整条链路的状态串起来。t_recycle_order 预约订单表核心字段字段名类型说明idbigint主键order_novarchar(32)订单编号user_idbigint预约用户IDcategory_idbigint回收垃圾类型weightdecimal(10,2)预估重量公斤addressvarchar(255)详细地址appoint_timedatetime预约上门时间statustinyint状态0待接单 1已接单 2已完成 3已取消handler_idbigint处理管理员IDfinish_timedatetime完成时间create_timedatetime创建时间状态字段用 Integer 类型而不是字符串是因为数据库层面用数字做索引和排序效率更高而且 Java 代码里用常量或枚举去映射数字可读性也有保障。完成订单时根据 weight 和积分规则计算应得积分写入积分流水。这个模块写起来不难但状态流转一定要放在 Service 层统一管理不能允许调用方直接修改 status否则状态会乱成一锅粥。内容管理这块就是 t_article 表维护垃圾分类知识文章字段包括标题、封面图、正文内容、所属分类、发布时间。为了减少运营工作量我当初还加了一个 t_question 表做常见垃圾分类问答库用户在检索不到结果时可以“提问”管理员在后台维护问题答案。这个小功能在答辩时加分不少因为它体现了一个“运营思维”而不只是纯粹的增删改查。3. 核心功能实现——几个关键模块的代码级拆解3.1 垃圾检索别名匹配与多字段模糊查询的实现垃圾检索是这个系统用户使用频率最高的接口体验好坏直接影响整个项目的口碑。我把这个接口单独列出来讲因为它不只是“一个 LIKE 查询”那么简单。先看核心代码Override public PageResultGarbageVO searchGarbage(String keyword, int page, int size) { // 先尝试按标准名精确匹配 LambdaQueryWrapperGarbageItem exactQuery Wrappers.lambdaQuery(); exactQuery.eq(GarbageItem::getName, keyword) .eq(GarbageItem::getDisableFlag, 0); GarbageItem exactItem garbageItemMapper.selectOne(exactQuery); if (exactItem ! null) { return buildPageResult(Collections.singletonList(exactItem), 1); } // 精确匹配失败再走别名匹配 LambdaQueryWrapperGarbageItem aliasQuery Wrappers.lambdaQuery(); aliasQuery.like(GarbageItem::getAliasName, keyword) .eq(GarbageItem::getDisableFlag, 0); ListGarbageItem aliasList garbageItemMapper.selectList(aliasQuery); if (!aliasList.isEmpty()) { return buildPageResult(aliasList, aliasList.size()); } // 最后退化为关键词模糊匹配 LambdaQueryWrapperGarbageItem fuzzyQuery Wrappers.lambdaQuery(); fuzzyQuery.like(GarbageItem::getName, keyword) .eq(GarbageItem::getDisableFlag, 0); PageGarbageItem pageResult garbageItemMapper.selectPage( new Page(page, size), fuzzyQuery); return buildPageResult(pageResult.getRecords(), pageResult.getTotal()); }这段代码拆开看有三个层次。第一层走精确匹配效率最高用户输入“废电池”就直接命中第二层走别名匹配解决“锰锌电池”“5号电池”这类叫法差异第三层退化为模糊匹配保证哪怕用户输入不完整也能返回相关结果。为什么刻意做了三层而不是一个 OR 查询因为三层之间是有优先级和数据质量差异的精确命中的结果可信度最高应该排在前面。如果混成一个 OR 查询返回结果里一会儿是精确匹配一会儿是模糊匹配排序会很奇怪而且索引利用率低查询变慢。另外一个细节是查询关键词本身要预处理——去掉首尾空格、把中文的全角括号转半角括号。用户输入“厨余垃圾 剩饭”这种带瑕疵的文本如果不处理匹配结果几乎必然是空的。我当年被这个问题坑过一次用户反馈“搜不到任何东西”排查下来发现是关键词尾部多了个看不见的全角空格。3.2 积分规则引擎让奖励机制灵活可配置积分规则一开始我是写死逻辑的——查询一次加 2 分签到加 1 分回收每公斤加 10 分。后来管理员提了个需求“这周搞活动查询积分翻倍。”我发现写死的逻辑在变化面前非常脆弱于是改成了一张 t_points_rule 规则表把积分计算从代码里彻底解放出来。先定义一个规则接口public interface PointsCalculator { /** * 计算本次行为应得积分 * param userId 用户ID * param sourceType 行为来源类型 * param params 扩展参数比如预约重量 * return 积分值返回0表示该规则不适用 */ int calculate(Long userId, String sourceType, MapString, Object params); }每个积分场景实现一个计算器比如 QueryPointsCalculator、RecyclePointsCalculator然后在配置中心的枚举里注册。这样新增一种积分行为只需要新增一个实现类不需要改动老代码。配置项存在 t_points_rule 表里字段包括 rule_code、rule_name、base_points、enabled、effective_date。管理员想搞活动直接改基础分值或者填一个生效日期区间即可代码里通过规则表读取配置再结合计算器算出最终积分。我强烈建议在设计这类激励功能时把“规则”和“代码”分开。课程设计阶段你当然可以写死常量但答辩的时候如果老师问“你的积分规则是怎么设计的如果产品想搞活动你需要改几行代码”只有规则的抽象设计才能接得住这个问题。而且规则表现在看是“过度设计”后面真到运营阶段你会发现它是刚需。3.3 回收预约状态机状态只能向前走不允许跳变预约回收的状态流转最容易出 bug。用户下单后状态是待接单(0)管理员接单变成已接单(1)完成后是已完成(2)用户取消是已取消(3)。如果代码里没有限制用户恶意把状态从 0 直接改成 2那订单就跳过接单直接“完成”了。实际项目里我通过 Service 层统一的流转方法解决这件事public void updateOrderStatus(Long orderId, Integer targetStatus, Long operatorId) { RecycleOrder order recycleOrderMapper.selectById(orderId); if (order null) { throw new BizException(订单不存在); } // 校验当前状态是否允许流转到目标状态 if (!canTransit(order.getStatus(), targetStatus)) { throw new BizException(非法状态流转: order.getStatus() - targetStatus); } // 如果是取消操作需要校验操作人身份用户只能取消自己的订单 if (targetStatus ORDER_STATUS_CANCELED !order.getUserId().equals(operatorId) !isAdmin(operatorId)) { throw new BizException(无权取消此订单); } // 完成订单时结算积分 if (targetStatus ORDER_STATUS_FINISHED) { settleRecyclePoints(order); } RecycleOrder update new RecycleOrder(); update.setId(orderId); update.setStatus(targetStatus); update.setHandlerId(operatorId); update.setFinishTime(targetStatus ORDER_STATUS_FINISHED ? LocalDateTime.now() : null); recycleOrderMapper.updateById(update); } private boolean canTransit(Integer current, Integer target) { // 0 - 1, 0 - 3, 1 - 2, 1 - 3 是允许的其余一律非法 switch (current) { case 0: return target 1 || target 3; case 1: return target 2 || target 3; default: return false; } }这段代码有几点值得细说。首先状态机判断放在 Service 层Controller 层只负责接收参数和调用这样无论前端怎么构造请求最终都绕不过这套校验。其次完成订单时的积分结算是自动触发的不是前端调完“完成接口”再另调一个“加积分接口”避免两步操作中间出错导致积分漏发。最后所有状态变更都校验操作人身份用户只能取消自己的订单管理员则可以接单和完成权限边界清晰。我见过不少版本的项目状态更新直接就是一句UPDATE t_recycle_order SET status #{status} WHERE id #{id}完全不做校验导致测试的时候状态乱跳、数据对不上。这种细节问题在答辩演示的时候被老师追问一下会相当尴尬。3.4 管理端统计报表用数据驱动垃圾分类策略管理端统计功能是我觉得这个项目最有“产品价值”的部分。不只是让管理员看到“有多少用户、多少订单”而是让管理员看到“什么垃圾被查得最多”“哪个小区参与度最高”“什么时段预约回收最多”这些数据能反馈到社区宣教和资源投放策略上。核心统计 SQL 其实不难。比如查“分类查询热度 Top 10”先用积分流水表拉出所有 QUERY 类型的记录再去关联垃圾条目表和分类表public ListCategoryStatisticsVO queryCategoryQueryStats(LocalDate start, LocalDate end) { // 按来源类型和所属分类分组统计查询次数 return recycleOrderMapper.selectCategoryStats(start, end); }对应的 XML 里写聚合 SQLselect idselectCategoryStats resultTypeCategoryStatisticsVO SELECT c.id AS categoryId, c.category_name AS categoryName, COUNT(*) AS queryCount FROM t_points_record pr INNER JOIN t_garbage_item gi ON pr.source_id gi.id INNER JOIN t_category c ON gi.category_id c.id WHERE pr.source_type QUERY AND pr.create_time BETWEEN #{start} AND #{end} GROUP BY c.id, c.category_name ORDER BY queryCount DESC /select这段 SQL 里有个容易忽略的细节积分流水里的 source_id 存的是垃圾条目 ID所以关联垃圾条目表才能拿到 category_id再关联分类表拿到分类名称。三层 JOIN 是必要的。如果当初设计流水表时只存了 source_type 没有 source_id那这个统计就完全做不了。这也是我在前面强调积分流水表要留来源ID的原因。按小区统计参与度同理先拿到 t_user 里的 community_id再关联回收订单表和用户表。如果系统后续接入地图可视化把小区经纬度加进用户表这些数据可以直接打点渲染热力图展示效果会一下子提升好几个档次。3.5 Vue 前端与 Spring Boot 的集成一套代码两种部署方式市面上很多 Spring Boot Vue 项目是前后端完全分离的开发时通过代理联调部署时起两个服务。但我建议在做这类管理系统时利用 Spring Boot 的静态资源映射机制把 Vue 的构建产物直接放进项目里。操作其实非常简单。前端 Vue 工程的vue.config.js里设置publicPath: ./这样打包出来的资源都是相对路径。然后执行npm run build把 dist 目录下的文件复制到 Spring Boot 工程的src/main/resources/static目录。Spring Boot 默认把 classpath 下的 static、public、resources 等目录映射为静态资源根路径所以打包后访问http://localhost:8080/就直接进入前端页面不需要额外配置。开发调试阶段也可以保持前后端分离模式前端跑在 dev server通过 proxy 把/api开头的请求转发到 Spring Boot 的 8080 端口。部署时再走打包集成方案一套代码两种跑法。这个集成方式其实并不复杂但它在答辩时的演示效果非常好——你只需要java -jar一个命令就能启动整个项目不像其他组还要开两个终端光这一点就能给老师留下“这个学生工程素养不错”的印象。注意前端路由一定要使用 hash 模式不要用 history 模式。因为 history 模式刷新页面时请求会直接打到 Spring Boot 的接口路由上而 Spring Boot 没有对应的 Controller会返回 404。hash 模式用的是#后面的路径浏览器不会把#后的内容发给服务器天然规避这个问题。4. 项目工程结构、配置与部署实战4.1 标准分层目录结构一眼看懂每个类的职责整个 Spring Boot 工程的目录结构完全遵循业界标准的分层架构controller - service - mapper - entity不用奇怪的包名尽量让人一看就明白。com.example.garbage ├── controller // 接口层只做参数接收和结果封装 │ ├── GarbageQueryController.java │ ├── RecycleOrderController.java │ ├── UserController.java │ └── AdminStatsController.java ├── service // 业务逻辑层 │ ├── impl │ │ ├── GarbageQueryServiceImpl.java │ │ ├── RecycleOrderServiceImpl.java │ │ └── PointsServiceImpl.java │ └── PointsCalculator.java // 积分计算器接口 ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体映射 ├── common │ ├── Result.java // 统一响应体 │ ├── BizException.java │ └── GlobalExceptionHandler.java └── config └── MybatisPlusConfig.java // 分页插件等配置我特别想强调统一响应体Result.java。所有接口返回的数据都包一层{ code, message, data }前端用同一套逻辑去处理和解析异常。这样写的好处有三点一是异常信息能统一拦截无论是业务异常还是系统异常前端拿到的都是结构化数据二是每个 Controller 不需要重复写 try-catch三是为将来做接口文档和联调省了大量沟通成本。这个类很简单就三四个字段和一个静态工厂方法public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT fail(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } }配合全局异常处理器业务抛出的BizException都会被自动转成Result.fail()返回。这是 Spring Boot 开发的标配玩法但很多新手项目里每个接口都自己拼 Map 返回代码重复严重看着也乱。工程结构这件事说白了就是“约定优于配置”把类和包的职责定清楚后面维护、加需求、甚至答辩讲代码都会轻松很多。4.2 application.yml 配置与数据库初始化的关键点配置这块最容易踩坑的是数据库和时区。我见过不少同学拿着项目跑不起来“启动正常但查询报错”最后检查发现是数据库连接串少了时区参数。稳妥的配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/garbage_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: disableFlag logic-delete-value: 1 logic-not-delete-value: 0这里几个配置项值得解释一下。useUnicodetruecharacterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai解决 JDBC 8 及以上版本对时间的时区校验问题——没有这个参数插入时间字段会直接报错useSSLfalse是因为本地开发环境不需要 SSL 加密省去证书问题。allowPublicKeyRetrievaltrue则是新版 MySQL 驱动连接需要的一项配置不加在某些版本下会抛 Public Key Retrieval is not allowed 异常。数据库初始化通常是这样的流程先创建数据库CREATE DATABASE garbage_system DEFAULT CHARACTER SET utf8mb4;再执行项目文档里的init.sql脚本。脚本里包含了所有建表语句和初始数据其中垃圾分类的字典数据一定要提前灌好不然用户打开系统查什么都返回空。我整理脚本时顺手把常见 200 多条垃圾条目和 50 多条别名都写进去了这部分数据量看起来不多但每条都需要去查分类标准、写建议工作量其实不小。拿到新环境第一步就是建库、导数据、启动、验证四步走完基本就妥了。4.3 从零启动项目Maven、IDEA、前端构建全流程假设你拿到的是源码加文档的完整交付那么启动项目按这套流程走基本不会卡壳。先确认本地环境JDK 版本必须和 pom.xml 里指定的一致。Spring Boot 2.7 用 JDK 8 或 11 都行Spring Boot 3.x 必须 JDK 17。我当时交付的版本是 Spring Boot 2.7因为考虑到大部分高校机房装的是 JDK 8兼容性更好。这一步看似基础但我真遇到过有人用 JDK 17 跑 Spring Boot 2.5 的旧项目启动时直接报 UnsupportedClassVersionError原因是 JDK 版本过低根本原因是搞反了两者的版本约束关系。接着导入 IDEA。选择 File - New - Project from Existing Sources然后定位到 pom.xml让 Maven 自动拉依赖。国内环境加载依赖慢的话在.m2/settings.xml里配置阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror依赖首次加载可能需要几分钟耐心等。然后修改 application.yml 里的数据库账号密码启动GarbageApplication.java的 main 方法。看到类似Started GarbageApplication in 3.2 seconds的日志就说明后端跑起来了。前端如果是集成模式直接访问http://localhost:8080/即可如果是分离模式先npm install再npm run serve访问 dev server 的地址。如果启动时报端口被占用常见原因是本地已经有其他 Java 进程占了 8080。可以先netstat -ano | findstr 8080看进程号再杀掉或者在配置里换个端口。这种小问题一多就会让人误以为项目有问题其实环境的事占了一半。4.4 源码与文档的搭配使用文档不是摆设这套项目附带的文档我拆成了三部分需求规格说明书、数据库设计说明书、接口文档。如果你是拿它来做课程设计这三份文档基本就是论文的骨架稍作加工就能作为毕业论文初稿。如果你是买家或学习者建议的阅读顺序是先看需求文档再打开数据库脚本对照表结构最后带着问题去源码里找实现。这样效率最高而不是一上来就从头到尾读代码——读代码是记不住的带着问题读才有针对性。文档里还有一份部署说明和演示脚本演示脚本里我列出了 8 组演示数据比如“榴莲壳”“大棒骨”“旧手机”“油漆桶”“小龙虾壳”这些容易混淆的垃圾专门用来在演示系统时制造亮点。这个细节对我个人来说价值极高因为现场演示最怕的就是输入一个词查不到结果提前准备一组稳的数据能避免这种尴尬。后面我会把其中的一部分放到常见问题里继续讲更细的坑。5. 常见问题与排坑实录5.1 中文乱码与时间字段报错的根因中文乱码是这个系统从开发环境到部署环境最常出现的兼容性问题。表现是页面上查询出来的垃圾名称全是问号或者数据库里存的是正常中文但接口返回乱码。排查思路要分层先看数据库表编码是不是 utf8mb4再看 JDBC 连接串有没有 characterEncodingutf8再看 Tomcat 的 URI 编码是不是 UTF-8。我遇到的大部分情况集中在数据库连接串缺失编码参数或者是 MySQL 5.7 环境下建表时没有显式指定字符集导致表使用了默认的 latin1中文自然就存不进去。解决办法很直接——建库时专门指定DEFAULT CHARACTER SET utf8mb4这个设置会传递到后续所有表。时间字段报错则是另一个高频问题。报错信息类似The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这种乱码提示既是时区问题也是编码问题。解决方案就是在连接串里明确指定 serverTimezoneAsia/Shanghai。还有一点实体类里的时间字段建议统一用 LocalDateTime不要用 java.util.Date前者时区处理更友好配合 JSON 序列化也更自然。5.2 垃圾检索命中不准索引和别名的双重优化如果你发现输入“塑料瓶”能查到但输入“可乐瓶”查不到这不是 SQL 写错了而是数据里根本没有“可乐瓶”这个名字。解决思路前面已经说了——维护别名。实际操作中要养成一个习惯每次从运营后台发现用户搜索但无结果的关键词就把它补充到对应垃圾条目的别名里。哪怕一时不知道“丑橘皮”该归到哪个分类也可以先用模糊匹配查看现有条目把“橘皮”的别名加上“丑橘皮”。坚持下去这个系统的字典数据会越来越厚实用户体验也会越来越好。另一个性能层面的问题是模糊查询走不走索引。LIKE %关键词%这种写法数据库优化器基本不会选择索引全表扫描在数据量几百条时没感觉等数据量冲到百万级就会显著变慢。课程设计阶段不用担心但如果这系统真的部署到小区里用户基数上去之后建议把检索改成“普通索引前缀匹配 全文索引ngram 分词”的组合。前缀匹配用LIKE 关键词%能命中索引全文索引对中国分词的支持也不错。这个优化在系统文档里有专门说明属于“面向未来扩展”的设计。5.3 并发下积分重复发放怎么保证只加一次积分模块的并发问题是老油条才会关注的细节。场景是这样的用户点击“签到”按钮因为网络慢前端做了重试两个请求几乎同时到达后端两个线程都读到用户当前没签到的状态然后都执行“加 1 分”的操作。结果就是用户只点了一次积分加了两分。单机情况下把方法加synchronized是有用的但集群环境下两个实例之间没法同步互斥。正解有两个要么用 Redis 分布式锁要么用数据库唯一索引兜底。课程设计阶段我用的是唯一索引方案。在 t_points_record 表上建一个联合唯一索引uk_user_source(user_id, source_type, source_id)签到积分记录的 source_id 固定为当天日期比如 20250601这样同一个用户一天只能插入一条签到记录重复请求第二次插入时数据库会抛 DuplicateKeyException。代码里捕获这个异常并忽略即可用户看到的效果就是一次签到、一分奖励。要注意的是如果需求允许一天多次签到比如一天能签到三次每次 1 分那唯一索引就不能用日期做 source_id而是改成“日期 次数”这些细节要根据实际业务调整。5.4 前端跨域与刷新 404两个常见部署坑开发模式下前端 dev server 跑在localhost:5173Vite 默认或localhost:8081后端跑在localhost:8080浏览器会拦截跨域请求。解决办法一是后端配置全局跨域过滤器二是前端 dev server 配 proxy。我推荐用代理方案因为生产环境集成了静态资源之后本身不存在跨域问题根本不需要全局打开跨域开关。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })刷新 404 的问题前面已经提过前端路由要用 hash 模式。但如果有人一定要用 history 模式也不是无解——在 Spring Boot 里加一个全局路由转发把非/api开头的路径都转发到index.html让前端路由接管。这种写法需要加一个 WebMvcConfigurer 的配置但我还是推荐直接用 hash 模式省心演示也更稳。5.5 MyBatis-Plus 自动填充create_time 不再手动赋值每个表都有 create_time 字段如果手动在每个 insert 里写setCreateTime(LocalDateTime.now())代码会非常啰嗦也容易漏。MyBatis-Plus 提供了字段自动填充机制用起来很舒服。实体类字段上加注解TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;然后写一个 MetaObjectHandler 的实现类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这样所有继承了公共父类字段的实体插入和更新时时间字段都被自动维护代码无法绕过这些填充逻辑也不会有遗漏。这套机制尤其适合“管理系统”这类字段规律性很强的项目几乎每个表都有创建时间和更新时间花十分钟配好后面省下的是几十处重复代码。5.6 线上演示必备几个最有冲击力的演示用例最后分享一个很多人教训深刻的点答辩演示和现场试用永远不要用真实用户随口输入的关键词来测系统。一定要提前准备一组稳、快、有对比度的演示用例。我自己的清单里常备这六组演示输入期望结果展示点榴莲壳其他垃圾建议“难以降解投放时注意包好”冷门知识展示大棒骨其他垃圾不是厨余垃圾纠正认知误区过期药品有害垃圾建议“连包装一起投放”安全提示能力可乐瓶可回收物清洗压扁后投放别名匹配能力小龙虾壳厨余垃圾常见食物垃圾识别旧手机可回收物建议“删除个人数据后再处理”处理建议的实用性这套数据我百试百灵。演示时故意从“容易猜错”的垃圾开始观众往往会想当然地以为大棒骨是厨余垃圾结果系统给出“其他垃圾”全场一下子就记住了这个知识点系统的“专业性”也就立住了。反而从“塑料瓶”这种理所应当的结果开始观众会觉得没什么特别。演示的节奏感也是产品体验的一部分对吧我在实际项目中体会最深的一点是做这类管理系统数据库的字典质量和流程的状态机设计比表面上的界面美观更重要。垃圾分类的准确性全靠数据支撑预约回收的可靠性全靠状态流转约束这两个地方扎实了系统就有灵魂。带源码和文档的完整交付确实省心但要想答辩或者汇报时讲得清楚建议把每个核心模块的“为什么这么设计”也吃透——尤其是我上面提到的别名表、积分流水、状态机三层结构每一个都能拿出来当亮点讲。这套系统后续还可以继续扩展比如对接小程序、给回收订单加路线轨迹、用定时任务生成日报推送都是不错的进阶方向但要记住底层的数据结构别轻易推翻前期的几张核心表设计得合理后续怎么玩都有底气。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑