家电销售展示平台这类项目,在初学者和毕业设计里一直热度很高,原因其实很简单:它把电商系统的主干流程完整地走了一遍,从商品展示到购物车再到订单结算,所有经典功能都有,技术栈又刚好踩在市场上最主流的SpringBoot+Vue这个组合上,拿出来无论是写进简历还是用于答辩,都能讲出实际内容。这次要拆解的这个基于SpringBoot+Vue、后端用Java+MySQL+MyBatis完成的家电销售展示平台管理系统,我会直接把整个项目从设计思路到表结构、接口实现、前端联调、部署排查全部过一遍,哪些地方容易踩坑、哪些代码值得逐行细看,都会结合我的实际经验讲清楚。适合正在做类似系统的人参考,也适合想让代码质量再往上走一个台阶的同学对照检查。
1. 项目定位与整体设计思路
1.1 家电销售场景的特点决定了系统要做什么
家电类商品和普通日用品有个非常大的区别:商品单价高、规格属性多、决策链路长。一台冰箱可能有容量、能效等级、面板材质、制冷方式等多个维度的参数,用户在购买前一定会反复对比,这就决定了展示平台不只是做一个商品列表那么简单。
这个系统的核心目标可以拆成三个层面:
- 展示层面:商品信息必须结构化呈现,分类导航要清晰,商品详情页要能承载足够多的参数信息,图片展示要有轮播效果。
- 交易层面:购物车、订单提交、库存扣减这些基础电商流程要闭环,用户可以完成从"加购"到"下单"的完整操作。
- 管理层面:后台需要能维护商品分类、上下架商品、处理订单状态,否则整个系统只是一个静态展示页,不配叫"管理平台"。
把这三个层面落在具体技术上,就是一个典型的前后端分离架构:Vue负责用户界面交互,SpringBoot提供RESTful接口,MySQL存业务数据,MyBatis负责数据库操作层。这个组合的市场占有率很高,遇到问题搜解决方案非常方便,这也是选择这个技术栈的一个重要原因。
1.2 技术选型:为什么是SpringBoot + Vue + MyBatis + MySQL
先看后端。SpringBoot现在的地位不需要多解释,它通过自动配置把Spring家族里那些繁琐的XML配置全部简化掉了,项目启动就是一个main方法的事情。对于这种管理系统,SpringBoot自带的Spring MVC可以很干净地处理接口路由和参数绑定,同时它和Spring Security、Redis这些周边生态无缝衔接,后续想加登录认证、缓存功能都很自然。
MyBatis在这个项目里的优势要细说。它不像JPA那样通过实体类自动映射表结构,而是把SQL写出来,由开发者完全掌控。家电商品表的字段通常比较多,查询条件组合也多——价格区间、品牌、能效等级、分类ID、关键字,这种多条件筛选场景用MyBatis的动态SQL<if>标签来拼查询条件,比用JPA的 Specification 要直觉得多。另外,订单生成时涉及多表关联查询,MyBatis通过自定义SQL能精准控制每一条语句的执行效率。
前端选Vue的原因更直白。Vue 2或者Vue 3配合Element UI组件库,能在很短的时间内搭建出后台管理界面和前台展示页面,组件复用和响应式数据绑定让页面状态管理变得非常省心。再加上Axios做HTTP请求,整个前端工程的组织方式清晰且容易上手。
MySQL在数据存储层面的角色就不用多说了,开源免费、稳定可靠,对这种中小规模的业务系统绰绰有余。InnoDB存储引擎支持事务和行级锁,正好覆盖订单生成和库存扣减这两个对数据一致性要求高的场景。
1.3 项目整体模块划分
我在设计这类系统时,习惯先把功能模块图在脑子里画出来再动手写代码。这个家电销售平台可以从用户侧和管理侧分别梳理:
- 用户前台:商品分类导航、商品列表(含搜索和筛选)、商品详情、购物车、订单确认与提交、订单列表。
- 管理后台:管理员登录、分类管理、商品管理(增删改查加上下架)、订单管理(查看、发货、完成)、基础数据统计。
模块划分清晰之后,后端接口的设计也跟着清晰。每一个功能点对应一组接口,Controller层只做参数接收和结果封装,Service层处理业务逻辑,Mapper层通过MyBatis操作数据库。这个分层在项目初期可能感觉多写了几个类,但等到功能迭代、出现线上问题需要排查时,你会非常感谢当初这个决定。
2. 数据库设计与核心表结构
2.1 表设计的基本原则
数据库设计是这个项目最不能省时间的环节。表结构如果乱了,后面写Mapper的时候会寸步难行。我见过太多人一上来就写代码,等到做订单功能发现需要关联查询的时候再回头加字段、改表,这个痛苦我太熟悉了。
设计的时候抓住两条主线:商品主线和订单主线。
商品主线是家电展示的基础。围绕商品,至少需要分类表、商品表、商品参数表。如果商品图片有多张,通常还需要商品图片表,或者用一个字段存多张图片的路径列表(JSON格式或逗号分隔)。对中小型项目来说,后者往往更省事,但可扩展性不如专门建表。
订单主线是交易闭环的核心。围绕订单,至少需要订单表、订单商品明细表,另外用户表则是贯穿两条主线的基石。下单时从商品表冗余一份快照数据到订单明细表这个操作,是很多初学者容易忽略的,后面我会详细说。
2.2 核心业务表的结构设计
先说商品分类表category,字段设计相当直接:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键ID |
| name | varchar(50) | 分类名称 |
| parent_id | bigint | 父分类ID,0表示一级分类 |
| sort | int | 排序值 |
| status | tinyint | 状态:1启用 0停用 |
这里有一个关键点:parent_id字段让分类可以无限层级,冰箱、洗衣机、小家电这种一级分类下面还能挂对开门冰箱、滚筒洗衣机这样的二级分类。前端展示导航栏时用一个递归组件就可以渲染整个树。
然后是商品表product,这是整个系统里字段最复杂的表,设计时多花一些心思:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键ID |
| category_id | bigint | 所属分类ID |
| name | varchar(100) | 商品名称 |
| subtitle | varchar(200) | 副标题/卖点描述 |
| main_image | varchar(255) | 主图地址 |
| detail | text | 商品详情(富文本) |
| price | decimal(10,2) | 销售价格 |
| original_price | decimal(10,2) | 原价/划线价 |
| stock | int | 库存数量 |
| sales | int | 销量统计 |
| brand | varchar(50) | 品牌 |
| energy_level | varchar(10) | 能效等级 |
| warranty_period | varchar(20) | 保修期 |
| status | tinyint | 状态:1上架 0下架 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
注意到没,我把brand、energy_level这些家电特有的属性直接做成了商品表的字段。有人可能会问,为什么不用单独的规格参数表?答案是家电产品的参数相对固定,不像服装类商品有大量SKU变体,直接把高频属性做成字段,查询时可以走索引,写Mapper也简单,对这个小项目来说是最优解。
用户表user的字段设计比较常规,核心字段是手机号、密码(MD5或BCrypt加密存储)、昵称、头像、创建时间。在这里要提醒一句,密码加密一定不要用明文,这是最基本的安全底线。
订单表order的字段要重点讨论一下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键ID |
| order_no | varchar(32) | 订单号 |
| user_id | bigint | 用户ID |
| total_amount | decimal(10,2) | 订单总金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| receiver_name | varchar(50) | 收货人 |
| receiver_phone | varchar(20) | 收货人手机号 |
| receiver_address | varchar(255) | 收货地址 |
| status | tinyint | 订单状态 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
| ship_time | datetime | 发货时间 |
| finish_time | datetime | 完成时间 |
order_no订单号这个字段,我建议用时间戳加随机数的方式生成,比如yyyyMMddHHmmss+ 4位随机数,这样可以保证单机部署下订单号基本不重复,同时在页面上展示时也足够直观。订单状态用数字枚举,0待付款、1已付款、2已发货、3已完成、4已取消,前后端对状态定义保持一致就行。
订单明细表order_item的字段同样关键:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键ID |
| order_id | bigint | 订单ID |
| product_id | bigint | 商品ID |
| product_name | varchar(100) | 商品快照名称 |
| product_image | varchar(255) | 商品快照图片 |
| price | decimal(10,2) | 成交单价快照 |
| quantity | int | 购买数量 |
| total_price | decimal(10,2) | 小计金额 |
核心思想是:product_name、product_image、price这三个字段都是下单时的快照,不直接关联实时商品数据。好处是即使后来后台把商品改名、改价甚至删除了,历史订单依然能还原当时的购买信息。这个设计对电商系统来说属于基本要求,但对初学者项目来说,能把这一层想明白并实现出来,是很大的加分项。
2.3 数据库设计的实操细节
MySQL在建表时有一些通用规范,属于"写过几次的人都会懂"的细节:
- 所有表都加
create_time和update_time字段,用datetime类型,然后用MyBatis的插入和更新自动填充。 - 主键统一用
bigint自增,不要用varchar当主键,关联查询时性能差异明显。 - 金额字段一定要用
decimal(10,2),绝对不要用float或double,浮点数算金额会出精度问题,这个坑我踩过不止一次。 - 逻辑删除比物理删除更安全。给业务表加一个
deleted字段,值为0正常、1已删除,查询时统一带上deleted = 0条件。这样误删数据还能救回来。
3. 后端接口开发与业务逻辑实现
3.1 SpringBoot项目的分层结构
后端代码的组织方式直接决定了项目的可维护性。推荐按功能模块而非技术类型来分包,比如这样:
com.example.appliance ├── controller │ ├── ProductController.java │ ├── CategoryController.java │ ├── CartController.java │ ├── OrderController.java │ └── UserController.java ├── service │ ├── ProductService.java │ ├── CartService.java │ ├── OrderService.java │ └── UserService.java ├── mapper │ ├── ProductMapper.java │ ├── CategoryMapper.java │ ├── CartMapper.java │ ├── OrderMapper.java │ └── UserMapper.java ├── entity │ ├── Product.java │ ├── Category.java │ ├── Order.java │ └── User.java ├── dto │ ├── ProductQueryDTO.java │ ├── OrderCreateDTO.java │ └── CartItemDTO.java ├── common │ ├── Result.java │ ├── ResultCode.java │ └── PageResult.java └── config └── WebConfig.javaController只负责接收前端传参和返回统一的结果对象。Service层写真正的业务逻辑。Mapper层是MyBatis的接口,对应XML文件或者注解SQL。实体类entity和数据库字段一一对应,dto则用于接口层面的数据交互,避免把Entity直接暴露给前端——比如新增商品时前端不需要传createTime,返回订单时也不需要把敏感字段带出去。
统一返回结果Result类也非常重要。我通常这样设计:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }这样前端 Axios 拦截器就可以统一处理code字段,不再需要每个接口单独判断。
3.2 MyBatis动态SQL实现多条件商品查询
商品列表页的搜索条件往往非常灵活,用户可能只选价格区间,也可能只选品牌,还可能同时选好几个条件。这种场景用 MyBatis 的动态 SQL 来处理最合适。
看一下商品查询的 Mapper XML 核心部分:
<select id="selectProductList" resultType="com.example.appliance.entity.Product"> SELECT * FROM product <where> <if test="categoryId != null and categoryId != 0"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="brand != null and brand != ''"> AND brand = #{brand} </if> <if test="energyLevel != null and energyLevel != ''"> AND energy_level = #{energyLevel} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> AND status = 1 AND deleted = 0 </where> ORDER BY <choose> <when test="sortRule == 'price_asc'">price ASC</when> <when test="sortRule == 'price_desc'">price DESC</when> <when test="sortRule == 'sales_desc'">sales DESC</when> <otherwise>id DESC</otherwise> </choose> </select>这里有几个非常重要的细节:
<where>标签会自动去掉第一个AND前缀,避免动态拼接出错。LIKE CONCAT('%', #{keyword}, '%')这个写法可以防止SQL拼接注入,不要直接写成'%${keyword}%',那个${}会被当成文本直接拼进SQL,存在注入风险。排序用<choose>做白名单校验,前端传什么排序规则就映射为对应的固定排序字段,避免把用户输入直接拼到 ORDER BY 后面。
分页处理我推荐用 PageHelper 插件,使用方法非常简单:
PageHelper.startPage(pageNum, pageSize); List<Product> productList = productMapper.selectProductList(queryDTO); PageInfo<Product> pageInfo = new PageInfo<>(productList);插件的原理是基于拦截器改写SQL,自动生成 LIMIT 语句,用起来比手动计算偏移量要省心太多。注意PageHelper.startPage后面必须紧跟第一条查询语句,中间不能有其他SQL操作,否则分页会失效。
3.3 购物车与订单生成的并发扣库存处理
购物车的实现有两个选择:存数据库表或者存Redis。对于中小型系统,数据库表就足够了,设计一个cart表,字段包含用户ID、商品ID、数量、勾选状态,用user_id + product_id做联合唯一索引。这个方案的好处是用户在不同设备上登录购物车数据都是同步的,坏处是多一次数据库查询。当然如果你的项目已经引入了Redis,购物车也完全可以存在Hash结构里,key是用户ID,field是商品ID,value是数量,这样性能更好。
订单生成是整个系统最核心的业务逻辑,必须考虑并发问题。核心代码逻辑是这样的:
@Transactional(rollbackFor = Exception.class) public Order createOrder(OrderCreateDTO dto) { // 1. 生成订单号 String orderNo = generateOrderNo(); // 2. 创建订单主记录 Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); // ... 设置收货人信息 order.setStatus(0); orderMapper.insert(order); // 3. 保存订单明细并计算总金额 long totalAmount = 0; for (CartItemDTO item : dto.getItems()) { // 查询商品并校验库存 Product product = productMapper.selectById(item.getProductId()); if (product == null || product.getStatus() != 1) { throw new BusinessException("商品已下架"); } if (product.getStock() < item.getQuantity()) { throw new BusinessException("库存不足:" + product.getName()); } // 尝试扣减库存,这个SQL是并发安全的关键 int rows = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new BusinessException("库存不足,请刷新后重试"); } OrderItem itemRecord = new OrderItem(); itemRecord.setOrderId(order.getId()); itemRecord.setProductId(product.getId()); itemRecord.setProductName(product.getName()); itemRecord.setProductImage(product.getMainImage()); itemRecord.setPrice(product.getPrice()); itemRecord.setQuantity(item.getQuantity()); itemRecord.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemMapper.insert(itemRecord); totalAmount = totalAmount + itemRecord.getTotalPrice().longValue(); } // 4. 更新订单总金额 order.setTotalAmount(BigDecimal.valueOf(totalAmount)); orderMapper.updateAmount(order.getId(), order.getTotalAmount()); // 5. 清空购物车中已下单的商品 cartMapper.deleteByUserIdAndProductIds(dto.getUserId(), dto.getProductIds()); return order; }这里最关键的是deductStock这条SQL,它是并发安全的保证:
<update id="deductStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>利用的是数据库的行锁:UPDATE语句执行时会锁定该行,另一个线程的相同更新必须等待。同时stock >= #{quantity}这个条件保证了库存不会被扣成负数。如果更新影响行数为0,说明库存不够,直接抛异常让事务回滚。这段逻辑看起来简单,但它是实践中验证过的可靠方案,比先查库存再扣减的"两步走"模式安全得多。
@Transactional注解保证了整个方法要么全部成功提交,要么全部回滚。要特别注意rollbackFor = Exception.class这个参数,不加它时Spring只在遇到运行时异常时才回滚,而BusinessException如果是受检异常就不会触发回滚,这个坑我见过很多人踩。
3.4 订单状态流转与管理端操作
订单状态的管理要谨慎。管理后台的操作无非是发货操作:把订单改成已发货状态,记录发货时间,用户端就能在订单列表中看到新的状态。我在写这类状态流转逻辑时,有一个经验:状态的变更用单独的更新语句,不要用UPDATE order SET status = #{newStatus} WHERE id = #{orderId} AND status = #{expectedCurrentStatus}。加上AND status = 期望当前状态这个条件可以避免重复操作导致的状态错乱。
4. 前端Vue页面实现与接口联调
4.1 工程结构与核心页面划分
前端的工程结构按照页面模块来组织:
src ├── api │ ├── product.js │ ├── cart.js │ ├── order.js │ └── user.js ├── router │ └── index.js ├── store │ └── index.js ├── views │ ├── home │ │ ├── Home.vue │ │ └── ProductList.vue │ ├── product │ │ └── ProductDetail.vue │ ├── cart │ │ └── Cart.vue │ ├── order │ │ ├── OrderConfirm.vue │ │ └── OrderList.vue │ ├── login │ │ └── Login.vue │ └── admin │ ├── Dashboard.vue │ ├── CategoryManage.vue │ ├── ProductManage.vue │ └── OrderManage.vue ├── components │ ├── ProductCard.vue │ ├── CategoryNav.vue │ └── Pagination.vue └── utils └── request.js前台页面用Vue Router做页面跳转,两个核心布局场景:首页和商品列表页共用顶部导航栏,商品详情页需要展示大图和多角度缩略图,购物车页面需要支持勾选、全选、修改数量这些交互。
后台管理页面用Element UI的el-table做列表展示,el-form做数据录入。商品管理页的编辑表单属于"动态表单":选中不同分类后可能需要填写不同的参数,这里可以利用Vue的v-if实现条件渲染,但需要注意每个字段的校验规则也要跟着条件动态设置,否则会出现校验不通过但页面没有提示的诡异情况。
4.2 Axios封装与路由守卫
前端和后端的交互,我建议所有请求都走统一的封装。request.js的核心逻辑如下:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:携带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }, error => { return Promise.reject(error) }) // 响应拦截器:统一处理返回结果 service.interceptors.response.use(response => { const res = response.data if (res.code === 200) { return res.data } else if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('登录已过期')) } else { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } }, error => { Message.error('网络异常,请稍后重试') return Promise.reject(error) }) export default service这样做的好处是:页面中的业务代码非常清爽,直接拿到数据不需要再解包result.data.data这种嵌套结构。401状态码统一跳转登录页,也不需要每个页面单独处理。
路由守卫方面,后台管理页需要登录才能访问,在router.beforeEach里做拦截:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path.startsWith('/admin') && !token) { next('/login?redirect=' + to.path) } else { next() } })这个重定向参数的写法有个好处:用户登录成功后会回到之前想访问的页面,体验比直接回首页好得多。
4.3 Vue组件化实现商品卡片与购物车交互
商品列表页的ProductCard.vue组件是整个前台页面的门面组件。它的核心思路是:组件内部不关心数据从哪来,只用props接收一个商品对象,然后渲染图片、名称、价格、销量和"加入购物车"按钮。父组件通过v-for循环渲染,并通过自定义事件@add-to-cart通知父组件做加购操作。
<template> <div class="product-card" @click="$emit('click', product)"> <div class="product-image"> <img :src="product.mainImage" :alt="product.name" /> <span v-if="product.stock <= 0" class="sold-out">已售罄</span> </div> <div class="product-info"> <h3 class="product-name">{{ product.name }}</h3> <p class="product-subtitle">{{ product.subtitle }}</p> <div class="product-price-row"> <span class="price">¥{{ product.price }}</span> <span v-if="product.originalPrice" class="original-price">¥{{ product.originalPrice }}</span> </div> <el-button type="danger" size="small" :disabled="product.stock <= 0" @click.stop="$emit('add-cart', product)" >加入购物车</el-button> </div> </div> </template>购物车页面的交互有三个容易出错的地方:全选逻辑、数量增减和价格同步。全选按钮的勾选状态不能用单独的变量记录,必须根据当前所有行的勾选状态计算得出。价格小计则建议用computed实时计算,保证和勾选状态联动。
购物车修改数量时,最好直接调后端的更新接口并更新购物车列表数据。有一个小坑是:用户在输入框里连续点击加号时,前端的点击事件里拿到的是修改后的数量,但此时上一次的请求还没返回,如果再点一次用的是旧的数量,会出现数量跳变。解决办法是等待接口返回后再更新本地数据,或者用防抖节流控制请求频率。
5. 部署运行与环境配置
5.1 本地运行步骤
这个项目要在本地跑起来,标准流程是这样的:
- 安装 JDK 8 或 11,配置
JAVA_HOME环境变量。 - 安装 MySQL 5.7 或 8.0,执行项目里附带的
init.sql脚本建库建表,初始化测试数据。 - 安装 Maven(IDEA 自带 Maven 也可以),在 IDEA 里导入后端项目,等待依赖下载完成。
- 修改
application.yml中的数据库连接信息,把用户名密码改成自己本机的。 - 运行
ApplianceApplication.java的main方法启动后端。 - 安装 Node.js 12+,在有
package.json的前端目录执行npm install。 - 执行
npm run serve启动前端开发服务器,默认端口是 8080,页面访问http://localhost:8080。
这里最常出问题的就是npm install速度极慢或者下载失败。解决方法是把npm源切换成国内镜像:
npm config set registry https://registry.npmmirror.com后端接口地址和前端开发服务器的跨域问题,我推荐用 Vue CLI 的代理配置解决,在vue.config.js中配置/api前缀的代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }注意我之前在Axios封装里baseURL写的是/api,后端服务的接口路径也要统一以/api开头,这样代理就能把请求转发到后端端口。运行过程中如果发现页面请求不通过,第一反应就检查这个代理配置是否生效,以及后端的context-path是否与转发路径一致。
5.2 高频报错与排查记录
我在实际调试这个项目时,整理了出现频率最高的几个问题:
问题一:MyBatis绑定异常
Invalid bound statement (not found): com.example.appliance.mapper.ProductMapper.selectProductList这个报错说明Mapper接口找到了,但对应的XML文件没有被加载。大概率是XML文件放错了位置,或者application.yml里的mapper-locations配置的路径不对。MyBatis默认只扫描classpath:mapper/*.xml,如果XML文件在src/main/java目录下且没有做资源处理,编译后不会出现在classes目录里。解决办法是把XML文件放到src/main/resources/mapper/目录下,然后配置:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.appliance.entity问题二:跨域请求被拦截
前端http://localhost:8080请求后端http://localhost:8081,浏览器控制台报CORS跨域错误。除了前面说的代理方案,也可以在后端统一处理。我习惯写一个配置类:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)和allowedOriginPatterns("*")必须搭配使用,如果写allowedOrigins("*")会发现请求带Cookie时跨域配置失效。
问题三:端口被占用
Application run failed: Web server failed to start. Port 8081 was already in use.这个最简单,改application.yml里的server.port,或者在命令行里netstat -ano | findstr 8081找到占用进程手动结束。Windows下用taskkill /pid 进程号 /f即可。
问题四:前端组件渲染不出数据
接口返回正常,页面表格却空白。最常见的原因是el-table的prop属性和后端返回字段对不上。后端如果用了@JsonProperty("product_name")或者返回的是productName而前端写成了product-name,都会出现空白。排查方法很简单:打开浏览器F12,看Network面板里的响应JSON字段名是什么,再对照页面里面的prop值去核对。
5.3 性能优化与后续扩展方向
系统能跑通只是第一步,上线后面临的问题才是更考验开发者的地方。
图片加载优化是商品展示类平台最立竿见影的优化点。家电商品的高清大图常常一张就有几百KB甚至几MB,列表页一次加载几十张图页面会卡到没法用。最简单的优化是在前端做懒加载:图片进入视口时才加载。用v-lazy指令或者直接监听滚动事件都可以。后端侧可以按需生成缩略图,比如主图展示压缩成宽400px左右的版本,详情页再展示高清大图,能明显提升列表页加载速度。
数据库查询层面,product表的category_id和brand这两个字段在查询条件里出现最多,建议加上普通索引:
ALTER TABLE product ADD INDEX idx_category_id (category_id); ALTER TABLE product ADD INDEX idx_brand (brand);如果后续用户量增长,可以考虑把商品列表接口加入Redis缓存:缓存键用分类加筛选条件做组合,缓存时间设置60秒。商品详情页变动频率更低,可以缓存5分钟。但注意每次商品上下架或者库存变化时要及时清除缓存,否则会出现库存不一致的情况。
订单关联查询和搜索功能是另外两个可以扩展的方向。数据量大之后,订单表可以按创建时间做分表,搜索功能可以引入Elasticsearch或者使用MySQL全文索引。但这些扩展对当前项目来说属于后话,需要根据实际业务规模来判断什么时候该做。
6. 一些实操中沉淀下来的看法
这个项目我前后带过几位开发者完整走了一遍,我自己也在不同环境里部署过好几次,有几点体会想分享。
第一个体会是,数据库表设计真的值得多花时间。很多人拿到需求就开始写Java代码,写到订单详情发现缺字段、写到购物车发现关联关系不对,再回来改表,还要连带改动已经写完的实体类和Mapper,那才是真正的痛苦。我现在的做法是先花一晚上把表结构设计好,画出ER图,和前端同事对齐接口字段,再开始写代码。一个好用的表结构设计,真的可以让后续编码时间缩短一半。
第二个体会是,写SQL时要多考虑代码之外的场景。比如翻页后数据有重复,多半是订单的一个子排序问题,编辑完商品不生效,先看看缓存,再检查Mapper里的更新是否有必要保留在SQL执行中替换所有的字段。这些问题遇到了才知道怎么排查,但提前心里有个底总是好的。
第三个体会是,前后端联调阶段一定要提前对齐字段命名规则。我见过太多次因为后端返回createTime,前端写create_time导致页面数据为空,双方还互相质疑对方接口有问题。建议后端统一采用驼峰命名返回JSON,前端直接使用即可。如果数据库字段是下划线风格,MyBatis配置了map-underscore-to-camel-case: true会自动转换,这个开关也不要在配置里漏掉。
最后再分享一个小的实操技巧:开发阶段把后端日志级别调到debug,重点观察MyBatis打印的SQL语句。很多业务逻辑不对的问题,看SQL基本就能定位是条件拼错了还是参数传错了。上线之后再调回info,避免日志量过大拖慢系统。
这个项目的完整代码实现,核心模块其实就是商品展示、购物车、订单、后台管理这一条线。把它吃透,SpringBoot后端接口开发、MyBatis持久层操作、Vue前端组件化这些技能点就都有了实打实的项目依托。后续如果想把项目做深,可以围绕支付接口集成、权限系统改造、商品秒杀场景优化这些方向继续延伸,那就又是另一个进阶故事了。