ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于SpringBoot+Vue+MyBatis的智能学习平台设计与实现

基于SpringBoot+Vue+MyBatis的智能学习平台设计与实现 1. 为什么我会去做一个智能学习平台需求分析与项目定位先说说这个项目的来龙去脉。去年团队接到一个高校实训平台的外包需求甲方一开始给的描述很简单我们要做一个在线学习系统能看视频、能刷题、能统计学习进度。听起来像是个普通的课程网站但聊深了才发现真正棘手的是智能两个字——他们希望平台能根据学生的答题情况和学习行为给出个性化的学习路径建议而不是把所有课程资源一股脑堆在页面上让学生自己翻。这时候我脑子里第一个组合就是SpringBoot Vue。原因很直接这个技术栈在Java生态里足够成熟招人好招部署省心而且前后端分离的架构天然适合管理后台 学生端 教师端这种多角色场景。配合MySQL MyBatis做数据持久化整套系统从零到上线我们大概花了三周左右。这篇文章就是把这个项目的设计与实现过程完整复盘一遍从需求拆解到数据库建模从后端接口设计到Vue前端联调再到部署时踩过的坑全部写出来。如果你是正在做毕业设计、个人作品集或者接手了一个类似的在线教育类项目这篇内容应该能帮你避开不少弯路。但先提醒一句任何技术方案都要跟着实际需求走智能这个词的落地程度决定项目复杂度。我见过很多人一上来就上推荐算法、上消息队列结果连基础的业务闭环都没跑通。下面我会按照当时推进项目的真实顺序来写该简的地方简该深的地方深。1.1 用户角色与核心业务流程梳理不管什么系统第一步永远是把谁在用、用来干什么想清楚。我们这个学习平台最终确定了三种角色学生注册登录、浏览课程、学习视频/文档、做练习、查看学习报告、生成个性化学习建议。教师管理课程与章节、上传教学资源、发布习题、查看学生学情统计数据。管理员用户管理、课程分类管理、系统参数配置、数据看板。围绕这三种角色核心业务流程可以归纳成一条主线学生选课 → 学习章节内容 → 完成课后练习 → 系统记录答题数据 → 统计薄弱知识点 → 生成推荐学习内容 → 学生根据建议继续学习。这个闭环里最容易被低估的是学习行为记录这一步。很多初版设计只记录了是否学完但要做个性化推荐必须细化到每个视频的观看时长、每道题的作答时间、错误选项等粒度。推荐不一定要用多复杂的模型基于知识点的错题统计分析 同知识点题目推送在中小规模系统里完全够用这也是我们最终采用的方案。1.2 功能模块拆分功能模块按角色拆成几大块对应到后面的后端Controller和Vue页面模块子功能涉及角色用户认证注册、登录、Token刷新、个人信息维护全部课程中心课程分类、课程列表、课程详情、章节进度学生、教师教学管理课程创建、章节编辑、视频/文档上传教师在线练习单选题/多选题/判断题、自动判分、错题记录学生、教师智能推荐薄弱知识点分析、个性化练习题推荐、学习路径建议学生数据统计学习时长统计、答题正确率、课程完成度教师、管理员系统管理用户管理、角色权限、分类管理管理员这个表在项目一开始就定了后面所有表结构和接口设计都围绕它展开。需要注意功能列表一定要控制住想做的和必须做的之间差距。比如视频上传我们最终做了但引入的是本地存储 前端Video.js播放而不是接入OSS转码服务因为实训场景的视频量级根本不需要那么重的方案。做项目最忌讳的就是在非核心环节上堆技术复杂度。2. 技术选型背后的真实考量我为什么坚持SpringBootVueMyBatis现在很多项目一上来就是Spring Cloud Alibaba全家桶、Redis缓存、ElasticSearch检索听起来很唬人但对于一个学习平台来说够用和能维护比技术新重要得多。我选型时核心考虑点有这么几个。2.1 后端框架SpringBoot 2.7 MyBatisSpringBoot 2.7是我们评估后觉得最稳的版本。为什么不用3.x因为3.x底层是Jakarta命名空间一些老牌三方库和插件兼容性要额外适配对于业务型项目没有必要。SpringBoot的自动配置极大简化了项目初始化和第三方集成但它的黑盒属性也意味着如果你不理解starter背后的原理出了问题会非常被动后面我在配置MyBatis和事务的时候就踩了典型的坑第4章会专门讲。MyBatis在这套系统里的定位是半自动化SQL控制。相比MyBatis-Plus原生MyBatis的XML配置更繁琐但它对SQL的可控性更强。考虑到系统里有大量统计分析类SQL学习时长聚合、知识点正确率计算等手写SQL反而比MyBatis-Plus的QueryWrapper更直观。另外MyBatis的一级缓存和二级缓存是一个必须理解透彻的机制尤其是二级缓存用不好就会出现脏数据。我们项目的缓存策略是基础字典表开启二级缓存业务表一律不开启用Spring Cache Caffeine管理热点数据比如首页课程列表。2.2 前端框架Vue2还是Vue3这个选择当时纠结了一下。Vue3 Composition API Vite确实是趋势但考虑到团队里部分成员对Vue2的Options API更熟而且项目周期紧最终选了Vue2 Element UI。如果从现在的视角回看我可能会选Vue3 Element Plus但对这个项目本身Vue2完全够用。前端用到的核心依赖包括Vue Router路由管理和权限控制联动路由守卫里做Token校验与角色跳转。Axios封装HTTP请求统一处理token注入、401跳转、错误提示。Element UI后台管理界面组件库。ECharts统计数据可视化大屏教师端/管理端。Video.js视频播放支持m3u8直播流的场景。Vue这部分最容易翻车的不是组件写不出来而是环境配置。很多新手在npm install阶段就开始报错后面第5章我会把环境安装、依赖版本冲突这些细节单独拿出来讲。2.3 数据库MySQL 8.0 字符集与事务隔离MySQL 8.0是当前Java项目的主流选择。8.0相比5.7有几个点对开发体验影响很大默认字符集是utf8mb4、支持窗口函数、JSON类型能力增强。我们项目中学生知识点掌握度排名这类需求用窗口函数一条SQL就能解决放在5.7里要写一堆子查询。连接MySQL时有个细节必须注意时区参数。JDBC连接串如果不加serverTimezoneAsia/Shanghai在高版本MySQL驱动下会直接报错或出现时间偏移8小时的问题。另外MySQL 8.0的mysql_native_password插件在Connector/J 8.x下虽然还能用但建议直接使用默认的caching_sha2_password避免后续版本升级出现验证插件不兼容。到这里项目基本背景和技术栈都已经明确了。接下来是设计阶段最关键的部分数据库到底怎么建表与表之间的关系怎么设计才能支撑智能推荐这个功能。这部分我会直接给出核心表的建表思路并解释每一个字段设计的理由。3. 数据库设计是项目的地基核心表结构与关键字段设计思路学习平台的表结构并不复杂但有几个地方如果设计不好后面写业务代码会非常痛苦。你别看很多教程里贴了一张ER图就带过了实际建表时每个字段的类型、长度、索引、默认值都要有依据。下面是我总结的几组核心表。3.1 用户与角色权限表设计用户表sys_user不是简单的一个账号密码字段就行。要支持角色区分常见的方案有两种一种是直接在用户表加role_id字段简单直接另一种是标准的RBAC模型用户表、角色表、用户角色关联表、权限表。这个项目最终用的是RBAC的简化版——有用户表、角色表、用户角色关联表但没有细粒度到菜单按钮权限因为三种角色的操作边界通过前端路由守卫 后端接口注解就可以控制再建一层权限表属于过度设计。用户表核心字段idbigint主键自增usernamevarchar(50)唯一索引passwordvarchar(100)存BCrypt加密后的密文nicknamevarchar(50)avatarvarchar(255)头像URLemail、phonestatustinyint0禁用 1正常create_time、update_timedatetime密码加密这里多说一句不要用MD5不要用SHA直接上BCrypt。Spring Security自带BCryptPasswordEncoder同样的密码每次加密出的密文都不一样因为内部掺了随机盐安全性远高于固定哈希。3.2 课程与章节表树形结构的设计课程分类我用了一张简单的二级分类表还是类目表 pid自关联这里说结论用单表自关联category表带parent_id字段。因为学习平台的分类最多两级如Java开发下的SpringBoot单表自关联既支持无限极扩展又不至于引入单独的组织树组件。课程主表course关键字段id、category_id关联分类表teacher_id关联用户表标识创建课程的教师title、cover、introvarchar/textleveltinyint难度等级statustinyint0草稿 1已发布 2下架is_freetinyint是否免费view_countint浏览量、like_count课程章节表course_sectionid、course_id普通索引section_name、video_url、content富文本sort_orderint用于章节排序durationint视频时长单位秒设计要点排序字段一定要有。前端展示章节列表时如果没想好排序规则就按创建时间排后面调整顺序会非常麻烦。sort_order用int排序时ORDER BY sort_order ASC, id ASC可控性拉满。3.3 学习记录与答题记录保证智能推荐的依据这是整个数据库设计中最需要花心思的部分。我们要支持学习进度跟踪也要支持薄弱知识点分析意味着学生每次答题、每个视频的观看进度都得落库。当时我们设计了这几张表study_record学习记录表记录学生每个章节的学习行为。字段包括user_id、section_id、watch_duration本次观看时长秒、is_completed是否学完、study_date。注意这里记录的是每一次进入章节的行为而不是汇总结果方便后续做学习趋势分析。exam_paper试卷表和exam_question试题表题目和试卷是多对多关系所以需要中间表paper_question。题库表exam_question核心字段question_type1单选 2多选 3判断、content、optionsJSON字符串存放选项列表、answer答案、analysis解析、knowledge_pointvarchar知识点标签、difficultytinyint难度。知识点的knowledge_point字段很关键推荐功能就是靠它把错题和同知识点题目关联起来。answer_record答题记录表每次答题都插入一条记录。字段user_id、question_id、paper_id、user_answer、is_correct、answer_time、create_time。这张表数据量会快速增长所以user_id和question_id必须建组合索引。后续统计正确率时直接sELECT COUNT(*) FROM answer_record WHERE user_id? AND is_correct1可能慢我们实际优化时加了knowledge_point冗余字段在答题记录上统计时可以避免回表查询。3.4 关键SQL的编写与索引优化项目跑起来之后最先扛不住的不是业务增删改而是统计报表。其中一个很典型的SQL是统计某学生在每个知识点的正确率SELECT knowledge_point, COUNT(*) AS total_count, SUM(is_correct) AS correct_count, ROUND(SUM(is_correct) / COUNT(*) * 100, 2) AS correct_rate FROM answer_record WHERE user_id #{userId} GROUP BY knowledge_point ORDER BY correct_rate ASC LIMIT 10这条SQL能找出正确率最低的10个知识点正好对应薄弱知识点分析。数据量上来后注意给answer_record表的(user_id, knowledge_point)建组合索引否则一次全表扫描几十万条记录接口响应时间直接飙到几秒。还有一个统计需求是课程完成度某个学生学了多少个章节。这里用study_record表即可SELECT course_id, COUNT(DISTINCT section_id) AS learned_section_count FROM study_record WHERE user_id #{userId} GROUP BY course_id这类SQL没有太多黑魔法重点在于最开始建表时就把study_record设计成流水表不要更新覆盖旧记录这样所有统计口径都能追溯。数据库结构确定后后端开发就有了明确的方向。接下来聊聊后端SpringBoot项目的分层设计和核心模块实现。这部分是完整源码项目的重心我会写清楚后端业务逻辑的分层、接口设计规范、权限验证方式以及MyBatis里那些让人头疼的细节。4. 后端核心模块实现从分层架构到MyBatis的那些坑SpringBoot项目的标准分层是Controller → Service → Mapper这个架构几乎人人都会但真正决定代码能否长期维护的是DTO/VO分离。别把数据库实体直接返给前端也别把前端传参直接塞进数据库更新语句。项目里我建了entity、dto、vo三个包entity对应数据库表结构dto接收前端入参vo返回给前端展示层。这样做的理由是数据库字段可能包含密码、逻辑删除标记等敏感信息如果不加处理直接序列化给前端就是安全事故。而且前端展示往往需要联合字段比如课程名教师名用VO对象组装后会非常干净。4.1 JWT认证与拦截器实现登录模块用的JWTJSON Web Token。用户登录成功后后端生成一个token返回前端把token存在localStorage里后续每次请求在请求头带Authorization: Bearer token。后端用拦截器统一校验。核心逻辑分三部分登录接口校验用户名密码 → 生成JWT → 返回给前端。JWT中我存了userId和role过期时间设为24小时。拦截器继承HandlerInterceptorAdapter或者实现HandlerInterceptor接口。preHandle方法里从请求头取token解析并校验如果无效直接返回401。注意要放行登录接口和静态资源路径。角色权限控制用自定义注解RequireRole(teacher)加到需要权限的Controller方法上在拦截器里读取注解并比对token中的role不匹配就返回403。JWT要注意一个点token一旦签发在有效期内是无法主动失效的。如果你要支持踢人下线或者修改密码后让老token失效就要引入黑名单机制用Redis存储被吊销的token。我们的项目里暂时用不到这个功能所以没做但理解这个缺陷很重要。4.2 MyBatis的XML映射与缓存问题复盘MyBatis的使用分两种风格注解SQL和XML SQL。我的建议是凡是SQL稍微复杂一点的一律写XML。注解里写长SQL不仅可读性差而且没法做动态SQL的复杂拼接。项目里所有Mapper接口都对应一个XML文件放在resources/mapper目录下。最典型的动态SQL片段是课程列表的条件查询select idselectCoursePage resultTypecom.example.entity.Course SELECT c.*, u.nickname AS teacher_name FROM course c LEFT JOIN sys_user u ON c.teacher_id u.id where if testcategoryId ! null AND c.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (c.title LIKE CONCAT(%, #{keyword}, %) OR c.intro LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND c.status #{status} /if /where ORDER BY c.create_time DESC /selectwhere标签会自动去掉多余的AND避免写WHERE 11这种丑陋的写法。LIKE CONCAT(%, #{keyword}, %)用了模糊查询要注意#{}是预编译占位符能防SQL注入切记不要在字符串拼接时用${}。MyBatis缓存这块必须单独说因为一级缓存和二级缓存都藏着时空陷阱。一级缓存是SqlSession级别的默认开启。在Spring集成环境下如果同一个SqlSession执行了两次相同查询第二次会直接走缓存这时候如果中间别的地方改了数据库比如另一个事务提交了读到旧数据就是脏读。解决方法是在需要实时性的查询Mapper上设置flushCachetrue或者干脆把Service方法加Transactional保证读写在一个事务里让一级缓存的生命周期可控。二级缓存是Mapper级别的跨SqlSession共享。我们项目只在字典表这类极少变更的表上开了二级缓存业务表一律不开。你如果真的想给业务表开二级缓存必须考虑缓存刷新策略否则课程改了标题另一个用户查询时还是旧数据那体验就是线上事故级别了。4.3 事务管理Transactional的正确打开方式在一个学习平台里事务最典型的场景是学生学习完一个章节后更新学习记录表 更新课程完成度统计表这两个操作必须一起成功或一起失败。此时在Service方法上加Transactional就能保证原子性。但Transactional有几个很容易踩的坑自调用失效同一个类里的方法A调用同一个类里的方法BB上的Transactional不会生效。因为Spring事务是通过AOP代理实现的自调用绕过了代理对象。异常被吞导致不回滚Transactional默认只对RuntimeException和Error回滚如果方法里catch了异常并正常返回事务是不会回滚的。所以要么让异常抛出去要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。锁表问题Transactional方法里如果做了远程调用或耗时操作会长时间占用数据库连接高并发场景下很容易把连接池打满。事务方法里尽量只做数据库操作。如果你在IDEA里想监控MyBatis实际执行的SQL推荐装一个MyBatis Log Free插件它能把MyBatis日志里的Preparing和Parameters还原成可直接执行的完整SQL调试的时候特别好用尤其是排查为什么这个条件没生效这种问题。4.4 接口设计与统一返回结果前后端分离项目的接口规范必须统一。我写了一个Result类所有接口都返回这个结构public class ResultT { private Integer code; // 200成功其他为失败 private String message; private T data; }为什么不用默认的HTTP状态码直接表意因为HTTP状态码在浏览器里会被Axios拦截器处理如果是200以外的状态码enterprise监控和用户提示逻辑容易混在一起。不如所有业务请求都返回200然后在JSON里用code区分业务状态遇到401时由拦截器在HTTP层面返回401让Axios统一跳到登录页。这个设计是业内比较主流的做法也便于前端统一处理。接口命名上我遵循RESTful风格资源用复数名词比如POST /api/auth/login登录GET /api/courses课程列表GET /api/courses/{id}课程详情POST /api/courses教师创建课程PUT /api/courses/{id}更新课程POST /api/study/sections/{sectionId}记录学习行为POST /api/exam/submit提交试卷写完这些接口后端就基本成型了。下面进入前端的开发环节这部分我重点讲讲Vue项目的搭建、环境坑、路由守卫和与后端联调过程中容易出的问题。5. Vue前端实现与前后端联调中的关键细节我这几年帮人看过的毕业设计项目里十个有八个卡在Vue环境配置上代码本身反倒是小问题。环境装不好后面全白搭。这里把前端从零跑通的流程和容易踩的坑理一遍。5.1 环境安装与项目初始化先说Node.js版本。Vue2项目建议Node 14或16Vue3配Node 16/18问题都不大但如果Node版本到了20再装一些老依赖比如node-sass大概率会编译失败。所以装环境之前先确认好Node版本用nvm管理多个Node版本是Linux/macOS下的好选择Windows下可以用nvm-windows。创建Vue2项目用官方CLInpm install -g vue/cli vue create study-platform-frontend注意创建时选择Manual模式勾选Router和VuexVue版本选2.x。创建完成后安装项目依赖这里最大的坑是npm install速度慢或者失败解决方案是把npm源切到国内镜像npm config set registry https://registry.npmmirror.com装Element UInpm install element-ui2.15.8为什么特意指名版本因为2.15.8是Element UI 2.x的最后一个稳定版坑最少直接用latest可能会匹配到Element Plus那是给Vue3用的语法完全不同。类似的兼容性问题在Vue生态里非常常见以后装任何包都要先确认它支持的是Vue2还是Vue3。5.2 前端路由规划与动态侧边栏Vue Router是前端页面的骨架。项目里我用了一套路由配置文件驱动侧边栏的方案所有页面组件按角色分目录views/student、views/teacher、views/admin路由表写在router/index.js里侧边栏菜单根据当前用户角色动态过滤路由生成。这样新增页面时只需要往路由表里加一条记录菜单会自动更新不需要手动维护两份代码。路由守卫是权限控制的第一道门槛router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(store.state.user.role)) { next(/403) return } next() })后端接口上已经做了权限校验为什么前端还要重复做一次路由拦截因为用户体验问题学生手动访问教师管理页面时后端会返回403但页面会闪一下白屏再跳转很突兀。前端直接拦截到403提示页体验就好很多。记住一个原则前端拦截是为了体验后端校验才是为了安全。5.3 Axios封装与Token管理Axios封装这一层是整个前端项目的基础设施。我在src/utils/request.js里做了统一封装import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(error.message || 网络异常) return Promise.reject(error) } )这里面有几个细节值得讲。每次请求前从localStorage拿token塞到请求头这是最常见的做法。有人会问为什么不用Cookie因为跨域环境下Cookie要配置withCredentials和CORS具体域名纯前端存储更省心。另外401时自动清token并跳转登录页这个逻辑要放在响应拦截器的error回调里因为后端对未认证请求返回的是HTTP 401业务状态码都走200。5.4 视频播放与m3u8流播放的坑学习平台的章节内容通常包含视频。我们最初用原生video标签发现播放MP4文件没问题但后期接入教师的直播录播流m3u8格式时彻底歇菜了——原生标签不支持HLS协议。解决方案是引入Video.js加上videojs-contrib-hls插件npm install video.js7.x videojs-contrib-hls2.x然后封装一个视频播放组件核心逻辑this.player videojs(this.$refs.videoPlayer, { sources: [{ src: this.videoUrl, type: this.videoUrl.endsWith(.m3u8) ? application/x-mpegURL : video/mp4 }], controls: true, autoplay: false, preload: auto })注意Vue2中要在mounted里初始化播放器在beforeDestroy里销毁播放器否则组件销毁后播放器实例残留切页面后视频还在响。这个bug排查了我整整一个下午最后发现是播放器没销毁。5.5 前后端联调跨域与接口调试前端开发时用npm run serve启动在localhost:8080后端接口在localhost:8080吗不对我们后端项目配的是8081端口这样就产生了跨域问题。解决跨域有两种方式方式一后端加CORS配置。在SpringBoot里写一个WebMvcConfigurer添加跨域映射Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方式二前端配置代理。在vue.config.js里配置devServer.proxy把/api开头的请求转发到后端地址module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }方式二更推荐。因为它在开发环境模拟了生产环境Nginx反向代理的行为上线后只需要在Nginx里配置同样的转发规则前端代码一行都不用改。如果你用方式一等打包上线到Nginx后还需要再配置一次跨域多一套东西要维护。生产环境部署时前端打包生成静态文件后端打成jar包Nginx把静态目录指向前端dist文件夹/api请求转发到Java服务的8081端口整个链路非常清爽。前后端联调通过后系统的核心功能基本就通了。但还有一个绕不开的话题所谓的智能到底怎么落地这一篇我放在最后讲因为它依赖前面的所有数据积累却又最容易被人做得华而不实。6. 智能推荐功能的落地不堆算法先把规则跑通最开始甲方提智能学习平台的时候团队里有人提议引入协同过滤算法有人提议上深度学习模型。我当时的判断是在这个阶段基于知识点的规则推荐比任何复杂算法都靠谱。原因有两点一是冷启动问题——新注册学生没有任何行为数据算法根本推不出东西二是解释性问题——学生看到我们推荐你学习SpringMVC因为你最近在SpringMVC相关题目上正确率只有40%这个推荐理由比算法偷偷摸摸推荐一门课更让人信服。6.1 薄弱知识点识别的两种策略策略一基于答题正确率。从answer_record表统计每个知识点的答题总数和正确数正确率低于阈值的比如低于50%直接标记为薄弱知识点。这个策略的优点是简单、直观、实时性好缺点是学生如果在某个知识点只答了1道题且做错了就会被打上薄弱标签数据噪音太大。所以我们加了一个条件答题数必须不少于5道题才参与统计。策略二基于学习行为。如果一个学生反复观看某章节视频但长期不完成章节练习通常意味着这个知识点对他来说有难度。这个推断不够严谨但作为补充信号很有价值。实现上可以用study_record表统计平均观看时长和完成率。最终推荐算法是一个加权公式weakness_score 0.7 * (1 - correct_rate) 0.3 * (1 - completion_rate)按weakness_score降序取Top5知识点为每个知识点找该分类下难度适中的3~5道题组装成一份个性化练习试卷。6.2 推荐的完整链路后端实现时我没有把推荐逻辑塞在一个Service里而是拆成了三步RecommendService负责收集特征数据正确率、学习完成度调用统计Mapper。计算weakness_score得到薄弱知识点列表。根据知识点查询题目组装试卷返回给前端。前端在学生的学习报告页面展示一个雷达图六维能力每个维度对应一个核心知识点分类直观呈现下方就是为你推荐的练习题列表。体验上比单纯列课程列表好很多学生能明确感知到这个平台懂我。6.3 推荐效果验证的口径推荐功能上线后一定要关注数据反馈。我们在教师端加了一个推荐学习效果统计对比学生做推荐题目的第一次正确率和后续相关知识点正确率提升幅度。虽然初期样本量不大但维护反馈数据有助于判断推荐策略是否需要调参。这里想说一个观点智能不是一次性做出来的而是一轮一轮调出来的。先跑一个简单版本看数据再迭代远比憋大招直接上模型要实际。7. 部署上线遇到的坑与调试经验总结项目写完不代表结束部署上线是另一个修罗场。这一章记录我们真正发布到Linux服务器时遇到的几个问题每一个都是线上环境独有的。7.1 MySQL 8.0时区与连接驱动版本服务器上装MySQL 8.0后后端jar包一启动就报错The server time zone value CST is unrecognized or represents more than one time zone.原因是MySQL驱动8.x对时区要求显式指定。解决办法是在application.yml的JDBC连接串加上参数url: jdbc:mysql://localhost:3306/study_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse第二个经常遇到的问题是把本地的数据库导入服务器后中文全部变成乱码。排查后发现是MySQL配置文件/etc/mysql/mysql.conf.d/mysqld.cnf里的character-set-server没设置成utf8mb4只改了建库时的charset根本不够服务端全局字符集不对连接层还是会出问题。改完配置文件重启MySQL再把数据重新导入一遍乱码问题才彻底解决。7.2 Vue前端打包后的路由404问题vue-router如果使用history模式打包部署到Nginx后直接访问首页正常但刷新非根路径比如刷新/student/courses会404。原因是Nginx找不到对应的物理文件路径。解决方法是配置try_fileslocation / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; }如果对SEO没有硬性要求也可以直接把vue-router切回hash模式地址栏多个#但刷新永远能找到页面省心不少。考虑到学习平台是内部系统不需要SEO我们最后用的就是hash模式线上再也没出现过刷新404。7.3 服务器部署的完整流程把之前零散的操作整理成一个可复制的部署清单服务器安装JDK 1.8、MySQL 8.0、Nginx、Git、Maven。拉取代码git clone前后端项目。后端执行mvn clean package -DskipTests生成jar包。使用nohup java -jar study-platform.jar nohup.log 21 启动。前端npm install后执行npm run build把dist目录下的文件上传到Nginx的html目录。配置Nginx反向代理把/api指向http://localhost:8081。初始化数据库用spring.sql.init或手动执行init.sql脚本建表插入初始管理员账号。执行完后访问服务器的IP就能看到登录页。7.4 IDEA调试MyBatis SQL的技巧最后说一个日常开发提效的方法。IDEA装MyBatis Log Free插件启动项目后它自动收集MyBatis日志在插件窗口里能看到带参数的真实SQL。排查为什么查出来的结果不对时直接复制插件生成的SQL去Navicat里执行验证很快就能定位是SQL问题还是参数问题。另一个技巧是在application.yml里配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会直接打印SQL和参数比插件还直观但生产环境记得关掉否则日志量会非常大。最后再分享一个我自己的体会。这个项目从需求分析到上线前后大概三周真正花时间最多的不是写业务代码反而是那些看起来不起眼的环境问题和部署问题。SpringBoot和Vue这种框架在网上随便一搜就有大把教程但能把一个完整项目从零到一真正跑起来并且让智能推荐这种功能落地、可解释、有反馈才是这个项目最大的价值。你可以试着把其中的智能推荐模块继续扩展比如基于学习行为的遗忘曲线提醒或者引入更细粒度的知识点图谱。数据都已经在你的表里了剩下的就是一步步把规则调好、把体验做顺。项目本身不难难的是你愿不愿意把它做到闭环。
返回列表