ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于SpringBoot的红色知识学习平台:JWT认证、数据库设计与部署全解析

基于SpringBoot的红色知识学习平台:JWT认证、数据库设计与部署全解析 这个项目是我去年帮学弟做毕业设计时从零到一完整跑通的一个SpringBoot实战项目平台名字叫“基于SpringBoot的红色知识学习平台”。前前后后改了七八个版本从最初连登录都跳不过去到最后顺利答辩、部署上线中间踩过的坑非常值得拿出来讲一讲。今天我把这个项目的整体设计思路、核心模块实现、数据库设计还有部署流程完整整理出来给正在做Java毕设、或者想找一个完整SpringBoot项目练手的朋友做个参考。这个项目说白了就是一个“在线知识学习系统”只不过内容主题是红色文化方向。用户端能注册登录、浏览分类知识文章、收藏内容、记录学习进度、参与在线答题管理端则负责内容发布、分类管理、用户管理、答题管理。技术上看它覆盖了JavaWeb方向几乎所有高频考点登录认证、权限控制、增删改查、分页查询、文件上传、接口鉴权、前后端交互所以说它是很适合拿来当毕业设计的题目。下面我就从项目设计、数据库、核心实现、部署和踩坑五个方面来拆解。1. 项目概览与整体设计思路1.1 这个项目是什么适合谁参考红色知识学习平台本质上是一个垂直领域的知识内容管理系统加上在线学习功能。它解决的痛点很直接传统线下学习材料分散、进度难跟踪、无法量化学习效果于是把这套流程搬到线上做成一个既可以“看内容”又可以“测效果”的平台。具体功能拆下来大概是这样的用户端注册登录、个人中心、知识文章浏览与搜索、内容分类筛选、文章收藏、学习进度记录、在线答题与成绩查看。管理端管理员登录、知识内容的新增编辑和上下架、分类维护、用户查询与管理、题目和答案维护、基础数据统计。适合参考的人我总结了三类第一类是正在做SpringBoot方向毕业设计的学生这个项目结构完整、技术点密集论文也好写第二类是自学SpringBoot三件套但缺一个完整项目串联知识的人跟着捋一遍能搞懂登录认证、权限控制、数据流转这些核心概念第三类是校内社团、单位内部想快速搭一个轻量学习平台的人代码精简不臃肿部署成本也低。1.2 技术选型为什么选SpringBoot而不是SSH关于技术栈很多同学会纠结学校课程教的是SSHStruts2SpringHibernate网上主流却是SpringBoot我到底选哪个我的建议非常明确只要不是老师硬性要求直接选SpringBoot。理由不复杂。第一SSH的XML配置实在太多了一个小的数据源配置都要写半天而SpringBoot的“约定优于配置”和自动装配机制把这些繁琐的模板代码全部干掉了。第二SpringBoot生态太成熟了Spring Security、MyBatis-Plus、Redis、MinIO这些常用组件都有极低的上手成本。第三毕业答辩的时候老师大概率会问“SpringBoot的自动装配原理是什么”“ starter 机制怎么理解”选SpringBoot等于把问题圈定在一个你自己熟悉且容易准备的范围内。我把SpringBoot的自动装配用生活化的方式理解你不用关心餐厅后厨怎么切菜只需要在菜单上勾选套餐等价于引入starter依赖后厨就会自动把米饭、菜、甜品配好端上来等价于框架自动帮你注入好所有Bean。你只要拿起筷子吃就行。这种思想让项目骨架搭起来非常快前后端工程师协作时也不容易因为配置问题扯皮。1.3 整体架构与请求链路我做的是前后端分离的方案。后端用SpringBoot只负责提供RESTful接口前端用Vue开发页面部署时用Nginx托管前端静态资源同时把API请求反向代理到后端服务。这样做的好处有三点。第一是答辩素材多前后端分离、跨域处理、接口文档这些都能当作亮点去讲第二是开发效率高我不用每次调样式都重启后端第三是部署清晰前端是纯静态文件后端是一个独立Java进程出问题更好排查。整体请求链路是这样的浏览器访问域名 - Nginx返回Vue静态页面 - 页面里的Ajax请求打到 /api 路径 - Nginx把 /api 开头的请求反向代理到 127.0.0.1:8080 - SpringBoot处理请求并访问MySQL - 返回JSON数据给前端渲染。需要注意的细节是前后端分离之后必然会遇到跨域问题。我在后端配置了全局CORS策略允许前端开发服务器的地址跨域访问同时在Spring Security里把OPTIONS请求放行否则浏览器预检请求会被拦截页面会莫名其妙地拿不到数据。2. 数据库设计与核心表结构2.1 建表之前想清楚的三件事很多同学做项目一上来就建表这是最容易返工的开端。我建模之前先问了自己三个问题。第一用户体系怎么设计我需要的角色很简单普通用户和管理员。不需要搞太复杂在用户表里用一个role字段区分即可。但密码必须加密存储不能是明文。第二知识内容怎么组织平台的内容是文章和视频混合视频需要存封面和播放地址。我把它们统一放在一张内容表里用content_type字段区分图文还是视频这样查询列表时逻辑简单。第三学习行为怎么落库学习进度、收藏、答题记录都属于“用户产生数据”要与内容表分离开单独建表这样内容表不会无限膨胀。建模前把这三件事想明白后面写Mapper和Service的时候会非常顺畅。2.2 用户、分类与内容模块表设计用户表user字段不算多但每个字段都有讲究id主键自增username用户名唯一索引passwordBCrypt加密后的密码nickname昵称做前台展示用avatar头像URLrole角色0表示普通用户1表示管理员status账号状态0禁用1正常create_time注册时间分类表category比较简单id、name、parent_id、sort_order。parent_id是为了以后做二级分类预留的现在内容量不大用一级分类也完全足够。知识内容表article是整个项目的货架表字段有id、category_id、title、summary、cover、content、content_type、video_url、view_count、status、create_by、create_time。create_by后面关联管理员表方便追溯是谁发布的status字段控制上下架不需要物理删除内容方便以后重新上架。索引方面我在category_id、status、create_time上建了联合索引。因为列表页最常用的查询条件是“某个分类下已上架的内容按照发布时间倒序”这个联合索引能直接命中避免全表扫描。2.3 学习行为与答题模块表设计学习行为涉及三张表。学习记录表study_record记录用户每次学习内容的情况字段有id、user_id、article_id、progress观看进度0到100整数、study_duration累计学习秒数、last_study_time。每次用户上报进度时如果同一条记录存在就更新不存在就新增。收藏表favorite很简单id、user_id、article_id、create_time。需要注意的是要加唯一约束user_id, article_id防止用户重复收藏同一条内容。答题模块我设计了两张表。题目表question存的是题库字段有id、category_id、title、question_type单选/多选/判断、options选项JSON字符串、answer正确答案、analysis答案解析、score分值。答题记录表exam_record存用户每次考试的整体情况字段有id、user_id、total_score、correct_count、question_count、duration、create_time。这里有个经验题目的选项我建议用JSON串存不要拆成四五个字段。因为单选、多选、判断三种题型的选项数量不一致用title、option_a、option_b这种固定字段去存会很别扭JSON串虽然查询时不好直接统计但考试答题场景基本都是读出来然后在内存里处理性能完全没问题。3. 核心功能实现与难点拆解3.1 JWT登录认证从原理到配置登录认证是这个项目里最容易被问、也最容易写崩的部分。我选的是Spring Security JWT的组合状态无关非常适合前后端分离场景。认证流程是用户提交用户名密码后端校验通过后用JWT工具类生成一个token串返回给前端前端存在localStorage里。之后每次请求前端在请求头里带上Authorization: Bearer token后端写一个过滤器去解析这个token解析成功就把用户信息塞进SecurityContext失败就返回401。先看JWT工具类的核心方法public class JwtUtil { private static final String SECRET_KEY your-secret-key-change-in-production; public static String generateToken(String username, String role) { return Jwts.builder() .setSubject(username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }然后是核心的过滤器继承OncePerRequestFilter在每个请求进来时尝试解析tokenpublic class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); try { Claims claims JwtUtil.parseToken(token); String username claims.getSubject(); String role claims.get(role, String.class); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(username, null, Collections.singletonList(new SimpleGrantedAuthority(ROLE_ role))); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // token无效或过期保持匿名状态 } } chain.doFilter(request, response); } }这里有几个细节必须注意。不能在所有请求上都强制校验token登录接口、注册接口和内容浏览接口应该放行。我在SecurityConfig里用permitAll()把无需认证的路径显式声明出来。token过期时间我设置的是24小时反复踩过坑才明白过期时间太短用户要频繁登录体验差太长又会有安全隐患毕业设计场景24小时比较合适。生产环境一般把密钥放到配置文件中不要硬编码在类里。3.2 知识内容的发布、浏览与分页内容模块的核心是分页查询。这里我直接用了MyBatis-Plus的IPage代码非常简洁public IPageArticleVO getArticlePage(int pageNum, int pageSize, Long categoryId) { PageArticle page new Page(pageNum, pageSize); LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); wrapper.eq(Article::getStatus, 1); if (categoryId ! null) { wrapper.eq(Article::getCategoryId, categoryId); } wrapper.orderByDesc(Article::getCreateTime); return articleMapper.selectPage(page, wrapper); }分页接口返回的内容我不会直接把数据库实体丢给前端而是转成一个VOView Object把createBy这种内部字段隐藏掉再额外追加一个isFavorite字段标示当前用户是否已收藏这条内容。这个字段在前端列表页非常关键不然用户根本不知道哪些内容收藏过了。内容发布的后台接口核心是文件上传。封面图我用本地上传方案保存到服务器指定目录然后返回一个访问URL。这里要注意本地目录不能随便找我建议统一放在项目配置的upload.dir下Nginx再把该目录映射为静态资源访问。这样后台上传图片后前端直接拿到一个可访问的图片地址不依赖后端再写一个图片流接口。图片上传的核心代码如下public String uploadFile(MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID() ext; File dest new File(uploadDir / fileName); try { file.transferTo(dest); } catch (IOException e) { throw new BusinessException(文件上传失败); } return /upload/ fileName; }文件名必须用UUID重写千万不能直接用原始文件名这是血的教训。要是上传两张同名的图片第二张直接覆盖第一张。文件大小也建议在Spring配置中限制一下默认最大1MB毕设场景足够免得有人传一个几十兆的大图把服务器带宽打满。3.3 学习进度与在线答题最容易出bug的地方学习进度这个功能看似简单做起来细节很多。前端在用户进入文章详情时通过接口上报一条学习记录。上报参数包括文章id、当前进度0到100的整数、本次学习时长。后端接口的逻辑是先查这个用户对这篇文章有没有历史记录有就更新没有就新增。有一个细节我优化过如果一个用户反复打开同一篇文章每次都新增记录表数据会膨胀得很快。所以我在代码里用user_id article_id做了一个唯一约束把“更新”作为主逻辑这样同一篇文章对同一个用户永远只有一条学习记录历史数据只是不断累积时长和进度。学习时长和进度连续上报时的数据一致性也要注意最好用定时器在前端每15秒上报一次而不是每次切换页面都上报减少接口压力。在线答题模块我做了随机抽题、自动判卷、成绩记录三件事。随机抽题很简单用SQL的随机排序取指定数量SELECT * FROM question WHERE category_id #{categoryId} ORDER BY RAND() LIMIT #{limit}自动判卷的核心思路是前端把用户选择的答案对象列表提交到后端后端遍历每一题与question表里的answer字段比对计算正确数量和总分然后把结果写入exam_record表。这里尤其要注意多选题的判分逻辑一题多选必须选项完全一致才算对少选多选都不得分。这个逻辑看起来很基础但实际开发时很多人会忽略边界条件。判分的Service方法核心片段public ExamResult submitExam(Long userId, ListUserAnswerItem answers) { int correctCount 0; int totalScore 0; for (UserAnswerItem item : answers) { Question question questionMapper.selectById(item.getQuestionId()); if (question null) continue; totalScore question.getScore(); if (checkAnswer(question, item.getUserAnswer())) { correctCount; } } // 写答案记录表、返回成绩 }4. 部署上线全流程实录4.1 部署前必须调整的配置项很多同学在本地跑得飞起一到部署就卡住。核心问题在于本地配置和服务器配置混在一起了。我建议在resources目录下拆出application-dev.yml和application-prod.yml两个配置通过spring.profiles.active来控制激活哪一套。生产环境配置必须改这几个地方数据库地址改成服务器的内网IP和真实库名数据库账号密码不能用root弱口令JWT密钥换成一个随机长字符串文件上传目录改成服务器的绝对路径比如/opt/upload。端口保持8080不用动因为外面有Nginx挡着。数据库初始化这一步我是先把本地的SQL文件导出来包括建表语句和初始数据然后上传到服务器执行mysql -u root -p knowledge_platform.sql有个很容易被忽略的坑MySQL的字符集。如果建库语句里没有指定utf8mb4导入中文数据后可能出现乱码。我初始化SQL开头都会带上CREATE DATABASE IF NOT EXISTS knowledge_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;4.2 服务器环境搭建服务器我用的是Linux环境。基础环境分三步走安装JDK 8、安装MySQL 5.7、安装Nginx。JDK安装很简单下载tar包解压后配置JAVA_HOME即可。如果机器内存只有2G建议给JVM设置一个合理的堆内存在启动命令里加上-server -Xms256m -Xmx512m避免默认值把内存撑爆。MySQL 5.7在CentOS上的安装命令是yum install mysql-community-server -y systemctl start mysqld grep temporary password /var/log/mysqld.log mysql_secure_installation安装完成后记得给新建的数据库用户只授权这个库的权限CREATE USER app_userlocalhost IDENTIFIED BY StrongPass123; GRANT ALL PRIVILEGES ON knowledge_platform.* TO app_userlocalhost; FLUSH PRIVILEGES;这样即使应用被注入SQL也只能影响这一个数据库不会拖垮整个MySQL实例。4.3 用Nginx托管前端并完成联调前端构建完以后dist目录就是一堆纯静态文件。我把它传到服务器的/var/www/knowledge-platform目录下然后配置Nginx。配置文件是最核心的部分直接贴出来server { listen 80; server_name your-domain.com; # 前端静态资源 root /var/www/knowledge-platform; index index.html; # 解决Vue路由刷新404问题 location / { try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传的图片文件 location /upload/ { alias /opt/upload/; } }注意location /api/这里proxy_pass后面如果没有接路径会把完整的 /api/xxx 传给后端如果proxy_pass写成了http://127.0.0.1:8080/那么 /api 前缀会被去掉两种写法后端接收到的路径不一样。我后端所有接口统一以 /api 开头所以这里直接用不加路径的写法比较省事。为了让后端服务在后台稳定运行、服务器重启后还能自动拉起我写了一个systemd服务文件虽然网上很多人直接nohup java -jar启动但systemd的好处是崩溃后自动重启日志统一管理答辩演示的时候即使进程挂了也能自动恢复[Unit] DescriptionKnowledge Platform Afternetwork.target [Service] Userroot ExecStart/usr/bin/java -jar /opt/app/knowledge-platform.jar --spring.profiles.activeprod Restartalways RestartSec10 [Install] WantedBymulti-user.target配置好之后执行systemctl daemon-reload再systemctl enable knowledge-platform开机自启问题也解决了。5. 常见问题与排查记录5.1 我在部署和答辩演示时踩过的坑把这个项目从头到尾跑完前前后后遇到了不少典型问题。我整理成一个表格每个问题都对应一个具体的解决思路拿过去就能用报错或现象具体原因解决方式启动报端口被占用本地8080被其他程序占用netstat -tlnp | grep 8080 找到占用进程kill 后释放Access denied for user数据库账号密码错误或没有授权核对账号密码并确认只授权了正确库Navicat连接报Public Key RetrievalMySQL 8.0的caching_sha2_password认证在JDBC url里加上allowPublicKeyRetrievaltrueuseSSLfalse中文乱码MySQL库表字符集不是utf8mb4改库表字符集为utf8mb4重启服务Vue刷新页面404前端路由是history模式Nginx没有配置try_files在Nginx配置里加try_files $uri /index.html图片加载失败Nginx没有映射/upload目录配置location /upload/ alias 到上传文件夹前端请求跨域没配置CORS或者Nginx代理路径不对后端加全局CORS配置前端统一走/api路径接口报401但用户已登录JWT token过期检查过期时间请求头是否带上Authorization5.2 关于源码、文档和项目演示的三点建议最后再分享三个在实际交付项目中积累的经验。第一个建议项目交付时一定要把部署文档写清楚。我见过太多同学代码写完了结果换一台电脑就不会跑了。我的习惯是写一份标准的README把jdk版本、数据库版本、初始化SQL步骤、配置文件改动点、启动命令、默认账号密码全部写清楚。做答辩演示之前按照文档从零部署一遍确保不会当场翻车。第二个建议不要把所有的坑都踩一遍才写代码。比如Spring Security的过滤器顺序、MyBatis-Plus分页插件的配置、CORS和Security的冲突这些点网上资料很零散。最靠谱的做法是先画一个接口清单把每个接口的路径、请求参数、返回结构定下来再写代码。接口定清楚了前后端并行开发效率反而更高。第三个建议项目源码命名要规范注释不能少。答辩老师不会一行行读代码但他会随机打开几个类看一眼。如果ServiceImpl里面全是a、b、c这种变量名即使功能没问题也会打低分。我在关键业务方法上都加了简单的中文注释把“这段代码是在做什么”写清楚看起来会专业很多。根据我实际做下来的经验这种偏内容管理方向的学习平台类项目最大的工作量不在CRUD本身而在权限控制、进度记录、判分逻辑这些细节上。只要能把这几个核心点讲清楚答辩和面试都基本没有大问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进