SpringBoot+Vue前后端分离游戏服务管理系统实战
最近整理了一套 PS 游戏服务网站的完整管理系统源码技术栈是 SpringBoot Vue MyBatis MySQL前后端分离前台展示 后台管理一体。折腾了几天把整个项目从零搭起来顺手把运行期间踩到的坑都记了下来。如果你正在找一套能跑通、能写进简历的 Web 全栈项目或者你是做游戏租赁、主机服务类小站想自己搞个管理后台这篇文章应该能帮你少走不少弯路。下面我会先把项目整体思路和技术选型讲清楚再按“后端原理 → 前端处理 → 完整实操 → 问题排查”的顺序复盘整个过程。我的环境是 Windows 11 JDK 1.8 SpringBoot 2.7.x Vue 2 Element UI MySQL 8.0文中会额外说明升级到 SpringBoot 3 和 Vue 3 时要注意的地方。1. 项目整体设计与思路拆解1.1 网站的核心闭环从浏览到下单这个项目的定位不是普通的“游戏资讯展示站”而是一个带交易属性的游戏服务管理平台。用户端的核心流程是浏览首页轮播和游戏库 → 查看游戏详情包含图文介绍和视频预告 → 选择服务类型租赁、维修、代练、回收都可以扩展 → 提交订单 → 在个人中心查看订单状态管理端的核心流程是登录后台 → 管理游戏分类和游戏内容 → 处理用户订单 → 发布公告和轮播图 → 查看基础数据。所以数据库表的设计也是围绕这个闭环来做的。最基础的表包括用户表、游戏分类表、游戏表、订单表、评论表、公告表、轮播图表。订单表必须和用户表、游戏表建立外键关系状态字段用整型数字表示0待支付、1进行中、2已完成、3已取消这样前端拿到数字之后映射成中文标签即可。实际编码时我的接口规划比较“朴素”不搞复杂的微服务拆解就是一个单体应用把后台管理接口和前台接口都放在同一个 SpringBoot 工程里用/api/user/**和/api/admin/**做路径区分然后用拦截器统一做权限校验。这种做法对学习和中小站点维护都很友好启动一个 Java 进程就全搞定了。1.2 为什么是 SpringBoot Vue MyBatis MySQL而不是别的先说 SpringBoot。这类管理系统最怕的就是“环境搭半天、配置写一屏”。SpringBoot 的自动装配机制把数据源、Web容器、事务管理器这些基础组件的初始化都收敛到了约定配置里一个spring-boot-starter-web依赖加进去内嵌 Tomcat 就起来了不用再单独部署 war 包。配数据源也只需要在application.yml里写 URL、用户名、密码三行。Vue 负责前端交互。为什么不用 JSP 或者 Thymeleaf因为管理后台的表格、弹窗、表单校验、路由跳转用前端框架来做体验会好很多而且前后端分离后接口调试和后期二次开发都轻松。Vue 2 的生态目前依然是教程最多的版本Element UI 组件库对管理系统来说基本开箱即用。当然新项目可以考虑 Vue 3 Element Plus Vite如果你拿到的源码是 Vue 2后面我会讲升级思路。MyBatis 在这个项目里承担 SQL 与 Java 方法之间的映射。相对于 JPAMyBatis 对 SQL 的控制更直接一点多表查询、动态条件拼接、复杂统计时不容易被 ORM 的“潜规则”坑到。再配合 MyBatis 的一级缓存、二级缓存机制对订单、分类这类读多写少的数据性能调优也比较可控。MySQL 作为存储层没什么好争议的。它稳定、普及率高、网上问题解决方案多。8.0 版本默认字符集是 utf8mb4能直接存 emoji 表情用户名昵称里带个表情不会报错。不过要提醒一下MySQL 8.0 默认认证插件是caching_sha2_password连接驱动的版本不能太低至少得用mysql-connector-java8.0.x。1.3 这个项目适合谁能学到什么如果你是计算机专业的在校生这个项目能覆盖课程设计和毕业设计的大部分要求有一个完整的前后端分离架构、有登录鉴权、有 CRUD、有订单流程、有文件上传、有数据统计答辩时能讲的点非常多。如果你是刚转行 Java 开发的初学者把这一套项目跑通后你会对“一个请求从前端按钮到数据库记录”的完整链路形成直观认知这是刷一百道面试题都换不来的。如果你自己经营着游戏设备租赁或者主机服务类的小业务那这套系统可以直接改改 Logo、商品数据就能上线使用。但我也要说句实话这套源码的价值在于“完整可运行、模块清晰、容易二次开发”而不是“技术多先进”。它就是一套标准的单体全栈项目里面用到的东西都是主流技术栈没有任何冷门黑科技。正因为这样它的学习曲线才平缓换任何一个人接手都能快速看懂。2. 后端设计原理SpringBoot 里那些容易翻车的点2.1 数据访问原生 MyBatis 与 MyBatis 缓存那些事我在这个项目里用的是原生 MyBatis没有用 MyBatis-Plus原因是标题里的技术栈就是 MyBatis而且原生写法更能让人理解 SQL 与参数映射的关系。每个 Mapper 接口对应一个 XML 文件XML 里的namespace必须和接口全限定名一致id必须和方法名一致resultType或resultMap决定了查询结果怎么封装成对象。写查询的时候我习惯把公共字段的映射抽取成一个sql片段比如游戏表的id, name, cover, price, status等字段在列表查询、详情查询、后台管理查询里都用include引用避免字段写散。这个习惯能帮你解决一个很实际的问题改表结构的时候不用满世界找 SQL 改字段名。MyBatis 的缓存机制是面试常考、实战也容易踩坑的点。一级缓存是SqlSession级别的默认开启同一个会话里执行相同的 SQL 会直接返回缓存对象。这里有个大坑如果中间执行了增删改操作一级缓存会被清空但如果你在一个事务里先查后改再查同一条数据查出来可能还是旧值。为什么会这样因为一级缓存的命中条件是“同一个 SqlSession 相同 SQL 相同参数”事务没提交前缓存不会失效但数据库里的数据实际上已经变了。二级缓存是namespace级别的默认关闭。这个项目里我没有全局开启二级缓存因为管理系统对数据实时性要求较高尤其是订单模块开启缓存后容易出现“用户付款了后台订单状态还是待支付”的脏读问题。如果你真的想用建议只对游戏分类、公告这类极少变更的数据开启并且把flushInterval设置得短一些同时给实体类实现Serializable接口否则会报序列化异常。2.2 动态 SQL 与“参数比较”的经典坑MyBatis 最实用的功能就是动态 SQL。这个项目的游戏列表页需要支持按分类、按关键字、按价格区间筛选如果靠 Java 代码拼接 SQL那代码里会充满if判断和字符串相加维护起来很痛苦。用where标签包住if条件就能优雅解决。select idselectGameList resultTypecom.example.pojo.Game SELECT * FROM t_game where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if /where ORDER BY create_time DESC /select这里就引出一个经常有人问的问题XML 里能不能直接写和答案是可以直接写但不行因为 XML 解析器会把当成标签开始。要么用lt;和gt;转义要么用![CDATA[ price 100 ]]包起来。我个人更推荐用转义因为 CDATA 块内的内容不能嵌套if等动态 SQL 标签把条件写在 CDATA 外面的话要注意别包错了范围。另外一个经典问题就是“单个数字字符比较”。很多人在写状态判断时这样写if teststatus 1大多数情况下没问题但如果传入的status是 String 类型而1是数字字面量MyBatis 在做 OGNL 表达式解析时会把1和1比较不同类型比较的结果不稳定可能为 false。我在做订单状态查询时就踩过这个坑从前端传过来的参数都是 String到了 Mapper 里若要比较数字最好在 Service 层先把类型转成 Integer或者在 XML 里写成status 1.toString()。更稳妥的做法是统一用字符串比较全部写成1然后保证数据库字段类型是 varchar 或 Service 层完成类型转换。2.3 自动装配、版本地狱与 SpringBoot“太高”的版本SpringBoot 的自动装配原理是面试高频题也是理解这个项目的钥匙。简单来说启动类上的SpringBootApplication是一个组合注解里面包含了SpringBootConfiguration、EnableAutoConfiguration、ComponentScan。核心是EnableAutoConfiguration它会通过spring.factoriesSpringBoot 2.7 及之前版本或AutoConfiguration.importsSpringBoot 2.7 以后版本加载所有候选的自动配置类再通过ConditionalOnClass、ConditionalOnMissingBean等条件注解判断是否生效。比如我们引入了spring-boot-starter-jdbc后DataSourceAutoConfiguration就会尝试根据application.yml里的spring.datasource配置创建数据源。你说我不用数据库行不行行那就把数据源相关依赖从 pom.xml 里排除否则启动就会报“Failed to configure a DataSource”错误。这个报错在初学者里出现的频率极高原因就是引入了 MyBatis 相关的 starter但没配置数据库连接。“SpringBoot 版本太高”是单独拎出来说的一个坑。很多人一创建项目就选了最新版的 SpringBoot 3.x然后发现网上大量教程代码跑不起来。原因有两个一是 SpringBoot 3 最低要求 JDK 17如果你电脑装的还是 JDK 8那就直接玩不了二是 SpringBoot 3 把javax.servlet迁移到了jakarta.servlet一些老代码的import javax.*全会报红。另外MyBatis 官方针对 SpringBoot 3 推出了专门的mybatis-spring-boot-starter3.x 版本不能直接用 2.x 版本否则启动会找不到SqlSessionFactory。我给这个项目选 SpringBoot 2.7.x不是为了守旧而是第一JDK 8 是目前大量公司和学校机器的标配2.7 兼容得最好第二网上的资料和开源组件几乎都兼容 2.x遇到问题能搜到答案第三2.7 是 2.x 系列最后一个版本功能稳定安全补丁也有支持。如果你的环境已经是 JDK 17 且想用 SpringBoot 3升级路径其实也不复杂把 javax 的 import 批量替换成 jakarta升级 starter 版本再注意一下Redis等中间件的客户端版本即可。3. 前端与部署Vue、m3u8 播放和打包难题3.1 Vue 项目初始化与组件/路由划分前端部分我用 Vue 2 Element UI通过 Vue CLI 创建工程。项目结构上views目录按角色划分用户端的Home.vue、GameList.vue、GameDetail.vue、OrderList.vue、Login.vue、Register.vue管理端的AdminLayout.vue、Dashboard.vue、GameManage.vue、UserManage.vue、OrderManage.vue。组件放到components目录比如HeaderBar.vue、GameCard.vue、Pagination.vue。路由的划分要注意权限问题。用户端路由用component: () import(../views/Home.vue)这种懒加载方式减少首屏加载时间。管理端的路由全部放在一个AdminLayout父路由下面子路由分别对应各个管理页面。然后在路由配置里加一个全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path.startsWith(/admin) !token) { next(/login) } else { next() } })这只是前端层面的路由拦截真正的权限校验在后端。前端拦截能提高用户体验但不能当作安全手段因为所有接口都可以直接请求。3.2 视频预览模块Vue 播放 m3u8游戏详情页要展示游戏预告或实机演示视频服务端存储的是 m3u8 切片文件。m3u8 是 HLS 协议下的视频播放列表格式它本身不是视频文件而是一个包含多个 ts 切片文件路径的文本索引。浏览器原生 video 标签不支持 m3u8 播放所以需要在 Vue 里集成hls.js这个库来实现。安装依赖npm install hls.js --save在组件中的核心写法import Hls from hls.js mounted() { const video this.$refs.video if (Hls.isSupported()) { const hls new Hls() hls.loadSource(this.videoUrl) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () { video.play() }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // 兼容 iOS Safari 原生支持的情况 video.src this.videoUrl } }这里有一个注意事项跨域请求 m3u8 时服务端需要在响应头里加Access-Control-Allow-Origin否则浏览器会把 ts 切片拦截掉表现就是第一秒能播后面一直卡在加载。SpringBoot 后端统一处理跨域后这个现象一般就不会出现。另外前后端分离开发时Vue 默认跑在 8080 端口SpringBoot 跑在 8080 端口两者冲突的话记得把 Vue 的 devServer 端口改成 8081我在项目里是直接通过vue.config.js配置代理转发这样前端请求/api自动转发到http://localhost:8080也不存在跨域问题了。3.3 打包部署布局异常与刷新 404 的解决办法前端开发完成后需要执行npm run build打包成静态文件。常见的坑有两个。第一个是“Vue 打包后布局异常”。开发环境正常打包后图片不显示、CSS 错乱、路由链接 404。原因基本都是静态资源路径问题。默认构建产物的资源路径是绝对的/js/xxx.js如果你的站点部署在服务器根路径下没问题但放到子目录或者用 Tomcat 二级路径访问时资源就找不到了。解决办法是在vue.config.js里设置module.exports { publicPath: ./, outputDir: dist, assetsDir: static }publicPath: ./改成相对路径后index.html 里引用的 JS/CSS 路径就是相对的放到任何目录都能访问。但要注意图片等资源通过 JS 动态拼接路径的场景相对路径可能失效所以能放在 CDN 或域名根路径下自然最好。第二个是“路由 history 模式刷新 404”。Vue Router 默认是 hash 模式URL 里有#刷新不会出问题但不太好看。改成mode: history让 URL 更干净后刷新/game/123这个地址后端找不到这个路径对应的资源就会返回 404。解决办法分两种情况用 Nginx 部署的话加一行try_files $uri $uri/ /index.html;把所有请求都重写给前端入口用 SpringBoot 部署的话写一个路由 fallback 处理类把非/api开头的请求全部转发到index.html。4. 完整实操从环境准备到核心模块跑通4.1 环境清单与安装配置流程我是从零开始搭环境的这里把每一步都列出来照着做基本不会跑偏。第一步是 JDK。这个项目用 JDK 1.8去 Oracle 官网或者用开源发行版比如 Adoptium 下载安装。安装完成后配置JAVA_HOME环境变量然后在命令行执行java -version确认输出里带1.8。如果电脑里已经装了高版本 JDK建议单独解压一个 JDK 8 目录通过 IDE 里切换 Project SDK 来指定不要改全局 JAVA_HOME以免影响其他项目。第二步是 Maven。去官网下载二进制包解压后配置MAVEN_HOME和 PATH再修改conf/settings.xml里的本地仓库路径和阿里云镜像。镜像配置是重点国内直连 Maven Central 下载依赖非常慢加了阿里云镜像后基本上秒下。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf nameAliyun Maven Mirror/name urlhttps://maven.aliyun.com/repository/central/url /mirror第三步是 MySQL 8.0。下载安装包后一路 Next 安装完中间会让你设置 root 密码。安装完成后用命令行或 MySQL Workbench 连接验证。需要提醒的是MySQL 8.0 默认字符集虽然支持 utf8mb4但你创建数据库时最好还是显式指定CREATE DATABASE ps_game DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;否则可能出现中文乱码尤其是你往表里导数据的时候表里字段的排序规则也要确认是utf8mb4_general_ci。第四步是 Node.js 和 npm。Vue 2 项目建议使用 Node 16.x 或 18.x太新的 Node 版本在安装老依赖时可能报错。安装完成后执行node -v和npm -v确认。然后npm install安装前端依赖依赖数量比较多耐心等一会。4.2 数据库设计与初始化脚本要点这个项目的核心表我设计成下面这个样子表名核心字段说明t_userid, username, password, nickname, avatar, phone, role, status用户表password 存 BCrypt 加密结果t_game_categoryid, name, sort, status游戏分类比如动作、冒险、赛车t_gameid, category_id, name, cover, preview_url, description, price, stock, status游戏表preview_url 存 m3u8 地址t_orderid, order_no, user_id, game_id, service_type, amount, status, remark, create_time订单表order_no 用时间戳随机数生成t_commentid, game_id, user_id, content, create_time评论表t_noticeid, title, content, create_time公告表t_bannerid, title, image, link, sort, status首页轮播图创建表之后先插入几组测试数据两个分类、五六款游戏、一个管理员账号、一个普通用户账号。管理员密码直接用 Java 代码生成 BCrypt 加密串我写了一个临时的 main 方法调用BCryptPasswordEncoder().encode(admin123)拿到加密结果再 insert 进去。前端上传的图片和视频放在服务器本地磁盘路径/upload下SpringBoot 里配置一个静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: System.getProperty(user.dir) /upload/); } }这样上传的文件就能直接通过http://localhost:8080/upload/xxx.jpg访问了。注意 Windows 路径和 Linux 路径的写法不同Linux 下要用/home/app/upload/这种绝对路径别把反斜杠混进去。4.3 核心接口与前端页面联动把接口先理清前后端联调就不容易乱。下面是我整理的核心接口清单。用户端方法路径说明POST/api/user/login用户登录返回 tokenPOST/api/user/register用户注册GET/api/games分页查询游戏列表支持分类/关键字/价格筛选GET/api/games/{id}游戏详情GET/api/games/{id}/comments按游戏查评论POST/api/user/comments发表评论POST/api/user/orders创建订单GET/api/user/orders查询当前用户订单列表GET/api/user/orders/{id}订单详情PUT/api/user/orders/{id}/cancel取消订单管理端方法路径说明GET/api/admin/dashboard首页统计数据用户数、订单数、游戏数GET/POST/PUT/DELETE/api/admin/games游戏增删改查GET/POST/PUT/DELETE/api/admin/categories分类管理GET/api/admin/orders订单列表PUT/api/admin/orders/{id}/status修改订单状态GET/POST/PUT/DELETE/api/admin/notices公告管理GET/POST/PUT/DELETE/api/admin/banners轮播图管理前端页面与接口对应关系是这样Home.vue在created钩子里调用GET /api/banners获取轮播图、调用GET /api/games获取热门游戏GameList.vue通过路由参数接收分类 id然后传给查询接口GameDetail.vue进入后先查详情再调评论接口下单操作在你的代码里就是往t_order插一条记录前端拿到成功后跳转到订单列表页。联调时最常见的问题就是字段名对不上后端返回createTime前端却用的create_time。解决办法是后端实体类加JsonProperty(createTime)注解或者前端统一用后端返回的驼峰字段。我这里为了方便直接把后端实体类属性都设计成驼峰格式数据库表字段用下划线通过 MyBatis 的mapUnderscoreToCamelCase配置自动映射。4.4 登录鉴权与权限控制的落地实现登录鉴权我用的方案是 JWT 拦截器不引入 Spring Security因为对于学习项目来说Spring Security 的过滤器链配置理解成本比较高容易劝退。JWT 方案足够满足系统的权限控制需求。核心思路是用户登录成功后用jjwt库生成一个包含用户 id 和用户名的 token设置过期时间为 24 小时。然后把 token 返回给前端前端存在 localStorage 中。后续每个请求在 axios 拦截器里自动带上Authorization: Bearer token头。axios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })后端写一个拦截器对/api/user/**和/api/admin/**下的请求做 token 校验public class JwtInterceptor implements HandlerInterceptor { 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 )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }管理端接口还需要额外判断role是不是admin。我的做法是在拦截器里把所有/api/admin/**的请求额外校验一次角色如果角色不对直接返回 403。用户端和管理端复用同一个登录接口只是前端根据返回的 role 决定跳转到首页还是后台。密码存储别用 MD5MD5 已经被彩虹表破解得差不多了。用 Spring Security 里的BCryptPasswordEncoder单独引入一个工具类即可不用引入整个 Spring Security。BCrypt 每次加密结果都不一样但校验时能识别安全性远高于 MD5。5. 常见问题与排查技巧实录5.1 MySQL 与 SpringBoot 连接阶段的坑连不上数据库大概能占到这个项目所有启动问题的八成。常见报错和解决办法如下报错信息原因解决办法Access denied for user rootlocalhost密码错误或用户无权限检查 url 里密码和 MySQL 实际密码是否一致Communications link failure数据库服务没启动或 url 写错端口确认 MySQL 服务已启动端口默认 3306Unknown database ps_game数据库不存在先执行建库语句Public Key Retrieval is not allowedMySQL 8.0 认证问题在 jdbc url 后加?allowPublicKeyRetrievaltrueuseSSLfalseTimezone 相关的报错MySQL 时区未设置在 url 后加serverTimezoneAsia/Shanghai关于jdbcurl我建议统一写成jdbc:mysql://localhost:3306/ps_game?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruerewriteBatchedStatementstruerewriteBatchedStatementstrue是我后来加的批量插入数据时性能差距非常明显。另一个在实战中容易遇到的是“MySQL 中更新子查询”的问题。比如你要更新所有热门游戏的价格同时把热门游戏按某条件排除掉写UPDATE t_game SET price price * 0.9 WHERE category_id IN (SELECT id FROM t_game WHERE ...)时MySQL 会报You cant specify target table t_game for update in FROM clause。这是因为 MySQL 不允许在 UPDATE 语句的子查询里引用目标表。解决办法是套一层临时表UPDATE t_game SET price price * 0.9 WHERE id IN ( SELECT id FROM ( SELECT id FROM t_game WHERE category_id 2 ) AS tmp )这个知识点很实用我在后台做批量改价功能的时候就用到了。5.2 项目启动后运行期的常见坑端口冲突是另一个高频问题。SpringBoot 默认端口 8080Vue devServer 默认也是 8080。我的解决方式是SpringBoot 端口保持 8080Vue 在vue.config.js里设置devServer: { port: 8081 }同时配置代理devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里的请求地址直接写/api/user/login就行由 devServer 转发到后端不需要写http://localhost:8080这种完整地址。MyBatis 相关的启动报错Invalid bound statement (not found)出现频率最高。解决思路是检查三点Mapper 接口和 XML 文件的 namespace 是否一致Mapper 接口方法名和 XML 里的 id 是否一致XML 文件是否被打包进了 target/classes 目录。关于第三点SpringBoot 打包默认只打包src/main/java下的.java文件XML 放在java目录下需要额外在pom.xml里配置build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build如果你把 XML 放在src/main/resources目录下就没有这个问题。我习惯把 XML 和 Mapper 接口放在同一个包下这样代码阅读更直观但打包配置不能漏。运行期还有一个常见现象接口返回了数据但前端表格空白。这时候按下 F12 看浏览器控制台的报错信息十有八九是字段名对不上或者返回结构不是前端预期的{ code: 200, data: [...] }格式。我的原则是后端统一返回一个 Result 对象public class ResultT { private Integer code; private String message; private T data; }前端 axios 响应拦截器里统一取response.data.data错误码非 200 时弹出 message。这样全局的错误处理逻辑就收敛到了一个地方不用每个页面都写 try-catch。5.3 面试向问答速查这套系统可以聊的深度这个项目如果写进简历面试官大概率会围绕以下问题深挖。提前准备好这些回答面试效果会比单纯背八股文好很多。“SpringBoot 自动装配是怎么实现的”你可以结合你的项目说SpringBoot 启动时通过EnableAutoConfiguration引入AutoConfigurationImportSelector它会读取META-INF/spring.factories或AutoConfiguration.imports文件里的配置类列表再根据 classpath 下的类是否存在、容器中是否已有对应 Bean 等条件决定是否加载。比如MybatisAutoConfiguration在项目引入了mybatis-spring-boot-starter且配置了数据源后才生效它内部会创建SqlSessionFactory和SqlSessionTemplate。“MyBatis 一级缓存和二级缓存的区别是什么”一级缓存是 SqlSession 级别默认开启会话关闭即失效二级缓存是 namespace 级别需要手动开启可以跨 SqlSession 共享。要注意二级缓存存在脏读风险因为不同 namespace 的更新操作不会自动清空其他 namespace 的缓存。我在项目里对订单模块没开二级缓存只对分类表这种低频变更数据使用并且设置了较短的 flushInterval。“你在项目中如何解决跨域”开发环境下用 Vue devServer 代理解决生产环境下前后端同域部署SpringBoot 的 Controller 上加CrossOrigin或统一配置CorsFilter兜底。注意CrossOrigin加到 Controller 类上只对当前类生效全局配置要在配置类里注册CorsFilterBean。“订单和库存一致性怎么保证”如果你的游戏表有库存字段下单时要先扣库存再创建订单用Transactional保证原子性。扣库存的 SQL 一定要带上库存条件UPDATE t_game SET stock stock - 1 WHERE id #{gameId} AND stock 0通过受影响行数判断是否扣减成功如果影响行数为 0 说明库存不足抛异常回滚事务。这套“乐观锁扣库存”的思路在面试里是加分项。“怎么处理接口幂等”比如用户重复点击下单按钮可能会创建多条相同订单。解决方案是前端按钮禁用后端在订单表里加一个用户和游戏组合的唯一索引或者在创建订单前先查一次是否已有同一用户对同一游戏的有效订单。我实际做的时候用后者简单可靠。5.4 还有几个小经验想分享开发时我建议开启 MyBatis 的 SQL 日志打印。在application.yml里配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每次执行 SQL 都会在控制台打印完整的预处理语句和参数值排查参数绑定问题非常直观。不过生产环境要关掉否则日志量太大还会把敏感数据打进日志文件。有些朋友喜欢用MyBatis Log Plugin这个 IDEA 插件它会把Preparing和Parameters两行拼成一条可直接执行的 SQL调试时确实方便但别完全依赖它因为你复制到 Navicat 里执行的 SQL 可能少了空格或引号有问题最终还是得回控制台看原始日志。个性化设置方面SpringBoot 启动时那个默认的 Spring ASCII 艺术字是可以改的。用 Spring Boot Banner 生成器在线制作一个自己想要的横幅把生成的内容放到src/main/resources/banner.txt里重启项目就能看到效果。这个不涉及任何业务逻辑纯粹是提升开发心情的小操作。上传文件保存到本地磁盘有一个隐患重启后文件不会丢但如果你迁移服务器文件不在项目目录里的路径可能忘了备份。更好的做法是把文件路径存进数据库迁移时把整个 upload 目录一起拷走就行。我现在的方案就是数据库里只存/upload/2025/01/xxx.jpg这种相对路径前端展示时再拼服务器地址。最后聊两句扩展方向这套系统跑通之后我自己又做了几个方向的扩展引入 Redis 缓存热门游戏列表减少了数据库压力为了加快开发效率把原生分页改成PageHelper插件分页代码一下子就简化了还引入了 RabbitMQ 消息队列用户下单后异步发送通知。这些扩展对学习来说是很好的进阶路线你可以根据自己的时间选一个方向去尝试。如果要做生产级部署推荐用 Docker Compose 把 MySQL 和后端、前端容器编排起来配合 Nginx 反向代理和 HTTPS 证书也算是完整的上线链路了。不过这些属于另一个话题了先把这篇文章里的东西消化掉跑通一遍比收藏一堆教程有用得多。我做这个项目最大的体会是代码能跑只是起点能讲清楚每一行配置背后的原因才是真的学会了。希望你也能从这套源码里收获同样的体会。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →