做这个“SpringBoot + Vue 的米家商城 / abo 管理系统”项目,前前后后折腾了小一个月。项目本身不算特别复杂,但把用户端商城和后台管理端揉在一起,又要保证代码能跑、能演示、能答辩,确实有不少需要注意的细节。这篇文章我就以自己的实操记录为主线,把整个项目从选型、设计到具体实现、部署排错的过程完整梳理一遍,用的技术栈就是标题里那套:SpringBoot + Vue + MyBatis + MySQL。
先说清楚这个项目是干什么的。它本质上是一个“小米商城”风格的电商系统,包含两个端:面向普通用户的商城页面(浏览商品、加购物车、下单),以及面向运营/管理员的后台系统(商品管理、订单管理、用户管理、数据统计)。很多同学在简历里写“商城项目”,但往往只做了前台展示,后台管理是空壳,答辩时一问就露馅。这个项目的好处在于前后台完整,且“abo”这种项目命名方式在老课程设计、毕业设计里边很常见,基本上等同于“一个完整可运行的 Java Web 全栈项目”的代名词。
不管你是准备拿它做毕业设计、课程设计,还是单纯想练手 SpringBoot + Vue 的全栈开发,这篇文章里提到的设计思路、代码坑点、环境配置和部署方式,都可以直接参考。下面从项目整体架构开始聊。
1. 项目整体设计与技术选型思路
1.1 这个商城系统到底需要做什么
很多新手拿到需求就急着建表,这是最要命的。先梳理清楚业务边界:既然是“米家商城” + “abo 管理系统”,那必然包含两类角色、两种操作场景。
用户端商城页面需要做的事很明确:
- 用户注册、登录,一般还要有 token 校验来维持会话状态;
- 首页商品展示,按分类、按关键字搜索商品;
- 商品详情页,展示商品图片、价格、库存、规格;
- 购物车操作,加入、修改数量、删除、批量选中结算;
- 订单流程,提交订单、生成订单号、模拟支付、查看订单列表和详情;
- 个人中心,修改个人信息、查看收货地址。
后台管理系统相对偏“管理":
- 管理员登录,不同管理员可能分配不同角色权限(这是“abo”命名里常带的点,即基于角色控制的管理系统);
- 商品管理:添加、修改、上下架、库存调整、商品图片上传;
- 订单管理:查看订单、发货、退款处理、订单状态流转;
- 用户管理:查看用户列表、禁用启用账号;
- 数据看板:展示销售统计、用户增长等简单图表。
把需求拆开写清楚后,才能确定数据库表和接口要设计到什么粒度。我在做的时候,刚好还看到热词里有一堆关于“vue 播放 m3u8”“vue 播放欢乐谷 m.3u8”的搜索,这说明很多同学想着在商城商品详情里加视频展示。这个点完全可行,后面我单独在 Vue 前端那节说播放的实现方式。
1.2 为什么选了 SpringBoot + Vue + MyBatis + MySQL
这套组合在今天几乎成了 Java 全栈入门项目的“标准配置”,不是没有道理。
后端用 SpringBoot,好处是约定优于配置,一个 main 方法就能启动整个 Web 服务。相比传统的 SSM(Spring + SpringMVC + MyBatis),SpringBoot 把大量 XML 配置简化成了自动配置和注解,省掉的不只是时间,还有新手最容易搞错的配置项。对商城这种中等规模项目来说,SpringBoot 的性能、生态和学习资料都足够。
MyBatis 相比 JPA 更受国内企业青睐,尤其是 SQL 可控这一点。商城系统里复杂查询很多——多表联查商品、订单状态统计、分组聚合,这些用 MyBatis 写 XML SQL 非常直观,出了问题也可以直接把 SQL 拿到数据库工具里跑一下排查。JPA 在这类场景下反而容易让人一头雾水。
前端选 Vue 而不用 React,原因很简单:Vue 中文文档齐全、上手曲线平缓、模板语法容易理解,而且生态里的 Element UI / Element Plus 有现成的后台管理组件库,堆积木式开发效率极高。
MySQL 就不多解释了,开源、轻量、足够稳,和 SpringBoot 整合资料遍地都是。
这套组合还有一个实际好处:招聘市场上 Java 后端岗位的 JD 里,SpringBoot + MySQL 几乎是必写项,Vue 作为加分项能让你在简历筛选阶段多留一会儿。
1.3 系统整体结构:前端站点 + 管理后台 + 后端服务
我把项目拆成了三个目录,方便分端开发。
mijia-mall ├── frontend-mall # 用户端 Vue 项目 ├── frontend-admin # 管理后台 Vue 项目 └── backend # SpringBoot 后端项目为什么用户端和管理后台的前端要分开成两个项目,而不是合并成一个?因为这两部分的页面风格差异很大,用户端讲究展示效果,后台讲究表格表单效率。如果硬塞在同一个 Vue 项目里,路由配置会变得臃肿,权限控制也更麻烦。两个前端项目共享同一个后端接口,是更清晰的方案。
后端项目按常规分层来:
com.example.mijia ├── controller # 接口层 ├── service # 业务逻辑层 ├── dao/mapper # MyBatis 数据访问层 ├── entity/domain # 实体类 ├── config # 配置类(跨域、拦截器、异常处理) ├── common # 公共返回对象、工具类、常量Controller 层只做参数接收和结果包装,Service 层写业务逻辑,Mapper 层只负责 SQL 操作,这种分层能让你在后期排查问题时快速定位。压测或者答辩时,别人问“订单库存扣减这段逻辑在哪”,你直接翻 Service 就能找到。
2. 核心模块拆解与数据库表设计
2.1 商品、订单、购物车三大模块的需求边界
商城类项目,核心永远是三个字:商品、订单、库存。做得好的系统,这三者之间的关系是环环相扣的。
商品模块是最基础的模块。商品要有分类,分类可以有多级,但在课程设计级别里建议只做一级分类,避免给自己挖坑。商品本身包含名称、图片、价格、原价、库存、销量、上下架状态、详情描述、创建时间。
订单模块是业务最重的模块。一张订单至少需要拆成订单主表和订单明细表。主表存订单号、用户 ID、总金额、支付状态、订单状态、收货地址快照、创建时间;明细表存每个商品的名称快照、单价快照、数量、小计金额。为什么要存“快照”而不是关联商品表去查?因为商品价格会变,用户下单后查看自己的历史订单,看到的价格应该是下单那一刻的价格。这是电商设计的常识,但很多新手项目里没有这个意识,被老师一问就答不上来。
购物车模块相对简单,一张表记录用户 ID、商品 ID、数量、选中状态即可。但要注意,购物车表里如果只存商品 ID,展示购物车列表时需要联查商品表拿到最新价格和图片、库存信息,接口返回的 DTO 需要做一次组装,这个在 MyBatis 里用关联查询处理很顺手。
2.2 后台管理端(abo)的表设计注意事项
很多课程设计里,“abo 管理系统”会带上权限管理功能,所以用户表要给一个 role 字段,比如 0 表示普通用户,1 表示管理员。在后台管理端登录时,通过 role 来判断是否允许访问管理接口。
这里有个关键设计:管理端的接口不能只靠前端路由隐藏,后端必须做权限校验。你不能让一个普通用户知道了接口地址,就通过后台接口把商品价格改了。最简单的做法是自定义一个拦截器,拦截所有/admin/**的请求,校验请求头里是否携带 token 并且 token 对应的用户 role 是否为管理员。这块代码量不大,但能体现你的安全意识,答辩时是加分项。
数据库的整体表设计,我一般是按这个顺序建的:
user:用户表category:商品分类表product:商品表cart_item:购物车表order:订单主表(注意 order 是 MySQL 的保留字,建表时最好改成orders或加反引号,我直接用了orders表名)order_item:订单明细表address:收货地址表
2.3 MyBatis 映射文件里的联表查询怎么写
商城项目的查询场景,十有八九要连表。比如前台商品列表按分类查询,要查出分类名称;购物车列表要查出商品的图片、价格、库存。MyBatis 里我习惯用 XML 文件来管理 SQL,而不是注解。因为 XML 里可以写分页 SQL、动态 SQL、结果映射,比注解更灵活,而且后期 SQL 优化时不用重新编译 Java 代码。
一个代表性的商品列表查询 XML 写法是这样的:
<select id="selectProductWithCategory" resultType="map"> SELECT p.id, p.name, p.price, p.main_image, p.stock, p.sales, p.status, c.name AS category_name FROM product p LEFT JOIN category c ON p.category_id = c.id <where> <if test="categoryId != null"> AND p.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND p.name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="status != null"> AND p.status = #{status} </if> </where> ORDER BY p.create_time DESC </select>注意LIKE CONCAT('%', #{keyword}, '%')的写法,不要直接写LIKE '%${keyword}%',前者会用预编译参数防止 SQL 注入,后者是字符串拼接,存在注入风险。这个细节在面试里经常会被问到,做项目时也要养成习惯。
分页我用的 PageHelper,这一块的细节后面单独说。
3. 后端开发实操:SpringBoot + MyBatis 关键实现复盘
3.1 项目初始化与依赖配置
项目初始化两个办法:一是用 IDEA 的 Spring Initializr 直接生成;二是去 Spring 官网 start.spring.io 下载压缩包。目前最新稳定版 SpringBoot 2.7.x 对课程设计项目已经完全够用,如果不想折腾 JDK 版本兼容,用 2.7.x 搭配 JDK 8 是最稳妥的方案。最新的 SpringBoot 3.x 要求 JDK 17,对很多本地只有一个 JDK8 环境的同学来说,反而不方便。
pom.xml里的关键依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>这里有两个容易踩的坑。第一个坑:MyBatis-spring-boot-starter 的版本不是随便选的,2.3.x 对应 SpringBoot 2.x,如果用 SpringBoot 3.x 必须用 mybatis-spring-boot-starter 3.x,不然启动直接报错。第二个坑:MySQL 8.0 以上的驱动类名是com.mysql.cj.jdbc.Driver,8.0 以下才是com.mysql.jdbc.Driver,自己写配置文件时容易搞混。
3.2 配置文件里的隐藏细节
在application.yml里,我最开始吃过一个亏:数据库连接 URL 没加时区参数,运行时报错Cannot create PoolableConnectionFactory。最终的连接配置是:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mijia_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.mijia.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置强烈建议打开。数据库字段create_time可以自动映射到实体类的createTime,省去一堆resultMap映射代码。很多新手不用这个配置,然后发现查出来的对象 createTime 是 null,折腾半天。
log-impl设置为StdOutImpl,可以在控制台打印执行的 SQL 和参数,极大方便调试。生产环境再关掉,本地开发坚决要开。当你发现查询结果不符合预期,第一件事就是看控制台打出来的 SQL 是不是你想象中那条。
3.3 MyBatis 一级缓存和二级缓存,到底要不要开
热词里许多人搜“mybatis 缓存”。这个项目里我的选择是:默认不开二级缓存,保持 MyBatis 的一级缓存。原因很简单,商城系统里商品库存、价格的实时性要求很高,开二级缓存很容易出现数据不一致。
解释一下区别,面试常问。MyBatis 的一级缓存是 SqlSession 级别的,同一个 SqlSession 中执行两条相同的 SQL,第二次直接走缓存不查数据库。一级缓存默认开启,作用范围极小,基本不会出问题。MyBatis 的二级缓存是 namespace 级别的,同一个 Mapper 的查询结果会被缓存到 JVM 内存里供多个 SqlSession 共享,看似能提升性能,但比如你更新了商品库存,如果缓存没有及时清空,用户看到的库存数可能是旧的,这在商城项目里不可接受。
真正需要缓存的数据,更稳妥的做法是用 Redis 做缓存,并且主动设置过期时间。课程设计项目中不建议引入 Redis,因为会让复杂度上升一个级别。兜底的方案是:如果你用了二级缓存,所有增删改操作对应的 Mapper 里必须配置刷新缓存属性。这个坑我踩过,商品上架后后台改了价格,前台依然显示旧价格,排查了很久才发现是缓存没刷掉。
3.4 分页插件 PageHelper 的实际用法
热词里很多人搜“mybatis 的分页插件的用法”。我这里直接给出在 SpringBoot 项目里的标准操作。导入依赖之后,在你的 Service 层调用:
public PageResult<ProductVO> getProductPage(Integer pageNum, Integer pageSize, Integer categoryId, String keyword) { PageHelper.startPage(pageNum, pageSize); List<ProductVO> list = productMapper.selectProductWithCategory(categoryId, keyword); PageInfo<ProductVO> pageInfo = new PageInfo<>(list); return new PageResult<>(pageInfo.getTotal(), pageInfo.getList()); }PageHelper.startPage的作用是从这行代码开始拦截下一条 SQL,自动拼接LIMIT语句。有一个很重要的注意事项:startPage后面必须紧跟着你要分页的那条 Mapper 查询,如果中间还调用了别的查询,分页就会作用到错误的 SQL 上。比如你先查了商品分类列表,再查商品主列表,startPage 放在查分类之前就会完蛋。
另一个容易忽略的点是,分页插件会把 COUNT 查询自动跑一遍,如果主查询 SQL 本身性能不好,COUNT 查询也会慢。所以,商品列表页的 SQL 尽量别写得过于复杂,索引建到位,保持 COUNT 简单。
3.5 事务、全局异常与上传文件的 XSS 处理
先谈事务。下单这个操作,典型的场景是:生成订单记录、扣减库存、清空购物车。这三步必须在一个事务里,否则中间某一环执行失败,会造成“订单生成了但库存扣了”这种脏数据。
在 SpringBoot 里,加一个@Transactional注解即可。但注意:事务默认只在运行时异常(RuntimeException)时回滚,如果抛出的是受检异常(Exception),不会回滚。所以如果你在 Service 里捕获了所有异常并返回一个错误结果对象,事务是不会触发的。我的习惯是:
@Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderRequest request) { // 1. 校验库存 // 2. 生成订单主表记录 // 3. 生成订单明细 // 4. 扣减库存 // 5. 清空购物车 }热词里还有个“springboot项目全局过滤器处理上传pdf文件时xss攻击”。这是安全相关的点。商城后台的商品介绍如果支持富文本,用户可能上传带有恶意脚本的内容。虽然这个项目的使用场景是课程设计,但你也要知道,全局过滤器可以拦截所有请求,清洗请求参数中的<script>标签等特殊字符。实现方式是实现 Filter 接口,对HttpServletRequestWrapper做参数重写。这块我不建议新手强行深入,知道这个概念并在答辩时能讲出来就赢了。
全局异常处理也是一个临场表现加分项。我用@RestControllerAdvice+@ExceptionHandler统一捕获业务异常、参数校验异常、未知异常,返回统一格式的 JSON。这样即使后端报错,前端拿到的也是可读的错误信息,而不是一堆异常堆栈。
4. 前端开发实操:Vue 两端项目的搭建与核心页面实现
4.1 Vue 环境配置与项目创建
热词里很多“vue安装及环境配置”“vue安装依赖”的搜索,说明新手在环境中卡住的不少。我先说我个人的标准流程。
Node.js 版本是一个大坑。Vue 3 + Vite 需要 Node 16 以上,Vue 2 + Vue CLI 4 适合 Node 14。如果你电脑上之前装过旧版 Node,建议先清干净再装 LTS 版本。确认命令:
node -v npm -v然后创建项目。用户端我用 Vue 3 + Vite:
npm create vite@latest frontend-mall -- --template vue cd frontend-mall npm install npm install vue-router@4 pinia axios element-plus npm run dev管理端相对保守,我用了 Vue 2 + Element UI。理由很奇怪但真实:管理端的 Table 组件 Element UI 用得太熟练了,开发速度快。如果你从头开始,我建议两个端统一用 Vue 3 + Element Plus,不用为了省事而分裂版本。
热词里出现的“electron 主渲染进程 ipc 通信 和vue有关系吗”,这里捎带说一句:Electron 的 IPC 通信属于桌面端开发范畴,和 Vue 本身没有直接关系,Vue 只负责页面渲染层。你搜索这个话题,多半是想把商城做成桌面应用,那是在 Vue 项目外面再套一个 Electron 壳,前后端的代码基本不用改。这个思路可以做成品提升,但不是必要的。
4.2 路由、状态管理与拦截器设计
Vue 项目的核心骨架是路由和状态。用户端我定义了这几条路由:
/首页/product/:id商品详情/cart购物车/order/confirm订单确认/order/list订单列表/login登录/register注册
管理端的路由走后台布局,嵌套路由:
/admin/login管理员登录/admin/dashboard数据面板/admin/product/list商品列表/admin/product/edit添加/编辑商品/admin/order/list订单列表/admin/user/list用户列表
状态管理方面,用户端和管理端都要存登录后的用户信息、token。我用 Pinia(Vue 3)和 Vuex(Vue 2)分别实现,核心是同一套逻辑:登录成功后把 token 存 localStorage,页面刷新后从 localStorage 恢复状态。
最重要的前端拦截器是 Axios 请求拦截和响应拦截。请求拦截器给每个请求头加上Authorization: Bearer <token>,这样后端拦截器才能识别用户身份。响应拦截器统一处理业务状态码,比如 401 时直接跳转登录页。
http.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) http.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { if (error.response && error.response.status === 401) { router.push('/login') } return Promise.reject(error) } )这种集中式拦截处理,好处是后端返回任何错误,前端都能统一弹提示,而不是在每一处接口调用里写一遍重复的 if 判断。
4.3 商品列表、购物车、订单页的核心交互逻辑
商品列表页和购物车页面是商城前端的门面。商品列表我用卡片布局,点击进入详情。商品数据通过 Axios 请求后端/api/product/page获取,分页加载,点击分类切换时重新请求。
购物车页面的关键逻辑是:全选、单选的计算方式。我用一个 computed 来维护“已选商品”的派生状态,计算总价时遍历购物车列表,筛选出勾选项,累加price * quantity。
订单确认页要处理流程切换:购物车勾选商品 -> 点击结算 -> 跳转订单确认页 -> 填写/选择地址 -> 提交订单 -> 模拟支付成功 -> 跳转订单列表。这个流程涉及多个页面之间的状态传递,最简单可靠的方式是路由 query 传参,传购物车项 ID 的集合,订单确认页再根据这些 ID 从后端查最新商品信息。不要用全局 store 保存临时数据,因为刷新页面会导致 store 被清空,体验很差。
商品详情页如果加视频播放,m3u8 的播放方案我上面已经预告了。m3u8 不是一种视频文件格式,而是 HLS 协议的索引文件。Vue 3 里推荐用 hls.js:
npm install hls.js然后封装一个简单的播放组件:
<template> <video ref="videoRef" controls autoplay muted></video> </template> <script setup> import Hls from 'hls.js' import { onMounted, ref } from 'vue' const videoRef = ref(null) const props = defineProps({ src: String }) onMounted(() => { if (Hls.isSupported() && props.src) { const hls = new Hls() hls.loadSource(props.src) hls.attachMedia(videoRef.value) } }) </script>这个组件直接用在商品详情页,传入商品带货视频的 m3u8 地址就能播放。如果本地没有视频资源,可以用 ffmpeg 把 mp4 转成 m3u8 切片,后面部署篇章我也提一下。
4.4 前后端联调:跨域和本地代理问题
开发环境最烦人的就是跨域。后端开发的地址是http://localhost:8080,前端是http://localhost:5173,端口不同就会触发浏览器的同源策略。
两个解决方案。
后端方案,写一个全局 CORS 配置类:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }前端方案,在 Vite 配置里加代理,前端请求/api开头时自动转发到http://localhost:8080:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })两种方案选一种即可,同时我推荐你把后端接口统一都加/api前缀,这样代理就只拦截/api开头的路径,避免误伤静态资源请求。联调阶段,字段名不一致、时间格式不对、返回结构不统一,是最常见的沟通矛盾点,所以接口返回统一用Result<T>封装:
public class Result<T> { private Integer code; private String message; private T data; }所有 Controller 都返回Result.success(data)或Result.error(msg),前端拦截器看到 code 不为 200 就统一处理错误,思路清晰,联调效率高出一大截。
5. MySQL 环境配置与数据库连接问题排查
5.1 MySQL 安装与字符集设置
热词里“mysql安装教程”“linux安装mysql”出现频率极高。Windows 上安装 MySQL 8.0 比较无脑,官网下了 zip 解压后,用管理员命令行执行:
mysqld --initialize-insecure mysqld install net start mysql mysql -u root注意执行mysqld --initialize-insecure之后,root 默认没有密码,首次登录后立刻设置:
ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码';Linux 上安装无非是 apt 或 yum 安装 mysql-server,但注意默认配置文件在/etc/mysql/mysql.conf.d/mysqld.cnf,需要确认bind-address和字符集配置。实际开发和部署时,数据库字符集统一用utf8mb4,排序规则用utf8mb4_unicode_ci。utf8mb4和utf8的区别是前者能存 emoji 和 4 字节字符。米家商城里商品描述如果你让用户填写一些带 emoji 的文字,utf8 会直接报错,utf8mb4 就好好的。
建库语句示范:
CREATE DATABASE mijia_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.2 数据库连接池配置
SpringBoot 的默认连接池是 HikariCP,不需要额外加配置就能工作。但生产环境要注意几个参数。我习惯在 application.yml 里显式配置:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000很多人在高并发下会遇到Connection is not available, request timed out这类报错,通常就是连接池连接被耗尽。商城场景下并发量虽然不高,但连接泄漏的问题要提前规避。最典型的连接泄漏原因是:查询结果没有正常关闭,或者事务包裹里包含了一些耗时的外部调用。排查连接泄漏的办法是在配置里打开 HikariCP 的泄漏检测:
spring: datasource: hikari: leak-detection-threshold: 60000这样一旦有连接持有超过 60 秒未归还,日志里会输出告警。
5.3 常见的 MySQL 连接报错与解决方法
把我在这个项目里真实遇到的几个错误列成表,方便你对照排查:
| 报错信息 | 常见原因 | 解决办法 |
|---|---|---|
Access denied for user 'root'@'localhost' | 密码错误或 user 表权限问题 | 检查密码;确认是用 root 还是新建账号 |
SSL connection error: ... | MySQL 8.0 默认开启 SSL,连接串未处理 | URL 加useSSL=false |
Public Key Retrieval is not allowed | MySQL 8.0 配合非 SSL 连接的认证问题 | URL 加allowPublicKeyRetrieval=true |
com.mysql.cj.exceptions.InvalidConnectionAttributeException | 连接串没有指定 serverTimezone | URL 加serverTimezone=Asia/Shanghai |
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' | Linux 上 mysql 服务未启动或 Socket 路径不对 | systemctl start mysql或确认 socket 路径 |
Unknown database 'mijia_mall' | 建库失败或库名写错 | 确保建库成功并检查库名大小写 |
那个ERROR 2002的报错尤其常见,尤其是在 Linux 服务器上部署的同学。/tmp/mysql.sock这个文件是 MySQL 客户端和服务器进程通信的通道,如果服务没有启动,Socket 文件就不存在。按顺序核查:先看服务状态,再看是否使用了错误的连接方式,最后确认权限。
还有一个诡异的“数据库突然连不上”问题,原因是连接池里的连接到了max-lifetime之后被服务端断开了,而客户端不知道还在用旧连接。用 Hikari 时,把max-lifetime设置得比 MySQL 默认的wait_timeout短一些,就可以规避这个坑。
6. 项目部署、源码解析与常见面试考点
6.1 前后端打包部署的全流程
开发完了之后,总得让项目跑在服务器上才能演示。打包部署这块我吃过不少亏,把完整流程记录一下。
前端打完包是纯静态文件:
npm run build以 Vite 项目为例,构建产物在dist/目录。把这个目录里的文件拷贝到 Nginx 的 html 目录下,再配置 Nginx 将/api请求反向代理给后端 SpringBoot 服务:
server { listen 80; server_name your-domain; location / { root /usr/share/nginx/html; index index.html; 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; } }注意try_files $uri $uri/ /index.html;这一行。Vue 路由默认是 history 模式,如果少了这行,你直接刷新/product/2这个页面会报 404,因为 Nginx 找不到对应的真实文件。加上这行后,所有刷新请求都会重定向到 index.html,再由 Vue 路由解析地址。
后端打包:
mvn clean package -DskipTests生成target/*.jar,然后通过java -jar xxx.jar启动。如果服务器内存不大,可以加 JVM 参数限制内存:
java -jar mijia-mall.jar --spring.profiles.active=prod部署时记得把application.yml里的数据库连接、上传文件保存路径等配置改成服务器环境对应的值。如果你图省事,也可以用 Docker 把前后端分别容器化,但这就会使项目复杂度上来了。课程设计要求不高的情况下,Nginx + Java 进程这种传统方案已经足够稳定。
6.2 SpringBoot jar 反编译:拿到别人的源码怎么分析
热搜词里有“怎么将springboot jar反编译成项目”,这个场景很实际:很多同学拿到的是别人打包好的 jar,想把它还原成能看懂、能修改的源码项目。
推荐的工具是 IDEA 自带的 Java Decompiler(Fernflower)或者 CFR。IDEA 打开一个 jar 文件时,通常能看到反编译出来的 class 源码,但这种方法适合单文件查看。想整体还原,我一般用cfr:
java -jar cfr.jar mijia-mall.jar --outputdir ./src反编译出来的目录结构大体还原了包结构,Controller、Service、Mapper 这些包都还在。但要注意,XML 文件如果被打进了 BOOT-INF/classes,也会一并解出来;实体类里的注释、一些常量值(比如数据库密码)都会被还原成明文。这提醒了两件事:第一,你自己打 jar 发布前,确保数据库密码等敏感信息不要硬编码在配置里,至少要用环境变量替换;第二,分析别人项目时,先看application.yml或者bootstrap.yml,再看 Controller,基本就能搞清楚项目有哪些接口,然后顺藤摸瓜看 Service 实现。
从 jar 逆向成“可运行的 IDEA 项目”,还需要自己补 pom.xml 或 build.gradle,因为反编译出来的只是 class 和资源,构建文件不会自动生成。你需要参考项目里用的依赖,重新搭一个 Maven 工程,然后把反编译代码拷进去。这个过程很容易积攒依赖冲突的问题,多数情况是重新手写一遍比逆向改造更省力。
6.3 SpringBoot、MyBatis 面试高频问题
这个项目做完之后,如果你要去面试 Java 开发岗,有几个知识点是大概率会被问到的。热词里已经有“springboot面试题”“mybatis面试题”了,我直接把和这个项目相关的常见题目和回答思路放这里:
一、SpringBoot 自动配置原理怎么理解?
回答思路:@SpringBootApplication包含@EnableAutoConfiguration,SpringBoot 会去读取spring.factories中配置的自动配置类,按条件注解@ConditionalOnClass、@ConditionalOnMissingBean等判断是否生效。比如DataSourceAutoConfiguration会在 classpath 里有DataSource相关类时自动创建连接池配置。面试官一般还想听到你举一个实际例子,这时你就说“项目里 SpringBoot 自动帮我配置了默认数据源 HikariCP”,这就把事情讲活了。
二、MyBatis 里 #{} 和 ${} 的区别?
这个属于送分题。#{}走预编译,能防止 SQL 注入;${}是字符串拼接,可能被注入。但${}在分页、排序字段动态替换时有需求。在商城项目里,排序字段如果可配置,建议用白名单校验后再拼入 SQL,绝对不要直接由用户输入。
三、SpringBoot 的 Controller、Service、Mapper 请求流转过程?
这是概念题,也是实测题。请求从浏览器发出,Nginx/前端代理转发到 SpringBoot,经过 DispatcherServlet 分发到对应 Controller,Controller 调 Service,Service 调 Mapper,Mapper 执行 SQL 返回结果,反向逐层封装,最终返回 JSON 给前端。这个流程能讲清楚,基本就证明你是真的写过项目的人。
四、如何解决后端查询的性能瓶颈?
这个在商城项目里可以结合商品列表分页讲。先看 SQL 执行计划有没有走索引,商品表的 category_id、status 字段要有索引;再看有没有 N+1 查询问题,比如循环查数据库的问题;实在难搞的 SQL 还可以用缓存解决,但在本项目里索引和分页优化已经足够。
五、Vue 项目的权限控制如何实现?
前端路由守卫 + 后端拦截器双重控制。前端 router.beforeEach 判断是否存在 token 和角色信息,没有则跳登录页;后端给需要权限的接口配置拦截器校验 token 和角色。
6.4 从源码到apk:vscode+vue 做手机软件是怎么回事
热搜词里还有一条“vscode +vue 怎么制作手机软件”,说实话看到这个问题,我估计提问者是把“Vue 写的网页”和“手机 App”搞混了。Vue 本身做的是 Web 页面,要变成手机 App 一般有三条路:
第一条路:把 Vue 项目打包成移动端 H5 页面,部署到服务器上,用户通过浏览器访问,这是最简单的移动端适配方案。
第二条路:用 uni-app 重写页面,它是 Vue 语法,可以编译到 iOS/Android 原生 App。
第三条路:用 Capacitor 或 Cordova 给 Vue 项目套壳,生成 APK。相比 Electron 桌面端,移动端套壳本质是一个 WebView 容器,加载你的 H5 页面。
如果这个米家商城项目想“变成手机软件”,最省事的方式就是第三条路,用 Capacitor:
npm install @capacitor/core npm install @capacitor/cli npx cap init npx cap add android npm run build npx cap copy android然后用 Android Studio 打开生成的android目录,直接就能打包 APK。后端接口地址要写你的公网地址,或者用内网穿透在局域网联调。但对于课程设计来说,我不建议在“变成 App”上花太多精力,容易陷进原生构建工具链的大坑。
最后分享一点实操体会
这个项目前前后后改了几版,我个人最大的感受是:商城系统看起来千篇一律,做起来细节才是最耗时的。比如“下单扣库存”时要考虑商品下架/库存不足的处理,“购物车结算”要考虑商品价格变动,“订单列表”要考虑状态筛选和分页,每一块都是真实电商业务里的核心问题,哪怕是一个课程级别的系统,也值得用严谨的态度去实现。
另外建议你在开发时给自己加一个小目标:不要只做到“能跑”,要能做到“能给别人讲懂”。把每一张表的字段设计理由、每个接口的事务边界、每个前端页面交互的数据流向都在心里过一遍,这类项目才真正变成了你的项目。如果你基于这篇文章搭好了环境、跑通了页面,后面再遇到“ES 检索”“Redis 缓存”“支付回调”这些进阶概念时,你至少已经有了稳固的落地场景去承载它们。