news 2026/9/26 6:58:39

基于SpringBoot+Vue的前后端分离在线家具商城项目实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SpringBoot+Vue的前后端分离在线家具商城项目实战解析

前后端分离的在线家具商城,这类项目我这两年带不少人跑通过。先说结论:这是一个非常典型的Java全栈练手项目,麻雀虽小五脏俱全——用户注册登录、商品分类浏览、购物车、下单结算、后台商品和订单管理全都有,而且SpringBoot + Vue + MyBatis + MySQL这套组合,是现阶段国内中小型团队做Web系统最主流的选型之一,你把这个项目吃透,出去面试或者接手外包项目,底子已经比大多数简历写“熟悉SpringBoot”的人扎实了。

这篇博文我不想只丢一个源码包让你自己看,而是直接把整个项目从需求拆解、技术选型、核心实现到部署上线的完整链路给你捋一遍。下面所有截图级的细节、配置文件和踩坑点,都是我在实际部署和改造这个项目的过程中整理出来的,适合正在做毕业设计、想入门全栈开发、或者准备做一套基础商城系统二次开发的读者参考。

1. 项目整体拆解:在线家具商城到底在做什么

1.1 为什么选“家具”这个品类做前后端分离项目

很多人一开始看到“家具商城”会觉得奇怪:做商城系统,用“图书”、“数码”不是更常见吗?我当时选家具这个品类,恰恰是因为它比图书、数码更有代表性。

家具类商品有几个显著特点:SKU(库存量单位)不算太多但属性复杂——一张沙发有材质、颜色、尺寸、风格、品牌五个维度;主图要求高——需要多角度展示图、详情轮播图;客单价高、订单状态流转长——从下单到发货再到收货确认,中间还可能要经历定制工期。这些东西会倒逼你把数据库设计得足够规范,把订单模块做完整,而不是像图书商城那样简单“下单-发货”两步走完。

从教学和毕设角度来看,家具商城还天然带一个亮点:后端管理端可以做得很有内容,比如商品多图上传、分类层级管理、库存预警、订单多条件筛选。这些功能拆出来都是面试喜欢问的高频点,写在简历上比“实现了用户登录注册”好看得多。

1.2 需求范围与功能模块清单

这个系统的整体角色分成两端:

  • 前台商城(用户端):面向C端消费者,核心使命是“让用户逛得爽、买得顺”。
  • 后台管理(管理端):面向运营和管理员,核心使命是“让商品和订单管得清”。

我们对照着来看模块清单:

端模块功能说明
前台用户模块注册、登录(JWT鉴权)、个人信息维护、收货地址管理
前台商品模块分类导航、商品列表(价格/销量排序)、关键词搜索、商品详情
前台购物车加入购物车、修改数量、删除、选中结算
前台订单模块订单创建、订单列表、订单详情、取消订单
后台商品管理商品CRUD、多图上传(本地存储)、上下架、库存修改
后台分类管理一级/二级分类维护、排序
后台订单管理订单列表查询、发货状态更新、订单备注
后台统计看板商品总数、订单总数、近7日订单趋势(简单ECharts图表)

这套功能范围我建议你拿到源码后第一件事就是对照着跑一遍,跑通之后你自然会对整个项目的数据流转有个整体的感知:用户在前台点下“提交订单”的那一刻,后端发生了什么,数据库里几张表的数据怎么联动。带着这个感知去读源码,效率会高很多。

2. 技术选型:SpringBoot + Vue + MyBatis + MySQL为什么能打

2.1 后端框架选型:SpringBoot的取舍

后端框架备选方案其实不少,SSH(Struts2 + Spring + Hibernate)、SSM(Spring + SpringMVC + MyBatis)、SpringBoot都有人用。但放在2024年这个时间节点,SpringBoot已经是事实上的Java后端标准,没有太多悬念。

选SpringBoot的理由很实在:

  • 内置服务器:自带的Tomcat让开发期零配置启动,“写好接口直接Run”,不用像传统SSM那样部署到外部Tomcat再启动,迭代效率提升是肉眼可见的。
  • 自动配置:数据库连接池、ORM框架、JSON序列化这些基础设施,SpringBoot用spring-boot-starter-xxx一拉就全有了,省掉大量XML配置。
  • 生态无缝衔接:SpringSecurity、Redis、RabbitMQ、文件存储这些后续扩展组件,全都是在SpringBoot上做starter集成,你现在用SpringBoot打底,后面不管加入什么功能都有现成方案。

