Spring Boot游戏评级论坛实战:JWT鉴权、MyBatis-Plus与Redis缓存设计

发布时间:2026/10/11 13:24:24
Spring Boot游戏评级论坛实战:JWT鉴权、MyBatis-Plus与Redis缓存设计
这段时间刚好在帮一位同学调试基于Spring Boot的游戏评级论坛毕设项目前前后后花了差不多一周。这套项目乍看名字只以为是“论坛”等真正上手才发现里面藏了不少功能点游戏信息管理、玩家评分、短评聚合、论坛发帖、帖子回复、个人中心、后台数据统计、管理员权限控制几乎把Web开发常见的内容都过了一遍。我把自己从拿到源码到最终跑通、甚至动手做二次扩展的完整过程和踩坑记录整理出来希望对正在做类似课题、或者打算拿这套项目应付毕业设计的同学有些参考。整体说的是“游戏评级论坛”技术栈和实现思路却有很强的通用性里面的评分模型、论坛版块、角色权限、缓存设计几乎可以平移成书评、视频点评、资源分享社区。1. 项目概述与需求拆解1.1 游戏评级论坛到底要解决什么问题游戏玩家群体中最常见的需求不是单纯“聊天”而是想快速了解一款游戏的口碑和实际体验。传统论坛偏重帖子交流缺少结构化评分而评分网站往往只能打分、不能深入讨论。这套项目想做的就是把两种玩法合并成一个平台用户既可以给游戏打一个客观分数、写一段简短评价又可以在游戏详情页下面进入对应的主题版块围绕某一个版本、某一个玩法机制发帖讨论。这样一融合数据的“厚度”就出来了。对我来说毕设项目最怕的是看起来功能很多、实际上每个模块都很浅。这套“游戏评级论坛”算是很典型的反面类型因为它天然有多个实体用户、游戏、评分、评论、帖子、回复、后台操作记录。把这些实体之间的关系画清楚代码结构自然就出来了答辩时老师问任何一条业务线都有东西可讲。1.2 功能需求梳理先把前台和后台拆开。前台面向普通注册用户核心功能包括用户注册、登录、退出、个人中心资料修改浏览游戏列表按类型、发布时间筛选查看游戏封面、开发商、评分均分、评价数量给游戏打分1到10分写一条最多200字的短评同一个用户对同一款游戏只能存在一条评分第二次打分等价于修改旧评分查看游戏详情下的评分列表与评论内容论坛板块浏览、帖子列表、发新帖、帖子详情查看、楼层回复我发布的帖子、我打过的评分记录后台由管理员操作功能包括管理员登录与权限校验普通用户访问题后台接口直接拒绝游戏的新增、编辑、删除封面图上传上下架状态管理用户管理查看注册列表禁用违规用户帖子管理置顶、加精、驱逐删除把垃圾帖子移出公开列表评论与评分审核处理恶意灌水内容简单统计例如游戏评分分布、论坛发帖趋势从实现难度看这套需求属于“麻雀虽小五脏俱全”没有特别难的算法但CRUD覆盖面很广前端也需要配合做出完整的交互流程。毕业设计评分往往很看重“完整的闭环”即用户能注册、登录、操作数据、数据能反馈到页面上、管理员能管理这个项目刚好全覆盖。1.3 技术选型思路与理由项目核心是Spring Boot这几乎是目前Java毕设的大趋势。相比多年前流行的SSH、SSMSpring Boot省去了大量XML配置自动装配能力强哪怕不深入了解底层原理也能快速把项目跑起来。但“能跑”和“做得好看”是两回事我建议在标准Spring Boot基础上做两件事第一引入MyBatis-Plus作为持久层框架单表CRUD直接用内置方法无需手写繁琐的Mapper接口和XML文件把更多精力留给业务逻辑第二引入Redis作为缓存内容不复杂但能给项目增加“中间件使用”的得分点。前端方面我选择Vue 2 Element UI Axios前后端分离。原因很现实这类项目通常需要远程调试前后端分离以后后端接口能单独在Postman里测试前端页面也能独立开发出了问题容易定位。如果时间特别紧也可以把Vue打包生成静态文件放进Spring Boot的static目录用同一个端口访问相当于伪前后端分离但代码目录还是分开的互不干扰。数据库选MySQL 8.0这是目前的保守选择兼容性比5.7好后面我会专门说时区引起的坑。整体架构就是短小精悍的三件套Spring Boot MyBatis-Plus MySQL缓存用Redis权限用JWT。没有引入微服务、消息队列这些重型组件原因也有一个毕设重点在于逻辑完整而不是技术堆砌堆太多自己解释不清楚反而容易被答辩老师追问到死角。2. 数据库设计与核心模型2.1 核心表划分与职责数据库设计是整个项目的地基很多早期集成Bug都源于表关系没理清。我的建议是先建模型图再动手写SQL。整个项目一共规划八张核心表user用户账号信息同时承担前台用户和后台管理员两种角色通过role字段区分game游戏本体信息rating用户对游戏的评分和短评一个用户对一个游戏至多一条记录game_comment如果希望评分和评论分离可以单独设计如果希望简单点评分和短评共用rating表本套代码选择的rating表直接存score与content字段forum_board论坛板块分类例如“新游讨论”“PC综合”“主机专区”forum_thread论坛主题帖子归属某个板块记录发布人、标题、正文、浏览量、回复量forum_reply帖子下的楼层回复admin_log管理员操作日志记录谁在什么时间删了什么内容其实还可以加一张user_like表做评论点赞但考虑到毕设体量非核心功能不要过多否则写数据迁移脚本都嫌烦。精悍的表结构比大开大合的设计更容易维护。2.2 关键表结构设计细节user表的结构如下字段包括用户id、用户名username、密码password_hash、昵称nickname、头像地址avatar、邮箱email、手机号phone、角色roleUSER或ADMIN、状态status0禁用1正常、注册时间create_time、最近登录时间last_login_time。username需要加唯一索引登录时才能快速定位。密码字段一定要存加密后的hash明文密码即使只是毕设项目也属于很低级的错误答辩时被批评一点都不冤。rating表是评分系统的核心设计上有几个值得讲的点使用联合唯一索引unique(user_id, game_id)这是保证用户只能评一次分的数据库层面的硬约束每次用户点击“重新评分”服务端应该先查询是否存在存在则执行update否则执行insert这就是典型的“不存在则插入存在则更新”分数字段score设置为tinyint类型取值1到10后端校验时再限定一次content字段存放短评长度限制为200字符防止超长文本写入冗余了create_time和update_time方便统计每日评分活跃度game表里我特意保留了avg_rating和rating_count两个冗余字段。没有这两个字段每次展示游戏列表都需要对rating表做聚合计算游戏数量一多查询速度就急转直下。有了冗余字段评分接口在写入rating表的同时用一条update语句把游戏的总评分和评分人数更新掉这样列表页直接读game表就行。实际测试下来这种“空间换时间”的取舍非常适合数据量一般的毕设场景。forum_thread表设计了status字段用0表示正常、1表示置顶、2表示逻辑删除。之所以不物理删除帖子是因为后面做后台“回收站”功能很方便。reply_count字段也是冗余每次回复成功后自增避免连表count。2.3 评分、热度权重设计评分均分计算看起来简单所有评分加总除以评分人数。但还需要考虑两个问题一是过快更新的准确性二是刷分风险。我在代码里对“同一个用户重新评分”做了处理如果用户之前打过分再次提交评分时直接用新值覆盖旧值同时重新计算均值。这样用户可以在玩了几十小时游戏后修正自己的评价逻辑上更合理。热度排序不能单纯看发帖时间否则老帖子永远没机会翻出来。我设计了一个heat_score字段在每次帖子被访问、被回复时重新计算。公式很简单也适合答辩解释heat_score view_count * 0.3 reply_count * 0.5 rating_count * 0.2这个公式给回复的权重最高因为回复代表有效互动浏览量次之评分人数作为补充。如果希望时间衰减效果可以再加一个“最后回复时间”作为排序辅助但毕设阶段上面的公式已经足够清晰。最开始我忽略了评分人数的热度价值后来两个人气很高但评分少的游戏一直排名靠后才把rating_count加进来效果明显变好。3. 核心功能实现详解3.1 登录注册与JWT鉴权毕业设计里会话管理有两条路线传统Session和JWT。这个项目采用前后端分离所以我选了JWT。服务端在登录成功后通过一个secret_key生成包含用户id、用户名、角色、过期时间的token字符串前端把它保存在localStorage里每次Axios请求在拦截器中加上Authorization头。JWT不是完全没有缺点最常见的问题是“无法主动让token失效”。我的处理方式是把正常退出的token加入一个Redis黑名单存key名token_blacklist:实际token过期时间与token本身一致这样退出后很短时间内外界无法继续使用旧token访问接口。虽然不能做到精确秒级失效但对毕设来说已经足够。注册接口需要做三个基本校验用户名非空、长度3到20位、用户名唯一。密码用BCrypt加密存储登录时通过PasswordEncoder校验输入的明文密码和数据库里的密文是否匹配。Spring Boot里只要引入spring-security-crypto依赖不需要引入完整Spring Security就能使用BCryptPasswordEncoder避免权限过滤器过多干扰业务接口。Token验证我选择写一个HandlerInterceptor实现而不是在每个Controller里手动解析。拦截器里先从请求头读取Authorization没有就直接返回统一错误token过期或黑名单命中则返回401状态码。角色校验用另一个注解或者判断用户role字段后台接口统一加上路径前缀/admin写拦截器时按路径前缀决定是否校验管理员身份非常省事。3.2 游戏评分与评论实现评分接口的完整流程做了四步第一步从JWT解析出当前用户id确保用户已登录。 第二步接收gameId和score参数校验游戏存在、分数在1到10之间短评长度不超过200。 第三步按user_id和game_id查询rating表如果存在记录则更新score和content否则插入新记录。 第四步更新game表的rating_count和avg_rating这一步必须和第三步放在同一个事务里否则会出现评分表写成功但统计字段没更新的情况。这里我踩过一个经典坑没有加事务后期在并发测试时出现游戏评分人数偶发对不上。其实严格来说高并发下“先查后插”本身也有竞态问题但在毕设场景里单纯加Transactional已经足够。如果想要更强健可以在插入时用ON DUPLICATE KEY UPDATE实现原子化但对于MySQL 8.0和低并发场景不额外复杂化也说得过去。核心Service方法大致是这个样子Transactional(rollbackFor Exception.class) public ApiResult submitRating(RatingRequest request, Long userId) { Rating rating ratingMapper.selectOne( new LambdaQueryWrapperRating() .eq(Rating::getUserId, userId) .eq(Rating::getGameId, request.getGameId())); if (rating null) { rating new Rating(); rating.setUserId(userId); rating.setGameId(request.getGameId()); rating.setScore(request.getScore()); rating.setContent(request.getContent()); ratingMapper.insert(rating); gameService.increaseRatingCount(request.getGameId()); } else { rating.setScore(request.getScore()); rating.setContent(request.getContent()); ratingMapper.updateById(rating); } gameService.recalculateAvgRating(request.getGameId()); return ApiResult.success(评分成功); }这里没有把重新评分的判断拆太细但逻辑层通过Transactional保证了数据库一致性。注意在压测或并发不高时不至于出大问题但如果要做生产级还需要在UserRating表中加入乐观锁版本号这是后话。3.3 论坛发帖与回复流程论坛模块比评分模块更贴近传统Web开发。发帖接口处理的数据包括版块id、标题、正文、图片附件地址。前端通过富文本编辑器生成HTML内容后端必须做两件事第一过滤危险标签第二限制内容长度。我使用Jsoup做白名单过滤只保留p、br、img、a、ul、ol、li等常用标签并给a标签强制添加relnofollow属性。这块如果不做之后可能被恶意用户植入脚本属于安全常识。帖子详情页的浏览数1我采用“同步1”方案也就是每次点击详情后直接update forum_thread set view_count view_count 1。虽然会多一次数据库写操作但实现简单。更进一步的做法是先用Redis自增保存浏览数等阈值到了一定数量再批量写入数据库但那在毕设里属于性能优化的加分项而非必须项。回复功能稍微复杂一点除了插入forum_reply记录还要同步更新forum_thread的reply_count同样需要事务控制。回复表本身只要有楼层号、回复内容、用户id、帖子id、创建时间即可。我还加了last_reply_time字段在帖子上用于“最新回复”排序效果比单纯按发布时间排序好不少页面活跃度一下就上来了。发帖前需要判断当前用户是否被禁言也就是查user表的status字段。这里一定要做后端校验不能只在页面上隐藏输入框因为懂接口的人可以绕过前端直接请求。3.4 后台管理模块后台管理的核心是权限和数据组织。我用数据库中的role字段区分管理员和普通用户约定后台接口的前缀为/admin。登录后JWT里带有role字段拦截器每次在放行前检查请求URI是否以/admin开头如果是就校验当前用户的角色是否为ADMIN否则直接返回403。游戏管理界面应该包含游戏名称、类型下拉、开发商、发行日期、封面图、简介文本域。封面图上传接口我选用了本地磁盘存储方案服务器把文件写到项目配置的uploadPath目录下然后把访问路径存到game表。浏览器访问图片时通过一个映射路径读取例如/upload/**映射到本地目录。这个方案不依赖外部云存储部署环境只需保证目录存在且有写权限。帖子管理里常做操作是置顶和精华。置顶本质是把thread表的top_flag改成1列表排序时ORDER BY top_flag DESC, create_time DESC精华则用essence_flag字段。如果管理员删除了某个帖子我建议采用逻辑删除保留数据用于后期可追溯。删除操作完成后写一条admin_log日志记下管理员id、操作类型、目标id、操作时间这样答辩时可以拿出来说明“全流程留痕”。数据统计模块可以做得轻量例如折线图展示近七天的注册数和发帖数饼图展示游戏类型分布。后端用一组聚合查询语句前端用ECharts画图代码量不大但视觉效果很好属于性价比超高的功能点。4. 实操过程与部署细节4.1 项目搭建与依赖配置Git新建项目根目录打开IDEA创建Spring Initializr项目。常规选择Java 8即可比Java 17兼容性更好尤其很多导师机器上的老旧环境。依赖栏先勾选Web、MySQL Driver、Redis、Validation、Lombok项目生成后再手动向pom.xml添加MyBatis-Plus和JWT的依赖。我用的关键依赖版本大致如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency /dependencies配置application.yml时数据源URL一定要加serverTimezoneAsia/Shanghai否则启动时MySQL驱动大概率报时区错误。Redis在本地开发时默认无密码生产服务器上如果设置了requirepass需要在spring.redis.password里填上对应值。MyBatis-Plus有两处必要配置一是逻辑删除的全局配置二是分页插件注入。很多同学抄了代码但还是分页无效基本都是因为没有把PaginationInnerInterceptor注入到MybatisPlusInterceptor中。4.2 关键接口实现示例评分的Controller层我会写成RestController RequestMapping(/api/rating) public class RatingController { PostMapping public ApiResult submit(RequestBody RatingRequest request, RequestAttribute(userId) Long userId) { return ratingService.submitRating(request, userId); } GetMapping(/game/{gameId}) public ApiResult listForGame(PathVariable Long gameId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { return ratingService.pageByGameId(gameId, page, size); } }登录用户id是从拦截器放行时通过request.setAttribute塞进去的这比在Controller里手动从token解析清爽得多。之后每个接口只要加上RequestAttribute(userId)就能拿到当前用户。论坛列表接口我用了MyBatis-Plus的Page对象分页查询结果里会自动带上total和pages前端据此渲染分页条。排序逻辑是置顶帖优先、最新回复优先。写查询时不要直接查全表条件里一定要有boardId和status过滤否则后台被逻辑删除的帖子也会露出来。4.3 远程调试与讲解经验毕设交付很常见的工作场景是甲方拿到源码后在自己的服务器上部署跑不通就需要远程调试。我的做法是在启动命令里加上调试参数java -jar game-rating-forum.jar \ -agentlib:jdwptransportdt_socket,servery,suspendn,address8000然后在IDEA的Run Configuration里创建Remote JVM DebugHost填服务器IPPort填8000就能像本地调试一样打断点了。要注意的是调试端口千万别暴露公网最好用SSH端口转发或者只允许内网IP访问否则会有被别人连入调试端口的安全隐患。给同学讲解项目时我习惯按照“一条完整业务线”来讲比如“从用户打开首页、点击游戏、看到评分、到发帖评论”的完整链路走一遍前端请求、后端Controller、Service、Mapper、数据库。这个方法讲起来最直观也最容易被理解。尽量不要一上来就拽微服务、分布式那些词听着高级但和本项目关系不大讲不透反而适得其反。4.4 打包部署后端打包用Maven先执行mvn clean package -DskipTests生成target目录下的jar包。前端Vue项目执行npm run build会把资源输出到dist目录。部署时有两种方式第一种把dist里的文件拷贝到Spring Boot静态目录后端打成单jar一个端口访问所有内容第二种保持前后端分离用Nginx作为Web服务器负载转发/api路径到本地后端的8080端口我更推荐第二种但毕设演示时第一种更省事。如果数据库和Redis都在云服务器本机需要注意防火墙和安全组放行端口否则远程连数据库会白忙一场。Linux部署的关键步骤可以用下面的命令概括# 上传jar包后执行 nohup java -jar game-rating-forum.jar --spring.profiles.activeprod app.log 21 # 前端dist目录放到nginx的html目录配置location /api反向代理部署完成后先看启动日志的“Started Application”字样再确认数据表已自动建好最后再用浏览器访问。很多同学达不到这一步卡在了数据库连接上这种情况我建议先把mysqld服务启起来再确认账号有权限访问该类库。5. 常见问题与排查技巧5.1 数据库时区连接失败我在集成调试第一天就遇到了“The server time zone value”的报错。直接原因就是MySQL连接串少了时区参数。解决方案很简单在jdbcUrl末尾加上serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。如果还遇到SSL告警可以再加useSSLfalse本地开发环境完全不需要TLS。5.2 MyBatis-Plus分页插件不生效分页查询返回total为0或者分页数据始终是全部大概率是MyBatis-Plus的拦截器没注册。需要在配置类中加入Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }如果已经加了但还不生效检查一下mapper方法的参数是否接收了Page对象并且Page必须是第一个参数。顺序写错分页也会失效。5.3 前后端跨域请求被拦截本地前端跑在8081端口、后端跑在8080端口浏览器跨域会阻止请求。我在后端写了一个全局CORS配置允许来自前端的Origin并开放GET、POST、PUT、DELETE方法。注意指定maxAge为3600能减少预检请求次数提升页面体验。真正部署时用Nginx反向代理以后前后端同域跨域问题自然消失。5.4 JWT过期后前端不自动跳转很多同学实现完登录后发现token过期页面只是接口报错却没有跳回登录页。我的前端Axios拦截器里会检查response.data.code如果是401或者某个自定义未登录码就清空localStorage并跳转到/login路由。后端也需要给未登录和token过期分别返回不同状态码配合起来体验才顺滑。一开始不做的严重后果是用户在页面上看着已经申请接口却不知道要重新登录。5.5 Redis连接失败导致启动失败只要引入了Redis依赖Spring Boot在启动时就会尝试连接它连接失败会导致整个项目起不来。开发用的Redis默认使用的是端口6379如果本机没装Redis或者装了但服务没启动项目就一直报“Connection refused”。这里有两个规避思路第一保证Redis服务在启动项目前已运行第二在项目配置里把timeout调低方便排查。放到服务器上还可能出现需要设置密码却忘了改配置导致认证失败建议统一用命令行测试redis-cli ping看看结果。写在后面的经验调试这套“游戏评级论坛”项目最深的感受是“功能越多越需要把基本功做扎实”。评分、发帖、回复、权限每一个模块看起来都能轻易说完但真正连接起来的时候隐性问题非常多。比如评分和热度的更新顺序、JWT黑名单与Redis的一致性、后台权限校验和前端路由守卫的配合稍有不慎就会出“本地能跑、换个环境就崩”的尴尬问题。我给即将使用这套源码的同学一个忠告拿到任何项目第一件事不是急着运行而是先画清楚数据表和调用关系第二件事是准备统一的响应格式因为前后端错乱的问题多发生在返回结构不统一上第三件事是重视事务和权限校验这会成为答辩时区分“套模板”和“真理解”的分水岭。如果哪天你在折腾这套项目时遇到了Bug也可以带着具体报错信息再回来聊我看到的都会尽力帮你定位。