下载过项目源码的人,大多有过这种体验:页面上明明白白写着“可直接运行”,拉下来之后却连环境都凑不齐,跑起来更是到处报错。但这个家电销售展示平台信息管理系统,我在干净的Windows和Linux环境里各跑了一遍,按正常顺序操作,不到半小时就能在浏览器里看到完整页面,技术栈就是标题里写的这三样:SpringBoot后端 + Vue前端 + MySQL数据库。这是最标准的Java全栈组合,也正是因为标准,当你遇到问题时随便一搜就是现成答案。这是这个项目适合学习和二次开发的最大优势。
它解决的问题很具体:一个家电品牌或者区域经销商,既需要对外展示电视、冰箱、空调、洗衣机这些商品,让顾客能浏览、加购物车、下单,又需要一个内部后台去录商品、调库存、处理订单。这套系统就把两个场景做进了同一个项目里,前台负责成交,后台负责运营,业务数据统一落到MySQL。不管你是正在做课程设计的学生,还是想学前后端分离开发的初学者,或者公司内部需要一个能快速跑起来的销售管理原型,都可以拿它当起点。
1. 这套家电销售平台到底在解决什么问题
1.1 前台展示与后台管理一体:一台设备、两个视角
很多刚接触这类项目的人会问,这不就是个电商系统吗?是,但“家电销售”和“普通电商”放在一起,侧重点不太一样。家电这类商品有几个特点:型号多、参数杂、单价高、库存变动频繁。顾客下单之前要反复对比参数,运营人员要经常调整价格和上下架状态,还要处理订单发货。如果只做一个静态展示页面,商品信息一改就要单独找开发,完全不现实。如果只做后台管理,顾客又看不到商品,没有任何销售转化能力。
所以这套系统的核心定位就是“一个项目解决两个端的问题”。用户打开前端页面看到的是一个完整的商城:轮播图、分类导航、商品列表、详情页、购物车、结算页。运营人员打开后台看到的是另一套界面:商品维护、分类管理、订单处理、用户管理、数据看板。两边共用同一套SpringBoot接口,数据全部读写在MySQL里。登录时用户的角色决定了你能访问哪些功能,普通用户看不到后台入口,管理员也主要在处理运营数据。
这种结构对中小规模业务来说是最合理的。不需要把前后台拆成两个独立系统部署,维护成本低,一个人改后端代码两边都同步生效。等到规模真做大了,再把管理端单独拆出去也不迟,因为接口本来就是按模块分的。
1.2 为什么技术栈偏偏是SpringBoot+Vue+MySQL
技术选型不是越新越好,而是要适合场景。这套系统选SpringBoot做后端,本质上是看中它的“开箱即用”。内嵌Tomcat,不用单独配置服务器;Spring MVC处理路由、参数绑定,开发接口效率高;Spring Data和MyBatis这类持久层框架把数据库操作也收拢得很好。对一个以CRUD为核心的管理系统来说,SpringBoot就是最稳妥的底座。
Vue的优势在交互层。商品列表需要根据分类、价格、关键词动态刷新,购物车要实时计算总价,后台管理需要各种弹窗表单,这些场景Vue的响应式数据绑定天然合适。组件化的写法让首页、商品详情、购物车这些页面可以拆成独立模块,后期要加需求只动对应组件就行。
MySQL对这笔数据来说是够用的。商品、用户、订单、购物车之间有着明确的关系,订单要关联用户,订单明细要关联商品,这种结构化数据用关系型数据库管理最顺手,而且MySQL的部署、备份、迁移成本都低。技术上三者搭配还有一个隐性优势:资料足够多。随便一个报错信息扔到搜索引擎里,基本都能找到对应解决方案。
对比之下,如果你用JSP+Servlet做这个项目,前台页面和后端逻辑耦合在一起,改样式都可能影响业务流程,维护体验很差。如果用微服务架构,光注册中心、网关、配置中心这几个组件就够折腾几天,对单个系统的业务量来说有点过度设计。SpringBoot+Vue+MySQL是“刚刚好”的组合。
2. 功能拆解:家电销售场景下,系统里都藏着哪些模块
2.1 前台商城:顾客能摸到的每个页面
前台的逻辑是围绕“从浏览到下单”这条路径展开的。首页通常由轮播图、分类入口、热销商品三个板块组成,轮播图放活动宣传,分类入口按“电视、冰箱、空调、洗衣机、厨卫电器、生活小家电”这些家电习惯分类,让用户第一时间找到目标品类。
进入商品列表页,需要支持按分类筛选、按品牌筛选、按价格区间筛选,还要能按销量、新品、价格排序。这里有一个关键细节:家电产品的品牌认知很强,用户经常直接搜“格力”“美的”“海尔”这类关键词,所以列表页的搜索不能只查商品名称,品牌字段也要纳入模糊匹配。
商品详情页是转化率的关键。除了主图、价格、库存这些基础字段,家电用户还特别在意能效等级、功率、尺寸、保修政策这些参数。所以详情页通常会有一个独立的参数区,把“品牌、型号、能效等级、额定功率、产品尺寸、重量、保修年限”这类规格逐条列出来。这个区域的字段不是写死在页面上的,而是商品表里有一个JSON或者文本字段存着,后端读取后由前端解析渲染。
购物车和结算流程就相对标准了。用户把自己看中的商品加进购物车,勾选要买的商品,生成订单,确认收货地址,提交支付。这套系统里的支付是模拟的,点击“支付成功”按钮直接变更状态,但订单状态流转是完整的:待付款、待发货、已发货、已完成、已取消。
2.2 后台管理:价格、库存、订单都在这里改
后台是运营人员的日常战场。第一个核心模块是商品管理,运营人员要能新增商品、修改价格、调整库存、上传主图和轮播图、修改详情内容、上下架商品。这里家电商品的特殊性在于图片多、参数杂。前台展示的图不是一张,而是需要轮播展示多角度图;参数也不只是标题和描述,要维护一整套产品规格。
除了商品管理,还有分类管理。分类结构往往是两级或者三级的树形结构,点击父级分类能展开子分类。订单管理模块要列出所有订单,按状态和下单时间筛选,对订单执行发货操作,异常订单可以关闭取消。用户管理负责查看注册用户列表,可以禁用恶意账号。内容管理主要维护首页轮播图和平台公告。
后台还有一个容易被忽略但很重要的部分:数据看板。一般的实现方式是显示今日订单数、今日销售额、总销售额、热销商品排行这几块核心指标。这个模块不需要复杂的报表系统,写几条聚合SQL就能跑出来,但对运营来说非常实用,可以快速感知平台整体状况。
2.3 前后台如何串起来:从浏览到发货的一条链路
如果用一句话概括整套系统的数据流,就是“用户在前台产生交易数据,运营在后台处理交易数据”。
用户浏览首页,点击进入商品列表,查看详情,把商品加入购物车,购物车记录被写入MySQL的购物车表。用户提交订单时,后端先根据购物车条目生成订单主表和订单明细表,扣减商品库存,再清空对应的购物车数据。订单生成后,前台用户中心里就能看到待付款订单。用户点击模拟支付后,订单状态变为待发货,后台订单管理列表同步出现这笔记录。运营人员发货后,订单状态变为已发货,用户中心再次同步更新。整个链路没有哪个环节需要重复录入数据,所有状态都是一次操作、两处可见,这也是“信息管理系统”这个名字的含义所在。
3. SpringBoot后端实现:登录鉴权、商品检索与订单事务
3.1 起步约定:统一返回体和全局异常处理
拿到这套源码,我建议先翻两个基础类,一个叫Result,一个叫GlobalExceptionHandler。这两个类决定了所有接口的返回格式,也决定了前端处理逻辑要怎么写。
Result类是所有Controller的返回包装,一般包含code、message、data三个字段。code为200表示成功,401表示未登录,500表示服务端异常,业务参数错误会返回其他自定义编码。前端axios拦截器统一判断code,只要不是200就走错误提示,完全不关心HTTP状态码是多少。
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.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; return result; } }配合Result的有全局异常处理类。业务运行过程中抛出BusinessException,被@RestControllerAdvice捕获后自动转成标准错误返回。这样最大的好处是Controller代码非常干净,只管正常流程,异常情况全部交给统一出口处理。这个设计看起来简单,实际是一名后端开发的基本功,很多项目写着写着代码混乱,问题就出在缺少统一返回结构和异常兜底。
3.2 登录鉴权:为什么用Token而不是Session
系统里有个登录模块,用户登录后会拿到一个token字符串,之后的请求都在Header里携带“Authorization: token”。这里要理解一个关键选择:为什么不用Session?
传统Session把登录状态存在服务器内存里,浏览器靠Cookie自动带上SessionId。这种方式单机部署没问题,但前后端分离后有一个很实际的问题:前端如果部署在另一个端口或者域名下,Cookie跨域处理非常麻烦。而且如果系统要做成小程序或者App的后端,Cookie那套机制基本行不通,接口层一定是无状态的Token方案。
登录接口的示意代码大概长这样:
@PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO dto) { // 校验用户名密码,失败抛出BusinessException User user = userService.checkLogin(dto.getUsername(), dto.getPassword()); // 生成token,可以把userId、username和时间戳编码进去 String token = JwtUtil.createToken(user.getId(), user.getUsername()); return Result.success(new LoginVO(token, user.getUsername())); }有了Token之后,还需要拦截器统一校验。拦截器实现HandlerInterceptor,preHandle里读取请求头的Authorization字段,校验token是否有效,无效直接返回401。放行规则要配置得仔细一点:登录、注册、商品列表、商品详情这些公开接口不需要token,购物车、下单、用户中心、后台管理这些接口必须校验。这部分在WebConfig里通过addInterceptors方法配置,开发人员维护一份白名单路径列表就够了。
3.3 商品检索与分页:MyBatis-Plus条件查询
商品列表是所有前台页面里最核心的接口。它需要支持分类筛选、关键词搜索、价格区间、排序方式、分页返回。如果自己拼接SQL,条件一多代码会变得很啰嗦,而且容易漏掉某个组合条件。这套系统里用的持久层框架如果是MyBatis-Plus,用LambdaQueryWrapper写条件查询会清爽很多。
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(categoryId), Product::getCategoryId, categoryId); wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword); wrapper.between(minPrice != null && maxPrice != null, Product::getPrice, minPrice, maxPrice); wrapper.orderByDesc("sales".equals(sort) ? Product::getSales : Product::getCreateTime); Page<Product> page = productMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);这里要留意两个细节。第一个是条件判断写在eq方法第一个参数里,这样前端没传分类id时就不会拼进SQL,避免“id = null”这种无效条件。第二个是分页查询用Page对象,返回结果里同时包含当前页数据列表和总记录数,前端分页组件需要的total值直接从里面取。这种写法比传统PageHelper的PageInfo结构化更好,前端拿数据更直接。
3.4 订单提交:主从表写入与库存扣减
订单模块是整个后端里含金量最高的地方。它涉及两个核心问题:主从表怎么设计、库存怎么保证不超卖。
所谓主从表,就是订单头表和订单明细表。订单头表只放这笔订单的整体信息:订单号、用户id、总金额、状态、收货人、收货地址。订单明细表放一个订单里包含的每一件商品:商品id、商品名称、购买时的单价、数量、小计金额。为什么必须拆两张表?因为一个订单可能包含多件商品,如果把商品信息直接塞进订单头表,那同样的订单号要在头表出现多条记录,订单的维度信息就重复了,统计订单数量和金额也会混乱。
提交订单的方法需要加Spring事务注解:
@Transactional(rollbackFor = Exception.class) public Long submitOrder(OrderSubmitDTO dto, Long userId) { // 1. 查询要结算的购物车商品项 // 2. 遍历商品,从product表读取最新价格、校验库存 // 3. 计算订单总金额,使用Decimal类型避免精度丢失 // 4. 插入order_master,拿到订单id // 5. 逐个商品插入order_item,并执行扣库存SQL // 6. 清空已购买购物车项 // 7. 返回订单号 }扣库存不要用“先查询库存再判断是否大于0,然后执行update”这种组合。并发高的时候,两个请求同时查到库存还剩1件,都认为可以卖,最后就会超卖。正确写法是用一条带条件的update语句:
UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这条SQL的执行结果会返回影响的记录数。如果返回0,说明库存不足或者商品下架了,直接抛出异常回滚整个事务。用一条SQL同时完成“检查库存”和“扣减库存”,数据库行锁保证同一时刻只有一个事务能改这一行,从根上避免超卖。
还有一点必须强调:订单金额以后端查询的数据库价格为准。前端提交过来的商品价格、订单总价都不可信,用户在浏览器里改个参数就能篡改请求数据。真正安全的做法是后端拿到商品id后重新查价格,自己重新算一遍总金额。
4. Vue前端实现:页面怎么搭、接口怎么调、状态怎么管
4.1 路由布局与页面组织
Vue项目拿到手,先看router/index.js,整个站点的页面结构一目了然。前台页面包括首页、商品列表、商品详情、购物车、订单确认、用户中心;后台管理通常是一个独立的路由块,里面嵌套商品管理、订单管理、分类管理等子页面。
路由这块有几个实用设计。商品详情页的路径一般带参数id,通过{ path: '/product/:id', name: 'ProductDetail' }声明,页面里用this.$route.params.id获取商品id。后台管理页面做成父子路由嵌套,父组件是一个包含左侧菜单和顶部栏的布局组件,子组件通过children配置关联到具体管理页面。这样后台不同功能页共享同一套布局,不用重复写侧边栏代码。
const routes = [ { path: '/', component: Home }, { path: '/product', component: ProductList }, { path: '/product/:id', component: ProductDetail }, { path: '/cart', component: Cart }, { path: '/admin', component: AdminLayout, children: [ { path: 'product', component: AdminProduct }, { path: 'order', component: AdminOrder }, { path: 'category', component: AdminCategory } ] } ]页面组件放在views目录下,公共组件放到components目录。比如商品卡片在首页和列表页都会用到,那就抽成一个ProductCard组件,传入商品对象,内部渲染图片、价格和名称,两处复用,改样式也只需要改一处。
4.2 axios封装:把接口调用统一起来
前端和后端通过HTTP接口通信,axios是所有请求的出口。项目里通常会有一个request.js文件对axios做统一封装,这个封装决定了前端调接口时有多省事。
import axios from 'axios' 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 }) // 响应拦截:统一处理返回码 service.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { // 401表示未登录,跳回登录页 if (res.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(new Error(res.message || '请求失败')) } return res }) export default service封装之后,页面里调接口就是一行的事:
const res = await getProductDetail(id) this.product = res.data前后端联调阶段,跨域问题是最常见的拦路虎。前端开发时启动在8080端口,后端运行在8081端口,直接发请求会被浏览器拦截。项目里一般通过Vue的devServer.proxy解决:
devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }这样一来,前端代码里写的所有baseURL为“/api”的请求,在开发环境都会自动转发到后端端口。浏览器看到的请求地址是同源的,不会触发跨域限制。这个配置是“前后端分离开发模式下必备的润滑剂”,很多新手项目联调卡住,不是后端接口有问题,而是代理没配好。
4.3 从加入购物车到提交订单:交互状态怎么走
购物车并不是页面上一个简单的数据数组,它需要和登录状态联动。如果用户还没登录就去点“加入购物车”,前端要引导去登录页,因为购物车数据存在后端数据库表里,必须有用户id才能关联。
加入购物车要传的参数包括商品id、购买数量,后端把数据写入cart_item表。这里不要在前端自己拼一个购物车数据结构存本地,因为用户换一台设备登录,购物车是空的,体验会很奇怪。把购物车数据落在服务端,前后端各端看到的数据天然一致。
提交订单时,前端要处理两个数据。一个是从购物车勾选区域拿到的商品条目id列表,另一个是收货地址表单。点击提交按钮后,调用后端submitOrder接口。成功之后页面跳转到订单详情或者订单列表页,同时购物车里已被购买的条目应该消失,这个更新逻辑在接口返回成功后前端手动刷新购物车列表即可。
这里有一个交互细节值得注意:用户在商品详情页直接点“立即购买”时,前端不能把一个单独商品当普通购物车流程走,而是要先调接口把这件商品加入购物车,再跳转结算页,或者后端提供单独的立即购买接口直接下单。多数项目的做法是复用购物车通道,把当前商品临时写入购物车再跳转,逻辑简单,后端也不用额外开发一套独立下单方法。
4.4 管理后台的表格、表单与图片上传
后台管理页面基本就是“表格+表单+弹窗”三件套。商品列表用el-table展示,每一行绑定一条商品数据,操作列放“编辑”、“上下架”、“删除”按钮。表格上方是搜索区域,分类选择、关键词输入框和查询按钮,点击查询后重新调用列表接口。
新增和编辑商品用的是同一个弹窗表单。弹窗打开,如果是编辑,先通过接口拿到该商品完整信息,回显到各个输入框;如果是新增,表单清空等待用户输入。因为商品字段多,表单通常按区块分组,基础信息区、图片展示区、参数规格区、价格库存区,用el-tabs或分组标题隔开。校验规则用el-form的rules属性,比如价格必填且是数字、库存必须是整数、商品名称必填且长度限制。
图片上传这个环节非常容易踩坑。el-upload组件的action属性直接指向后端的上传接口,后端把文件保存到服务器的upload目录,返回图片访问URL。难点在回显。前端表单里要展示已上传的图片,而后端返回的可能是相对路径“/upload/xxx.jpg”,浏览器需要能通过这个路径访问到图片文件。解决方法是后端增加静态资源映射,把本地的upload目录暴露成HTTP可访问路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); } }这个配置不配好,后台传完图片提交成功,前台页面上就是一片灰图,看着像接口出了问题,实际只是静态资源没映射。
5. MySQL表结构:重SKU家电商品的落库设计
5.1 核心表清单与关系
数据库是整个系统的地基。看表结构,比看代码更容易理解业务边界。这套系统核心表大约八张,关系非常清晰:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 平台用户、管理员 | id、username、password、role、phone |
| category | 商品分类 | id、parent_id、name、sort |
| product | 商品基本信息 | id、category_id、name、price、stock、status |
| cart_item | 购物车条目 | id、user_id、product_id、quantity |
| order_master | 订单主表 | id、order_no、user_id、total_amount、status |
| order_item | 订单明细 | id、order_id、product_id、product_name、price |
| banner | 首页轮播图 | id、image_url、link_url、sort |
| notice | 公告或留言 | id、content、create_time |
用户和商品之间通过购物车表、订单表建立间接联系。分类表和商品表是1对N关系。订单主表和订单明细表是1对N关系。从表的粒度可以看出,这套系统不是一个简单的信息展示站,而是把交易核心链路都包含了。
5.2 商品表字段:价格、库存、上下架与参数
商品表的字段设计决定了后续所有业务开发的空间。一个完整的product表大概率长这样:
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `name` varchar(200) NOT NULL COMMENT '商品名称', `subtitle` varchar(500) DEFAULT NULL COMMENT '卖点副标题', `main_image` varchar(500) DEFAULT NULL COMMENT '主图URL', `images` varchar(2000) DEFAULT NULL COMMENT '轮播图,逗号分隔', `detail` text COMMENT '富文本详情', `params` varchar(2000) DEFAULT NULL COMMENT '规格参数JSON', `price` decimal(10,2) NOT NULL COMMENT '售价', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存', `sales` int(11) NOT NULL DEFAULT 0 COMMENT '销量', `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;为什么params字段要用JSON?因为家电商品参数不确定,电视要尺寸和分辨率,冰箱要容积和能效等级,洗衣机要洗涤容量和电机类型。如果每个参数都建一个字段,改需求就要改表结构。把参数整体存成JSON字符串,后端读写时作为一个整体处理,前端拿到后动态渲染参数表格,这样商品参数的扩展性大大增加。代价是无法直接用SQL查询某个具体参数,但电商系统的商品筛选通常只涉及价格和分类,这个取舍是划算的。
images字段用逗号分隔多个图片URL也是同样的思路。一张商品主图表只存一张图片,轮播图需要多张,建子表又过于麻烦,用分隔符存储是一个常见的轻量方案。
5.3 订单主表与订单明细表为什么要拆两张
订单相关的两张表是整个数据库中最重要的设计。拆分的原因我前面提过,但从表结构角度还需要补充几个关键字段。
订单主表order_master应该包含订单号、用户id、订单总金额、支付金额、订单状态、收货人姓名、收货人电话、收货地址、下单时间、支付时间、发货时间。订单号不能直接用自增id,因为订单号要展示给用户和客服,需要具备唯一性和一定的随机性,常见做法是“yyyyMMddHHmmss + 随机数”生成,或者直接用雪花算法生成。
CREATE TABLE `order_master` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL, `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待付款 1待发货 2已发货 3已完成 4已取消', `receiver_name` varchar(50) NOT NULL, `receiver_phone` varchar(20) NOT NULL, `receiver_address` varchar(200) NOT NULL, `remark` varchar(500) DEFAULT NULL COMMENT '订单备注', `create_time` datetime NOT NULL, `pay_time` datetime DEFAULT NULL, `deliver_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单明细表order_item需要把商品信息冗余进来:商品id、商品名称、商品主图、购买单价、数量、小计金额。这里把商品名称和主图冗余到明细表,千万不要图省事只存商品id。因为商品名称、价格、图片未来可能变化,用户下单后要看到的是“下单那一刻的商品快照”,如果再去关联查询商品表,商品如果被下架删除,历史订单就查不到完整信息了。
5.4 金额与库存字段的取舍
金额字段用decimal(10,2),这是硬性要求。不要用float或者double,二进制浮点数在运算中会产生精度误差,订单金额如果出现0.000000001的偏差,财务对账会出现非常麻烦的问题。decimal按字符串存精确小数,虽然计算性能略低,但对交易系统来说正确性优先。
库存字段用int,扣库存用前面说的条件更新SQL。这里还要处理一种情况:商品数据初始化时,库存字段和销量字段要同时控制。上下架状态字段status放在商品表而不是订单表,因为它是商品属性;订单的取消、发货状态放在订单主表,因为它是交易流转属性。把这两种状态混在一个字段里处理,是很多新手项目后期改不完Bug的根源。
6. 运行复现:我在干净环境里的完整启动过程与报错修复
6.1 环境准备:JDK、Maven、Node、MySQL怎么装
启动这个项目不需要太高配的电脑,但版本要对。我复现时用的环境如下:
| 组件 | 版本要求 | 说明 |
|---|---|---|
| JDK | 1.8+ | 大多数此类项目基于JDK1.8开发,8以上版本兼容 |
| Maven | 3.6+ | 管理后端依赖 |
| MySQL | 5.7+ 或 8.0 | 5.7最稳,8.0注意驱动配置 |
| Node.js | 14+ | Vue CLI项目通常需要Node 12以上 |
| IDE | 任意 | 后端用常用IDE,前端用VSCode或通用IDE |
我这边的习惯是后端项目用通用IDE打开,前端项目单独用终端操作。不能让IDE自动帮你解决所有配置问题,命令行里能看到完整的编译和报错信息。
6.2 数据库导入与后端启动
第一步先建数据库。打开MySQL命令行,创建数据库并导入项目里提供的SQL脚本:
mysql -uroot -p CREATE DATABASE IF NOT EXISTS home_appliance DEFAULT CHARSET utf8mb4; exit; mysql -uroot -p home_appliance < home_appliance.sql导入成功后,打开后端的application.yml,改两处:数据库账号、数据库密码。如果MySQL端口不是3306,把url里的端口也改掉。这里要注意url里通常带有参数,比如useSSL=false、serverTimezone=Asia/Shanghai、characterEncoding=utf8,这些参数不要删,尤其是serverTimezone,MySQL 8不配会报时区错误。
然后启动后端。项目如果用Maven管理,直接执行:
mvn spring-boot:run首次启动Maven会下载大量依赖,时间取决于网络,这是正常现象。看到“Tomcat started on port(s): 8081”这行日志,说明后端已经起来了。这时候可以先在浏览器访问一下后端的健康检查地址,或者用接口调试工具调一下登录接口,确认能拿到JSON返回,再继续启动前端。
6.3 前端npm依赖安装与启动
前端项目单独一个目录。终端进入目录后,先执行依赖安装:
npm install如果网络不好或者安装失败,用国内镜像:
npm install --registry=https://registry.npmmirror.com依赖装完后执行:
npm run serve看到“Compiled successfully”和本地访问地址,一般是localhost:8080,打开浏览器就能看到商城首页。到这里,前后端联调就算打通了。用默认管理员账号登录后台,修改一条商品价格或者下架一个商品,刷新前台页面看是否同步生效,这是一个很好的冒烟测试方法。
6.4 启动会碰到的三个高频问题
用四个最常见的坑做个对照,遇到直接照做:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 后端启动报ClassNotFound,驱动名是com.mysql.jdbc.Driver | MySQL 8之后驱动类改名 | 改成com.mysql.cj.jdbc.Driver |
| 后端启动报时区错误 | 数据库连接url缺少serverTimezone | url加serverTimezone=Asia/Shanghai |
| 前端npm install卡在node-sass | 当前Node版本和node-sass不兼容 | 使用Node 14版本,或者把node-sass换成sass |
| 前端页面调接口报跨域 | devServer.proxy未配置或配置了但没重启 | 检查vue.config.js代理配置并重启npm run serve |
还有一个经常被忽视的问题:数据库端口、后端端口、前端端口三者是否冲突。MySQL默认3306,后端改8081,前端8080,这是三个完全不同的端口,一般不会有冲突。如果你机器上已经有应用占用了8080,前端启动会提示端口被占,打开vue.config.js把devServer的port改掉就行。
7. 二次开发路线:从“能运行”到“能商用”
7.1 拿到源码先做什么:代码走读清单
很多同学拿到项目的第一反应是直接跑起来,然后开始乱点。我建议先花30分钟做一次代码走读,把项目的骨架摸清楚,后面改起需求来效率会高很多。走读顺序可以有这么几条线。
先从application.yml开始看,端口是多少、数据库配置在哪、有没有自定义的配置项。然后打开Controller目录,把每个Controller的@RequestMapping路径列出来,前端页面和后端接口的对应关系就清楚了。再看Service层,尤其是商品查询和订单提交这两个核心方法,理解数据是怎么流转的。最后打开数据库客户端,对照表结构看代码里的实体类,确认每个表的字段和索引方式。
走读过程中最值得画时间的地方是订单提交的Service实现。整条链路里事务边界在哪、扣库存的SQL长什么样、异常回滚怎么触发,这些都是整个项目技术含量的浓缩。把它读透了,你不仅会改这个项目,以后自己写交易系统也知道核心逻辑往哪里放。
7.2 家电业务扩展:以旧换新、安装预约这些需求怎么加
既然叫“家电销售展示平台”,那就可以顺着家电行业的真实业务往下扩展。三个典型需求:以旧换新、安装预约、售后工单。
以旧换新的实现思路:在订单提交前增加旧机估价环节。用户填写旧机品牌、型号、使用年限,系统给出估价金额,订单金额减去估价后生成折扣码。实现上可以在数据库增加一个exchange_activity表,考察用户提交的信息和生成优惠码的关联。在order_master表里增加一个discount_amount字段,记录优惠金额,后台可以基于这个字段做对账。
安装预约是大家电特有流程。用户买空调、洗衣机,下单后通常会预约安装时间。实现上可以在订单提交后增加一个预约入口,选择一个时间段,后台相关页面生成安装工单。要拆表的话,installation_order表和订单表是1对1关系,包含预约时间、安装地址、安装师傅、完成状态。修改量不算大,主要是理解订单状态机要增加新分支:已支付待预约、已预约待安装、安装完成。
售后工单模块就更偏客服系统了。用户提交保修申请,后台管理员分派给服务人员。核心表warranty_order包含订单id、用户id、故障描述、处理状态、处理记录。这个模块的实现会锻炼你对“状态流转”的理解,比网上零散的状态机教程更贴近真实业务。
7.3 上线部署:前端打包、后端jar、Nginx托管
二次开发后如果真要投入使用,部署方案要提前想清楚。开发模式下前端通过proxy代理解决跨域,生产环境不能这么干,得用正式的反向代理。
前端执行npm run build,生成dist目录。这个目录就是纯静态文件,HTML、JS、CSS,交给任意Web服务器托管即可。后端执行mvn clean package,生成一个可执行jar包,命令行运行:
java -jar home-appliance.jar --spring.profiles.active=prod生产环境的配置用环境变量或者Spring的profile机制管理,数据库密码、上传路径、端口这些参数不要在代码里写死。数据库单独部署在独立机器上,至少配置每天自动备份,可以用mysqldump加定时任务实现。
最关键的Nginx配置是把“/api”前缀的请求转发到后端jar,把其他所有请求交给前端静态文件:
server { listen 80; server_name your-domain.com; root /opt/home-appliance/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files那行是为了支持Vue Router的history模式。前端路由在页面刷新时会向服务器发起真实的文件路径请求,如果不做回退配置,比如直接刷新“/product/12”这样的地址,Nginx找不到对应文件会返回404。前端如果改成hash路由可以规避这个问题,但history模式更正规,Nginx配置必须配合到位。
再说一点,真正的生产环境最好把上传的图片目录和项目代码分开,图片放到独立目录,Nginx直接用alias指向那个目录,避免频繁发布打包时把用户上传的图片覆盖掉。这个细节很多人会在上线之后才意识到,早期规划好能省掉很多麻烦。
最后说点我的体会。项目能跑只是及格线,这套系统真正有价值的地方在于它把“前后端分离”这件事的完整链路摆在了你面前——前端点击、请求发出、路由转发、后端处理、SQL执行、数据回显,每一步都有源码可以翻。很多人照着这个项目跑通之后,隔一个月就能自己从零搭一个小型管理系统,原因就是他把这套链路的每一环都读明白了。如果你手里正好有这套源码,我的建议很直白:先别急着改功能,按照7.1的清单把代码完整走读一遍,再动手也不迟。