从学习角度我也建议你跳过传统的SSM阶段直接上SpringBoot。XML配置那套是老一辈程序员的历史包袱,你花两周死磕applicationContext.xml的bean定义,对理解现代Web开发几乎没有增量收益,不如把这时间花在接口设计、数据库建模、权限方案上。

2.2 数据持久层:MyBatis凭什么替代JPA

持久层框架的选择是个争论不休的话题,我的态度很明确:国范围内你想找一套最快上手、SQL最可控的方案,MyBatis依然是首选。

对比一下你就懂了:

对比维度MyBatisSpring Data JPA
SQL可控性手写SQL,慢查询一眼就能定位动态生成的SQL,复杂查询需要查日志
上手门槛会SQL就会用,XML直接写需要理解ORM思想、懒加载、持久化上下文
复杂查询多表联查、动态条件都很好写关联查询容易踩N+1坑
国内普及度绝大多数公司Java岗都在用外企和部分新项目用得多

家具商城这种系统有个很典型的场景:商品列表需要多条件动态查询(分类+价格区间+排序方式,条件可能有也可能没有)。在MyBatis里用<where>加<if>标签写动态SQL,逻辑清晰、执行SQL一眼看懂;在JPA里你得靠Specification或者@Query拼接,写出来又长又绕。所以我坚持用MyBatis,本质上是为了让“SQL”这件事回归直觉——你写出什么SQL,数据库就执行什么SQL,性能全部在掌控之中。

2.3 前端方案:Vue 2还是Vue 3

这个项目我用的是Vue 2 + Element UI,如果在2024年新开项目,我会建议你直接上Vue 3 + Element Plus。但这里有个现实考量:现在市面上大量存量项目、毕设源码、外包系统都是Vue 2,你想要快速跑通这套源码,Vue 2是绕不开的学习成本。而且Vue 2的核心思想(组件化、数据响应式、路由、状态管理)在Vue 3里基本一脉相承,你先把Vue 2的项目啃明白,迁移到Vue 3只需要学Composition API和新的生态组件即可。

Vue在前端分离这个架构里承担的角色很清晰:

  • 组件化:首页的导航栏、商品卡片、轮播图、购物车列表,全部拆成独立组件,相互不干扰,这就是前后端分离里前端部分的自洽与模块化——后端按接口切分,前端按组件切分。组件化带来最大好处是可复用,比如商品卡片组件,首页推荐位、商品列表页、搜索结果页三处共用,改一个地方三处生效。
  • 数据响应式:数据变更自动更新视图,你在购物车页面点“+1”,总价实时刷新,不需要手动操作DOM,这种开发体验比直接写jQuery舒服太多。
  • 生态配套:VueRouter管页面跳转、Axios管HTTP请求、Vuex/Pinia管全局状态,三件套把前端工程化拼图补齐了。

2.4 前后端分离的架构优势与通信机制

聊聊“前后端分离”本身。早年的JSP开发模式是前端写死在Java里,一个页面既要写HTML又要嵌套Java代码,改个样式可能要重新编译整个项目。前后端分离之后,职责边界非常清晰:

  • 前端只负责页面渲染、用户交互、调用接口渲染数据,跑在浏览器里。
  • 后端只负责业务逻辑、数据持久化、权限校验,跑在服务器上。
  • 通信载体是JSON格式的RESTful API,前端发HTTP请求,后端返回JSON,谁也不关心对面内部怎么实现。

开发模式上,只要接口文档定义好,前后端可以并行开发,前端用Mock数据先渲染页面,后端同步写接口,最后联调时把Mock换成真实接口就行。部署模式上,前端构建成静态文件扔Nginx,后端打包Jar或War独立运行,两边各部署各的,扩容时甚至可以分别做负载均衡。这套架构能成为今天Web开发的主流,靠的是实打实的工程效率提升,不是概念炒作。

通信的细节部分有个必须掌握的机制——跨域。你在开发环境访问localhost:8080的Vue页面,却要向localhost:9090的SpringBoot接口发请求,浏览器会因为协议、域名、端口不一致而阻止,这就是跨域问题。经典解法是在SpringBoot里加一个CORS全局配置类,允许指定来源访问。这个配置我在后面的实操部分会给出完整代码。

