简介:这是一套面向高校计算机专业毕业生的校园失物招领系统毕设项目源码,基于Spring Boot + Vue.js实现前后端分离架构,聚焦校园场景下的失物登记、认领匹配、状态追踪与管理员审核等核心业务,兼顾实用性与教学完整性。资源包共97个文件,含69个Java后端逻辑类、13个XML配置与Mapper映射文件、9张界面截图与头像素材(JPG)、2个YML配置文件、1个MySQL建库建表SQL脚本,以及Groovy代码生成脚本等,结构清晰、模块划分明确,便于理解分层设计与前后端联调流程。已有5225人学习下载,代码完整、注释规范,数据库脚本可直接导入,前后端工程均经调试验证,下载解压后按文档说明即可一键运行,适合作为Java全栈开发入门实践或毕业设计参考范例。
1. 为什么一个校园失物招领系统,成了 SpringBoot + Vue 毕业设计的「高频稳态解」?
不是因为功能多炫酷——它连支付都不需要;也不是因为技术多前沿——没用 Flink 实时计算,也没上 Kubernetes 编排。恰恰相反,它胜在「边界清晰、需求真实、容错友好、交付可见」:学生丢了耳机/书包/校园卡,扫码拍照上传,后台按楼栋+时间+关键词筛出匹配项,失主私信认领,管理员一键标记“已归还”。整个链路没有模糊地带,所有角色(学生、宿管、辅导员)操作路径明确,数据流向单向可追溯。
这正是 SpringBoot + Vue 前后端分离架构最擅长的战场:后端专注 RESTful 接口定义、数据库事务控制、文件上传校验与权限收敛;前端专注表单交互、图片预览、地图定位(Mapbox Vue 可插拔)、状态流转动画。它不考验分布式一致性,但极度考验接口契约稳定性——比如“上传失物”必须返回uploadId供后续关联,否则前端无法跳转详情页;也极度考验部署轻量性——毕业答辩现场用一台 4G 内存笔记本跑通全栈,比在云服务器上配 Nginx 反向代理更实在。
如果你正卡在「选题空泛→代码堆砌→答辩被问住」的死循环里,这个项目不是模板,而是一套可验证的工程闭环训练场:从spring-boot-starter-web初始化到vue-router动态路由配置,从 MyBatis 多表联查写法到 Vue Composition API 的响应式封装,每一步都能在本地复现、调试、截图、录屏。它不承诺“高并发”,但保证“能讲清每一行为什么这么写”。
2. 从零初始化:SpringBoot 后端骨架搭建与核心模块分层
2.1 用 Spring Initializr 创建最小可行工程(JDK 17 + Maven)
提示:别用 Spring Boot 3.x 最新版(如 3.2+),当前主流教学环境仍以 2.7.x 为稳定基线。
spring-boot-starter-web、spring-boot-starter-data-jpa、spring-boot-starter-validation是三大基石,其余按需添加。
# 推荐命令行初始化(避免 IDE 插件版本错位) curl https://start.spring.io/starter.tgz \ -d dependencies=web,jpa,h2,validation,lombok \ -d javaVersion=17 \ -d baseDir=lostfound-backend \ | tar -xzvf -解压后打开pom.xml,确认关键依赖版本:
spring-boot-starter-parent版本锁定为2.7.18(LTS 支持至 2025 年)spring-boot-starter-data-jpa自动引入 Hibernate 5.6.x,兼容 MySQL 8.0+h2仅用于本地开发调试,正式部署前替换为mysql-connector-java
逻辑分层不是教条,而是为了应对答辩时被追问:“这张表的更新逻辑,是在 Service 层做事务控制,还是在 Controller 里直接 save?” 我们按Controller → Service → Repository → Entity四层落地:
LostItemController.java:只做参数校验(@Valid)、调用 Service、统一返回Result<T>包装体LostItemService.java:含事务注解@Transactional,处理“上传失物+生成缩略图+触发通知”原子操作LostItemRepository.java:继承JpaRepository<LostItem, Long>,自定义查询方法如findByBuildingAndStatusOrderByCreateTimeDescLostItem.java:实体类,用@Table(name = "t_lost_item")显式指定表名,避免 Hibernate 自动生成下划线命名冲突
2.2 数据库建模:聚焦「失物生命周期」而非 E-R 图完美性
校园场景决定字段必须带业务语义,而非纯技术范式。例如:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
id | BIGINT PK | AUTO_INCREMENT | 主键,不用 UUID —— 分页查询性能差、MySQL 索引碎片高 |
building_code | VARCHAR(10) | NOT NULL | 宿舍楼编号(如D3,J1),非外键!避免跨表 JOIN 拖慢列表页 |
status | TINYINT | DEFAULT 0 | 0=待认领, 1=已认领, 2=已过期(7天未处理自动归档) |
image_urls | JSON | NULL | 存数组["/uploads/abc123.jpg", "/uploads/def456.jpg"],不用单独建图集表 |
contact_way | VARCHAR(50) | NULL | 微信昵称/手机号/学号,不加密存储(毕业设计无合规要求,但需在答辩时说明) |
注意:
image_urls用 JSON 类型是权衡之举。MySQL 5.7+ 原生支持,查询可用JSON_CONTAINS(image_urls, '"abc123.jpg"');若用 PostgreSQL 则改用JSONB;若坚持关系型设计,则建t_lost_image表并加lost_item_id外键——但答辩时需解释“为何为 1:N 关系牺牲列表页 200ms 响应”。
2.3 文件上传模块:绕过 Spring Boot 默认大小限制的三步实操
默认spring.servlet.multipart.max-file-size=1MB,根本传不了带定位信息的原图。修改application.yml:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB # 额外加固:Tomcat 层级限制(Spring Boot 2.7 内嵌 Tomcat 9.0) server: tomcat: max-swallow-size: 10MB但光改配置不够——前端传的是multipart/form-data,后端需显式接收并落盘:
@PostMapping("/api/lost-item") public Result<Long> createLostItem( @RequestPart("item") @Valid LostItemDTO dto, @RequestPart("images") MultipartFile[] images) { // 注意:不是 MultipartFile image // 1. 校验图片数量(≤3 张) if (images.length > 3) { return Result.fail("最多上传3张图片"); } // 2. 逐张保存并生成 URL(实际项目应异步+OSS,此处同步写本地) List<String> urls = new ArrayList<>(); for (MultipartFile file : images) { String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); Path uploadPath = Paths.get("uploads", fileName); Files.createDirectories(uploadPath.getParent()); file.transferTo(uploadPath); urls.add("/uploads/" + fileName); // 前端访问路径 } // 3. 保存主记录(urls 存入 JSON 字段) LostItem item = new LostItem(); BeanUtils.copyProperties(dto, item); item.setImageUrls(new ObjectMapper().writeValueAsString(urls)); return Result.success(itemRepository.save(item).getId()); }关键点:@RequestPart替代@RequestParam,因MultipartFile必须用@RequestPart绑定;transferTo()调用前必须createDirectories(),否则 Linux 下报NoSuchFileException;URL 返回/uploads/xxx.jpg,意味着需在 Spring Boot 中配置静态资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:uploads/"); } }3. Vue 前端工程搭建与核心页面实现逻辑
3.1 Vue CLI 创建标准项目(Vue 3 + Composition API + Router 4)
提示:别用 Vite 初学者模板!虽然快,但
vue-router动态路由、pinia状态管理、axios拦截器的调试链路在 Vue CLI 下更透明,适合答辩现场 debug。
npm install -g @vue/cli vue create lostfound-frontend --default cd lostfound-frontend npm install axios vue-router@4 pinia@2 element-plus@2.3.0目录结构按功能域划分,而非技术类型:
src/ ├── api/ # 所有 axios 请求封装(login.js, lostItem.js) ├── assets/ # 静态资源(logo.svg, mapbox-token.js) ├── components/ # 可复用 UI(ImageUpload.vue, StatusBadge.vue) ├── router/ # 路由定义(index.js 含动态路由 /item/:id) ├── stores/ # Pinia store(useUserStore.js, useLostItemStore.js) ├── views/ # 页面级组件(HomeView.vue, PublishView.vue, DetailView.vue) └── App.vue # 根组件,含 <router-view>router/index.js中定义动态路由,这是答辩高频考点:
const routes = [ { path: '/', name: 'Home', component: () => import('@/views/HomeView.vue') }, { path: '/item/:id', name: 'Detail', component: () => import('@/views/DetailView.vue'), props: true // 将 route.params 作为 props 传入组件,避免 this.$route.params.id }, { path: '/publish', name: 'Publish', component: () => import('@/views/PublishView.vue'), meta: { requiresAuth: true } // 路由守卫依据 } ]3.2 发布失物页面:Vue 3 Composition API 封装图片上传逻辑
PublishView.vue不写data(){return{}},全部用setup()+ref/reactive:
<template> <div class="publish-form"> <el-form :model="form" :rules="rules" ref="formRef"> <el-form-item label="所在楼栋" prop="buildingCode"> <el-select v-model="form.buildingCode" placeholder="请选择"> <el-option label="D3 宿舍楼" value="D3"/> <el-option label="J1 教学楼" value="J1"/> </el-select> </el-form-item> <el-form-item label="图片上传" prop="imageFiles"> <ImageUpload v-model:files="imageFiles" :max="3"/> </el-form-item> <el-button @click="submitForm">发布失物</el-button> </el-form> </div> </template> <script setup> import { ref, reactive } from 'vue' import { ElMessage } from 'element-plus' import { useLostItemStore } from '@/stores/useLostItemStore' import ImageUpload from '@/components/ImageUpload.vue' const formRef = ref(null) const imageFiles = ref([]) // ref([]) 而非 reactive({ files: [] }),因需响应式数组长度变化 const form = reactive({ buildingCode: '', description: '' }) const rules = { buildingCode: [{ required: true, message: '请选择楼栋', trigger: 'change' }], description: [{ required: true, message: '请填写物品描述', trigger: 'blur' }] } const lostItemStore = useLostItemStore() const submitForm = async () => { await formRef.value.validate() // 触发 Element Plus 表单校验 try { const formData = new FormData() formData.append('item', new Blob([JSON.stringify(form)], { type: 'application/json' })) imageFiles.value.forEach(file => formData.append('images', file)) // 与后端 @RequestPart 名称一致 const id = await lostItemStore.createLostItem(formData) // 调用 store 中封装的 API ElMessage.success('发布成功!') // 跳转详情页(动态路由) router.push({ name: 'Detail', params: { id } }) } catch (err) { ElMessage.error(err.response?.data?.message || '发布失败') } } </script>关键细节:
v-model:files是 Vue 3 的v-model语法糖,等价于:files="imageFiles" @update:files="val => imageFiles = val"FormData构造时,item字段必须是Blob或String,不能直接append('item', form),否则后端收不到 JSON 解析router.push()用命名路由name: 'Detail'而非路径/item/123,避免硬编码路径变更风险
3.3 地图定位增强:Mapbox Vue 集成与坐标反查楼栋
校园失物高频发生在固定区域,用 Mapbox 替代百度/高德(免密钥、开源、轻量):
npm install mapbox-gl @mapbox/mapbox-gl-vuecomponents/MapSelector.vue封装地图选择器:
<template> <div ref="mapContainer" class="map-container" /> </template> <script setup> import { onMounted, ref, defineEmits } from 'vue' import mapboxgl from 'mapbox-gl' import '@mapbox/mapbox-gl-vue' const emit = defineEmits(['select-location']) const mapContainer = ref(null) let map = null onMounted(() => { map = new mapboxgl.Map({ container: mapContainer.value, style: 'https://api.mapbox.com/styles/v1/mapbox/streets-v12/style.json?access_token=YOUR_TOKEN', center: [116.3974, 39.9093], // 北京某高校坐标 zoom: 16 }) // 点击地图获取坐标,并反查最近楼栋(简化版:查预设点位) map.on('click', (e) => { const { lng, lat } = e.lngLat // 实际项目:调用后端 /api/building/nearby?lng=xx&lat=yy // 此处模拟:根据坐标范围匹配楼栋 const building = getBuildingByCoord(lng, lat) emit('select-location', { lng, lat, building }) }) }) const getBuildingByCoord = (lng, lat) => { const buildings = [ { code: 'D3', bounds: [[116.395, 39.908], [116.398, 39.911]] }, { code: 'J1', bounds: [[116.399, 39.907], [116.402, 39.910]] } ] for (const b of buildings) { const [[minLng, minLat], [maxLng, maxLat]] = b.bounds if (lng >= minLng && lng <= maxLng && lat >= minLat && lat <= maxLat) { return b.code } } return '未知区域' } </script>答辩时可演示:点击地图任意位置,自动填入“所在楼栋”下拉框——这比手动选择更符合真实场景,且技术点清晰(事件监听、坐标计算、组件通信)。
4. 前后端联调避坑指南:那些让答辩前夜崩溃的 5 个真实问题
4.1 现象:前端 Axios 请求 403 Forbidden,但 Postman 调用同一接口成功
原因:Spring Security 默认开启 CSRF 保护,而 Vue CLI 开发服务器(http://localhost:8080)与后端(http://localhost:8081)跨域,浏览器会拦截带 Cookie 的请求,CSRF Token 无法传递。
解决:开发阶段关闭 CSRF(生产环境需重开并配 Token 机制):
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) // 关键:禁用 CSRF .cors(cors -> cors.configurationSource(corsConfigurationSource())); return http.build(); } }4.2 现象:Vue 打包后部署到 Spring Boot 的static目录,访问首页白屏,控制台报Failed to load module script
原因:Vue Router 默认用history模式,刷新/item/123时,Spring Boot 静态资源处理器找不到该路径,返回 404。
解决:两种方案二选一
- 方案 A(推荐):Vue Router 改用
hash模式,在router/index.js中:const router = createRouter({ history: createWebHashHistory(), // 替换 createWebHistory() routes }) - 方案 B:Spring Boot 配置兜底路由,将所有非 API 请求重定向到
index.html:@Controller public class SpaFallbackController { @GetMapping(value = "/**/{path:[^\\.]*}") public String redirect() { return "forward:/index.html"; } }
4.3 现象:MySQL 报错Incorrect string value: '\xF0\x9F\x93\xB7' for column 'description'
原因:Emoji 表情(如 📷)需utf8mb4字符集,而 MySQL 默认utf8只支持 3 字节,无法存 4 字节 UTF-8 字符。
解决:三步走
- 修改 MySQL 配置文件
my.cnf:[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci - 重建数据库:
CREATE DATABASE lostfound CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - Spring Boot 连接串加参数:
jdbc:mysql://localhost:3306/lostfound?useUnicode=true&characterEncoding=utf8mb4
4.4 现象:npm run build后图片路径错误,显示为http://localhost:8080/img/logo.123abc.png,但实际应为/img/logo.123abc.png
原因:Vue CLI 默认public目录资源通过绝对路径引用,但assets目录图片经 webpack 处理后生成 hash,需配置vue.config.js:
module.exports = { publicPath: './', // 关键:相对路径,避免根域名污染 configureWebpack: { resolve: { alias: { '@': path.resolve(__dirname, 'src') } } } }4.5 现象:管理员登录后,前端Pinia store中用户信息丢失,刷新页面变未登录
原因:Pinia 默认 store 是内存态,刷新即销毁。需持久化到localStorage。
解决:用pinia-plugin-persistedstate插件:
npm install pinia-plugin-persistedstate// main.js import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) createApp(App).use(pinia).mount('#app')并在stores/useUserStore.js中声明:
export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: {} }), persist: true // 启用持久化 })5. 毕业答辩加分项:三个可现场演示的「小而深」技巧
5.1 后端接口文档自动生成:用 SpringDoc OpenAPI 3 替代 Swagger2
Swagger2 在 Spring Boot 2.6+ 已弃用,springdoc-openapi-ui是新标准。只需加依赖:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.6.14</version> </dependency>启动后访问http://localhost:8081/swagger-ui.html(注意路径不是/swagger-ui/),自动生成带Try it out按钮的文档。更关键的是——它能导出标准 OpenAPI 3.0 JSON,答辩时可当场演示:
- 点击右上角
Export→OpenAPI JSON - 将 JSON 粘贴到 https://editor.swagger.io
- 生成客户端 SDK(如 TypeScript Fetch)或 Mock Server
这证明你不仅写了接口,还遵循了行业协作规范,比手写 Word 接口文档高两个段位。
5.2 前端图片压缩:在上传前用compressorjs降低流量与存储压力
学生拍的失物照动辄 3~5MB,直接上传既慢又占空间。compressorjs可在浏览器端压缩:
npm install compressorjs在ImageUpload.vue中:
import Compressor from 'compressorjs' const compressImage = (file) => { return new Promise((resolve, reject) => { new Compressor(file, { quality: 0.6, // 压缩质量 0.1~1.0 maxWidth: 1200, // 限制最大宽度 success(result) { resolve(result) // result 是 Blob 对象,可直接 append 到 FormData }, error(err) { reject(err) } }) }) } // 上传前调用 const handleFileChange = async (files) => { const compressedFiles = [] for (const file of files) { if (file.size > 1 * 1024 * 1024) { // 大于 1MB 才压缩 compressedFiles.push(await compressImage(file)) } else { compressedFiles.push(file) } } emit('update:files', compressedFiles) }答辩演示:上传一张 4MB 原图,对比压缩后 800KB,加载速度提升 5 倍——这是看得见的优化。
5.3 数据库敏感字段脱敏:用 MyBatis TypeHandler 实现「学号自动掩码」
失物详情页显示联系人学号(如2021123456),但答辩演示时不宜暴露真实学号。用 MyBatis 自定义TypeHandler:
@MappedTypes(String.class) @MappedJdbcTypes(JdbcType.VARCHAR) public class StudentIdMaskTypeHandler implements TypeHandler<String> { @Override public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getResult(ResultSet rs, String columnName) throws SQLException { String raw = rs.getString(columnName); if (raw == null || raw.length() < 8) return raw; return raw.substring(0, 4) + "****" + raw.substring(8); // 2021****56 } // ... 其他方法省略 }在LostItem.java中标注:
@Column(name = "contact_way") @Type(type = "com.example.lostfound.handler.StudentIdMaskTypeHandler") private String contactWay;效果:数据库存2021123456,前端拿到2021****56,且不侵入业务代码,答辩时可指着代码说:“这是 MyBatis 的扩展点,比在 Service 层字符串拼接更优雅。”
我带过 17 届毕业设计,见过太多同学把「前后端分离」做成「前后端拼接」:后端吐 HTML,前端塞 JS;或者 Vue 里写this.$http.get('/api/item?id='+id),却不知axios拦截器能统一加 Token。这个校园失物招领系统,本质是一次工程习惯的刻印——从application.yml的缩进空格数,到git commit -m的中文描述,再到答辩 PPT 里那张curl -X POST http://localhost:8081/api/lost-item的终端截图,全是可信的证据链。
别追求「别人没做过的功能」,先确保「自己写的每一行,都能在答辩时说出它存在的唯一理由」。比如你用了Mapbox,就准备好解释「为什么不用高德?因为高德需要企业资质认证,而 Mapbox 免费额度够校园场景用一年」;比如你数据库没加索引,就坦诚说「当前数据量 < 1000 条,B+Tree 查询耗时 < 5ms,索引收益小于维护成本」。
这些细节不会写在论文里,但会在老师点头的瞬间,成为你工程素养的无声注脚。
希望帮到你。
本文还有配套的精品资源,点击获取