ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Thymeleaf打造游戏百科系统:服务端渲染与数据建模实战

SpringBoot+Thymeleaf打造游戏百科系统:服务端渲染与数据建模实战 1. 技术选型的实际考量为什么这套系统我没有走前后端分离1.1 从游戏百科信息系统反推技术栈这次的项目目标很明确用 SpringBoot 基于 JavaWeb 技术栈做一套《战舰世界》游戏百科信息系统把舰船资料、国家线、舰种分类、科技树信息和版本更新内容集中起来前台提供给玩家浏览检索后台让维护人员管理词条。一开始我看到这个题目下意识觉得跟普通的信息管理系统没什么区别无非就是建几张表、写几个 CRUD 接口、套一个后台模板。但是真正开始动手以后我发现这类游戏百科项目最麻烦的地方不在框架而在数据本身——战舰世界里的舰船数据横跨多国多条科技线每条船又有等级、血量、主炮、鱼雷、防空、装甲、航速、转向半径、隐蔽性等一大堆属性光是决定一艘战舰在系统里要保存哪些字段、哪些字段需要单独建表、百科文章和舰船数据怎么关联就比写业务代码多花了三倍时间。从技术栈上看传统的 JavaWeb 项目通常指 Servlet JSP JDBC 的形态但今天再从这个套路起步意义不大。Servlet 时代的配置繁琐程度用过的人都知道web.xml 里写一堆映射项目打个 war 包丢进 Tomcat 才能跑调试一次要重启半天。SpringBoot 本质上把 JavaWeb 开发从配置驱动变成了约定驱动内嵌 Tomcat、自动装配、Starter 依赖开箱即用这些优势对于一个信息展示为主的项目来说太合适了。我的选择是 SpringBoot 2.7.18 JDK 8 MySQL 8.0 MyBatis-Plus 3.5.3 Thymeleaf这个组合成熟稳定相关资料也最多出了问题基本都能搜到解法对新手格外友好。1.2 SpringBoot 版本与 JDK 匹配别在一开始就被版本绊倒版本选择是我最想强调的一点。很多同学在起步阶段就卡在环境上最常见的情况是打开 Spring Initializr直接选了最新的 SpringBoot 3.x 版本然后项目一启动就报错。原因很简单SpringBoot 3.x 把原来的 javax 命名空间迁移到了 jakarta大量老教程里的import javax.servlet.*在新项目里全部失效MyBatis-Plus 的旧版本也需要替换成针对 SpringBoot 3 的依赖这一轮折腾下来还没写业务代码就已经把人劝退了。我建议做这类 JavaWeb 游戏百科系统稳扎稳打选 SpringBoot 2.7.18。它支持 JDK 8兼容经典的 javax 包和 MyBatis-Plus 3.5.x 的配合非常顺畅网上能找到的示例几乎都是这套。如果后续有强制升级的需求再处理 jakarta 迁移也不迟。JDK 版本方面JDK 8 至今仍是企业级项目的主流运行环境本地开发完全够用。另一个容易踩的坑是 SpringBoot 的版本号必须和 Maven 仓库里能下载到的依赖保持一致建议用 2.7.x 的最终维护版本不要用太冷门的中间版本否则可能拉动一些不兼容的传递依赖。1.3 服务端渲染的优势百科内容更需要 SEO 和首屏速度关于是否要上 Vue 前后端分离我也纠结过一阵。现在的热门做法是 SpringBoot 只提供 JSON 接口前端用 Vue Element UI 单独搭一套后台路由、状态管理、跨域请求全部自己处理。但如果把百科信息系统这个场景放进来你就会发现服务端渲染更合适。百科网站的核心用户是来查资料的玩家他们希望通过搜索引擎直接命中某个舰船词条而前后端分离的页面内容是动态生成的搜索引擎抓取难度大、首屏等待时间长这跟百科信息系统的定位是冲突的。服务端渲染配合 Thymeleaf 模板页面数据直接在服务端拼好返回给浏览器首屏速度快URL 是真实的语义化路径对 SEO 非常友好。也更符合 JavaWeb 项目的经典形态答疑、部署、演示都直接简单。我第一次选的方案是 Thymeleaf 做前台展示后台管理页也用同一个模板引擎所有页面都走服务端渲染不写一行 JavaScript 框架代码。实际跑下来这套组合在局域网和云服务器上体验都很流畅。2. 数据建模过程一艘战舰需要保存哪些字段2.1 核心表 ship 的设计与字段取舍关于数据建模整个项目最关键的部分是舰船表。我第一版设计的时候想把这些字段全部塞进一张表里——舰名、国籍、舰种、等级、血量、主炮、副炮、防空、鱼雷、航速、转向半径、隐蔽性、装甲数据、升级模块、历史背景介绍。写了一半发现这种做法最大的问题不是字段多而是游戏版本一更新舰船的数据会发生变化如果只保留当前值历史版本的数据就丢失了。参考百科类网站的做法我把舰船基础资料和版本状态拆开舰船表保存相对稳定的基础信息如舰名、类型、国家、等级而血量、主炮参数这些会随平衡性调整的数据放在另一个版本关联表中。实际的舰船表结构类似这样CREATE TABLE ship ( id BIGINT NOT NULL AUTO_INCREMENT, cn_name VARCHAR(100) NOT NULL COMMENT 舰船中文名, en_name VARCHAR(100) DEFAULT COMMENT 舰船英文名, nation_id BIGINT NOT NULL COMMENT 所属国家, type_id BIGINT NOT NULL COMMENT 舰种战列舰/巡洋舰/驱逐舰/航母, tier TINYINT NOT NULL COMMENT 等级 I-X, is_premium TINYINT DEFAULT 0 COMMENT 是否金币船, intro TEXT COMMENT 简介, status TINYINT DEFAULT 1 COMMENT 1正常 0下线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_nation_type_tier (nation_id, type_id, tier) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT舰船基础表;这里有个细节值得注意cn_name字段我加了普通索引之外的联合索引idx_nation_type_tier因为列表页最常见的查询场景就是按国家舰种等级过滤这个联合索引可以让数据库直接走索引过滤而不是全表扫描。很多毕业设计项目不考虑索引问题数据量几百条时没关系一旦词条库扩展到几千艘舰船加上百科文章没有索引的查询就会明显变慢。2.2 关联表和扩展表类型、国家和百科文章舰船表之外我建了nation、ship_type、article三张核心表。国家表用来管理游戏里的阵营线每条国家线有名称、旗帜图标、背景介绍舰种表管理战列舰、巡洋舰、驱逐舰、航空母舰这几个大类以及每个大类下的细分说明。这两张表属于典型的字典表数据量不大但在前端筛选菜单、详情页展示分类信息时都会用到单独建表的好处是后续如果游戏新增舰种不需要改代码只要往表里插入一条记录前台菜单就能自动多出一个选项。百科文章表的设计比舰船表简单一些但核心要解决的是一篇文章如何关联多个舰船词条的问题比如一篇介绍美国战列舰发展史的文章需要关联到多个具体舰船。我最初想直接在文章表里加一个ship_ids字段用逗号分隔存储这样查询最简单但后来发现如果引用的舰船被下线或者改名文章内容很容易出现死链。最后我用了多对多关联表article_ship_relation来维护关系虽然写代码时多了一步关联查询但从数据完整性角度看是值得的。2.3 初始化数据的两种方式手动 SQL 和启动导入系统做好以后面临一个很现实的问题数据从哪来。百科信息系统没有数据就是一个空壳演示效果很差。我采用了两种方式配合。第一种是准备一份完整的初始化 SQL 脚本包含国家、舰种、二十多艘典型舰船和几篇百科文章用于数据库的首次导入第二种是在项目里加了一个DataInitializer组件应用启动时检测舰船表为空就自动批量插入一批内置数据这样换一台新机器部署时只要数据库连接配置正确启动完就有基础数据可以展示。这里我特别想提醒一点初始化数据脚本一定要写清楚INSERT语句的字段顺序并且每条插入前最好加REPLACE INTO或者先判断是否已存在避免重复执行脚本导致主键冲突。我在实测中因为有几次手动执行脚本又重启应用结果数据重复了一遍列表页出现同一艘船出现两次的情况。后来我在初始化脚本里统一加了INSERT IGNORE才解决了这个问题。3. 前台百科功能的实现列表、筛选、详情页3.1 首页内容编排热门词条和最近更新前台首页是整个系统的门面我把它分成三个区域顶部是站内搜索框中间是热门舰船模块下面按舰种分成几个栏目展示最近编辑过的词条。热门舰船用访问量排序我在舰船表里加了一个view_count字段每次用户访问详情页就让浏览量加一。这种实现方式在并发量低的时候完全没问题高并发场景下肯定受不了每次都写库但作为教学项目和毕业设计演示简单直接才是第一原则。页面渲染上Thymeleaf 用起来比 JSP 舒服很多。首页通过 Controller 向 Model 添加数据模板里直接使用表达式读取。我专门配置了一个全局对象把国家列表、舰种列表放进去这样任何页面需要下拉菜单和筛选条件时都可以直接调用不用在每一个 Controller 方法里重复查询。使用ControllerAdvice配合ModelAttribute可以把字典数据注入到所有页面的 Model 中这是一个很实用的小技巧。3.2 舰船列表的查询与分页逻辑列表页是用户浏览舰船信息的主入口需要满足三个筛选条件按国家、按舰种、按等级并且支持关键词模糊搜索。我设计了一个ShipQuery对象封装所有查询条件Controller 接收前端参数后传给 Service 层处理。MyBatis-Plus 的LambdaQueryWrapper写条件查询非常直观用不用某个条件取决于参数是否为空Override public IPageShip pageShips(ShipQuery query) { PageShip page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperShip wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getKeyword()), Ship::getCnName, query.getKeyword()) .eq(query.getNationId() ! null, Ship::getNationId, query.getNationId()) .eq(query.getTypeId() ! null, Ship::getTypeId, query.getTypeId()) .eq(query.getTier() ! null, Ship::getTier, query.getTier()) .orderByAsc(Ship::getTier) .orderByAsc(Ship::getCnName); return shipMapper.selectPage(page, wrapper); }分页这里有个常见的坑很多人发现selectPage返回的总数不对或者分了页但还是查全部数据原因多半是 MyBatis-Plus 的分页插件没有生效。SSM 时代做分页需要手动写 PageHelper 插件MyBatis-Plus 虽然内置了分页能力但必须先把PaginationInnerInterceptor注册进容器否则分页参数会被静默忽略。这个问题我在后面踩坑记录部分会再展开。3.3 详情页的数据组装和推荐逻辑详情页是百科全书的核心在用户角度进入一艘舰船的详情页后应该看到基本信息、属性数据、历史简介、同类型或同国家的其他舰船推荐。因为这些数据分散在多张表里我单独写了一个ShipDetailVO来聚合。Service 层先根据 ID 查出舰船基础信息再查国家表拿国家名称查舰种表拿舰种名称查属性表拿该船当前版本的数据最后查推荐列表。其中推荐逻辑看似简单但很考验思路。我当时定了一个规则优先推荐同国家且相邻等级的舰船如果找不到再推荐同舰种的其他船。这样用户在一个词条页面停留的时间会明显增加因为随时可能点进旁边一条船继续浏览。对于百科网站来说这种内容之间的串联非常重要它直接影响用户访问深度。后来我在这部分加了一个limit 3查询只推荐三艘船避免页面过长让用户失去耐心。4. 后台管理模块权限控制、词条维护和图片上传4.1 管理员登录与会话拦截器后台管理模块面对的是一套独立的/admin/**路径我在里面实现管理员登录、词条管理、文章管理和数据统计。登录采用经典的 Session 方案用户提交账号密码后Service 层校验密码是否正确正确就把管理员信息写入 Session后续请求通过拦截器判断是否有登录态。密码存储方面我直接用 BCrypt 加密而不是常见的 MD5。MD5 在数据库中容易被查表碰撞破解BCrypt 内置随机盐相同密码每次加密结果不同安全性高一个数量级。很多教程为了演示方便直接用明文密码这我完全不推荐Spring Security 里自带BCryptPasswordEncoder即使不引入完整的安全框架也可以直接用它做加密。拦截器实现如下public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute(adminUser) null) { response.sendRedirect(/admin/login); return false; } return true; } }然后通过WebMvcConfigurer注册到拦截器链中只拦/admin/**同时放行/admin/login和静态资源。这样哪怕有人猜到后台 URL 也无法绕过登录页面。4.2 词条维护新增、编辑、上下线的实现套路词条维护模块本质上就是 CRUD但我在设计 Controller 时故意没有把所有方法堆在一个类里而是按资源拆了三个 ControllerShipAdminController、ArticleAdminController、NationAdminController。每个 Controller 负责自己那一类资源的维护接收参数用统一的表单对象接收并做后端校验。新增和编辑页共用一个模板页面只是通过ship.id是否为空来区分是新增还是修改提交到同一个save接口里处理这个模式在处理大量表单页面时非常省事。上线下线这个操作很有意思我一开始想做逻辑删除也就是在ship表里加一个deleted字段前台查询时只显示未删除的数据。后来发现其实不需要真正删除任何东西直接用status字段做下线就够了等于把删除操作变成状态切换。好处是可以防止误删而且一旦词条中有外部引用下线比删除带来的坏链影响小得多。演示的时候我还能故意下线一艘船给评审展示前台如何隐藏该词条后台又如何恢复。4.3 图片上传与静态资源映射游戏百科系统中舰船截图和图标非常重要。我一开始把图片上传后的路径直接存在数据库里比如/upload/ship/xx.jpg但启动应用后发现访问这个路径 404。原因很简单SpringBoot 默认只把classpath:/static/目录下的文件当作静态资源处理上传到本地磁盘的目录不在这个范围内。需要额外配置资源映射把磁盘目录映射为一个虚拟路径spring: web: resources: static-locations: classpath:/static/,file:${file.upload-path}配合一个简单配置类Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadPath); }上传文件时要控制大小SpringBoot 默认单文件上传限制是 1MB对于舰船截图这个尺寸基本不够用。我实际上传的是高清图纸和截图原图动辄几 MB所以我在application.yml里调整了两项配置spring.servlet.multipart.max-file-size10MB和max-request-size20MB。这个配置在开发阶段很容易被忽略等到上传大图直接报文件大小超限排查还要花不少时间。5. 项目跑通的关键配置和踩坑记录5.1 application.yml 里最容易忽视的配置项一套系统能不能顺利跑起来一半看代码一半看配置。先放一套我实测可用的核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/wows_wiki?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 thymeleaf: cache: false encoding: UTF-8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0配置里有三个地方是我反复被坑之后才记住的。第一个是数据库连接 URL 里的serverTimezoneAsia/Shanghai如果不设置连接 MySQL 8 时经常报时区错误因为新版驱动的时区判断依赖这个参数。第二个是characterEncodingutf8与数据库本身字符集的配合数据库、连接 URL、页面编码三者不一致时中文乱码就会反复出现。第三个是 MyBatis-Plus 的logic-delete-field配置和实体类里的TableLogic注解要配套否则逻辑删除不生效。5.2 MyBatis-Plus 分页插件未注册的排查过程做个记录以防有人重蹈覆辙我第一次写完列表页分页测试时发现 page 一直返回全部数据total 永远是总行数。当时第一反应是 SQL 写错了检查半天没错。后来去看 MyBatis-Plus 文档才发现要从 3.4 版本开始分页必须由外部注入分页拦截器框架默认没有装配这个组件。官方给的方式是在配置类里注册MybatisPlusInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注册之后重启分页立即恢复正常。这个问题看起来不起眼但如果是第一次接触 MyBatis-Plus 的人很容易在排查 SQL 上浪费大量时间。如果你发现selectPage方法查出来没有分页效果优先检查这个配置类是不是被 Spring 容器扫描到了。5.3 中文乱码与路径问题的排查过程中文乱码是 JavaWeb 项目里永不过时的话题这次也不例外。我遇到的情况是数据库存中文正常但页面上显示???。排查链路是这样的——先看数据库连接 URL 是否带了characterEncodingutf8其次看页面模板文件的编码是否是 UTF-8再看浏览器响应的 Content-Type 是否包含 charset。最后发现问题是 IDE 的默认文件编码是 GBK新建的 HTML 模板文件虽然保存为 .html但实际编码已经乱了。这个问题的根源在于开发工具配置而不是运行环境后来我在 IDE 设置里把项目所有文件的默认编码改成 UTF-8并重新从模板引擎生成了页面乱码才彻底消失。还有一类容易忽略的路径问题Thymeleaf 页面里引入的 CSS、JS 最好使用{/css/bootstrap.min.css}这样的网关表达式生成绝对路径如果页面位于/ship/list这种二级路径直接写css/style.css相对路径会在浏览器中解析成/ship/css/style.css结果样式全部丢失。这类问题看起来是页面不好看了实际上是路径解析的经典案例排查思路是打开浏览器控制台看网络请求404 的静态资源基本就是相对路径写错了。6. 系统后续的优化思路索引、缓存和用户贡献体系6.1 数据库索引和 SQL 优化的实际手段系统上线运行一段时间词条数据增多以后我重新审视了一遍所有 SQL 的执行计划。首页热门舰船查询、列表筛选查询最频繁我给相关字段建了联合索引效果显著。一个小技巧是在 MySQL 中EXPLAIN SELECT ...可以清楚看到 SQL 的 type 字段如果显示ALL就是全表扫描显示ref或range表示走了索引。我建议所有列表查询都跑一遍 EXPLAIN这是最直观的优化手段。像like %某关键词%这种模糊查询是没法走普通索引的属于全表扫描。百科系统的搜索量不小但这个问题在数据量几百条的时候完全不需要过度设计。如果后续数据量大到明显卡顿再考虑 MySQL 全文索引或者引入搜索引擎。我的原则是先保证逻辑正确、代码清晰性能问题出现后再针对性优化不在一开始就为了可能永远不会出现的十万级数据做过度设计。6.2 字典数据缓存改造从省事到更省事国家列表、舰种列表这类字典数据的特点是读多写少、更新频率极低。最初我是用ControllerAdvice每次都查库后来观察到这个查询在页面加载中占据了不必要的数据库连接数就改造成了 Spring Cache 缓存。实现方式很简单在查询方法上标注Cacheable(cacheNames dicts, key nationList)首次查询后结果自动进入缓存后续请求直接命中缓存不再查数据库。后台修改了字典数据后调用缓存管理的evict方法清除对应缓存保证修改能立即生效。如果你不想引入额外的缓存中间件Spring Boot 的默认缓存实现ConcurrentMapCacheManager也能用它把数据放在 JVM 堆内应用重启后缓存清空重建。对百科系统这样的中小型应用这种本地缓存已经够用。如果将来部署到多实例环境下再考虑用 Redis 统一缓存也不迟。6.3 从管理员维护走向用户贡献的扩展设计最后聊聊系统未来的扩展方向。目前的词条管理完全依赖管理员手工维护这是一个信息更新不可持续的模式。真正庞大的游戏百科内容必然来自玩家社区的协作贡献。基于这个思路在现有表结构上可以扩展两个模块用户注册登录为用户提供个人中心词条编辑历史表保存每一次修改的前后版本差异支持审核通过后发布上线。这种用户提交 管理员审核的机制技术上并不复杂。只需要新增user表、contribution表和contribution_detail表贡献记录的字段格式跟现有词条字段保持一致后台增加一个审核列表页管理员可以对比新旧内容、选择通过或驳回。这样整个系统的定位就从管理员维护的单向信息站升级成了由社区驱动、管理员把控质量的协作百科数据来源问题也彻底解决了。我最想把这个扩展做出来的原因不是技术难度而是它真正从架构上解决了百科系统内容从哪里来这个根本问题。游戏百科信息系统做到后面我最大的体会是任何信息系统业务逻辑再复杂也是结构化的真正耗费心力的往往是数据怎么组织、内容怎么更新、系统怎么持续运转。这个项目虽然以 SpringBoot 和 JavaWeb 为基础但核心价值其实不在框架本身而在于把游戏里零散的信息变成一个有序、可查、可持续维护的知识库。如果你也想做一个类似的百科项目我建议先把数据模型想透把一条舰船从资料到页面的完整数据链路走通再考虑怎么炫技加功能。地基打稳了上面盖什么楼都不难。
返回列表