在家电行业摸爬滚打了几年,从早期用Excel表格管库存、手写销售单的日子,到后来参与搭建真正能支撑多门店、多角色协同的销售管理平台,我对这类系统的痛点和价值体会很深。今天要聊的这套“企业级家电销售展示平台管理系统”,基于SpringBoot+Vue+MyBatis架构,数据层用MySQL,是一套完整可运行的前后端分离项目。它解决的正是家电销售场景里商品展示、库存流转、订单跟踪、销售统计这一连串的日常核心问题。
如果你正在做毕业设计、找项目练手,或者公司要快速搭一套内部销售管理系统,这套源码的架构和实现思路都非常值得参考。它不是那种玩具级增删改查,而是把企业级项目常用的分层架构、权限控制、状态流转都串起来了。下面我会从整体设计思路、数据库建模、后端核心实现、前端交互逻辑、部署避坑几个维度逐一拆解,把我实际跑通这个项目的经验和踩过的坑都写出来。
1. 项目整体定位与核心价值拆解
1.1 这类系统到底在管什么
先梳理清楚业务边界。家电销售平台的业务链条,通常是从“商品上架展示”开始,到“客户下单购买”,再到“财务对账统计”结束。中间穿插着库存变动、订单状态更新、客户信息维护、操作员权限分配这些环节。
这套系统的核心模块大致可以分成六块:
- 基础数据管理:品牌、品类、规格型号、供应商信息,这是所有业务的地基。
- 商品展示管理:家电商品有图片、能效等级、卖点描述、价格区间,前端需要有清晰的分区展示。
- 库存管理:入库、出库、实时库存查询,以及库存预警。
- 订单管理:下单、改单、发货、退款/退货,全生命周期状态跟踪。
- 客户管理:会员信息、联系方式、消费记录,支撑复购和回访。
- 销售统计:按品类、品牌、时间段维度聚合销售额、销量、利润。
如果只做一个单机版的增删改查,那用个Bootstrap加几个页面就结束了。但企业级意味着要有账号体系、角色权限、操作日志、数据隔离,还要考虑并发环境下库存不超卖,这些才是这套SpringBoot+Vue架构真正发挥价值的地方。
1.2 为什么值得上手学习
这套项目最大的学习价值在于技术栈组合非常“典型”:SpringBoot负责后端接口,Vue负责前端交互,MyBatis负责数据库操作,MySQL存数据。求职市场上这类岗位需求量最大,大部分企业内部管理系统的技术形态就是如此。
我见过不少简历上写着“熟悉SpringBoot+MyBatis”的开发者,但对前端页面怎么和后端接口对接、动态菜单和权限控制怎么落地、订单状态怎么流转这些问题,一问就含糊。这套项目恰恰把这些关键链路完整走了一遍。它不是花架子,而是把真实业务和技术栈结合得很好。学完之后,你能讲清楚一次请求从前端按钮点击到后端SQL执行的完整过程,这比背一百道面试题都有用。
2. 技术架构与核心设计思路
2.1 前后端分离的协作模型
这套系统采用典型的前端分离架构:Vue项目独立开发、独立部署,通过HTTP接口与SpringBoot后端通信。前端只负责渲染数据和收集用户操作,后端只负责业务逻辑和数据持久化,两边通过JSON格式交换数据。
前端项目里,页面路由由Vue Router管理,状态用Vuex或Pinia维护,HTTP请求统一走Axios封装。后端项目按Controller-Service-Mapper三层分包,Controller层接收和响应请求,Service层处理业务规则,Mapper层基于MyBatis与数据库交互。
这个分离模型的优势在多人协作时尤其明显:前端开发和后端开发可以并行推进,只要事先约定好接口数据结构,两边互不阻塞。部署上也可独立扩展,比如后端服务扛不住压力时可以多开实例,前端静态资源用Nginx做缓存加速,互不影响。
2.2 为什么选SpringBoot而不是SSH或SpringCloud
很多初学框架的人容易陷入技术选型的纠结。实际上对这类单体业务系统,SpringBoot是最稳妥的选择。早期SSH(Struts2+Spring+Hibernate)配置繁琐,启动慢,现在已经不是主流;SpringCloud微服务全家桶对这个体量的系统又是过度设计,引入注册中心、网关、配置中心后,没解决业务问题,反而增加运维负担。
SpringBoot的“自动配置+Starter依赖”机制把繁琐的XML配置变成了约定和注解,内置Tomcat直接打包运行,非常适合快速交付。同时它对MyBatis的集成非常成熟,mybatis-spring-boot-starter一个依赖搞定,配合注解或XML写SQL都很顺手。如果你以后要往微服务方向升级,SpringBoot也是基础底座,不会有架构迁移的沉没成本。
2.3 MyBatis与MySQL搭配的加分点
MyBatis的优势在于SQL可以精确控制,适合报表统计、多表关联这类复杂业务场景。在家电销售系统里,销售统计报表经常需要按月份、品牌、门店维度做多表聚合,用MyBatis在XML中手写SQL可以精细控制索引和查询计划,这点比全自动ORM(比如Hibernate的HQL)更可控。
MySQL作为底层存储,选用InnoDB引擎,支持事务外键和行级锁。订单、库存这些核心数据需要强一致性,InnoDB的事务机制能保证多个操作要么全部成功要么全部回滚,这是业务系统最基本的安全底线。MySQL的生态也最成熟,部署资料多,遇到问题好排查,对学习者极度友好。
3. 数据库建模与核心业务表设计
3.1 核心表结构与字段说明
结合家电销售的业务特性,我把核心表拆解为下面几组。这是整套系统的数据骨架,建表时留下足够扩展余地很重要。
用户与权限体系:
| 表名 | 关键字段 | 作用说明 |
|---|---|---|
| sys_user | id, username, password, real_name, role_id, status | 系统登录账号,密码加密存储 |
| sys_role | id, role_name, role_code, description | 角色定义,如管理员/销售员/仓库员 |
| sys_menu | id, menu_name, parent_id, path, icon, sort | 菜单目录结构,驱动前端路由动态生成 |
| sys_role_menu | role_id, menu_id | 角色与菜单的关联关系 |
商品与库存体系:
| 表名 | 关键字段 | 作用说明 |
|---|---|---|
| product_category | id, category_name, parent_id, sort | 家电分类,如空调/冰箱/洗衣机 |
| product_brand | id, brand_name, description | 品牌信息 |
| product_info | id, product_code, name, category_id, brand_id, model, energy_level, price, cost_price, unit, status | 商品核心信息,能效等级是家电独有属性 |
| stock_info | id, product_id, warehouse_id, quantity, safety_stock | 实时库存,注意与商品表解耦 |
订单与客户体系:
| 表名 | 关键字段 | 作用说明 |
|---|---|---|
| customer_info | id, customer_name, phone, address, level, create_time | 客户档案,手机号建唯一索引 |
| order_main | id, order_no, customer_id, user_id, total_amount, discount_amount, pay_amount, order_status, create_time | 订单主表,记录订单整体信息 |
| order_detail | id, order_id, product_id, product_name, price, quantity, subtotal | 订单明细表,一单多品明细拆分 |
| sale_record | id, order_id, product_id, sale_date, sale_quantity, sale_amount | 销售流水表,专门支撑统计查询 |
这三组表之间通过外键或逻辑主键关联,比如订单主表与明细表就是典型的一对多关系。可以看到我将库存从商品表中单独拆出来,原因是家电销售中一个商品多个仓库库位的场景很常见,拆开后扩展到多仓模式时不需要改动商品主数据。
3.2 几个关键的建表设计细节
这里特别说明三个容易踩坑的设计决策。
金额字段必须用decimal。家电单价动辄几千上万,用float/double会产生精度误差,比如19.99乘以3得到59.970000000000006。MySQL中decimal(10,2)可以精确存小数,Java侧对应BigDecimal,运算时用BigDecimal.valueOf或字符串构造器,不要直接传double。
库存表要有乐观锁版本字段。我这套项目的stock_info表加了一个version字段,每次更新库存时执行“update stock_info set quantity = quantity - #{num}, version = version + 1 where product_id = #{productId} and version = #{oldVersion}”,如果更新影响行数为0,说明并发下出现冲突,需要重新查询库存再决定是否提示用户。这是电商场景防超卖的经典方案,比直接在应用层加synchronized可靠得多。
逻辑删除优于物理删除。商品下架、订单作废等场景,我给业务表统一加了deleted字段,默认0。查询时所有SQL强制带deleted = 0条件。这样一个“误删”操作可以随时恢复,也为后续审计留了数据。但要注意,唯一索引不能建立在“可能逻辑删除的记录”上,否则重复删除会报主键冲突,解决方式是唯一索引中带上deleted字段。
4. 后端核心模块实现与关键代码解析
4.1 登录认证与权限控制的落地方式
这套系统的后端安全设计走的是JWT+拦截器方案。用户输入账号密码,后端校验通过后生成一个带过期时间的token返回前端,前端把token存在localStorage,每次请求在请求头加Authorization字段,后端拦截器统一校验。
核心拦截器逻辑大致如下:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; // 放行跨域预检请求 } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BusinessException(401, "未登录或登录已过期"); } // 解析token并存入ThreadLocal,供Service层获取当前用户 LoginUser loginUser = JwtUtil.parseToken(token.replace("Bearer ", "")); UserContext.set(loginUser); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); // 防止线程池复用导致用户串号 } }这里的细节在于UserContext用ThreadLocal存储当前登录用户,在请求结束后必须清理。如果不清理,Tomcat线程池复用时下一个请求可能读到上一个用户的身份信息,这是很隐蔽的权限漏洞。
菜单权限在前端路由做了动态生成:用户登录后,后端根据其角色返回可访问的菜单列表,前端用addRoute动态注册路由,菜单栏和按钮都按权限渲染。但要注意,前端隐藏菜单只是体验优化,真正的安全性靠后端的接口校验。比如管理员接口必须再校验一次角色,不能只看是否登录就放行。
4.2 订单创建与库存扣减的事务处理
订单模块是整个系统的核心逻辑。创建订单时,涉及插入订单主表、插入明细表、扣减库存、记录销售流水四个操作,这四个操作必须在同一个数据库事务中执行,否则会出现“订单生成了但库存没扣”这种数据不一致。
我是这样实现的:
@Transactional(rollbackFor = Exception.class) public OrderMain createOrder(OrderCreateDTO dto) { // 1. 校验参数与库存 List<OrderItem> items = dto.getItems(); for (OrderItem item : items) { StockInfo stock = stockMapper.selectByProductId(item.getProductId()); if (stock == null || stock.getQuantity() < item.getQuantity()) { throw new BusinessException(500, "商品 " + item.getProductId() + " 库存不足"); } } // 2. 生成订单号并落主表 String orderNo = generateOrderNo(); OrderMain main = new OrderMain(); main.setOrderNo(orderNo); // ... 设置客户、金额、状态为待付款 orderMapper.insert(main); // 3. 插入明细并累计金额 BigDecimal total = BigDecimal.ZERO; for (OrderItem item : items) { OrderDetail detail = new OrderDetail(); // ... 填充商品快照信息 orderDetailMapper.insert(detail); total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } main.setTotalAmount(total); orderMapper.updateAmount(main); // 4. 扣减库存(乐观锁) for (OrderItem item : items) { int rows = stockMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new BusinessException(500, "库存扣减失败,请重试"); } } // 5. 插入销售流水 // ... return main; }有一个细节很多人忽视:事务只有遇到RuntimeException(及其子类)回滚,如果代码里捕获异常后不抛出,事务不会回滚。所以我自定义了BusinessException走运行时异常,并配置rollbackFor = Exception.class兜底。
订单号生成用“yyyyMMddHHmmss + 6位随机数”的格式,再加一个数据库唯一索引防重。不要用UUID直接当订单号,太长且无序,用户体验差,业务上排障也不方便。
4.3 销售统计报表的SQL优化思路
家电销售平台最常用的功能就是统计报表。常见的需求是按月份查看各品牌销量占比、按销售员汇总业绩、查某段时间内销量Top10单品。
这类统计SQL如果不注意性能,数据量一上来就非常慢。我的做法是:先建好联合索引,比如order_detail表建(product_id, sale_date)的组合索引,order_main表建(create_time, user_id)组合索引,让Where过滤条件能走索引。然后在大表单上避免写select *,明确列出需要的字段。
一个典型的按品牌汇总销售情况查询:
SELECT b.brand_name, SUM(od.quantity) AS total_quantity, SUM(od.subtotal) AS total_amount FROM order_detail od JOIN product_info p ON od.product_id = p.id JOIN product_brand b ON p.brand_id = b.id WHERE od.sale_date >= #{startDate} AND od.sale_date < #{endDate} GROUP BY b.brand_name ORDER BY total_amount DESC统计功能在Java侧用Map接收返回结果,配合ECharts在Vue端做柱状图和饼图展示。报表接口一定要做参数校验,比如限制时间跨度最大90天,防止有人一次性查全表数据把数据库拖死。
5. 前端工程化与核心页面交互逻辑
5.1 Vue项目的目录划分和请求封装
前端采用Vue CLI或Vite创建的标准工程,目录划分如下:
src/api:按后端模块拆分的接口方法,比如product.js、order.js、dashboard.js,所有调后端的方法都收敛到这里。src/router:路由配置,分静态路由和动态路由两个部分。src/store:Vuex模块化设计,存放用户信息、菜单权限、Token状态。src/views:页面组件,布局、商品、订单、统计等。src/utils:axios实例、日期格式化、通用校验等工具函数。
axios实例的封装有几个要点。基础URL绑定环境变量,超时时间设15秒,请求拦截器统一加请求头token,响应拦截器统一处理后端返回码。后端约定返回结构为“code + message + data”的JSON格式,前端拦截器中code为0时正常返回data,code非0就弹出统一错误提示,401则跳转登录页。
// src/utils/request.js 核心逻辑 service.interceptors.response.use( (response) => { const res = response.data if (res.code !== 0) { Message.error(res.message || '系统错误') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message)) } return res.data }, (error) => { Message.error(error.message || '网络异常') return Promise.reject(error) } )这样封装之后,页面组件里调用接口只需要关心业务数据,接口返回处理好坏都由拦截器统一兜底,代码非常干净。
5.2 商品展示与订单流程的交互设计
商品展示页是面向销售员的核心工作台。列表页支持按品类、品牌、关键字筛选,分页展示商品卡片。卡片包含商品图、名称、型号、当前库存、销售价。库存低于安全值时,库存数字标红,这就是库存预警功能。
点击卡片进入商品详情,详情页展示规格参数、能效等级、卖点说明、“库存状态标签”,并有一个“加入购物车”按钮。购物车数据存在前端内存里,确认结算时一次性组装订单数据提交后端,后端创建订单成功后会弹“订单提交成功,跳转待付款列表”。
订单列表页是整个平台交互最复杂的地方。它要支持按订单号、客户姓名、状态筛选,还要支持查看详情、发货、退单操作。我用Tab页签拆成“全部订单”“待付款”“待发货”“已完成”“已退款”五个分区,每个分区对应一个后端查询状态参数,交互明确,代码也好维护。
做Vue表格时还有几个习惯:大数据量列表不要一次性渲染全部,必须分页;编辑状态用弹窗而非另开页面;删除和作废操作必须有二次确认;表格操作按钮要根据订单状态动态显隐,比如“已退款”状态就不允许再点发货。
5.3 路由守卫与按钮权限的实现
前端权限有两层:页面级权限,通过后端动态菜单生成路由;按钮级权限,通过自定义指令v-permission控制。
路由守卫我写在全局前置守卫里,核心逻辑是:用户未登录(没有token)访问任何页面都跳转登录页;已登录但本地没有菜单信息时,先调用后端获取菜单,再动态注册路由,然后跳回原目标页面。
router.beforeEach(async (to, from, next) => { const token = store.getters.token if (!token) { if (to.path === '/login') { next() } else { next('/login') } return } if (store.getters.menuLoaded) { next() } else { // 拉取动态菜单并注册 const menus = await store.dispatch('fetchMenus') menus.forEach(route => router.addRoute('layout', route)) next({ ...to, replace: true }) } })按钮级指令的实现也很直接,定义一个全局指令v-permission,绑定值就是按钮对应的权限标识,指令内部检查当前用户权限列表,没有就移除该DOM节点。
6. 部署环境搭建与常见问题排查
6.1 从零把项目跑起来的完整步骤
我自己从拿到压缩包到页面完整跑通,大概花了半小时。整理一下最省事的流程。
第一步,准备数据库。本地装MySQL 8.0或5.7,创建数据库,字符集选择utf8mb4,然后导入项目里提供的sql文件。注意如果sql文件较大,用命令行source方式导入比图形化工具更稳。
第二步,更配后端。打开application.yml,确认三处配置:数据源的url、账号、密码,Redis地址(如果系统用到Redis做缓存,我建议先关掉Redis相关逻辑跑通再打开),文件上传路径。启动类直接运行,看到Tomcat started on port(s): 8080就说明后端起来了。
第三步,启动前端。在vue目录下执行npm install安装依赖,然后npm run dev进入开发模式,默认端口8088或项目里Vite配置的端口。浏览器访问后能打开登录页,说明前后端联通。
因为前后端分离,开发环境下必然涉及跨域问题。我在后端写了一个全局CORS配置,允许前端地址跨域,注意allowCredentials设为true时,allowedOrigins不能用*,要写具体地址,否则浏览器会拦截带cookie的请求。
6.2 生产环境的部署策略
开发模式跑通后,要部署到服务器上,推荐用这套组合:MySQL + 后端jar包 + Nginx前端静态资源。前端先执行npm run build生成dist静态文件,把dist目录上传服务器指定路径;后端用mvn package打包成jar,通过systemd维护进程。
Nginx配置里有一个重要操作:把后端接口反向代理到本机的8090端口,并解决前端静态资源刷新404的问题。核心配置如下:
server { listen 80; server_name example.domain.com; root /opt/emall/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }上面的try_files配置非常关键。Vue是单页应用,前端路由由history模式控制,不配置这行的话,用户刷新/order/list页面会直接404,因为服务器上根本没有这个物理路径。
6.3 我实际遇到的四个高频问题
第一个是数据库连接时区报错。MySQL 8.0默认时区设置会导致连接报错,解决方式是在jdbc url后面加上serverTimezone=Asia/Shanghai和useSSL=false,这两个参数几乎是必加的。
第二个是后端启动后访问接口返回404。这种情况通常是SpringBoot的包扫描路径问题,启动类必须放在父包下,确保Controller/Mapper接口能被扫描到。
第三个是前端页面能打开但列表数据加载不出来。先在浏览器F12看Network,如果请求是红色且提示CORS错误,优先检查后端CORS配置;如果请求发出去了但返回500,去看后端控制台堆栈,多半是SQL写错或者字段对不上。
第四个是tomcat文件上传大小限制。家电商品图经常超过1MB,SpringBoot默认的单文件大小只有1MB,上传大图会报错。在配置里改大即可:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB6.4 如何把项目变成本人的实战亮点
很多人拿到项目后只会跑通,不会讲,这是很可惜的。我的建议是往这三个方向做深入改造,写在简历上和面试交流都很有说服力。
第一个方向是并发安全。把订单创建接口用压测工具测一下,观察扣减库存是否超卖,然后优化代码逻辑,把乐观锁、分布式锁都用上,并记录优化前后的性能数据。
第二个方向是缓存加速。商品分类、品牌这类不常变的数据加Redis缓存,销售统计结果做5分钟短缓存,对比缓存前后的接口响应时间。
第三个方向是多租户隔离。如果不用多门店,可以加company_id字段做数据隔离,所有查询自动拼接该条件,这就是“从单企业系统升级成SaaS平台”的一条实践路径。
我在指导某个同学做毕设时就让他选第二个方向,他加了Redis缓存后接口响应时间从300ms降到50ms出头,答辩时这个优化点经得起追问,比单纯说“我会用框架”有说服力得多。
7. 这是一个很值得花时间吃透的项目
最后说点实际的。市面上的源码项目很多,但大部分要么功能太简单学不到东西,要么代码太混乱无从下手。这套家电销售展示平台,技术栈主流,业务场景贴近真实行业需求,代码分层清晰,从数据库到前端页面闭环完整,是很适合花时间吃透的一套项目。
我个人的体会是,跑通它只是开始,真正有价值的是深度走一遍它的订单事务、库存并发扣减、动态路由权限这几个核心难点,每个难点背后都对应一个特定技术场景,把这些场景吃透,再去读其他系统的源码就会有种“原来都是见过的套路”的感觉。
如果你正在准备面试,建议抽一个晚上把项目完整跑通,再做一个小改造,把改造前后的对比数据记下来。这比在简历上写“熟悉SpringBoot”有力得多。技术这东西,光看不练终究是纸上谈兵,动手跑一遍,遇到的问题和解决思路,就是你最有价值的东西。