校园资源共享毕设项目实战:从需求分析到高可用架构落地
写在前面
去年指导学弟做毕设,题目是“校园资源共享平台”。原以为两周能收尾,结果从需求撕逼到上线演示,足足熬了两个月。踩坑无数,也攒下一套可直接套用的“轻量级高可用”模板。今天全部摊开,能 copy 的代码直接拿走,不能 copy 的思路至少让你少掉 200 根头发。
一、需求澄清:把“伪需求”拍死在黑板
资源冲突
同一把“西门子示波器”被 3 个人预约到同一时段,老师却只看演示结果——冲突现场=翻车现场。
→ 必须引入“时段独占锁”。用户权限混乱
学生、教师、管理员都能登录,但看到的功能完全不同。
→ RBAC 模型(Role-Based Access Control)+ JWT 无状态鉴权,省掉 Session 共享烦恼。文件存储随意
早期把图片直接扔在project/upload,Git 一提交,仓库 2 GB,CI 直接爆炸。
→ 对象存储(MinIO)+ 统一 CDN 域名,仓库只存代码。
二、技术选型:别让“过度设计”拖垮进度
| 维度 | 候选方案 | 结论 | 理由 |
|---|---|---|---|
| 主数据库 | MySQL vs SQLite | MySQL | 并发写>20 时 SQLite 锁整个文件,演示现场必卡 |
| 缓存 | Redis vs 本地 Map | Redis | 预约锁需进程级共享,本地 Map 在多实例下失效 |
| 文件存储 | 本地磁盘 vs MinIO | MinIO | 本地盘在 Docker 重启后路径漂移,MinIO 兼容 S3,迁云零改造 |
| 前端框架 | Vue2 vs Vue3 | Vue3 | 组合式 API 让表格分页逻辑更清爽,build 体积降 30% |
| 部署包 | jar 单文件 vs Docker | Docker | 机房服务器只有 Ubuntu 18.04,Dockerfile 一把梭,环境差异归零 |
三、架构总览:一张图先建立“上帝视角”
- 网关层:Nginx 反向代理 + HTTPS(Let's Encrypt 免费证书)
- 业务层:Spring Boot 3.x,内嵌 Tomcat,端口 8080
- 数据层:MySQL 8.0 主库 + Redis 7.0 缓存
- 存储层:MinIO 单节点(生产可扩 MinIO 集群)
- 前端:Vue3 静态文件托管在 Nginx,与后端同域,避免 CORS 额外配置
四、核心模块拆解与代码级实现
4.1 资源发布:统一 DTO + 参数校验
// ResourceDTO.java public record ResourceDTO( @NotBlank(message = "名称不能为空") @Length(max = 50) String name, @NotNull @Min(1) Integer totalQty, MultipartFile image ) {}- 用
record省掉 Getter/Setter,DTO 只负责“穿数据”,不做业务。 - 文件字段单独放 DTO,否则
@RequestBody无法解析multipart/form-data。
4.2 预约锁机制:Redis 分布式锁 + 幂等 Token
- 用户点击“预约”→前端带
uuid作为幂等键 - 后端执行 Lua 脚本,保证“查询-占坑-写记录”原子性
// ReservationService.java public String reserve(ReserveCommand cmd) { String key = "lock:res:" + cmd.getResourceId() + ":" + cmd.getStartTime(); String val = Thread.currentThread().getName() + ":" + System.nanoTime(); Boolean ok = redisTemplate.execute( RedisScript.of("if redis.call('exists',KEYS[1])==0 then " + "redis.call('set',KEYS[1],ARGV[1],'ex',ARGV[2]); return 1; else return 0; end"), List.of(key), val, "30"); if (!Boolean.TRUE.equals(ok)) { throw new BizException("时段已被抢占"); } try { reservationMapper.insert(cmd); return cmd.getId(); } finally { if (val.equals(redisTemplate.opsForValue().get(key))) { redisTemplate.delete(key); // 仅自己删自己的锁 } } }- 过期时间 30 秒,防止业务宕机留下死锁。
finally块做“谁加锁谁释放”,避免 A 用户释放 B 用户的锁。
4.3 JWT 无状态鉴权:双 Token(Access + Refresh)
# application.yml jwt: access-expire: 15 # 分钟 refresh-expire: 1440 # 分钟 = 1 天// JwtFilter.java if (token != null && jwtUtil.validate(token)) { UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( jwtUtil.getUserId(token), null, List.of(new SimpleGrantedAuthority("ROLE_" + role))); SecurityContextHolder.getContext().setAuthentication(auth); }- Access Token 存内存,Refresh Token 存 HttpOnly Cookie,XSS 偷不走。
- 用户角色放在
Authentication里,方法级安全直接用@PreAuthorize("hasRole('ADMIN')")。
4.4 文件上传:MinIO 客户端封装
// MinioService.java public String upload(MultipartFile file) { String bucket = "resource"; String objName = UUIDUUID() + "-" + file.getOriginalFilename(); try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucket) .object(objName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return "https://cdn.example.com/" + bucket + "/" + objName; } catch (Exception e) { throw new BizException("上传失败"); } }- 返回 CDN 地址,前端直接
<img :src="url">,后端磁盘零压力。 - 上传前检查 ContentType,禁止
text/html,防止 XSS 挂马。
五、性能 & 安全:把“万一”当“一定”来防
重复提交
前端按钮置灰 + 后端幂等 Token(见 4.2),30 秒内同一uuid只能成功一次。XSS 过滤
使用jsoup的Whitelist.basic()清洗富文本,JSON 返回统一Content-Type: application/json,杜绝“意外解析”。冷启动优化
Spring Boot 3 原生 AOT 编译后启动 1.8 s → 0.9 s;把 MinIO、MySQL 连接预热放在CommandLineRunner,演示现场不尴尬。接口防刷
基于 Redis 的令牌桶,每秒 10 次,超限返回 429,IP+用户双维度。
六、生产环境踩坑实录
| 坑 | 现象 | 根因 | 解决 |
|---|---|---|---|
| CORS 配置遗漏 | 前端 404 不报错,但数据拿不到 | Nginx 与后端端口不同,浏览器预检失败 | 在application.yml打开spring.web.cors.allowed-origin-patterns=https://*example.com |
| 静态资源 404 | 打包后图标空白 | Vue 默认baseUrl="/",部署到子目录/share/ | 设置vite.config.ts→base: '/share/',并同步修改 Nginxlocation |
| 数据库连接池耗尽 | 压测 50 并发直接 500 | Hikari 默认最大 10,演示当天忘记改 | 生产配置maximum-pool-size=cpu*2+1,并打开leak-detection-threshold=5000 |
七、部署脚本:一条命令跑起来
# 克隆即运行 git clone https://github.com/yourname/campus-share.git cd campus-share cp .env.example .env docker-compose up -d # 访问 https://localhost:8443docker-compose.yml里已经把 MySQL、Redis、MinIO、Spring Boot 编排好,端口无冲突。- 首次启动自动执行
flyway migrate,建表+初始数据,毕设演示零手动。
八、可继续折腾的脑洞
多校区共享网络
把 MinIO 升级成“多站点纠删码”,MySQL 用 Group Replication,Redis 上 Cluster,不同校区就近访问最近节点,流量用 DNS 分地域解析。微信小程序接入
后端接口无需改动,只新增wx.login获取openid,把openid与本地user_id做绑定即可。前端用 Taro 或 uni-app 一套代码同时发 H5 + 小程序,毕设加分项到手。
九、小结
整套代码从 0 到 1 大概 3k 行,核心就三句话:
“锁”用 Redis,“文件”用 MinIO,“权限”用 JWT。
剩下的都是体力活——把异常打印清楚、把日志切成文件、把 Docker 镜像压到 100 MB 以内。
如果你已经跑通本文的docker-compose,不妨试着把“多校区”或“小程序”任选一个做下去,下次站在答辩 PPT 前,你会感谢今天多写的这一行注释。