Spring Boot+Vue校园失物招领系统毕设:表设计、状态流转与避坑指南

发布时间:2026/9/25 3:31:04
Spring Boot+Vue校园失物招领系统毕设:表设计、状态流转与避坑指南
简介这是一份面向计算机专业学生的毕业设计项目——校园失物招领系统完整源码包适合用作课程设计、大作业或毕设答辩的参考范本。系统针对校园物品丢失与认领效率低的问题涵盖信息发布、失物匹配、用户管理等典型功能完整呈现需求分析、数据库设计、前后端交互到测试交付的Web开发全流程。压缩包共631个文件大小27.11MB以后端PHP脚本、页面模板、JavaScript脚本和CSS样式为主体配合JPG/PNG图片素材另含SQL数据库脚本、配置文件、伪静态规则及说明文档覆盖系统运行、部署与二次开发所需的资源。已有134人学习。借助源码与文档可掌握Web系统分层结构、登录认证与权限控制、数据表设计及安全防护等实现思路PPTX和MD文件还能辅助毕设答辩展示与论文撰写适合希望从完整项目中提升编码与工程能力的学生。1. 校园失物招领系统毕设不是简单CRUD而是完整闭环拿到这个《校园失物招领系统》的毕设压缩包如果你以为只是把增删改查跑通就能交差多半会在第一次互相演示时翻车。失物招领系统最难的点不在代码量而在捡到物品-发布-申请认领-审核-领回这条闭环一个失物可能被多个人同时申请认领管理员要能审核学生要能查看进度每一条记录都要可追溯。这个选题特别适合计算机科学与技术、软件工程的学生最稳妥的技术路线是 Spring Boot Vue 前后端分离把登录、发布、认领、审核、统计串成一个完整链路既满足毕设工作量答辩也有东西可讲。本文按我实际带毕设的经验把这个系统从表结构到前后端联调拆开讲清楚顺带把最容易踩的坑提前标出来。2. 先想清楚再动手失物招领系统的核心流程与表设计没想清楚业务流程就开始建表是这类毕设项目最普遍的失分点。校园失物招领涉及两类用户学生发布者/认领者和管理员通常是失物招领处老师。系统里流动的也不是静态物品而是一条条状态不断变化的信息。所以在写第一行代码之前我建议先画一张状态流转图再反推表结构。2.1 从捡到到领回四个状态节点把业务串起来我在给毕设学生梳理需求时习惯把失物信息的状态拆成四个节点状态值含义触发条件0待认领发布者录入失物/招领信息后初始状态1审核中/待确认有人提交认领申请后台标记2已领回认领者确认领到物品流程闭环3已失效超过N天无人认领由管理员标记下架这里有一个容易被忽略的点失主和拾主都可以发布信息。捡到东西的人发布的是招领信息丢了东西的人发布的是寻物启事。两者字段几乎一样只差一个type字段区分但业务流程不同招领信息需要被认领寻物启事需要被人来提供线索。很多毕设项目把这两类混在同一个失物表里后面做认领逻辑时就开始在代码里堆 if-else越写越乱。我的做法是共用一个物品表但用type status两个字段组合驱动不同的服务方法避免复制一份 Controller。状态流转的具体规则是发布招领信息之后状态为 0失主看到信息后提交认领申请这时不直接改失物的状态而是生成一条认领记录失物状态变为 1表示有人认领待审核管理员或发布者核对后把认领记录置为通过失物状态变为 2如果超过 30 天没人认领管理员手动批量把状态改成 3。寻物启事则简单一些发布后只有寻找中和已找到两个状态匹配成功时由发布者手动切换。这个设计可以在答辩时清晰讲出每个状态对应的代码位置避免被问状态为什么没有流转记录。2.2 表结构设计学生、物品、认领记录、通知四张表把关系理清基于上面流程我一般拆四张核心表用户表、物品表、认领记录表、通知表。分类表可以并入物品表用一个category字段而不是单独建表减少联表查询对毕设项目更友好。用户表关键字段id、username、passwordBCrypt 加密、real_name、phone、rolestudent/admin、college学院。注意real_name和phone要单独存因为认领核实时需要展示不能在 username 里塞真名。物品表关键字段id、user_id发布人、title、description、typelost/found、category、place、happen_time、image_url、status、create_time。type是 1 为寻物、2 为招领这个枚举值建议在前后端共用一份常量注释避免前端传0/1后端传true/false对不上。image_url可以放多个用逗号分隔或者单独建一张图片表让字段更干净考虑到毕设体量用逗号分隔更简单。认领记录表是整条流程的关键id、item_id、claim_user_id、reason、status0 待审核/1 通过/2 拒绝、audit_user_id、audit_time、create_time。为什么必须有audit_user_id因为认领不是自动通过需要人工核对答辩时老师大概率会问怎么防止有人冒领这条审核链路就是你的回答。通知表id、user_id、content、is_read、create_time。当认领申请被通过或拒绝时给认领人写一条通知发布者发布的信息有人申请认领时也给他写一条通知。通知表能显著提升系统完整性的观感但实现成本很低一张表两个插入语句而已。外键我在实际项目里基本不建逻辑关联靠 Java 层控制删除用逻辑删除字段deleted0/1。原因有两个一是 MySQL 外键在多表操作时容易把批量删除搞得很痛二是毕设代码里用 MyBatis Plus 的注解就能完成查询外键反而增加启动约束检查。表的create_time、update_time用数据库的CURRENT_TIMESTAMP自动填充代码里不手动传减少脏数据。2.3 技术选型为什么 springbootvue 是毕设主流而不是 JSP现在搜基于springboot的java毕设springbootvue毕设几乎成了计算机毕设选题的标配这个选型本身没错。Spring Boot 负责后端接口Vue 负责页面渲染前后端通过 JSON 交互代码分层清楚答辩时无论是画架构图还是演示接口文档都很直观。相比 JSP/Servlet 的传统单体项目前后端分离的强项在于前端可以单独启动、单独调试接口用 Postman 能直接测试不需要每次重启整个 Tomcat。对比微服务架构失物招领系统又不存在真正需要拆分的业务模块强行用 Spring Cloud 反而引入注册中心、网关一堆概念工作量全花在环境搭建上。所以最适合毕设的是单体 Spring Boot 前端静态资源分开部署。这里给一个选型建议版本不要盲目追新。Spring Boot 用 2.7 左右搭配 MyBatis Plus 3.x资料多、坑少Vue 用 2.x 加 Element UI组件成熟表格、弹窗、上传都有现成方案。用 Vue 3 和 Element Plus 也可以但很多教程和社区问题还是基于 Vue 2 写的新手遇到报错去搜大概率搜到的是 Vue 2 答案。如果你对前端不熟用 Vue 2 起步的容错率更高。数据库用 MySQL 5.7 或 8.0 都可以注意驱动依赖版本别搞混。3. 用Spring Boot把后端跑通最小可运行代码与参数说明后端我按能跑通的最小集来搭不要一上来就分一堆包。先实现用户登录和物品发布两个核心链路再往里面加认领、审核这样每加一个功能都能立刻通过接口验证。3.1 项目骨架与依赖pom.xml里最关键的三个配置新建 Spring Boot 项目时pom.xml 里最关键的依赖是下面这几个parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依赖清单不要贪多。spring-boot-starter-web提供 MVC 和 Tomcatmybatis-plus帮你省掉大量 Mapper XMLjjwt做登录令牌lombok让实体类少写 getter/setter。注意 mybatis-plus 和 spring boot 版本要兼容3.5.3 配 2.7.x 没问题如果换 Spring Boot 3.x 就得上 mybatis-plus 3.5.5 以上否则启动直接报错。写代码前把application.yml里的连接串、端口、日志级别配好server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_lost?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0url 里的serverTimezoneAsia/Shanghai必须写否则 MySQL 8 默认时区和本地不一致日期查询会出现 8 小时的偏差。map-underscore-to-camel-case打开后数据库的create_time就能自动映射到createTime。logic-delete配置让删除变成 update避免物理删除后历史认领记录查不到。3.2 接口实现发布、查询、认领、审核四个核心API后端接口我习惯按业务动词来命名而不是按表名。Controller 里只需要四个核心接口发布信息、分页查询、提交认领申请、管理员审核。RestController RequestMapping(/api/item) public class ItemController { Resource private ItemService itemService; PostMapping(/publish) public Result? publish(RequestBody Valid ItemDTO item) { // 从登录上下文取当前用户id发布人不能从前端传 Long userId AuthContext.getUserId(); item.setUserId(userId); item.setStatus(0); return Result.success(itemService.publish(item)); } GetMapping(/page) public ResultIPageItemVO page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword, RequestParam(required false) Integer type, RequestParam(required false) Integer status) { return Result.success(itemService.pageQuery(page, size, keyword, type, status)); } PostMapping(/claim) public Result? claim(RequestBody ClaimDTO dto) { return Result.success(itemService.claim(dto)); } PostMapping(/audit) public Result? audit(RequestBody AuditDTO dto) { // 这里需要做管理员权限判断见3.3 return Result.success(itemService.audit(dto)); } }发布接口里user_id一定从后端登录态拿不能用前端传来的 userId 字段这是接口安全的一个基本要求。分页查询的四个参数里keyword用 like 匹配标题和描述type和status是精确过滤这三个是可选的不传就查全部。claim接口提交时后端要校验当前用户不是发布者本人避免出现自己发自己认领的尴尬数据。audit接口把认领记录的状态改成通过或拒绝同时修改物品表状态。对应的 Service 实现里claim 的逻辑是三件事先查认领记录是否已存在防止重复认领再插入认领记录最后把物品状态改成 1。audit 的逻辑是更新认领记录状态、把物品状态改成 2已领回或退回 0待认领、给认领人写一条通知。这三步必须放在一个事务里用Transactional标注否则中途报错会出现认领记录写入了但物品状态没更新的脏数据。3.3 JWT登录与角色权限学生和管理员怎么区分登录接口用 JWT 生成令牌前端把令牌存到 localStorage每次请求在 header 里带上。后端写一个拦截器解析令牌并把用户信息放到 ThreadLocal 里Controller 里通过 AuthContext 取。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } Claims claims JwtUtil.parse(token.substring(7)); AuthContext.setUserId(claims.get(userId, Long.class)); AuthContext.setRole(claims.get(role, String.class)); return true; } }token 过期时间我一般设在 2 小时学生演示环境下不会太长也不至于演示一半要重新登录。角色权限在拦截器里只做基础校验对/api/admin/**的请求额外判断 role 是否为 admin。比较合理的做法是单独写一个 AdminInterceptor 继承上面的逻辑或者直接加一个注解RequireRole(admin)通过 AOP 实现。毕设里用最简单的方式即可在 Controller 方法里用AuthContext.getRole()判断不通过就 throw new BizException(无权限)。JwtUtil 里要特别注意 jjwt 0.9.1 版本对 JDK9 的兼容问题我遇到过报错没找到javax.xml.bind.DatatypeConverter需要在 pom 里额外加两个依赖或者直接换成 jjwt 0.11.x 用parseClaimsJws方法。更省事的方案是手动引入 hutool-jwt代码量差不多但不用处理老版本库的类加载问题。4. 前端Vue页面从列表到认领流程一个页面把状态展示清楚前端部分我用 Vue 2 Element UI。失物招领系统页面不多登录注册页、失物大厅列表、发布页、个人中心、管理后台。把路由规划好再逐个页面填充组件。4.1 页面路由与状态管理这里不适合用复杂状态库路由设计上把需要登录的页面和公开页面分开。失物大厅和详情页是公开的访客也能看但发布、认领、个人中心必须在登录后访问。基础的路由配置如下import Vue from vue import Router from vue-router import LostHall from ../views/LostHall.vue import Publish from ../views/Publish.vue import AdminDashboard from ../views/AdminDashboard.vue Vue.use(Router) const router new Router({ routes: [ { path: /, name: LostHall, component: LostHall }, { path: /publish, name: Publish, component: Publish, meta: { requiresAuth: true } }, { path: /admin, name: AdminDashboard, component: AdminDashboard, meta: { requiresAuth: true, requiresAdmin: true } } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login }) } else { next() } })很多教程会建议装 Vuex但这个小项目里需要跨页面共享的状态只有当前用户信息和 tokentoken 已经在 localStorage用户信息可以从 token 里解析完全不需要引入 Vuex。引入状态管理库只会增加一个必须维护的 store 文件对课设代码来说是不必要的复杂度。如果后续要加的页面多再把用户信息放入 Vuex 也不迟。路由守卫里必须注意白名单登录页、失物大厅、注册页、详情页不需要登录其他页面都检查 token。如果你的后端接口有更细的权限前端路由守卫只是体验优化真正的权限控制必须在后端拦截器里做否则有人直接调接口就能绕过前端页面。4.2 核心页面拆解失物列表、详情弹窗、认领表单失物大厅是这个系统门面我用表格加卡片混合的方式展示。Element UI 的 el-table 适合展示结构化信息但失物招领更直观的是卡片式布局我一般用 el-card 结合 flex 布局做瀑布流。每张卡片上放物品图片、标题、地点、时间、状态标签点击卡片进入详情。详情弹窗里最关键的是状态判断物品状态为 0待认领时显示我要认领按钮状态为 1待审核时显示审核中标签状态为 2已领回时显示已找到标签并把认领按钮禁用。这个判断如果直接写在模板里状态数字和文字对应关系容易乱建议写一个计算属性computed: { statusText() { const map { 0: 待认领, 1: 审核中, 2: 已领回, 3: 已失效 } return map[this.detailData.status] || 未知状态 } }认领表单只有三个字段认领原因、联系电话、希望领取时间。发布者的联系方式不需要在表单里重复填系统里已有。提交认领后按钮变成已申请同时禁用防止用户手滑连点两次提交出两条认领记录。列表页的分页用 el-pagination 组件page和size两个参数与后端接口保持对应。需要注意切换页码时要把 keyword 一并带上否则你搜索校园卡查第二页结果变成了全量的第二页这是一种很常见的状态丢失 bug。处理方法是在 data 里维护一个searchForm对象分页和搜索共用这一个状态每次请求都从里面取值。4.3 前端接口封装与时间格式化两个容易翻车的小点前端接口统一封装到一个 request.js 里利用 axios 拦截器做 token 注入和错误码统一处理import axios from axios 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 response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } )容易翻车的第一处后端返回的时间是带时区的 ISO 字符串直接显示在页面里会出现 2024-06-01T10:30:00 这种格式。我在前端定义了一个全局过滤器用 dayjs 格式化显示为 2024-06-01 10:30。后端如果设置的是 Asia/Shanghai前端 format 也要对应不要一个用本地时区一个用 UTC。容易翻车第二处接口路径的 baseURL。开发环境用 vite/webpack 的代理把/api转发到 8080 端口生产环境把后端打包进同一个域就能直接访问。我见过很多学生直接把 axios 的 baseURL 写成http://localhost:8080然后前端 5173 端口访问时出现 CORS 跨域失败虽然可以靠后端配置跨域解决但不建议因为一旦部署到服务器IP 和端口变了你又要改代码。正确做法是走代理配置本地开发零跨域。5. 避坑指南答辩前最容易暴露的5个问题我看了不少学生的失物招领系统功能都能跑但一问细节就露出原型。下面这几个坑是出现频率最高的按现象、原因、解决来写希望你能对照自己的项目检查一遍。5.1 图片上传后刷新就丢了现象发布失物时上传的图片当时能显示刷新页面后图片裂了。原因把图片存到了前端项目的 static 目录或后端 resources 的临时目录项目重新启动时文件被清理或者用了 IDEA 启动时自动写入 target 目录切换工作目录后路径对不上。解决生产环境应该用独立文件服务或对象存储但毕设项目最简单的做法是配置一个独立的本地上传目录比如/data/upload然后把/upload/**映射为静态资源路径。注意不能把上传目录放在 src 下面否则重新打包就清空了。具体做法是在配置类里重写addResourceHandlers把本地磁盘路径映射到 URL。答辩时可以明确说出图片存哪、怎么隔离比含糊说存在项目里好很多。5.2 分页查询把已认领的也查出来了现象失物大厅里显示已领回的物品用户还能看到并且点进去。原因分页查询的 where 条件没过滤 status或者过滤了但前端没传。前端列表接口把所有状态的数据都拉回来了导致用户能看到已经被人领走的物品甚至还能提交认领。解决列表接口默认只查status0待认领的数据已领回和已失效的数据在管理后台单独看。如果确实想在详情页查看历史物品需要后端单独提供history接口并显式传 status 条件不要在pageQuery里用一个 flag 来区分那样代码会越来越乱。同时前端调用/api/item/page时也要把 status 参数固定传 0防止后端没有默认值时行为不一致。5.3 认领申请没有幂等同一个失物被重复申请现象同一个用户对同一个物品提交两次认领后台出现两条认领记录。原因前端按钮是防了但后端接口没有校验。快速连点或者用 Postman 重放请求都能绕过前端这是接口设计里最常见的疏漏。解决在 service 里先查claim_record表判断item_id和claim_user_id是否已有 status0 或 status1 的记录存在则直接抛出异常。同时在数据库上给item_id claim_user_id加唯一索引双保险。这样即使代码有并发漏洞数据库也会拒绝第二条记录。5.4 JWT过期时间设置过长现象登录一次好几天 token 都有效老师问如果学生手机丢了怎么办。原因为了测试方便把过期时间设成了 7 天演示时不用频繁登录但这个问题在答辩中很容易被追问。解决把 token 有效期调到 2~4 小时并提供一个刷新接口前端在 401 时自动刷新。如果不想做刷新逻辑至少可以把过期时间作为配置项放在application.yml里答辩时说明这里是可配置的线上会调短老师就会认可这个设计意识。5.5 跨域配置错误现象前端访问后端接口浏览器报 CORS 错误后端明明有CrossOrigin注解还是报。原因用了CrossOrigin但没处理预检请求或者允许的 origin 写成了*且allowCredentials为 true这两个组合是冲突的浏览器直接拦截。解决不要用CrossOrigin逐接口配置而是写一个全局 CorsFilter自定义allowedOriginPatterns、allowedMethods、allowedHeaders并设置allowCredentialstrue。注意前端通过代理访问时不需要跨域配置只有直接改 baseURL 才需要。我一般推荐前者开发环境用代理线上部署时把前端打包交给后端托管跨域问题根本不会出现。6. 让系统从能跑变成能答辩加这3个功能分数和实用度一起涨6.1 图片上传用本地存储还是OSS答辩时老师常问图片存哪。用 OSS 要注册、配 bucket免费额度有限演示时还可能因为网络问题加载慢不推荐在毕设里用。本地存储的配置方式前面说过注意上传接口要限制文件类型和大小用 MultipartFile 接收后用 UUID 重命名避免文件名冲突和路径注入。同时保存原图时压缩一份缩略图列表页加载会快很多这也是一个可以主动讲的优化点。6.2 失物统计报表让答辩PPT多一页数据说话用 ECharts 在管理后台加两个图表按月失物数量柱状图和失物类别饼图。数据来自后端一个统计接口按月份 group by 查询。这里注意 SQL 层日期函数和时区的问题如果查询结果少了一天检查数据库时区是否和 Java 服务一致。统计页不需要写很长代码一个/api/admin/stats接口加两个组件就够但对系统完整度的提升非常明显。6.3 认领二维码把系统从Web端延伸到线下给每条待认领物品生成一个二维码打印出来贴在失物招领处。二维码指向前端详情页扫描用户直接进入详情认领。这个功能实现成本很低qrcode 库几行代码但答辩非常加分。二维码内容是详情页完整 URL不是后端接口否则扫码看到的是 JSON。配合通知表用户扫码认领后发布者能收到通知整个闭环就真正完整了。我每次带毕设都会强调一件事毕业设计不是堆功能而是用一条主线把关键业务走通让每张表、每个接口都能回答为什么存在。校园失物招领系统最值得投入的就是认领审核这条链路把它做扎实胜过做一堆没用的页面。希望帮到你。本文还有配套的精品资源点击获取