最近有朋友问我,家电销售展示类平台有没有一套比较完整的参考代码,说是公司里急着要做一个官网加展示后台的东西,想着从零起步又怕走弯路。我回想了一下,这几年接过不少类似的企业级项目,过程里踩过的坑还挺多的。正好我手上有一套自用的家电销售展示平台管理系统,基于 SpringBoot + Vue + MyBatis 架构,数据库用 MySQL,功能覆盖前台产品展示、分类筛选、详情介绍,后台的销售数据统计、订单管理、产品上下架,整体思路比较成熟。今天就把这套东西从设计到落地完整拆给你看,包含了技术选型逻辑、数据库建表、后端接口、前端页面、部署踩坑,全是干活时总结出来的经验。
这套系统的定位是“企业级”,并不只是单一的前端模板,而是一个前后端分离、接口风格统一、可扩展的管理平台。企业级意味着要稳定、易维护、能多人协作开发,同时还要考虑权限、异常处理、日志记录、可监控性这些不是一眼看得到但上线必须要的东西。所以我会尽量少讲表面套话,多讲“为什么要这么设计”和“实际写代码中遇到的坑”,如果你想照着做,直接抄作业也没问题。
1. 项目整体设计与技术选型思路
1.1 为什么选择 SpringBoot + Vue + MyBatis + MySQL
先说结论:这个组合是目前中小型企业管理平台里最省心、最容易被团队接受的一套。企业要的往往不是最先锋的技术,而是招人容易、资料多、出问题能快速定位的技术栈。
SpringBoot 解决了传统 Spring 配置繁琐的问题,过去做一个项目要写一堆 XML,现在一个启动类加上 auto-configuration,就能把大部分配置覆盖掉。而且它内嵌 Tomcat,打成一个 jar 包就能跑,部署成本低。对于家电展示平台这种以稳定查询、偶尔写入为核心的业务,SpringBoot 完全够用,不需要引入过于复杂的微服务体系,否则团队维护成本会高得离谱。
Vue 在前端方面最大的优势是组件化。家电展示平台的页面结构高度复用,比如商品卡片、轮播图、分页组件、后台表格,都可以抽象成组件。组件化之后前端代码整洁,也不容易出现一个页面改了、另一个页面跟着出错的低级问题。同时 Vue 对后端工程师很友好,模板语法简单,数据驱动视图,理解成本比 React 要低一些。
MyBatis 则是数据访问层的经典选择。很多人纠结到底用不用 JPA,我的建议是:如果你的项目里有复杂的多条件查询、动态排序、报表统计,MyBatis 的 SQL 控制力明显更强。家电商品筛选条件多,比如品牌、价格区间、能效等级、产品类型,这些查询组合靠动态 SQL 写出来非常直白。JPA 在这类复杂查询上要么拼接 Specification,要么写 JPQL,理解起来反而绕。
MySQL 就不用多说,开源、免费、稳定,企业级选型里最稳妥的数据库。家电平台的数据量一般不至于到需要上分布式数据库的程度,垂直拆表、合理加索引已经很够用了。
1.2 系统模块划分与核心业务流程
一个完整的家电销售展示平台,从业务上可以切成两大块:前台展示端和后台管理端。
前台展示端包括首页装饰大图、商品分类导航、商品列表、商品详情、搜索功能,以及购物车和结算相关操作。这里所谓“销售展示”并不一定要求像京东那样具备完整的在线支付,很多企业提供的是展示加询价模式,用户可以提交订单,然后销售人员在后台跟进处理。所以在设计订单表时,我会预留支付状态,但不会把支付流程作为核心阻塞项,实际接入时可以按需扩展。
后台管理端包括管理员登录、权限控制、商品管理、分类管理、轮播图管理、订单管理、销售统计。商品管理里面还要考虑上下架、库存数量调整、商品图片维护。订单管理需要支持列表筛选、详情查看、状态流转处理。
核心业务流程可以概括成:用户浏览前台商品,按照分类或关键词筛选,进入商品详情,选择规格和数量,加入购物车或直接提交订单;后台管理员登录管理界面,查看新订单、处理订单状态、更新商品信息和库存。整个链路虽然不是特别复杂,但是牵扯到用户鉴定、数据一致性、并发扣库存等问题,如果设计时不提前考虑,后面业务跑起来会很被动。
2. 数据库设计与核心表结构拆解
2.1 家电产品的数据特点与建表原则
家电产品跟普通的衣服、书籍差异挺大的,最明显的就是属性不统一。比如冰箱要标注容量、制冷方式、能效等级,电视要标注屏幕尺寸、分辨率、刷新率,空调要标注匹数、冷暖类型。如果全塞到一张表里,字段会非常臃肿,而且很多字段对其它品类是空的。
我建议的做法是采用“基础表 + 扩展表”的模式。商品主表保存公共字段,比如名称、品牌、价格、分类ID、上下架状态;商品参数表用 key-value 方式存各自特有属性。这样既满足展示需求,又不会导致主表字段爆炸。实际开发中很多人一开始舍不得拆表,后期加属性就要改表结构,非常痛苦,这点一定想清楚。
建表原则还有一条:金额相关字段一律用 decimal,不要用 float 或 double。家电客单价高,比如一台冰箱几千块钱,浮点数运算会出现 0.1 加 0.2 不等于 0.3 的情况,金额出现精度错是绝对不能容忍的。
2.2 关键表结构与字段设计示例
这里我贴几张核心表的精简建表语句,字段都经过实际项目验证,可以照着抄。
商品分类表category:
CREATE TABLE `category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID', `parent_id` bigint(20) DEFAULT '0' COMMENT '父分类ID,0表示顶级', `name` varchar(50) NOT NULL COMMENT '分类名称', `sort` int(11) DEFAULT '0' COMMENT '排序号,越小越靠前', `status` tinyint(1) DEFAULT '1' COMMENT '状态:1启用,0停用', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';这里parent_id的设计支持二级分类,比如“家用电器 -> 冰箱”,也可以扩展到多级。实际业务中一般两级就够了,再多的话前端导航会很难排版。
商品主表product:
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `category_id` bigint(20) NOT NULL COMMENT '分类ID', `name` varchar(100) NOT NULL COMMENT '商品名称', `sub_title` varchar(200) DEFAULT '' COMMENT '副标题/卖点', `main_image` varchar(255) DEFAULT '' COMMENT '主图路径', `price` decimal(10,2) NOT NULL COMMENT '销售价', `original_price` decimal(10,2) DEFAULT '0.00' COMMENT '原价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `status` tinyint(1) DEFAULT '1' COMMENT '状态:1上架,0下架', `brand` varchar(50) DEFAULT '' COMMENT '品牌', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品主表';我特意在category_id和status上加了索引。因为前台列表页最频繁的查询就是按分类筛选且状态为上架的商品,索引能直接命中,走不了全表扫描。
订单主表orders:
CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(1) DEFAULT '0' COMMENT '订单状态:0待处理,1已确认,2已完成,3已取消', `receiver_name` varchar(50) NOT NULL, `receiver_phone` varchar(20) NOT NULL, `receiver_address` varchar(255) NOT NULL, `remark` varchar(500) DEFAULT '', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';订单编号要唯一,这是硬性要求。我一般用日期加随机数再塞一个自增因子进去,确保并发下不会重复。
订单明细表order_item:
CREATE TABLE `order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `product_name` varchar(100) NOT NULL, `product_price` decimal(10,2) NOT NULL, `quantity` int(11) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';订单明细一定要保存商品名称和下单时的价格快照,不能只关联商品 ID。因为商品价格可能调整,等三个月后看历史订单,你总不能让用户看到的是现在的价格。
这种表和字段的设计比较常规,但对于家电销售平台来说足够清晰了。后端写 SQL 的时候,多表关联逻辑不会太复杂,性能也好控制。
3. 后端开发与核心接口实现
3.1 SpringBoot 项目初始化与分层结构
后端我用标准的四层结构:controller、service、mapper、entity。Controller 负责接收前端参数并返回统一结果,Service 写业务逻辑,Mapper 层写数据库交互,Entity 对应表结构。很多人觉得分层麻烦,但项目一复杂,这几层能有效避免改动一个地方影响全局。
项目初始化建议直接用 Spring Initializr 生成,依赖选择 Spring Web、MyBatis、MySQL Driver、Lombok。版本号方面,如果你不追求新特性,就不要乱升级,SpringBoot 2.7.x 是目前市面上教程最多、问题反馈最全的版本。用新版本虽然安全但对初学者不友好,遇到奇怪的报错半天查不到资料。
启动类的结构很简单,主要提一下包结构:
@SpringBootApplication @MapperScan("com.demo.mapper") public class SalesPlatformApplication { public static void main(String[] args) { SpringApplication.run(SalesPlatformApplication.class, args); } }@MapperScan一定不要忘,否则 Mapper 接口无法被绑定到 Spring 容器里。开发中最常见的问题之一就是忘了加这个注解,启动时报差找不到 Mapper。
统一返回结果类建议放到独立位置,所有接口都复用,代码会干净很多。我用这个结构:
@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.msg = "操作成功"; result.data = data; return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.code = 500; result.msg = msg; return result; } }好处就是前端拿到响应后只需判断code是否为 200,不需要针对每个接口特殊处理。
3.2 基于 MyBatis 的数据访问层开发
MyBatis 开发中,我最常用的是 XML 文件来写复杂 SQL,而不是注解。注解适合简单查询,但一旦涉及动态 SQL,注解里写<script>标签既不美观还容易出错。XML 文件的自动提示比较完善,动态 SQL 拼接也更清晰。
比如商品列表分页查询,前端可能传分类ID、品牌、价格区间、排序方式,这时候就是一个典型的多条件查询。我在 SQL 里这么写:
<select id="selectProductPage" resultType="com.demo.entity.Product"> SELECT id, category_id, name, sub_title, main_image, price, brand, stock, status FROM product <where> status = 1 <if test="categoryId != null and categoryId != ''"> AND category_id = #{categoryId} </if> <if test="brand != null and brand != ''"> AND brand = #{brand} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> </where> <choose> <when test="sort == 'price_asc'"> ORDER BY price ASC </when> <when test="sort == 'price_desc'"> ORDER BY price DESC </when> <otherwise> ORDER BY id DESC </otherwise> </choose> </select>注意<=在 XML 里不能直接写小于号,必须转义成<,这个是新手高频报错点。<where>标签会自动去掉前缀多余的AND,比手动写 where 1=1 要优雅得多,还能避免索引失效的问题。
3.3 核心业务接口示例
以提交订单为例,我把整个接口的实现流程拆一下。
Controller 层:
@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/submit") public Result<?> submit(@RequestBody OrderSubmitDTO dto) { orderService.submit(dto); return Result.success(null); } }Service 层是整个流程的核心,简化版如下:
@Transactional public void submit(OrderSubmitDTO dto) { // 1. 生成订单编号 String orderNo = generateOrderNo(); // 2. 创建订单主记录 Orders order = new Orders(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); order.setTotalAmount(...); order.setReceiverName(dto.getReceiverName()); order.setReceiverPhone(dto.getReceiverPhone()); order.setReceiverAddress(dto.getReceiverAddress()); order.setStatus(0); orderMapper.insert(order); // 3. 保存订单明细 & 扣库存 for (OrderItemDTO item : dto.getItems()) { OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setProductName(item.getProductName()); orderItem.setProductPrice(item.getProductPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); int count = productMapper.reduceStock(item.getProductId(), item.getQuantity()); if (count == 0) { throw new BusinessException("库存不足"); } } }这里必须加@Transactional注解,因为主表插入、明细插入、库存扣减三个动作要么全部成功,要么全部失败。如果在插入明细成功后扣库存失败,程序抛异常时会有事务回滚,订单不会变成半残废状态。
扣库存的 SQL 也不能大意。以前很多新手喜欢先 select 库存,在 Java 里判断再 update,这种写法在高并发下一定会超卖。正确操作是直接在 SQL 里做原子扣减:
<update id="reduceStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity} </update>这样只有当库存剩余数量满足扣减量时才会更新成功,update 返回 1 表示扣减成功,返回 0 表示库存不足。把并发的问题压到数据库层解决,比在应用层加锁要可靠得多。
4. Vue 前端设计与业务对接
4.1 前端工程结构与路由设计
前端我用的是 Vue 2 + Vue CLI 的经典组合,虽然 Vue 3 已经普及,但企业里存量项目很多还是 Vue 2,这套代码对多数人有直接参考价值。如果新项目,用 Vue 3 + Vite 也行,思路一致。
前端工程结构大致如下:
src ├── api/ # 封装所有后端接口请求 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 全局状态管理 ├── views/ # 页面组件 │ ├── home/ │ ├── product/ │ ├── cart/ │ └── admin/ ├── utils/ # 常用工具方法 └── main.js路由部分核心是区分前台和后台,后台需要登录权限。我用路由守卫做拦截:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('admin_token'); if (to.meta.requiresAuth && !token) { next('/admin/login'); } else { next(); } });只要后台路由配置里加meta: { requiresAuth: true },未登录用户就会被踢到登录页面。这个方法简单高效,适合管理端。
4.2 商品展示页与购物流程的关键实现
商品展示页最重要的是列表渲染和筛选交互。列表我用 Element UI 的分页组件:el-pagination,切换页码时调用后端接口,把当前的分类ID、品牌、价格区间、排序方式当作参数传给后端。这里要注意一个问题:筛选条件变化时必须把页码重置为 1,否则用户处于第 5 页时重新筛选,会发现页面空白,因为第 5 页可能已经没有数据了。
商品详情页要处理图片展示和规格选择。家电的图片通常比较多,我会用图集模式,主图下面一排缩略图,点击切换。规格选择如果只是简单数量选择,本地 state 处理即可;如果有颜色、容量等维度,建议用动态渲染多个规格组,用户勾选后再拼成 SKU 标识,向后端查询库存。
购物车我建议存在前端localStorage,同时在后端也保存一份关联用户ID的购物车数据。这样局部清缓存不会彻底丢了用户购物车,登录后还能同步。很多企业平台为了先上线,只做前端本地购物车,但用户换个设备购物车就不见了,体验很糟糕。
axios 请求封装是比较容易被忽略的点。我在utils/request.js里统一设置 baseURL、超时时间、请求拦截器和响应拦截器。响应拦截器里统一处理 401 跳转登录、业务码错误提示等。这样一来页面里的请求代码很简洁,不会每个页面重复写错误处理。
5. 部署上线与性能优化实录
5.1 本地联调与打包部署
前后端项目开发完,联调阶段最容易出问题的是跨域。前端跑在 8080,后端跑在 8081,直接请求必然跨域。我的处理方式是在后端写一个全局 CORS 配置,而不是在前端配代理,因为生产环境前端资源放在 Nginx 下,后端接口不在同一个域名时,后端允许跨域才是最终解决方案。
一个简单的配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*"); } }联调没问题后开始打包。后端执行mvn clean package -DskipTests,生成 jar 包;前端执行npm run build,生成 dist 目录。部署时我用 Nginx 托管前端静态文件,同时用反向代理转发/api请求到后端服务:
server { listen 80; server_name your-domain.com; root /var/www/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files是为了解决前端路由刷新 404 的问题。如果不加这行,刷新/admin/product页面时 Nginx 会去查找对应文件,找不到就报 404,加了之后会回退到 index.html,交给前端路由去处理。
5.2 常见性能瓶颈与优化方案
家电展示平台的性能瓶颈通常集中在商品列表查询和商品详情查询,这两个接口是高频低返回,压力大。
第一优化点一定是 SQL 加索引。前面建表时我已经加了category_id和status索引,实际查询时还要通过EXPLAIN验证索引是否生效。特别注意不要在WHERE条件字段上做函数运算,比如WHERE status = 1没问题,但如果写成WHERE DATE(create_time) = '2024-01-01',索引就会失效。
第二优化点是加缓存。项目初期可以直接用 Spring 自带的 cache,或者简单引入 Redis。对于商品列表数据,可以把整个页面的 JSON 字符串缓存到 Redis,设置五分钟过期。这样同一时间多人刷新首页时,后端不用每次都查数据库。这个策略尤其适合首页大图、热销榜单这类数据变化不大的场景。
第三优化点是前端图片懒加载。家电商品图都是高清大图,如果页面一次性渲染几十张,流量和渲染压力都不小。用 Vue 指令或者 Element UI 自带的lazy属性,让图片滚动到视口附近再加载,首屏速度会有明显提升。
优化这事不要一上来就上分布式缓存或读写分离,先把 SQL 和静态资源的成本降下来,大部分问题都能解决。杀鸡用牛刀只会增加维护成本。
6. 常见问题与实战避坑清单
6.1 环境与启动问题
开发时遇到最多的就是本地启动失败。端口被占用,提示Port 8080 was already in use,解决方式是改后端配置文件端口,或者找到占用进程杀掉。还有一个藏在深处的问题:JDK 版本和 SpringBoot 版本不匹配,比如 JDK 17 跟 SpringBoot 2.6 以下版本会有兼容性问题,会导致启动直接报错。建议 JDK 8 配 SpringBoot 2.x,JDK 17 配 SpringBoot 3.x。
MySQL 连接失败也非常常见。大部分情况是配置里的用户名密码错误,或者驱动版本问题。注意 SpringBoot 2.x 默认用的 MySQL 驱动类是com.mysql.cj.jdbc.Driver,需要引入对应版本的 mysql-connector-java,而且 URL 里要带上serverTimezone=Asia/Shanghai,否则会报时区错误。
6.2 后端与数据库常见坑
我整理一个高频问题速查表,方便你直接对照:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
Invalid bound statement | Mapper XML 文件没扫描到 | 检查mapper-locations路径,XML 要放到 resources 下对应目录 |
| 查询字段返回 null | 实体字段与数据库列名没映射 | 开启map-underscore-to-camel-case: true,或者写 resultMap |
| 插入数据后获取不到自增主键 | 没配置useGeneratedKeys | XML 中设置useGeneratedKeys="true",keyProperty 对应实体属性 |
| 金额出现 0.99999 | 字段类型用了 float/double | 改为 decimal(10,2),Java 里用 BigDecimal |
| 并发扣库存超卖 | SELECT 判断后 UPDATE | 使用stock >= #{quantity}条件原子更新 |
还有一个值得单独强调的点:MyBatis 中动态 SQL 里的<if test="brand != null and brand != ''">,如果你传的参数本身是" "空格,它不会判断为空,查询时就会带一个空字符串条件导致结果异常。前端保证 trim,后端也要做防御。
6.3 前端与联调常见坑
前端最崩溃的问题是接口请求成功了但页面不更新。排查思路先看控制台,再看 Network 里响应结构是否正常。有时候后端返回的data是一个数组包在code里面,前端却拿res.data.list,自然渲染不出来。要养成先打日志看结构的习惯。
另一个常见问题是列表更新后刷新页面数据没变化。这基本是浏览器缓存导致的 GET 请求缓存。解决方式可以在 axios 的 GET 请求里加一个时间戳参数,或者请求头设置Cache-Control: no-cache。尤其是后台管理页面修改商品后,回到列表还看到旧数据,很影响体验。
还有分页组件触发的坑:el-pagination的current-change事件只在当前页改变后触发,如果当前页是第 1 页,你点上一页按钮它不会触发事件,需要额外监听prev-click和next-click,或者用size-change配合处理。这类小细节写的时候不好发现,测试一多就暴露了。
7. 从重构到扩展的思路备忘
这套系统做完之后,后续扩展方向一般集中在两块:一个是营销功能,一个是数据服务。营销功能比如优惠券、限时秒杀、拼团,核心在订单模块上增加促销字段和优惠明细,商品模块增加活动价和活动库存。数据服务则是把销售统计报表做得更丰富,按品牌、品类、时间段等维度做销售排行。
我个人推荐在项目稳定后,把后端接口规范统一整理成文档,尤其是给前端调用时容易混淆的状态码和字段含义。很多项目后期维护困难,并不是代码写得多差,而是接口状态码语义混乱,同一个业务错误有时候返回 200,有时候返回 500,前端根本没法统一处理。
最后还有一点建议:源码和技术框架只是基础,真正决定项目好不好用的是业务细节。家电销售平台看着简单,但仔细想想,光商品参数、展示模板、订单状态流转这些点就够琢磨一阵子。如果你准备照着这个思路做自己的项目,最好先把业务流程画清楚,再写代码,别急着动手。先把 MySQL 表建好、后端接口理通,前端页面是水到渠成的事。我写这套系统时最深的体会就是:瓶颈往往不在技术难度,而是对业务的理解深度。