☰
SpringBoot+Vue3+MyBatis游戏销售平台前后端分离源码解析
2026/10/7 22:12:33 网站建设 项目流程

开始做这个项目之前,我先说说结论:这套"Java SpringBoot+Vue3+MyBatis 游戏销售平台系统源码|前后端分离+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/Hibernate,MyBatis 的性能可预测性更强,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。接口返回结构必须统一,这套源码里的Result<T>封装是必须有的,否则前端要针对每个接口写不同的错误处理逻辑。

这样做的好处我实际体验非常明显:

  • 并行开发效率高,前端拿 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 id="selectProductList" resultType="com.gamestore.entity.Product"> SELECT * FROM product <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> </where> <choose> <when test="sortField != null and sortField == 'sales'"> ORDER BY sales DESC </when> <otherwise> ORDER BY create_time DESC </otherwise> </choose> </select>

<where>标签会智能去掉第一个条件前面的 AND,这个细节如果手写WHERE 1=1拼接,一旦遇到索引字段加了函数或者类型转换,查询性能就废了。所以用 MyBatis 的<where>不光是整洁的问题,是正确写法。

${}和#{}的区别,这套源码里也有体现。${}直接拼接字符串,有 SQL 注入风险,只能用来拼接列名或表名;#{}</code> 是预编译参数占位符,用的 JDBCPreparedStatement,安全且能走数据库预处理缓存。排序字段因为不能做参数绑定,只能用${...},但必须做白名单校验,比如只允许传sales、create_time、price` 这几个固定值,防止注入。

关于 MyBatis 缓存,这是很多人在面试和实际调优中都绕不过去的话题。一级缓存是 SqlSession 级别的,默认打开,同一个 SqlSession 内相同 SQL 只查一次数据库。二级缓存是 Mapper 级别的,需要手动开启:

<mapper namespace="com.gamestore.mapper.ProductMapper"> <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> </mapper>

不过我不建议在电商系统里依赖 MyBatis 的二级缓存,原因是缓存失效的粒度太粗,只要这个 mapper 对应的表发生了一次 insert/update(比如下单导致库存变化),整个缓存就清空,高并发场景下缓存反而成了压力源。更靠谱的方案是这种简单的业务系统直接用 Redis 缓存热点商品数据,或者在服务层用本地 Caffeine 缓存。

2.3 统一返回结构、全局异常,这是代码可读性的关键

这套源码里有一个Result<T>统一返回体,我实际用下来觉得这个设计非常有用。它的结构是 code、message、data 三个字段,code 为 2000 表示成功,非 2000 表示各类业务异常。前端 Axios 拦截器只需判断 code 是否为 2000,是则取 data,否则统一弹 toast。

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 2000; r.message = "success"; r.data = data; return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> 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 状态管理(存储购物车数据和用户 token)
  • src/router/:路由配置
  • src/utils/request.js:Axios 实例封装
  • 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/list,Vite 开发服务器把请求转发到后端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/static:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-vue-dist</id> <phase>prepare-package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.outputDirectory}/static</outputDirectory> <resources> <resource> <directory>${project.basedir}/../frontend/dist</directory> <filtering>false</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?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword

这里有两个常见坑:MySQL 8.0+ 的 driver 是com.mysql.cj.jdbc.Driver,MySQL 5.7 是com.mysql.jdbc.Driver,版本对不上启动直接报错;serverTimezone=Asia/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 加characterEncoding=utf-8,表字段要utf8mb4
并发下单库存扣成负数没有使用条件更新扣库存改成UPDATE product SET stock = stock - n WHERE id = ? AND stock >= n
MyBatis SQL 里#{}传表名报错表名不能参数化绑定改用${}且必须做白名单
Docker 安装 MySQL 失败容器内权限或初始化脚本问题检查挂载目录权限,或直接用本机 MySQL
JWT token 过期后仍能访问接口拦截器没配置过期校验在 SpringMVC 拦截器里解析 token 并校验 expiration

5.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 + 静态资源服务器的成本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询