做企业级游戏服务站点的那几个月把 SpringBootVue 这套组合彻底摸透了。这个系统源码是一个典型的面向主机游戏玩家的服务门户涵盖前台展示、会员中心、内容管理和订单流程技术栈就是标题里那套SpringBootVueMyBatisMySQL。前后端分离、RBAC 权限、动态条件查询、Nginx 部署这些关键词加起来意味着它不只是个 demo而是能直接跑业务、能上线运营的完整工程。这篇文章我就从架构设计、核心实现、部署交付到坑位排查完整拆一遍给正在做类似项目或者准备拿这套技术栈接手企业级开发的同学一点参照。1. 整体设计思路与技术选型1.1 这个系统到底在解决什么问题传统的游戏资讯站大多还是静态页面堆内容编辑写完文章上传用户只能看不能互动更谈不上会员转化和订单闭环。而企业级的游戏服务网站核心诉求是四件事内容能持续运营、用户能注册登录并沉淀、会员服务能售卖、后台能管所有业务数据。这套系统要回答的就是这几个问题。前台给玩家看游戏库、看资讯、看攻略、看活动注册登录后能开通会员、能下单后台给运营人员管游戏条目、管文章发布、管用户状态、管订单流转。前后台共用一套用户体系靠权限区分角色所有操作有记录数据落库可追溯。这是它区别于普通个人项目的最关键一点——不是功能堆砌而是围绕内容 用户 订单三条线形成了业务闭环。我见过很多类似的源码项目功能罗列得很多但业务逻辑不闭环。比如能注册不能登录、能下单不能查单、后台改了状态前台不同步这类问题在这套系统里基本没有原因是它的模块划分一开始就是按业务域拆的而不是按页面拆的。1.2 为什么是 SpringBoot Vue MyBatis MySQL而不是别的这套组合放到今天依然是企业级项目里最常见的技术选型之一原因是它足够稳也足够好招人。SpringBoot 解决了传统 SSM 项目里繁琐的 XML 配置和容器部署问题内嵌 Tomcat打一个 jar 包就能跑开发期用 Maven 管理依赖私服一套团队协作非常顺。Vue 这边组件化开发让前后端彻底分离成为可能。后端的同学完全不用关心页面怎么渲染前端同学也不用等后端页面模板两边只需要约定好接口形式。配合 Element UI 这类组件库后台管理系统的开发速度能快一倍以上。MyBatis 选型要特别说一下。它跟 JPA 和 MyBatis-Plus 相比最大的优势是 SQL 完全可控。游戏服务网站这种业务搜索条件极其复杂——按平台、按类型、按价格区间、按上架状态、按时间排序组合条件能有好几十种。用 JPA 写这种动态查询要么拼接 Specification要么写 JPQL很别扭。用 MyBatis 的 if where 标签几十行动态 SQL 就能把所有场景覆盖掉而且每一句 SQL 都在自己手里出现慢查询可以直接捞出来优化对后期运维太重要了。MySQL 更不用多说企业级业务系统的首选关系型数据库。这套系统的事务场景集中在订单和会员开通上MySQL 的 InnoDB 引擎在这个体量下表现非常稳定。配合 Redis 做缓存是加分项但这个项目本身不依赖 Redis 也能跑属于可平滑升级的架构。1.3 模块边界划分前台、后台、公共层我拿到源码的第一件事是看包结构模块边界清不清楚直接决定后期维护成本。这套系统的分层是比较标准的前台门户模块门户首页、游戏库列表、游戏详情、资讯文章、会员套餐展示、用户登录注册、个人中心、订单列表和支付入口。后台管理模块管理员登录、仪表盘统计、游戏内容管理、资讯发布管理、用户管理、订单管理、会员套餐配置、系统基础配置。公共模块统一返回结果封装、全局异常处理、JWT 工具类、文件上传处理、分页查询封装、权限拦截器、跨域配置。前台和后台是两套独立的 Vue 项目各自编译部署通过 Nginx 做路径分发。这样做的好处是权限边界清晰后台项目的代码不会暴露到前台前端包体也更小。很多小项目为了省事只做一个管理端前台用后端模板渲染那种方案后期的迭代成本很高前后台分离才是企业级项目的常态。2. 核心业务逻辑与数据库设计2.1 用户体系与权限控制的完整链路用户体系是整个系统的地基。这套系统用的是标准的 RBAC 模型用户表、角色表、用户角色关联表、菜单权限表。用户端只有一种角色——普通会员后台支持管理员、编辑、运营等角色各自的菜单权限可以按需分配。密码存储用的是 BCrypt 加密不是 MD5这一点我给好评。MD5 加盐虽然也不是完全不能用但 BCrypt 是自带盐值的慢哈希算法同样的密码每次加密结果都不同暴力破解的成本高很多。Spring Security 的 BCryptPasswordEncoder 可以直接拿来用不需要额外引入整套 Security 框架。登录认证用的是 JWT。用户登录成功后后端签发一个 token包含用户 ID、用户名、角色标识、过期时间前端存在本地每次请求在请求头里带上。后端用一个拦截器统一校验 token 有效性校验通过就把用户信息放进上下文后续业务代码里直接取当前登录用户。这里有个细节要注意JWT 一旦签发在过期之前是没法主动失效的所以后台禁用用户时除了改数据库状态还要配合 token 黑名单机制或者缩短 token 有效期。这套系统的做法是后台改状态后前端下一次请求会因权限校验失败被踢出登录态结合短期 token 基本够用。权限拦截器有个常见的坑当项目里同时存在拦截器和跨域配置时拦截器如果没有放行预检请求前端所有带 token 的请求都会在 OPTIONS 阶段被拦下来表现就是接口能通但拿不到数据。后续在排查章节我会专门讲。2.2 游戏内容与资讯的数据模型设计游戏库是这个站点的核心内容资产。游戏表设计的字段大致是游戏名称、封面图、游戏平台PS4/PS5/PC 等多选、游戏类型、发售日期、商店价格、折扣价格、是否热门推荐、上下架状态、点击量、游戏简介、游戏详情富文本。这里有两个设计细节值得借鉴。第一个是平台字段用逗号分隔的字符串存储比如PS4,PS5查询时用 FIND_IN_SET 或者 LIKE 匹配简单高效。如果用关联表存游戏和平台的映射查询会多两次 JOIN对读多写少的内容场景来说不划算。第二个是价格字段用 decimal(10,2)状态字段统一用 tinyint。很多初学者喜欢用 varchar 存状态比如 1 代表上架0 代表下架这在代码里到处要写字符串比较还容易出现大小写不一致的问题。tinyint 配合代码里的枚举类可读性和安全性都更好。资讯文章表相对简单标题、封面、摘要、正文、作者、发布时间、置顶状态、发布状态。但要注意资讯和游戏是两种完全不同的内容类型后台需要分开管理所以独立建表是正确的。文章正文一般用富文本编辑器产生 HTML存入 longtext 字段前端展示时用 v-html 渲染。2.3 订单与会员服务的状态流转设计会员服务是这个网站的主要变现方式比如月卡、季卡、年卡每种套餐对应不同的价格和时长。套餐表字段包括套餐名称、套餐类型、原价、售价、时长天数、是否上架。用户购买流程是前台选择套餐 → 创建订单 → 模拟支付 → 支付成功回调 → 更新订单状态 → 给用户增加会员时长。订单表核心字段订单号、用户 ID、套餐 ID、支付金额、订单状态、创建时间、支付时间、完成时间。订单状态机是我看这个项目时重点关注的部分。它定义了五类状态待支付、已支付、处理中、已完成、已取消。为什么要区分已支付和已完成因为支付成功后还要执行开通会员的逻辑这个操作如果直接丢在支付回调里同步执行万一开通失败钱收了但服务没到账就变成事故了。稳妥的做法是把状态拆开支付成功进入已支付后台通过定时任务或者消息队列补偿未完成的订单全部处理完才置为已完成。这套系统源码里用的是同步处理加异常捕获状态设计已经预留了异步补偿的扩展空间这点看得出发版的人是有线上经验的。3. 前后端交互细节与关键实现3.1 后端接口规范与统一返回格式企业级项目最怕接口风格不统一。有的接口返回 Map有的直接返回实体类前端对接时每个接口要单独适配维护成本极高。这套系统的做法是统一封装 ResultDTO包含三个字段code业务状态码、message提示信息、data业务数据。成功时 code 为 200失败时按业务语义返回不同的状态码比如 401 未登录、403 无权限、500 系统异常。前端 Axios 响应拦截器统一处理这些状态码200 直接取 data401 跳转登录页并清除本地登录态其他状态码弹出 message 提示。这样做的好处是业务代码里不再需要每个接口单独写成功失败的判断逻辑。分页请求也做了统一封装。前端传递 pageNum 和 pageSize后端返回 PageResult 对象包含当前页数据列表、总条数、总页数、当前页码。前端表格组件直接绑定这些字段后台列表类页面几乎不用写重复的分页逻辑。3.2 前端路由守卫与菜单权限落地Vue 前端这边的核心是路由守卫。每个需要登录才能访问的页面在路由配置里标记 meta.requiresAuth全局前置守卫里判断如果没登录直接重定向到登录页并带上 redirect 参数登录成功后跳回原页面这个交互细节很加分。后台管理端的权限控制更细。不同角色的用户能看到的菜单不一样这套系统用动态路由实现用户登录后拉取自己的菜单权限列表前端用 router.addRoutes 动态挂载路由这样普通用户即使手动输入管理端的 URL也无法渲染没有权限的页面组件。按钮级别的权限控制做在前端指令里比如 v-permission 指令检查当前用户是否拥有某个操作权限码没有就移除按钮 DOM。虽然按钮级权限真正安全的校验还是在后端接口层但前端做一层能明显改善体验——没权限的操作按钮直接不显示比点了弹报错提示强得多。3.3 MyBatis 动态 SQL 与多条件组合查询游戏列表页是这个网站访问量最高的接口条件组合复杂平台筛选、类型筛选、价格排序、折扣活动、关键词搜索。这类查询用 MyBatis 的 if where 标签是最顺手的写法。select idselectGameList resultTypemap SELECT g.id, g.name, g.cover, g.price, g.discount_price, g.platform, g.type, CASE WHEN g.discount_price 0 THEN g.discount_price ELSE g.price END AS show_price FROM game g where if testplatform ! null and platform ! AND FIND_IN_SET(#{platform}, g.platform) /if if testtype ! null and type ! AND g.type #{type} /if if testkeyword ! null and keyword ! AND (g.name LIKE CONCAT(%, #{keyword}, %) OR g.intro LIKE CONCAT(%, #{keyword}, %)) /if if testonSale ! null AND g.status 1 /if /where choose when testsort price_ascORDER BY show_price ASC/when when testsort price_descORDER BY show_price DESC/when when testsort salesORDER BY g.sales_count DESC/when otherwiseORDER BY g.create_time DESC/otherwise /choose /select注意这里有个重要原则所有参数拼接都用 #{}不是 ${}。#{} 是预编译占位符MyBatis 会生成带 ? 的 PreparedStatement可以完全避免 SQL 注入。${} 是字符串替换虽然偶尔在动态排序字段里会用到比如 ORDER BY ${sortColumn}但这种场景必须对变量做严格白名单校验否则等于把数据库裸奔给攻击者。价格排序这里我特意用了 CASE WHEN 表达式把折扣价和原价统一计算成 show_price 字段来排序。如果用原价排序用户看到的结果和实际支付价格不一致体验非常割裂。这种细节往往决定一个系统好不好用。3.4 文件上传与静态资源映射游戏封面和资讯配图涉及文件上传。这套系统的做法是后端接收 MultipartFile校验文件类型和后缀生成 UUID 文件名保存到本地磁盘指定目录然后把可访问的 URL 存入数据库。这里要注意两个坑。第一个是文件类型校验不能只看前端传的 Content-Type因为这是可伪造的要后端根据文件内容判断魔数图片的魔数比如 JPEG 是 FFD8FF、PNG 是 89504E47。第二个是上传文件大小要在 SpringBoot 配置里显式限制不限制的话容易被大文件拖垮内存。spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB生产环境下本地磁盘目录建议单独挂载不要放在应用目录里否则应用升级时容易误删。Nginx 里加一个 location 映射到上传目录图片请求就不会经过后端应用减轻应用层压力。如果后续图片量大了把这个目录整体迁到对象存储配置层改动很小对业务代码几乎没有侵入。4. 部署交付与工程化优化4.1 从开发到生产的环境配置管理企业级项目必须区分开发环境、测试环境、生产环境。这套系统用 Maven 的 profile SpringBoot 多配置文件方案解决环境切换application-dev.yml本机数据库地址、日志级别 DEBUG、开启 SQL 输出application-prod.yml生产数据库地址、日志级别 INFO、关闭 SQL 输出、配置正式的密钥pom.xml 里配置三个 profile打包时用-P prod指定激活环境这里要特别提醒生产环境的数据库密码和 JWT 密钥不应该写死在 application.yml 里至少要用环境变量注入更规范的做法是接入配置中心。源码项目里一般会放一份默认配置方便本地启动但你自己部署上线时一定要改掉默认密码这是最基本的安全底线。前端这边通过环境变量区分接口地址。开发环境用 Vite 的 proxy 把 /api 代理到后端 localhost:8080规避了开发期跨域问题生产环境打包时接口地址设为 Nginx 反向代理的路径前后端同域部署浏览器层面不存在跨域。4.2 数据库备份与账号安全游戏服务网站上线后数据库是命脉。用户数据、订单数据、内容数据一旦丢失业务基本停摆。我见过一个项目因为没做备份运营人员误删了内容表只能靠记忆手工补回。这套系统的数据库设计里没有内置备份功能所以上线前必须靠外部策略兜底。推荐至少两层备份每天凌晨全量备份开启 binlog 实现增量恢复。备份文件保留最近 30 天异机存储一份。数据库账号权限也要收敛。应用连接数据库的账号只授予 SELECT、INSERT、UPDATE、DELETE 权限不给 DROP、ALTER 这类 DDL 权限。这样做的好处是即使应用被注入攻击攻击者也只能操作数据不能删库。后台如果要做表结构变更用单独的迁移账号完成。4.3 Nginx 部署与前端刷新 404 处理前后端分离部署的标准形态后端 jar 包跑在某个端口前端静态文件由 Nginx 托管Nginx 把 /api 前缀的请求反向代理到后端服务。具体配置大致是server { listen 80; server_name your-domain.com; root /opt/web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }最后这个try_files $uri $uri/ /index.html;必须写。Vue Router 用 history 模式时路由地址是路径格式比如 /games/detail/1024用户刷新页面时浏览器会向服务器请求这个路径Nginx 找不到对应文件就 404 了。try_files 让 Nginx 在找不到静态文件时回退到 index.html由前端路由接管页面就能正常渲染。后端 SpringBoot 服务建议用 systemd 托管配置开机自启和自动重启。启动时加上内存参数比如 -Xms512m -Xmx512m生产环境不要用默认值防止内存占用失控。5. 踩坑实录与排查速查表5.1 跨域配置与拦截器的顺序问题这个坑几乎是每个前后端分离项目都会遇到的。开发环境下前端端口是 5173后端是 8080跨域请求必须后端放行。很多人只在 SpringBoot 里加了 CORS 配置发现 GET 请求偶尔能通带 token 的请求一律失败控制台报错全是 CORS policy。原因在于预检请求。前端发送带自定义请求头比如 Authorization的请求时浏览器会先发一个 OPTIONS 预检请求询问服务器允许不允许。如果权限拦截器没有对 OPTIONS 请求放行预检请求直接被拦截器拦截返回 401浏览器认为服务器不允许跨域真实请求压根不会发出。解决方法是拦截器里加一行判断if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }有几个小项目因为这个问题排查了整整一天最后发现就是这么一行逻辑。企业级项目建议在开发环境就把这些边界情况暴露出来别等上线前联调才炸。5.2 订单支付回调的重复通知处理支付成功后支付平台回调接口默认会通知多次直到商户返回成功标识。如果回调处理逻辑没有做幂等每通知一次就加一次会员时长用户白嫖风险极高。这套系统的做法是回调处理前先查询订单状态只有待支付状态的订单才执行开通逻辑同时给用户会员时长表加唯一索引同一个订单号只能生成一条开通记录。这个设计非常务实。幂等不是靠代码逻辑保证的而是靠数据库唯一约束兜底。代码逻辑可能漏数据库约束不会漏。5.3 慢 SQL 定位与索引优化游戏列表页由运营配置的热门推荐和条件筛选组成数据量一旦上来不加索引的 LIKE 查询和排序会拖垮接口响应。排查慢查询的方法是开 MySQL 慢查询日志把执行时间超过 2 秒的 SQL 捞出来EXPLAIN 分析扫描行数。常见优化手段是状态字段加普通索引、平台字段虽然存储多值但有 LIKE 前缀查询需求的话配合全文索引、价格排序字段如果有条件的话加联合索引、资讯列表的发布状态和时间做复合索引。这个系统里订单表是最容易出现性能瓶颈的订单号必须加唯一索引查用户订单列表的 user_id 加普通索引这两个索引直接决定后台订单页能不能秒开。5.4 前端白屏与接口数据格式校验有段时间后台系统偶发白屏刷新才好排查定位到接口返回的数据里某个字段是 null前端模板里直接渲染了这个嵌套属性JavaScript 报错导致整个组件树挂载失败。现在的 Vue 3 对这种问题的容错比 Vue 2 好一些但最佳实践还是要做两层防御。后端返回前对关键数据做非空校验该补默认值的补默认值前端模板中用可选链操作符?.兜底。企业级项目里这类问题特别常见因为数据不可能永远都完整。6. 常见问题速查表问题现象可能原因解决思路前端请求接口报 404Nginx 未配置 proxy_pass或 /api 路径不匹配检查 Nginx 配置和后端 context-path带 token 的请求失败GET 偶尔通拦截器拦截了 OPTIONS 预检请求拦截器放行 OPTIONS 请求上传图片超过 1MB 报错SpringBoot multipart 未配置调整 max-file-size 配置点击菜单白屏控制台报路由守卫死循环路由守卫重定向逻辑有误检查登录态判断逻辑列表查询慢页面加载超过 3 秒缺少索引或 SQL 未命中索引EXPLAIN 分析并补充索引用户支付成功但会员时长没到账订单状态更新和会员开通不在同一事务开启事务失败时主动抛出异常回滚本地能跑服务器上时间差 8 小时MySQL 时区和 JVM 时区不一致连接串加 serverTimezoneAsia/Shanghai后台部署后刷新 404Vite 打包未用 history 模式配置 Nginx配置 try_files 回退 index.html7. 部署上线后的运维心得项目交付不是代码写完就结束了上线后的运维占了整个生命周期的大部分精力。这套系统跑稳之后我有几个比较深的体会。日志必须做切割和保留策略。SpringBoot 默认的日志如果不配置会一直写进同一个文件几个月后单文件几个 GB排查问题时打开文件都卡。用 Logback 的滚动策略按天分割保留最近 30 天线上问题定位效率高很多。数据库连接池参数一定要调过才上线。HikariCP 默认连接数对一个小型站点够用但高峰期如果接口响应慢连接数会迅速打满出现connection is not available报错。把 maximum-pool-size 从默认的 10 调到 20配合合理的超时时间能抗住几倍的流量波动。最后一个小技巧所有核心接口加一个简单的耗时日志用 MDC 把订单号或者用户 ID 带进日志上下文。这样一旦用户反馈某个功能异常直接按用户 ID 搜日志能快速还原他的操作路径不用让用户反复提供截图。这也是企业级项目跟 demo 项目的区别——出了问题能不能快速定位决定了业务方对技术团队的信任度。这个系统的源码整体上是干净、可落地的拿来学习企业级项目的完整链路很合适。如果你打算基于它做二次开发优先从支付对接和后台统计报表这两个方向入手这两个模块是当前版本相对基础的部分也是实际运营中最需要迭代的地方。