ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot+Vue广场舞团信息系统毕设开发复盘

Spring Boot+Vue广场舞团信息系统毕设开发复盘 广场舞团信息系统这个课题是我第大三下学期定下的毕设方向。当时身边不少同学都选了图书管理、学生考勤这类经典题目说实话已经有点审美疲劳了——答辩时十个人里有六个都是这些选题评委问的问题翻来覆去就那几个很难出彩。而我选广场舞团一方面是觉得这个场景足够真实另一方面是它确实能串起Spring Boot后端和Vue前端的大部分核心技术点。等你真的把需求拆开就会发现广场舞团管理绝不只是“增删改查”那么轻巧它背后涉及团队招募、活动报名、视频教学资源播放、动态权限菜单这些有分量的模块做完之后无论对技术成长还是论文工作量都有实打实的帮助。这篇内容我打算按一个完整的毕设项目开发闭环来复盘——从需求拆解、技术选型、表结构设计、后端链路、前端难点到联调阶段踩过的坑再到答辩准备把真正有用的东西都梳理出来。如果你也准备做“XX管理信息系统”这类Java全栈毕设或者正在纠结Spring Boot和Vue之间那些版本、部署、跨域问题这篇文章应该能帮你省下不少时间。1. 广场舞团的信息化需求拆解这个系统到底在管什么1.1 从“队长手机通讯录”到“数字化团队管理”很多人在定这个选题时第一反应是“广场舞不就是放个音乐大家一起跳吗有什么好管理的”。但真正去调研之后你会发现广场舞团队的日常管理痛点相当突出。一个典型的中型广场舞团通常有一位队长、一两位副队还有几十名到上百名不等的队员。队伍的日常工作包括招募新队员和队员资料登记发布每周排练计划组织演出报名和现场签到管理和分发舞蹈教学视频转发社区的演出通知和比赛信息记录器材、服装、场地等资源在没有信息系统的情况下这些工作基本靠微信群聊和队长的备忘录。群聊消息刷新快报名接龙动不动就被其他闲聊冲散队员资料散落在一张张Excel表格里数据格式五花八门舞蹈教学视频通过微信发到群里几天后过期无法查看新队员想自学又没有渠道。我的毕设项目正文里最终把业务模块收敛成这几个方向队伍管理、队员管理、活动管理、视频资源管理、公告资讯和个人中心。系统用户分成三类角色普通队员、团队管理员、系统管理员。普通队员可以浏览团队、申请加入、查看活动日历、报名活动、观看教学视频、查看个人报名记录团队管理员负责审核入团申请、发布活动和公告、上传排练视频、统计队员考勤系统管理员则做全局的账号处置和团队审核。这样每个角色都有明确的操作边界后面做权限设计就有了清晰的切入点。1.2 核心业务闭环从创建团队到沉淀风采一个系统如果只是把“增删改查”列出来那叫功能清单不叫业务分析。我在需求阶段给自己定了一条主线描述整个系统如何支撑一支舞团的完整生命周期队长注册账号后创建队伍系统生成团队主页和专属队员招募信息普通用户看到招募信息后提交入团申请队长审核通过后该用户变成团队成员队长定期发布排练活动和演出通知队员在线报名活动当天管理员在后台记录实际到场人数每一次排练和演出的视频、照片由管理员上传到视频资源模块按“曲目”和“舞蹈类型”分类存储队员可以随时在线播放学习活动过程和团队动态以公告形式推送给全体成员形成团队文化沉淀。这个业务闭环一旦跑通每个模块之间的关联关系就非常清楚。比如“报名”这个功能它的数据来源是“活动”但实际操作者是“队员”并且要防止同一队员重复报名同一场活动。设计表结构时这些关联关系直接决定了外键逻辑和唯一索引的位置。我当时把这条闭环画在论文的需求分析章节答辩时评委一眼就能看懂系统的价值所在这比堆功能点有效得多。2. 技术选型Spring Boot版本、Vue版本、对象存储方案怎么定2.1 最终选型一览组合背后的现实理由我的项目最终选型如下表这个组合在2024年左右做毕设属于比较主流又稳妥的搭配技术点选型版本说明后端基础框架Spring Boot2.7.18国内资料多生态成熟ORM框架MyBatis-Plus3.5.x系列单表CRUD基本免写SQL安全鉴权Spring Security JWT学习成本相对可控数据库MySQL 8.0主力业务数据存储缓存Redis存放验证码、热点数据缓存对象存储Minio存放视频、图片等大文件前端框架Vue 3 Vite组合式API开发构建速度快UI组件库Element PlusVue 3的官方适配组件前端状态管理PiniaVue 3推荐方案HTTP请求Axios统一拦截器处理Token和错误提示视频播放hls.js免安装播放m3u8格式流媒体先说后端版本的选择。Spring Boot 2.7.18和Spring Boot 3.x是一个让人纠结的点。Spring Boot 3要求JDK 17以上并且把javax包迁移到了jakartaMyBatis-Plus这类第三方库在3.0初期版本的兼容性并不好。我最后选2.7.18而不是最新版主要原因是成熟稳定、网上踩坑帖子多。如果你的指导老师没有强制要求用新版本Spring Boot 2.7系列配合JDK 1.8或者JDK 11是容错率最高的组合。前端Vue 3和Vite的组合目前已经是新项目的默认选择。Vite的开发服务器启动速度和热更新体验比Webpack好太多用一次就回不去了。但要注意Vue 3带来的Composition API写法、defineProps/defineEmits宏、Teleport等新特性如果你平时看的是Vue 2教程初上手会有明显的适应期。我当时专门花了一周时间把Vue 3的响应式原理和新语法从头过了一遍这个时间花得值后面写业务代码时基本没有因为语法问题卡壳。2.2 为什么用Minio而不是直接存服务器本地项目里有一块“视频教学资源”功能这决定了它不能像纯文本管理系统那样只操作数据库。广场舞教学视频通常是一段几分钟到十几分钟的MP4文件一个文件动辄几十MB甚至上百MB。如果直接上传到后端服务器本地目录问题接踵而至打包成JAR后上传路径的绝对依赖会让你在服务器上找文件找到怀疑人生视频请求需要通过Tomcat额外配置静态资源映射开发时能用部署后路径一变就容易出Bug后期如果换服务器或者做负载均衡文件分散在多台机器上管理难度陡增所以我引入了Minio来做对象存储。它是一个开源的轻量级对象存储服务兼容亚马逊S3协议单机版部署非常简单下载一个二进制文件就能启动默认端口9000。在Spring Boot端我们通过Minio的Java SDK完成初始化客户端、上传文件、生成访问链接这些操作。视频文件先上传到Minio再把存储后的访问路径保存到MySQL的video表中业务逻辑和存储位置彻底解耦。这样本地开发和服务器部署的环境差异会小很多迁移数据也只是一桶一桶的视频对象整体备份。顺带一提“Minio加入到SpringBoot”这个点是很多人在热搜里找的实际集成核心步骤就三步引入minio依赖、配置客户端连接参数、封装一个上传服务。后面我会在第四章详细展开代码。3. 数据库表设计每张表的字段都是为什么而存在3.1 核心表结构总览我最终设计的核心表一共九张它们之间不是孤岛而是围绕“队伍—成员—活动”这条主线展开的。简单列一下表名用途核心字段user系统用户id, username, password, nickname, phone, avatar, roleteam广场舞团队id, name, intro, logo, leader_id, statusteam_member团队成员关系id, team_id, user_id, status(申请中/已通过/已拒绝), create_timeactivity排练/演出活动id, team_id, title, content, activity_time, location, max_peopleactivity_signup活动报名记录id, activity_id, user_id, sign_status, sign_timeresource_video教学视频资源id, title, type, cover_url, video_url, duration, uploader_idnotice团队公告通知id, team_id, title, content, publish_timeuser_favorite收藏关系id, user_id, video_iddict_type / dict_data数据字典用于维护舞蹈类型、视频分类等枚举项注意team_member和activity_signup这两张表它们都属于典型的“关系表”承载的是多对多关系还带了业务状态字段。比如team_member表的status字段并不是业务层面必需的但它恰好是“入团审批流程”的数据支撑。用户提交申请时插入一行status0的记录队长审批通过之后把status更新成1这样可以完整保留审批历史而不是等到审批时才临时建关系。3.2 几个必须提前想清楚的字段设计细节这里我想单独说几个容易踩坑的设计决策。第一个是主键策略。MyBatis-Plus默认的ASSIGN_ID生成的雪花ID是Long类型我全部使用了Long主键而不是数据库自增ID。理由有两个一是避免在分布式场景下主键冲突的隐患虽然毕设单机也能跑但这是一个好习惯二是前端JavaScript处理Long类型精度问题后端在返回JSON时需要对ID做数字转字符串处理或者用BigInt接收。我当时是在后端全局配置了Jackson的Long序列化转换为String这个细节不注意列表页点击编辑时ID精度丢失会导致查不到记录属于隐藏较深的坑。第二个是时间字段。所有时间字段统一使用datetime类型包括报名时间、创建时间、活动时间。千万不要用varchar存时间看似输入方便但后续按时间范围筛选、排序、统计时各种恶心的类型转换会让你崩溃。实体类里我用LocalDateTime接收配合MyBatis-Plus的自动填充功能在插入和更新时自动写入create_time和update_time不用在每个Service里手工set省了一堆重复代码。第三个是视频表的设计。resource_video表里我冗余了cover_url和video_url两个字段。为什么要冗余一个封面图因为个人中心的收藏列表、团队风采展示页都需要展示视频卡片如果没有封面字段前端就要额外拉取视频流再截帧性能很差。管理员上传视频时后端统一用ffmpeg截取一帧画面作为默认封面再上传到Minio前端拿到的URL直接显示在列表里体验好得多。3.3 一个典型的建表SQL参考拿activity_signup表举例建表语句如下CREATE TABLE activity_signup ( id bigint(20) NOT NULL COMMENT 主键, activity_id bigint(20) NOT NULL COMMENT 活动ID, user_id bigint(20) NOT NULL COMMENT 报名用户ID, sign_status tinyint(4) NOT NULL DEFAULT 1 COMMENT 报名状态1正常 2取消, sign_time datetime NOT NULL COMMENT 报名时间, PRIMARY KEY (id), UNIQUE KEY uk_activity_user (activity_id,user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动报名记录表;这里最关键的是一行UNIQUE KEY uk_activity_user (activity_id, user_id)。我在设计时就预测到“重复报名”是最容易出现的业务问题这行唯一索引在数据库层面直接兜底。哪怕前端按钮被连点了十次后端没做好幂等数据库也会因为唯一索引冲突抛出异常。后端再在Service层捕获这个唯一键冲突异常返回“你已报名该活动”的友好提示双重保险。4. 后端实现的核心链路登录鉴权、文件上传、视频播放的落地方式4.1 JWT登录鉴权与权限控制的完整流程登录鉴权我选择了Spring Security JWT的组合。听起来高大上实际操作起来核心链路并不复杂用户提交用户名密码后端用BCryptPasswordEncoder比对密码比对通过后生成一个JWT Token返回给前端前端把Token存到localStorage里之后每次请求都通过Axios拦截器带上Authorization头后端自定义一个JwtAuthenticationFilter继承OncePerRequestFilter在请求进Controller之前解析Token并塞入当前登录用户信息。这里有几个细节值得展开说。Spring Security的默认行为是拦截所有请求所以需要在SecurityConfig配置类里放行注册接口、登录接口和部分公共查询接口。我当时放行的路径包括/api/user/login、/api/user/register、/api/team/list、/api/video/public/**以及/api/file/**——因为资源视频的预览图也要在未登录状态下可见否则首页展示不出封面图。JWT的有效期我设置为2小时。2小时其实偏短用户挂着页面超过2小时就会突然登录失效体验不好但设置过长又有安全风险。我的解决方式是在前端请求拦截器里判断状态码401如果Token过期自动用localStorage里保存的“refreshToken”调一次刷新接口拿到新Token后重新发起刚才失败的请求。这个刷新逻辑让页面在主动操作时不会突然跳登录页。当然这个方案的接受程度因人而异不少教学项目都是过期跳登录页但如果你写到答辩里提到“我的系统支持Token自动续期”评委通常会眼前一亮。密码安全也是一个常见的答辩追问点。我数据库里存的是BCrypt加密的散列值而不是明文密码。BCrypt的一个特点是每次加密同一明文得到的散列值都不同这天然抵御了彩虹表攻击。Spring Boot的Spring Security Crypto模块自带了BCryptPasswordEncoder用起来就是new BCryptPasswordEncoder().encode(password)和.matches(rawPassword, encodedPassword)两个方法的事。4.2 Minio文件上传和hls.js视频播放m3u8免安装方案项目正文里有一大块视频资源模块“上传视频 → 转码切片 → 在线播放”这条链路是我觉得整个项目里技术含量最高的部分。首先引入Minio依赖dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency然后在 application.yml 里配置连接信息minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: dance-video后端封装一个工具类Component public class MinioTemplate { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Value(${minio.bucket}) private String bucket; private MinioClient minioClient; PostConstruct public void init() { minioClient MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } public String upload(MultipartFile file, String objectName) throws Exception { if (!bucketExists(bucket)) { makeBucket(bucket); } minioClient.putObject(PutObjectArgs.builder() .bucket(bucket) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint / bucket / objectName; } }视频播放这里要单独拎出来讲。原始MP4文件有几MB到几十MB不等直接拉到前端播放加载慢不说用户拖动进度条时往往要缓冲半天。主流做法是用ffmpeg把MP4转成HLSHTTP Live Streaming格式也就是把视频切片成大量.ts小文件再生成一个.m3u8索引文件。播放器通过解析m3u8文件来按需加载对应时间段的切片实现边下边播体验流畅很多。这个转换要在后端完成因为浏览器端做不了视频转码。我在后端是这样处理的管理员上传视频后先把原始MP4存到Minio临时目录然后调用服务器上已安装的ffmpeg命令把视频转换为HLS切片格式输出文件再上传到Minio的video目录下ffmpeg -i input.mp4 -c:v libx264 -c:a aac -hls_time 10 -hls_playlist_type vod -hls_segment_filename output_%03d.ts output.m3u8这条命令把视频切成每段10秒的ts切片生成output.m3u8索引。然后再把生成的m3u8和ts文件批量上传到Minio。前端播放就简单了。Vue项目里我引入了hls.js封装了一个视频播放器组件。原生video标签是无法直接播放m3u8的但hls.js在浏览器端解析m3u8索引并把ts切片喂给video元素根本不需要用户安装任何播放器或者浏览器插件。核心代码如下import Hls from hls.js function playM3u8(videoEl, src) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(src) hls.attachMedia(videoEl) hls.on(Hls.Events.MANIFEST_PARSED, () videoEl.play()) } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src src videoEl.play() } }这段兼容了iOS Safari原生播放m3u8的情况因为iOS上不经过hls.js也能直接播。Vue组件销毁时记得调用hls.destroy()释放资源否则多个播放器实例来回切换会内存泄漏。4.3 后端业务接口的骨架与统一返回格式我所有的Controller返回类型都统一为ResultT里面包含code、message和data三个字段。成功时code为200业务异常时code为自定义错误码例如30001表示重复报名、30002表示无权操作。这样的好处是前端Axios拦截器只需要判断code不用每个接口单独写success判断逻辑。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.code code; r.message msg; return r; } }业务接口本身用MyBatis-Plus的IService简化了不少。比如分页查询队伍列表PageTeam page new Page(current, size); LambdaQueryWrapperTeam wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(keyword), Team::getName, keyword); wrapper.eq(Team::getStatus, 1); wrapper.orderByDesc(Team::getCreateTime); teamService.page(page, wrapper);这种写法没有手写一行SQL复杂查询再用Select注解补充。MyBatis-Plus提供的LambdaQueryWrapper类型安全且代码可读性高Spring Boot项目开发效率比传统MyBatis XML方式高不少。5. 前端Vue实现中的几个硬骨头路由、播放器、请求封装5.1 动态路由与前端权限控制我的系统有三种角色如果每个角色进来看到的菜单都一样那权限设计就形同虚设。这里我用的是动态路由方案。用户登录后后端返回当前用户的角色编码前端根据角色编码动态生成可访问的路由表通过router.addRoute()注册到Vue Router中。具体流程是这样的路由分成两部分静态路由和动态路由。静态路由只包含登录页、注册页 commons 的404页面登录成功后把用户的角色信息存到Pinia里在路由全局前置守卫中如果当前用户已登录但动态路由尚未注册就调用后端菜单接口获取该角色的菜单权限清单前端根据权限清单找到对应的Vue组件路径动态构造RouteRecordRaw数组逐个router.addRoute(routeItem)注册完成后调用next({ ...to, replace: true })重新进入目标路由// 动态路由示例 function buildRoutes(menus) { const routes [] menus.forEach(menu { routes.push({ path: menu.path, name: menu.name, component: () import(/views/${menu.component}.vue), meta: { title: menu.title, icon: menu.icon } }) }) return routes }特别注意component: () import(...)中的路径不能是纯动态拼接的变量否则Vite的构建工具无法静态分析出这个文件的依赖关系。我的做法是维护一个组件映射表把后端传来的component字符串映射到真正import的组件对象上这样既灵活又不会打包失败const viewMap { TeamManage: () import(/views/team/TeamManage.vue), ActivityManage: () import(/views/activity/ActivityManage.vue), VideoManage: () import(/views/video/VideoManage.vue), MemberList: () import(/views/member/MemberList.vue) }动态路由的核心价值是权限可控同时也是毕设答辩的一个亮点前端展示的菜单严格依赖后端下发的权限数据而不是写死在代码里。5.2 Vue播放m3u8hls.js封装与组件复用上一章讲了后端生成m3u8的过程前端这里再补充一个封装细节。我在components/VideoPlayer.vue里做了三件事接收两个propssrcm3u8地址和poster封面图地址watch到src变化就销毁旧实例并重新初始化播放器在onUnmounted钩子中销毁Hls实例template div classvideo-player-wrapper video refvideoRef :posterposter controls loop playsinline/video /div /template script setup import Hls from hls.js import { ref, onUnmounted, watch } from vue const props defineProps({ src: String, poster: String }) const videoRef ref(null) let hls null function initPlayer() { if (!props.src || !videoRef.value) return if (hls) { hls.destroy() hls null } const video videoRef.value if (Hls.isSupported()) { hls new Hls() hls.loadSource(props.src) hls.attachMedia(video) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src props.src } } watch(() props.src, initPlayer) onUnmounted(() { if (hls) hls.destroy() }) /script这个组件被封进VideoPlayer后在队伍风采页、视频列表页可以重复使用。这里踩过的坑有两个。第一个是Vue路由切换时如果VideoPlayer组件被销毁了但Hls实例没有destroy控制台会报“Cannot read property destroy of null”之类的问题页面切换多了还会卡顿。第二个是局域网IP访问和本地访问的问题Minio服务监听的是9000端口前端如果通过localhost后端转发到Minio文件地址播放器请求时可以把localhost换成后端服务器IP才能通部署时这个细节特别容易遗漏。5.3 Axios请求拦截、Token注入与开发环境跨域Axios在前端的重要性不用多言我几乎每个页面都要用到它。项目里我统一在utils/request.js中封装了axios实例并配置了请求拦截器和响应拦截器。const service axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) service.interceptors.request.use(config { const user useUserStore() if (user.token) { config.headers.Authorization Bearer ${user.token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { const user useUserStore() user.clearToken() router.push(/login) } ElMessage.error(error.message || 请求失败) return Promise.reject(error) } )开发环境跨域问题是Vue前端和Spring Boot后端分端口跑的必考题。Vue开发服务器跑在5173端口后端接口跑在8080端口如果不做处理浏览器会拦截跨域请求。我的做法是直接在Vite配置里加转发配置让前端的/api请求转发到后端服务// vite.config.js server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这样前端代码里所有请求都写成/api/xxx开发时走转发打包后部署时交给Nginx做同样的转发处理前后端接口路径完全一致不需要在环境变量里改来改去。6. 联调阶段最典型的三个事故完整排查链路复盘6.1 事故一上传视频后前端拿到的播放地址404这是我在做视频模块时遇到的最磨人的问题。现象是后端接口正常返回了视频URL但前端把这个URL放到video标签里播放器直接报404视频永远加载不出来。我当时的排查链路是这样的先看后端返回的URL格式确认是http://127.0.0.1:9000/dance-video/xxx.m3u8这样的地址在浏览器直接访问这个URL返回的xml错误提示里写着AccessDenied这时候我第一反应是Minio的Bucket权限问题Minio新建的Bucket默认是私有的不允许匿名访问。到Minio控制台给bucket加上download策略问题就解决了但后面在打包部署时又遇到一次404情况复杂一些。服务器上Minio返回的URL用的是服务器内网IP前端浏览器访问不到。这个问题本质上是把Minio的endpoint写死成了内网地址而前端页面在另外一台机器上访问。解决方式是让上传时返回的URL统一使用对外可访问的域名加Bucket路径或者通过Nginx对9000端口做一层地址映射。所以如果你的Minio和前端不在同一台机器或同一个内网里一定要提前确认endpoint地址是前端浏览器可达的。还有一次404就比较弱智了是Minio工具类里upload方法拼接URL时少写了一个斜杠拼出了dance-videofile.m3u8这样的错误路径。这种问题靠眼睛很难看出来我最后是在数据库里对比正常记录和异常记录才发现的。6.2 事故二Vue项目打包后页面刷新404这是所有Vue Router使用history模式的玩家都会踩的坑。我在本地开发时一切正常打包后用npm run build生成dist目录把dist扔到服务器上点击页面内部的跳转没问题但只要按F5刷新或直接访问某条子路由服务器就会返回404。排查思路是先确认这不是前端代码的问题。前端打包出来的index.html是在服务器某个静态目录下当用户访问/team/list时服务器在这个路径下找不到对应的物理文件自然就404了。我又不能用hash路由——虽然能解决刷新404但URL里带#很丑而且没法接微信分享链接。最后的解决方式是在Nginx站点配置里加一段地址重定向规则把所有非静态文件的请求都落到index.html上server { listen 80; server_name your.domain.com; root /path/to/dist; index index.html; location / { try_files $uri $uri/ /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; } }注意try_files $uri $uri/ /index.html这段它表示先尝试按当前URI查找静态文件找不到就返回index.html由前端路由接管页面展示。这一步做完刷新404的问题就彻底消失了而且顺带把/api/路径的请求转发到后端8080端口前后端部署到同一台服务器上也变得很干净。6.3 事故三登录接口报跨域登录成功后又循环跳转登录页这个问题的排查链路更有意思。现象是最开始从Vue页面调/api/user/login控制台报CORS错误浏览器拦截了响应。我的第一反应是在后端加一个CorsFilter配置允许跨域。加上之后登录能通了但登录成功后跳到首页还没待两秒又被踢回登录页来回循环。定位过程是这样的先看网络请求发现很多接口返回401再看请求头发现这些请求根本没带上Authorization字段回头看登录流程发现前端把Token存到了Pinia里但页面刷新后Pinia里的数据会重置用户信息丢失token也没了进一步定位发现我没有做Pinia的持久化也没有在刷新时重新从localStorage恢复状态根因找到了。解决方案是在Pinia的store里做好持久化最简单的做法是手动在store定义里读取localStorage初始值状态变化时同步写入export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , nickname: localStorage.getItem(nickname) || , role: localStorage.getItem(role) || }), actions: { setLoginInfo(data) { this.token data.token this.nickname data.nickname this.role data.role localStorage.setItem(token, data.token) localStorage.setItem(nickname, data.nickname) localStorage.setItem(role, data.role) }, clearToken() { this.token this.nickname this.role localStorage.clear() } } })至于那个后端CorsFilter后期我改成Vite转发和Nginx转发之后后端跨域配置其实已经不需要了留着也不影响但真正的生产环境没有必要开放全站CORS。这个事故让我总结出一个排查原则登录相关的问题先不要怀疑后端先看Token是不是真的送到请求头里了。前端状态管理在刷新后丢失Token是比后端接口出错更高发的坑。7. 答辩准备与后续扩展毕设交付之外的加分项7.1 我准备的答辩问题清单答辩老师通常不会把代码从头到尾看一遍他们更习惯从核心场景切入问技术选型的理由和实现细节。我根据自己的项目内容整理出了一份高频问题清单为什么选Spring Boot 2.7而不是3.x答生态兼容性和稳定优先同时了解3.x的改动点JWT和传统Session有什么区别答无状态、适合前后端分离、天然适配多端登录如果活动报名并发量很大你怎么防止超卖答数据库唯一索引兜底 前端请求幂等 必要时用Redis分布式锁Minio和阿里云OSS有什么区别答Minio是开源自建的S3协议实现数据自主可控短期零成本视频转m3u8切片是什么思路答ffmpeg转码HLS协议播放端按需拉切片降低首屏加载压力Spring Boot自动装配原理了解吗答EnableAutoConfiguration加载spring.factories中配置的自动配置类配合条件注解按需生效尤其最后一个问题Spring Boot自动装配几乎是Java开发面试必考考点。我在答辩前专门用一条主线把这个问题理清楚了Spring Boot启动时通过SpringFactoriesLoader加载META-INF/spring.factories文件里的自动配置类列表再结合ConditionalOnClass、ConditionalOnMissingBean这类条件注解决定哪些配置真正生效。你项目里用到了MySQL、Redis、Minio这些starter每个starter本质上都是这样一个带有Spring Boot自动装配配置的jar包。把这个逻辑想明白不光是项目里能用面Java开发岗时也更稳。7.2 如果时间充裕可以继续扩展的方向毕设验收完之后我其实还想给系统加几个扩展点它们对能力的提升比很多基础CRUD大很多。第一个是数据可视化报表。现在系统里积累的活动报名数据、队员考勤数据都是原始记录如果能引入ECharts做一个管理端数据看板展示每周活动次数、报名趋势、舞蹈类型分布整个系统就更有“信息化”的味道了论文里的应用效果展示也多了一张图。第二个是消息推送给队员。活动提醒如果只依赖网页公告栏队员不登录就看不着。可以扩展一个消息中心站内信加简单提醒或者对接企业微信机器人把活动通知推到群里。这一块虽然技术上不难但商业价值上非常贴近真实场景。第三个是展厅场景里的视频推荐。视频资源多了之后用简单的内容标签匹配就可以实现“相似舞蹈推荐”不需要复杂的机器学习算法一个基于标签相似度的推荐排序就够毕设用了。第四个是移动端适配。现在前端是PC端的管理后台形态广场舞队员很多习惯用手机做一套移动端H5页面或者直接考虑小程序方案系统覆盖人群会广很多。这些扩展方向不用全做挑一个做深做透在答辩时就能额外展示你的思考能力和工程视野。最后再分享一个实操小技巧。毕设项目中依赖版本号一定要固定住不要用RELEASE或者latest这种动态版本号。我的项目里Spring Boot 2.7.18搭配MyBatis-Plus 3.5.3.2这两个版本我确认过兼容性因为MyBatis-Plus新版偶尔会要求更高版本的Spring Boot内核对不上会报ClassNotFoundException。如果你用了高版本的Spring Boot 3还要注意javax与jakarta包名差异不少老教程里的代码会直接编译失败。这些都是我一遍遍踩出来的经验一句话总结就是做毕设稳定压倒一切能跑通到交付的那一刻比版本追新更重要。希望这篇复盘对你做同类Java全栈项目有实际帮助。
返回列表