资讯详情

资讯详情

SpringBoot智能垃圾分类系统开发实战:从架构设计到部署上线

做毕设选到这个题目的同学我先把话放在前面基于SpringBoot的智能垃圾分类系统并不是一个只能用来应付答辩的“玩具项目”。垃圾四分类可回收、有害、厨余、其他本身是一个真实存在的业务场景加上SpringBoot Vue前后端分离、图像识别接口调度、积分激励体系这些模块之后它既能覆盖Java后端开发的核心知识点又能做出一个可以演示、可以扩展、能写进简历的真实项目。这篇文章我会从项目整体设计、技术选型、数据库设计、核心接口实现、踩坑记录落到部署打包完整复盘一遍我做这个项目的全过程给你一份可以直接照着做的参考。我接手这个项目时它给我的第一感觉是“模块比想象中多”。用户端有登录注册、扫码投放、分类查询、积分商城管理端有用户管理、分类管理、投递记录、积分管理后端还要接图像识别接口做投放正确性判断前端还要有大屏展示垃圾分类数据统计。这套东西如果真从头自己码一个人没日没夜也得两三周。所以动手之前把架构拆明白、把工作排清楚比闷头写代码重要得多。1. 项目整体设计与技术选型1.1 为什么用SpringBoot而不是SSH或SSM现在很多高校的Java课程还在讲SSHStruts Spring Hibernate或者SSMSpring SpringMVC MyBatis但到了毕设阶段我强烈建议直接用SpringBoot。原因不复杂SSM的配置太繁琐了光spring-mvc.xml、mybatis-config.xml、web.xml这三件套就能劝退一批初学者。SpringBoot通过自动配置把大部分重复劳动消灭掉了你只需要一个带SpringBootApplication注解的启动类内嵌Tomcatjava -jar直接跑起来。我选的是SpringBoot 2.7.x版本。为什么不选3.x因为3.x要求JDK 17而很多同学的机器上装的是JDK 8学校实验室的机器更可能是JDK 8。选2.7.x意味着JDK 8也能跑MyBatis Plus、Redis、各种工具包的兼容性也最稳。这个选择看似简单实际上是给自己省掉了一整类“版本地狱”问题。1.2 核心依赖清单与版本搭配我实测下来这套依赖组合是最稳的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis Plus 持久层框架 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Redis 缓存 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- Lombok 简化实体类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- JWT 登录鉴权 -- dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency !-- 微信小程序登录需要的Http客户端 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency /dependencies这套组合里MyBatis Plus是真正的效率神器。单表CRUD完全不用写SQLBaseMapper接口自带selectById、selectPage、insert这些方法。对于垃圾分类系统这种以单表操作居多的业务写SQL的机会其实很少大部分时候只需要在ServiceImpl里拼条件构造器QueryWrapper。这个框架选对了能省下一大半工作量。1.3 前后端分离架构与目录规划整个项目我用的是标准的前后端分离后端跑在localhost:8080前端Vue项目跑在localhost:8081前端开发服务器通过Nginx或代理转发解决跨域。目录结构我分得比较简单粗暴但非常实用garbage-classification-system ├── backend │ ├── src/main/java/com/example/garbage │ │ ├── controller # 控制层接收前端请求 │ │ ├── service # 业务层核心逻辑 │ │ ├── mapper # 数据持久层接口 │ │ ├── entity # 数据库实体类 │ │ ├── config # 配置类跨域、JWT拦截器、Redis │ │ ├── common # 公共工具类、统一返回结果 │ │ └── utils # 工具类 │ └── src/main/resources │ ├── application.yml │ └── mapper # MyBatis XML文件 ├── frontend │ ├── public │ └── src │ ├── api # 请求封装 │ ├── assets # 静态资源 │ ├── components # 公共组件 │ ├── router # 路由配置 │ ├── store # 状态管理 │ ├── views # 页面 │ └── App.vue └── sql └── garbage.sql # 数据库初始化脚本后端按controller-service-mapper三层分包前端按vue-element-admin的管理后台风格组织这种结构也是面试官最熟悉的。2. 核心功能模块拆解2.1 用户端登录、注册与积分体系用户端我做了三个入口小程序端、H5端、管理后台。小程序端是普通居民使用的H5是给临时访客或者没装小程序的用户用的管理后台是给物业/环卫管理人员用的。登录方式我做了两层账号密码登录使用JWT签发token登录成功后前端把token存在本地每次请求带在请求头里。微信小程序登录小程序端通过wx.login()拿code后端拿code去微信接口换openid实现免注册登录。我给Hutool的HttpUtil封装了一个方法实测下来比用RestTemplate要简洁得多。积分体系是这类系统的点睛之笔。我在数据库里设计了一张points_record表记录每次投放产生的积分变动。投放正确加10分投放错误不扣分但推送纠正提醒连续投放还能拿额外的连续签到奖励。积分可以用来兑换垃圾袋、香皂这类小商品。这是为了让用户产生“使用黏性”不然系统就是个摆设。登录鉴权的核心代码我放在一个拦截器里Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/login) || request.getRequestURI().contains(/register)) { return true; } String token request.getHeader(Authorization); if (StrUtil.isEmpty(token)) { throw new RuntimeException(未登录请先登录); } try { // 解析token验证签名和过期时间 DecodedJWT jwt JWT.require(Algorithm.HMAC256(your-secret-key)).build().verify(token); Integer userId jwt.getClaim(userId).asInt(); request.setAttribute(userId, userId); return true; } catch (JWTVerificationException e) { throw new RuntimeException(登录状态已过期请重新登录); } } }然后在WebConfig里注册拦截器只拦截/api/**路径。这里有个细节注册拦截器的时候要把静态资源和登录接口排除掉否则前端页面都打不开。这个坑很多第一次写拦截器的同学都会踩我后面在常见问题里详细说。2.2 智能识别图像分类接口与兜底策略说“智能”肯定不能只做增删改查。我接了一个图像识别服务用户上传一张垃圾照片后端把图片交给识别接口返回分类结果和置信度。接口设计是PostMapping(/identify) public Result identify(RequestBody IdentifyRequest request) { // 校验base64图片是否为空 if (StrUtil.isEmpty(request.getBase64Image())) { return Result.error(图片不能为空); } // 调用识别服务 String category imageRecognitionService.recognize(request.getBase64Image()); // 关联分类信息返回 GarbageCategory gc garbageCategoryService.getByCode(category); return Result.success(gc); }识别服务的实现是我封装的Service public class ImageRecognitionService { public String recognize(String base64Image) { // 调用百度AI垃圾分类接口或者其他云服务 // 请求参数imagebase64编码的图片数据 // 返回结果解析垃圾分类结果 置信度 MapString, Object params new HashMap(); params.put(image, base64Image); JSONObject result HttpUtil.post(https://aip.baidubce.com/rest/2.0/image-classify/v1/garbage, params); Integer errno result.getInt(errno); if (errno ! 0) { // 接口调用失败时兜底为“其他垃圾”并标记为待人工审核 return other; } String category result.getJSONArray(result).getJSONObject(0).getStr(type); return convertToFourCategories(category); } }这里有一个关键设计识别接口永远不能作为唯一的判定来源。我加了两层兜底如果识别接口超时或返回异常自动归类为“其他垃圾”因为其他垃圾是四分类里最常见的兜底。如果单次识别的置信度低于70%在管理后台生成一条人工审核任务由管理人员查看图片后再确认分类。这两个兜底策略保证系统在任何情况下都能正常走完投递流程不会因为第三方接口抖动导致业务流程中断。这一点在答辩时很加分评委老师会认为你有“生产环境思维”。2.3 管理后台分类管理、投递记录与数据大屏管理后台面向两类角色系统管理员和垃圾分拣员。分拣员负责处理“待人工审核”的识别结果管理员管理整个系统的配置。页面我用的是Vue Element UI。Element UI的表格组件做管理列表非常好用栅格布局做表单页面也很顺手。数据大屏部分我用ECharts画了四个图每日投递量趋势图折线图垃圾类别占比图饼图各小区参与用户排行横向柱状图本月积分发放总量数字翻牌器大屏页面的数据全部来自一个/api/dashboard/statistics接口后端聚合查询四张表的数据。ECharts请求一次拿全量JSON数据前端解析后绘图不需要每张图单独发请求。3. 数据库设计与核心实现3.1 数据表设计思路从用户到投递记录的完整闭环数据库我建了8张核心表它们构成了整个系统的数据骨架表名核心字段作用userid, openid, nickname, avatar, phone, points, role用户信息与积分余额garbage_categoryid, name, code, description, icon四分类基础数据可回收、有害、厨余、其他garbage_itemid, category_id, name, tips具体垃圾条目如“废电池”“果皮”用于检索查询delivery_recordid, user_id, category_id, image_url, status, score, create_time投放记录核心业务表points_recordid, user_id, change_amount, type, remark, create_time积分流水points_goodsid, name, points, stock, image积分兑换商品exchange_recordid, user_id, goods_id, points_cost, status, create_time积分兑换记录announceid, title, content, create_time系统公告核心业务表是delivery_record它记录的是“用户拿什么垃圾、投到哪个桶、判断是否正确、给加了多少分”。这张表的status字段有几个取值1待审核、2审核通过、3审核驳回。通过状态之后才会触发积分增加逻辑。拿垃圾名检索分类这个需求我是这样实现的public GarbageCategory findCategoryByItem(String itemName) { QueryWrapperGarbageItem wrapper new QueryWrapper(); wrapper.like(name, itemName); GarbageItem item garbageItemMapper.selectOne(wrapper); if (item null) { return null; } return garbageCategoryMapper.selectById(item.getCategoryId()); }这里用的是like模糊匹配性能在数据量小的情况下完全没问题。如果要优化可以把“干电池”“纽扣电池”这类常见垃圾的别名存成字典表做精确匹配优先、模糊匹配兜底。3.2 Redis缓存设计分类数据与Token刷新Redis在这个系统里不是撑门面用的它解决的是实打实的问题。垃圾分类的基础数据比如四分类的图标、描述、投放指南是不经常变的我把它缓存起来第一个用户请求时从数据库读并写入Redis之后所有用户都直接命中缓存。缓存key设计为garbage:category:list过期时间2小时。积分加锁也用了Redis。用户投放垃圾后增加积分这个操作如果并发执行可能会出现积分重复加的问题。我用Redis的setnx做了一把简单的分布式锁public boolean tryLock(String key, String requestId, long expireTime) { Boolean result redisTemplate.opsForValue().setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS); return Boolean.TRUE.equals(result); } public void unlock(String key, String requestId) { String value (String) redisTemplate.opsForValue().get(key); if (requestId.equals(value)) { redisTemplate.delete(key); } }上锁时带上requestId是防止误删别人的锁解锁时先比对该值再删除这是分布式锁里最基础的保障。虽然在毕设答辩时面试官一般不会追问到这么细但写出来能让代码的“专业感”上一个档次。3.3 投递打分逻辑正确性判断与积分计算投递判断是整个系统的核心业务逻辑。用户上传垃圾照片系统识别出分类结果用户再选择自己投递的垃圾桶类别两者比对public DeliveryResult handleDelivery(DeliveryRequest request) { // 1. 判断用户是否登录 Integer userId request.getUserId(); // 2. 图片识别分类 String identifiedCategory imageRecognitionService.recognize(request.getImageBase64()); // 3. 比对用户选择的分类 boolean correct identifiedCategory.equals(request.getSelectedCategory()); // 4. 积分计算 int score correct ? 10 : 1; // 5. 记录投递信息 DeliveryRecord record new DeliveryRecord(); record.setUserId(userId); record.setCategoryId(request.getCategoryId()); record.setImageUrl(request.getImageUrl()); record.setStatus(correct ? 2 : 3); record.setScore(score); deliveryRecordMapper.insert(record); // 6. 更新用户积分 if (correct) { userService.addPoints(userId, score); } return DeliveryResult.builder() .correct(correct) .identifiedCategory(identifiedCategory) .score(score) .tips(getTipsByIdentified(identifiedCategory)) .build(); }错误投放给1分是“参与分”目的是不让用户因为投错了而完全丧失积极性。这个设计在业务角度上是说得通的识别错误是系统能力问题用户愿意参与投递本身就值得鼓励。我把这个逻辑在文档里写清楚答辩时也能体现出业务思考。4. 项目环境配置与前端联调4.1 后端环境JDK、Maven、MySQL、Redis后端跑起来需要四个基础环境我列成清单JDK 8安装包直接去官网下配好JAVA_HOME和PATH环境变量命令行输入java -version能出来就行。Maven 3.6用来管理项目依赖。IDE里配置好Maven仓库地址不配置的话下载依赖会非常慢。MySQL 5.7或8.0执行sql/garbage.sql脚本初始化数据。Redis 6.xWindows直接解压启动Linux上用systemctl start redis。开发环境不设密码生产环境务必设置。我踩过一个印象很深的坑MySQL版本不同驱动配置也不同。MySQL 5.7用com.mysql.jdbc.DriverMySQL 8.0要用com.mysql.cj.jdbc.Driver。连接字符串里的serverTimezoneAsia/Shanghai必须加不加的话数据库连接会报时区错误。application.yml的核心配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 timeout: 5000ms mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: autolog-impl配成StdOutImpl可以在控制台打印每一条SQL语句联调时非常有用生产环境再把它注释掉。4.2 前端环境Node.js、Vue CLI与跨域代理前端我用Vue 2 Element UI ECharts这套组合在网络上有海量资料遇到任何坑查起来都方便。Node.js版本建议用16.x或18.x新版Node对老项目的兼容性有时候反而不好。前端本地联调必须解决跨域问题。我在Vue项目的vue.config.js里配置代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }这样前端请求/api/login时开发服务器会把请求转发到http://localhost:8080/api/login绕开了浏览器同源策略的限制。后端我同时配了全局CORS跨域双保险Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns(*)和allowedOrigins(*)的区别。开了allowCredentials(true)之后allowedOrigins不能写*必须写具体域名或者用allowedOriginPatterns。这个细节如果不知道前端哪怕配了代理浏览器也会报跨域错误。4.3 接口文档的关键设计统一返回格式前后端联调最怕的就是各写各的我在项目一开始就统一了返回格式Data public class ResultT { private Integer code; // 200 成功500 失败 private String message; // 提示信息 private T data; // 返回数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }前端封装了统一的request.js每次请求自动带上token、自动处理401跳转登录页、自动弹错误提示。这样前端代码干干净净不用每个页面都处理一遍错误逻辑。5. 核心业务逻辑的代码实现5.1 登录注册与JWT的完整实现用户登录接口接收用户名和密码校验通过后生成JWT token返回PostMapping(/login) public ResultMapString, Object login(RequestBody LoginRequest request) { // 1. 查询用户 QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(username, request.getUsername()); User user userMapper.selectOne(wrapper); // 2. 用户不存在或密码错误 if (user null || !user.getPassword().equals(MD5Util.md5(request.getPassword()))) { return Result.error(用户名或密码错误); } // 3. 生成JWT有效期24小时 Algorithm algorithm Algorithm.HMAC256(your-secret-key); String token JWT.create() .withClaim(userId, user.getId()) .withClaim(username, user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() 24 * 3600 * 1000)) .sign(algorithm); // 4. 返回用户信息不含密码和token user.setPassword(null); MapString, Object data new HashMap(); data.put(user, user); data.put(token, token); return Result.success(data); }这里有个细节数据库中存的密码一定是MD5加密后的值绝对不存明文。注册时MD5Util.md5()一下再入库。虽然MD5不算最强的加密方案对毕设来说够用但在文档里可以提一句“生产环境建议使用BCrypt”体现你的安全认知。5.2 图像识别接口的调度与降级图像识别模块我单独抽了一个Service把所有第三方调用的细节封装在里面上层业务不感知具体用的是哪家服务。这样做的好处是如果后续要换识别服务商只需要改这一个类其他代码不用动。Service public class ImageRecognitionService { Autowired private StringRedisTemplate redisTemplate; private static final String RECOGNITION_URL https://your-recognition-service/api/v1/garbage-classification; public String recognize(String base64Image) { // 1. 先去缓存查避免重复调用三方接口产生费用 String cacheKey recognition: SecureUtil.md5(base64Image); String cached redisTemplate.opsForValue().get(cacheKey); if (StrUtil.isNotEmpty(cached)) { return cached; } // 2. 调用第三方接口 JSONObject params new JSONObject(); params.set(image, base64Image); String resultJson HttpUtil.post(RECOGNITION_URL, params.toString()); JSONObject result JSONUtil.parseObj(resultJson); // 3. 解析分类结果 if (result.getInt(code) ! 0) { throw new BusinessException(识别服务异常); } String category result.getStr(category); // 4. 固化为四分类格式 String normalized normalizeCategory(category); // 5. 写入缓存防止同一张图反复调用 redisTemplate.opsForValue().set(cacheKey, normalized, 24, TimeUnit.HOURS); return normalized; } }这个服务我做了三个优化缓存去重、异常兜底、四分类归一化。缓存的key是图片内容的MD5值同一张图24小时内重复识别直接命中缓存。异常兜底在Controller层捕获返回“其他垃圾”的默认分类。归一化是因为第三方接口返回的可能不只是四分类还可能有“可回收金属”“电子垃圾”这种细类需要映射成四分类的code。5.3 数据统计大屏的动态查询统计接口的SQL实现其实是这类系统最容易出问题的部分。我第一版写了四条独立SQL分别查不同页面结果数据对不上。后来改造成一个聚合查询用UNION ALL把多组统计结果合并成一个返回结构前端拿到直接解析。SELECT total_delivery AS metric, COUNT(*) AS value FROM delivery_record UNION ALL SELECT total_users AS metric, COUNT(*) AS value FROM user UNION ALL SELECT total_points AS metric, SUM(score) AS value FROM delivery_record WHERE status 2 UNION ALL SELECT today_delivery AS metric, COUNT(*) AS value FROM delivery_record WHERE create_time CURDATE()虽然不是一个常规的关系型查询姿势但对大屏场景很实用。前端只需要response.data.list遍历渲染就能拿到所有指标数据不用发多个请求做并行控制。6. 常见问题与排查技巧实录6.1 数据库连接失败时区问题与驱动新旧这个问题出现的频率极高。报错信息一般是The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这是MySQL 8.0或更高版本对时区校验变严导致的。解决办法是在连接串后面加上serverTimezoneAsia/Shanghaiurl: jdbc:mysql://localhost:3306/garbage_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai另外还有一个小概率问题如果MySQL是5.7驱动用com.mysql.cj.jdbc.Driver会报ClassNotFoundException。这时候把驱动换成com.mysql.jdbc.Driver即可。对应到依赖里就是用mysql-connector-java的5.x版本而不是8.x版本。6.2 跨域问题前端能调到接口但浏览器拦截表现是后端测试接口用Postman能通前端调用也能发出请求但浏览器报Access-Control-Allow-Origin错误。原因通常是后端返回的CORS头部不对。排查步骤按顺序来打开浏览器F12看Network里请求的Response Headers有没有Access-Control-Allow-Origin字段。如果没有检查后端的CorsConfig是否真的生效有可能Configuration没扫描到或者定义了两个配置类互相覆盖。如果字段有但值是*且前端代码开启了withCredentials那就是前面说过的allowedOrigins(*)与allowCredentials(true)冲突的问题。前端联调阶段我一般建议直接使用Vue代理生产部署再用Nginx做反向代理这样可以把跨域问题整体“消灭”而不是“解决”。6.3 MyBatis Plus分页查询不生效配置缺失很多人写完分页代码发现total始终为0或者第二页开始查不到数据。问题出在漏了分页插件配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个BeanPage对象里的total不会自动填充分页就退了化成普通列表查询。6.4 图片上传大小超限SpringBoot默认限制1MB用户上传垃圾照片如果图片超过1MB会直接报MaxUploadSizeExceededException。SpringBoot默认的单个文件大小限制是1MB这个值明显不够用。手机拍的照片动不动就三四MB。修改配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB还有更轻微的问题前端Vue上传时如果用的axios没有设置Content-Type: multipart/form-data后端用MultipartFile接收会报Required request part is not present。这个我建议在后端Controller里加一个Base64接收方案作为替代PostMapping(/upload) public ResultString upload(RequestBody UploadRequest request) { // 前端把图片转成base64字符串传过来 // 后端解码后存储到本地或OSS byte[] bytes Base64.getDecoder().decode(request.getImageBase64()); String fileName UUID.randomUUID().toString() .jpg; FileUtil.writeBytes(bytes, uploadPath fileName); return Result.success(fileName); }这个方案能绕开所有multipart相关的疑难杂症在毕设项目里反而是最稳的。你要演示的时候直接用手机拍张照前端fileReader转base64传给后端一气呵成。6.5 前端页面刷新后的数据保持Token过期与路由守卫管理后台里切换菜单页面刷新用户信息突然消失这是很影响体验的问题。原因是用户信息只放在了Vuex的state里刷新以后state被清空。解决办法有两个用户信息同时放到localStorage里刷新后从localStorage恢复。不存完整用户信息只存token刷新后调用/api/user/info重新拉取用户信息。第二个方案更安全也更接近生产实践。我在路由守卫里这样处理router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); return; } if (!store.state.userInfo token) { store.dispatch(fetchUserInfo).then(() next()); } else { next(); } })这样刷新后路由跳转时会自动触发用户信息重新加载用户体验就能保持“看起来一直是登录状态”。7. 项目打包、部署与文档写作的实用建议7.1 前后端分别打包的完整流程后端打包非常简单。在IDEA右侧Maven面板双击clean然后package或者命令行mvn clean package -DskipTests打完包在target目录生成一个garbage-0.0.1-SNAPSHOT.jar命令行直接java -jar garbage-0.0.1-SNAPSHOT.jar看到Started Application in x.xxx seconds就说明启动成功。这里有个容易忽略的点-DskipTests一定加上不然Maven会自动跑单元测试如果你的测试类里有依赖数据库的用例打包就会失败。前端打包是npm run build生成dist目录里面是静态文件Nginx指向这个目录即可。7.2 数据库初始化脚本与测试数据sql/garbage.sql文件里不仅要建表还要插入基础数据。没有基础数据的话前端页面就是空荡荡的演示效果非常差。我插了大概100条测试数据6种垃圾分类图标、50种常见垃圾条目电池、纸箱、剩饭、玻璃瓶、过期药品等、10个积分商品、3条公告。有了这100条数据前端页面一打开就有内容可看ECharts有数据能画图管理员后台有投递记录能翻页演示流程才完整。我见过有人答辩时打开系统一片空白场面极其尴尬这完全是可以靠初始化数据避免的。7.3 项目文档写作从“代码跑通”到“答辩高分”源码配套的文档我建议按这个结构写项目概述背景、意义、核心功能、技术架构需求分析用例图、功能模块图、角色权限说明系统设计数据库ER图、表结构说明、接口设计核心功能实现重点展示图像识别、积分逻辑、JWT鉴权三个模块的代码和运行截图系统测试功能测试用例和执行结果其中第4部分是评分的分水岭。不要只贴RestController类要贴有业务深度的代码比如图像识别的兜底策略、积分的分布式锁再配上截图和数据实测结果。这些内容才体现工作量和技术水平。写文档的速度也有讲究每个章节不要等代码全部写完才开始一边写代码一边截运行截图最后统稿不然到截止日期前会被文档压垮。8. 写在最后几个让我少走弯路的体会整个系统从搭骨架到跑通到写成文档我前后花了不到两周。回头复盘最值得说的几点第一先跑通界面再补业务逻辑。我第一版先做登录、用户管理、垃圾分类列表这些“看得见”的功能页面能跑起来再去填图像识别、积分逻辑这些“看不见”的核心心理压力小很多进度也更容易掌控。第二图片识别一定是调用云服务而不是自己去训练模型。有人纠结“图像识别要自己搞CNN”这是毕设的大坑。你花两周训练出的模型精度可能还不如云服务调用一次。用云服务接口然后把精力放在业务闭环上这是效率最高的路径。第三答辩演示前一定要准备一套“备胎数据”。把WiFi断掉、把第三方接口可能超时的情况想在前头前端用本地mock数据顶上演示就不会因为外部依赖翻车。这个项目做完SpringBoot、MyBatis Plus、Redis、JWT、Vue这几个关键词都实际用了一遍写简历的时候能说清楚“我做了一个xx系统包含xx模块用了xx技术解决了xx问题”而不仅仅是“懂SpringBoot”。这就够了真的够了。如果你正在为选题发愁照着这个思路去拆解你的项目会少掉很多头发。
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →