很多人看到“SpringBoot3 Vue3 失物招领系统”这个标题第一反应可能是这不就是一个平平无奇的 CRUD 毕设题吗确实失物招领这种信息管理类系统在功能上不算新颖但它恰恰是毕业设计里最典型、也最值得认真打磨的一类项目。更关键的是这一次的技术栈不再是老掉牙的 Spring Boot 2.x JSP而是 SpringBoot3 Vue3 这套当前企业级开发的主流组合。选择这个题目意味着你不仅能完成毕设还能在简历上写下一条真实的前后端分离项目经验。这篇内容不打算跟你聊空泛的概念而是以“零基础能复现”为目标把整个系统拆开揉碎从需求分析、数据库设计到后端接口、前端页面再到部署答辩时可能踩的坑全部过一遍。你拿到手的不只是一份源码和数据库脚本而是一套“为什么这样设计”的方法。无论你是正在准备毕设的学生还是想练手的前端开发只要你愿意花两三天跟着实操这个项目就能真正跑起来并且跑得明明白白。1. 项目整体设计与技术选型拆解1.1 失物招领系统的核心需求到底是什么很多第一次做毕设的人拿到题目就急着建表写代码结果写到一半发现功能对不上需求文档推倒重来。失物招领这个题目核心需求其实是两个信息闭环一是“丢东西的人”和“捡到东西的人”如何高效匹配二是“认领”这个动作如何被可信地完成。拆开来看系统至少要覆盖以下场景访客或注册用户发布一条失物信息描述丢失物品的特征、时间、地点最好能上传图片。同样有人捡到物品发布一条招领信息。系统需要根据物品的关键词、分类、遗失或拾获地点为双方做模糊匹配推荐。捡到方看到认领请求后需要核实物品细节比如颜色、品牌、特殊记号核实通过后才允许认领。管理员要能管理所有信息比如下架虚假信息、删除违规内容、查看统计数据。1.2 为什么选 SpringBoot3 Vue3 这套组合这是很多同学在选题答辩时一定会被问到的问题。SpringBoot3 是 2022 年底发布的重大版本它把 JDK 基线提升到了 17意味着你可以用上真正的现代 Java 特性比如record、sealed class、switch表达式。更重要的是SpringBoot3 全面拥抱了 Jakarta EE 命名空间这标志着 Spring 体系正式告别了 javax 时代。你在简历里写“熟悉 SpringBoot3”面试官至少能确认你接触的不是那些 2018 年的老教程。Vue3 这边同理。组合式 API 带来的逻辑复用能力配合script setup语法开发体验比 Vue2 的选项式 API 舒服太多了。再加上 Vite 的极速冷启动前端开发效率完全不是 vue-cli 能比的。这两个框架组合起来意味着你做的不是那种“为了毕设而毕设”的演示项目而是一套接近真实企业研发流程的技术方案。顺便说一句以后你想在这个项目基础上加功能比如加 Redis 缓存、接消息队列这套架构也不会成为瓶颈。1.3 前后端分离架构下的模块划分失物招领系统虽然不大但前后端分离的目录结构还是要规范。后端我采用的是经典的分层结构controller接收请求参数校验返回统一响应体。service业务逻辑比如发布失物、处理认领、匹配推荐。mapper数据访问层基于 MyBatis-Plus 实现。entity数据库实体映射。dto前端交互的数据传输对象避免直接暴露实体。config跨域、拦截器、异常处理等配置类。前端基于 Vue3 Vite目录结构同样清晰views页面级组件比如首页、发布页、详情页、管理后台。components公共组件比如图片上传、信息卡片、分页栏。api按模块拆分的 axios 请求封装。router前端路由配置包括动态路由守卫。storePinia 状态管理主要存用户登录态。2. 数据库设计这是整个项目的地基2.1 核心表结构与字段设计失物招领系统的数据库设计本质上是在描述现实世界中的业务流程。我最终设计为六张核心表它们之间的依赖关系非常清晰user用户表昵称、手机号、密码BCrypt 加密、角色普通用户/管理员、创建时间。lost_item失物表标题、描述、物品分类、丢失地点、丢失时间、图片、状态待匹配/认领中/已找到、发布人 ID。found_item招领表字段与失物表基本对称但状态里包含“未被认领/待核实/已归还”。claim_record认领记录表认领人 ID、关联的失物或招领 ID、认领说明、核实状态待核实/已通过/已拒绝、认领时间。category物品分类表预设分类比如证件、电子产品、生活用品、衣物等。采用独立表而不是硬编码是为了让管理员可以在后台灵活增删分类。admin_action_log操作日志表记录用户和管理员的关键操作比如下架、删除、认领通过等。这块很多同学会偷懒不做但答辩时操作日志往往能证明你的系统考虑到了安全审计。2.2 为什么要这样设计表结构一个常见的设计困惑是失物和招领信息这么像为什么不合成一张表这个我纠结过但最终拆开了。原因有两个一是业务含义不同失物是“我在找东西”招领是“我这里有一件东西等人认领”它们的发帖流程、状态流转、后续操作都不一样二是分开后模糊匹配和状态管理都更简单SQL 写起来也更直观。你想想如果合成一张表每个字段都得加一个“类型”标志去区分查询逻辑到处都要写if徒增复杂度。关于claim_record这张表我想多说两句。它是保证系统可信度的核心。没有这张表认领就只是即时通讯里的聊天记录没有任何系统层面的流程可控性。有了它你可以管理完整的认领流程用户提交认领申请捡到物品的人核实细节通过后标记归还。每一步都有数据留痕这也是答辩时体现业务完整性的亮点。2.3 数据库脚本中的关键细节这里必须强调一个新手高频踩坑点字符集和排序规则。如果建表时采用默认的latin1或错误的utf8注意 MySQL 的utf8其实不是真正的 UTF-8中文数据很容易乱码或在模糊匹配时出现诡异行为。我建库时统一使用CREATE DATABASE lost_found CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;另外每个表都加上create_time、update_time这两个通用字段配合 MyBatis-Plus 的字段自动填充这样你就不需要每处 insert、update 都手动维护时间省心又规范。索引方面lost_item表的user_id、status、location字段建议建立索引因为按地点和状态过滤是最高频的查询场景。索引不是越多越好但这两个字段的组合索引对匹配查询的性能提升非常明显。3. 后端核心模块从零实现一个完整的失物招领流程3.1 环境准备与版本选型千万别在这些细节上翻车SpringBoot3 是个“挑剔”的版本它对运行环境有硬性要求。我再强调一遍JDK 必须是 17 及以上版本。有的同学电脑上装的是 JDK8直接跑 SpringBoot3 项目启动就会报错查半天发现是版本问题心态直接炸裂。推荐的环境组合如下组件版本说明JDK17最好装 JDK17 LTS 版本Maven3.6建议用 IDEA 内置的 Maven省心MySQL5.7 或 8.08.0 对 utf8mb4 支持更好Node.js16.20Vite5 要求 Node 版本不能太低npm/yarn/pnpm任一推荐 pnpm安装依赖快且省磁盘另外特别提醒SpringBoot3 的包名从javax.*改成了jakarta.*。如果你在网上搜到很多 2021 年的老代码直接复制里面还是javax.servlet这种 import那你的项目会编译不过。解决办法很粗暴看到javax改成jakarta看到Spring Boot 2.x版本的依赖坐标改用Spring Boot 3.x对应的版本号即可。3.2 统一响应体与全局异常处理这是专业代码的底线很多新手写的后端接口返回结构五花八门有的直接返回Map有的返回null就算成功。前端调接口的时候一会儿要处理data.data一会儿又要处理result.list体验极差。我在这个项目里写了统一的响应体Data public class ResultT { private Integer code; 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; } }统一响应体看起来简单但它能保证前后端联调时有一个固定的“契约”。配合全局异常处理器你可以轻松捕获业务异常、参数校验异常、数据库异常并转换成统一的错误结构返回给前端。答辩时如果老师问“你的系统是怎么处理异常的”你就能回答“通过RestControllerAdvice做了全局异常拦截业务异常和系统异常分开处理避免向上层抛出模糊堆栈。”3.3 核心接口实现信息发布、模糊匹配、认领流程发布失物信息这个接口看似简单但要注意几点。首先是图片处理我建议前端先把图片传到服务端的独立存储目录后端只保存图片的访问 URL而不是把 base64 图片塞进数据库。很多新手图省事把图片直接以 base64 文本存进数据库字段能跑但当单条记录图片过大时数据库膨胀得极快查询性能也会明显下降。接口设计如下PostMapping(/lost) public ResultLostItemVO publishLost(RequestBody Validated LostItemDTO dto) { // 1. 从 JWT 中解析当前用户 Long userId JwtUtil.getCurrentUserId(); // 2. DTO 转实体设置初始状态为 MATCHING LostItem item LostItemMapper.INSTANCE.toEntity(dto); item.setUserId(userId); item.setStatus(0); item.setCreateTime(LocalDateTime.now()); // 3. 插入数据库 lostItemService.save(item); return Result.success(LostConvert.toVO(item)); }模糊匹配推荐是系统的亮点功能。它的本质是“用文本相似度找信息”当用户发布一条失物信息时后端从内容中提取关键词然后去招领表里检索相关性最高的记录。这里用最简单的实现方式就是 MySQL 的LIKE匹配配合字段优先级排序。比如标题匹配的产品权重比描述匹配的记录高 3 倍。SELECT * FROM found_item WHERE status 0 AND ( title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %) ) ORDER BY (CASE WHEN title LIKE CONCAT(%, #{keyword}, %) THEN 1 ELSE 0 END) DESC当然这只是一种入门级实现。如果你想在答辩时展示更高的技术水平可以引入倒排索引用 Redis 做搜索缓存或者干脆接入 ES。不过毕设阶段SQL 级别的模糊匹配已经能完整表达业务逻辑了。认领流程是状态机思维的最佳实践。认领申请从“待核实”到“已通过/已拒绝”每一步都必须校验前置状态。我采用的是最稳妥的方式所有状态流转都通过 Service 层的方法驱动而不是让前端直接传目标状态值。比如“通过认领”这个方法Transactional public ResultString approveClaim(Long claimId) { ClaimRecord claim claimMapper.selectById(claimId); if (claim null) { return Result.error(认领记录不存在); } if (claim.getStatus() ! 0) { return Result.error(当前状态不允许操作); } claim.setStatus(1); // 已通过 claimMapper.updateById(claim); // 同时更新对应招领信息的状态为“已归还” foundItemMapper.updateStatus(claim.getFoundItemId(), 2); return Result.success(认领通过); }3.4 JWT 登录鉴权与拦截器的正确打开方式用户登录后后端签发一个 JWT 令牌前端存在本地localStorage 或 Pinia 维护每次请求在 axios 拦截器里携带Authorization: Bearer token。后端通过 Spring Boot 拦截器统一校验。这个方案是无状态的扩展性极好特别适合前后端分离架构。拦截器的代码本身就是毕业设计的一个考点Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口等白名单 String uri request.getRequestURI(); if (uri.startsWith(/api/user/login) || uri.startsWith(/api/user/register)) { return true; } // 校验令牌 String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录或登录已过期); } String jwt token.substring(7); // 解析失败则抛异常 Claims claims JwtUtil.parse(jwt); request.setAttribute(userId, claims.get(userId)); return true; } }我这套实现里最关键的一个细节是把 userId 塞进 request 的 attribute 里这样在 Controller 层不需要再次解析 JWT直接用HttpServletRequest.getAttribute(userId)就能拿到当前用户。这是面试时经常被追问的“无状态登录如何识别用户”的问题标准解法。3.5 MyBatis-Plus 在项目里的实际用法MyBatis-Plus 算是国内 Java 后端开发的“事实标准”了。它把单表 CRUD 简化到了极致你不用手写简单的增删改查 SQL继承BaseMapperT后selectById、selectList、updateById、deleteById这些方法直接用就行。但要注意MyBatis-Plus 的“条件构造器”LambdaQueryWrapper特别强大比如实现“按地点模糊搜索不过期失物信息”LambdaQueryWrapperLostItem wrapper new LambdaQueryWrapper(); wrapper.eq(LostItem::getStatus, 0) .like(LostItem::getLocation, location) .orderByDesc(LostItem::getCreateTime); ListLostItem items lostItemMapper.selectList(wrapper);这里有个坑字段名用 Lambda 表达式的方法引用相比手写字符串的QueryWrapper能绕过数据库字段改名引发的运行时错误。原因在于它采用属性名引用编译期就会校验。强烈建议所有条件查询都使用 LambdaQueryWrapper别图省事写字符串。3.6 日志配置与 logback-spring.xml 的正确写法这个项目里我特意配置了 logback-spring.xml。很多同学在开发时习惯直接System.out.println()打印信息这在小项目里凑合能用但到了部署环境或遇到线上问题排查时没有统一日志就是一个灾难。SpringBoot3 默认使用的是 Logback 日志框架我们要做的就是把它的输出规范化。一个实用的日志配置重点如下appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern charsetUTF-8/charset /encoder /appender appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/lost-found.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/lost-found.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern /encoder /appender日志按天滚动、最多保留 30 天历史文件这基本是生产级别的要求。通过这个配置文件你还能区分不同环境下日志文件的输出地址这也方便线上部署后的日志收集。4. 前端核心页面实现Vue3 Vite Pinia Element Plus4.1 Vue3 项目的初始化与关键依赖前端部分的初始化非常讲究。不要再用vue create那是 Vue CLI 的老方式现在主流是新项目直接用 Vite 脚手架pnpm create vite lost-found-web --template vue cd lost-found-web pnpm install pnpm add axios vue-router pinia element-plus特别说明一下Element Plus 是一个基于 Vue3 的桌面端组件库官网兼容 Vue3 的生态几乎都向它看齐。它的全量引入很简单对毕设项目也算够用毕竟不需要额外的按需加载的构建优化但我们更推荐按需自动导入的方式这里用到官方配套插件unplugin-auto-import和unplugin-vue-components。如果按需导入配置好了你会发现开发体验提升一个档次且打包体积更小。4.2 组合式 API 与script setup语法减少 30% 的代码量Vue2 时代的写法是data()、methods、computed、watch分开维护一旦页面逻辑多了代码会出现“一跳就是半屏”的割裂感。Vue3 的组合式 API 则允许你按功能去组织代码而不是按类型。比如失物详情页我会把“获取详情”、“认领操作”、“图片预览”三个功能各自拆成一块逻辑代码可读性直接提升一个台阶。举个最简单的例子发布表单页的逻辑可以这样组织script setup import { ref, reactive } from vue import { publishLostItem } from /api/lost import { ElMessage } from element-plus const form reactive({ title: , category: , location: , time: , description: , images: [] }) const loading ref(false) async function handleSubmit() { if (!form.title || !form.location) { ElMessage.warning(请填写标题和丢失地点) return } loading.value true try { const res await publishLostItem(form) ElMessage.success(发布成功) } finally { loading.value false } } /script注意script setup的意思是该组件在 setup 阶段就会执行不需要额外导出setup()函数。所有顶层变量、函数会自动暴露给模板不需要return。相比 Vue2这个语法糖真的帮你省掉了很多模板代码。4.3 axios 封装与请求拦截器前后端对接的核心前后端分离项目里axios 的二次封装是必不可少的。我在api/request.js中做了如下处理import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { // 未授权跳转登录页 router.push(/login) return Promise.reject(new Error(未登录)) } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )这个封装解决了三个核心痛点自动携带 JWT 令牌、统一处理 HTTP 错误和业务错误码、统一展示错误提示。这些体验看起来不起眼但答辩时如果老师让演示一个“接口报错”的场景你的系统能弹出一个友好提示而不是浏览器默认报错这就是专业的细节。4.4 首页失物卡片流与分类筛选的快速实现首页是整个系统的门面我的实现思路是顶部是一个轮播图展示失物照片下面是一个分类 Tab 栏点击不同分类会过滤展示对应失物卡片。卡片用 Element Plus 的el-card组件搭建每个卡片展示缩略图、标题下面附带地点和时间。列表页的数据加载我采用“进入页面自动获取 切换分类重新请求”的模式。需要说明的是这里不建议一次性把全部数据捞回来在前端做过滤那时候数据量一大页面会卡。正确做法是每次切换分类都向后端请求对应分类的数据让分页和过滤都交给服务端完成async function loadList() { loading.value true try { const res await getLostList({ category: currentCategory.value, page: pageNum.value, size: pageSize.value }) list.value res.data.records total.value res.data.total } finally { loading.value false } }4.5 Pinia 状态管理为登录态和用户信息选型状态管理我选了 Pinia 而不是 Vuex理由很简单Pinia 的 API 更简洁没有 mutation 这一层且天然支持 TypeScript。更重要的是Vue 官方已经把 Pinia 列为了标准状态库未来 Vuex 基本会被 Pinia 完全取代。你在这里使用 Pinia 是顺势而为也说明你跟着技术趋势走。登录态存储的一个关键细节是token 不要只存在内存里刷新页面就丢失了。我的做法是登录成功后把 token 同时存入 localStorage 和 Pinia 仓库里每次页面刷新后在路由守卫里重新从 localStorage 恢复用户状态import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token), userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }), actions: { setLoginInfo(token, userInfo) { this.token token this.userInfo userInfo localStorage.setItem(token, token) localStorage.setItem(userInfo, JSON.stringify(userInfo)) }, logout() { this.token null this.userInfo null localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) } } })5. 认领流程的前后端联动把业务闭环跑通5.1 认领申请弹窗的设计与实现用户在失物详情页看到一个“我要认领”的按钮点击后弹出认领申请表单。这个表单通常要求填写认领原因、联系方式等。这个弹窗的提交动作要跟后端claim_record表的插入逻辑一一对应。核心代码如下async function submitClaim() { if (!claimForm.reason) { ElMessage.warning(请填写认领说明) return } const res await submitClaimAPI({ lostItemId: itemId.value, reason: claimForm.reason, phone: claimForm.phone }) ElMessage.success(认领申请已提交请等待对方核实) dialogVisible.value false }这里有个非常重要的体验细节提交认领申请后要在弹窗上明确提示“请等待对方核实”。因为业务逻辑是捡到东西的人需要先确认物品细节然后才能在“收到的认领申请”列表中操作。如果没有这个提示用户可能反复提交产生大量无效数据。5.2 “我的发布”和“收到的认领申请”两块工作台系统里要区分三类认领状态流转分别是“待核实”“已通过”“已拒绝”。用户登录后可以在“我的发布”中查看自己发布了哪些失物或招领信息在“收到的认领申请”中查看别人对某条招领信息的认领请求。这块的前端路由设计可以这样规划/my/lost我发布的失物列表每条可以查看详情、删除。/my/found我发布的招领列表每条可以查看详情、处理认领申请。/my/claims我发起的认领申请记录展示每条申请的实时状态。对应后端的接口命名也就顺理成章GET /api/my/lost、GET /api/my/found、GET /api/my/claims。这套 RESTful 风格的 API 设计简单直观答辩时也能讲清楚“我是怎么组织接口资源的”。5.3 核验细节用状态流转控制认领成功当“捡到物品的用户”在“收到的认领申请”里点击“通过”按钮时系统要做的不只是把claim_record状态改成已通过同时要把对应的found_item状态改成已归还。更进一步我还在通过操作时给认领人发送一条“物品确认归还”的通知记录保证整个流程闭环。这里我用到了事务注解Transactional public void approveClaim(Long claimId) { ClaimRecord claim claimMapper.selectById(claimId); if (claim null || claim.getStatus() ! 0) { throw new BizException(当前状态不允许操作); } claim.setStatus(1); claimMapper.updateById(claim); FoundItemDTO found foundMapper.selectById(claim.getFoundItemId()); found.setStatus(2); foundMapper.updateById(found); // 写入通知记录 notificationService.send(claim.getClaimUserId(), 你的认领申请已通过请尽快联系对方取回物品); }事务在这里至关重要因为“更新认领状态”和“更新招领状态”必须同时成功或同时失败。如果不加事务一旦第二个 SQL 执行失败前端看到的认领状态已经更新但招领信息还是“未被认领”这个数据不一致就非常尴尬。这也是“数据库事务”在毕设答辩时最容易被追问的知识点你把这个场景讲明白老师就知道你真的理解事务的作用。6. 常见问题排查与避坑指南这些都是实战总结6.1 SpringBoot3 版本兼容问题javax 到 jakarta 的迁移如果你在开发中遇到ClassNotFoundException: javax.servlet.*或者The type javax.servlet cannot be resolved根本原因就是 SpringBoot3 命名空间变更。解决办法是把所有import javax.servlet的代码改为import jakarta.servlet。如果你在 Maven 项目里还引用了需要javax的老版本依赖比如pagehelper的旧版、某些swagger依赖需要找到兼容 SpringBoot3 的新版本坐标换成新依赖。这里给一份我在项目里验证过的关键依赖版本依赖版本说明spring-boot-starter-parent3.2.x作为 parent 引入mybatis-plus-boot-starter3.5.5支持 SpringBoot3 需要 3.5.3 以上knife4j-openapi3-jakarta4.4.0接口文档已适配 jakartajjwt0.11.5JWT 工具库6.2 Vue3 安装依赖时常见的权限问题在 macOS/Linux 环境中用 npm 全局安装依赖时经常会遇到EACCES: permission denied的权限报错。最好的解决办法是不要用 sudo 去强行提权而是使用 nvm 管理 Node.js 版本。当 Node 安装在用户目录下全局依赖也就装到用户目录完全不需要 root 权限。还有一个非常经典的坑Vite 启动后在浏览器访问显示“地址已被占用”那是因为默认端口 5173 被占用。你可以在项目根目录的vite.config.js中自定义端口server: { host: 0.0.0.0, port: 3000, open: true, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这里同时展示了 Vite 开发服务器的反向代理配置它能把/api开头的请求转发到后端的 8080 端口。这个配置让前端在本地开发时规避了跨域问题并且后端代码不需要额外配置 CORS当然生产环境我们还是会配 CORS双保险。6.3 MySQL 连接数据库时的时区与权限问题连接 MySQL 报Public Key Retrieval is not allowed是高频问题。原因MySQL 8.0 默认使用 caching_sha2_password 的身份认证插件如果你没有配置 SSL 或公钥检索某些驱动版本就连不上。两个解决方法任选一是 JDBC URL 后面加allowPublicKeyRetrievaltrue二是在创建用户时指定mysql_native_password认证插件。另外就是时区问题推荐在 JDBC URL 里明确设置时区避免服务器时间和本地时间差 8 小时spring.datasource.urljdbc:mysql://localhost:3306/lost_found?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai6.4 跨域问题不是配置一次就万事大吉前后端分离项目里跨域配置是毕设答辩中最容易出问题的环节之一。有时候你在后端配置了 CORS前端还是报跨域原因就是前端通过 Vite 代理访问就没问题但直接请求后端地址比如把 API 配置成http://localhost:8080时触发了跨域。我的建议是双管齐下前端开发环境一律走 Vite 代理后端不配置 CORS 都行生产环境部署时后端配置 CORS 允许所有来源或者限定域名。后端 CORS 配置写成这样Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }6.5 数据库连接池配置HikariCP 的参数不调等于白配SpringBoot3 默认集成的是 HikariCP 连接池这也是业界性能最好的连接池之一几乎没有之一。如果不主动配置它的默认值也能跑但为了让系统更稳定我建议做以下调整spring.datasource.hikari.minimum-idle5 spring.datasource.hikari.maximum-pool-size15 spring.datasource.hikari.idle-timeout600000 spring.datasource.hikari.max-lifetime1800000 spring.datasource.hikari.connection-timeout30000maximum-pool-size不是越大越好它跟数据库实例的连接上限有关。对于一个毕设项目15 个连接绰绰有余。但把连接池参数展示在配置文件中能证明你关注系统资源的使用上限这在答辩中是非常加分的“工程素养”展示。7. 接口文档也是交付物的一部分用 Apifox 或 Knife4j 生成一个不留文档的后端接口对前端开发和团队协作都是灾难。我现在习惯在项目里直接集成 Knife4j它是增强版 Swagger它基于 OpenAPI3 规范支持 SpringBoot3 和 Jakarta。依赖配好以后访问http://localhost:8080/doc.html就能看到可视化接口文档页面。配合注解写文档的效率很高Operation(summary 发布失物信息) PostMapping(/lost) public ResultLostItemVO publishLost(RequestBody Validated LostItemDTO dto) { ... }接口文档不仅仅是为了答辩它有一个非常实际的作用前端可以对照着文档 mock 数据提前开发页面不用等后端写好接口再联调。这个流程对齐后整个项目的开发效率会提升非常多。如果你不想后端集成 Swagger也可以选择 Apifox 这类工具在软件里手写接口定义。但无论如何接口文档必须是交付物的一部分而不是可选项。8. 数据库脚本与测试数据准备源码库里的隐藏加分项很多同学在“源码 数据库”里只放一个建表语句表里连一条数据都没有。你想想老师拿到项目想演示一遍结果登录后首页空空如也还要自己去发布信息看效果体验能好吗我在数据库脚本里预置了两类数据一是管理员账号和测试用户账号二是丰富的失物/招领演示数据。测试数据的诀窍是要像真实用户发布的那样有细节。比如一条失物数据“黑色长款钱包内有身份证、银行卡若干于图书馆三楼丢失。”另一条招领数据“捡到一个学生卡卡面印有男生照片学号疑似 2021 级在二食堂门口捡到。”这两条数据除了直观展示列表还能用来演示模糊匹配的效果——当用户搜索“学生卡”或“钱包”时这一对数据就能被检索出来。数据库脚本文件的组织也建议分门别类lost_found_schema.sql建库建表语句。lost_found_data.sql预置测试数据。lost_found_user.sql用户初始化数据密码要用 BCrypt 加密后的字符串存储不能明文。在文档里写清楚“打开数据库脚本按以下顺序执行”对拉低新手操作门槛非常有帮助。9. 部署与答辩准备别让项目死在最后一步9.1 本地部署的完整流程备忘整个项目的部署流程并不复杂但顺序错了会连环报错。我的推荐操作顺序是创建数据库并执行lost_found_schema.sql和lost_found_data.sql。修改后端配置文件里的数据库账号密码。执行mvn spring-boot:run确认后端启动成功控制台打印端口号。在lost-found-web目录执行pnpm install安装前端依赖。执行pnpm dev启动前端开发服务器。浏览器访问http://localhost:3000用测试账号登录。如果做生产环境部署前端需要先pnpm build生成dist目录然后使用 Nginx 托管静态文件并将/api路径反向代理到后端服务。这也是真实项目的部署方式。9.2 答辩时老师喜欢问的问题提前准备好答案根据我带过的历届学生经验答辩老师对这个项目的提问通常会集中在以下方向为什么使用前后端分离架构答前后端解耦后端可以统一提供 API前端可独立迭代也方便团队分工这是现代 Web 开发的主流模式。JWT 和传统 Session 有什么区别答Session 是服务端存储登录状态扩展时依赖 Session 复制或粘性会话JWT 是无状态的Server 不保存用户登录状态适合分布式部署扩展更轻松。MyBatis-Plus 和 MyBatis 的关系是什么答MyBatis-Plus 是 MyBatis 的增强工具保留了 MyBatis 的灵活 SQL 能力同时提供通用的单表 CRUD 方法提高开发效率。数据库表为什么不设计成一张大表答表拆分是为了业务边界清晰、降低冗余、提升查询效率。例如失物表和招领表虽然字段相似但状态流转语义不同分开设计避免了逻辑混淆。这些问题都不是背答案而是你对项目理解程度的真实体现。认真做完这个项目这些问题都会变成你的直觉。10. 我在实操中的体会与后续扩展建议整套做下来我的体会是失物招领系统虽然名字朴素但它覆盖了一个业务系统的完整生命周期。从需求梳理到数据库设计从后端接口到前端交互从状态流控到权限处理每一步都是一个开发者真正需要掌握的技能。更重要的是这个项目有清晰的实际应用场景——你甚至可以把它部署到一台小服务器上在校园里试着用一用那种“自己写的系统被真实使用”的感觉比任何课程设计的分数都有说服力。如果你顺利把基础版本跑通后续可以扩展的方向其实非常多。最推荐的是给系统加上图片上传到对象存储比如阿里云 OSS、腾讯云 COS的功能现在本地存储方案在真实项目中已经越来越少见。其次是引入 Redis 做热门失物信息的缓存降低数据库压力。再进阶一点可以把“推荐的失物/招领”做成一个简单的基于标签的推荐算法让用户看到更贴合自己历史的物品信息。这些扩展点每一个都可以作为“主要工作亮点”写进毕设报告里也能成为你面试时的谈资。起点虽然只是一套 CRUD但它搭建起来的工程思维和知识体系会一直陪伴你往后走得更远。