news 2026/10/15 13:17:42

SpringBoot+Vue企业级进销存系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue企业级进销存系统实战

1. 项目概述:当SpringBoot遇上Vue的企业级进销存实践

去年接手公司老旧进销存系统改造时,我面临着一个典型的技术抉择:是继续维护那套基于JSP的祖传代码,还是用现代技术栈彻底重构?最终选择了SpringBoot+Vue的全栈方案,不仅将系统响应速度提升了3倍,还让新同事的上手时间从两周缩短到两天。这套技术组合拳在企业管理系统中展现出的威力,值得用一篇实战总结来记录。

企业进销存管理系统本质上是对"物流、资金流、信息流"的三流合一管控。传统方案往往陷入两个极端:要么是功能臃肿的ERP套件,要么是Excel表格的简单电子化。而SpringBoot+Vue的组合恰好找到了平衡点——后端用SpringBoot实现稳定的业务逻辑处理,前端用Vue构建灵活的操作界面,中间通过RESTful API进行数据交互。这种架构下,库存预警的实时推送从原来的轮询5秒缩短到WebSocket的毫秒级响应,采购单的生成时间也从分钟级降到秒级。

2. 技术选型背后的逻辑拆解

2.1 为什么是SpringBoot而不是SpringMVC?

在2015年之前,我们团队确实还在用SpringMVC+XML配置的方案。但每次新增一个商品分类字段,都要修改至少5个文件:实体类、DAO接口、Mapper文件、Service层、Controller层。SpringBoot的约定优于配置原则让这一切变得简单:

// 商品实体类 @Data @TableName("goods") // MyBatis-Plus注解 public class Goods { @TableId(type = IdType.AUTO) private Long id; private String code; private String name; // 省略其他字段... } // 对应的Mapper接口 public interface GoodsMapper extends BaseMapper<Goods> { // 基础CRUD方法已自动继承 }

配合MyBatis-Plus的代码生成器,新增字段现在只需要修改实体类后重新生成模板代码。实测显示,常规业务模块的开发效率提升了60%以上。

2.2 Vue在前端领域的优势体现

对比过Angular和React之后,选择Vue的核心原因在于其渐进式特性。我们的老系统需要分阶段改造,Vue可以逐个功能模块替换,而不用一次性重写整个前端。这个优势在库存管理模块改造时尤为明显:

  1. 先用Vue重写了库存查询页面,保留原有的JSP提交逻辑
  2. 逐步将表单提交改为axios异步请求
  3. 最后用Vue-Router实现完整的前端路由

这种渐进式迁移让系统始终保持可用状态,业务部门几乎没感知到技术改造的过程。

3. 核心模块实现细节

3.1 采购管理中的并发控制

采购入库时最怕遇到超卖问题。我们采用Redisson分布式锁+版本号乐观锁的双重保障:

