
开始做这个项目之前我先说说结论这套Java SpringBootVue3MyBatis 游戏销售平台系统源码前后端分离MySQL数据库本质上是一个标准的电商系统改造版只是商品对象从普通实物换成了游戏、卡密、虚拟道具这类数字商品。技术栈组合是当前 Java 后端岗位最主流的一套SpringBoot 做接口服务Vue3 做前端页面MyBatis 搞定数据库持久层MySQL 存业务数据。这套源码适合正在准备毕设的在校生、打算跳槽 Java 全栈的工程师以及想快速搞一个完整项目练手的人。它能解决的问题很明确从前台商品展示、购物车、下单支付流程到后台商品管理、订单管理、用户管理一整套前后端分离的电商业务闭环。我先说一下我拿到这套源码后的整体感受它没有花里胡哨的微服务拆分也没有复杂的中间件依赖就是老老实实把电商核心链路做扎实了。但你千万别小看这种老实我见过太多项目一上来就上 Spring Cloud Alibaba、上 RabbitMQ、上 Redis 缓存结果连最基本的商品上下架、库存扣减都没做对。这套源码的定位很清晰——用最少的组件把业务跑通同时保留足够的扩展点。下面我从设计思路、数据库、后端实现、前端联调、部署运维五个维度把这套系统的里里外外拆一遍。1. 游戏销售平台的整体设计思路与选型分析1.1 需求拆解游戏销售平台到底要卖什么、管什么游戏销售平台和普通电商平台有一个很大的区别卖的是虚拟商品而不是实物。这里说的虚拟商品又分几种形态这套源码基本都覆盖了。第一种是标准游戏本体或 DLC比如某款 3A 大作的激活码、Steam 充值卡这类商品交付的是 CDKey 或兑换码所以数据库里必须有卡密库存的概念用户下单后系统自动发货把卡密明文返回给用户。第二种是游戏账号或代练服务这类是服务型商品需要平台方在后台人工处理订单所以订单状态里要有一个虚拟发货或等待处理的状态。第三种是点卡、游戏币充值这类商品有固定面额和兑换比例SKU 相对简单但订单量通常最大。从需求层面看前台功能包括用户注册登录、商品分类浏览、关键词搜索、购物车管理、订单提交与支付、个人中心订单查询后台功能包括商品管理上下架、库存维护、卡密批量导入、订单管理发货、取消、查询、用户管理、分类管理。如果再算上数据统计看板那就是一个完整的电商中后台了。这套源码的核心模块划分我建议你重点看这几个用户模块注册、登录、JWT 鉴权、个人信息维护商品模块分类、商品列表、商品详情、库存管理购物车模块加购、修改数量、删除、批量结算订单模块下单、支付模拟、取消、发货、订单明细后台管理模块商品 CRUD、卡密导入、订单处理1.2 技术选型的取舍为什么偏偏是这四个组件我见过不少项目为了追求技术新颖硬塞了一堆框架进去最后项目体积膨胀、维护成本极高、连原作者都跑不起来。这套源码在选型上其实非常克制我逐个说。SpringBoot 选 2.x 或 3.x 都行关键是它解决了配置地狱问题。以前做 SSM 项目光 spring-context、spring-mvc、mybatis-spring 的 XML 配置就能写满两页纸SpringBoot 用自动配置把这些全干了只留一个 application.yml 解决问题。对内嵌 Tomcat打包成 jar 直接java -jar就能跑部署成本极低对新手特别友好。Vue3 相比 Vue2 最大的变化是 Composition API也就是setup语法。它让组件逻辑聚合度更高同一个业务功能的数据、计算属性、方法可以写在一起不像 Options API 那样必须按 data、computed、methods 分块。配合 Vite 的秒级热更新前端开发体验和 Vue2 时代完全是两个量级。组合式函数useXxx可以完美抽离业务逻辑比如把加入购物车整个流程封装成一个函数多个页面复用。MyBatis 的核心优势是 SQL 可控。电商系统里订单报表、销售统计这类查询往往没有现成的 CRUD 能用需要写手写 SQL 关联多张表用 MyBatis 可以把 SQL 直接写在 XML 里做精细控制而且动态 SQL 的if、foreach在写多条件筛选时非常顺手。相比 JPA/HibernateMyBatis 的性能可预测性更强DBA 审查 SQL 也方便。MySQL 就是久经考验的关系型数据库事务支持完善InnoDB 引擎的锁机制和 MVCC 机制在电商订单场景下足够用了。它的生态工具链也很成熟Navicat、DataGrip 连上就能操作网上资料最多踩坑了搜一下就有答案。1.3 前后端分离到底分的是什么前后端分离的核心是把数据渲染这件事从前端模板中剥离出来让后端只负责返回 JSON 数据前端只负责拿数据渲染页面。这套源码的项目结构是标准的前后端分离工程game-store ├── backend # SpringBoot 后端工程 │ ├── src/main/java/com/gamestore │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ ├── entity │ │ ├── dto │ │ ├── common │ │ └── config │ ├── src/main/resources │ │ ├── mapper/*.xml │ │ └── application.yml │ └── pom.xml └── frontend # Vue3 前端工程 ├── src │ ├── api │ ├── views │ ├── components │ ├── stores │ ├── router │ └── utils ├── vite.config.js └── package.json分离的核心契约就是 API 接口文档。前后端只认接口的 URL、入参、返回结构其他互不干涉。例如前端负责用户点登录按钮拿 token后端只负责核对用户名密码签发 token。接口返回结构必须统一这套源码里的ResultT封装是必须有的否则前端要针对每个接口写不同的错误处理逻辑。这样做的好处我实际体验非常明显并行开发效率高前端拿 mock 数据开发页面后端按接口文档开发服务互不阻塞部署灵活后端服务只出 API前端可以单独部署到 Nginx也可以用热词里提到的vue打包放进springboot中方式二次集成后期拆分微服务、增加新的前端入口比如小程序端、管理端都不需要动后端核心代码提示如果你只是本地学习前后端分离意味着要同时启动两个服务我建议你先把后端跑起来并用 Postman 测通几个核心接口再启动前端联调否则两头一起报错你很难定位问题在哪。2. 数据库设计与 MyBatis 持久层核心细节2.1 表结构设计思路这套源码的数据库是怎么规划的游戏销售平台的数据库设计比普通电商多了一个关键点卡密库存。商品表除了商品基本信息还要关联一个独立的卡密表或者直接在商品表里维护库存数量。这套源码用了一张game_stock表来单独管理卡密库存这样实现了商品信息和可售库存解耦我认这是一个不错的做法。我梳理了几个核心表的设计要点用户表user主键id、用户名、密码BCrypt 加密存储、昵称、手机号、邮箱、头像、状态、创建时间。密码必须加密存储明文存密码的代码在简历上就是送命题。MySQL 里如果选 VARCHAR 存 MD5长度 32 就够但 BCrypt 哈希是 60 位所以密码字段要给到 VARCHAR(60) 以上。分类表category主键、分类名称、父分类 ID支持二级分类、排序、状态。游戏平台通常有PC 游戏主机游戏游戏点卡账号服务这些分类用父分类 ID 实现树形结构前端渲染分类菜单时按父 ID 分组。商品表product主键、分类 ID、商品名称、商品封面图、商品详情描述、单价DECIMAL(10,2)、库存数量、销量、状态上架/下架、创建时间、更新时间。这里有两个细节价格绝不能用 DOUBLE电商场景必须用 DECIMAL否则浮点误差在累计结算时会出问题库存字段只是个剩余可卖数的冗余真正的批次库存要依赖卡密表。卡密表game_stock主键、商品 ID、卡密内容、状态未售出/已售出/已使用、订单 ID 关联售卖时间。这个表的设计很关键用户购买虚拟商品后系统需要从这个表里取一条未售出的记录标记为已售出并绑定订单号。购物车表cart主键、用户 ID、商品 ID、购买数量、加入时间。我注意到这个表没有做 勾选状态前端用 Pinia 管理勾选状态传结算时只传勾选的商品 ID 列表这是一种省事的做法避免了频繁更新数据库勾选状态。订单表order订单号唯一、用户 ID、订单总金额、订单状态待付款/已付款/已发货/已取消/已完成、支付时间、发货时间、创建时间。订单明细表order_item主键、订单 ID、商品 ID、商品名称快照、商品图片快照、成交单价、购买数量。这里要理解快照的重要性用户在订单历史里看到的商品名称和价格必须是下单那一刻的值如果关联实时商品表商家改了商品名或价格历史订单也会跟着变这是电商系统的大忌。关于主外键这套源码没有用物理外键全部靠应用层逻辑维护关系。这个设计我认为符合生产环境惯例因为物理外键在插入更新时会触发额外的一致性检查高并发下单场景下性能受影响而且项目演进中改表结构时会很痛苦。用逻辑外键靠 SQL 的 JOIN 就能保持查询关联。2.2 MyBatis 的动态 SQL 和缓存这层是最容易被面试官问到的MyBatis 持久层在这套源码里主要体现为 Mapper 接口加 XML 文件的模式。我建议你重点看商品列表查询这个 mapper因为它是动态 SQL 的典型应用多条件筛选加排序。select idselectProductList resultTypecom.gamestore.entity.Product SELECT * FROM product where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where choose when testsortField ! null and sortField sales ORDER BY sales DESC /when otherwise ORDER BY create_time DESC /otherwise /choose /selectwhere标签会智能去掉第一个条件前面的 AND这个细节如果手写WHERE 11拼接一旦遇到索引字段加了函数或者类型转换查询性能就废了。所以用 MyBatis 的where不光是整洁的问题是正确写法。${}和#{}的区别这套源码里也有体现。${}直接拼接字符串有 SQL 注入风险只能用来拼接列名或表名#{}/code 是预编译参数占位符用的 JDBCPreparedStatement安全且能走数据库预处理缓存。排序字段因为不能做参数绑定只能用${...}但必须做白名单校验比如只允许传sales、create_time、price 这几个固定值防止注入。关于 MyBatis 缓存这是很多人在面试和实际调优中都绕不过去的话题。一级缓存是 SqlSession 级别的默认打开同一个 SqlSession 内相同 SQL 只查一次数据库。二级缓存是 Mapper 级别的需要手动开启mapper namespacecom.gamestore.mapper.ProductMapper cache evictionLRU flushInterval60000 size512 readOnlytrue/ /mapper不过我不建议在电商系统里依赖 MyBatis 的二级缓存原因是缓存失效的粒度太粗只要这个 mapper 对应的表发生了一次 insert/update比如下单导致库存变化整个缓存就清空高并发场景下缓存反而成了压力源。更靠谱的方案是这种简单的业务系统直接用 Redis 缓存热点商品数据或者在服务层用本地 Caffeine 缓存。2.3 统一返回结构、全局异常这是代码可读性的关键这套源码里有一个ResultT统一返回体我实际用下来觉得这个设计非常有用。它的结构是 code、message、data 三个字段code 为 2000 表示成功非 2000 表示各类业务异常。前端 Axios 拦截器只需判断 code 是否为 2000是则取 data否则统一弹 toast。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.code 2000; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }全局异常处理用RestControllerAdvice是配套操作。Controller 里不做 try-catch业务层抛出BusinessException比如库存不足、商品已下架全局处理器统一捕获并转成Result.error。这样代码里就不会出现那种控制器里密密麻麻的 try-catch logger.error的丑陋写法。注意异常状态码和 HTTP 状态码不要混为一谈。Result.code是业务状态码HTTP 状态码统一用 200 返回这样前端拦截器容易处理。但如果是未登录或者权限不足最好明确用 HTTP 401/403让 Axios 的error回调去统一跳转登录页。混合用很容易导致逻辑混乱。3. SpringBoot 后端核心业务实现订单和库存代码是如何落地的3.1 商品列表和详情的接口实现Vue 页面不能直接连数据库商品列表接口的逻辑相对简单Service 层调用 ProductMapper 查询条件列表返回给 Controller。但这里面有一个细节值得你学习分页处理。这套源码给了两种方案一种是直接手动计算 limit offset 传 Param另一种是引入 PageHelper 或 MyBatis-Plus 的分页插件。我推荐你在学习阶段用最笨的LIMIT #{offset}, #{pageSize}去理解分页原理搞清楚第 N 页的偏移量是如何计算的。等理解了再引入分页插件否则你只会调一个PageHelper.startPage()一旦遇到复杂 SQL 的 count 语句优化问题就抓瞎了。商品详情接口还涉及一个浏览量 1的异步操作。简单项目直接同步UPDATE product SET view_count view_count 1 WHERE id ?就行但你可以在代码里注释一句——生产环境这块应该改成消息队列异步落库。这个细节写进简历里会让面试官认为你真的思考过性能瓶颈。3.2 购物车和订单流程下订单的数据库事务边界怎么划购物车加入、修改数量、删除这些接口本质就是 cart 表的增删改但有一个点容易踩坑加入购物车时如果该用户和商品已经在购物车里存在应该走数量累加而不是再插一条重复记录。这个要先查一遍再决定 insert 还是 update或者用 INSERT ... ON DUPLICATE KEY UPDATE — 前提是给 user_id product_id 建了唯一索引。订单流程是整套系统的核心亮点。我把它完整串一遍用户从前端购物车勾选商品点结算前端把商品 ID 列表和数量传给后端后端进入createOrder方法这个方法必须用Transactional包起来Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto) { // 1. 计算订单总金额并生成订单号 // 2. 插入 order 主表 // 3. 批量插入 order_item 明细 // 4. 扣减库存 product.stock stock - quantity // 5. 如果商品是虚拟卡密从 game_stock 取卡密并绑定订单 // 6. 清空购物车中已结算的商品 }这里面的难点是第 4 步扣库存。如果直接SELECT stock FROM product WHERE id ?然后在 Java 代码里算出新库存再UPDATE并发场景下会超卖。我实测这套源码用的是乐观锁思路UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这条 SQL 本身保证原子性和条件检查影响行数为 0 时说明库存不够事务直接抛异常回滚。用生活化类比这就像飞机选座系统不是先查一遍座位再用程序逻辑判断而是直接占座检查座位是否存在一步完成在高并发下不会出现两个人同时看到最后一个座位。第 5 步取卡密要考虑的是并发重复取的问题。正确写法是UPDATE game_stock SET status SOLD, order_id #{orderId}, sold_time NOW() WHERE product_id #{productId} AND status UNSOLD AND id ( SELECT id FROM game_stock WHERE product_id #{productId} AND status UNSOLD LIMIT 1 )用 UPDATE 语句自带的行级锁来抢占卡密MySQL InnoDB 默认对 UPDATE 加行锁多个并发事务同时执行这条 SQL 时只有一个事务能成功锁定该行其他事务等待。这是最稳的虚拟商品发货方案不需要额外引入分布式锁。提示Transactional只能通过代理对象调用才生效同类的this.method()调用是绕过了代理注解会失效。我看到这套源码里 createOrder 和 cancelOrder 如果互相调用务必通过 Spring 注入的 Service Bean 调用这个坑追查起来相当费时间。3.3 订单状态的流转逻辑模拟支付和定时关单订单状态的流转用状态机来理解最清楚禁止使用 mermaid这里我改用文字描述待付款 → 已付款 → 已发货 → 已完成待付款状态超时未支付应该自动关闭释放库存。很多初学项目会把超时关单用定时任务实现每 30 秒扫一次订单表找到超过 15 分钟未支付的订单取消并回滚库存。这套源码也是这么做的我毫不避讳地说这是最低成本的简单方案在实际生产里订单量大时会有点慢但没有引入消息队列前够用。模拟支付就是一个接口把订单状态从待付款改为已付款。真实场景是在用户点击支付后跳转三方支付回调回调验签后更新订单状态。这里我建议你在项目 README 里明确写清楚支付接口是本地模拟的而不是伪造了对接某支付渠道简历上如实说明就好。4. Vue3 前端工程与前后端联调实操4.1 Vue3 项目结构和核心依赖Vite 工程目录怎么组织前端这部分我用 Vite 创建工程整体目录如下src/api/按模块拆分的接口请求函数src/views/页面级组件src/components/业务级组件商品卡片、订单状态标签等src/stores/Pinia 状态管理存储购物车数据和用户 tokensrc/router/路由配置src/utils/request.jsAxios 实例封装src/layout/后台管理布局技术依赖就这几个核心的vue-router 做路由、Pinia 做状态管理、Axios 发请求、Element Plus 做后台管理界面。Vue3 项目一定要配合 ESNext 语法script setup会让代码比 Options API 省一半代码量。script setup import { ref, onMounted } from vue import { getProductList } from /api/product const products ref([]) const loading ref(false) onMounted(async () { loading.value true try { const res await getProductList({ page: 1, pageSize: 12 }) products.value res.data.records } finally { loading.value false } }) /script对比 Vue2 的 data 里定义数组、methods 方法和 mounted 钩子script setup里一切都是普通变量和函数逻辑上聚合度非常高。这是哪个 Vue 版本核心亮点必须适应。4.2 Axios 封装、鉴权、跨域联调期 90% 的问题都在这三层开发阶段前后端分离最大的痛点是跨域。我在联调阶段遇到的报错基本都集中在这块。正确做法是前端 Vite 配置代理// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/product/listVite 开发服务器把请求转发到后端http://localhost:8080/api/product/list浏览器看到的请求是同源的不触发 CORS。changeOrigin: true会把请求头里的 Host 改成目标地址后端不需要额外的 CORS 配置。我在不修改后端代码的情况下仅靠 Vite 代理就解决了 90% 的联调环境问题。Axios 封装是另一块核心。request.js里创建一个 Axios 实例然后加请求拦截器和响应拦截器import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理业务码和 401 request.interceptors.response.use( response { const { code, message, data } response.data if (code 2000) return data // 其他业务错误统一提示 ElMessage.error(message) return Promise.reject(new Error(message)) }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )拦截器这里要解释一个容易忽略的问题token 失效过期或被踢下线时前端必须跳登录页并清理本地存储。如果不做 401 拦截用户会看到一个半死不活的页面还以为系统出 Bug 了。4.3 Pinia 管理购物车页面状态与后端持久化如何协同购物车在前端和数据库里都有数据初学者最容易犯糊涂的是到底以哪个为准这套源码的策略是未登录时购物车数据存 Pinia内存中刷新就没了登录后购物车与后端同步加入购物车接口把商品信息写入后端 cart 表页面加载时拉取最新购物车数据。Pinia 主要用于跨页面共享购物车数量和勾选状态一个简单的 store 就能实现import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ cartCount: 0, selectedIds: [] }), actions: { setCartCount(count) { this.cartCount count } } })市面上很多毕设项目的购物车只做前端内存或者只做数据库这套源码两边都有并且主从明确是一个加分的细节。你面试的时候如果能把这个双写策略和同步时机讲清楚会明显显得比背概念的人有项目实践概念。4.4 前端打包进 SpringBoot一套免跨域的部署姿势热搜词里有vue打包放进springboot中这类需求我把这个方案也讲透它是前后端分离项目联调稳定后、单机部署时最常见的整合方式。在项目pom.xml里配置 maven-resources-plugin 把前端dist目录复制到src/main/resources/staticplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId executions execution idcopy-vue-dist/id phaseprepare-package/phase goals goalcopy-resources/goal /goals configuration outputDirectory${project.build.outputDirectory}/static/outputDirectory resources resource directory${project.basedir}/../frontend/dist/directory filteringfalse/filtering /resource /resources /configuration /execution /executions /plugin执行顺序是先在frontend/目录里npm run build生成 dist再回到backend/目录执行mvn package插件会把 dist 整个塞进 SpringBoot 的静态资源目录。启动后端后直接访问http://localhost:8080/就是前端首页/api开头的请求会命中 Controller 下面的接口不再产生跨域问题。注意前端生产环境的请求地址要用相对路径/api/...不能写死成http://localhost:8080/api/...否则打包后部署到任意域名都是错的。我在实际部署时遇到过不少人在baseURL里写死 localhost最后部署到服务器上就废掉。5. 本地运行全流程与高频问题排查手册5.1 从零启动这套项目数据库初始化到页面访问的完整操作我按最标准的流程演示一遍本地跑通这套源码的步骤环境是 Windows JDK 1.8 MySQL 5.7 Node 16。第一步初始化数据库。项目根目录下通常有sql/game_store.sql文件这是数据库脚本。打开 MySQL 客户端执行mysql -u root -p game_store.sql执行成功后会产生 game_store 数据库及全部表。我建议你在执行前看一眼脚本确认里面没有 DROP DATABASE 之类的破坏性语句另外注意表前缀和索引有没有建上。第二步改后端配置。打开backend/src/main/resources/application.yml修改数据库连接串、用户名和密码spring: datasource: url: jdbc:mysql://localhost:3306/game_store?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword这里有两个常见坑MySQL 8.0 的 driver 是com.mysql.cj.jdbc.DriverMySQL 5.7 是com.mysql.jdbc.Driver版本对不上启动直接报错serverTimezoneAsia/Shanghai不写的话如果服务器时区不是 UTC时间字段会差 8 个小时。第三步启动后端。在backend/目录执行mvn spring-boot:run看到 Started Application in x.xx seconds 说明启动成功。第四步启动前端。在frontend/目录执行npm install npm run dev浏览器访问http://localhost:5173就能看到前台页面了。如果是需要vue打包放进springboot的单体部署模式参考上一节内容即可。5.2 我实测过程中整理的高频问题速查表我把这个项目运行过程中容易反复出现的问题整理成了一张速查表这些问题每一行我都踩过至少一次现象可能原因解决方案8080 端口被占用其他 Java 服务占用了端口改server.port或netstat -ano查 PID 结束进程数据库连接超时 Access denied密码错误或账号无权限检查 application.yml 的 username/password前端请求接口 404代理没生效或接口路径不匹配看 Network 面板实际请求的 URL 是否走http://localhost:8080检查 Vite 代理路径前端请求接口 500 Unknown column实体类字段和数据库列名映射不对检查 resultMap 或是否开启驼峰映射map-underscore-to-camel-case: true商品图片不显示图片上传路径写到了本地磁盘要把上传目录映射成静态资源路径或配置 Nginx 映射购物车加多个商品但数量不累加没有处理已存在商品查询 cart 表是否存在同 user_id product_id 记录存在则 set quantity quantity 1中文乱码MySQL 连接字符集问题JDBC URL 加characterEncodingutf-8表字段要utf8mb4并发下单库存扣成负数没有使用条件更新扣库存改成UPDATE product SET stock stock - n WHERE id ? AND stock nMyBatis SQL 里#{}传表名报错表名不能参数化绑定改用${}且必须做白名单Docker 安装 MySQL 失败容器内权限或初始化脚本问题检查挂载目录权限或直接用本机 MySQLJWT token 过期后仍能访问接口拦截器没配置过期校验在 SpringMVC 拦截器里解析 token 并校验 expiration5.3 线上部署时最容易轻视的几个细节我在部署这套项目到一台 2 核 4G 的云服务器时踩了几个跟本地完全不一样的坑。打包后端 jar 时我建议显式跳过测试mvn clean package -DskipTests不然单元测试里如果有数据库依赖打包会卡半天甚至失败。前端打包记得先清掉旧的 dist 目录Vite 打包有时不清理旧文件会导致新增资源不一致。Nginx 配置做反向代理时前端应用单独部署的要点是把所有/api请求转发到后端服务server { listen 80; server_name yourdomain.com; root /opt/game-store/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }location / { try_files $uri $uri/ /index.html; }这行很关键Vue Router 如果用的是 history 模式去掉了 URL 里的 # 号刷新某个子路径时 Nginx 会报 404这个配置能保证所有路径都回退到 index.html由前端路由接管。如果你不想配这个就改用 hash 模式路由URL 里带个 # 号部署零成本。数据库层面的优化我建议把 MySQL 的max_connections调大到 200 左右给每个连接配上wait_timeout避免连接数被占满。这个项目和 SpringBoot 连接池HikariCP配合时默认连接池大小是 10 个如果并发高会有连接池等待。这个参数在生产环境不要照搬默认值根据业务量评估一下。心得我在部署时还遇到过前端页面刷新后 404、后端日志显示Connection refused、MySQL 数据库连接数飙满等一连串问题。排查顺序我推荐从网络层到应用层——先确认浏览器 Network 面板里的请求有没有到服务器再确认 Nginx 有没有把请求转发到后端端口最后看 SpringBoot 日志有没有异常。很多人一上来就翻 SpringBoot 日志结果问题出在 Nginx 的 proxy_pass 写错了浪费了大量时间。6. 项目深度改机与扩展方向这套源码可以往哪里延伸这套源码对我来说最大的价值是它把电商最有代表性的链路完整跑通了——用户、商品、购物车、订单、库存、后台管理这是我强烈建议你认真跑一遍的原因。但如果你想让它从课程设计水平上升到简历里能撑两轮技术面试的程度我建议做下面几件事。第一件把支付逻辑相关的坑补上。目前的订单超时关单是定时任务扫表如果订单量到了百万级定时任务每次扫全表会非常慢。更优雅的方案是引入延时队列Redis 的 ZSET 或者 RabbitMQ 的 TTL死信队列都行把支付超时关单这种需求从扫描变成被动触发。你要是能把这个优化方案写出来面试官会眼前一亮。第二件把搜索能力升级。商品搜索目前是 MySQL 的 LIKE 模糊查询当商品数量超过十万条LIKE %关键字%无法走索引扫描成本很高。升级到全文检索MySQL FullText 或者引入 ElasticSearch是标准演进路线。我建议至少理解 MySQL 的全文索引和 ES 倒排索引适用场景的两者对比面试常问。第三件前台加个用户评论和评分模块。游戏销售平台最怕遇到注册-买-走人这种一次性用户评论体系的加入会让整个项目立刻贴近真实业务用户下单完成后可以对商品打分和留言后台审核评论是否显示。这牵扯到数据表的关联、权限归属和多对多关系设计对数据库能力是更好的训练。第四件做性能监控和数据埋点。简单接入 Spring Boot Actuator 看/actuator/health、/actuator/metrics之类的端点判断一个服务的健康状态不是什么难事但很多人没用过。前端接入一个轻量的埋点统计商品点击量、用户路径对游戏平台选品运营来说非常实用。我个人在实际操作中体会最深的是这项目的代码风格不像课程演示那么规整到失真它带有一种真实业务项目的混沌感——比如订单状态散落在多个方法里、购物车没有做勾选持久化、部分接口的参数校验不完整。但这种混沌恰恰是真实世界的状态你接手代码后的第一件事不是抱怨而是理清它的状态流转和设计意图然后在一个小范围内动手改造。这个能力才是从会写 CRUD 的人到能维护业务系统的人的分水岭。最后再分享两个小技巧第一学习的过程一定要动手改掉至少一个业务 bug比如把库存扣减从普通更新改成条件更新把默认密码从明文改成 BCrypt 加密第二想办法把项目部署到一个真实的在线地址哪怕只是临时服务器加一个 IP 访问面试时直接打开手机演示给面试官看比任何八股文都更有说服力。前端打包放在 SpringBoot 静态资源目录里一台服务器搞定全站访问这个技巧用熟练了很多小项目的部署都能省一笔 Nginx 静态资源服务器的成本。