ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot3+Vue3失物招领系统开发实战:从零搭建前后端分离项目

2026/10/5 2:43:30 拓冰建站 浏览量
SpringBoot3+Vue3失物招领系统开发实战:从零搭建前后端分离项目 直接开门见山个人开发者、在校学生做SpringBoot3 Vue3失物招领系统是当下一个非常实用的技术练手组合。一方面失物招领业务逻辑不复杂非常适合用来做毕业设计、课设作业或实训项目另一方面它涵盖了用户认证、物品发布、图片上传、状态流转、后台管理这些常见模块能让你把前后端分离、RESTful API、组件通信这些核心知识点在同一个项目里完整走一遍。这篇就围绕这个系统从零开始讲清楚整个技术思路和实操步骤不管你还在补SpringBoot基础还是对Vue3只是停留在“能用模板”的阶段按着下面的路径走下来都能落地跑起来。顺便说一下这类项目的核心痛点从来不是“代码写不出来”而是“模块之间怎么串起来”。所以我会重点讲清楚数据模型设计、接口约定、前后端联调和权限控制这几个真正卡人的环节。不管你最终是想改成社团招领平台还是想给学校做一个小程序配套的后台这套思路都能直接迁移。1. 项目核心价值为什么“失物招领”最适合练手1.1 业务规模适中覆盖全栈核心技术点失物招领系统不像电商那样需要复杂的库存、订单状态机也不像社交平台那样需要实时推送。它的核心业务就几条线用户登记拾到物品、用户浏览搜索失物、失主提交认领申请、管理员审核认领并核销记录。听起来简单但这几条线一旦展开你会自然接触到下面的技术点用户体系注册、登录、Token鉴权、角色区分普通用户/管理员。数据管理物品分类、状态流转待认领、审核中、已认领、已过期、图片存储。前后端交互RESTful API设计、JSON序列化、文件上传、跨域处理。项目组织前端工程化Vite Vue3 Pinia、后端分层Controller / Service / Mapper、环境配置。说实话能把这些点完整串起来并且讲清楚“为什么这样设计”已经超过不少培训班的项目水平。对毕业设计和面试项目来说这个体量刚好——不会因为业务复杂导致代码失控也不会因为没有业务深度而被怀疑是纯仿照。1.2 适合人群与实际使用场景这个项目最典型的几类使用者毕业设计/课程设计需要一个业务完整、能演示、能写进论文的系统。失物招领既有业务闭环又能体现出你掌握了前后端分离开发模式答辩时有清晰的“功能亮点”可讲。实训项目在团队开发中模块边界清晰分工明确。前端三个人、后端两个人就能做Git协作也相对容易。项目学习者想快速体验一套前后端分离应用怎么从空目录变成可运行系统这个项目比“仿某论坛”代码量更小但技术栈覆盖很全。别担心零基础后面我会把每一步都拆开。你只需要会基础的Java语法和JavaScript语法其他都是跟着做过程中学。2. 技术选型与整体架构设计2.1 为什么是SpringBoot3而不是Spring Boot 2SpringBoot3相比2.x最大的变化是强制基于Jakarta EE 9/10规范并且默认使用Java 17及以上版本。对学习项目来说SpringBoot3最大的价值在于“新”——你用的写法、依赖、文档大多对应当前主流教程不至于学了之后又被迫补差异。另外SpringBoot3在配置上更加精简很多以往需要手动处理的循环依赖、冗余配置在启动时就直接报错能逼着你从一开始就写出规范的Bean组织方式。当然如果你电脑里只有JDK8那选SpringBoot2.7会更容易启动。但既然标题就是SpringBoot3我建议尽量升级到JDK17或21顺手把Java的新特性也体验一下。下载时注意区分版本SpringBoot3要求Java 17是硬门槛。2.2 前端为什么选Vue3 ViteVue3的组合式APIComposition API最大的优势是把同一业务相关的状态和逻辑放在一起而不是像Vue2的选项式API那样非要拆成data、methods、watch。项目里如果用setup语法糖写起来很顺而且代码量少很多。Vite作为构建工具冷启动速度比Webpack明显快几乎不用等这对平时在本机反复调试前端体验非常关键。配套的UI组件库建议Element Plus。它和Vue3原生契合表格、表单、弹窗、上传组件比较全做后台管理界面很省事。注意Element Plus官方对Vue3版本有要求建议用npm install element-plus下载当前最新稳定版避免和Vite版本冲突。状态管理我选Pinia比Vuex轻API设计合理而且TypeScript支持更好。2.3 数据模型设计用三张表把业务逻辑钉死后端设计最忌讳上来就写代码。先把表结构想清楚后面所有接口都会顺畅。我建议至少这样三张表但可以根据需求扩展。用户表sys_user字段名类型说明idbigint主键usernamevarchar(50)登录名passwordvarchar(255)加密后的密码nicknamevarchar(50)昵称显示用avatarvarchar(255)头像地址rolevarchar(20)普通用户user / 管理员admincreate_timedatetime创建时间密码一定不能明文存我用SpringSecurity自带的BCryptPasswordEncoder加密。这样就算数据库泄露密码也不会直接暴露。物品表lost_item字段名类型说明idbigint主键titlevarchar(100)物品标题descriptiontext详细描述categoryvarchar(50)分类证件/手机/钱包/其他typevarchar(20)lost 寻物 / found 失物招领imagevarchar(255)图片URLlocationvarchar(100)拾到/丢失地点contactvarchar(100)联系方式statusvarchar(20)pending / claimed / completeduser_idbigint发布人IDcreate_timedatetime发布时间认领表claim_record字段名类型说明idbigint主键item_idbigint关联的物品IDuser_idbigint认领人IDreasontext认领说明特征描述、凭证statusvarchar(20)待审核 / 已通过 / 已拒绝create_timedatetime申请时间这套模型看起来简单但你做的时候会发现一个关键点状态流转是核心。物品状态从“待认领”到“已认领”同时要保证认领记录状态同步。所以接口设计时我会采用“先提交申请再管理员审核通过同时把物品状态改为已认领”这样逻辑链完整不会出现物品被标记已领取但没人认领的脏数据。3. 后端核心模块实现接口设计 登录鉴权3.1 登录鉴权SpringSecurity JWTSpringBoot3的SpringSecurity和旧版比写法上最大区别是WebSecurityConfigurerAdapter没了现在要定义SecurityFilterChainBean。对于这种前后端分离项目我习惯用JWT做无状态认证。用户登录成功后后端返回一个Token前端把Token存到本地后续每个请求在Header里带上Authorization: Bearer token。核心代码差不多是这样Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/auth/register, /api/items/**, /files/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .sessionManagement(manager - manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }注意几点我允许匿名用户查看物品列表和详情所以/api/items/**放行发布、认领这些操作都要登录。/api/admin/**只允许管理员访问SpringSecurity通过角色前缀“ROLE_ADMIN”来判断我这里数据库存的是admin需要加ROLE_前缀或自定义权限映射。JWT过滤器要写在UsernamePasswordAuthenticationFilter之前否则Token根本不会被解析。JWT生成和解析我用的io.jsonwebtoken:jjwt-api然后配合jjwt-impl和jjwt-jackson。版本建议用0.11.5以上我遇到过0.9.1在JDK17下抛NoClassDefFoundError就是因为用了老的库。3.2 文件上传本地存储还是云OSS图片传到哪这是个非常实际的问题。很多教程直接让你把文件写到本地然后通过静态资源映射访问但对部署到服务器上的项目重启或重新部署后图片容易丢。我建议分两步本地开发阶段直接存到项目根目录下的upload/文件夹SpringBoot里用以下配置映射为静态资源spring: web: resources: static-locations: file:./upload/然后接口里这样接收文件PostMapping(/api/upload) public ResultString upload(MultipartFile file) throws IOException { String path System.getProperty(user.dir) /upload/; String originalFilename file.getOriginalFilename(); String fileName UUID.randomUUID() _ originalFilename; file.transferTo(new File(path fileName)); return Result.success(/files/ fileName); }注意文件名一定要用UUID加前缀防止重名覆盖。有人直接用originalFilename结果两个不同用户上传了同名的“1.jpg”后一个把前一个覆盖掉这是实战中非常低级的错误。部署阶段建议从本地文件改成云OSS或七牛云扩展性强。这个替换也很简单只需要把upload方法里的存储逻辑换成对象存储SDK即可接口签名不变前端完全无感。毕业设计答辩时可以把这个作为“解决图片持久化”的亮点提一下。3.3 物品发布与状态流转接口物品发布接口基本就是接收前端表单组装成LostItem后存库。但有一点特别需要注意发布人的ID不能从请求参数里拿应该从JWT解析后的当前登录用户获取。否则任何人都可以伪造他人ID发布物品。真实项目里这叫“越权漏洞”也是面试官非常喜欢问的点。示例代码如下PostMapping(/api/items) public Result? publish(RequestBody LostItem item) { Long userId CurrentUserUtil.getUserId(); // 从SecurityContext获取 item.setUserId(userId); item.setStatus(pending); lostItemMapper.insert(item); return Result.success(item); }同理认领申请的接口也要传给user_id但要从Token里解析而不是让前端传一个要认领的userId。认领逻辑代码长这样Service public class ClaimService { Transactional public void applyClaim(Long itemId, String reason) { LostItem item lostItemMapper.selectById(itemId); if (item null) throw new BizException(物品不存在); if (item.getStatus().equals(claimed)) throw new BizException(该物品已被认领); ClaimRecord claim new ClaimRecord(); claim.setItemId(itemId); claim.setUserId(CurrentUserUtil.getUserId()); claim.setReason(reason); claim.setStatus(pending); claimMapper.insert(claim); } Transactional public void approveClaim(Long claimId) { ClaimRecord claim claimMapper.selectById(claimId); // 把认领记录改为已通过 claim.setStatus(approved); claimMapper.updateById(claim); // 同时把关联物品改为已认领 LostItem item lostItemMapper.selectById(claim.getItemId()); item.setStatus(claimed); lostItemMapper.updateById(item); } }看到没Transactional非常重要。如果只改认领表不改物品表系统就会出现数据不一致而事务能让这两步一起成功或一起回滚。面试时这也是一个可以主动讲的“技术深度”点。4. 前端实现关键路径从搭建到联调4.1 创建Vue3项目和基础目录结构我建议用Vite创建项目命令如下npm create vitelatest lost-frontend -- --template vue cd lost-frontend npm install npm install vue-router4 pinia element-plus axios装完之后把src下面分几个文件夹api放接口请求函数router路由配置storesPinia状态views页面组件components公共组件一般我还会在main.js里统一注册Element Plusimport { createApp } from vue import App from ./App.vue import router from ./router import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css const app createApp(App) app.use(router) app.use(createPinia()) app.use(ElementPlus) app.mount(#app)4.2 Axios拦截器封装解决Token注入和统一错误提示如果每个请求都手动加Header代码会非常啰嗦。我通常在src/utils/request.js里封装一个axios实例加两个拦截器import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带 Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { ElMessage.error(登录状态已过期请重新登录) router.push(/login) } else { ElMessage.error(error.response?.data?.msg || 请求失败) } return Promise.reject(error) } )这里的baseURL: /api是为了配合Vite的代理在vite.config.js里这样配server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前后端联调时不会出现跨域问题。如果你后端也加了CORS配置双重处理不是不行但容易把自己搞晕。我更推荐只在前端代理解决这样后端接口保持纯净。4.3 页面设计列表页、发布页、详情页、管理页前端页面不需要设计得很复杂但有几个页面一定要有物品大厅列表页通过卡片或列表展示所有物品支持按类型寻物/招领、分类、关键词筛选。这里我直接调用/api/items?typefoundcategorycard这类带参数的GET接口后端用MyBatis Plus的QueryWrapper动态拼接条件。发布页一个表单字段包括标题、描述、分类、类型、图片、地点、联系方式。用Element Plus的el-form加上表单校验。图片上传走我们前面定义的/api/upload接口上传成功后把返回的URL存到表单的image字段里提交时再把整个JSON发给后端。物品详情页展示物品详情和发布人联系方式下方有一个“我要认领”按钮。点击后弹窗要求填写认领理由。前端把这个理由提交给/api/items/{id}/claim接口。后台管理页只有管理员能进入。主要展示所有待审核的认领记录管理员点击通过或拒绝。前端根据role字段控制路由权限比如在路由守卫里判断如果要去/admin路径但当前用户不是管理员则跳回首页。4.4 路由守卫与Pinia状态管理用Pinia保存用户信息和登录状态可以让多个页面共享。比如导航栏要根据用户角色显示不同菜单这时候store就很方便// stores/user.js export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }), actions: { setLogin(data) { this.token data.token this.userInfo data.userInfo localStorage.setItem(token, data.token) localStorage.setItem(userInfo, JSON.stringify(data.userInfo)) }, logout() { this.token this.userInfo {} localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })路由守卫里这样用router.beforeEach((to, from, next) { const auth useUserStore() if (to.meta.requiresAuth !auth.token) { next(/login) } else if (to.meta.requiresAdmin auth.userInfo.role ! admin) { next(/) } else { next() } })这里有个很常见的问题刷新页面后Pinia里的数据会丢失。所以我在state里从localStorage做初始化而不是只在内存里保存。否则用户刷新后Token明明还在但用户信息变成空对象前端就误判为未登录。5. 环境搭建与零基础实操全流程5.1 开发环境清单建议按这个组合来能最大限度减少版本报错软件版本建议JDK17或21不要低于17Maven3.8Node.js18及以上MySQL8.0Redis可不装本项目不强制依赖IDE后端IntelliJ IDEA前端VS Code数据库我建议用MySQL 8.0字符集用utf8mb4否则中文会乱码。在application.yml里这个属性很容易被忽略spring: datasource: url: jdbc:mysql://localhost:3306/lost_found?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时用MyBatis Plus时最好在Mapper接口上加上Mapper注解或者在启动类上加MapperScan否则Spring容器找不到Mapper启动直接失败。很多人第一次跑项目就卡在这里。5.2 从空目录到能跑的后端步骤在IntelliJ IDEA里新建一个Spring Initializr项目选SpringBoot3.x依赖加上Spring Web、MySQL Driver、MyBatis Plus手动引入、Validation、Security。在pom.xml里添加MyBatis Plus的依赖注意SpringBoot3需要mybatis-plus-spring-boot3-starter如果你引了旧版spring-boot-starter可能会遇到类冲突。配置application.yml包括数据源、MyBatis Plus的map-underscore-to-camel-case: true下划线自动映射驼峰。写好User实体、Mapper、Service、Controller先跑通“注册”接口注册时用BCryptPasswordEncoder加密密码用户名唯一校验成功返回JWT。再实现登录接口登录时校验密码通过后生成JWT返回给前端。继续实现物品CRUD、认领流程、文件上传。这里我建议不要一次性堆所有代码。每写完一个接口就用Postman测一次确认通了再往下写。5.3 零基础学习顺序安排如果你是刚接触SpringBoot和Vue3建议按这个顺序推进先学会启动一个最简单的SpringBoot Web项目理解“Controller接收请求 → 返回JSON”这种基本链路。再学MyBatis Plus的单表CRUD把增删改查接口跑通。接着学JWT鉴权理解Token怎么发出、怎么校验。前端先搭一个页面用Axios把后端的接口数据展示出来。再逐步完善所有模块。这个过程大概两周能搞定。不要试图一开始就看懂全部代码你只需要每天推进一小块两周后回头会发现整体已经能串联起来了。5.4 部署上线时的一个关键配置如果你想把项目部署到云服务器后端要打成jar包运行前端要npm run build生成dist目录然后用Nginx托管。Nginx里需要配反向代理把/api转发到后端端口server { listen 80; server_name yourdomain.com; root /var/www/lost-frontend/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里注意Vue3的History路由模式刷新页面会出现404。解决方式是Nginx里加try_files $uri $uri/ /index.html;否则部署完一刷新就白屏。前端的location /里要加上这一行。6. 常见问题与排查技巧实录6.1 启动时报“Failed to configure a DataSource”这个报错只要出现在启动类旁边或者控制台最顶部多半是你没配数据源或者配置没生效。排查顺序看application.yml是否在src/main/resources目录下。看url、username、password是否和你本机MySQL一致。看是否引入mysql-connector-java或com.mysql:mysql-connector-j依赖。本地MySQL服务是否启动Windows下服务里查看MySQL进程有没有运行。如果全都正常试一下在SpringBoot测试类里加一个SpringBootTest如果测试启动成功但主应用启动失败重新检查启动类的包路径和配置。6.2 跨域问题前端报CORS错误跨域是前后端分离项目的高频问题。如果你没用前端代理那就需要后端加CORS配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); CorsConfiguration config new CorsConfiguration(); config.setAllowedOrigins(List.of(http://localhost:5173)); config.setAllowedMethods(List.of(*)); config.setAllowedHeaders(List.of(*)); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意setAllowedOrigins不要写成*否则配合JWT的Authorization头会导致跨域预检失败。或者你在前端vite.config.js里配置代理习惯上我更推荐这个方式。6.3 JWT解析出来的用户名是null这个通常是因为SpringSecurity过滤器执行顺序不对。确保你的JWT过滤器是在UsernamePasswordAuthenticationFilter之前注册的并且SecurityContext已经拿到了Authentication。同时确认你在JwtAuthenticationFilter里设置了SecurityContextHolder.getContext().setAuthentication(authentication)否则后续AuthenticationPrincipal或SecurityContextHolder.getContext().getAuthentication()都是空。6.4 前端刷新404、部署后图片不显示刷新404的问题上面提到了用Nginx的try_files解决。图片不显示则先看后端返回的URL是否是相对路径。如果你存的是/files/xxx.jpg那么前端请求的域名如果和后端不一致就需要在返回时拼接完整地址。最简单的方式是后端返回时用request.getScheme()request.getServerName()request.getServerPort()拼成绝对URL。6.5 我踩过的一个坑Element Plus表单校验不生效表单校验不生效最常见的原因是你把rules绑定到了el-form上但el-form-item的prop没有写字段名或者本地数据不是ref()包裹的响应式对象。在Vue3的setup里el-form的model绑定的必须是reactive对象字段名要和prop完全一致。还有一个小细节如果用了ref包裹表单值要通过form.value.xxx访问否则校验函数里拿到的值始终是undefined。7. 项目扩展空间让毕业设计更有亮点基础功能做完之后如果你想让项目在答辩中更出彩可以在现有基础上扩展一到两个附加功能。1. 使用WebSocket做实时失物通知。比如有人发布了和你之前登记寻物相似类型的失物可以实时推送给用户。这个改动不算大但技术含金量明显提升。2. 引入Redis做接口缓存和验证码。把物品列表的热门接口缓存到Redis能明显提高响应速度面试时还可以聊一聊缓存一致性问题。3. 增加Excel导出。管理员后台可以将失物信息导出成Excel用EasyExcel实现非常简单但很贴合日常使用场景不是一个“为做而做”的功能。4. 接入地图定位。在发布失物时选择位置标记前端用高德地图或百度地图展示失物拾取地点。这个功能视觉效果好也比较容易做出亮点。我自己在实际组织这个项目时最深的体会是一开始不要被SpringSecurity吓住。很多人绕开鉴权直接做CRUD结果做完CRUD之后发现根本不敢叫“系统”因为谁都能改数据。你把SpringBoot3 Vue3的组合用在一个业务闭环完整的系统里最大的收获其实不是“代码量”而是你理解了“数据如何受控地流动”——从登录到发布从认领申请到管理员审核每一步都讲究只有合法的人能操作对应的数据。这种对数据权限的理解放到任何一个后台管理项目里都是通用的。如果你正卡在某个报错或者环境问题上可以试试先简化环境不要上来就跑完整的前后端联调先让后端“裸跑”通再让前端直接写死数据调试页面最后再通过代理把两边接上。像剥洋葱一样层层排查你会发现问题出现的范围和原因清晰得多。这套流程掏心窝子说比任何“一键脚手架”生成的代码都更能让你真正学到东西也正是这类系统作为毕业设计最值得你自己动手做一遍的原因。