SpringBoot2+Vue3+MyBatis-Plus在线家具商城系统
做这套基于 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 则是前端的主流方向。相比 Vue2Vue3 的 Composition API 让组件逻辑的组织方式更灵活类似功能的代码可以聚合在一起不用像 Options API 那样分散在 data、methods、computed 里。Vite 开发服务器比 Webpack 快很多尤其在修改代码后热更新的响应速度几乎是秒级。而且如果你以后要学 uni-app 或小程序开发Vue3 的语法习惯也能直接迁移过去学习成本是复用的。MyBatis-Plus 的价值在于它把单表 CRUD 的样板代码砍掉了很大一部分。单表操作不需要写 XML 映射只需要让 Mapper 继承 BaseMapperselectById、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(用户名或密码错误)处理器会把它转成 code500、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?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 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/ShanghaiMyBatis-Plus 和 MySQL8.0 之间可能出现时区相差 8 小时的问题查出来的时间数据永远是昨天这个坑非常经典。驱动类用的是 com.mysql.cj.jdbc.Driver这是 MySQL8.0 驱动的新名称而旧版的 com.mysql.jdbc.Driver 虽然在兼容模式下也能用但控制台会输出 Deprecation 警告最好直接用新名称。逻辑删除配置的意思是在数据库表中加一个 deleted 字段删除操作不再执行 DELETE 语句而是执行 UPDATE 语句把 deleted 置为 1。这种方式在商城项目中很有意义尤其是在统计销量、回溯数据的时候硬删除会把历史数据彻底抹掉而逻辑删除保留了痕迹。但逻辑删除也带来一个新的坑默认情况下MyBatis-Plus 的所有查询都会自动带上 deleted0 这个条件如果你想查询已经删除的数据反而需要自己写 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 和用户名比如MapString, 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 vuelatest可以创建 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.pushVue3 里需要 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_PASSWORD123456 \ -p 3306:3306 \ -v /data/mysql/conf:/etc/mysql/conf.d \ -v /data/mysql/data:/var/lib/mysql \ -d mysql:8.0无论是本地还是 DockerMySQL8.0 安装完成后都要注意 root 用户的认证方式。8.0 默认使用 caching_sha2_password旧版 Navicat 和部分第三方工具可能连不上提示 Authentication plugin caching_sha2_password cannot be loaded。解决办法是执行下面的 SQL把 root 用户的插件改成 mysql_native_passwordALTER USER rootlocalhost 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 21 加 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 21 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 后面加 allowPublicKeyRetrievaltrue查询中文乱码数据库字符集或连接字符集不对数据库、表、连接串都统一用 utf8mb4MyBatis-Plus 分页不生效未注册分页插件配置 MybatisPlusInterceptor 并添加 PaginationInnerInterceptor接口 404 或扫描不到启动类包位置不在根包将启动类放在所有子包的上级目录Vue 页面白屏路由 history 模式未处理兜底Nginx 配置 try_files 指向 index.html跨域请求被拦截前后端域名不一致开发环境用 Vite 代理生产环境用 Nginx 反向代理逻辑删除的数据查不到查询条件自动过滤了 deleted1需要查询已删数据时重写 SQL5.2 MyBatis-Plus 逻辑删除和查询 deleted 的坑MyBatis-Plus 的逻辑删除确实方便但它有个隐藏问题很多人不知道如果在一对多关联查询时子表使用了逻辑删除而自己写的自定义 SQL 没有带上 deleted 条件可能会出现数据混乱。比如查询订单明细时如果在 XML 里手写 SELECT 语句MyBatis-Plus 是不会自动帮你拼接 deleted0 的必须自己在 SQL 里手动加。这个坑容易出现在使用自定义 SQL 做多表关联查询时排查起来也比较隐蔽因为单表操作一切正常就到多表时数据不对。还有一个场景需要统计所有商品总数包括已删除的商品时默认查询会忽略 deleted1 的数据。这种需求虽然不多但如果遇到了可以有两种处理方式。第一种是忽略逻辑删除使用 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 时区问题。最开始连接串里没加 serverTimezoneAsia/Shanghai导致插入数据库的时间比本地时间晚 8 小时。我通过日志和数据库直观对比才发现问题后来在 application.yml 里同时设置了连接串时区和 Jackson 时区两个都保持一致才彻底解决。这个问题的隐蔽性在于它不报错只是数据不正确靠肉眼很难发现。另一个坑是 Vue3 项目打包后登录页无法访问。排查后发现是因为 router 的 createWebHistory 需要后端配合我直接改成 createWebHashHistory 后就正常了虽然 URL 里多一个 #但对小型项目来说更省事。如果后期想用 history 模式就配好 Nginx 的 try_files没有技术难度只是配置问题。还有一个通用建议数据库表字段一定要加 create_time 和 update_time并且用 MyBatis-Plus 的自动填充功能维护这两个字段。堪称良心功能。配置一个 MetaObjectHandler 实现类插入时自动填充创建时间和更新时间更新时自动填充更新时间业务代码里根本不用去管时间问题。后期想排查数据变更、排查 bug、做数据分析的时候这两个字段能帮你定位问题。根据我个人的实际经验这类全栈商城项目最大的学习价值不在于功能有多复杂而在于让你完整经历一次“数据库建模 - 后端接口 - 前端页面 - 联调 - 部署上线”的闭环。很多人在学校练习时都是零散地写 demo这个项目能让各个环节真正串联起来。最后再分享一个小技巧项目写完后把启动步骤和部署过程整理成一个简短的 README别觉得这是浪费时间过两个月你再回来看代码时这份文档就是你找回手感最快的路径。