public Result purchase(PurchaseDTO dto) { String lockKey = "stock_lock:" + dto.getGoodsId(); RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待100ms,锁持有时间30s if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) { Goods goods = goodsMapper.selectById(dto.getGoodsId()); if (goods.getStock() + dto.getAmount() > goods.getMaxStock()) { return Result.error("超过最大库存限制"); } // 使用版本号防止并发修改 goods.setStock(goods.getStock() + dto.getAmount()); goods.setVersion(goods.getVersion()); if (goodsMapper.updateById(goods) > 0) { // 记录采购流水... return Result.success(); } return Result.error("操作冲突,请重试"); } } finally { lock.unlock(); } }

3.2 销售出库的事务管理

销售出库需要同时更新库存、生成出库单、扣减客户账户余额,必须保证事务原子性:

@Transactional(rollbackFor = Exception.class) public Result sale(SaleDTO dto) { // 1. 校验库存 Goods goods = goodsMapper.selectById(dto.getGoodsId()); if (goods.getStock() < dto.getAmount()) { throw new BusinessException("库存不足"); } // 2. 扣减库存 goods.setStock(goods.getStock() - dto.getAmount()); goodsMapper.updateById(goods); // 3. 生成出库单 OutboundOrder order = new OutboundOrder(); // 设置订单属性... outboundOrderMapper.insert(order); // 4. 更新客户账户 CustomerAccount account = accountMapper.selectByCustomerId(dto.getCustomerId()); account.setBalance(account.getBalance() - dto.getTotalAmount()); accountMapper.updateById(account); return Result.success(order.getId()); }

这里特别注意@Transactional的默认回滚范围是RuntimeException,而我们的BusinessException是继承自Exception的,必须显式声明rollbackFor。

4. 前后端协作的实战技巧

4.1 API设计规范

我们制定了严格的接口规范来保证前后端协作效率:

  1. 统一响应格式:
{ "code": 200, "message": "success", "data": { // 业务数据 }, "timestamp": 1630000000000 }
  1. 错误码分级:
  • 4xx:客户端错误(如400参数错误,401未授权)
  • 5xx:服务端错误(如500系统异常,503服务不可用)
  1. 分页统一结构:
{ "records": [], "total": 100, "size": 10, "current": 1 }

4.2 Vuex状态管理实践

对于频繁使用的基础数据(如商品分类、仓库列表),我们采用Vuex进行全局管理:

// store/modules/baseData.js const state = { categories: [], warehouses: [] } const mutations = { SET_CATEGORIES(state, data) { state.categories = data }, SET_WAREHOUSES(state, data) { state.warehouses = data } } const actions = { async loadBaseData({ commit }) { const [catRes, whRes] = await Promise.all([ api.getCategories(), api.getWarehouses() ]) commit('SET_CATEGORIES', catRes.data) commit('SET_WAREHOUSES', whRes.data) } }

在App.vue的created钩子中触发初始化加载:

created() { this.$store.dispatch('baseData/loadBaseData') }

5. 性能优化关键点

5.1 数据库索引规划

经过慢SQL分析,我们在以下字段建立了组合索引:

  • goods表的category_id和status
  • stock表的warehouse_id和goods_id
  • order表的create_time和customer_id

特别注意避免索引失效的情况:

-- 不会使用category_id索引 SELECT * FROM goods WHERE status = 1 AND category_id + 0 = 101 -- 使用覆盖索引优化 -- 原SQL: SELECT * FROM goods WHERE category_id = 101 -- 优化为: SELECT id, name, price FROM goods WHERE category_id = 101

5.2 前端懒加载策略

对于商品列表这类可能数据量大的模块,我们采用分页+虚拟滚动方案:

<template> <div class="goods-list" @scroll="handleScroll"> <div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div> <div class="scroll-content" :style="{ transform: `translateY(${offset}px)` }"> <div v-for="item in visibleData" :key="item.id" class="goods-item"> <!-- 商品内容 --> </div> </div> </div> </template> <script> export default { data() { return { allData: [], // 全部数据 visibleData: [], // 可视区数据 itemHeight: 80, // 每项高度 offset: 0, // 偏移量 visibleCount: 10 // 可视区域显示数量 } }, computed: { totalHeight() { return this.allData.length * this.itemHeight } }, methods: { handleScroll(e) { const scrollTop = e.target.scrollTop this.offset = scrollTop const start = Math.floor(scrollTop / this.itemHeight) this.visibleData = this.allData.slice( start, start + this.visibleCount ) } } } </script>

6. 典型问题排查实录

6.1 N+1查询问题

初期在获取订单列表时出现了严重的性能问题,检查发现是经典的N+1查询:

// 错误示例 List<Order> orders = orderMapper.selectList(queryWrapper); orders.forEach(order -> { User user = userMapper.selectById(order.getUserId()); order.setUserName(user.getName()); });

解决方案:

  1. 使用MyBatis-Plus的@TableField注解进行关联查询
@Data public class Order { private Long userId; @TableField(exist = false) private String userName; } // Mapper接口 @Select("SELECT o.*, u.name as user_name FROM order o LEFT JOIN user u ON o.user_id = u.id") List<Order> selectOrderWithUser();
  1. 或者使用ResultMap实现结果映射
<resultMap id="orderWithUser" type="Order"> <id property="id" column="id"/> <result property="userId" column="user_id"/> <result property="userName" column="user_name"/> </resultMap> <select id="selectOrderWithUser" resultMap="orderWithUser"> SELECT o.*, u.name as user_name FROM order o LEFT JOIN user u ON o.user_id = u.id </select>

6.2 前端内存泄漏

在商品编辑组件中发现内存持续增长,经排查是未解绑的全局事件:

// 错误示例 mounted() { window.addEventListener('resize', this.handleResize) } // 正确做法 mounted() { window.addEventListener('resize', this.handleResize) }, beforeDestroy() { window.removeEventListener('resize', this.handleResize) }

对于定时器也同样需要注意:

data() { return { timer: null } }, mounted() { this.timer = setInterval(() => { this.refreshData() }, 5000) }, beforeDestroy() { clearInterval(this.timer) }

7. 安全防护方案

7.1 接口防刷策略

对于登录等敏感接口,我们采用Redis实现限流:

public boolean tryAcquire(String key, int maxCount, int expireSeconds) { String redisKey = "rate_limit:" + key; Long count = redisTemplate.opsForValue().increment(redisKey); if (count != null && count == 1) { redisTemplate.expire(redisKey, expireSeconds, TimeUnit.SECONDS); } return count != null && count <= maxCount; } // 在Controller中使用 @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { String clientIp = getClientIp(); if (!rateLimitService.tryAcquire(clientIp, 5, 60)) { throw new BusinessException("尝试次数过多,请稍后再试"); } // 正常登录逻辑... }

7.2 XSS防护方案

前后端协同防御XSS攻击:

  1. 后端统一过滤:
public String cleanXSS(String value) { if (StringUtils.isEmpty(value)) { return value; } return HtmlUtils.htmlEscape(value); }
  1. 前端显示时使用vue-sanitize:
import VueSanitize from "vue-sanitize"; Vue.use(VueSanitize); // 在模板中使用 <div v-html="$sanitize(htmlContent)"></div>
  1. 设置HTTP安全头:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.headers() .xssProtection() .and() .contentSecurityPolicy("script-src 'self'"); } }

8. 部署架构设计

8.1 容器化部署方案

采用Docker Compose编排服务:

version: '3' services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: root123 volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d ports: - "3306:3306" redis: image: redis:6 ports: - "6379:6379" volumes: - ./redis/data:/data backend: build: ./backend ports: - "8080:8080" depends_on: - mysql - redis frontend: build: ./frontend ports: - "80:80"

8.2 持续集成流程

GitLab CI配置示例:

stages: - build - test - deploy backend-build: stage: build script: - cd backend - mvn clean package artifacts: paths: - backend/target/*.jar frontend-build: stage: build script: - cd frontend - npm install - npm run build artifacts: paths: - frontend/dist deploy-prod: stage: deploy script: - scp backend/target/*.jar prod-server:/app - ssh prod-server "docker-compose up -d --build" only: - master

9. 项目演进方向

9.1 微服务化改造

随着业务复杂度提升,我们计划按功能模块拆分服务:

  1. 基础数据服务:商品、分类等基础信息管理
  2. 采购服务:供应商管理、采购流程
  3. 库存服务:实时库存、仓库管理
  4. 销售服务:客户管理、销售流程

使用SpringCloud Alibaba套件:

  • Nacos:服务注册与配置中心
  • Sentinel:流量控制与熔断降级
  • Seata:分布式事务解决方案

9.2 移动端适配方案

基于uniapp开发跨平台移动端:

  1. 共用现有后端API
  2. 对高频操作接口增加缓存层
  3. 使用websocket实现实时库存提醒

核心优势:

  • 一套代码同时生成iOS、Android、小程序
  • 复用现有Vue技术栈,学习成本低
  • 与管理系统共享业务逻辑

在库存盘点等移动场景下,实测显示使用手机扫码录入比PC端键盘输入效率提升40%以上。这种渐进式的架构演进,既保证了系统的持续可用性,又能不断吸收新技术带来的红利。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:17:41

MINIPCIE开发板实战:蜂窝模组AT指令调试与物联网方案验证指南

1. 一块小板子背后的门道&#xff1a;MINIPCIE开发板到底能干什么 第一次拿到这块板子的时候&#xff0c;我盯着上面密密麻麻的焊盘和那个标准MINIPCIE座子看了半天。说实话&#xff0c;如果你平时只玩过树莓派或者Arduino那种排针一插就完事的板子&#xff0c;MINIPCIE这个形态…

作者头像 李华
网站建设 2026/10/15 13:17:04

Proteus 8.13最小系统仿真:从点亮LED到波形验证全流程

简介&#xff1a;本资源是面向单片机初学者与电子类专业学生的Proteus仿真学习套包&#xff0c;聚焦电路设计、嵌入式开发与软硬协同调试核心能力培养&#xff0c;有效解决无硬件条件下的单片机项目实践难题。压缩包含400余个完整可运行的Proteus仿真工程&#xff08;.pdsprj为…

作者头像 李华
网站建设 2026/10/15 13:16:41

Oracle笔试面试题深度拆解:从SQL底层机制到实战能力迁移

1. 为什么Oracle的笔试题总让人“一看就会&#xff0c;一做就废”聊Oracle笔试面试题之前&#xff0c;先得把一件事说透&#xff1a;Oracle的题目设计思路和很多互联网大厂完全不是一个路子。互联网公司偏爱算法题&#xff0c;考的是你临场推导的能力&#xff1b;而Oracle这类企…

作者头像 李华
网站建设 2026/10/15 13:16:33

AWGN信道下BPSK与QPSK误码率对比:从理论到Simulink仿真全解析

简介&#xff1a;这份课程设计文档围绕MATLAB/Simulink环境下的通信系统建模与仿真展开&#xff0c;以AWGN信道为背景&#xff0c;系统比较BPSK与QPSK两种数字调制方式的误码率性能。正文先从BPSK与QPSK的调制解调原理出发&#xff0c;介绍了相位键控的基本思想、相干解调结构及…

作者头像 李华
网站建设 2026/10/15 13:16:25

FDE大模型抢手岗位:收藏这份超全学习指南,小白也能逆袭!

本文介绍了AI行业最抢手的FDE岗位&#xff0c;强调其不仅需要技术能力&#xff0c;更需要主动性、成熟度和优秀的技术实现过程。文章以Palantir公司为例&#xff0c;指出该岗位更看重应聘者的主动发现问题、解决实际问题的能力&#xff0c;以及向非专业人士清晰表达的能力。国内…

作者头像 李华