做这套基于 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 的在线家具商城系统源码时,我最大的感触是:这套技术栈的组合几乎可以算作 Java Web 全栈开发的“标准答案”之一。它不像那些动辄上云的分布式微服务项目那么重,但足够把前后端分离、接口设计、数据库建模、权限控制、订单流转这些核心技能完整地走一遍。无论你是准备毕业设计、课程设计,还是想找一套能真正跑起来、能讲清楚原理的练手项目,这套家具商城都非常合适。而且它自带文档,这对后续二次开发和答辩演示来说,能省下大量整理资料的时间。
网上类似的“XX商城系统”源码很多,但多数存在几个问题:老技术栈像 JSP + Servlet,代码结构混乱;或者数据库用了老版本,驱动配置、SQL 方言跟 MySQL8.0 对不上;还有一种情况是前后端不分离,写出来的东西跟企业实际开发脱节。这套家具商城用的 SpringBoot2 分离后端服务、Vue3 管理前端页面、MyBatis-Plus 操作数据库、MySQL8.0 存储数据,链路完整,技术上也是目前中小型项目里用得最多的一套组合。接下来我就按实际开发流程,把整个项目的设计思路、核心实现、部署过程和踩坑记录都拆开讲一遍。
1. 项目整体设计与技术选型思路
1.1 为什么是 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0
先说后端。SpringBoot2 到现在依然有庞大的用户群体,starter 机制让配置变得非常轻,内嵌 Tomcat 让部署变成了“打 jar 包 + 跑命令”两件事。可能有人会问:现在 SpringBoot3 都出来了,为什么还要用 SpringBoot2?原因很简单,很多学校教材、企业存量项目、云服务商的镜像模板都还停留在 SpringBoot2 时代,而且 SpringBoot2 对 JDK8 的兼容性最好,团队协作时不会因为 JDK 版本问题引发一堆莫名其妙的报错。再加上大部分毕业设计和课程设计在环境上更倾向 JDK8 + SpringBoot2,这个组合最容易“一把过”。
Vue3 则是前端的主流方向。相比 Vue2,Vue3 的 Composition API 让组件逻辑的组织方式更灵活,类似功能的代码可以聚合在一起,不用像 Options API 那样分散在 data、methods、computed 里。Vite 开发服务器比 Webpack 快很多,尤其在修改代码后热更新的响应速度,几乎是秒级。而且如果你以后要学 uni-app 或小程序开发,Vue3 的语法习惯也能直接迁移过去,学习成本是复用的。
MyBatis-Plus 的价值在于,它把单表 CRUD 的样板代码砍掉了很大一部分。单表操作不需要写 XML 映射,只需要让 Mapper 继承 BaseMapper,selectById、selectPage、insert、updateById 这些方法就都有了。它还支持逻辑删除、自动填充、乐观锁插件,这些功能在商城类项目里非常实用。
MySQL8.0 带来的提升,一个是 utf8mb4 字符集成为默认,存储 emoji 表情和生僻字不会再报错;另一个是窗口函数和公共表表达式(CTE),虽然商城项目用的不多,但复杂统计的时候会舒服很多。同时 MySQL8.0 的默认身份认证插件是 caching_sha2_password,这个细节会让不少人在连接数据库时踩坑,我后面会专门讲。
1.2 家具商城的功能模块划分
从角色角度来看,整个系统分成两大块:前台用户端和后台管理端。
前台用户端包括注册登录、浏览商品、按分类筛选、搜索商品、查看商品详情、加入购物车、修改购物车数量、结算下单、订单支付(模拟流程)、查看订单列表、取消订单、确认收货、个人资料修改。这个流程基本上是所有电商项目的标准闭环,把这一套走通,就掌握了电商系统最核心的业务链路。
后台管理端包括商品管理(新增、编辑、上下架、库存调整)、分类管理、订单管理(查看、发货)、用户管理、轮播图管理、数据概览。后台管理的核心价值是让你理解“管理类页面”的开发套路:表格展示 + 弹窗表单 + 删除确认 + 分页刷新。这个套路在后面的企业项目中会反复出现。
这里有一个设计上的细节:家具属于大件商品,所以它的商品描述通常会包含材质、尺寸、颜色等属性,和卖数码产品、卖衣服的电商稍有区别。在设计数据库字段时,我会把通用字段放在主表里,比如商品名称、价格、库存、封面图、状态等。至于材质、大小这些属性,根据项目复杂度决定是加字段还是加一个商品属性表。如果只是毕设级别,直接在商品表加 summary 文本字段存储描述信息就够了。
1.3 数据库表设计与工程目录结构
这套系统的核心表主要有用户表、分类表、商品表、购物车表、订单表、订单明细表、轮播图表,一共七张。设计时需要注意几个关联点:购物车表和商品表是多对一关系,购物车表里只保存商品 ID 和数量,不冗余商品名称和价格,查询时再关联商品表。订单表和订单明细表是一对多关系,订单表保存收货人信息、订单总金额和状态,明细表保存下单瞬间的商品快照,比如商品名称、商品封面、单价、数量。为什么明细表要保存快照?因为商品的名称和价格后续可能会调整,但已经生成的订单必须保持下单当时的信息,否则用户看到的历史订单会“变”,这是电商系统里很重要的一个设计原则。
工程目录方面,后端采用标准的 SpringBoot 分层结构,controller、service、mapper、entity、config、common 这些包一层层分清楚。前端按 Vite + Vue3 的默认目录结构组织,views 下面再按用户端和管理端分目录。前后端分离项目的目录规划影响的是后期维护效率,一个好的团队协作习惯就是目录即文档,看一眼目录结构就能知道某个功能代码大概在哪里。
2. 后端核心模块设计与实现解析
2.1 启动类与统一返回结构
后端启动类没有太多可以说的,标准写法是 @SpringBootApplication + main 方法。重点是启动类所在的包位置一定要放在所有子包的上级,SpringBoot 默认会扫描启动类所在包及其子包下的组件。很多新手把启动类放在某个子包里面,结果 Controller 扫描不到,接口全 404,排查半天才发现是包位置不对。
统一返回结构非常关键。我习惯用一个 Result 类包装所有接口返回值,里面包含 code、message、data 三个字段。code 为 200 表示成功,code 为 500 表示系统错误,code 为 401 表示未登录或 token 失效。这样前后端联调时,前端可以统一在 axios 响应拦截器里处理业务异常,而不用每个请求单独判断。如果项目中每个 Controller 返回的数据结构都不一致,前端联调的工作量会成倍增加。
再加一个全局异常处理器,用 @RestControllerAdvice 注解。业务方法里抛出业务异常后,由全局处理器统一捕获并转换成 Result 对象返回。比如用户登录时密码错误,我抛一个 BusinessException("用户名或密码错误"),处理器会把它转成 code=500、message=业务提示内容的结构返回给前端。这种设计的核心逻辑是,业务异常由全局处理器兜底,Controller 代码不需要每个地方都写 try-catch,代码会清爽很多。
2.2 MyBatis-Plus 的配置与使用细节
MyBatis-Plus 的依赖引入后,需要在 application.yml 里配置数据库连接、逻辑删除字段和日志输出。下面是一份我在项目中使用的配置样例:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/furniture_mall?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有几个细节值得说。连接串里的 serverTimezone 如果不设置为 Asia/Shanghai,MyBatis-Plus 和 MySQL8.0 之间可能出现时区相差 8 小时的问题,查出来的时间数据永远是昨天,这个坑非常经典。驱动类用的是 com.mysql.cj.jdbc.Driver,这是 MySQL8.0 驱动的新名称,而旧版的 com.mysql.jdbc.Driver 虽然在兼容模式下也能用,但控制台会输出 Deprecation 警告,最好直接用新名称。
逻辑删除配置的意思是,在数据库表中加一个 deleted 字段,删除操作不再执行 DELETE 语句,而是执行 UPDATE 语句把 deleted 置为 1。这种方式在商城项目中很有意义,尤其是在统计销量、回溯数据的时候,硬删除会把历史数据彻底抹掉,而逻辑删除保留了痕迹。但逻辑删除也带来一个新的坑:默认情况下,MyBatis-Plus 的所有查询都会自动带上 deleted=0 这个条件,如果你想查询已经删除的数据,反而需要自己写 SQL 或者用自定义方法。
分页插件也需要单独配置一个 MybatisPlusInterceptor 的 Bean 并加入 PaginationInnerInterceptor,不然调用 selectPage 没有效果,返回的总记录数永远是 0。这个配置网上说得多,但我实际见过不少项目漏掉这一步,分页怎么调都不对。插件的配置其实就是注册一个 Bean,几行代码搞定。
2.3 用户登录与 JWT 权限控制
用户模块的核心是注册和登录。注册时密码不能明文存储,我用的是 Spring Security Crypto 模块里的 BCryptPasswordEncoder,每次加密的结果是随机的,即使两个用户密码相同,密文也不同,安全性更高。登录校验通过后,签发一个 JWT token 给前端,前端后续每次请求都在请求头里带上 Authorization: Bearer token。
JWT 的结构包含 Header、Payload、Signature 三部分,Payload 里可以存放用户 ID 和用户名,比如:
Map<String, Object> claims = new HashMap<>(); claims.put("userId", user.getId()); claims.put("username", user.getUsername()); String token = Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 7)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact();我设置的过期时间是 7 天,这个时长对毕设项目来说比较合适。如果时间太长,安全性下降;太短的话,用户频繁重登,体验不好。
后端的权限控制,我用一个拦截器实现。拦截器里读取请求头里的 token,解析出用户 ID,然后放入 ThreadLocal 或者 HttpServletRequest 的 attribute 里,业务层可以直接获取当前登录用户的信息。需要注意,拦截器要放行登录接口、注册接口、商品查询接口,其他的接口都要校验 token。如果 token 缺失或过期,返回 401 状态码,前端收到 401 后自动跳转到登录页。这个流程是前后端分离项目中最常见的认证方案,理解了原理,面试时能讲得头头是道。
2.4 商品、购物车与订单的核心业务逻辑
商品模块的重点是商品列表分页查询和条件筛选。我用 MyBatis-Plus 的 LambdaQueryWrapper 构造查询条件,根据前端传入的分类 ID、关键字、价格区间来动态组装条件。LambdaQueryWrapper 的好处是编译期能检查字段名是否正确,避免字符串写错导致的运行时异常。排序条件用 Page 对象的 orderBy 参数实现,比如按销量排序、按价格排序、按新品排序,这些都是商城列表页的常规需求。
购物车模块在前后端分离项目里的实现方式有两种:一种是把购物车数据存在后端数据库里,另一种是存在本地 localStorage。这个项目用的是后端存储方式,好处是用户换设备后购物车数据还在,而且和订单系统联动更方便。购物车接口包含加入购物车、修改数量、删除商品、查询列表几个核心操作。每次加入购物车时,先查一下该用户是否已经添加过这个商品,如果已经存在,直接增加数量,而不是新增一条记录,避免购物车出现两行相同商品的数据。
订单模块是整个项目里业务逻辑最密集的部分。订单流程是:用户从购物车勾选商品,选择或填写收货地址,点击提交订单,系统根据购物车里的商品计算总金额、生成订单号、扣减库存、保存订单和订单明细。库存扣减这里要注意并发问题,最好使用乐观锁机制。MyBatis-Plus 提供了 @Version 注解实现乐观锁,商品表中加一个 version 字段,更新库存时通过库存数大于等于购买数作为条件,防止超卖。如果更新失败,说明库存已经被其他请求扣掉了,就提示用户“商品库存不足”或“请重试”。
订单生成后,状态为待支付。因为商城主流程需要模拟完整闭环,所以支付环节我没有真的对接第三方支付,而是做成了“模拟支付”按钮,点击后直接把订单状态改成待发货。这种处理方式在毕设项目中完全够用,只要把状态机的流转做清楚就行。订单状态我设计为:待支付、待发货、待收货、已完成、已取消,状态只能按这个顺序流转,后端在更新状态时要做校验,不能允许用户把已取消的订单直接改成已完成。
3. 前端 Vue3 工程化实践与联调
3.1 Vite 工程搭建与核心依赖
前端部分我用的构建工具是 Vite,官方脚手架命令是 npm create vue@latest,可以创建 Vue3 + Vite 的标准工程。项目需要的核心依赖包括 Vue Router、Pinia、Axios、Element Plus。Vue Router 负责路由跳转,Pinia 负责全局状态管理,比如记录登录用户信息和购物车数量,Axios 负责 HTTP 请求,Element Plus 提供现成的 UI 组件,比如表格、表单、弹窗、消息提示。
Element Plus 按需引入的方案有两种,一种是使用 unplugin-vue-components 和 unplugin-auto-import 这两个插件实现自动导入,另一种是在 main.js 里全量引入。对于项目体量来说,全量引入也不是不行,但打包体积会变大。我建议用按需自动导入的插件,配置好之后写代码时不需要 import,组件直接用就行,比如在 template 里写了 el-button 和 el-table,插件会自动把相关样式和组件打包进来。
路由的规划分为两级。一级路由是用户端和管理端两个布局 Layout,二级路由是具体的业务页面。用户端包括首页、商品列表、商品详情、购物车、订单结算、个人中心;管理端包括商品管理、分类管理、订单管理、用户管理、轮播图管理。路由配置和组件文件的映射关系要一一对应,组件路径写错是控制台报找不到模块的高频原因。
3.2 首页、商品列表与详情页的组件拆分
首页布置了轮播图、分类导航、热销商品推荐几个区域。轮播图我用 Element Plus 的 el-carousel 组件,数据来自后端轮播图表。分类导航根据商品分类动态生成,点击某个分类就跳转到商品列表页并带上分类 ID 参数。热销商品推荐调用后端接口,按销量倒序查询前 8 条数据,渲染成卡片列表,点击卡片进入商品详情页。
商品列表页的功能点比较集中:分类筛选、关键字搜索、价格排序、销量排序、分页加载。列表页和搜索结果的页面前端逻辑基本一致,区别只是请求参数不同,一个带 categoryId,一个带 keyword。我单独封装了一个 ProductQueryParams 对象来管理这些查询参数,切换排序方式或者翻页的时候,只需要修改参数对象里的字段,然后重新调用查询接口。Vue3 的组合式函数(useProducts)可以把这个过程封装起来,让组件代码更清晰。
商品详情页展示商品的基本信息、图片、价格、库存、描述,以及数量选择器和“加入购物车”“立即购买”按钮。sku 这种复杂的多规格逻辑在这个项目里没有做,商品直接是单规格模型,对毕设来说足够。详情页需要关心的是,加入购物车时要检查是否登录,没有登录就弹窗提示并跳转登录页,登录后调购物车接口,成功后更新 Pinia 里的购物车数量。
3.3 Axios 封装与跨域代理
前端所有请求都通过 axios 实例发起,我在项目里单独建了一个 request.js 文件,核心思路是把 baseURL、请求拦截器、响应拦截器统一封装。请求拦截器里从 localStorage 里取 token,有 token 就加到请求头里。响应拦截器里判断返回数据,如果 code 是 200,直接返回 data;如果 code 是 401,清空本地登录状态并跳转登录页;其他错误码统一弹 ElMessage 提示。
const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res.data; } if (res.code === 401) { localStorage.removeItem('token'); router.push('/login'); } ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); }, error => { ElMessage.error('网络异常'); return Promise.reject(error); } );开发环境解决跨域,我在 vite.config.js 里配置了 server.proxy 代理,把 /api 前缀的请求转发到后端的 8080 端口,代理配置大概是这样的:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }前后端分离联调阶段,跨域是最折磨人的问题之一。如果后端没有配置 CORS,前端请求会直接报 Cross-Origin Request Blocked。用 Vite 代理可以同时解决开发和部署的路径适配问题,前端的接口都写成 /api 开头,部署时再用 Nginx 把 /api 反向代理到后端服务,同一套代码不需要改任何接口路径。
3.4 Vue Router 路由守卫与权限控制
前端路由守卫的作用是控制页面访问权限。用户端的大部分页面是公开的,但购物车、订单结算、个人中心这些页面必须登录后才能访问。管理端的所有页面必须要求角色为管理员,否则不能进入。我在 router.beforeEach 全局前置守卫里做校验,逻辑比较直观:判断目标路由的 meta 是否需要登录,如果需要且本地没有 token,就跳转到登录页并带上重定向参数;如果目标是管理端页面,再校验本地存储里的角色信息。
Pinia 在项目里管理两个核心状态:用户信息和购物车数量。用户信息在登录成功后从后端返回并存储,同时写入 localStorage,刷新页面后从 localStorage 恢复。购物车数量在每次加入购物车、删除购物车商品、下单成功后调用一次查询接口刷新,这样首页导航栏上的购物车角标始终能保持最新数据。
这里要提一下 Vue2 转 Vue3 的差异。Vue3 里废除了 Vue2 的 Options API 写法中的 this.$store、this.$route,一切暴露出来的属性都需要在 setup 函数里显式获取。比如 Vue2 里直接用 this.$router.push,Vue3 里需要 const router = useRouter(),然后 router.push。初次迁移时容易搞混,但写几天就适应了。
4. 部署环境准备与自动化发布
4.1 MySQL8.0 安装与初始化配置
MySQL8.0 的安装,本地电脑上我用的是压缩包方式,解压后配置一个 my.ini 文件,然后执行 mysqld --initialize-insecure 初始化,再执行 net start mysql 启动服务。用压缩包方式的好处是免安装、干净、卸载也容易,环境变量配置好之后命令行就能用。Linux 服务器上则推荐用 Docker 安装,一条命令就能拉起 MySQL8.0 容器,避免手动处理各种依赖问题。
docker run --name mysql8 \ -e MYSQL_ROOT_PASSWORD=123456 \ -p 3306:3306 \ -v /data/mysql/conf:/etc/mysql/conf.d \ -v /data/mysql/data:/var/lib/mysql \ -d mysql:8.0无论是本地还是 Docker,MySQL8.0 安装完成后都要注意 root 用户的认证方式。8.0 默认使用 caching_sha2_password,旧版 Navicat 和部分第三方工具可能连不上,提示 Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法是执行下面的 SQL,把 root 用户的插件改成 mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;数据库建好后,把项目附带的后端 SQL 文件导入。推荐用 Navicat 导入,右键数据库,选择“运行 SQL 文件”,导完后检查一下几张核心表的数据量,确认表结构、初始数据都存在。
4.2 SpringBoot 后端打包与 jar 部署
后端打包之前,先把 application.yml 里数据库地址、用户名、密码改成服务器环境的值。然后执行 maven 打包命令:
mvn clean package -DskipTests成功后会生成 target 目录下的 jar 包,通过上传工具传到服务器,用 java -jar 命令启动:
nohup java -jar furniture-mall-0.0.1-SNAPSHOT.jar > app.log 2>&1 &加 nohup 和 & 是让进程在后台持续运行,日志输出到 app.log 文件,服务器断开 SSH 连接也不会影响程序运行。如果想查看启动日志,用 tail -f app.log 实时跟踪。这里有个细节:如果 Linux 服务器上的 JDK 版本和本地开发不一致,启动时可能会报 UnsupportedClassVersionError,所以保持两端 JDK 版本一致是基本前提。
4.3 用 Nginx 托管前端静态资源与反向代理
前端构建之前,需要把 axios baseURL 或者代理配置确认好。构建命令是 npm run build,产物是 dist 目录。把 dist 目录上传到服务器,然后配置 Nginx。
server { listen 80; server_name your-domain.com; location / { root /data/www/furniture-mall; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }前端路由使用 history 模式时,刷新页面会出现 404,所以必须配置 try_files 把所有请求都指向 index.html,由前端路由接管页面展示。后端接口通过 /api 前缀统一转发,这样服务器只需要开放 80 端口,后端的 8080 端口不直接暴露在外面,安全性也会好一些。
4.4 Jenkins 自动部署流程配置
如果后续提交代码比较频繁,手动打包上传的方式就很浪费时间。我配置过基于 Jenkins 的自动部署流水线,流程大概是:开发完代码 push 到 Git 仓库,Jenkins 监听代码仓库变化后触发流水线任务,自动拉取代码、执行 Maven 打包、通过 SSH 把 jar 包传输到服务器,然后执行重启脚本。重启脚本核心内容是先杀掉旧进程再启动新进程:
kill -9 $(pgrep -f furniture-mall.jar) nohup java -jar /data/app/furniture-mall.jar > /data/app/app.log 2>&1 &Jenkins 流水线里用到了 Git 参数、SSH Agent、Shell 脚本几个插件。这个流程配置一次,后面每次部署就完全自动了,特别适合频繁改 bug 的阶段。如果项目规模不大,也可以直接用阿里云效之类的平台做 Web 自动化部署,原理一样,都是取代码、构建、上传、重启四步。
5. 常见问题排查与经验心得
5.1 高频报错速查表
我把项目运行过程中遇到的高频问题整理成一张表,方便快速定位:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 启动报 Access denied for user | 数据库账号密码配置错误 | 检查 application.yml 中数据库连接信息 |
| 启动报 Public Key Retrieval is not allowed | 连接串缺少 allowPublicKeyRetrieval | 在 JDBC URL 后面加 &allowPublicKeyRetrieval=true |
| 查询中文乱码 | 数据库字符集或连接字符集不对 | 数据库、表、连接串都统一用 utf8mb4 |
| MyBatis-Plus 分页不生效 | 未注册分页插件 | 配置 MybatisPlusInterceptor 并添加 PaginationInnerInterceptor |
| 接口 404 或扫描不到 | 启动类包位置不在根包 | 将启动类放在所有子包的上级目录 |
| Vue 页面白屏 | 路由 history 模式未处理兜底 | Nginx 配置 try_files 指向 index.html |
| 跨域请求被拦截 | 前后端域名不一致 | 开发环境用 Vite 代理,生产环境用 Nginx 反向代理 |
| 逻辑删除的数据查不到 | 查询条件自动过滤了 deleted=1 | 需要查询已删数据时重写 SQL |
5.2 MyBatis-Plus 逻辑删除和查询 deleted 的坑
MyBatis-Plus 的逻辑删除确实方便,但它有个隐藏问题很多人不知道:如果在一对多关联查询时,子表使用了逻辑删除,而自己写的自定义 SQL 没有带上 deleted 条件,可能会出现数据混乱。比如查询订单明细时,如果在 XML 里手写 SELECT 语句,MyBatis-Plus 是不会自动帮你拼接 deleted=0 的,必须自己在 SQL 里手动加。这个坑容易出现在使用自定义 SQL 做多表关联查询时,排查起来也比较隐蔽,因为单表操作一切正常,就到多表时数据不对。
还有一个场景:需要统计所有商品总数,包括已删除的商品时,默认查询会忽略 deleted=1 的数据。这种需求虽然不多,但如果遇到了,可以有两种处理方式。第一种是忽略逻辑删除,使用 MyBatis-Plus 的 @InterceptorIgnore 注解标注方法;第二种是直接用原生 SQL 写死查询,绕过实体类的通用逻辑。两种方式看场景选择,我更倾向直接写原生 SQL,语义更明确。
5.3 Vue3 开发中实操细节提醒
Vue3 项目实际开发中,有几个细节值得注意。第一是响应式数据的声明方式,基础类型用 ref,引用类型用 reactive,但 reactive 不能直接重新赋值整个对象,否则会丢失响应性。第二是 Vue3 的 v-model 在自定义组件上的用法和 Vue2 有差异,Vue3 里 v-model 默认是 update:modelValue 事件,如果从 Vue2 迁移代码,自定义组件的 model 属性写法要改。第三是 watchEffect 和 watch 的区别,watchEffect 会自动追踪依赖,而 watch 需要明确指定要监听的源数据源。
前端还有一个细节容易漏,就是给商品图片加上加载占位图。如果后端返回的图片地址加载慢,页面布局会被撑乱。我用 Element Plus 的 el-image 组件时会给插槽设置一个骨架屏占位,用户体验会好很多。另外,列表页滚动回顶部的问题也要注意,切换分页或跳转详情页后,页面停留在上一个滚动位置是很怪异的体验。
5.4 我踩过的一些坑和实际建议
这次开发中我踩过的比较值得记录的坑,一个是 MySQL8.0 时区问题。最开始连接串里没加 serverTimezone=Asia/Shanghai,导致插入数据库的时间比本地时间晚 8 小时。我通过日志和数据库直观对比才发现问题,后来在 application.yml 里同时设置了连接串时区和 Jackson 时区,两个都保持一致才彻底解决。这个问题的隐蔽性在于它不报错,只是数据不正确,靠肉眼很难发现。
另一个坑是 Vue3 项目打包后登录页无法访问。排查后发现是因为 router 的 createWebHistory 需要后端配合,我直接改成 createWebHashHistory 后就正常了,虽然 URL 里多一个 #,但对小型项目来说更省事。如果后期想用 history 模式,就配好 Nginx 的 try_files,没有技术难度,只是配置问题。
还有一个通用建议:数据库表字段一定要加 create_time 和 update_time,并且用 MyBatis-Plus 的自动填充功能维护这两个字段。堪称良心功能。配置一个 MetaObjectHandler 实现类,插入时自动填充创建时间和更新时间,更新时自动填充更新时间,业务代码里根本不用去管时间问题。后期想排查数据变更、排查 bug、做数据分析的时候,这两个字段能帮你定位问题。
根据我个人的实际经验,这类全栈商城项目最大的学习价值不在于功能有多复杂,而在于让你完整经历一次“数据库建模 -> 后端接口 -> 前端页面 -> 联调 -> 部署上线”的闭环。很多人在学校练习时都是零散地写 demo,这个项目能让各个环节真正串联起来。最后再分享一个小技巧:项目写完后,把启动步骤和部署过程整理成一个简短的 README,别觉得这是浪费时间,过两个月你再回来看代码时,这份文档就是你找回手感最快的路径。