恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+Vue3+MySQL打造智能家居销量数据分析系统
首页
资讯中心
/
SpringBoot+Vue3+MySQL打造智能家居销量数据分析系统
SpringBoot+Vue3+MySQL打造智能家居销量数据分析系统
发布时间:2026/10/6 5:12:22
1. 为什么我会做一套这样的“销量分析”系统先交代一下背景。我之前在一家做智能家居设备的公司帮忙整理销售报表每天最头疼的事情就是打开 ERP 导出一堆 Excel再用数据透视表去查什么产品卖得好、哪个门店连续三个月下滑、线上和线下渠道的占比变化。数据量一上来Excel 就卡而且业务这边想看环比那边想看 Top10Excel 要做到基本每个维度都建一张透视表。后来我实在受不了决定自己写一套“智能家居销量数据分析系统”也就是现在这个名叫 jrabo 的项目。这套系统解决的是三个核心问题一是把散落在 MySQL 里的订单数据统一管理二是用后端接口按时间、品类、门店、渠道等维度做聚合统计三是用 Vue3 页面把这些统计数据直接画成折线图、柱状图和饼图。业务人员打开浏览器输入账号密码不用碰数据库不用碰代码就能看到当天、当月、上个月的销量趋势和销售排行榜。对开发同学来说它的主要意义是提供了一个“从零搭建一个完整前后端分离项目”的参考SpringBoot 负责提供 REST APIVue3 负责渲染和交互MyBatis 负责 SQL 管理MySQL 负责存储整个过程不依赖第三方重型框架理解起来相对顺畅。适合读这篇内容的人有两类。一类是刚学完 Java 基础和数据库想拿一个完整项目练手或者做毕业设计的同学另一类是工作中需要快速搭建内部数据分析后台但不想引入 Hadoop、Flink 那套重体系的后端工程师。这个项目的代码量和复杂度刚好卡在“能跑起来”和“看得懂”之间没有微服务没有分布式就是一个很实在的单体 Web 项目加一点点可视化技巧。2. 技术选型SpringBootMyBatis 管后端Vue3MySQL 管展示与存储这里面的取舍逻辑很多人一看到“智能家居销量数据分析”就觉得要用推荐系统、大数据平台其实完全不是一回事。智能家居行业虽然品类多但单门店单天的订单量也就是几百到几千条即使铺到全国几十家门店一年下来的明细订单也不过几百万条。这种量级用 MySQL 加合理的索引一点问题没有强行上大数据反而是给自己找麻烦。所以项目标题里那四个关键词每个都是基于实际业务量级和平时的维护成本选的。2.1 后端选 SpringBoot 而不是手写 Servlet 或 SSM 的原因SpringBoot 最直接的价值是把“配置”这件事压到了最低。我不需要再写那么复杂的 XML不需要手动配置事务管理、数据源、包扫描用一个SpringBootApplication就能把项目拉起来。相比传统的 SSM 组合SpringBoot 自带内嵌 Tomcat本地用mvn spring-boot:run就能直接启动打出来的 jar 包也能直接跑这对经常要在客户环境部署的内部工具来说非常重要。另外一个原因是招人和接手成本。团队里大多数 Java 开发都会用 SpringBoot而 MyBatis 在国内后端团队的普及率又非常高大家习惯了写 SQL 而不是让 ORM 自动生成 SQL。销量分析这种场景SQL 里经常涉及多表 join、分组、条件动态拼接MyBatis 的 XML 用来做这些事非常顺手。2.2 前端选 Vue3 而不是 Vue2Composition API 和 Vite 带来的实际变化Vue3 出来之后前端最大的变化是组合式 API。以前用 Vue2 的 Options API逻辑片段分散在data、methods、computed里一个页面如果既有销量图表又有多条件筛选几十个字段堆在一起非常难维护。Vue3 的script setup写法可以把“加载筛选条件”“请求接口”“给 ECharts 赋值”这些逻辑按功能聚合在一起阅读顺序和业务处理顺序是一致的。开发环境上也换了 Vite冷启动确实快。原先 Webpack 动不动等好几秒Vite 几乎秒开改完代码页面热更新的体验好很多。如果你还在 Vue2 阶段我的建议是直接上手 Vue3因为现在的 Element Plus、Naive UI 这些组件库都已经全面适配 Vue3生态不存在问题。2.3 MySQL 在这个项目里承担的职责以及为什么不用 NoSQL智能家居销量分析的数据是非常标准的二维表订单号、商品编码、门店、时间、数量、金额。这种数据放在 MySQL 里既方便按索引查询也方便做事务。很多同学有个误区觉得分析系统就要用 ClickHouse、MongoDB 之类存储但它们的部署和运维成本远高于 MySQL而且在这个项目规模下MySQL 加上合适的联合索引完全能在一秒内返回聚合结果。我在设计时特意把维度表和事实表拆开商品、品类、门店、经销商这些作为维表销售订单明细作为事实表。分析需求变化时只需要调整 SQL 的 group by 维度不需要改存储结构。MySQL 的date、decimal类型也精确覆盖了时间统计和金额统计的需求。3. 数据库与后端核心从表结构设计到 MyBatis 动态统计 SQL这部分是整个项目里最能学到东西的地方。业务分析类的系统核心其实不是前端特效也不是框架多新而是数据库表结构能不能支撑分析查询以及 SQL 能不能写清楚。3.1 核心表设计与字段说明我在 jrabo 里主要设计了六张表品类表category、商品表product、门店表store、渠道表channel、销售订单主表sale_order、销售订单明细表sale_order_item。category表字段很少就是id、name和parent_id做成支持两级分类的形态。product表除了基础信息还冗余了category_id因为在按品类统计销量时如果商品表和品类表分开 joinSQL 会啰嗦很多冗余字段可以避免每次都多表连接。store表里有region_id和store_type主要为了支持按区域和直营/加盟门店进行交叉分析。sale_order表放订单头信息包括订单编号、门店 ID、渠道 ID、下单时间、订单状态、应收金额和实收金额。订单明细表sale_order_item保存每一行商品的销量、单价和折扣价。两张表通过order_id关联。这里要注意统计销量要使用明细表的quantity统计金额要优先使用sale_order的实收金额避免重复计算。表结构不复杂但我会加上几条常用索引sale_order.order_time上建普通索引sale_order_item.product_id和sale_order_item.order_id上建索引查询经常按日期范围过滤这个索引能带来非常明显的性能提升。3.2 初始化演示数据的两条路径源码里提供了两套初始化方式。第一套是写一个init_data.sql往表里插入几十条手工数据适合快速跑通功能。第二套是我写的一个DataGenerator工具类用 Java 随机生成 50 万条销售记录方便看大数据量下的查询效果。生成脚本的原理很简单先预置 20 个商品、10 个门店、4 个渠道然后从两年前某一天开始每天生成随机数量的订单再用随机明细行数填充明细表。生成大量数据时我踩过一个小坑不要一条一条 insert效率太低。最好改成批量插入比如每攒够 1000 条明细执行一次insert。虽然 MyBatis 的动态 SQL 可以用foreach实现批量插入但参数数量超过 MySQL 限制时会报错。实际写法是控制每批条数比如一张明细表一次插入不超过 500 行保证参数数量在 500 乘字段数以内稳定且快速。3.3 MyBatis XML 里的销量统计 SQL怎么解决动态条件后端最重要的接口是“销量趋势图”和“商品排行”。销量趋势要求前端传一个时间范围、一个门店维度、一个品类维度后台就要拼接条件按天或者按月分组求和。这种动态拼接如果用 JPA 写会比较别扭但在 MyBatis 的 XML 里非常自然。我摘一段核心写法来说明思路select idselectSalesTrend resultTypemap SELECT DATE_FORMAT(o.order_time, %Y-%m-%d) AS date_key, SUM(oi.quantity) AS total_quantity, SUM(oi.actual_amount) AS total_amount FROM sale_order o INNER JOIN sale_order_item oi ON o.id oi.order_id where o.status FINISHED if teststartDate ! null and startDate ! AND o.order_time gt; #{startDate} /if if testendDate ! null and endDate ! AND o.order_time lt; #{endDate} /if if teststoreId ! null AND o.store_id #{storeId} /if if testcategoryId ! null AND oi.category_id #{categoryId} /if /where GROUP BY date_key ORDER BY date_key /select重点是gt;和lt;的转义。XML 里直接写其实可以但写必须转义否则解析会失败。这是新同事最容易踩的坑。还有一个地方要在真实项目中留意DATE_FORMAT在 MySQL 里按天分组没有问题但如果数据表很大对order_time做函数处理会导致索引失效。我的建议是接口层尽量限制时间范围不超过一年或者在表里增加一个冗余的date_key字段在写入时就算好避免查询时做函数计算。3.4 MyBatis 从 XMLConfigBuilder 到 Mapper 执行的初始化链路很多人用 MyBatis 只是会写接口却不知道启动时它是怎么把 XML 和接口绑定起来的。我在项目里特地把初始化过程梳理了一遍这也是面试特别喜欢问的点。Spring Boot 集成 MyBatis 后SqlSessionFactory创建时会调用XMLConfigBuilder。这个类会解析 MyBatis 的主配置文件也就是我们设置的mybatis-config.xml或者application.yml里的 MyBatis 属性。它首先读取settings、typeAliases、mappers之类的节点然后通过XMLMapperBuilder加载每个 Mapper 的 XML 文件。加载过程中每个select、insert、update、delete标签都会被解析成一个MappedStatement最终注册到Configuration对象里。接口层的绑定在后面的MapperRegistryMyBatis 扫描Mapper注解的接口为每个接口生成一个代理对象MapperProxy。当我们调用接口方法时MapperProxy根据方法名找到对应的MappedStatement再把参数传给 SQL 执行器。理解了这条链路你再看到XMLConfigBuilder就不会觉得它是个神秘黑盒其实它就是 MyBatis 生命周期中最前面的“解析器”。4. Vue3 前端可视化页面从路由到 ECharts 图表的完整链路如果后端只提供接口使用价值很有限。真正让业务觉得厉害的是 V3 页面里那些能点能筛的图表。这部分我会从前端工程结构讲起顺便聊聊我在开发环境和生产环境遇到的各种问题。4.1 Vite 创建工程后的目录规划与依赖安装我用npm create vitelatest frontend -- --template vue创建了 Vue 项目然后安装了 vue-router、pinia、axios、echarts 这几个基础依赖。目录结构按页面和组件拆分frontend/src ├── api/ // axios 封装与后端接口定义 ├── assets/ ├── components/ // 通用图表、筛选栏组件 ├── router/ // 路由表 ├── stores/ // pinia 状态 ├── views/ // 页面登录、仪表盘、排行、门店分析 ├── App.vue └── main.js这里我有个习惯所有接口请求都放在api/下而不是在各组件里直接写axios.get。这样以后后端接口地址变化只改一个文件。组件只关注接收数据并传给图表职责非常清楚。4.2 Composition API 与 Options API 的选择为什么新页面都用 setup我第一版是用 Vue2 的 Options API 写的后来迁移到 Vue3 时发现销量分析页面有筛选、loading、图表、导出等好几个状态用data定义变量就会超过十几个methods里的方法按字母排序看着很累。换成script setup后我可以把“获取趋势数据”相关的变量和函数紧挨着写逻辑内聚明显更强。以趋势图为例核心代码结构大概是这样script setup const loading ref(false) const trendData ref([]) const queryParams reactive({ startDate: , endDate: , storeId: null }) async function fetchTrend() { loading.value true try { trendData.value await salesApi.getTrend(queryParams) } finally { loading.value false } } watch(queryParams, fetchTrend, { deep: true }) onMounted(fetchTrend) /script这里用reactive包筛选条件用watch监听筛选变化自动刷新请求。思路比 Options API 直观很多不用再写watch: { queryParams: { deep: true }}那套略显绕的写法。如果你是从 Vue2 转过来的建议直接忘掉data和methods的分隔把“一个功能的所有逻辑”当成分组的唯一标准。4.3 用 ECharts 把接口数据“翻译”成图表销量趋势图我用的是 ECharts 折线图。接口返回的date_key、total_quantity、total_amount需要转换成 ECharts 的xAxis和series结构。注意total_amount是 decimal 类型后端会转成字符串返回前端要转成数字再绘制否则折线会显示成字符串拼接的异常形状。商品排行图我用了一个柱状图加一个横向条形图按数量 Top10 展示。具体转换逻辑不复杂但对一个分析系统来说图表的提示文案和空数据展示很重要。懒加载场景经常有人不看图表所以我加了 ECharts 的datazoom和tooltip配置方便快速缩放区间看细节。图表组件应该独立封装。我的做法是写了一个BaseChart.vue组件内部处理init、resize、setOption和dispose页面只用传一个option对象。这样不管是折线图还是饼图都用同一个组件避免在每个页面都去写 ECharts 的生命周期代码整洁很多。4.4 开发环境跨域与生产环境部署的两种设置方式前后端分离后开发环境最常见的就是跨域报错。Vue 的 dev server 跑在 5173SpringBoot 跑在 8080浏览器直接请求跨域。我的处理方式是在vite.config.js里配置代理export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })前端所有接口路径都以/api开头开发时代理会把它转到 SpringBoot。生产环境则不同我会把 Vue 打包后的dist文件放进 SpringBoot 的src/main/resources/static由 SpringBoot 一起提供静态资源这样整个应用只有一个 jar 包内部完全同源不存在跨域问题。这个方案非常适合内部系统省去了 Nginx 的配置环节。5. 部署实录一个 jar 搞定所有功能以及我踩过的几个版本坑这个项目如果只是本地跑起来其实很简单但部署到线上、换个环境跑问题就来了。我把自己遇到过的几个典型问题展开说说这些问题在搜索热门词里出现频率也很高说明不是个别现象。5.1 JDK、Node、MySQL 的版本组合建议先说结论我用的组合是 JDK 11、SpringBoot 2.7.18、Node 18、MySQL 8.0。这个组合相对保守兼容性最稳。SpringBoot 3.0 开始要求 JDK 17很多小伙伴下载了最新版 JDK 21一导入项目就直接编译报错。如果你的项目是基于源码跑先看清楚 pom 里的 SpringBoot 版本。如果 spring-boot-starter-parent 写的版本以 3.x 开头就必须用 JDK 17 或更高如果以 2.x 开头建议用 JDK 8 或 JDK 11。太多时候问题不是出在代码上而是版本之间不匹配。MySQL 8 自带caching_sha2_password认证方式如果你用老版本的 mysql-connector-java会连不上数据库。源码里我用的驱动版本是 8.0.33并且在 JDBC URL 中加了参数下一个节详细说。5.2 把 Vue 打包进 SpringBoot 的 static 目录实现单 jar 部署前端路由如果使用history模式会有点麻烦。因为打包进 SpringBoot 后刷新某个子路由时 Tomcat 找不到对应的路径会返回 404。我的处理方法有两个第一把前端路由改成createWebHashHistory()这样 URL 里会出现#服务器始终只认index.html第二配置一个 SpringBoot 的WebMvcConfigurer让所有非 API 的路径都转发到/index.html。第一种最简单适合内部系统但#在 URL 里不好看。第二种体验更好需要加一段代码Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }注意我这里让带有.的路径不转发是为了避免静态资源文件被错误指向。再用npm run build生成dist把dist下的文件复制到src/main/resources/static最后执行mvn clean package就能得到一个包含前端和 API 的完整 jar 包。5.3 排查“SSL 连接错误”和“时区异常”的完整过程第一次连 MySQL 8 的时候我启动项目直接报了一个Could not create connection to database server后面跟着SSL connection error。我当时第一反应是改 URL然后把useSSLfalse加进去但发现还是报只是错误变成了Public Key Retrieval is not allowed。后来查了 MySQL 8 的加密插件机制才明白MySQL 8 默认要求客户端用 RSA 公钥加密密码但驱动默认不自动获取服务器公钥。完整解决方式是在 JDBC URL 上同时加上两个参数jdbc:mysql://localhost:3306/smart_home?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai第serverTimezone参数是为了解决第二个坑MySQL 驱动 8.x 要求必须指定服务器时区。如果漏了会报The server time zone value йʱ is unrecognized乱码一样的时间。这个警告其实已经算很友好了但没经验的人看到满屏乱码会以为编码坏了。所以这三个参数我建议不管有没有报错都直接写上能省很多事。我把排查过程梳理成一个简单的 checklist确认 MySQL 服务启动3306端口可登录。确认驱动坐标版本与 MySQL 版本匹配。确认连接串带useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai。如果依然报认证失败检查 MySQL 用户的host是否允许远程访问。如果超时检查服务器防火墙和云厂商安全组是否放行端口。5.4 SpringBoot 版本太高导致的兼容性问题与降级方案有次我图新鲜升级到 SpringBoot 3.2 之后项目里原来写的javax.*import 全部编译失败因为 Jakarta EE 把包名改成了jakarta.*。如果源码里大量代码用的是javax.servlet、javax.validation升级到 SpringBoot 3 会非常痛苦。我的实际做法是除非有必须用 JDK 17 的理由否则 jrabo 这类内部系统保持 SpringBoot 2.7 更省心。MyBatis 在 SpringBoot 3 下也要使用mybatis-spring-boot-starter的 3.x 版本不能再沿用 2.x。很多在线编译报错就是从这个组合错位来的。所以我把源码固定在这个相对稳定的版本上并在pom.xml里注释了版本兼容说明方便后续维护。6. 从这套 jrabo 源码继续演进我推荐的二次开发方向项目做完之后我并没有停在“能跑”这一步。销量分析系统的价值主要来自迭代下面这几个方向是我实际用过或验证过的写出来供你参考。6.1 引入 Redis 缓存高频统计目前最耗时的接口是首页大盘因为它要把多个维度的查询一次性算出来。如果业务人员每天上午集中打开页面数据库会被连续打超过几百次。我后来给大盘接口加入了 Redis 缓存第一次请求时查 MySQL把结果存到 Redis 里有效期设 5 分钟。这样同一时间段的重复查询不会继续打到 MySQL 上。缓存的 key 设计需要注意不能直接用接口名要把筛选条件拼接进去。比如dashboard:sales:2025-01-01:2025-01-31:storeId1:categoryId3。如果忘了组合条件就会出现用户切换门店后看到还是上一家门店数据的幻觉这种 bug 在业务上很严重。6.2 用定时任务同步 ERP 数据真实业务中订单数据可能不在 MySQL 里而是躺在 ERP 系统、电商后台或者进销存软件中。我的扩展做法是写了一个定时任务每天晚上 12 点增量同步前一天的数据。实现上就是 Spring 的Scheduled(cron 0 0 0 * * ?)注解把外部订单拉取到本地库再更新维度表。数据分析系统的实时性要求不高T1 完全够用定时任务比消息队列轻得多。做这一步的前提是明细表必须有update_time或者create_time字段否则增量同步很难判断哪些是新数据。建议在建表时就考虑不要等数据量大了再回头补。6.3 要不要迁移到 MyBatis-Plus换与不换的边界有人建议我直接用 MyBatis-Plus 代替 MyBatis单表 CRUD 不用写 XML确实方便。但我的建议是如果项目以复杂统计查询为主原生的 MyBatis 更可控因为聚合 SQL 完全按需手写不会因为方法自带的条件构造器写出不易读的复杂逻辑。如果后续还要做很多单表维护页面比如商品管理、门店维护换成 MyBatis-Plus 就能节省大量时间。最理想的折中方案是保留 MyBatis同时引入 MyBatis-Plus 仅作为代码生成器的输出基础。生成实体类、Service、Controller 之后再根据需求在 XML 里补充复杂 SQL。总之工具是为人服务的不要因为“大家都用”就盲目迁移要看你日常开发的重心在哪。最后再分享一个小技巧数据分析类项目的核心竞争力不是技术多炫而是“维度灵活、口径准确”。无论怎么迭代采购、运营、财务对“销量”的统计口径很可能不一样有人按发货算有人按支付算。这套系统在订单状态字段里预留了FINISHED和CANCELLED的区分就是为了避免后续因为口径问题返工。你在自己的项目里也一定要先把指标定义清楚再写复杂 SQL。