Vue+Spring Boot全栈实战:青年大学习成果记录管理系统开发

📅 发布时间:2026/9/10 3:58:49
Vue+Spring Boot全栈实战:青年大学习成果记录管理系统开发
管理员催学习记录、学生交截图、统计报表做得头大这种事在高校里几乎每周都在重演。我这次做的“VueSpring Boot的青年大学习成果记录管理系统”说白了就是把“有没有看、看了多久、截图交没交、积分加没加”这条琐碎链路全部线上化让数据自己跑让人别再当人肉Excel。项目采用前后端分离架构前端用Vue 2 Element UI后端用Spring Boot 2.6 MyBatis-Plus数据库用的MySQL 8.0身份认证走JWT Redis学习进度记录里还兼容了m3u8/HLS视频流的播放与时长上报。对正在做毕业设计、课设或者想系统走一遍全栈项目从设计到落地的新人来说这篇文章会尽量把你可能踩的坑直接帮你踩掉从需求拆解到数据库建模从接口设计到前端页面再到联调部署每个环节都会给出实际能用的方案。1. 为什么选 Vue Spring Boot而不是其他组合1.1 这个系统到底要解决什么问题先别急着写代码得先搞清楚业务现状。以前学习任务下发之后最原始的统计模式就是管理员在群里丢一个链接大家看完之后截图发群班干部挨个保存再人工对着名单打钩。这个流程有三个痛点非常致命第一是截图很容易造假拿一张旧图或者别人的图顶替管理员根本看不出来第二是统计滞后到截止日期了还有一堆人没交班干部挨个催催完还得改Excel第三是数据无法沉淀这个月谁学了多少、哪个年级完成率高、哪几个班长期垫底完全没有一个历史数据仓库来做分析。这个系统要解决的就是这三件事学习过程可追溯、提交审核线上化、统计报表自动化。说白了它不是要取代学习本身而是把“学习之后那一堆记录、审核、统计的烂摊子”打包带走。学生在系统里看视频前端通过定时器把观看进度实时上报后端记录累计时长达到阈值自动标记为完成。学生再补充提交学习截图管理员在后台一键审核系统自动累计积分。所有数据落库之后ECharts看板直接出学院完成率、个人排行榜、历期趋势图。1.2 前后端分离架构的考量选前后端分离不是跟风是因为这个系统天然就是“多端展示、单点管理”的场景。学生用手机浏览器访问前端页面比较多管理员在电脑上操作后台将来如果要做微信小程序只需要复用后端API前端重新写一套就行。如果做成传统的Thymeleaf模板方案页面渲染逻辑和后端业务逻辑耦合在一起小程序端没法接接口也不好统计。考虑到系统里还要接入视频播放、进度上报、实时通知这类交互频繁的功能分离架构的在开发期和维护期的收益都非常明显。后端只负责纯API接收JSON请求、校验参数、查库、返回统一响应。前端只负责交互和渲染axios请求后端、Vue Router控制页面跳转、Vuex管理登录状态和用户信息。两边靠一套RESTful接口契约协作。契约定清楚之后前后端可以并行开发我这边后端还没写完前端朋友就可以拿Mock数据先做着。这个对人的时间利用率、项目的推进速度都有实际帮助。1.3 技术栈的横向对比先摆一下最终选的这套组合Spring Boot 2.6.13稳定的企业级版本依赖管理省心生态成熟遇到坑基本都能搜到解决方案。MyBatis-Plus 3.5.x单表CRUD不用写SQL内置分页插件代码量能少三分之一。复杂统计再用XML写原生SQL。MySQL 8.0免费、稳定满足这个业务量绰绰有余。Vue 2.6 Element UIElement UI对表格、表单、对话框、分页这类后台管理场景的支持非常顺手能快速搭出可用后台。JWT Redis登录后颁发tokentoken无状态存储用户权限信息放Redis做缓存退出或重置密码能主动失效。ECharts数据可视化报表。有人可能会问为什么不用Vue 3 Element Plus我统一解释一下Vue 3的组合式API确实更好用但很多应届生和课设场景里Vue 2 Element UI的资料量更大、坑更少涉及视频播放M3U8、路由权限、导出Excel这些常见需求时搜到的现成方案几乎都是Vue 2时代的。当然如果你熟悉Vue 3结构上完全可以平移核心的逻辑是不变的。后端的替代方案也有比如若依框架自带代码生成器开发速度更快但它封装太重很多初学者会迷失在框架的坑里反而不利于理解原理。自己从零搭一套Spring Boot项目虽然前期多花点时间但是每一个配置你都清楚为什么这么写。2. 需求拆解与数据库设计2.1 核心业务流程梳理这个系统有两种核心角色学生和管理员。学生看任务、看视频、上报进度、提交成果、看自己的积分。管理员管任务、管用户、审核记录、看统计报表。如果学校分学院层级还可以加一层院级管理员但初期角色太多了会撑大工作量我建议第一版只做两种角色通过用户表里的role字段来区分后面要扩展再改即可。核心流程里最值得细说的是“学习成果上报”这一条线整个状态机如下管理员发布新一期学习任务上传视频地址、设置学习截止时间系统生成一条learning_task记录。学生在前端看到当前任务点击“去学习”进入视频播放页。视频开始播放后前端每10秒向后端上报一次当前进度后端更新study_record里的watch_duration字段。播放进度超过视频总时长的90%系统自动把is_complete置为true。学生选择上传学习截图提交后进入待审核状态。管理员在审核列表里查看记录和截图点“通过”或“驳回”。通过后系统自动增加学生积分同时通过WebSocket给该学生推送一条“审核已通过”通知。这条链路里的关键点在于完成状态和审核状态必须是两个字段。一开始我偷懒用一个state字段表示“未学习、学习中、待审核、已通过”结果出现这种情况视频看到一半截图先交了或者视频看完了忘记提交状态没法准确表达。后来拆成两个字段才彻底清爽。经验就是一个字段只负责一个维度的状态别贪多。2.2 数据库表结构设计数据库设计是这个项目里决定上限的部分。表与表之间不能乱字段命名要有规矩主键统一用自增id或者雪花id。我用的是自增id因为并发量不大自增简单且排序索引效率高。下面把核心表的字段放出来都是可以直接落地的。学生用户表student字段名类型说明idbigint主键student_novarchar(32)学号登录账号唯一索引passwordvarchar(128)bcrypt加密后的密码namevarchar(32)姓名college_idbigint所属学院关联college表class_namevarchar(64)班级phonevarchar(20)手机号roletinyint0学生、1管理员statustinyint0正常、1停用create_timedatetime创建时间管理员表没有单独建我觉得就这个体量一张用户表用role字段区分就够了。如果系统要做得更规范或者管理员的字段和学生差异很大再拆一张sys_user表也不迟但先期别过度设计。学习任务表learning_task字段名类型说明idbigint主键task_novarchar(32)期数编号如“2024-01”titlevarchar(128)任务名称video_urlvarchar(255)视频地址兼容mp4、m3u8cover_urlvarchar(255)封面图durationint视频总时长单位秒start_timedatetime开始时间end_timedatetime截止时间statustinyint0草稿、1已发布、2已结束学习记录表study_record字段名类型说明idbigint主键student_idbigint学生idtask_idbigint任务idwatch_durationint已观看时长秒total_durationint视频总时长秒complete_flagtinyint0未完成、1已完成screenshot_urlvarchar(255)学习截图地址audit_statustinyint0待审核、1通过、2驳回audit_remarkvarchar(255)审核备注audit_timedatetime审核时间create_timedatetime首次上报时间update_timedatetime最后上报时间这张表是核心中的核心注意watch_duration是前端定时上报累加出来的不是后端自己计时。就是说用户暂停了、关页了后端就以最后一次上报的数据为准。还要注意同一对student_id和task_id只能存在一条主记录重复上报走的是更新逻辑而不是新增逻辑否则会产生大量垃圾数据。积分表score简单一点就是student_id、total_score、month_score、update_time。学习记录审核通过的接口里同时去更新这张表。为了性能审核通过是可以容忍偶尔丢一次积分的场景直接UPDATE也可以没必要用事务外层套太复杂。学院表collegecollege_name、sort_order。这张表在统计报表里会用到。通知表noticestudent_id、content、status0未读、1已读、create_time。WebSocket推通知之前先把消息落库前端断线重连后还能拉取到未读列表这个设计很重要纯走WebSocket内存消息用户刷新一下就丢了。2.3 统一响应体与接口风格后端接口我用了统一的Result 返回结构避免一半接口返回JSON对象、一半返回字符串前端处理起来很乱。结构如下{ code: 200, message: success, data: {} }code200表示成功401需要重新登录500业务或系统错误。业务异常我统一用了RestControllerAdvice做全局异常捕获主动抛出的业务异常可以自定义code但对外先统一用500即可。传参上GET请求用RequestParamPOST请求用RequestBody所有REST接口的路径都遵循资源式命名比如POST /api/auth/loginGET /api/task/currentPOST /api/study/recordPOST /api/study/record/uploadPUT /api/study/record/auditGET /api/stats/overviewGET /api/stats/college-rankingGET /api/stats/export统一响应体看起来是个很小的设计但联调的时候真的能省掉特别多沟通成本。前端axios响应拦截器里只需要判断code如果code401就统一弹出“登录已过期”并跳转登录页不需要每个页面单独写错误处理。3. 后端核心模块实现3.1 项目初始化与分层结构工程结构我是按常见的四层来组织的controller、service、mapper、entity。四层结构在项目初期看起来有点“重”但等业务逻辑复杂起来以后大家各归各位找代码不迷路。实际创建的包结构如下com.example.youthstudy ├── common // 统一响应、异常处理、常量 ├── config // 配置类如CorsConfig、WebSocketConfig、MybatisPlusConfig ├── controller // 接口层只做参数接收和结果返回 ├── entity // 数据库实体类 ├── mapper // MyBatis-Plus Mapper接口 ├── service // 业务逻辑层核心逻辑全在这 │ └── impl ├── utils // 工具类如JwtUtil、FileUploadUtil └── dto // 接收前端参数的传输对象避免直接用实体接收创建Spring Boot项目我用的是Spring Initializr依赖勾选了Spring Web、MyBatis-Plus这个从Maven中央仓库引入、MySQL Driver、Lombok、Validation。pom.xml里几个关键依赖需要注意版本对齐Spring Boot 2.6.13MyBatis-Plus 3.5.3.1注意这个版本需要Java 8以上jjwt 0.9.1JWT生成与解析hutool-all 5.8.x各种工具方法强烈推荐easyexcel 3.3.x导出Excel报表spring-boot-starter-websocket如果项目里要接Swagger接口文档springfox 3.0.0用在Spring Boot 2.6.x上会有一个著名的坑启动直接报空指针因为Spring Boot 2.6默认的路径匹配策略从AntPathMatcher换成了PathPatternMatcher。解决办法是在application.yml里加一行spring: mvc: pathmatch: matching-strategy: ant_path_matcher这行配置不加Swagger一启动就挂。如果你不用Swagger而是用Apifox或Knife4j也同样要关注这个匹配策略问题。这是后端环境配置里最容易卡的坑之一。3.2 JWT登录认证与权限控制登录接口的逻辑学生输入学号和密码后端查库校验通过则用用户ID、角色生成JWT token返回给前端同时把用户基本信息放入Rediskey是tokenvalue是用户对象过期时间设为24小时。为什么JWT已经无状态了还要存Redis因为系统里要支持“管理员强制用户下线”的场景只靠JWT的过期时间是做不到的。Redis里存一份后台改用户状态或者重置密码的时候把对应的token删了立刻就能让那个token失效。JWT的工具类我用jjwt封装生成token时设置subject为用户id放入role角色和过期时间。核心代码大致是这个逻辑public String generateToken(Long userId, Integer role) { MapString, Object claims new HashMap(); claims.put(role, role); return Jwts.builder() .setClaims(claims) .setSubject(String.valueOf(userId)) .setExpiration(new Date(System.currentTimeMillis() 86400000)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); }注意SECRET_KEY不能写死在代码里放到application.yml的配置项中部署的时候通过环境变量覆盖。接口鉴权我用了一个拦截器HandlerInterceptor对所有/api/**请求做拦截白名单放行/api/auth/login。拦截器里从请求头拿到Authorization解析token校验通过就把userId和role放到ThreadLocal里业务层可以直接获取当前登录用户。这个ThreadLocal用完记得remove否则Tomcat线程池复用会串数据。只做登录拦截还不够管理员接口和学生接口还得分开。我用自定义注解RequireRole(role 1)来实现在拦截器里判断注解上要求的角色和当前用户角色是否匹配不匹配直接返回403。如果没有这个设计学生登录后直接构造一个/api/stats/export请求就能把全学院的报表下载走这是严重越权漏洞必须堵死。3.3 学习进度上报与审核接口学习进度上报是系统里最频繁的写操作学生播放视频期间每10秒打一次点。这个接口需要注意的地方是不能让学生随便传一个watch_duration999999就把任务刷完成。后端的校验逻辑是前端每10秒上报一次当前进度点后端只记录进度点的最大值并且这个值不能超过视频总时长的100%。同时后端还要判断两次上报时间间隔如果间隔小于8秒认为是异常请求直接忽略更新。这样能在不搞太复杂的风控前提下挡住大部分低成本刷数据。核心更新接口service层大致逻辑StudyRecord record studyRecordMapper.selectOne( new LambdaQueryWrapperStudyRecord() .eq(StudyRecord::getStudentId, studentId) .eq(StudyRecord::getTaskId, taskId)); if (record null) { // 首次学习创建记录watchDuration为0 } // 只取更大的值 int newDuration Math.max(record.getWatchDuration(), req.getWatchDuration()); if (newDuration totalDuration * 0.9) { record.setCompleteFlag(1); } record.setWatchDuration(newDuration); studyRecordMapper.updateById(record);这里为什么要Math.max而不是直接覆盖因为网络请求有延迟前端定时器10秒一次但两个请求到达后端的时间顺序可能会乱如果后面到的请求覆盖了前面的就会把进度“改小”了。用Math.max就能保证单调递增是这种场景的必备防抖设计。审核接口是管理员专用的需要加RequireRole(role 1)注解。审核时同时更新audit_status和audit_time如果通过则加积分、发WebSocket通知。审核场景还有一个“批量通过”的常见需求前端只勾选多个记录ID后端用list接收批量处理。这里建议走事务一个失败全部回滚保证记录状态和积分数据的一致性。3.4 视频存储与M3U8播放支持视频文件有两种存法一种是把mp4文件直接放到服务器或OSS前端用普通video标签播放另一种是为了应对大视频和弱网环境把视频转码成HLS流m3u8 ts分片。青年大学习这类内容视频通常有几十分钟如果用mp4直出格式用户手机网络稍微差一点拖动进度条会卡得很难受。我在这个项目里接入了m3u8播放方案实测移动端兼容性很好。后端不需要自己实现转码。我用的方案是将视频源文件上传到服务器后通过ffmpeg转码成HLS格式。一条命令就能转ffmpeg -i input.mp4 -codec copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8这里-codec copy表示不重新编码转码速度非常快基本是文件拷贝的速度。hls_time 10表示每个ts分片10秒。ffmpeg转出来的产物是一个m3u8索引文件和一堆ts分片文件。后端把m3u8地址存到learning_task的video_url字段前端拿到之后用HLS播放器播放。部署时这里有个Nginx关键配置必须处理m3u8和ts分片的访问要允许跨域否则前端从自己域名请求视频地址Nginx默认会拦掉OPTIONS预检请求。我踩过一次播放器一直报跨域错误排查了半天最后在Nginx的location块里加了三行add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers *;如果你有防盗链需求再加valid_referers判断就行。学生端播放的时候前端还会做“离开页面自动暂停上报一次”的处理避免用户看到一半直接关页丢失最后一段进度。3.5 数据统计与报表导出数据统计是本系统里最能体现价值的部分。我用MyBatis-Plus写一个统计Service底层用原生SQL做聚合。核心指标比如“某期任务各学院完成率”的SQL长这样SELECT c.college_name, COUNT(DISTINCT s.id) AS total_students, COUNT(DISTINCT CASE WHEN r.complete_flag 1 THEN s.id END) AS completed_students, ROUND(COUNT(DISTINCT CASE WHEN r.complete_flag 1 THEN s.id END) / COUNT(DISTINCT s.id) * 100, 2) AS complete_rate FROM student s LEFT JOIN college c ON s.college_id c.id LEFT JOIN study_record r ON r.student_id s.id AND r.task_id #{taskId} GROUP BY c.id ORDER BY complete_rate DESC这里的LEFT JOIN和CASE WHEN的组合很关键。如果用INNER JOIN没参与学习的学生会被直接过滤掉统计结果就会虚高。LEFT JOIN能保证每个学院的所有学生都出现在结果里没学的学生算到分母里但不进分子这样得到才是真实完成率。统计结果接口不直接返回图表数据而是返回前端可以直接用的数据结构每个学院一行包含total、completed、rate。前端ECharts拿到后渲染柱状图和饼图。导出Excel我用的是EasyExcel。为什么不用POI直接写EasyExcel的流式写入方式对大数据的处理更友好内存占用低。导出接口的逻辑很简单查数据集合用EasyExcel的write方法输出到HttpServletResponse的输出流同时设置响应头让浏览器识别为附件下载。导出文件名要注意做一下URL编码否则浏览器下载时中文名会乱码。实际效果是管理员在前端点“导出报表”按钮浏览器直接下载一个带多Sheet的Excel第一个Sheet是学院完成率汇总第二个Sheet是每个学生每期任务的学习明细第三个Sheet是积分排行榜。这个报表拿来直接给上级交差就很方便了不需要再人工二次加工。3.6 审核结果实时通知审核结果的实时通知当初是面试官追问“有没有做过WebSocket”时我想起来加的功能。Spring Boot集成WebSocket其实非常轻量核心就三步引入spring-boot-starter-websocket依赖。写一个WebSocketConfig配置类注册WebSocketHandler。写一个WebSocketHandler类管理连接和消息推送。我在系统里的用法很简单学生前端登录成功后建立WebSocket连接连接地址里带上token。后端在握手阶段解析token拿到userId用一个ConcurrentHashMap维护userId和WebSocketSession的对应关系。管理员审核记录时审核通过后往该学生userId对应的session发送一条消息“您在第X期学习中的成果记录已通过审核积分已到账。”学生前端收到消息后弹一个通知同时刷新当前积分显示。这里要考虑WebSocket连接不稳定、手机锁屏断开的情况所以我在通知表里做了落库。每次发送WebSocket消息之前先插入notice表标记为未读。学生重新进入页面时不仅通过WebSocket接收实时消息也会拉取所有未读通知补齐到页面顶部的小铃铛里。这种双通道设计保证了消息不丢这个经验是从聊天系统里迁移过来的。WebSocket配置还有个细节Spring Boot默认的web socket端点和Spring MVC的DispatcherServlet对路径的拦截规则不同如果项目里配了Shiro或者Spring Security必须手动放行WebSocket握手请求。这个项目没引安全框架只做了拦截器所以只要放行/ws/**路径就行了。4. 前端核心模块实现4.1 初始化工程与目录结构前端用Vue CLI创建工程我习惯在创建完基础结构后重新梳理一下目录因为脚手架默认生成的目录太散后期维护不便。最终目录结构如下src ├── api // 所有接口请求定义按模块拆分文件 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Vuex状态管理 ├── utils // 工具类如request.js axios封装 ├── views // 页面级组件 │ ├── login │ ├── dashboard │ ├── task │ ├── record │ ├── audit │ └── user ├── App.vue └── main.jsElement UI在main.js里全局注册ECharts我按需引入不整包引入否则打包体积会大得离谱首次打开要白屏好几秒。按需引入的方式是在一个独立的echarts.js文件里统一引入需要用到的图表组件然后挂载到Vue原型上页面直接this.$echarts访问。axios封装是前端的基础设施我在utils/request.js里做了统一处理基础URL用环境变量区分开发/生产请求拦截器里自动从Vuex或localStorage读取token放入Authorization请求头响应拦截器统一解包Result遇到code401就清空登录状态并跳转登录页。这样业务代码里只需要写this.$http.get(/xxx)拿到的就是data字段不用每层都解包一次。4.2 Vue Router路由设计与权限控制前端路由有两层嵌套外层叫Layout包含顶部导航栏和左侧菜单。所有需要登录的页面都挂在Layout下。路由表里每个页面通过meta字段标记需要的角色{ path: /audit, name: Audit, component: () import(/views/audit/index.vue), meta: { title: 成果审核, roles: [admin] } }路由守卫写在router.beforeEach里核心逻辑分三步第一步判断本地有没有token没有就跳转登录页第二步判断store里有没有用户信息没有就调用获取用户信息接口第三步判断目标路由的meta.roles和当前用户角色是否匹配不匹配就跳403页面。这个流程是后台管理系统最常见的标准做法。这里有个实际开发容易踩的坑动态路由或者说权限路由。早期我把所有路由一次性注册了学生端也能访问管理员页面虽然在接口层做了拦截但页面看到了还是体验不好。如果要做“菜单根据角色动态生成”要把路由拆成两部分公共路由登录页和Layout在初始化时就注册管理员和学生的业务路由分别定义成数组挂在菜单数据里根据后端返回的role字段动态addRoute。这个方案看起来高级但对初学阶段复杂度有点高。我做的是折中路由全量注册但侧边栏菜单根据角色过滤显示用户手输地址访问倒是能进页面但页面里的接口会返回403。对内部管理系统来说这个安全级别够用了。4.3 视频学习页面与进度上报视频播放器我用的video.js配合videojs-contrib-hls插件同时兼容mp4和m3u8两种格式。页面上的核心交互不难难的是把播放进度和上报时机处理好。我定义了一个上报策略统一在utils/reportProgress.js里封装监听video标签的timeupdate事件每秒钟获取一次当前currentTime维护一个lastReportSec变量。当currentTime - lastReportSec 10秒时向上报接口发起POST请求把parseInt(currentTime)传给后端。页面visibilitychange事件变为hidden用户切走或锁屏时立即上报一次。组件beforeDestroy时立即上报一次。上报使用axios默认的异步请求不做阻塞等待。视频播放页还做了“未登录不允许观看”的判断路由守卫已经拦截了但页面里还会再次检查store里的用户信息因为路由守卫只能保证进入时的状态用户刷新页面时Vuex数据会丢需要重新拉取。播放器初始化时还有个小细节如果后端返回的m3u8地址需要携带token才能访问那么video.src不能直接放视频地址得用fetch先请求视频地址拿到带鉴权参数的URL再赋值给播放器。不过这个项目里Nginx对视频做了公开访问没有加鉴权所以省了一步。如果做了防盗链需要注意这个点。4.4 数据看板与图表可视化数据看板页面是管理员登录后的默认首页我用ECharts做了四个模块顶部四个统计卡片总人数、本期完成率、平均积分、待审核数、学院完成率柱状图、近六期完成率折线图、个人积分排行榜。图表数据全部来自/stats/overview和/stats/college-ranking接口。柱状图渲染时有一个容易忽略的细节容器div必须有明确的宽度和高度否则ECharts初始化出来是一个0×0的画布啥也看不见。我在mounted里调用this.$nextTick确保DOM渲染完成后才初始化图表。另外一个经验是页面里的图表在window.resize时要调用chart.resize()方法否则浏览器窗口拉大后图表还是原来大小显得很呆。我在destroyed钩子里一定要removeEventListener同时调用chart.dispose()释放实例否则多页面切换后内存泄露页面越来越卡。统计卡片的数字我用了一个vue-count-to组件做滚动动画从0滚到目标数字视觉效果好代码也不复杂。这类小组件不用自己写npm装一个用就行重点还是把数据拉准、渲染对。4.5 学习记录提交与我的中心学生端页面相对简单核心是两个当前任务页和我的记录页。当前任务页展示最新一期学习任务的信息标题、封面、截止时间、视频时长。如果学生还没开始学习显示“去学习”按钮如果学完了但还没提交成果显示“提交截图”按钮如果提交了但还在审核中显示“审核中”的标签如果审核通过了显示积分已到账。这个状态展示依赖学习记录接口返回的complete_flag和audit_status前端根据这两个字段的组合渲染不同状态。这里的组合判断我在前端统一封装成了一个方法getStatusText(record) { if (!record) return 未学习; if (record.auditStatus 1) return 已通过; if (record.auditStatus 0 record.completeFlag 1) return 待审核; if (record.completeFlag 1) return 待提交; return 学习中; }我的记录页是一个列表展示该学生所有历史学习记录包括每期任务的完成情况、积分变化。列表用el-table渲染分页用el-pagination数据请求带pageNum和pageSize参数。每一行右侧放一个“详情”按钮弹对话框展示观看时长、完成时间、审核备注等详细信息。5. 联调、部署与高频问题排查5.1 前后端联调阶段的环境配置前后端分离联调时最大的坑是跨域。开发阶段前端跑在8080端口后端跑在8081端口前端请求/api接口被浏览器直接拦截。解决跨域有两条路线一条是在后端加CorsConfig配置类允许跨域访问一条是在前端脚手架里配置proxy代理让前端请求转发到后端。我实际用的方案是后端加CORS配置因为这样无论是本地联调还是将来部署到Nginx后面接口行为都一致。CorsConfig的核心代码是注册一个CorsFilter放行所有来源、所有请求头、所有方法并允许携带凭证。注意allowedOriginPatterns()和allowedOrigins()的区别如果允许携带CookieallowedOrigins不能写*必须写allowedOriginPatterns否则浏览器会报错。这个细节不注意到联调时跨域问题排查半天都查不完。联调阶段的另一个建议是准备一个ApiFox或者Postman的接口集合后端每写一个接口就调通一个把每个接口的请求参数和返回结果保存下来。联调时前端可以直接导入这个集合快速看到接口的出入参格式不用反复找后端对字段名。5.2 部署上线与Nginx配置部署时后端和前端分开部署。后端打包成jar包服务器上装好JDK 8和MySQL用systemd做进程守护。前端打包成dist目录用Nginx托管。Nginx配置有两个作用一是托管前端静态文件二是反向代理后端接口。一个完整的server块大致如下server { listen 80; server_name www.example.com; # 前端静态资源 root /opt/youthstudy/dist; index index.html; # 解决Vue Router history模式刷新404 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 视频分片与上传文件访问 location /uploads/ { alias /opt/youthstudy/uploads/; add_header Access-Control-Allow-Origin *; } }location /里的try_files配置是Vue Router history模式必须配的否则用户在某个子路由下刷新Nginx找不到对应的文件直接返回404。如果不写这行前端页面一刷新就白屏这个问题在第一次部署时几乎必踩。Nginx还有一个坑是默认上传大小限制1MB系统中学生要上传截图管理员要上传视频图片都传不上去。需要在http块里加入client_max_body_size 50m视视频大小调整。不调这个配置前端上传大文件时会报413 Request Entity Too Large第一次遇到时容易懵。5.3 高频问题速查表把开发过程中遇到的高频问题整理成一张表方便遇到同样问题的同学直接对照排查问题现象可能原因解决方案Spring Boot启动报错Swagger空指针Spring Boot 2.6默认PathPattern匹配策略和springfox不兼容application.yml加spring.mvc.pathmatch.matching-strategyant_path_matcher后端启动时数据库连接失败MySQL未启动或连接串少了时区参数连接串加?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai前端请求接口报CORS错误后端未配置跨域或配置写错检查CorsConfigallowedOriginPatterns不能和allowCredentials同时用*上传图片报413Nginx默认限制上传大小1MBhttp块里加client_max_body_size 50m下载Excel文件名中文乱码响应头Content-Disposition未做URL编码用URLEncoder.encode对文件名编码视频播放M3U8报跨域Nginx返回ts分片时缺少CORS头location /uploads/里加add_header Access-Control-Allow-Origin *审核通过后学生端没有及时收到通知WebSocket连接断开或没有落库确认前端建立了ws连接确认通知落库页面刷新自动拉取未读刷新前端页面后菜单/用户信息丢失Vuex是内存态刷新即丢失在路由守卫里判断store为空时调用获取用户信息接口重新填充学生不断上报进度后台数据异常大前端定时上报频率太高、后端没做频率限制后端校验两次上报间隔不低于8秒超过就忽略多标签页打开系统WebSocket重复连接没有做连接去重前端关闭旧连接再新建或后端按userId替换已有session最后再分享一个我在这个项目里体会最深的一点技术栈选型本身不太能拉开差距真正让系统好用的是那些容易被忽略的细节设计比如审核状态和完成状态分开存、进度上报做幂等和防抖、消息推送做落库兜底、Excel导出做多Sheet。把这些细节一个个抠扎实系统就算不华丽也会非常“抗用”。如果你准备照着做一版建议先把业务状态机画清楚再动手写代码后面会省下大量改表结构的返工时间。