3. 核心实现细节与实操要点

3.1 数据库设计:9张核心表的设计思路

数据库是这套系统的地基,出问题全是连锁反应。我按业务模块把核心表拆开讲讲设计思路:

表名核心字段设计要点
userid, username, password, nickname, avatar, phone密码必须加密存储,我用的是BCrypt加盐哈希,绝对不能明文
addressid, user_id, receiver, phone, province, city, detail一个用户对应多个地址,所以单拎出来做表
categoryid, name, parent_id, sortparent_id为0表示一级分类,实现两级分类结构
productid, category_id, name, subtitle, main_image, price, stock, sale_count, status, detail用逻辑删除(status字段)而不是物理删除,便于订单历史数据追溯
product_imageid, product_id, url, sort一张商品多张图片,用子表存
cart_itemid, user_id, product_id, quantity, checked联合约束user_id + product_id唯一,同一商品复用一条记录
orderid, order_no, user_id, total_price, status, receiver_infoorder_no全局唯一;收货人信息冗余存入订单表,防止地址变更后订单查不到旧地址
order_itemid, order_id, product_id, product_name, product_image, price, quantity商品快照字段(名称/图片/价格)必须冗余,否则商品改价后订单金额对不上
adminid, username, password管理端单独账号体系,和用户端隔离

几个容易被人忽略的细节:

  • 金额字段用十进制类型:decimal(10,2)而不是float/double。浮点数的精度问题在金额计算上会直接导致对不上账,这是初学者常踩的坑。
  • 订单号和雪花算法:不要用数据库自增ID当订单号,因为会暴露销量、而且多表合并时容易冲突。我项目里用UUID截取加时间戳生成了个32位的order_no,虽然没那么花哨,但足够满足唯一性要求。
  • 逻辑删除的设计:商品表用status字段(0下架、1上架、2删除)代替物理删除。因为订单详情里要展示历史商品信息,物理删除会导致外键悬空。同理用户也是逻辑删除,防止user_id在订单里查无此人。

3.2 后端分层与接口设计:Controller-Service-Mapper怎么组织

后端代码结构我建议你直接照搬经典三层架构,别玩花活:

com.example.mall ├── controller // 接收请求、参数校验、返回统一结果 ├── service // 业务逻辑编排、事务管理 ├── mapper // MyBatis数据访问接口 + XML文件 ├── entity // 数据库实体类 ├── dto // 数据传输对象(前端传入/返回) ├── config // 全局配置(跨域、拦截器、静态资源映射) ├── common // 统一返回结果、异常处理、常量 ├── util // 工具类(JWT工具、MD5工具) └── interceptor // JWT登录校验拦截器

Controller层我强调两点:

  • 统一返回结构。所有接口都返回Result.success(data)或Result.error(msg),结构固定为{code, message, data}。这样前端拦截器可以统一判断code是否为200,不用每个接口单独写错误处理。三端联调(前端、后端、测试)只看code就能定位问题。
  • RESTful风格。比如商品接口设计为GET /product/{id}、GET /product?categoryId=1&pageNum=1&pageSize=10&sort=price_desc。路径上的名词代表资源,HTTP方法代表操作,语义清晰自解释,前端对接零沟通成本。

Service层是业务逻辑的重头戏,最核心的就是下单接口。它不是一个简单的INSERT,而是一个涉及多张表的事务操作:

