失物招领系统毕设实战:从零搭建一个高可用的校园级应用
“老师,我就想做个小网站,能把丢的东西发上去,让失主自己找回来,行吗?”
“行,但你得把登录、图片上传、状态变更、消息提醒、后台审核全做完,还要能演示。”
——以上对话每年 3 月都在各个高校毕设群里循环播放。
如果你也卡在这个阶段,这篇笔记可以当“避坑说明书”用:从需求混沌到答辩演示,一条线捋清。
1. 背景痛点:为什么“小系统”总被导师打回?
图:需求膨胀过程
需求模糊
只写一句话:“实现失物招领功能”。结果做到一半发现要分角色(学生/拾主/管理员)、要审核、要推送,代码被迫返工。技术选型混乱
网上一搜全是“21 天学会 XXX”,今天 Django 明天 Flask,后天又听说 Spring Cloud 牛,环境搭一半硬盘炸了。代码结构松散
控制器里写 SQL、前端把 axios 写进mounted一了百了,答辩时老师一句“事务在哪”直接原地沉默。演示现场翻车
本机跑得好好的,现场连的是教室 Wi-Fi,图片路径全是localhost:8080,投出去 404,气氛瞬间凝固。
2. 技术选型:Spring Boot + Vue3 为什么更适合“小白速通”
| 维度 | Spring Boot | Django/Flask | 备注 |
|---|---|---|---|
| 学习曲线 | 注解驱动,IDEA 提示友好 | 需要理解 MTV/蓝图 | 对只写过 C 实验课的同学,Java 语法更熟悉 |
| 脚手架生态 | start.spring.io 一键依赖 | 手动 pip 收集 | 省掉找 jar 包的时间 |
| 事务&安全 | Spring Security 直接配 | 要自己写中间件 | 毕设时间紧,能少写就少写 |
| 前端组合 | Vue3 + Vite 热更新 2s 内 | React 配 webpack 稍重 | 笔记本配置一般时,Vite 启动速度肉眼可见 |
一句话总结:Spring Boot 给你“开箱即用”的后端,Vue3 给你“秒级热重载”的前端,把有限的脑细胞留给业务,而不是调环境。
3. 核心实现细节
3.1 用户身份校验(RBAC 最简版)
- 角色仅三类:游客(只读 GD)、普通用户(可发布)、管理员(可审核)。
- Spring Security + JWT:登录后返回
accessToken,前端放在Authorization头,拦截器统一校验。 - 密码加密:BCryptPasswordEncoder,强度 10 足够毕设演示。
3.2 物品状态机(未认领 / 已认领 / 已过期)
数据库加status字段tinyint(1),代码里用枚举锁死:
public enum ItemStatus { UNCLAIMED(0), CLAIMED(1), EXPIRED(2); private final int value; ItemStatus(int value){ this.value = value; } public int getValue(){ return value; } }- 过期策略:每天 02:00 跑批,发布 30 天未更新即标记
EXPIRED。 - 状态扭转接口统一走
/api/items/{id}/status,PUT 请求,body 只传目标状态码,后端校验合法性,防止前端乱改。
3.3 图片上传与存储
- 本地开发:直接落盘
/upload,Spring Boot 静态目录映射。 - 生产环境:MinIO 私有云,SDK 上传后返回
http://minio.xxx/xxx.jpg,数据库只存 URL。 - 缩略图:后端同步生成 300×300 WebP,降低 60% 流量,演示时肉眼可见“秒开”。
4. 关键代码片段(可直接抄)
4.1 RESTful 控制器(Clean Code 版)
@RestController @RequiredArgsConstructor @RequestMapping("/api/items") public class ItemController { private final ItemService itemService; /** * 发布失物/招领 */ @PostMapping public ApiResponse<Long> create(@Valid @RequestBody ItemDTO dto, Authentication auth){ // 只允许已登录用户发布 Long userId = Long.valueOf(auth.getName()); Long itemId = itemService.create(dto, userId); return ApiResponse.success(itemId); } /** * 认领 * 幂等:同一人重复认领返回同一结果 */ @PutMapping("/{id}/claim") public ApiResponse<Void> claim(@PathVariable Long id, Authentication auth){ Long claimerId = Long.valueOf(auth.getName()); itemService.claim(id, claimerId); return ApiResponse.success(); } }要点:
- 用
@Valid做参数校验,省去一堆if - 认证信息直接注入,不在方法里写
getUserByToken这种冗余逻辑
4.2 Vue3 组件(认领按钮)
<template> <el-button type="primary" :disabled="status !== 0" :loading="loading" @click="handleClaim"> {{ status === 0 ? '认领' : '不可认领' }} </el-button> </template> <script setup> import { claimItem } from '@/api/item'; const props = defineProps({ itemId: Number, status: Number }); const loading = ref(false); async function handleClaim(){ try{ loading.value = true; await claimItem(props.itemId); ElMessage.success('认领成功,请联系失主'); // 触发父组件刷新 emit('refresh'); }finally{ loading.value = false; } } </script>- 用
<script setup>语法糖,少写export default - 按钮状态与后端状态机保持一致,杜绝“前端说能领,后端拒绝”的尴尬
5. 安全性 & 性能:别让“小项目”成为“小炸弹”
防止重复提交
前端按钮loading锁 + 后端claimer_id唯一索引,双保险。SQL 注入
Spring Data JPA 默认预编译,千万别手痒写createNativeQuery("select * from xxx where id = "+id)。XSS
富文本用 Markdown 渲染,禁止直接v-html拼接用户输入。静态资源加速
生产环境把图片、JS、CSS 全扔 CDN,回源配置 2 小时,演示时老师手机 4G 也能秒开。接口限流
基于 Bucket4j 给“发布”接口限制 10 次/分钟,防止有人脚本刷库。
6. 生产环境避坑指南
本地 vs 线上路径差异
用 Spring Profile 隔离:application-dev.yml指向file:/tmp/uploadapplication-prod.yml指向minio.endpoint
打包时加-Dspring.profiles.active=prod,别再手动改代码。
数据库迁移
引入 Flyway,SQL 脚本按版本号命名:V1.0.1__create_item.sql,上线前自动执行,回滚也有记录。微信/短信通知
- 微信:用“测试号”即可,模板消息接口免费,每天 200 条足够演示。
- 短信:阿里云短信 0.036 元/条,毕业设计预算 50 元封顶,记得把密钥放 Nacos,禁止硬编码。
服务器选型和费用
学生机 1C2G 足够跑 Jar+MySQL,带宽 1 M 别放高清大图;演示前把热点数据缓存到 Redis,秒开不卡。
7. 可继续折腾的脑洞
- 多校区通用平台:在
user表加campus_id,物品表加location_geo,后端用 MyBatis-Plus 动态 SQL 拼接校区查询,一套代码给多个学院一起用。 - 扫码认领:生成带
itemId的二维码,打印贴在失物招领柜,失主微信扫码→跳小程序→一键认领,现场演示效果炸裂。 - 图像相似搜索:把图片向量存入 Milvus,实现“上传一张耳机图,自动给出相似失物”,直接变身 AI 项目。
8. 小结与思考
整个系统从 0 到答辩只花了 4 周,核心经验就一句:先让流程跑通,再让代码好看,最后才让功能膨胀。
如果你已经能顺利发布、认领、过期、通知一条龙,不妨想想:
“下一步,是不是给每个校区配一个柜子,再把扫码功能接上,让师弟师妹们毕业设计继续迭代?”
动手吧,柜子已经就位,就等你的下一行代码。