如果让我给今年的毕设课题排个优先级,"基于SpringBoot的冬奥会科普平台"属于那种一眼看过去就让人觉得有讲头的类型。它不是一个脑门一热造出来的伪需求,而是把SpringBoot技术栈的所有核心能力,都通过一个真实可用的内容型Web应用串了起来:用户认证、内容发布、数据检索、缓存加速、知识答题、后台管理、部署上线,一个都不少。你要是正在选毕设题,或者想用Java后端做一套能拿得出手的项目,这个方向刚好卡在"技术有深度"和"实现有边界"之间的最佳位置。
我做完这套平台最大的感受是:它表面是一个冬奥知识站点,本质是一套通用的"内容管理+轻互动"系统。文章、视频、题库、排行榜、收藏、后台审核,这些模块拆开看都是SpringBoot开发里最高频的业务场景,换一个主题照样能跑。所以这篇就把我整个设计和实现过程完整复盘一遍,从需求拆解到技术选型,从核心代码到Docker部署,再到那些不踩一遍根本记不住的坑,全部分享出来。
1. 项目概述与需求拆解
1.1 为什么选冬奥科普这个方向
做毕设最容易犯的错,是选一个"纯管理系统"——用户表、订单表、CRUD一套,做完自己都说不清解决了什么问题。冬奥科普平台不一样,它的价值在于真实存在的科普缺口。
冬奥会项目多、规则杂,冰壶、冬季两项、雪车雪橇这些项目的规则和装备,大部分人是一知半解的。传统资讯站虽然内容全,但是以单向输出为主,用户看两页就划走了。科普平台要想留住人,必须解决两个问题:一是内容要成体系,不能东一篇西一篇;二是学习要有反馈,不能干巴巴地看。
所以我把平台定位成"可看、可学、可考"三位一体。可看是指图文和视频科普,可学是指按项目分类的知识库,可考是指答题闯关和积分排行。这三个词定下来,后面的功能模块、数据库表、接口设计就都有了主线。
1.2 功能需求分析
基于这个定位,我把系统拆成三个端:前台门户、用户中心、后台管理。
前台门户解决"看什么"。首页做轮播图推荐,放置热门科普文章和精选视频;项目百科按照冬奥会的15个分项来组织内容,每个分项有项目介绍、竞赛规则、比赛场馆、历史渊源;视频专区负责冬奥科普短视频的播放和展示。
用户中心解决"学得怎么样"。注册登录后可以参与知识竞答,系统会随机抽取题目组成一套试卷,交卷后立即判分,分数计入排行榜。答题记录在个人中心里可以随时回看,同时用户可以收藏感兴趣的科普文章,形成自己的知识清单。
第三个是后台管理,也就是管理员视角。管理员负责内容发布、文章审核、视频上传、题目维护、用户管理、数据统计。我特别强调审核这个环节,因为科普平台面向大众,内容准确性很重要,不能谁都能直接发文章,管理员审核通过后才能上线展示。
1.3 用户角色与权限规划
系统按角色分为游客、注册用户、管理员三种,权限边界必须清晰。
游客只能访问公开的科普内容和搜索结果,权限最小;注册用户在前台内容的基础上,多了答题、收藏、评论、查看个人中心的功能;管理员则拥有后台的所有操作权限。这里我用SpringBoot的拦截器加角色标记来解决,登录时把角色写进JWT,接口层用注解做权限校验。
页面规划就顺着权限走:游客落地页是首页,注册用户落地页是知识广场,管理员落地页是后台工作台。每个人进来看到的第一屏都不一样,这个细节对答辩加分很关键,能直接体现你考虑过用户分层。
2. 技术选型与架构设计
2.1 SpringBoot版本怎么选
版本这块我得先说一句:别盲目追新。我项目里用的是SpringBoot 2.7.18,不是3.x。为什么?因为2.7.x是javax命名空间,JDK 8直接跑,网上资料最多,各种中间件版本的兼容性最成熟;3.x切到jakarta命名空间后,很多老教程和老依赖会踩坑。2026年做毕设,2.7.x依然是最稳妥的选择。
顺带讲一下SpringBoot为什么省事,这几乎是必被问到的点。它靠的是自动装配机制:启动类上的@SpringBootApplication是一个组合注解,里面包含了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。SpringBoot通过META-INF下的spring.factories文件加载大量的AutoConfiguration类,再用@ConditionalOnClass、@ConditionalOnMissingBean这类条件注解判断"当前环境下要不要装配这个配置"。比如你引入了redis依赖,RedisAutoConfiguration就生效,自动创建RedisTemplate的Bean。这就是为什么我们不需要写一堆XML配置。
2.2 数据库设计与持久层方案
持久层我选的是MyBatis-Plus,理由很简单:单表CRUD不用手写SQL,内置分页插件,代码生成器一键生成实体、Mapper、Service,能把大量重复劳动省下来。
数据库用MySQL 8.0。核心表我设计了7张:用户表user、管理员表admin、文章分类表category、科普文章表article、视频表video、题目表question、答题记录表answer_record。另外加了一张用户收藏表user_collection。这里给两个关键表的结构做参考。
文章表article的关键字段:
- id:主键
- title:文章标题
- category_id:所属项目分类
- content:文章正文,LongText类型
- cover_image:封面图地址
- view_count:浏览量
- like_count:点赞数
- status:状态,0草稿、1待审核、2已发布
- create_time、update_time:时间字段
题目表question的关键字段:
- id:主键
- category_id:所属项目分类
- question_type:题型,0判断题、1单选题
- title:题干
- option_a到option_d:选项,判断题只用A和B
- answer:正确答案
- analysis:答案解析
- difficulty:难度等级
MyBatis-Plus的分页插件用法也顺手说下。先在配置类里注册一个MybatisPlusInterceptor Bean,在里面添加PaginationInnerInterceptor,然后在Mapper层接收Page参数即可:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询代码就是按IPage传参:
Page<Article> page = new Page<>(current, size); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Article::getStatus, 2).orderByDesc(Article::getCreateTime); articleService.page(page, wrapper);在MyBatis-Plus里分页查询的核心诀窍是:如果你写了自定义SQL并且带分页,Mapper接口的方法参数里必须有一个IPage类型的参数,否则分页不会生效。这个坑我后面还会在踩坑实录里提。
2.3 缓存、文件存储与前端方案
Redis我用在两个场景:一个是热门文章的缓存,另一个是答题排行榜的存储。热门文章列表是首页高频访问数据,每次都查MySQL完全没有必要,文章发布后写进缓存,设置2小时过期。排行榜用Redis的ZSet结构,member是用户ID,score是答题总分,天然支持按分数排序,省去数据库的排序压力。
文件存储没有引入MinIO,因为科普平台是毕设体量,图片和视频用本地磁盘存储就足够了。我在application.yml里配置了一个自定义的上传目录,通过SpringBoot的WebMvcConfigurer做静态资源映射,让upload目录下的文件能通过URL直接访问。如果以后要扩展成生产级系统,再平滑替换成MinIO或OSS,接口设计时把文件访问统一走一个返回URL的方法,这点很重要。
前端我用的Vue3加Element-Plus,前后端分离开发。Vue负责页面交互,SpringBoot只提供JSON接口。不过我也要提醒一句:如果前端基础不够,用Thymeleaf模板引擎做服务端渲染也能完成这个项目。Thymeleaf的好处是没有跨域问题、不用单独部署Node环境,但前后端分离的架构在答辩时更能体现工程化能力,团队协作也方便。有精力就上Vue,时间紧就用模板引擎,不要因为追求架构导致完不成。
2.4 项目目录结构设计
一个好的目录结构能让后期维护少掉很多头发。我的后端工程是这样分的:
com.example.olympics ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 数据传输对象 ├── vo // 视图对象 ├── config // 配置类,跨域、拦截器、序列化 ├── common // 通用类:结果封装、异常处理、常量 ├── utils // 工具类:JWT、文件上传 ├── interceptor // 登录拦截器 └── OlympiCsApplication.javacontroller层只负责参数接收和结果返回,不写业务逻辑;service层处理具体业务;mapper层只写SQL或MyBatis-Plus的封装方法。dto和vo分开的原因也值得说一句:前端需要什么字段,vo就给什么字段,不直接把entity暴露出去,避免把密码、状态码这些内部字段泄漏给前端。这个设计在工程界叫"接口隔离",答辩时是实打实的加分项。
3. 核心功能实现与关键代码细节
3.1 用户登录与JWT权限拦截
用户认证我用的是JWT配合拦截器方案。用户登录成功后,服务端生成一个token返回给前端,前端把token存在localStorage里,每次请求在Authorization请求头带上。后端拦截器统一校验token,解析出用户ID和角色,存进ThreadLocal供后续业务使用。
核心拦截器代码如下:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals("OPTIONS")) { return true; } String token = request.getHeader("Authorization"); if (token == null || "".equals(token)) { throw new BusinessException(401, "未登录,请先登录"); } // 解析JWT,拿到用户信息 Claims claims = JwtUtil.parseToken(token); UserContext.set(claims.get("userId"), claims.get("role")); return true; } @Override public void afterCompletion(...) { UserContext.clear(); } }这里有个容易忽略的细节:ThreadLocal一定要在请求结束后清理,使用afterCompletion方法执行clear,否则Tomcat线程池复用时会把上一个请求的用户信息带到下一个请求。我不止一次见过有人漏掉这步,结果用户A能看到用户B的收藏列表,属于严重的安全事故。
密码存储我用的BCrypt加密,不用MD5加盐。BCrypt是spring-security-crypto里现成的工具,每次加密结果带随机盐,即使两个用户密码相同,密文也不一样,更安全。JWT的密钥放在配置里,用Base64编码的随机字符串,主密钥不硬编码在代码中,这是原则问题。
3.2 首页内容聚合与关键词检索
首页要聚合轮播图、热门文章、推荐视频、项目分类导航几个数据源。我最开始是每个接口各查一次,前端依次请求,后来发现接口多了加载慢,就把首页聚合做成一个接口:后端用CompletableFuture并发查文章、视频、分类,汇合后返回。并发查的效率比串行快了不少,也展示了"为性能做设计"的思路。
检索功能给了两个方案。原始方案是MySQL的LIKE模糊查询,简单直接,数据量在几千条时完全够用;进阶方案是引入Elasticsearch或HanLP分词,能对搜索词做语义匹配和分词处理。对于毕设体量的科普平台,LIKE加倒序已经能覆盖使用场景,但要理解ES方案的存在意义,万一答辩老师问起来,能说出区别就是加分项。我实际用的是LIKE加复合条件,同时支持按分类过滤、按标题搜索、按内容搜索。
热门文章的缓存策略我花了点心思。Redis存的key设计成hot_article_list,每次查询先查缓存,有就直接返回,没有就查库再回填,并且给key设置2小时过期。文章浏览量更新不直接写库,先生自增Redis里的计数器,定时任务每5分钟把增量同步到MySQL。这一步跳过了很多高并发教程讲的复杂方案,但对个人项目来说,性价比最高。
3.3 知识竞答与排行榜模块
答题模块是互动性的核心,也是我最喜欢写的一块。
题库维护交给后台,题目按冬奥项目分类,每道题有题型、选项、正确答案、答案解析和难度。用户每次答题,后端从题库里按分类随机抽取10道题组卷,确保每次进来题目顺序和内容都不一样。前端把用户答案以数组形式提交,后端逐个判分,然后生成答题记录并计算总分。
判分核心逻辑:
public QuizResult submitAnswer(QuizSubmitDTO dto) { List<Question> questions = questionService.listByIds(dto.getQuestionIds()); int score = 0; for (Question q : questions) { String userAnswer = dto.getAnswers().get(q.getId().toString()); if (userAnswer != null && userAnswer.equals(q.getAnswer())) { score += q.getDifficulty(); // 难度越高,分值越大 } } // 保存答题记录 answerRecordService.saveRecord(dto.getUserId(), dto.getQuestionIds(), score); // 更新排行榜 redisTemplate.opsForZSet().incrementScore("quiz_ranking", String.valueOf(dto.getUserId()), score); return new QuizResult(score, totalQuestions); }这里我给不同难度的题目设置了不同分值,简单题5分,中等题8分,难题10分,排行榜的名次就更有区分度。用户答题后能看到正确答案和解析,这个设计既能吸引用户反复挑战,又达到了科普的目的。排行榜页面直接用ZSet的反序查询,两条代码搞定:
Set<ZSetOperations.TypedTuple<String>> range = redisTemplate.opsForZSet().reverseRangeWithScores("quiz_ranking", 0, 49);排行榜分数存在Redis里,同时每周把快照持久化到MySQL,防止Redis重启数据丢失,这个备份机制在上线后很关键。
3.4 后台内容管理与XSS攻击防护
后台编辑器我选了wangEditor,富文本编辑器。这里必须提一个安全问题:富文本内容里可以被注入JavaScript脚本,也就是XSS攻击。SpringBoot默认不会帮你过滤参数,必须自己处理。
我的做法是写一个全局过滤器,对提交的content参数做白名单式清洗。简单说就是保留下正常教学需要的标签,把script、iframe、onerror这类危险内容和属性全部剔除,同时结合HttpServletRequestWrapper的包装机制,在参数进入Controller之前就完成过滤。
public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); chain.doFilter(xssRequest, response); } }XssHttpServletRequestWrapper里重写getParameter和getInputStream方法,对富文本内容做标签清洗。注意上传PDF、图片这类二进制内容时不要走XSS过滤链,否则文件内容会被破坏,这个坑在热词里也有人提到。我的处理方式是仅对参数类型为文本格式的字段启用过滤,白名单之外一律转义。
文件上传部分要注意两点:一是限制上传文件大小,spring.servlet.multipart.max-file-size设置成10MB,避免大文件拖垮服务;二是上传类型校验不能只靠前端,后端要做二次校验,通过扩展名和文件头两种方式判断真实类型,防止伪造文件上传。
3.5 视频科普与转码方案
视频科普是平台的重头戏之一。最直接的做法是上传MP4,前端用video标签播放,成本最低。但我做了个加分项:用FFmpeg把视频转成m3u8分片格式,前端通过hls.js播放。
为什么要转码?因为m3u8是流媒体协议,边下边播,初始加载快。MP4则通常需要下载完整文件才能拖动播放,用户网速不好时看视频特别卡。FFmpeg转码命令很简单:
ffmpeg -i input.mp4 -codec copy -bsf:v h264_mp4toannexb -m3u8 slist 1 -hls_time 10 -hls_list_size 0 output.m3u8我把转码做成异步任务,视频上传后立即返回上传成功的URL,后端线程池排队处理转码任务,转码完成后更新视频状态。这样做的好处是,管理员上传大视频时不会被请求阻塞,用户体验和后台体验都好。考虑到视频数量不大,转码失败的视频重新跑一次任务就行,不需要搞复杂的任务调度队列。
4. 项目部署、测试与常见问题排查
4.1 本地开发调试环境搭建
开发环境我用IDEA加Maven,Java版本选了JDK 8,配合SpringBoot 2.7.18。Maven依赖下载慢的问题,在settings.xml里配置阿里云镜像可以大幅提速。
接口调试这块我没有用Postman,而是接入了knife4j,这是swagger的增强版,界面好看、分组清晰。SpringBoot里只需引入依赖并加一个@EnableKnife4j注解,访问/doc.html就能看到所有接口文档,还能在线调试。这个工具对写接口联调实在太友好,一个Controller写完就能在页面上直接传参测试,前端同事也不用追着问接口参数。
另外必须开热部署devtools。不加这个,每次改Java代码都要重启服务,开发效率至少低三分之一。devtools的配置方式是引入spring-boot-devtools依赖,开发时它会监听classpath变化自动重启应用。注意它只在本地开发时启用,打包生产时排除掉,不然线上每次配置文件变化都会触发重启。
4.2 Docker部署SpringBoot实战
部署这块我用Docker加docker-compose编排,一台服务器从零到整个系统跑起来大概需要三条命令。
先写后端服务的Dockerfile,我用了多阶段构建,第一个阶段用maven容器编译打包,第二个阶段用JRE镜像运行,镜像体积能小很多:
FROM maven:3.8-openjdk-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jdk-alpine WORKDIR /app COPY --from=builder /app/target/olympics-platform.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]然后是docker-compose编排,把MySQL、Redis、后端服务、Nginx四块串起来:
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: olympics_db volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" redis: image: redis:7.0 ports: - "6379:6379" app: build: . depends_on: - mysql - redis ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod nginx: image: nginx:1.25 volumes: - ./nginx.conf:/etc/nginx/nginx.conf - dist:/usr/share/nginx/html ports: - "80:80" depends_on: - appNginx负责静态页面和反向代理,前端构建产物放入镜像,/api路径代理到后端的8080端口。上传的图片和视频单独挂载一个数据卷,这样容器重建后文件还在。数据库密码和JWT密钥这类敏感配置不要直接写在docker-compose里,用环境变量方式是更好的实践。
4.3 高频踩坑记录速查表
这个列表是血的教训换来的,每一条都对应一次实际排障。
| 问题现象 | 排查思路 | 解决方法 |
|---|---|---|
| 前端请求跨域报错 | 检查后端Cors配置 | 写一个WebMvcConfigurer的addCorsMappings,或加@CrossOrigin |
| 数据库连接报时区错误 | 连接串缺时区参数 | URL加serverTimezone=Asia/Shanghai |
| Redis存中文变成乱码 | 默认JDK序列化器问题 | 配置StringRedisSerializer和Jackson序列化器 |
| 分页不生效,返回全量数据 | Mapper方法缺IPage参数 | 确保IPage在参数列表里且作为第一个参数 |
| 上传大视频转码报错 | 请求大小超限 | 调大multipart max-file-size和Tomcat的max-swallow-size |
| 本地启动端口占用了 | 其他进程占8080 | 换端口或用lsof -i:8080查出占用进程 |
分页不生效是最隐蔽的。如果你用MyBatis-Plus写自定义SQL,比如多表联查带分页,Mapper方法里如果没有Page参数,插件根本不会执行分页逻辑,最后返回全部数据。查半天发现是少传了一个参数。
另外再分享一下源码备份的教训。有一次我误删了项目的部分源码,只剩一个打好的jar包,最后是借助反编译工具还原了类结构才找回关键代码。所以一定记得用Git管理工程,打包后的jar包也保留一份,双保险。这里想提醒所有做毕设的同学:与其指望反编译,不如每次改完代码顺手git push。
4.4 系统测试巡检清单
最后说说测试。很多毕设做完了功能,但整体跑一遍总会发现零零散散的问题。我列了一个检查清单,上线前按顺序排查一遍,能挡掉80%的现场翻车事故。
按角色走流程:先游客访问首页、看文章、看视频、搜索关键词;再注册新号登录,写收藏、答一轮题、查看排行榜、检查个人中心的答题记录;最后切管理员账号进后台,试发布一篇文章、上传一条视频、改一道题、审核一篇待发布内容。每个环节都带数据对比,比如浏览量是否自增、积分是否入榜、审核前后前端是否可见。
性能层面重点关注首页响应时长,打开浏览器Network面板观察聚合接口的耗时。如果大于2秒,优先看是否有循环查库的坏味道,其次看Redis缓存有没有生效。Redis是否命中,可以通过查询接口前先查看Redis是否有值的日志来验证。
安全层面必查入口:未登录直接调用户中心和加分机制接口应被拦截,管理员接口游客不能访问,上传接口拒绝非白名单格式。这四项在答辩演示时是老师必点的雷。
最后的经验之谈
这套科普平台做完,我个人最大的收获不是背熟了SpringBoot的API,而是搞清楚了一个完整项目是怎么从需求长成代码的。科普类系统的核心逻辑是"内容组织加用户互动",文章、视频、题库这三类内容资产通过分类和标签建立连接,再通过答题、收藏、排行这些激励机制让用户留下来。这个模式不是冬奥限定,你换成环保科普、传统文化科普、健康知识科普,照样能跑。
如果你也要做类似的课题,我的建议是别一上来就写代码,先用两天把角色、功能、数据表三条线捋清楚。功能可以砍,但每条数据表之间的关系必须想明白。实际编码时优先把登录权限和后台管理做完,再补前台页面,因为后台管理承载了系统最复杂的操作逻辑,做完了它,前台只是数据的展示层。
动手之前记得配好Git仓库,每完成一个模块提交一次。答辩前的晚上你会感激这个习惯。就这些,祝你的SpringBoot科普平台一次跑通。