企业级电子产品销售管理系统:SpringBoot+Vue+MyBatis+MySQL源码解析
发布时间:2026/9/30 4:22:46来源:尧图网络
最近在整理一套企业级电子产品销售管理系统的完整源码技术栈是 SpringBoot Vue MyBatis MySQL。这套东西在电商和软件外包行业里出现频率非常高尤其是中小型公司做内部ERP或者准备拿源码做毕业设计、二次开发的场景基本都绕不开这个组合。我花了两天时间把项目完整跑通从数据库初始化到前端启动从权限模块到订单流程顺手把几个常见的坑也都填了。这篇文章不聊浮在表面的功能介绍而是把整个系统拆开讲清楚每个模块为什么这么做、关键代码怎么写、上线时有哪些问题容易踩。不管你刚学SpringBoot不久还是准备用这套源码做求职项目只要想快速理解一套完整的企业管理系统都可以照着思路落地。1. 项目定位与整体设计思路1.1 电子产品销售场景的特殊性电子产品不是普通快消品它的销售管理天然比服装、日用品更复杂。手机、笔记本、显示器、耳机这些商品通常有多个规格比如颜色、内存、存储容量、套餐版本。同一个SPU下面挂着好几个SKU价格还不一样库存也需要拆开统计。再加上不少电子产品有序列号需要跟踪、有保修期需要记录订单发货之后还可能涉及售后换货这些都会让数据库表结构和对接口的设计多出不少门道。这套源码最好的一点是没有把商品设计成简简单单的一张表而是按SPU和SKU两层来做。系统里还有一个非常容易被忽略的功能库存联调。客户下单时锁库存订单取消后释放库存发货后再减扣库存。如果这一步不做成事务多卖几台机器就很容易出现超卖实际跑过生产环境的人应该都能理解。1.2 为什么选 SpringBoot Vue MyBatis MySQL这个组合在中小团队里面非常务实。SpringBoot 让后端开发不用再经历 SSH 时代那种繁杂的 XML 配置内嵌Tomcat打成一个Jar包就能跑部署成本很低。Vue 在国内前端社区普及度极高Element UI、Vant这些组件库让后台管理系统可以快速搭出可用的页面。MyBatis 的优势则在于 SQL 是你自己控制的复杂的销售统计、多表关联查询都很好写不会像某些 ORM 那样在性能瓶颈时干着急。MySQL 就更不用说了绝大部分中小项目连它的性能上限都用不到配套资料多出问题排查也方便。这里多说一句看到“企业级”三个字不要下意识觉得就得上一套微服务、分布式缓存。实际以电子产品销售管理系统的数据量和业务复杂度单体应用完全够用。引入微服务反而会增加事务一致性、部署链路、运维监控的负担对中小团队并不友好。这套源码走的就是单体加前后端分离的路子理解起来也更适合学习。2. 系统核心功能与模块拆解2.1 功能总览整套系统可以分成六个核心模块系统管理、商品中心、库存管理、客户管理、订单管理、销售报表。系统管理负责用户、角色、菜单权限商品中心负责分类、品牌、SPU、SKU以及上下架库存管理处理出入库、库存锁定和盘点客户管理维护客户资料和等级订单管理贯穿下单、审核、发货、售后销售报表则按日、按月汇总销售数据。这些模块覆盖了电子产品销售业务的主链路也足够支撑一个中小型电商公司的日常运营。模块之间的依赖关系也比较清晰销售订单一定是从客户和SKU数据延伸出来的库存变动又由订单状态驱动。源码里把这种依赖拆成了独立Service互相通过接口调用没有出现互相循环引用的坏味道。2.2 商品中心SPU 与 SKU 的分离设计商品中心是整个系统最基础的部分。SPU 是“标准化产品单元”比如一台 iPhone 15SKU 是“库存量单位”比如 iPhone 15 蓝色 128G。实际销售场景里客户选的是SKU下单扣的库存也是SKU的库存所以在源码中product_sku表承担了价格、库存、SKU编码等核心字段而product_spu表只保存商品公共信息比如标题、品牌、分类、封面图。页面端用了一个两级联动的选择方式先选SPU再选具体规格生成SKU。这里有个细节值得学习SKU编码在设计时就要预留足够长度而且用编码拼接的规则要稳定比如“分类前缀日期流水号”不要随便在代码里硬编码否则后面做批量导入会很痛苦。2.3 订单中心状态机是关键订单模块是销售系统的命脉。源码里的订单状态不是简单存一个字符串而是用整数状态码维护了一套状态机待支付、已支付待发货、已发货、交易完成、已取消、售后中。每个状态之间的流转都有校验比如发货动作只能在已支付待发货状态下执行取消订单时如果已经发货就必须走售后流程。订单数据采用主订单表和订单明细表拆分主订单记录客户、总金额、状态、下单时间明细表记录每个SKU的单价、数量、优惠信息。这样拆的好处是统计订单总额容易后续做发货单、售后单也能直接引用明细。我见过很多新手项目把订单明细直接拼成字符串字段存着后面想按SKU统计销售额的时候恨不得把数据库重新设计一遍。2.4 库存与采购锁定库存避免超卖库存管理模块常见的有三个概念总库存、锁定库存、可用库存。客户下单时增加锁定库存同时减少可用库存订单付款发货后再把锁定库存转成真正的出库记录。如果订单超时未支付被取消就反向释放锁定库存。这套源码在stock_record表里记录了每一次库存变动的来源来源可以是订单、采购、盘点、手动调整。这个设计非常有价值一旦库存对不上账可以沿着记录表去追溯是哪一步出了问题而不是对着一个孤零零的总库存字段发呆。2.5 客户与销售报表客户管理不只是存一个电话和地址它还要记录客户等级、来源渠道、销售负责人。电子产品客单价高很多企业会按等级给折扣所以customer表里等级字段会关联到价格策略。每次下单后系统会把订单归属到对应的销售人员账号下方便月底算提成。销售报表模块则通过SQL聚合查询完成。日销售报表关注订单数量和成交金额月销售报表可以对比不同品类、不同SKU的销冠。报表页面用前端图表展示后端只负责返回汇总数据。开发时需要注意聚合查询的SQL在数据量大之后要加合适的索引否则报表打开会很慢。3. 数据库设计与 MyBatis 落地实践3.1 核心表结构设计我重新整理这套源码时第一件事是把数据库脚本过了一遍。核心表大概有九张用户表、角色表、菜单表、客户表、商品SPU表、商品SKU表、订单主表、订单明细表、库存记录表。下面给出 SKU 表的简化建表语句方便理解字段设计逻辑。CREATE TABLE product_sku ( id bigint NOT NULL AUTO_INCREMENT, spu_id bigint DEFAULT NULL, sku_code varchar(64) NOT NULL COMMENT SKU编码, spec_info json DEFAULT NULL COMMENT 规格信息如颜色、内存, price decimal(10,2) NOT NULL COMMENT 销售价, cost_price decimal(10,2) DEFAULT NULL COMMENT 成本价, stock_total int NOT NULL DEFAULT 0 COMMENT 总库存, stock_locked int NOT NULL DEFAULT 0 COMMENT 锁定库存, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sku_code (sku_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;SPU 表与 SKU 表通过spu_id关联分类字段可以放 SPU 表也可以单独建分类表做树形结构。如果是手机、笔记本这种多级分类建独立分类表更好维护。订单主表和明细表则用order_no字段关联明细表里冗余了商品名称、SKU编码、单价这样历史订单即使商品被删也能查看。3.2 逻辑删除与唯一索引的取舍这套源码里所有业务表都加了deleted字段做逻辑删除。好处很明显用户误删了商品、客户数据还在数据库里管理员可以找回坏处是每次查询都要记得带条件并且唯一索引不能直接建立在被逻辑删除的数据上。比如 SKU 编码如果做过唯一索引删除后再次插入相同编码会失败所以项目里最终用的是“逻辑删除唯一索引里包含删除标记”的方案或者直接用一张回收站表来绕过这个限制。个人建议核心业务数据用逻辑删除日志和中间表可以物理删。不要一刀切都逻辑删除否则业务表膨胀很快查询性能也会受影响。3.3 MyBatis XML 与动态 SQL 的写法MyBatis 在这套系统里的作用很直接把复杂查询写在XML里由程序员完整控制。比如订单列表检索通常需要根据客户名称、订单状态、时间范围多个条件动态拼SQL这时where标签和if标签就非常合适。select idlistOrders resultTypemap select o.order_no, o.total_amount, o.status, c.customer_name, o.create_time from sales_order o left join customer c on o.customer_id c.id where if testcustomerName ! null and customerName ! and c.customer_name like concat(%, #{customerName}, %) /if if teststatus ! null and o.status #{status} /if if teststartTime ! null and o.create_time gt; #{startTime} /if if testendTime ! null and o.create_time lt; #{endTime} /if /where order by o.create_time desc /select这里有一个常见的坑XML里写小于号可能被解析成标签所以要么用lt;要么用![CDATA[]]。源码里处理得比较干净所有比较操作都改用了gt;和lt;这也是我建议新人直接模仿的写法。另外MyBatis启动时会通过XMLConfigBuilder读取全局配置文件如果你自定义了Configuration或者扩展了TypeHandler记得关注mybatis-config.xml的配置顺序标签顺序错了会直接启动失败。缓存方面本地一级缓存默认开启二级缓存如果要做最好只对很少变化的字典表开启订单明细这类高频修改的表开缓存反而容易出脏数据。3.4 MySQL 连接配置与常见坑SpringBoot 2.x 默认连接池是 HikariCP性能很好。连接配置里有两个容易踩坑的参数serverTimezone和useSSL。MySQL 8.x 如果没指定时区JDBC连接经常报The server time zone value错误。推荐在application.yml里这样配置spring: datasource: url: jdbc:mysql://localhost:3306/electronics_sales?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000allowPublicKeyRetrievaltrue这个参数在MySQL 8.0使用 caching_sha2_password 认证时经常用到否则连接会提示Public Key Retrieval is not allowed。字符集推荐统一utf8mb4不然存商品描述里的表情符号会变成乱码。4. 后端 SpringBoot 核心实现4.1 工程目录与启动类后端代码层次清晰按模块分包。controller层只负责接收参数和返回结果service层处理业务mapper层与数据库交互。以订单为例目录是com.example.electronics ├── controller │ └── OrderController.java ├── service │ ├── OrderService.java │ └── impl │ └── OrderServiceImpl.java ├── mapper │ └── OrderMapper.java ├── entity │ └── SalesOrder.java ├── common │ └── Result.java └── config └── WebMvcConfig.java启动类上需要加MapperScan(com.example.electronics.mapper)这样mapper接口才能被扫描到。如果不加每个Mapper接口上单独加Mapper也是可以的但类多了会比较啰嗦。4.2 登录与权限拦截器源码里的权限控制没有引入Spring Security而是用了JWT加拦截器的轻量方案。登录成功后生成token前端每次请求在header里带上Authorization: Bearer token。后端写一个拦截器统一校验token并解析出当前用户ID放入ThreadLocal。核心代码逻辑是这样public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录); } Long userId JwtUtils.parseToken(token.replace(Bearer , )); UserContext.set(userId); return true; } }用拦截器而非Spring Security对小项目来说更直观也方便学习。不过要记住拦截器只能拦接口请求静态资源和预检请求要放行否则开发环境调试时会出现跨域OPTIONS请求被拦截的问题。权限方面菜单表和角色表关联用户登录后返回菜单列表。按钮级别的权限可以做成前端自定义指令比如v-permissionorder:export后端接口再用AOP或者拦截器校验一遍前端的操作按钮只控制显隐真正的权限校验必须放在后端接口层。4.3 订单入库与库存扣减事务订单提交是整个系统最需要保证一致性的地方。用户下单要同时做三件事写入订单主表、写入订单明细表、扣减SKU库存。任何一步失败都不能留下半截订单。源码在OrderServiceImpl的createOrder方法上加了Transactional(rollbackFor Exception.class)并且用乐观锁控制库存扣减防止并发超卖。Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { SalesOrder order new SalesOrder(); order.setOrderNo(generateOrderNo()); // 保存主表 orderMapper.insert(order); for (OrderItemDTO item : dto.getItems()) { // 检查库存并扣减 int rows skuMapper.deductStock(item.getSkuId(), item.getQuantity(), item.getVersion()); if (rows 0) { throw new BusinessException(库存不足或数据已变动); } // 保存明细 orderItemMapper.insert(...); } return order.getId(); }注意deductStock的SQL并不是简单的update product_sku set stock_total stock_total - #{quantity}而是带乐观锁版本号update product_sku set stock_locked stock_locked #{quantity}, stock_total stock_total - #{quantity}, version version 1 where id #{skuId} and stock_total #{quantity} and version #{version}这样两个用户同时提交订单时只有一个更新会成功另一个rows返回0业务层直接抛出“库存不足”异常事务回滚。这个方案比悲观锁更好落地也不需要引入分布式锁。4.4 销售报表的SQL实现销售数据统计是管理后台价值最高的模块。日销售报表其实就是按天分组求和但要注意时区问题。代码里没有用date(create_time)直接转而是先按DATE_FORMAT(create_time, %Y-%m-%d)分组再在Java侧做格式化。select DATE_FORMAT(create_time, %Y-%m-%d) as day, count(distinct order_no) as order_count, sum(total_amount) as sales_amount from sales_order where status in (2,3,4) and create_time #{startTime} group by DATE_FORMAT(create_time, %Y-%m-%d) order by day desc这里有个容易踩的坑订单状态如果是“待支付”不能算进销售业绩如果订单被取消更不能算。所以查询条件里要严格控制状态值。统计SQL如果发现速度变慢优先检查create_time字段有没有索引其次再看是否要按月份分表。中小规模下加索引就足够。5. 前端 Vue 工程实现5.1 环境搭建与工程初始化前端部分是基于 Vue 2 Element UI 搭建的也可以用 Vue 3 Element Plus 改造。我建议本地Node版本保持在16到18之间如果版本太高npm install 时有些原生依赖会编译失败。第一步是安装依赖并启动开发服务器npm install npm run serve开发环境调用后端接口时需要处理跨域。最简单的办法是在vue.config.js里配置代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端请求/api/login时实际上会转发到后端的/login浏览器里不会出现跨域报错。需要留意代理只在开发环境生效生产环境你是把dist文件放到Nginx里的所以还要在Nginx配置里再做一次反向代理。5.2 axios 封装与Token管理项目里的请求并没有在每个页面里直接调用axios而是封装了一个request.js。它的作用很统一自动加token、统一处理错误状态码、返回结果里面的data解出来。平时开发时不需要在每个接口里都重复处理401。import axios from axios import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use(response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) }) export default service这套通用封装在后台管理项目里够用了。遇到文件流下载时需要在请求里加上responseType: blob并且单独处理不要走统一的JSON解析逻辑。5.3 动态路由与按钮权限前端菜单不是写死的而是用户登录后根据后端返回的菜单列表动态生成。Vue Router里保存一份静态路由表登录页、404页等再在路由守卫里把动态菜单通过router.addRoutes添加进去。这里需要额外维护一个标志位避免菜单刷新后重复添加。按钮级权限一般做成自定义指令Vue.directive(permission, { inserted(el, binding) { const points store.state.user.points if (points !points.includes(binding.value)) { el.parentNode el.parentNode.removeChild(el) } } })页面恢复或者刷新时如果token还在要重新请求用户信息和菜单不然刷新之后菜单会消失这是新手很容易忽略的问题。5.4 商品编辑与SKU组件商品表单是后台系统中交互最复杂的部分。分类用级联选择器品牌用下拉框封面图用上传组件。SKU部分则是动态表格用户选择颜色、内存后前端自动生成规格组合再填写每个SKU的价格和库存。实际做的时候可以用Element UI的el-table嵌套el-form-item每一行绑定一个sku对象。这里最大的坑是动态表单校验时prop路径需要写完整比如skuList.0.skuCode如果用方括号表示法很容易写错。上传图片时如果后端只是简单存本地路径前端上传成功后再把返回的URL塞到表单里。源码里给了一张默认图片避免测试环境下商品没有图片导致页面裂图。生产环境建议把图片放到MinIO或者OSS不然服务器重启容易丢文件。5.5 使用 Vue DevTools 调试很多人调试前端还是靠console.log一把梭但后台管理页面查表格数据、查组件状态其实用Vue DevTools更高效。安装浏览器插件之后可以直接在Vue组件面板里查看data、computed、props甚至可以临时修改值来观察页面变化。接口返回的数据异常时先在Network面板确认响应结构再切到Vue组件里看是哪个字段没有渲染出来。我调试SKU动态表格时就靠DevTools定位到某个sku对象缺少price字段页面才显示NaN。6. 部署上线与问题排查实录6.1 本地启动完整步骤如果你拿到这套源码建议按下面顺序启动不要一上来就npm install。新建数据库electronics_sales执行sql/init.sql初始化脚本修改application.yml里的数据库账号密码启动后端SpringBoot应用确认端口8081可以访问Swagger或接口文档在前端目录执行npm install再执行npm run serve访问http://localhost:8080用管理员账号登录。管理员账号和初始密码一般在sql/init.sql里如果登录后需要修改密码记得看下sys_user表里的密码字段是否加密保存避免直接用明文。6.2 Linux 服务器部署生产环境部署时前端执行npm run build生成dist目录然后放到Nginx的静态目录下。Nginx配置需要同时解决两个问题一是单页应用刷新时404需要配置try_files $uri $uri/ /index.html;二是API请求要反向代理到后端服务。server { listen 80; server_name your.domain.com; location / { root /opt/dist; index index.html; try_files $uri $uri/ /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; } }后端Jar包部署可以用nohup java -jar app.jar但更推荐用systemd托管这样服务器重启后服务能自动拉起。JVM参数里可以加上-Xms512m -Xmx1024m -Dfile.encodingutf-8避免中文乱码和堆内存不足。6.3 常见问题速查表我把这套源码运行过程中最常遇到的问题整理成了表格直接照着排查会快很多。现象可能原因处理方式后端启动报“端口被占用”8081端口被其他进程占用netstat -tunlp前端登录后刷新菜单消失刷新时没有重新请求动态菜单在路由守卫里判断store是否为空并重新拉取上传图片后页面不显示图片访问路径找不到静态资源映射SpringBoot添加/upload/**静态资源映射查询订单中文乱码JDBC连接字符集未指定URL加characterEncodingutf8表使用utf8mb4连接MySQL报时区错误MySQL未设置时区或JDBC版本不匹配连接URL加serverTimezoneAsia/Shanghai并发下单超卖扣库存SQL没有控制条件使用stock_total #{quantity}并加版本号动态表单SKU数据丢失前端组件重复渲染且未绑定唯一key为每个sku行添加唯一id作为key打包后刷新404Nginx没有配置try_files添加try_files $uri $uri/ /index.html;6.4 源码二次开发与改造建议这套源码完全可以做二次开发但改的时候别贪快。先花半小时把订单状态流程梳理清楚再打开权限模块。真正需要扩展时可以考虑在当前架构上改进三个方面一是商品搜索集成Elasticsearch或HanLP分词提升搜索体验二是文件存储统一迁移到MinIO避免本地磁盘占用三是引入Redis缓存菜单和用户权限减少每次请求都查数据库的压力。尤其要注意增加新字段时别只改代码数据库脚本也要同步维护。最好引入Flyway做数据库版本管理每次新增表或修改字段都记录一个版本团队协作时不会再出现“我本地能跑你本地报错”的情况。最后分享一个我在整理这套源码时的习惯拿到项目先不急着跑起来先看init.sql再读application.yml然后打开主流程的Controller看一遍接口。别人写得很好的地方不要直接删先理解为什么这么设计。这套代码里面订单和库存的事务处理、SKU的动态表单、MyBatis的条件查询都是很值得在实际项目中复用的经验。把基础设计吃透后面不管你是做毕业设计还是真正接手公司项目都会轻松很多。
网站建设高端定制企业官网