ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot实战:从零搭建游戏创意工坊与推广平台

Spring Boot实战:从零搭建游戏创意工坊与推广平台 简介面向Java后端与Web全栈初学者、毕业设计选题学生这套基于Spring Boot的游戏创意工坊与推广平台源码包完整实现了游戏创意分享、浏览、评论互动与开发者推广等核心业务可帮助理解前后端分离开发与MySQL持久化设计。压缩包共690个文件约29.31MB包含156个Java后端源码、119个Vue前端页面、63个JS逻辑文件、20个XML配置及SQL数据库脚本等另有bat快速安装/运行/构建脚本便于本地复现项目。目录区分admin管理端、front用户端及mvnw等Maven Wrapper配置资源组织结构清晰可直接对照学习。目前已有54人学习浏览适合在Spring Boot项目实战、毕设设计或简历项目中快速借鉴参考。1. 从创意工坊到推广平台的 Spring Boot 落地一个游戏创意工坊看起来只是让用户上传创意、交流讨论但放到毕设或真实业务场景里它真正考验的是内容管理和分发推广两条链路能否在一个 Spring Boot 项目里稳定跑通。创意工坊解决的是 UGC 内容的产生、审核和展示问题推广平台解决的是内容如何触达用户、如何用活动或公告带动活跃的问题两者合在一起构成了一个完整的社区型 Web 系统。不带任何现成框架用 Spring Boot MySQL 从零实现时大多数人会在角色权限设计、作品审核状态机、文件存储路径、以及列表页性能上反复踩坑。这套项目最值得拆解的地方也正在于此。本文会按照先画架构和表、再写核心接口、再看数据库优化、最后处理部署的顺序往下走所有设计决策都基于可运行、可验证的最小闭环能直接照着写代码也能在实际业务里套用。2. 基于 Spring Boot 的整体架构与核心表设计2.1 为什么选择 Spring Boot 而不是 Servlet 原生或 SSM游戏创意工坊这类项目有典型的分层需求前端需要 RESTful 接口、后台需要权限拦截、业务层需要事务控制、持久层需要灵活的 SQL 拼装。用 Servlet 原生写法会把这些全部摊在 Controller 里后期维护成本很高SSM 本身没有问题但配置繁琐光是 Spring、Spring MVC、MyBatis 三个框架的 XML 配置就要花掉大量时间。Spring Boot 通过自动配置把这一步压缩到极简一个SpringBootApplication就能启动整个应用尤其适合以毕设或中小型项目为粒度的交付场景。选型时并不需要追求最新的 Spring Boot 3.x如果环境是 JDK 8 主流云服务器我一般会选 Spring Boot 2.7.x。2.7 版本依然兼容 javax 命名空间资料多、依赖坑少很多第三方 SDK 也仍然以 javax 为准。Spring Boot 3 强制 JDK 17 和 jakarta 命名空间除非项目有明确的新技术指标否则没有必要在这个量级的系统里给自己增加迁移成本。2.2 数据表设计围绕作品 - 用户 - 活动三条主线创意工坊和推广平台的数据模型可以抽象成三条主线和若干辅助表。第一条主线是用户体系第二条是作品/创意内容体系第三条是推广体系。下面这张表结构直接对应数据库的建表脚本字段命名和类型都适合 Spring Boot 项目直接使用。表名核心用途关键字段说明user用户信息id, username, password, avatar, rolerole区分管理员/普通用户/创作者work创意作品id, user_id, title, cover, file_url, status, like_countstatus控制审核状态work_tag作品标签关联work_id, tag_id一对多标签方便筛选comment作品评论id, work_id, user_id, content, parent_id支持楼层回复promotion推广活动id, title, cover, content, start_time, end_time活动时间区间内展示banner首页轮播id, image_url, link_url, sort推广位配置用户表和作品表之间是一对多关系作品表和标签表是多对多关系由于关联最频繁的是列表查询和筛选我在设计时直接用中间表work_tag而不是在 work 表里存一个逗号拼接的 tag 字符串。逗号拼接虽然省一张表但做按标签筛选作品时需要 in 操作无法走索引数据量一上来就会出现明显的性能抖动。2.2.1 作品表的状态机设计作品表里最核心的字段是status它不只是上架/下架两态。真实创意工坊场景里作品从用户创建到最终展示至少要经历待审核(0) - 已通过(1) - 已下架(2)这条链路大部分项目还会加入草稿(3)和审核驳回(4)。建议用tinyint类型存储配合 Java 枚举类做类型安全映射。public enum WorkStatus { PENDING(0, 待审核), APPROVED(1, 已上架), OFF_SHELF(2, 已下架), DRAFT(3, 草稿), REJECTED(4, 已驳回); private final int code; private final String desc; WorkStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }这里用枚举而不是魔法数字是因为审核操作在多处会被调用比如管理员审核接口、用户重新提交草稿的接口、定时任务自动下架的接口。如果到处都是if (work.getStatus() 1)改状态逻辑时需要全文搜索极易漏改。2.2.2 推广活动与 banner 的挂载方式推广平台部分我见过很多设计有的是直接在banner表里写死link_url指向某个前端路由有的是用promotion表加position字段区分首页头部、首页中部、侧边栏等位置。推荐后者因为推广平台的核心需求是运营人员能自己配置而不是每次改活动都要发版。position字段配合sort排序字段查询时ORDER BY sort ASC即可拿到指定位置的展示列表推广位的增删改就变成了纯数据库操作。3. 核心接口实现创意提交、审核流转与列表渲染3.1 项目分包与通用响应结构分包结构我一般按controller / service / mapper / entity / common五层组织common里放统一响应类、异常处理器和拦截器。统一响应对象是前后端联调的基础如果每个接口都返回不同结构前端 Axios 拦截器写起来就很痛苦。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } }这个类的核心价值在于把业务状态码和 HTTP 状态码解耦。现实情况是很多接口即使业务失败也需要返回 HTTP 200比如参数校验失败、会话过期让前端通过code字段统一判断否则前端的错误处理逻辑会被 HTTP 状态码和业务状态码搅在一起。实际项目中还会配合RestControllerAdvice做全局异常捕获把抛出的BusinessException转换成统一的Result.error()返回避免堆栈信息直接暴露给前端。3.2 用户认证JWT 无状态登录与拦截器注册游创平台不适合用 Session因为作品浏览和下载接口会被大量访问session 存在内存中会造成横向扩容困难项目里我更常使用 JWT 方案。用户登录成功后服务端签发 token前端每次请求在Authorization头中带上该 token后端通过拦截器解析并注入用户上下文。Spring Boot 中注册拦截器需要实现WebMvcConfigurer接口。Component public class AuthInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; 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 || !token.startsWith(Bearer )) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } token token.substring(7); Integer userId jwtUtil.parseToken(token); if (userId null) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\token失效\}); return false; } UserContext.set(userId); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.remove(); } }拦截器的preHandle方法里有两个容易忽略的细节一是 OPTIONS 预检请求必须在业务判断前放行否则前端跨域配置会连环报错二是 token 解析完成后要把用户信息放进ThreadLocal封装的UserContextafterCompletion里再 remove否则线程池复用时会出现用户串号。密码存储不建议用 MD5 明文应当使用BCryptPasswordEncoder把盐和哈希一起放在同一个字符串里MyBatis 查询后直接比对即使数据库泄露也不会立刻暴露明文密码。3.3 创意作品发布的完整链路创意上传这个接口是整个平台的核心也是最容易出问题的模块。前端流程是用户填写标题、上传封面、选择标签、提交文件服务端要做的事包括保存作品元数据、磁盘文件存储、标签关联写入。三步中任何一步失败都需要通过事务回滚。第一步用Transactional标注 service 方法即可但需要注意自调用导致的失效问题——同一个类里methodA()调用methodB()时methodB上的Transactional不会生效必须通过代理对象调用。Service public class WorkServiceImpl implements WorkService { Override Transactional(rollbackFor Exception.class) public void publishWork(WorkPublishDTO dto, Integer userId) { Work work new Work(); work.setUserId(userId); work.setTitle(dto.getTitle()); work.setCover(dto.getCoverUrl()); work.setFileUrl(dto.getFileUrl()); work.setStatus(WorkStatus.PENDING.getCode()); work.setLikeCount(0); workMapper.insert(work); workTagMapper.deleteByWorkId(work.getId()); if (!CollectionUtils.isEmpty(dto.getTagIds())) { workTagMapper.batchInsert(work.getId(), dto.getTagIds()); } // 发送审核通知异步操作 messageService.sendAuditNotice(userId, work.getId()); } }rollbackFor Exception.class是必须显式声明的。Spring 默认只对RuntimeException回滚如果messageService.sendAuditNotice()里抛出一个受检异常事务不会回滚就会出现作品已入库但通知没发出去的中间状态。文件上传推荐存储在服务器本地磁盘或云对象存储不推荐塞进 MySQL 的 BLOB 字段。数据库存 BLOB 会导致行体积膨胀SELECT列表页时如果误把该字段查出来磁盘 I/O 和内存占用都会直线飙升。数据库里只保存file_url路径上传接口通过 MultipartFile 直接写入指定目录# 项目根目录下创建 upload/ 目录存放用户上传的图片和文件 mkdir -p /data/game-workshop/uploadpublic String uploadFile(MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String filename UUID.randomUUID() ext; String datePath new SimpleDateFormat(yyyy/MM/dd).format(new Date()); File dir new File(UPLOAD_DIR datePath); if (!dir.exists()) { dir.mkdirs(); } File dest new File(dir, filename); try { file.transferTo(dest); return /upload/ datePath / filename; } catch (IOException e) { throw new BusinessException(文件上传失败); } }这里有三点需要强调。第一文件名必须用 UUID 重命名不能用用户上传的原名内部文档测试时经常见到接口说明.pdf这种中文名覆盖问题实际上是为了防止路径穿越和重名覆盖。第二按日期分目录可以避免单个目录文件数过多导致文件系统 inode 耗尽。第三transferTo方法在某些 servlet 容器下会先写临时文件再移动如果目标目录和临时目录不在同一文件系统就会抛异常必要时要改用FileOutputStream手动拷贝。3.4 作品列表分页与标签筛选列表接口是访问量最大的接口推荐使用 MyBatis Plus 的分页插件PaginationInnerInterceptor。用 PageHelper 或者手写 LIMIT 也可以但 MyBatis Plus 在 Spring Boot 项目里集成更顺滑同时也保留了手写 SQL 的灵活性。分页查询时不能只传 pageNum 和 pageSize排序字段和筛选条件要一起传入。select idselectPageByCondition resultTypecom.example.entity.WorkVO SELECT w.id, w.title, w.cover, w.like_count, w.status, u.username AS author_name, GROUP_CONCAT(t.name SEPARATOR ,) AS tag_names FROM work w LEFT JOIN user u ON w.user_id u.id LEFT JOIN work_tag wt ON w.id wt.work_id LEFT JOIN tag t ON wt.tag_id t.id where if testkeyword ! null and keyword ! AND w.title LIKE CONCAT(%, #{keyword}, %) /if if teststatus ! null AND w.status #{status} /if /where GROUP BY w.id, u.username ORDER BY w.like_count DESC, w.id DESC /select这个 SQL 用GROUP_CONCAT把同一作品的多标签合并成一个字符串返回给前端直接展示省去了一次循环查标签。ORDER BY w.like_count DESC, w.id DESC保证点赞量相同的情况下新作品排前面分页时不会出现重复数据。这里的坑在于GROUP BY和ORDER BY的字段必须完全一致或满足函数依赖MySQL 5.7 及以上默认开启ONLY_FULL_GROUP_BYSELECT的字段必须包含在GROUP BY里否则直接报错。4. MySQL 优化索引设计、全文检索与慢查询排查4.1 索引设计让每一条查询都有索引兜底创意工坊平台的查询特点是一路带条件一查连多表。核心索引有三条work表的user_id索引解决我的作品查询status索引解决管理员按状态筛选双重条件则走联合索引(status, like_count)。如果status字段区分度高只有 0/1/2/3/4单列索引效果有限联合索引的价值在于排序场景。ALTER TABLE work ADD INDEX idx_user_id (user_id); ALTER TABLE work ADD INDEX idx_status_create_time (status, create_time); ALTER TABLE comment ADD INDEX idx_work_id (work_id);第二行索引(status, create_time)能同时支撑按状态查列表按时间倒序的场景查询条件命中status后create_time的排序可以直接从索引里取出不需要 filesort。用EXPLAIN验证时Extra列如果展示Using filesort说明排序绕过了索引需要调整索引顺序或增加覆盖字段。4.2 慢查询日志与通用排查路径系统上线后最常见的现象是列表页越翻越慢。排查路径是先开慢查询日志找准慢 SQL再用 EXPLAIN 看执行计划最后按最左前缀原则补索引。开发机搭建时可以在 MySQL 配置文件里开启慢查询记录[mysqld] slow_query_log 1 slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 log_queries_not_using_indexes 1# 查看执行计划重点看 type、rows 和 Extra 三列 EXPLAIN SELECT * FROM work WHERE status 1 ORDER BY create_time DESC LIMIT 20;type列是 all 表示全表扫描rows是预估扫描行数网络搜索里常见的索引失效问题多数出在查询条件中使用了函数或隐式类型转换。比如work表的status是 tinyintJava 里传入字符串1时 MySQL 可以转换但如果status传成true或 VARCHAR 串就很容易索引失效。排查索引问题时优先确认两个字段的类型是否匹配。另一个高频坑是深分页问题。LIMIT 100000, 20会扫描前 100020 行然后丢弃前 100000 行性能会随着页码增大急剧下降。如果后台管理界面确实需要翻到很后面常见的替代方式是把分页改成基于游标传入last_seen_idSQL 条件变为WHERE id #{lastSeenId} ORDER BY id ASC LIMIT 20。缺点是不能跳页但绝大多数后台管理操作是连续翻页这个取舍是划算的。4.3 全文检索用倒排思路替代 LIKE作品标题和简介的关键字搜索场景下LIKE %keyword%无法利用索引表里两三万行时还能扛数据量过十万就会拖慢主库。如果只用一个 MySQL 实例做法是建一个work_fulltext表单独存搜索内容使用 MySQL 的内置 FULLTEXT 索引ALTER TABLE work ADD FULLTEXT INDEX ft_title_desc (title, description); SELECT * FROM work WHERE MATCH(title, description) AGAINST(像素 冒险 IN NATURAL LANGUAGE MODE) LIMIT 20;AGAINST里可以写多个关键词MySQL 会自动分词并匹配。这样搜索流量和正常列表流量在查询上做到了物理隔离全文索引的更新由应用层在作品通过审核后同步写入一致性通过事务控制。如果数据量到了百万级再考虑迁移到 Elasticsearch但那是另一个量级的话题了。5. 部署上线与配置踩坑从源码到可访问站点的最短路径5.1 本地环境准备与项目初始化拿到源码包后先做环境对齐再做配置修改。推荐的 JDK 版本是 1.8Maven 版本 3.6MySQL 5.7 或 8.0 均可。先在 MySQL 里执行项目附带的 sql 文件完成建库和数据初始化mysql -uroot -p123456 /path/to/init.sql-- 使用数据库 USE game_workshop; -- 查看已导入的表数量 SHOW TABLES;执行完之后确认表数据无误再修改 Spring Boot 项目里的application.yml。这个文件是所有配置的集中地数据库连接、文件上传路径、JWT 密钥都在这里。项目里最常见的启动失败原因就是数据库地址、账号密码对不上其次是时区配置。spring: datasource: url: jdbc:mysql://localhost:3306/game_workshop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 50MB max-request-size: 100MB mvc: static-path-pattern: /upload/** mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpldriver-class-name在 MySQL 5.7 下可以写com.mysql.jdbc.Driver但 MySQL 8.0 必须换成com.mysql.cj.jdbc.Driver且 url 里必须带serverTimezone参数否则连接会直接报Connection refused或时间转换异常。上传目录的访问依赖 static-path-pattern 映射到本地磁盘的upload目录否则前端的图片 URL 会 404。5.2 打包命令与 jar 包启动本地开发跑通后需要构建可部署的 jar 包。Spring Boot 内嵌 Tomcat不需要单独装 Tomcat一条命令就能跑起来# 跳过测试打包 mvn clean package -DskipTests # 启动 jar 包指定 JVM 参数和激活配置 java -Xms256m -Xmx512m -jar game-workshop-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod-Xms和-Xmx分别指定堆内存初始值和最大值如果是 1 核 2G 的云服务器建议设为-Xms512m -Xmx512m即可加大-Xmx反而会挤压操作系统文件缓存的可用内存。--spring.profiles.activeprod用于激活不同环境的配置文件项目里应当准备application-dev.yml和application-prod.yml两份prod 里的数据库密码通过环境变量注入不写死在 yml 文件里。启动之后先看日志。Spring Boot 的日志会直接打到控制台用tail盯住输出java -jar game-workshop.jar app.log 21 tail -f app.log看到Started Application in xx seconds即启动成功。如果端口被占用通过lsof -i:8080确认占用进程或在启动参数中追加--server.port8081指定其他端口。5.3 项目内文件上传路径与前端联动jar 包方式和 IDE 方式运行有一个显著区别new File(upload/)的相对路径基准不同。在 IDEA 里跑基准是项目根目录用java -jar部署基准是 jar 包所在目录。因此生产环境必须把上传路径写成绝对路径app: upload-dir: /data/game-workshop/upload对应的代码里用Value(${app.upload-dir})注入替换掉写死的相对路径。前端图片显示时/upload/2024/03/07/xxx.png这个 URL 经过 Nginx 反向代理后指向 Spring Boot如果部署了 Nginx也可以把/upload/直接交给 Nginx 静态文件服务不需要经过 Java 层转发减少一层内存拷贝location /upload/ { alias /data/game-workshop/upload/; }这样配置之后静态资源请求直接由 Nginx 返回只有接口请求才转发到后端后端 Tomcat 的连接数和线程压力会明显下降。5.4 用 curl 验证核心接口部署完成后不用急着打开前端页面先用 curl 按接口文档冒烟一遍# 1. 用户登录获取 token curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123} # 2. 携带 token 获取作品列表 curl -X GET http://localhost:8080/api/work/list?page1size10 \ -H Authorization: Bearer 替换为实际token # 3. 模拟上传文件multipart 形式 curl -X POST http://localhost:8080/api/file/upload \ -H Authorization: Bearer token \ -F file./test.png这三个接口分别覆盖了认证、业务查询、文件上传三条核心链路。如果第 2 步返回 401检查 token 是否过期或Authorization头拼写如果第 3 步返回 500先去/data/game-workshop/upload看目录是否存在、是否有写权限。文件目录建议使用chown给启动 jar 包的系统用户授权不要图省事用 root 跑 Java 进程。以上逐条通过项目才算真正落地。本文还有配套的精品资源点击获取
返回列表