@Transactional public Order createOrder(Long userId, List<CartItemDTO> items, Long addressId) { // 1. 校验购物车数据,计算总金额 // 2. 检查商品库存,不满足则抛出异常 // 3. 生成订单主记录(状态:待付款) // 4. 批量生成订单明细(商品名称/图片/价格快照) // 5. 扣减库存:UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity} // 6. 清空购物车中已下单的商品 return order; }

注意第5步的写法:WHERE stock >= #{quantity}是防超卖的关键,通过UPDATE语句受影响行数是否为0来判断库存是否足够,这种“乐观锁扣减”在单体应用里已经够用,不需要引入Redis分布式锁那么重。方法上的@Transactional保证这6步要么全部成功、要么全部回滚——比如库存扣减成功但订单明细插入失败,整个事务回滚,库存不会白白扣掉。

MyBatis的XML部分,商品列表的动态查询是我最想让你看懂的案例:

<select id="selectProductList" resultType="com.example.mall.entity.Product"> SELECT * FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY ${sort} -- 这里注意用${},排序字段属于白名单配置,不能走预编译 </select>

<where>标签会自动处理掉第一个条件的AND,<if>实现了条件动态拼接。这套写法就是MyBatis的看家本领,面试十有八九会问到,你源码里多找几个这种例子,自己能默写出来就算真掌握了。

3.3 前端页面与组件拆解:从登录页到购物车的完整链路

前端项目结构我建议这样组织:

src ├── api // 接口请求模块(按业务模块拆:product.js, order.js, user.js) ├── assets // 静态资源(图片、全局样式) ├── components // 公共组件(Header, Footer, ProductCard, Pagination) ├── router // 路由配置(含守卫) ├── store // Vuex状态管理(购物车角标、用户信息) ├── views // 页面组件 │ ├── home // 首页 │ ├── product // 商品列表、详情 │ ├── cart // 购物车 │ ├── order // 订单确认、订单列表 │ ├── user // 登录、注册、个人中心 │ └── admin // 后台管理页面 └── utils // 工具(axios封装、token存储)

核心请求封装直接用Axios实例,统一注入token和响应拦截:

// utils/request.js import axios from 'axios' import router from '@/router' const service = axios.create({ baseURL: '/api', // 开发环境走Vue代理 timeout: 10000 }) // 请求拦截:带上token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token // 后端从Header取值 } return config }) // 响应拦截:统一处理code service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { // 401跳登录页 if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

购物车角标这个功能最能体现Vuex的价值:用户在不同页面(首页加到购物车、商品详情页看购物车、下单页清空购物车)时,角标数字是全局共享的状态,放进Vuex维护,一个commit触发,所有相关组件自动更新。

前端几个容易忽略的细节:

  • 路由守卫:router.beforeEach((to, from, next) => {})里判断路由meta是否需要登录,没token就跳转/login?redirect=当前路径,登录成功后跳回原页面,这个交互细节很提升体验。
  • 图片别名:Vue CLI默认的@指向src目录,配置好了别瞎改,不然一堆import报错。
  • 组件通信:父子组件用props和$emit,跨页面用Vuex,全局事件(如购物车变更)可以用一个简单的$bus(new Vue())来做,但别在项目里到处乱用,容易把数据流搞乱。

3.4 鉴权与安全:JWT登录态方案

前后端分离项目不能靠Session维护登录态,因为浏览器和服务器跨域了、而且前端可能是多端(Web/H5/小程序)。我用的是JWT(JSON Web Token)方案,流程如下:

  1. 用户输入用户名密码,后端校验通过,签发一个token返回给前端。
  2. token由三部分组成:Header(加密算法)、Payload(用户ID、过期时间)、Signature(服务端密钥签名)。密钥在服务端私藏,用HS256算法签名,客户端无法伪造。
  3. 前端把token存到localStorage,后续所有请求在Header里带上Authorization: token。
  4. 后端写一个拦截器,拦截除了登录、注册、商品列表、商品详情以外的所有接口,校验token、解析出用户ID放入ThreadLocal(用户上下文),供Controller直接使用。
  5. 管理端单独用一套AdminInterceptor做校验,防止普通用户token访问管理接口。

核心代码:

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equals(request.getMethod())) return true; String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BizException(401, "未登录"); } try { Claims claims = Jwts.parser().setSigningKey(secretKey) .parseClaimsJws(token).getBody(); // 用户ID存入ThreadLocal UserContext.set(claims.get("userId", Long.class)); return true; } catch (ExpiredJwtException e) { throw new BizException(401, "登录已过期"); } catch (JwtException e) { throw new BizException(401, "非法token"); } } }

这个方案有几个需要特别注意的坑:

  • 登录过期处理:前端拦截器拿到401要跳登录页,但Ajax请求要避免无限循环跳转,我没用window.location.href直接跳,而是用router.push并带上redirect参数。
  • 敏感信息不进Payload:JWT的Payload部分只是Base64编码,不是加密,别把密码、手机号明文塞进去,放个用户ID就够了。
  • 注销问题:JWT是无状态的,服务端不能主动使它失效。所以我的方案是前端删除本地token,配合短过期时间(2小时)。真要严格注销控制,得引入Redis黑名单,这个属于进阶扩展了。

4. 完整部署教程:从源码到上线

4.1 本地开发环境搭建

先把环境准备好,我用的是目前稳定且兼容性最好的版本组合:

工具版本建议备注
JDK1.8 或 11SpringBoot 2.x最稳的是1.8,装3.x才需要17
Maven3.6.3+配置阿里云镜像加速依赖下载
Node.js14.x 或 16.xVue2项目别装Node 18+,容易报OpenSSL错误
MySQL5.7 或 8.08.0注意serverTimezone=Asia/Shanghai
IDEIDEA 2023+后端强烈建议IDEA,前端用VS Code即可

环境搭建三个高频报错先给你预防针:

  • Maven依赖下载慢或者失败:在~/.m2/settings.xml配阿里云镜像,别用默认中央仓库。
  • Node版本太高导致的OpenSSL问题:Vue2老项目的webpack版本和Node18不兼容,启动报error:0308010C:digital envelope routines::unsupported,三个解法任选:换Node16、或者用NODE_OPTIONS=--openssl-legacy-provider环境变量硬撑。
  • MySQL8认证插件问题:JDBC连接报Unable to load authentication plugin 'caching_sha2_password',把连接驱动换成mysql-connector-java最新版即可。

数据库初始化我直接给你完整流程:

# 1. 登录MySQL mysql -u root -p # 2. 创建数据库(utf8mb4比utf8更好,兼容emoji和生僻字) CREATE DATABASE furniture_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 3. 导入SQL脚本 use furniture_mall; source /path/to/furniture_mall.sql; # 4. 查看导入是否成功 SHOW TABLES;

后端配置文件里的数据库连接和密钥,照着这样的格式改:

spring: datasource: url: jdbc:mysql://localhost:3306/furniture_mall?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver jwt: secret: 自定义一个至少32位的密钥字符串 expire: 7200000 # 过期时间毫秒数,2小时

前端环境搭建两条命令搞定:

npm install npm run serve

如果npm install卡在某个包上,先清缓存再换源:

npm config set registry https://registry.npmmirror.com rm -rf node_modules package-lock.json npm install

4.2 后端打包:jar包运行与Tomcat war包两种方案

后端打包有两条路线,我逐一说明适用场景。

方案A:Jar包直接运行(推荐)

SpringBoot内置Tomcat,你最终交付的就是一个可执行的jar包,服务器上只要装了JDK就能跑。步骤:

# 在项目根目录执行 mvn clean package -DskipTests # 得到 target/furniture-mall-0.0.1-SNAPSHOT.jar java -jar target/furniture-mall-0.0.1-SNAPSHOT.jar

生产环境用nohup启动,避免SSH断开导致进程退出:

nohup java -jar furniture-mall.jar --spring.profiles.active=prod > logs/app.log 2>&1 & echo $! > app.pid

方案B:War包部署到外部Tomcat

有些学校或公司的运维环境只允许把应用放进Tomcat的webapps目录,这时候需要改造:

  1. 修改pom.xml:<packaging>war</packaging>。
  2. 启动类继承SpringBootServletInitializer并重写configure方法:
@SpringBootApplication public class MallApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(MallApplication.class); } }
  1. 打包后把war放到Tomcat的webapps目录,启动Tomcat自动解压部署。

