做电商系统这个方向,网上随便一搜就是一堆源码和课程。但真正能把SpringBoot、Vue3、MyBatis、MySQL这一套组合讲明白,让初学者拿到手能跑起来、能看懂、能改着用的项目,其实没有想象中那么多。今天分享的这套网上购物商城系统,就是典型的“前后端分离+SSM+MySQL”教学级实战项目,它不追求花哨的业务玩法,而是把电商最核心的用户、商品、购物车、订单这条链路完整做通了。无论你是正在学Java后端的在校学生,还是准备做课程设计、毕业设计的开发者,又或者只是想把前后端分离的协作流程跑一遍的初级程序员,这套项目都很值得拿来拆解和复现。接下来我按项目设计、数据库建模、后端实现、前端实现、环境部署、踩坑记录这个顺序,把整套项目的关键细节一次说清楚。
1. 项目整体设计与技术选型逻辑
1.1 为什么是这一套技术栈
做商城类系统,技术选型永远排在第一位。市面上做电商后端的选择很多,老一点的有SSH(Struts+Spring+Hibernate),现在主流的是SpringBoot+MyBatis或者SpringBoot+JPA,还有一部分团队会用更为轻量的Node.js或者Go。但这套项目最终选择SpringBoot+Vue3+MyBatis+MySQL,核心考虑是这三个方面。
第一是生态成熟度。SpringBoot从推出到现在,已经成为Java后端开发的事实标准,它通过自动配置大大降低了Spring繁重的XML配置成本,内置Tomcat让部署也变成了单Jar包运行。第二是资料丰富。MyBatis在国内中小型项目里的占有率一直不低,网上几乎能搜到任何场景的SQL写法,这对于学习者来说非常友好。第三是前端技术的发展。Vue3经过这些年的沉淀,组合式API的设计让代码组织更清晰,配合Vite构建工具,开发体验比Vue2时代流畅很多。把这四个技术点组合在一起,恰好覆盖了一个现代Web全栈开发的主流工作流:前端通过HTTP调用后端接口,后端通过MyBatis操作MySQL完成数据读写。
提示:如果你已经用过JPA或者MyBatis-Plus,也可以在这套项目基础上做替换。但建议至少先把MyBatis原生XML方式搞清楚,因为动态SQL和手写SQL的能力在任何团队里都是硬需求。
1.2 前后端分离到底在分离什么
很多初学者对“前后端分离”这个说法有误解,以为只是把前端页面和后端代码放在两个文件夹里,其实完全不是。前后端分离的核心是运行架构和职责边界的分离。前端项目和后端项目分别独立启动、独立部署,前端运行在浏览器的开发服务器上,后端运行在Java虚拟机里,两者之间只通过JSON格式的RESTful API进行通信。
这种架构带来的实际好处非常明显。开发和调试的效率会高很多,前端不需要安装JDK和MySQL,后端也不需要关注页面长什么样。团队协作上,前端负责页面渲染和交互逻辑,后端专注接口性能和数据处理,职责边界清晰。部署时前后端可以独立扩容,前端静态资源扔给Nginx,后端服务做多实例负载均衡,互不影响。当然,这样也让项目多了两个进程和跨域一起调试的复杂度,这是需要接受的代价。
我特别想提醒一点,前后端分离的项目里,“接口契约”是最容易出问题的地方。前端说接口返回了data字段,后端返回的却是list字段,这种问题在联调阶段特别常见。所以这套项目的接口设计一开始就要约定统一的返回格式,比如所有接口都返回{ code, message, data }结构,前端拿到response后先判断code再取数据,这样可以省掉大量无休止的扯皮。
2. 数据库设计:电商系统的地基
2.1 核心表结构规划
电商系统的数据库设计是这个项目的灵魂。如果表结构建得混乱,后面所有代码都会变得很难维护。这套商城项目采用了比较经典的五张核心业务表外加一张用户地址表的设计:用户表(user)、商品分类表(category)、商品表(product)、购物车表(cart_item)、订单表(order)、订单详情表(order_item)、用户地址表(address)。
用户表的设计有几个细节值得注意。除了常规的id、username、password、phone、email之外,建议增加created_at和updated_at两个时间字段,前者用于记录注册时间,后者在用户修改信息时自动更新。密码字段存的是BCrypt加密后的哈希字符串,长度设定为60,千万别用varchar(20)这种长度去存密码,否则加密串根本塞不进去。状态字段status用于控制账号是否可用,0表示正常,1表示禁用,这个字段在后期做账号管理体系时会非常有用。
商品表是整个系统里字段最丰富的表。除了id、name、description、price、stock、image、category_id这些基础字段之外,还可以增加sales_count销量字段和status上下架状态。price字段一定要用decimal(10,2),不能用float或者double,因为二进制浮点数在涉及到金额计算时会出现精度丢失问题。stock库存字段用int,并且在下单时扣减库存,这块涉及事务和并发控制,后面在后端实现部分会详细说。
购物车表最核心的设计在于唯一约束。一个用户对同一个商品只能有一条购物车记录,所以cart_item表要设置(user_id, product_id)联合唯一索引,同时保存quantity数量和selected是否勾选状态。这样前端每次把商品加入购物车时,后端只需要做一个“存在则更新数量、不存在则插入”的upsert操作,用一条INSERT ... ON DUPLICATE KEY UPDATE就能搞定。
订单相关的表设计是最容易劝退新手的部分。主订单表和订单明细表是典型的父子表结构,order表记录订单的总体信息,比如订单号、下单用户、收货地址、总金额、订单状态;order_item表记录每个订单包含哪些商品、数量、单价、快照信息。注意,order_item里一定要保存商品名称、商品图片、商品单价这些快照数据,不能下单之后再去关联product表查询,否则商品后续改了名称或价格,历史订单就全乱了。
2.2 表关系与设计心得
这几张表的关系用一句话就能说清楚:用户下订单,订单关联地址,订单包含多个订单明细,订单明细对应商品,购物车是用户和商品的多对多关系通过中间表体现。但在真正建表的时候,有几点心得我觉得比表关系本身更重要。
第一个心得是命名规范。表名和字段名一律使用小写加下划线的风格,比如order_item、created_at,避免使用驼峰命名。MySQL在Linux环境下对表名是区分大小写的,如果代码里写的表名大小写不一致,换一台服务器部署就会报错,这种问题排查起来特别浪费时间。Java代码里使用驼峰命名,通过MyBatis的mapUnderscoreToCamelCase配置自动转换,两端各守各的规范,不需要手动去别名匹配。
第二个心得是逻辑外键优先。不要滥用数据库物理外键约束,尤其是电商这种高写入场景。物理外键会在插入、更新时产生额外的锁检查,影响性能,也给后续的数据清理带来麻烦。实际项目中,表与表之间的关系更多是由应用层来保证的。比如删除一个商品之前,先去购物车表和订单明细表检查是否有引用记录,这种手动检查的方式比数据库外键更灵活、也更符合现代开发习惯。
第三个心得是索引不要乱建。很多初学者喜欢在每一个字段上都加索引,这反而是误区。索引会加速查询,但也会拖慢插入和更新。实际项目中只需要为重点查询字段建立索引就好:商品表的category_id分类查询索引,order表的user_id用户订单查询索引,order_item表的order_id关联索引,以及前面说的cart_item表的联合唯一索引。这四个索引基本覆盖了商城系统90%以上的查询路径。
3. 后端落地:SpringBoot + MyBatis 的关键细节
3.1 项目结构与分层规范
拿到项目源码之后,首先建议花十分钟把后端目录结构看清楚。这套项目的后端结构是非常清晰的三层架构加两个辅助模块:controller接收前端请求,service处理业务逻辑,mapper通过MyBatis操作数据库,再加上config配置类、common通用类、entity实体类、dto数据传输对象这几个辅助包。
com.shop.demo ├── common // 统一返回结果、异常处理、常量 ├── config // CORS配置、MyBatis配置、拦截器配置 ├── controller // 接口层,负责参数接收和响应 ├── dto // 请求参数对象和返回参数对象 ├── entity // 对应数据库表的实体类 ├── mapper // MyBatis的Mapper接口 ├── service // 业务逻辑层接口 │ └── impl // 业务逻辑实现类 └── SpringBootApplication这个分层的核心思想是依赖的单向流转:controller层依赖service层,service层依赖mapper层,谁也不能越级调用。严格禁止在controller里直接写SQL或者直接操作SqlSession,也禁止service层直接去处理HttpServletRequest这些Web层的东西。你会发现,遵守这套分层规范以后,整个项目的可维护性明显提高,每层各司其职,出问题的时候只需要定位对应层级就行。
DTO和entity必须区分开,这一点我想多说两句。entity实体类是数据库表的直接映射,字段和表字段一致,JPA风格的开发里经常把它直接返回给前端,这在简单的系统里问题不大,但项目一旦复杂就会埋雷。比如用户表的password字段,如果不加@JsonIgnore处理,查询用户信息时就会把密码哈希串返回给前端。DTO就是为了解决这种问题存在的,它可以只包含当前接口需要返回的字段,也可以组合多个表的数据。虽然写DTO会增加一点代码量,但对于前后端分离项目来说,这是非常值得的投入。
3.2 用户登录与JWT鉴权实现
登录鉴权是几乎所有电商系统的前置功能。这套项目采用的是JWT方案,即用户登录成功后,后端生成一个包含用户信息的Token返回给前端,前端在后续请求的请求头里带上这个Token,后端通过拦截器校验Token的合法性和有效期。
JWT由三部分组成:Header、Payload、Signature。Header声明加密算法,Payload可以存放用户id、用户名、过期时间等业务数据,Signature则用服务器端的密钥对前两部分进行签名,防止数据被篡改。这里有一个新手常犯的错误,以为把密码放进了Payload就安全了,这是严重误区。JWT的Payload部分是Base64编码,等于明文存储,任何人都可以解码看到内容。所以Payload里只能放用户id、用户名这类非敏感信息,密码这样的字段是绝对不能放进去的。
后端实现上需要三步。第一步是在用户登录接口里,用BCryptPasswordEncoder的matches方法校验密码。第二步是校验通过后使用Jwts.builder生成Token并设置过期时间,比如24小时。第三步是编写一个HandlerInterceptor拦截器,在preHandle方法中从请求头取Authorization字段,解析Token获取用户信息,并放入ThreadLocal供后续代码使用。这里要注意排除掉登录、注册、商品列表这些无需鉴权的接口路径,否则用户未登录时连商品都看不了。
生成Token的那段代码看起来不复杂,但有几个细节决定了它好不好用。密钥要单独配置在application.yml中,通过@ConfigurationProperties或者@Value读取,不要硬编码在类里。过期时间建议用ttl属性统一管理,存活时间可以根据业务需要调整。Token解析失败时不要直接抛异常,而是返回统一的未授权状态码,让前端跳转登录页。
实操心得:我在做这套项目的过程中,把拦截器里处理OPTIONS请求的逻辑放在最前面,这一点非常关键。浏览器在跨域请求时会先发送一个OPTIONS预检请求,如果拦截器对OPTIONS直接校验Token,前端就会频繁收到401错误,导致登录后刷新页面就退出登录。
3.3 MyBatis动态SQL在商品搜索中的应用
商品搜索和筛选功能是最能体现MyBatis动态SQL价值的地方。一个商城系统里,用户可能按关键字搜索商品名,也可能按分类筛选,还可能要按价格区间过滤,这些条件可能是多个组合出现的。如果为每一种组合都写一条单独的SQL语句,工作量巨大且不可维护。MyBatis的<where>和<if>标签就是为这种场景设计的。
<select id="selectProductList" resultType="com.shop.demo.entity.Product"> select id, name, description, price, stock, image, category_id, sales_count, status from product <where> and status = 0 <if test="keyword != null and keyword != ''"> and name like concat('%', #{keyword}, '%') </if> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="priceMin != null"> and price >= #{priceMin} </if> <if test="priceMax != null"> and price <= #{priceMax} </if> </where> order by id desc </select>这段SQL里有几个细节值得学习。<where>标签会自动去掉第一个满足条件的and前缀,不用手动处理逻辑。动态条件全部使用<if>判断,如果用户没有传某个参数,对应的条件就不会拼进SQL。使用concat('%', #{keyword}, '%')做模糊查询要特别注意,如果关键字里包含%字符,需要通过ESCAPE语法做转义,防止用户输入%查询出全部数据。不过对于普通学习项目来说,这个风险级别可以接受,但如果你在企业里开发,处理用户输入时一定要多留一个心眼。
除了查询,动态SQL在批量插入和批量更新场景同样好用。比如确认下单后生成订单明细,一条订单包含多个商品,如果循环单条插入,每插一条都要执行一次SQL,性能和代码可读性都不理想。使用MyBatis的<foreach>标签可以一次批量插入几十条明细数据,这种方式在真实项目中非常常见。
3.4 下单流程的事务控制
下单是整个电商系统中最复杂的业务操作,它涉及到多个表的联动修改。用户点击下单之后,后端要完成这些事情:生成订单主表记录、生成订单明细列表、扣减商品库存、清空对应的购物车记录。这四个操作必须保证要么全部成功,要么全部失败,任何一个环节出错,都会造成数据不一致。
SpringBoot中使用@Transactional注解来声明事务边界是最简单的方式。但只加注解是远远不够的,有几个问题必须处理。第一,事务默认只在RuntimeException异常时回滚,如果代码里捕获了异常并打印了日志,事务就不会回滚,数据就会处于中间状态。正确的做法是让异常继续向外抛出,或者在rollbackFor属性中指定捕获的异常类型。第二,事务方法不能是private方法,不能被this调用,因为Spring的事务代理机制要求方法调用必须通过代理对象进行。在同一个类内部通过this调用加了事务注解的方法,事务是不生效的,这个坑我见过无数人踩。
还有一个非常容易被忽视的点是库存扣减。下单时扣减库存,不应该先查库存、再判断、再更新,这种check-then-act的写法在多用户并发下单时会出现超卖问题。正确的做法是使用带有条件的UPDATE语句直接扣减:
update product set stock = stock - 1 where id = #{productId} and stock > 0这段SQL先检查库存大于0再执行扣减,通过数据库的行锁保证了原子性。如果更新的影响行数为0,说明库存不足,业务线程就可以立即抛出异常促使事务回滚。这是教科书级别的防超卖写法,代码简单却非常有效。
注意:这里的防超卖方案只适用于单库单节点的项目。如果是高并发场景,还需要引入分布式锁、Redis预热库存、MQ异步削峰等一整套方案,那是另一个量级的课题了。作为学习项目,先把事务和条件更新的思路跑通就好。
4. 前端落地:Vue3 组合式 API 的核心细节
4.1 前端目录规划与API统一封装
Vue3项目工程化之后,目录结构直接决定了代码的清晰度。这套项目前端使用的是标准的Vite+Vue3结构,src下面按功能模块进行划分:api目录集中管理所有后端接口请求,router目录配置前端路由,stores目录使用Pinia做状态管理,views目录放页面组件,components目录放可复用的视图组件。
个人最看重的是api层的统一封装。因为前后端分离的项目里,前端有大量重复的网络请求逻辑:每个请求都要加Token请求头,每个接口都要处理超时和错误状态码,每个请求几乎都要解析统一的返回结构。如果不做封装,每个页面里写一遍axios配置,代码会变得非常冗余且难以维护。
import axios from 'axios' import { useUserStore } from '@/stores/user' import router from '@/router' const http = axios.create({ baseURL: '/api', timeout: 10000 }) http.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) http.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { // 业务错误统一提示 return Promise.reject(new Error(res.message)) } return res.data }, error => { // 401 跳转登录 if (error.response && error.response.status === 401) { const userStore = useUserStore() userStore.clearLogin() router.push('/login') } return Promise.reject(error) } )这是整套前端代码的基石。请求拦截器统一附加Token,响应拦截器统一处理业务错误和登录失效跳转,每个页面调用接口时只需要关心业务数据,不需要关心这些繁琐的公共逻辑。接口函数统一放在api目录下按业务模块拆文件,比如user.js封装登录注册相关的接口,product.js封装商品查询相关的接口,cart.js封装购物车相关的接口,order.js封装订单相关的接口,页面组件从这些文件导入函数使用。
需要特别说明的是baseURL设置为/api而不是完整的后端地址,这是配合开发环境代理和上线环境Nginx转发的一个技巧。本地开发时Vite配置proxy把/api开头的请求转发到后端服务,上线后Nginx把/api开头的请求转发到后端服务,前端代码里不需要区分环境,只需要关注/api前缀。这种约定大大提高了前端的可移植性。
4.2 购物车与订单的状态管理
购物车这个功能模块是前端状态管理的重头戏。购物车数据有几种存储方案:存在后端数据库、存在本地localStorage、存在前端内存。这套项目选择了后端数据库为主导的存储方案,购物车数据以user_id和product_id为维度保存在MySQL中。前端每次打开购物车页面时通过接口拉取购物车列表,用户增删改购物车条目时调用后端接口同步数据。
这种方案的好处是数据跨设备同步,坏处是每次操作都要发起HTTP请求,交互响应会有一点延迟。为了优化体验,我采用了一个折中做法:组件内维护一份购物车列表的响应式数据,操作时先更新本地数据让页面立即响应,再发送请求给后端持久化,请求失败时再回滚本地状态。这个“乐观更新”的策略在交互体验和数据一致性之间找到了很好的平衡。
状态管理工具方面,这套项目用的是Pinia。Vue3正式推荐的状态管理库,相比Vuex更简洁,TypeScript支持也更友好。整个项目中被放在stores里管理的主要是两类数据:用户登录信息和购物车数量角标。前者全局使用,后者涉及顶部导航栏和各页面间的联动。比如用户把商品加入购物车后,导航栏右上角的购物车数量角标要立刻刷新,这种跨组件通信状态放到Pinia里管理是最合适的选择。
import { defineStore } from 'pinia' import { getCartList } from '@/api/cart' export const useCartStore = defineStore('cart', { state: () => ({ totalCount: 0 }), actions: { async fetchCartCount() { try { const list = await getCartList() this.totalCount = list.reduce((sum, item) => sum + item.quantity, 0) } catch (e) { // 忽略错误,不阻塞页面渲染 } } } })这其实暴露了一个设计思路:不是所有组件内都去请求购物车列表拿数量,而是通过一个全局的store统一管理和分发。各页面在关键时机调用fetchCartCount,导航栏通过store的响应式数据自动刷新,这个模式在实际项目中非常实用。
4.3 路由守卫与权限控制
前端的权限控制不是真正的安全边界,真正的安全判断必须放在后端服务端。但前端通过路由守卫做一层拦截,可以避免用户直接访问未登录页面时发起一串注定失败的请求,提升体验的同时也减轻了后端压力。这套项目在Vue Router中使用了全局前置守卫,在进入页面前检查登录状态。
router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.path === '/login') { // 已登录用户访问登录页,直接跳转首页 if (userStore.token) next('/') else next() } else { if (!userStore.token) next('/login') else next() } })代码逻辑很简单。页面配置路由时只需要把需要登录的页面加入一个meta.requiresAuth标记,守卫里统一判断。这里有一个需要提前想清楚的点:用户关闭浏览器再打开时,Pinia中的用户信息会丢失,但后端日志保存的Token还在有效期内。所以store在初始化时要尝试从localStorage恢复Token状态,并在页面刷新后根据localStorage里的用户信息恢复登录态。如果只依赖内存中的状态,刷新页面就会被误判为未登录。
另外,前端的“登录态失效”处理一定要配合后端的401状态码。用户在操作过程中Token过期,后端会返回401,前端响应拦截器需要清空本地登录信息并跳转登录页。这个流程要和登录页的redirect参数联动,用户重新登录后能跳回原页面,而不是每次都被打回首页。这个细节如果做好了,整个系统的使用体验会明显高一个档次。
5. 环境搭建与部署实践
5.1 本地环境配置清单
在动手复现这套项目之前,先把本地环境准备齐,官方说法叫环境依赖。我把整套开发过程需要的工具体系整理成了一张清单:
- JDK 8或JDK 17:SpringBoot 2.x系列对JDK8支持最稳定,3.x系列则要求JDK17。项目启动前先确认版本匹配。
- Maven 3.6以上:用于管理后端依赖和打包构建。
- Node.js 16以上:Vite 4.x脚手架需要Node版本支持,版本太老会启动报错。
- Vue3前端依赖安装:项目根目录执行npm install。
- MySQL 5.7以上或者MySQL 8.0:SQL脚本在项目doc目录里,直接用命令行或图形工具导入。
- Redis:如果不需要验证码和热点缓存功能,先不用安装也可以跑通全部业务。
这些工具本身安装流程并不复杂,但环境变量配置是新手最容易卡住的环节。JDK的JAVA_HOME、Maven的MAVEN_HOME、Node的PATH路径,每项都要配置到系统环境变量中。一旦配置错误,命令行输入java -version会提示找不到命令,或者能在某个终端窗口用、换一个终端窗口就失效。建议配置完成后要新开一个终端窗口验证,因为旧窗口加载的环境变量不会自动更新。
数据库初始化要提醒一下。导入SQL脚本后要检查数据量和后台账号,项目源码里通常会有测试数据,包含几个分类、几十个商品以及一个测试管理账号。记得把数据库的字符集设定为utf8mb4,不然商品描述里的特殊符号无法正常存储。
5.2 跨域问题与联调配置
前后端分离项目第一次启动时,遇到最多的问题就是跨域。本地开发时前端运行在Vite默认的5173端口,后端运行在SpringBoot默认的8080端口,不同端口之间直接发请求会产生跨域拦截。解决办法有两种,推荐优先使用Vite的代理配置。
在vite.config.js文件中配置:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })这个配置的意思是:前端发出的所有以/api开头的请求,由Vite开发服务器转发到http://localhost:8080,后端接口中不需要再有/api前缀。浏览器端看到请求是同源的,自然不存在跨域问题。这种代理方式还有一个额外的好处:后端接口即使没有开启CORS配置,本地开发也不受影响。
如果你不打算用代理,也可以在后端直接配置CORS。
@Configuration public class CorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }; } }需要留意的是,allowedOrigins("*")和allowCredentials(true)不能同时使用,否则会报错,这时需要用allowedOriginPatterns。另外,上线时一定不要用通配符放开所有来源,应该替换成真实的域名。生产环境的安全配置和本地联调完全是两回事。
5.3 部署方案与注意事项
部署阶段是很多学习者没有接触过的环节,也是完整项目必须要走的一步。前后端分离项目的部署思路是:前端静态资源由Web服务器托管,后端服务独立运行。最常见的组合是Nginx+SpringBoot。
前端先执行npm run build,构建结果会生成一个dist目录,里面是打包压缩后的静态文件。把dist目录整个上传到服务器上,然后配置Nginx,让访问的根路径指向这个目录。同时还要配置反向代理,将请求路径中带/api的请求转发到后端SpringBoot服务。这样一个80端口的站点就能同时服务前端静态资源和后端接口,这也是生产环境最常见的模式。
server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/shop/dist; 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模式下刷新404的问题。因为前端路由跳转是在浏览器端完成的,但用户手动刷新页面时,浏览器会向服务器请求/product/123这个路径,服务器上根本不存在这样的文件,如果不重写成index.html,页面就会报404。加上这条配置后,所有未匹配到文件的路径都会回到index.html,由前端路由接管后续展示。
后端部署相对简单,打包成jar后使用java -jar shop.jar运行。但真实环境不建议直接前台运行,一定要结合nohup或者systemd做后台进程管理。我习惯用systemd编写一个service文件,配置好JVM参数、运行目录、日志输出,这样可以用systemctl start shop启动、systemctl status shop查看运行状态,Java进程崩溃后还能自动重启。
6. 常见问题与排查技巧实录
6.1 启动报错与运行异常清单一览
任何项目都不可能一次跑通,我把这套项目中最容易遇到的一批问题和对应的排查方向整理成了表格,这些都是我自己实操中真实踩过的和有学员反馈过的。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 后端启动报数据库连接失败 | MySQL未启动、密码错误、数据库不存在 | 先确认MySQL服务在运行,再用命令行测试连接,最后检查application.yml配置 |
| 前端npm install报错 | Node版本与依赖不兼容 | 尝试删除node_modules和package-lock.json后重新安装,或者升级Node版本 |
| 前端页面能打开但登录后接口返回404 | 代理配置中的rewrite把接口路径改写错误 | 打开浏览器开发者工具,查看请求的URL是否和后端Controller路径匹配 |
| 请求接口报CORS错误 | 后端未配置CORS或配置了冲突项 | 检查allowedOriginPatterns和allowCredentials配置;建议优先用Vite代理解决 |
| 登录时报用户名或密码错误 | 数据库初始数据没有账号,或密码加密方式不一致 | 确认注册接口的BCrypt加密和校验方法一致,用注册的账号测试登录 |
| 查询商品列表一直返回空 | 数据库表里没有商品数据,或status字段被条件过滤 | 直接查数据库确认数据存在且status=0 |
| 下单成功但库存没变 | 事务未生效,或事务方法被同类内部调用 | 检查@Transactional注解位置和调用方式 |
| 前端刷新页面后一直跳登录页 | 未从localStorage恢复用户信息 | 检查store初始化和路由守卫的逻辑 |
这里要特别强调报表里事务未生效的问题。Spring的事务是基于AOP的,原理是通过代理对象来增强目标方法。如果Service内部某个私有方法加了@Transactional,而公开方法直接调用它,事务是不会生效的,因为调用方持有的是this对象,不是代理对象。实际开发中解决方法是把需要事务的代码单独拆到另一个Service中,或者直接把事务注解加在公开的入口方法上。
6.2 日志定位与问题分析手段
很多新人遇到报错第一反应就是浏览器白屏或者页面报500,然后束手无策。这里分享一套标准的排查流程,适用于绝大多数系统性错误。第一步,先看后端控制台或者日志文件的异常堆栈。SpringBoot默认输出的自带框架信息很全,包括异常类型、出错的具体代码行数、参数值。第二步,借助前端Console输出。Vue3项目里的错误信息通常会明确告诉你哪个组件、哪个模块出了问题,同时查看Network面板里接口请求的StatusCode。第三步,根据状态码缩小范围。404是路由或接口路径问题,500是后端业务异常,401是Token失效,403是权限不足,400通常是参数校验不过关。
这套项目里我把日志级别调整了一下,把SQL语句打印开关打开。MyBatis在application.yml里配置日志级别为DEBUG,就能在控制台看到每条查询的SQL语句、传入的参数和查询结果。看到实际执行的SQL之后,大部分数据查询问题都能定位到是SQL本身写错了还是参数没传对。
logging: level: com.shop.demo.mapper: debug这样配置后,控制台会输出mybatis执行时的SQL和参数。有了这些信息,基本不会出现“完全不知道错在哪”的状态。当然,把SQL日志开到DEBUG级别只建议在开发环境使用,生产环境还是应该把日志级别调回INFO,避免大量SQL刷屏影响磁盘空间和性能。
6.3 性能优化与安全加固思考
最后聊一聊项目做完之后可以继续深化的方向。这套商城系统跑通核心流程之后,它仍然是一个“能用”但“还不够好”的系統。如果用于学习进阶或者真实上线,有几个明显可以优化的点。
第一个是缓存优化。商品详情页、分类列表属于访问频率高、更新频率低的数据,非常适合引入Redis缓存。用户查询商品时先查Redis,命中就直接返回,未命中则查数据库然后回填缓存。注意事项是缓存更新策略要做好,比如商品修改或删除时要把对应的缓存也更新或清除掉,否则用户会一直看到旧数据。第二个是数据库连接池参数和分页查询优化。在不引入复杂组件的前提下,把数据库连接池的合理容量配上、给必要的SQL加上覆盖索引,都会有不错的性能提升。
安全层面也要养成习惯。密码加密要使用BCrypt,JWT密钥要使用足够长的随机字符串,接口层要做参数校验,防止用户传入恶意数据。后端拦截器里对所有写操作要做权限校验,比如删除订单的操作必须确认该订单属于当前登录用户,不能通过改写请求参数去操作他人数据。这些思路不需要复杂的框架和技术,就是开发习惯的问题,但恰恰是习惯决定了一个项目的安全底线。
最后分享一点个人体会:做商城系统这类项目,最大的价值不在于“跑通”,而在于你把整个数据流想明白了。一个用户从注册、浏览商品、加入购物车、提交订单到支付成功,这条链路上每一步都涉及前端状态管理、后端接口设计、数据库事务和性能优化的问题。把这套项目完整拆解一遍,比单纯看几十个视频教程都更有收获。如果你正在学这套技术栈,我建议先照着跑通,再尝试修改一个功能模块的完整流程,比如增加一个优惠券功能,自己去设计表和接口,这个过程中踩到的坑,才是真正长在你身上的技能。
我个人的习惯是每做一个项目就写一份完整的部署文档和接口清单,把每个接口的地址、请求参数、返回结构都记录下来。这个习惯在后期维护时能省下大量时间。希望你也能在复现这套项目的过程中,建立起属于自己的一套开发流程和文档习惯。