ARTICLE DETAIL

资讯详情

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

Spring Boot+MySQL+Thymeleaf游戏信息发布平台实战

Spring Boot+MySQL+Thymeleaf游戏信息发布平台实战 之前接触过很多游戏资讯站的内容发布需求真正想把信息发布、分类检索、文章详情、评论互动做到能稳定上线其实并不只是“套个模板”那么简单。本文围绕“经典武侠游戏信息发布平台”的完整搭建过程整理一套基于 Spring Boot MySQL Thymeleaf 的可运行实战方案从数据库设计到后端接口再到页面渲染、部署排查一次性讲清楚。无论是想入门 Web 开发的新手还是有基础想拿完整项目练手的开发者这篇教程都能直接照着操作。1. 背景与核心概念1.1 游戏信息发布平台是什么可以把游戏信息发布平台理解为“一个服务于游戏玩家的内容站点”。举个例子假设玩家想了解某个经典武侠游戏的最新资料片攻略、合区公告、玩法评测、玩家心得这些内容如果散落在微信群和朋友圈里很容易丢失如果集中在同一个网站上按分类展示、按关键字搜索、按时间排序用户浏览起来就会方便很多。一个典型的游戏信息发布平台至少应包含以下能力内容列表首页展示最新文章支持分页。分类筛选按“最新资讯”“玩法攻略”“活动公告”等分类筛选内容。关键字搜索用户输入关键词后可以检索标题和摘要。内容详情文章正文、作者、发布时间、浏览次数。管理录入运营人员可以新增、编辑、删除文章。用户互动评论、点赞等功能可以根据业务需求扩展。从技术实现角度看这类站点本质上是“内容管理系统CMS”。它不需要复杂的分布式架构但对数据一致性、检索效率、内容安全和可维护性有明确要求。本文就以一个可运行的最小闭环为例把它从前到后完整实现出来。1.2 技术架构拆解本文采用业界非常成熟的一套组合Spring Boot提供 Web 容器、依赖注入、接口开发能力。MySQL负责数据持久化存储。MyBatis负责数据库与 Java 对象之间的映射。Thymeleaf服务端模板引擎直接渲染 HTML 页面。Bootstrap / 原生 CSS负责页面样式本文以简洁为主重点在于功能性。整体请求链路如下浏览器请求 - Spring Boot Controller - Service - Mapper(MyBatis) - MySQL ^_______________________ 返回页面/JSON __________________|该架构的优点是分层清晰每一层只做一件事后续想拆分为前后端分离架构也很容易。1.3 为什么推荐这种方案很多入门者会问直接用静态 HTML 写几个页面不就行了吗当然可以但静态页面的问题在于内容一旦增加重复修改的成本很高。比如 100 篇文章要改标题手工改 100 个 HTML 文件显然不现实。通过数据库存储、后端动态渲染的方式新增一篇文章只要插入一条记录列表页和详情页会自动更新。这是“数据驱动页面”的核心价值也是本文方案的关键思路。2. 环境准备与版本说明2.1 运行环境开发前需要准备以下工具工具说明JDK本文使用 Java 17建议 11 以上均可Maven3.6 以上MySQL8.0 版本5.7 也兼容IDEIntelliJ IDEA 或 Eclipse浏览器Chrome / Edge版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。如果你本机 JDK 版本不同只需要在pom.xml中调整java.version即可。2.2 项目结构规划创建项目根目录game-portal后续文件按如下结构组织game-portal/ ├── pom.xml └── src/main/ ├── java/com/example/gameportal/ │ ├── GamePortalApplication.java │ ├── controller/ │ │ ├── ArticleApiController.java │ │ └── PageController.java │ ├── service/ │ │ └── ArticleService.java │ ├── mapper/ │ │ └── ArticleMapper.java │ ├── entity/ │ │ └── Article.java │ └── common/ │ ├── Result.java │ └── PageResult.java └── resources/ ├── application.properties ├── schema.sql ├── data.sql ├── mapper/ArticleMapper.xml └── templates/ ├── index.html ├── article-list.html └── article-detail.html3. 数据库设计与初始化3.1 核心表结构设计信息发布类网站最重要的表是“文章表”。为了减少冗余把“分类”单独建表。这里先创建数据库再创建两张核心表。CREATE DATABASE IF NOT EXISTS game_portal DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE game_portal; CREATE TABLE game_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名称, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章分类表; CREATE TABLE game_article ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 文章ID, category_id BIGINT NOT NULL COMMENT 所属分类ID, title VARCHAR(200) NOT NULL COMMENT 标题, summary VARCHAR(500) DEFAULT COMMENT 摘要, content MEDIUMTEXT COMMENT 正文内容, author VARCHAR(50) NOT NULL DEFAULT admin COMMENT 作者, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0草稿1已发布, view_count INT NOT NULL DEFAULT 0 COMMENT 浏览次数, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_category_id (category_id), KEY idx_create_time (create_time), CONSTRAINT fk_article_category FOREIGN KEY (category_id) REFERENCES game_category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章信息表;这里有几个设计细节值得注意使用utf8mb4而不是utf8因为武侠游戏资讯中经常出现特殊符号与生僻字utf8mb4能完整支持。分类和文章通过外键关联保证数据完整性。增加status字段方便以后做“草稿箱”和“下线”功能。增加view_count字段避免每次统计浏览数都去查日志表。3.2 写入初始化数据导入基础分类和几条演示文章方便项目启动后直接看到效果。INSERT INTO game_category (id, name, sort_order) VALUES (1, 最新资讯, 1), (2, 玩法攻略, 2), (3, 活动公告, 3); INSERT INTO game_article (category_id, title, summary, content, author, status, view_count) VALUES (1, 门派平衡调整内容前瞻, 本次调整将对部分门派技能进行优化重点涉及PVP场景表现。, 门派平衡一直是玩家关注的重点本次优化旨在提升职业差异化。具体数值以游戏内实际公告为准。, 官方小编, 1, 128), (2, 新手快速升级路线, 从入门到满级的一条效率路线适合新玩家快速体验游戏内容。, 新手阶段建议优先完成主线任务加入帮会后可以获得额外经验加成。每日副本和活动也不要落下。, 攻略组, 1, 356), (3, 服务器定期维护公告, 每周例行维护时间安排请玩家提前做好下线准备。, 为保证服务器稳定运行每周四上午进行例行维护预计维护时长2小时。维护期间无法登录请相互转告。, 运营团队, 1, 89);初始化数据之后项目启动即可看到三条不同分类的文章便于验证分页、分类筛选和详情页功能。3.3 设计要点说明类别和文章分开还有一个好处将来增加“专题”“合集”等概念时不需要改动已有表结构。所有文章统一走game_article表字段扩展时只需增加列不破坏已有逻辑。在项目早期表结构不要过度设计满足当前业务并且保留扩展余量即可。4. 后端接口开发4.1 引入依赖与配置文件创建pom.xml引入 Spring Boot Web、MyBatis、MySQL、Thymeleaf 等依赖。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies注意 MyBatis 与 Spring Boot 3 的版本匹配问题。如果使用旧版mybatis-spring-boot-starter很可能启动报错建议以官方兼容矩阵为准。配置文件src/main/resources/application.propertiesspring.application.namegame-portal server.port8080 spring.datasource.urljdbc:mysql://localhost:3306/game_portal?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver mybatis.mapper-locationsclasspath:mapper/*.xml mybatis.configuration.map-underscore-to-camel-casetrue spring.thymeleaf.cachefalsemap-underscore-to-camel-casetrue的作用是把数据库字段create_time自动映射为 Java 属性createTime可以少写很多映射语句。4.2 实体类编写创建文章实体类对应数据库表game_article。package com.example.gameportal.entity; import java.time.LocalDateTime; public class Article { private Long id; private Long categoryId; private String title; private String summary; private String content; private String author; private Integer status; private Integer viewCount; private LocalDateTime createTime; private LocalDateTime updateTime; public Long getId() { return id; } public void setId(Long id) { this.id id; } public Long getCategoryId() { return categoryId; } public void setCategoryId(Long categoryId) { this.categoryId categoryId; } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary summary; } public String getContent() { return content; } public void setContent(String content) { this.content content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author author; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status status; } public Integer getViewCount() { return viewCount; } public void setViewCount(Integer viewCount) { this.viewCount viewCount; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime createTime; } public LocalDateTime getUpdateTime() { return updateTime; } public void setUpdateTime(LocalDateTime updateTime) { this.updateTime updateTime; } }这里没有使用 Lombok是为了降低初学者对环境插件的依赖。如果你习惯使用 Lombok可以自行加上Data注解简化代码。4.3 Mapper 持久层创建ArticleMapper接口。package com.example.gameportal.mapper; import com.example.gameportal.entity.Article; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; Mapper public interface ArticleMapper { Article selectById(Param(id) Long id); ListArticle selectPage(Param(offset) int offset, Param(size) int size, Param(categoryId) Long categoryId, Param(keyword) String keyword); long count(Param(categoryId) Long categoryId, Param(keyword) String keyword); int insert(Article article); int update(Article article); int deleteById(Param(id) Long id); int increaseViewCount(Param(id) Long id); }对应的 XML 映射文件src/main/resources/mapper/ArticleMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.gameportal.mapper.ArticleMapper select idselectById resultTypecom.example.gameportal.entity.Article SELECT id, category_id, title, summary, content, author, status, view_count, create_time, update_time FROM game_article WHERE id #{id} AND status 1 /select select idselectPage resultTypecom.example.gameportal.entity.Article SELECT id, category_id, title, summary, author, status, view_count, create_time, update_time FROM game_article WHERE status 1 if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR summary LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY create_time DESC LIMIT #{offset}, #{size} /select select idcount resultTypelong SELECT COUNT(*) FROM game_article WHERE status 1 if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR summary LIKE CONCAT(%, #{keyword}, %)) /if /select insert idinsert parameterTypecom.example.gameportal.entity.Article useGeneratedKeystrue keyPropertyid INSERT INTO game_article (category_id, title, summary, content, author, status, view_count) VALUES (#{categoryId}, #{title}, #{summary}, #{content}, #{author}, #{status}, 0) /insert update idupdate parameterTypecom.example.gameportal.entity.Article UPDATE game_article SET category_id #{categoryId}, title #{title}, summary #{summary}, content #{content}, status #{status} WHERE id #{id} /update delete iddeleteById DELETE FROM game_article WHERE id #{id} /delete update idincreaseViewCount UPDATE game_article SET view_count view_count 1 WHERE id #{id} /update /mapper在列表页查询中故意不查询content大字段是为了减少无效数据传输。文章正文只在详情页加载这样一个列表接口返回 10 条记录时不会把 10 篇长文全部拉到内存里性能更好。4.4 Service 业务层这里封装分页查询、详情查询、新增、更新、删除逻辑。先定义通用的分页结果封装类PageResultpackage com.example.gameportal.common; import java.util.List; public class PageResultT { private long total; private int page; private int size; private ListT list; public PageResult(ListT list, long total, int page, int size) { this.list list; this.total total; this.page page; this.size size; } public long getTotal() { return total; } public int getPage() { return page; } public int getSize() { return size; } public ListT getList() { return list; } }再定义一个统一的 API 返回结构Resultpackage com.example.gameportal.common; public class ResultT { private int code; private String message; private T data; public Result(int code, String message, T data) { this.code code; this.message message; this.data data; } public static T ResultT success(T data) { return new Result(200, success, data); } public static T ResultT error(String message) { return new Result(500, message, null); } public int getCode() { return code; } public String getMessage() { return message; } public T getData() { return data; } }接下来是业务实现ArticleServicepackage com.example.gameportal.service; import com.example.gameportal.common.PageResult; import com.example.gameportal.entity.Article; import com.example.gameportal.mapper.ArticleMapper; import org.springframework.stereotype.Service; import java.util.List; Service public class ArticleService { private final ArticleMapper articleMapper; public ArticleService(ArticleMapper articleMapper) { this.articleMapper articleMapper; } public PageResultArticle page(int page, int size, Long categoryId, String keyword) { if (page 1) { page 1; } if (size 1 || size 100) { size 10; } int offset (page - 1) * size; ListArticle list articleMapper.selectPage(offset, size, categoryId, keyword); long total articleMapper.count(categoryId, keyword); return new PageResult(list, total, page, size); } public Article detail(Long id) { Article article articleMapper.selectById(id); if (article ! null) { articleMapper.increaseViewCount(id); } return article; } public int create(Article article) { if (article.getStatus() null) { article.setStatus(1); } return articleMapper.insert(article); } public int update(Article article) { return articleMapper.update(article); } public int delete(Long id) { return articleMapper.deleteById(id); } }这里要解释两点分页参数做了边界保护避免用户传负数导致 MySQL 报错。查询详情时调用increaseViewCount来增加浏览量。这个操作是“读后写”如果并发量极大可以改成定时批量更新但在中小型信息站点里直接更新完全够用。4.5 Controller 接口层后端提供两类入口一类是返回 JSON 数据的 REST API方便以后做移动端或前后端分离另一类是返回 HTML 页面的页面控制器用于浏览器直接访问。创建ArticleApiControllerpackage com.example.gameportal.controller; import com.example.gameportal.common.PageResult; import com.example.gameportal.common.Result; import com.example.gameportal.entity.Article; import com.example.gameportal.service.ArticleService; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/articles) public class ArticleApiController { private final ArticleService articleService; public ArticleApiController(ArticleService articleService) { this.articleService articleService; } GetMapping public ResultPageResultArticle page(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { return Result.success(articleService.page(page, size, categoryId, keyword)); } GetMapping(/{id}) public ResultArticle detail(PathVariable Long id) { Article article articleService.detail(id); if (article null) { return Result.error(文章不存在); } return Result.success(article); } PostMapping public ResultLong create(RequestBody Article article) { articleService.create(article); return Result.success(article.getId()); } PutMapping(/{id}) public ResultLong update(PathVariable Long id, RequestBody Article article) { article.setId(id); articleService.update(article); return Result.success(id); } DeleteMapping(/{id}) public ResultLong delete(PathVariable Long id) { articleService.delete(id); return Result.success(id); } }页面控制器PageController负责跳转 Thymeleaf 模板package com.example.gameportal.controller; import com.example.gameportal.entity.Article; import com.example.gameportal.service.ArticleService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; Controller public class PageController { private final ArticleService articleService; public PageController(ArticleService articleService) { this.articleService articleService; } GetMapping(/) public String index(RequestParam(defaultValue 1) int page, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword, Model model) { model.addAttribute(pageResult, articleService.page(page, 10, categoryId, keyword)); model.addAttribute(categoryId, categoryId); model.addAttribute(keyword, keyword); return index; } GetMapping(/article/{id}) public String detail(PathVariable Long id, Model model) { Article article articleService.detail(id); model.addAttribute(article, article); return article-detail; } }5. 前端页面渲染5.1 首页列表页创建src/main/resources/templates/index.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title游戏信息发布平台/title style body { font-family: Microsoft YaHei, sans-serif; background: #f5f6f8; margin: 0; } .container { max-width: 960px; margin: 0 auto; padding: 20px; } .header { background: #2d3a4b; color: #fff; padding: 15px 0; } .header h1 { margin: 0; font-size: 22px; } .nav { margin: 10px 0; } .nav a { margin-right: 15px; text-decoration: none; color: #2d3a4b; } .nav a.active { font-weight: bold; } .search-box { margin: 15px 0; } .search-box input { width: 300px; padding: 8px; } .search-box button { padding: 8px 16px; } .article-item { background: #fff; padding: 15px; margin-bottom: 10px; border-radius: 6px; } .article-item h2 { font-size: 18px; margin: 0 0 6px; } .article-item h2 a { text-decoration: none; color: #222; } .article-item p { color: #666; margin: 6px 0; } .meta { color: #999; font-size: 13px; } .pagination { margin: 20px 0; } .pagination a { margin-right: 8px; } /style /head body div classheader div classcontainer h1经典武侠游戏信息发布平台/h1 /div /div div classcontainer div classnav a th:href{/} th:classappend${categoryId null} ? active全部/a a th:eachcat : ${categories} th:href{/(categoryId${cat.id})} th:text${cat.name}/a /div div classsearch-box form methodget th:action{/} input typetext namekeyword th:value${keyword} placeholder输入标题或摘要关键词 button typesubmit搜索/button /form /div div classarticle-item th:eacharticle : ${pageResult.list} h2 a th:href{/article/{id}(id${article.id})} th:text${article.title}文章标题/a /h2 p th:text${article.summary}摘要/p div classmeta 分类IDspan th:text${article.categoryId}1/span · 作者span th:text${article.author}admin/span · 浏览span th:text${article.viewCount}0/span · 发布时间span th:text${#temporals.format(article.createTime, yyyy-MM-dd HH:mm)}2024-01-01/span /div /div div classpagination a th:href{/(page${pageResult.page - 1},categoryId${categoryId},keyword${keyword})} th:if${pageResult.page 1}上一页/a span th:text第 ${pageResult.page} / ${(pageResult.total pageResult.size - 1) / pageResult.size} 页/span a th:href{/(page${pageResult.page 1},categoryId${categoryId},keyword${keyword})} th:if${pageResult.page * pageResult.size pageResult.total}下一页/a /div /div /body /html这里使用了 Thymeleaf 的表达式语法。需要注意首页展示时分类导航数据需要在 Controller 中额外传入或者使用game_category表的查询接口。为简化教程页面中暂时用分类 ID 展示读者可以进一步把分类名称也查出并传给页面。5.2 文章详情页创建src/main/resources/templates/article-detail.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title th:text${article.title} - 游戏资讯标题/title style body { font-family: Microsoft YaHei, sans-serif; background: #f5f6f8; margin: 0; } .container { max-width: 860px; margin: 0 auto; padding: 20px; } .card { background: #fff; padding: 30px; border-radius: 8px; } .card h1 { font-size: 26px; } .meta { color: #999; font-size: 14px; margin-bottom: 20px; } .content { line-height: 1.8; color: #333; } .back { display: inline-block; margin-top: 30px; color: #2d3a4b; } /style /head body div classcontainer div classcard h1 th:text${article.title}文章标题/h1 div classmeta 作者span th:text${article.author}admin/span · 浏览span th:text${article.viewCount}0/span · 发布时间span th:text${#temporals.format(article.createTime, yyyy-MM-dd HH:mm)}2024-01-01/span /div div classcontent th:text${article.content} 正文内容 /div a classback href/返回首页/a /div /div /body /html这里细节很多。比如说th:text会把内容当作纯文本渲染避免用户提交的内容中出现script标签时被浏览器直接执行这就是最基本的 XSS 防御。如果确实需要支持富文本 HTML必须使用白名单过滤不能直接th:utext。5.3 发布页面一个信息发布平台当然要有内容录入入口。这里提供一个简单的发布页面开发阶段可以直接用表单提交文章。!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title发布文章/title /head body div classcontainer h1发布新文章/h1 form th:action{/api/articles} methodpost p label分类ID/label input typetext namecategoryId /p p label标题/label input typetext nametitle stylewidth: 100%; /p p label摘要/label textarea namesummary rows2 stylewidth: 100%;/textarea /p p label正文/label textarea namecontent rows10 stylewidth: 100%;/textarea /p p label作者/label input typetext nameauthor valueadmin /p button typesubmit提交/button /form /div /body /html这里表单methodpost提交的是普通表单后端接口前方用的是RequestBody如果直接对接需要写一点适配。更推荐的方式是用浏览器开发者工具或 Postman 发送 JSON 请求或者把表单提交接口单独设计一个。考虑到是教程这里只展示页面形态实际联动时把表单请求改成 AJAX 即可。6. 运行与验证6.1 启动项目完成上述代码后启动GamePortalApplication的main方法。控制台看到类似下面的日志说明启动成功Tomcat started on port(s): 8080 (http) Started GamePortalApplication in 2.5 seconds如果启动失败优先检查数据库是否已经创建、账号密码是否正确。6.2 页面访问测试浏览器访问http://localhost:8080/预期看到首页列表包含三条初始化文章。点击文章标题进入详情页可以看到完整正文并且刷新页面后浏览量会增加。再测试分页和搜索在搜索框输入“攻略”点击搜索预期只显示标题或摘要包含“攻略”的文章。6.3 API 测试用 curl 测试文章分页接口curl http://localhost:8080/api/articles?page1size5预期返回 JSON{ code: 200, message: success, data: { total: 3, page: 1, size: 5, list: [ { id: 3, title: 服务器定期维护公告, summary: 为保证服务器稳定运行每周四上午进行例行维护..., author: 运营团队, viewCount: 89 } ] } }测试新增文章接口curl -X POST http://localhost:8080/api/articles \ -H Content-Type: application/json \ -d { categoryId: 2, title: 副本通关路线分享, summary: 分享当前版本比较高效的副本通关路线, content: 副本通关的关键是队员配置和技能衔接..., author: 测试小编, status: 1 }新增成功后再次刷新首页可以看到新文章出现在列表首位。7. 常见问题与排查问题现象常见原因解决思路启动报错Access denied for user数据库账号密码错误检查application.properties中的账号密码后端中文乱码数据库字符集或连接参数未指定 UTF-8建库使用utf8mb4URL 加characterEncodingutf8列表显示空白schema.sql未执行或数据未初始化手动执行 SQL 脚本确认表和数据存在分页点击无效链接参数拼接错误使用 Thymeleaf{}表达式避免漏传参数详情页浏览量不增加事务未提交或接口未调用检查detail方法是否调用increaseViewCount发布接口返回 415请求头没有Content-Type: application/json使用 Postman 时选择 JSON 格式提交使用高版本 MySQL 启动报时区错误连接串缺少serverTimezone添加serverTimezoneAsia/Shanghai排查时还有一个通用技巧开启 MyBatis 的 SQL 日志在application.properties中加入mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl这样控制台会打印每一条执行的 SQL。看到 SQL 才能判断到底是查询条件写错还是数据本身不存在。8. 最佳实践与工程建议8.1 内容安全与权限控制信息发布平台最容易出现的安全问题是 SQL 注入和 XSS。MyBatis 的#{}预编译机制本身就能防御大部分 SQL 注入但要注意不要图省事去拼接${}。本文所有 SQL 都用了#{}这是正确的做法。在前端展示环节页面模板中统一使用th:text而不是th:utext可以防止用户提交的富文本内容里出现恶意脚本。如果业务确实需要展示 HTML 格式的正文建议引入像 OWASP Java HTML Sanitizer 这样的白名单过滤工具而不是完全信任用户输入。发布、编辑、删除文章接口目前没有任何权限限制这在生产环境是绝对不行的。建议接入 Spring Security至少做到管理员可以新增、编辑、删除文章。普通用户只能浏览。密码必须使用 BCrypt 加密存储。所有管理端口禁止暴露到公网。8.2 性能优化信息发布站点的读操作远多于写操作可以做以下优化第一列表页不要查询content大字段本文的 XML 中已经体现了这一点。第二加 Redis 缓存。热门文章详情可以缓存 5 到 10 分钟直接减少数据库压力。第三建立合适的索引。game_article表已经对category_id和create_time建了索引这能保证在按分类和时间排序时不会全表扫描。更进一步的方案是把文章正文存储到对象存储或 CDN数据库只保存元数据和正文地址。但这对目前这个体量来说为时过早项目早期不要过度设计。8.3 部署与备份开发完成后的生产部署推荐使用如下流程mvn clean package -DskipTests java -jar target/game-portal-0.0.1.jar \ --spring.profiles.activeprod正式环境需要准备生产配置文件application-prod.properties把数据库连接、日志目录都独立出来不要和开发环境混用。数据库备份是最容易被忽视的环节。每天至少执行一次逻辑备份mysqldump -uroot -p game_portal backup_$(date %Y%m%d).sql涉及更新或删除操作之前先备份数据再在测试环境执行一次 SQL确认影响行数无误后再在低峰期操作生产库。9. 总结与学习路线本篇文章围绕游戏信息发布平台完整演示了从数据库设计、后端接口开发、页面渲染到项目运行验证的整个过程。核心价值并不只是那几条接口而是“数据驱动页面”的思想文章内容存进数据库页面动态渲染后续的所有扩展都建立在清晰的表结构和分层代码之上。如果你想把项目继续做得更完整下一步建议按顺序学习这些内容分类管理新增、排序、上下线分类当前分类数据还是写死的把它做成可维护的管理页面后站点才算真正完整。用户体系与登录接入 Spring Security实现管理员权限和用户评论身份。富文本编辑器集成 wangEditor 或 Markdown 编辑器让运营人员可以排版正文。缓存优化引入 Redis把热点文章缓存起来。日志与监控接入 SLF4J Logback记录操作日志和异常日志。技术栈不在多能解决实际问题才是关键。本文的代码并不复杂但它是许多中大型内容系统的雏形。建议你把项目跑起来再尝试改造一两个功能点比如增加标签、增加点赞、增加评论。每一步改动都会让你对这些框架的理解更深一层。
返回列表