ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的社区智能垃圾管理系统:从架构设计到部署避坑

基于SpringBoot+Vue的社区智能垃圾管理系统:从架构设计到部署避坑 简介这是一套基于Spring Boot与Vue的社区智能垃圾管理系统完整源码面向毕业设计、课程设计、大作业或工程实训场景适合初学者以及有一定基础的Java学习者参考。项目采用典型的前后端分离结构后端使用Spring Boot框架搭建前端基于Vue.js开发同时提供SQL数据库脚本和配套说明文档可清晰了解从数据表到接口再到页面的完整流程。资源包内共797个文件、约40MB其中186个Java文件对应后端业务逻辑127个Vue文件实现前端页面70个JS文件负责交互处理另有159个SVG图标、CSS样式、XML配置等辅助文件。压缩包中还包含1个SQL脚本以及批处理启动、打包脚本方便直接导入常用开发工具后运行。整个源码经过精心调试可在原有基础上修改扩展适合作为毕业设计或初期项目立项基础目前已有98人学习使用中遇到问题可联系作者获得支持。1. 社区智能垃圾管理系统从“随手扔”到“分类可追溯”去年帮一个街道做信息化改造社区搞垃圾分类积分试点一开始全靠志愿者蹲在垃圾房旁边拿纸笔登记哪户扔了什么、积了几分。一个月下来登记表摞了厚厚一沓月底核算积分要两个人对两天还经常被居民投诉“我明明扔了可回收物积分没到账”。社区智能垃圾管理系统这个方向就是把投放登记、分类查询、积分统计全部搬到线上。基于SpringBoot Vue做前后端分离后端管接口和数据前端管扫码查询和管理员看板投放记录可查、积分自动累计、分类结果秒回。这套设计很适合两类人一是拿它当SpringBoot和Vue全栈练手项目的学生二是有社区、物业、园区信息化需求的开发者照着这套思路换掉垃圾场景就是一套通用的网格化登记系统。2. 技术选型与前后端分离架构为什么是SpringBootVue数据流怎么走2.1 选型理由SpringBoot稳定生态与Vue组件化的分工现在做这类管理系统最常见的组合就是SpringBoot Vue。SpringBoot负责提供RESTful接口、连数据库、做业务校验Vue负责渲染页面、管交互状态。前后端通过JSON交换数据逻辑边界很清晰。选SpringBoot而不是SSH或者裸Servlet是因为它的自动配置和starter机制能省掉大量样板代码——引入一个spring-boot-starter-web就带起内嵌Tomcat打出来的jar包直接java -jar跑部署成本低适合社区这类单机或小集群场景。Vue这边选它是因为组件化拆起来顺手一个垃圾查询框是一个组件一个积分看板是一个组件彼此之间用props和事件通信不会像jQuery时代那样一改功能就全局牵一发动全身。这种架构还有个连带好处前端可以独立开发和调试。Vue开发时用npm run dev起一个本地服务后端接口还没写好时可以用mock数据先画页面后端写完后再通过代理或CORS把请求转发过去。前后端并行推进交付周期比单体JSP快不少。我第一次做这类项目时也纠结过要不要用模板引擎直接渲染后来发现垃圾分类这种页面要频繁刷新局部状态Vue的双向绑定比JSP重新渲染整页要顺滑得多。2.2 springboot项目结构与数据流设计我一般会按SpringBoot标准分层建目录Vue这边用vue-cli或vite创建项目后再按view和component拆分。一个典型的社区智能垃圾管理系统后端目录是这样的com.example.garbage ├── GarbageApplication.java # SpringBoot启动类 ├── controller/ │ ├── UserController.java # 用户注册、登录、积分查询 │ ├── GarbageController.java # 垃圾类别查询、投放记录 │ └── StatsController.java # 分类统计、排行榜 ├── service/ │ ├── UserService.java │ ├── GarbageService.java # 垃圾识别与分类核心逻辑 │ └── RecycleService.java # 投放记录与积分事务 ├── mapper/ │ ├── UserMapper.java │ ├── GarbageItemMapper.java │ └── RecycleRecordMapper.java ├── entity/ │ ├── User.java │ ├── GarbageItem.java │ └── RecycleRecord.java └── config/ ├── CorsConfig.java # 跨域配置 └── MybatisPlusConfig.java # 分页插件等Controller层只做参数接收和结果返回不写业务逻辑。Service层处理业务规则比如垃圾名称怎么匹配、积分怎么算Mapper层用MyBatis-Plus操作数据库。数据流是单向的Vue发起axios请求 - SpringBoot Controller接住 - Service处理 - Mapper查询MySQL - 再逐层返回JSON给前端。这样分层的好处是分类规则调整只改Service数据库表结构调整只改Mapper互不影响。Vue侧目录对应为src/api放接口封装src/views放页面如Login.vue、GarbageQuery.vue、Dashboard.vuesrc/router放vue路由配置。这个结构也是springboot项目结构里最常见的那套照它搭后期排查问题能顺着目录快速定位。2.3 数据库表设计用户、垃圾类别、投放记录与积分整个系统的核心数据模型是四张表用户表、垃圾类别表、投放记录表、积分账户表。用户表存居民或管理员账号垃圾类别表存四分类及对应关键词投放记录表存每次投放的时间、垃圾类型、重量积分账户表存累计积分和可回收物的积分明细。建表语句如下CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT BCrypt加密后的密码, role TINYINT DEFAULT 0 COMMENT 0居民 1管理员, address VARCHAR(100) COMMENT 楼栋房号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE garbage_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(20) NOT NULL COMMENT 可回收/厨余/有害/其他, keywords TEXT COMMENT 关键词,逗号分隔, description VARCHAR(255) COMMENT 投放说明 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT垃圾类别表; CREATE TABLE recycle_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, garbage_name VARCHAR(100) COMMENT 居民输入的垃圾名称, category_id BIGINT NOT NULL, weight DECIMAL(10,2) DEFAULT 0.00 COMMENT 重量kg, points INT DEFAULT 0 COMMENT 本次获得积分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投放记录表; CREATE TABLE points_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT UNIQUE NOT NULL, total_points INT DEFAULT 0, version INT DEFAULT 0 COMMENT 乐观锁版本号 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT积分账户表;这里几个要点一是所有中文表字段都用utf8mb4能直接存Emoji和特殊符号二是recycle_record建了(user_id, create_time)的联合索引因为排行榜和投放大屏最常按用户和时间段查这个索引能把查询从全表扫描降下来三是points_account加version字段做乐观锁用户同时投两单时不会把积分加丢。积分规则我建议做成可配置的参数可回收物每公斤100积分厨余每公斤10积分有害和其他不计分这些值不要写死在代码里后面调激励力度会频繁改。智能分类在这套设计里不靠图像识别而是关键词规则加知识库。真正生产环境有摄像头图像识别但大多数毕业设计和社区试点项目用的是垃圾名词到四分类的映射表。这里有一个设计取舍把关键词表直接建在garbage_category里维护“香蕉皮、西瓜皮、剩菜剩饭”这些高频词。投放记录关联到楼栋房号可归类到具体住户以便月底自动生成每家每户的积分排行。这样设计支撑两个核心功能居民输入一个垃圾名系统返回分类管理员看分类统计和投放热力图——这个热力图用时间加楼栋两个维度做透视SQL里用GROUP BY按天聚合即可不需要引入大数据组件。3. SpringBoot后端从实体类到接口的核心实现3.1 实体类与MyBatis-Plus配置后端代码从实体类和框架配置说起。先用MyBatis-Plus因为它内置了单表CRUD写垃圾类别这种简单表可以少写大量Mapper XML。application.yml核心配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/garbage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0逻辑说明url里characterEncodingutf8和serverTimezoneAsia/Shanghai是这类项目里最容易被忽略的两个参数——不加前者中文入库变问号不加后者日期差8小时。map-underscore-to-camel-case让数据库的create_time自动映射到实体类的createTime。逻辑删除配置deleted字段以后删除用户默认是UPDATE而不是DELETE保留操作记录这对社区这种需要追溯的场景很有用。实体类写法Data TableName(garbage_category) public class GarbageCategory { TableId(type IdType.AUTO) private Long id; private String categoryName; private String keywords; private String description; }参数说明TableName指定表名TableId(type IdType.AUTO)表示数据库自增主键。如果实体属性名和表字段不一致用TableField(实际字段名)显式指定比如可以把categoryName改成name只要表里是category_name一样能映射。这里我建议保持字段名和数据库一致少写注解后期加字段不容易乱。3.2 垃圾识别与分类接口关键词匹配与规则兜底垃圾分类接口是整套系统的核心。居民在输入框敲一个垃圾名系统要立刻判断属于哪一类。生产环境有做图像识别和智能秤的但最常用的可落地方案是关键词匹配加规则兜底。GarbageService里维护一个关键词到分类的映射启动时从数据库加载到内存避免每次查询都打数据库。Service public class GarbageService { private static final MapString, Long KEYWORD_MAP new ConcurrentHashMap(); private final GarbageItemMapper garbageItemMapper; private final GarbageCategoryMapper categoryMapper; public GarbageService(GarbageItemMapper garbageItemMapper, GarbageCategoryMapper categoryMapper) { this.garbageItemMapper garbageItemMapper; this.categoryMapper categoryMapper; } PostConstruct public void loadKeywordMap() { ListGarbageCategory categories categoryMapper.selectList(null); for (GarbageCategory c : categories) { String[] keywords c.getKeywords().split(,); for (String kw : keywords) { KEYWORD_MAP.put(kw.trim(), c.getId()); } } } public Long matchCategory(String garbageName) { String normalized garbageName.replaceAll(\\s, ).toLowerCase(); for (Map.EntryString, Long entry : KEYWORD_MAP.entrySet()) { if (normalized.contains(entry.getKey())) { return entry.getValue(); } } // 未命中兜底到“其他垃圾” return categoryMapper.selectOne( new LambdaQueryWrapperGarbageCategory() .eq(GarbageCategory::getCategoryName, 其他垃圾) ).getId(); } }逻辑说明PostConstruct在应用启动时执行把类别表里每一类的关键词列表拆出来放进ConcurrentHashMap匹配时用字符串包含判断而非全等这样“香蕉皮”和“香蕉 皮”在去掉空格后都能命中“香蕉皮”。兜底返回“其他垃圾”这一设计很关键——垃圾分类宁可给一个保底分类也不能让接口报错让居民干等。参数说明ConcurrentHashMap在并发请求下不会出现扩容死循环这是HashMap在多线程下的一个翻车点replaceAll(\s, )去掉了半角空格、全角空格和Tab规则压缩后再匹配LambdaQueryWrapper是MyBatis-Plus的查询构造器.eq(GarbageCategory::getCategoryName, 其他垃圾)等效于SQL里的WHERE category_name 其他垃圾。Controller层写法RestController RequestMapping(/api/garbage) public class GarbageController { private final GarbageService garbageService; private final RecycleService recycleService; GetMapping(/query) public Result query(RequestParam String name) { Long categoryId garbageService.matchCategory(name); GarbageCategory category garbageService.getById(categoryId); return Result.ok(category); } PostMapping(/recycle) public Result recycle(RequestBody RecycleRecordDTO dto) { recycleService.submitRecycle(dto); return Result.ok(投放成功); } }说明Result是统一返回体包含code、message、data三个字段。前端axios拦截器只看code非200就弹错误提示。这里参数校验用NotBlank注解比手工判空更省事配合Valid在方法参数上标记SpringBoot会自动返回400错误不用每个接口都写if (name null)。3.3 投放记录与积分接口事务与springboot定时任务投放记录必然涉及积分这两个操作必须在一个事务里完成否则会出现记录写了、积分没加的脏数据。做法在RecycleService.submitRecycle上加Transactional。同时积分账户用乐观锁保证并发安全。Service public class RecycleService { private final RecycleRecordMapper recordMapper; private final PointsAccountMapper pointsMapper; Transactional(rollbackFor Exception.class) public void submitRecycle(RecycleRecordDTO dto) { GarbageCategory category categoryMapper.selectById(dto.getCategoryId()); int points calcPoints(category.getCategoryName(), dto.getWeight()); RecycleRecord record new RecycleRecord(); record.setUserId(dto.getUserId()); record.setGarbageName(dto.getGarbageName()); record.setCategoryId(dto.getCategoryId()); record.setWeight(dto.getWeight()); record.setPoints(points); recordMapper.insert(record); PointsAccount account pointsMapper.selectById(dto.getUserId()); int newPoints account.getTotalPoints() points; int updated pointsMapper.update(null, new LambdaUpdateWrapperPointsAccount() .eq(PointsAccount::getUserId, dto.getUserId()) .eq(PointsAccount::getVersion, account.getVersion()) .set(PointsAccount::getTotalPoints, newPoints) .set(PointsAccount::getVersion, account.getVersion() 1)); if (updated 0) { throw new RuntimeException(积分更新冲突请重试); } } private int calcPoints(String categoryName, BigDecimal weight) { return switch (categoryName) { case 可回收 - weight.multiply(BigDecimal.valueOf(100)).intValue(); case 厨余 - weight.multiply(BigDecimal.valueOf(10)).intValue(); default - 0; }; } Scheduled(cron 0 30 20 * * ?) public void dailySummary() { // 每天20:30汇总各楼栋投放量写入统计表供大屏展示 ListMapString, Object stats recordMapper.countByBuilding(); statsMapper.batchInsert(stats); } }逻辑说明Transactional(rollbackFor Exception.class)指定任何异常都回滚默认只回滚RuntimeException如果抛的是自定义CheckedException就会不回滚。积分更新用乐观锁UPDATE时带上version如果影响行数为0说明账户被其他请求改过就抛异常回滚让用户重新提交。Scheduled cron表达式“0 30 20 * * ?”表示每天20点30分执行这是springboot定时任务最直接的一种用法别漏了在启动类上加EnableScheduling。参数说明calcPoints里的乘数就是积分倍率以后调整激励强度只改这一个方法即可。管理员在后台要看投放记录列表直接给一个分页接口最实在。MyBatis-Plus的分页插件必须在配置类里注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置完成后Service里用Page对象接收即可public PageRecycleRecordVO pageRecords(int pageNum, int pageSize, Long userId) { PageRecycleRecord page new Page(pageNum, pageSize); LambdaQueryWrapperRecycleRecord wrapper new LambdaQueryWrapper(); if (userId ! null) { wrapper.eq(RecycleRecord::getUserId, userId); } wrapper.orderByDesc(RecycleRecord::getCreateTime); PageRecycleRecord result recordMapper.selectPage(page, wrapper); // 再联表查出用户名和垃圾类别名称组装VO return convertToVO(result); }参数说明PaginationInnerInterceptor(DbType.MYSQL)里DbType指定数据库类型如果写成ORACLE分页生成的SQL会是ROWNUM写法MySQL下必报错。orderByDesc按投放时间倒序让最新记录排在最前面管理员不用翻页找刚发生的投放。组装VO时注意别在for循环里逐条selectById那会触发N1查询我一般先取回所有userId集合用IN一次性查出User列表再在内存里组装接口响应时间能从几百毫秒降到几十毫秒。4. Vue前端页面搭建与接口对接4.1 Vue项目初始化与vue路由配置前端用Vue框架搭建。无论是vue2加element-ui还是vue3加element-plus第一步都是创建项目并安装依赖。这里以Vue3为例npm create vitelatest community-garbage -- --template vue cd community-garbage npm install npm install axios vue-router4 element-plus逻辑说明npm create vite是当前最主流的vue安装及环境配置方式比vue-cli更快axios负责发HTTP请求vue-router4是配合Vue3的路由版本注意vue2用vue-router3版本装错会导致路由根本注册不上。装完依赖后配置vue路由// src/router/index.js import { createRouter, createWebHashHistory } from vue-router; const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /dashboard, component: () import(../views/Dashboard.vue), meta: { requiresAuth: true } }, { path: /garbage, component: () import(../views/GarbageQuery.vue), meta: { requiresAuth: true } }, { path: /records, component: () import(../views/RecordList.vue), meta: { requiresAuth: true } } ]; const router createRouter({ history: createWebHashHistory(), routes }); router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } }); export default router;说明createWebHashHistory是vue路由里最稳妥的模式URL带#号打包后丢给SpringBoot静态目录也不会刷新404。如果你追求好看用createWebHistory(HTML5模式)就必须后端配forward到index.html否则刷新必白屏这个坑在第5章细讲。这里用的是vue动态路由的一种基础写法——路由懒加载组件在访问时才加载首屏体积明显变小。beforeEach是全局前置守卫没有token直接踢回登录页这是这类管理系统的标配逻辑。4.2 axios封装与API对接统一拦截器与跨域页面里直接axios.post(http://localhost:8080/api/...)不是不能跑但每个请求都要重复写baseURL和token后患无穷。我会统一封装一个request实例// src/api/request.js import axios from axios; import { ElMessage } from element-plus; import router from ../router; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); if (res.code 401) { router.push(/login); } return Promise.reject(new Error(res.message)); } return res.data; }, error { ElMessage.error(error.message || 网络异常); return Promise.reject(error); } ); export default request;逻辑说明baseURL设为/api开发环境通过Vite代理转发到后端生产环境由SpringBoot的server.servlet.context-path或反向代理处理这样前端代码里就不用写死IP和端口换环境只需改配置。请求拦截器统一注入token每次登录后把token放localStorage下次请求自动携带。响应拦截器把所有返回体统一解包到res.data业务组件里拿到的直接是data字段不用每个页面都判code。401时自动踢回登录页这个机制能防住token过期后页面继续静默请求黑匣子一样挂掉的情况。开发环境的代理配置在vite.config.js里加export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });参数说明changeOrigin设为true后后端看到的请求Host就是后端地址很多后端鉴权按Host校验不设这一项会出现“接口通了但认证不过”的玄学问题。匹配路径用了前缀匹配/api开头全部转发到8080端口和axios的baseURL正好对应。4.3 分类查询与投放页面前端交互与echarts统计前端页面的核心交互是居民输入垃圾名 - 防抖请求 - 显示分类结果 - 点确认投放 - 后端记积分。查询组件示例template div classquery-panel el-input v-modelkeyword placeholder输入垃圾名称如香蕉皮 clearable inputonInput / div v-ifcategory classresult-card p分类结果{{ category.categoryName }}/p el-button typeprimary clicksubmitRecycle确认投放/el-button /div /div /template script setup import { ref } from vue; import request from ../api/request; const keyword ref(); const category ref(null); let timer null; function onInput() { clearTimeout(timer); timer setTimeout(async () { const data await request.get(/garbage/query, { params: { name: keyword.value } }); category.value data; }, 300); } async function submitRecycle() { await request.post(/garbage/recycle, { userId: localStorage.getItem(userId), garbageName: keyword.value, categoryId: category.value.id, weight: 0.5 }); } /script逻辑说明input事件里做了一个300毫秒防抖居民还在打字时不发请求停下来才查询不然每敲一个字符都打一次后端社区几百户同时用会把接口拖垮。clearTimeout在每次输入时重置计时器这是防抖的标准写法。确认投放时前端传的weight是临时写死的0.5kg真实项目会接智能秤或让居民手动输入接入智能秤后把这个字段换成秤重返回值即可。后端Result统一返回结构对这个交互特别重要——前端拿到res.data直接赋给category页面代码干净不少。管理员的分类统计看板用ECharts画柱状图或饼图。做法是在npm install echarts后用computed把接口返回的分类占比数据转成饼图需要的name/value结构再挂到chart实例上。这类看板注意一个细节ECharts容器在Vue的display:none状态下初始化会拿不到宽度图表宽度变0需要等tab切换动画结束再调resize()或者用v-show而不用v-if破坏已渲染的图表实例。5. 部署与配置避坑新手最容易翻车的5个地方5.1 前后端跨域Access-Control-Allow-Origin报错现象前端起在5173端口后端8080端口浏览器控制台报“Access-Control-Allow-Origin”错误请求被拦截。原因浏览器同源策略前端页面所在源和接口源不一致SpringBoot默认不放开跨域头。解决开发期用Vite代理转发见4.2的proxy配置如果坚持直连后端就在后端加CorsConfigConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns()配allowCredentials(true)是允许带Cookie跨域的写法用allowedOrigins()加allowCredentials(true)会被浏览器拒绝很多人卡在这一条。CORS要区分预检请求OPTIONS方法必须放行否则前端post带json时实际会先发OPTIONS直接404。5.2 数据库中文乱码与时区问题现象插入“可回收垃圾”后数据库显示“???”或者查出来的create_time比本地时间慢8小时。原因建库时用了默认latin1连接串没指定characterEncodingJDBC默认时区是UTC北京是UTC8。解决建库语句写CREATE DATABASE garbage DEFAULT CHARSET utf8mb4连接串加characterEncodingutf8和serverTimezoneAsia/Shanghai具体配置见3.1的yaml。已经乱掉的数据可以用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4补救但最好是建表时就统一。这个坑排起来很快但一旦上线再改历史数据要清洗很痛苦。5.3 SpringBoot版本太高导致的依赖冲突现象用SpringBoot 3.2加MyBatis-Plus 3.5.3启动直接报ClassNotFoundException: javax.servlet.ServletException项目用springboot框架正常但接口全部404。原因SpringBoot 3把javax迁移到了jakarta老版MyBatis-Plus和spring-boot-starter-web还在用javax包。这种情况经常出现在新下载项目模板的毕业设计上springboot版本太高反而成了负担。解决稳妥方案是SpringBoot 2.7.x加MyBatis-Plus 3.5.x这个组合在社区级项目里足够稳定如果一定要用SpringBoot 3就换mybatis-plus-spring-boot3-starter。另一个常见依赖坑是pagehelper和MyBatis-Plus分页插件同时存在两个分页拦截器会打架导致SQL被拼接两次limit项目里只保留一个分页实现即可。提示创建项目时不要无脑选最新稳定版先确认配套的MyBatis-Plus、shiro、jwt这些库是否已经跟上jakarta迁移。5.4 Vue打包放进SpringBoothistory路由白屏现象npm run build后把dist目录复制到SpringBoot的src/main/resources/static启动访问首页空白刷新一个二级路径又404。原因vue-router默认用createWebHistoryURL不带#SpringBoot的静态资源映射找不到前端的路由路径比如/garbage这个路径对应的是SPA里的页面不是实际文件。解决我一般直接用createWebHashHistoryURL变成/#/garbage刷新时SpringBoot忽略#后面的部分永远落在index.html上。如果非用history模式后端加一个转发RequestMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; }参数说明这个路径正则排除了带点号的路径避免静态资源如.js/.css被错误转发。常见的“vue打包放进springboot中”白屏问题八成就是没处理这层路由映射。还有一个细节Vue的publicPath要配成相对路径vite默认是/部署到子路径时资源全丢改成./最省事。5.5 垃圾识别规则的边界错别字与复合垃圾现象输入“香蕉皮”能返回“厨余垃圾”输入“香蕉 皮”或“香蕉皮儿”就返回“其他垃圾”居民在试了几次后直接放弃使用。原因关键词匹配用全等判断没有做归一化规则库里词汇量太少对口语化表达不敏感。解决一是在匹配前做归一化去掉所有空白、全角转半角、小写化代码见3.2的normalized二是维护同义词和错别字表比如“香蕉皮”和“香蕉皮儿”映射到同一个关键词三是规则库不要做死在代码里把keywords字段做成可维护的管理员在后台能增删热更新时重新调用loadKeywordMap。匹配优先级也要定清楚同时命中“纸箱”和“可回收”时按关键词长度降序取最长匹配避免“纸箱”命中“其他”这种子串误判。这些边界规则看似小实际上决定了系统上线后居民愿不愿意用是最影响口碑的“软件体验”。6. 验证与进阶接口联调、识别规则热更新与性能观察系统写完不能只跑通登录注册就交付垃圾分类系统的主链路是“查分类 - 提交投放 - 积分到账”我建议上线前按这条链路做三轮验证。第一轮是接口联调用Postman或Apifox跑一遍/query和/recycle重点看返回code是否为200、积分字段是否按规则计算第二轮是并发验证拿JMeter对/garbage/query发200个线程观察平均响应时间和有没有乐观锁冲突日志社区场景的并发量不大接口超过200ms就要查N1查询第三轮是投放数据抽样造一周模拟数据跑排行榜和大屏统计核对和SQL聚合结果一致。进阶技巧都集中在识别规则上。系统上线后居民并不会按关键词表说话“泡沫箱”“泡沫”这种说法比比皆是很多表达规则表里根本没有。我的习惯是加一张未命中记录表matchCategory兜底返回“其他垃圾”时把原始输入写进这张表再用一个springboot定时任务每天汇总未命中词。管理员看到高频未命中词直接在后台往garbage_category的keywords里追加负载均衡后触发一次loadKeywordMap热更新识别率就一天天涨起来。这个闭环比在代码里堆if else健康得多因为规则数据归运营代码逻辑归开发。性能观察方面我踩过的坑是在PostConstruct里一次性加载全部关键词社区级别几千条没问题但如果你把规则库扩到几十万条启动时间会明显变长。遇到这种情况就把加载改成懒加载首次查询时初始化并在内存里维护一个loaded标志。还有一次我图省事用HashMap存关键词映射压测时发现CPU飙高排查半天是并发扩容死循环换成ConcurrentHashMap后就好了——这类细节没有玄学全是血泪经验。等你把这些验证跑完规则热更新和数据闭环转起来这套基于SpringBootVue的社区智能垃圾管理系统才算真正可交付。希望帮到你。本文还有配套的精品资源点击获取
返回列表