两种方案怎么选:jar好比便携式炒锅,自带加热源随处可用;war好比电炉台面上的锅,必须依托于外部容器。我强烈建议你学会jar方式,因为这才是SpringBoot生态的默认姿势,war是历史兼容产物。

4.3 前端构建与Nginx部署

这套前端300行配置你直接照抄,核心就两个作用:托管静态文件 + 反向代理后端接口:

server { listen 80; server_name your-domain.com; # 改成你的域名或服务器IP # 前端静态资源 location / { root /usr/share/nginx/html/furniture-mall; index index.html; try_files $uri $uri/ /index.html; # Vue history模式必须加这句 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传图片的访问映射 location /images/ { alias /data/images/; # 图片存在服务器的这个目录下 } }

这里有两个必须理解的配置点:

  • try_files $uri $uri/ /index.html;:Vue Router开了history模式(URL不带#号)时,前端路由在服务端其实不存在对应文件。用户访问/product/1直接刷新,Nginx找不到这个文件就会404,这行配置把所有找不到的文件路径都回退到index.html,由前端路由接管。没有这行,刷新必挂。
  • location /api/代理到后端:前端请求/api/product/list,Nginx把请求转发到127.0.0.1:9090/product/list(注意proxy_pass末尾的/会替换掉匹配的/api/前缀)。这样浏览器访问的是同一域名,不存在跨域问题,生产环境根本不需要CORS配置。

构建命令和执行过程:

# 前端项目目录 npm run build # 产物在 dist 目录 ls dist/ # dist/index.html dist/static/... # 部署到Nginx目录 sudo cp -r dist/* /usr/share/nginx/html/furniture-mall/ # 重新加载Nginx sudo nginx -s reload

部署完成后访问http://服务器IP/,看到首页就说明前端起来了,点一个商品加入购物车,如果数据正常返回,说明前后端链路通了。

4.4 前后端联调与生产环境配置

联调阶段经常出现本地好好的、上线就坏的情况,排查顺序很重要。我先说一个最容易翻车的点:环境配置分环境。

你别把开发环境的数据库密码、JWT密钥写死在application.yml里传到服务器。正确做法是:

# application-dev.yml spring: datasource: url: jdbc:mysql://localhost:3306/furniture_mall_dev password: dev_password # application-prod.yml spring: datasource: url: jdbc:mysql://你的生产库地址:3306/furniture_mall password: prod_password

启动时指定环境:

java -jar app.jar --spring.profiles.active=prod

另一个生产环境必踩的坑是上传图片的路径。本地开发随便存到项目resources下就行,但jar包运行后,往jar内部写文件要么不生效要么重启丢失。正确做法是在配置里指定外部路径:

upload: path: /data/images

然后写一个静态资源映射配置类,把/images/**映射到磁盘目录:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceLocations("file:" + uploadPath + "/"); } }

这样商品图片传到/data/images/xxx.jpg,前端访问/images/xxx.jpg,Nginx再把这个路径映射到磁盘,中间没有任何jar包内部写文件的坑。

5. 常见问题与排查技巧实录

5.1 跨域问题:前后端联调第一道坎

现象:前端localhost:8080访问后端localhost:9090接口,控制台报错Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy。

原因:浏览器同源策略拦截。不过有一个容易误解的点——跨域是浏览器的行为,不是服务器的行为,后端接口其实正常返回数据了,但浏览器不让JS读取。

我项目里给SpringBoot加一个CORS配置类解决:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意事项:如果allowedOriginPatterns配了*,前端请求带token时浏览器会先发一次OPTIONS预检请求,这里allowCredentials(true)和allowedOriginPatterns("*")同时使用,在浏览器中会被判定为非法配置(因为*太宽泛),需要把*换成具体的前端地址http://localhost:8080才不报错。这是很多新手照着教程配好之后依然报跨域的原因。

5.2 PageHelper分页插件的两大坑

分页用MyBatis最流行的插件PageHelper,使用本来很简单:

PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.selectProductList(query); PageInfo<Product> pageInfo = new PageInfo<>(list); // pageInfo.getList() 就是分页数据,getTotal()是总数

但有两个坑我踩过好几次:

坑一:startPage和查询必须紧邻。startPage开启一个线程本地变量,如果不马上执行查询,这个变量不会自动清除,可能导致下一次查询被意外分页。所以在PageHelper.startPage()和mapper查询之间绝对不能插入任何其他DB操作,否则分页条件就串了。

坑二:多表JOIN时分页不准。如果SQL里有LEFT JOIN,PageHelper会自动在SQL外面包一层SELECT COUNT(0) FROM (原SQL) tmp_count来统计总数,但商品表关联图片表时一对多会产生重复行,导致总数偏大。解决方法是:在select语句里用DISTINCT关键字,或者干脆子查询先聚合再关联,比如商品主查询里不直接JOIN product_image,而是把图片URL做成子查询。

5.3 MyBatis缓存导致的幽灵数据

现象:后台修改了商品价格,前台刷新还是旧价格;重启项目就好了,跑一段时间又复发。

原因:这是MyBatis二级缓存造成的。在单个Mapper的XML里配置了<cache/>开启二级缓存后,同一Mapper的查询结果会被缓存到内存。如果你在管理端直接执行update修改了商品数据,而查询缓存的key没有同步失效,就会返回脏数据。

排查方式:看MyBatis日志里开了Cache Hit Ratio(缓存命中率统计),命中率高且数据不对,基本就是缓存问题。

这个项目我的建议是:学习阶段直接去掉<cache/>配置,不需要做缓存优化。等到系统真的出现性能瓶颈,再去考虑Redis做热点数据缓存,那是一个完整独立的优化专题。MyBatis缓存是个看着简单实际上容易踩坑的东西,没必要在小项目里用它。

5.4 Vue Router history模式刷新404

现象:本地npm run serve一切正常,点击页面跳转也没事,但部署到Nginx后,在/product/1这种二级路径刷新,页面404。

原因:/product/1在Nginx里找不到对应的静态文件,Nginx直接返回404。

解决办法前面已经提到了,location /块里加try_files $uri $uri/ /index.html;。我再补充一个注意点:如果前端项目不是部署在根路径,而是部署在子路径(比如/mall/),那Nginx配置、Vue的base配置、路由的createWebHistory(process.env.BASE_URL)要三者保持统一,否则资源路径全部错乱。

5.5 数据库连接与中文乱码排查

先给个快速自查表:

症状大概率原因解决
页面表单提交中文乱码数据库连接URL没有characterEncoding=utf8在JDBC URL加参数
数据库表中查询乱码建表时字符集不是utf8mb4ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4
JSON返回乱码SpringBoot没有配置Jackson编码确保spring.http.encoding.force=true
控制台中文乱码IDEA控制台编码问题IDEA设置里File Encodings全改成UTF-8

还有一个特别隐蔽的坑:MySQL的sql_mode里如果有ONLY_FULL_GROUP_BY,某些GROUP BY查询会报错,比如商品列表里想按分类聚合统计数量,但SELECT字段包含非聚合列。换个思路改成子查询,或者合理修改sql_mode,别硬刚。

5.6 前端启动常遇的Node版本和依赖安装问题

前端项目clone下来,npm install步骤经常出幺蛾子:

  • 安装卡在node-sass:node-sass是Vue2老项目的经典痛点,它需要下载二进制文件,在国内网络经常超时。我建议你把样式方案降级,把less/sass都换成纯CSS,或者使用dart-sass替代node-sass。
  • 报错Maximum call stack size exceeded:这是Node版本过高导致的,node-sass等老依赖与高版本Node不兼容。换Node 16即可。
  • .env环境变量没生效:Vue CLI的.env.development只在npm run serve时加载,.env.production只在npm run build时加载,别把开发变量写到production文件里。

6. 个人实操心得与拓展建议

6.1 从这个项目往后延伸的三种方向

先把基础版本跑通、能改、能部署,然后你可以根据自己的目标做三种方向的延伸。

方向一:面试突击型。把高并发相关基础补上:Redis缓存商品热点数据、秒杀场景用Redis预扣库存、消息队列削峰。核心不一定要真的实现,但至少要在简历和面试里能讲清楚:商城的哪个模块存在热点问题、你的方案思路是什么。这个项目就是你的“弹药库”,所有场景都能落地到一个真实模块上。

方向二:商业接单型。把它改造成一个可交付的多商户商城,把商品模型扩展成“店铺+商品”,订单增加售后流程,后台加权限管理(不同管理员不同权限),支付接入微信/支付宝。说实话不少外包商城的原始版本就是我描述的这种结构,你在上面做加法减轻了从零开始的巨大工作量。

方向三:技术深造型。把单体拆成微服务:SpringCloud Gateway做网关、Nacos做注册中心、用户/商品/订单三个独立服务、分布式事务用Seata、统一日志用ELK。这个项目源码足够清晰,拆起来会比较顺,因为表结构和业务边界本来就划分得干净。

6.2 最后几条实在的建议

根据我反复跑这个项目的实际体验,整理几条可能会帮你少走弯路的建议:

  • 先跑通再读代码。别一开始就纠结某一行代码为什么这么写。先把环境搭起来、项目跑起来、走一遍“注册→登录→加购物车→下单→后台发货”的完整流程,你对系统的体感会完全不一样。带着问题读代码,效率翻倍。
  • 改代码前先备份数据库。这个项目的SQL脚本虽然可以反复执行,但如果你已经手动改了表结构,重跑脚本可能报错。养成习惯:动手前mysqldump导出一份备份,出问题随时回滚。
  • 接口调试用Postman或Apifox。前端页面没调通之前,先用工具测后端接口。比如验证登录接口返回的token能不能通过拦截器、商品搜索接口的排序参数是否生效。工具先把后端验证完,再联调前端,出问题时定位范围小很多。
  • 保留一个全量SQL初始化文件。每次修改表结构或者新加测试数据,都同步更新到SQL初始化脚本里。这样不管是换电脑、换服务器、给同学部署,一条命令还原整个环境,不用靠“经验”去手动补数据。

最后再分享一个小技巧:项目里所有的配置文件都尽量用url和密码分离的写法,application.yml里不要写死具体环境变量,统一通过--spring.profiles.active=xxx和@Value注入。这样代码在哪个环境部署都不用改源码,只需要改启动参数和环境变量,前后端分离项目的部署灵活性就是这么体现出来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 6:57:48

共享厨房租赁系统开发实战:Spring Boot+MyBatis设计核心

做共享厨房租赁这个选题的毕设&#xff0c;在很多老师眼里可能觉得就是个普通的管理系统&#xff0c;但我自己做完一遍之后最大的感受是&#xff1a;业务逻辑的深度决定了这个项目的上限。技术框架确实就是Spring Boot那一套&#xff0c;难点全在“租赁”这两个字——怎么设计订…

作者头像 李华
网站建设 2026/9/26 6:57:30

GPT Images 2.5 与 AI 自主推进工作:从图像理解到任务闭环的工程实践

1. 从“ZHO 用 GPT Images 2.5 演示 AI 自主推进工作”说起&#xff1a;这个演示到底在讲什么第一次看到“ZHO 用 GPT Images 2.5 演示 AI 自主推进工作”这个标题&#xff0c;我脑子里冒出来的第一个念头不是“又一个模型更新”&#xff0c;而是“自主推进”这四个字。模型迭代…

作者头像 李华
网站建设 2026/9/26 6:57:12

网络安全越来越难干?从漏洞挖掘到AI安全的破局思路

前几天在安全群里看到一条吐槽&#xff0c;大意是&#xff1a;“现在挖个漏洞是真难&#xff0c;平台给的币越来越少&#xff0c;审核越来越严&#xff0c;动不动就给你标个重复。”底下跟了一串1。我自己的感受其实也差不多——入行那会儿和现在&#xff0c;完全就是两个世道。…

作者头像 李华
网站建设 2026/9/26 6:57:09

进程间通信管道详解:匿名管道与命名管道原理及实践

从实际开发的角度讲&#xff0c;今天聊一个老生常谈但是又特别容易踩坑的话题&#xff1a;进程间通信之管道&#xff0c;也就是匿名管道和命名管道。不管你是写Linux后端服务、嵌入式程序&#xff0c;还是做系统工具&#xff0c;只要涉及多进程协作&#xff0c;"进程间通信…

作者头像 李华
网站建设 2026/9/26 6:57:07

Django与Flask混合开发:新能源S店保养管理系统实战

前阵子帮本地一家新能源品牌的S店把保养业务从“微信群接龙纸质工单”整顿成了线上管理系统。这个系统本质上就是一个基于Python的Web管理平台&#xff1a;主业务用Django&#xff0c;辅助实时服务用Flask&#xff0c;把预约、接车、派工、施工、质检、结算和保养提醒全部串成了…

作者头像 李华
网站建设 2026/9/26 6:56:37

基于Spring Boot+Vue的高校教育资源共享平台完整实现方案

在高校里做资源共享平台&#xff0c;最麻烦的从来不是代码&#xff0c;而是“资源分散”这件事本身。最近帮一位学弟完整实现了一个基于Spring Boot Vue的前后端分离高校教育资源共享平台&#xff0c;从需求梳理、数据库设计、接口开发&#xff0c;到前端联调、Docker部署&…

作者头像 李华