资讯动态

Spring Boot + Vue进销存系统:四角色权限模型与库存事务实战

发布时间:2026/9/10 9:24:06 来源:尧图企业网站定制
做了好几个进销存之后我越来越觉得这玩意儿看着简单想做好真不容易。表面上是商品、供应商、客户那一堆CRUD实际上最核心的问题是多角色协同下的数据一致性——采购员下的单怎么流转到仓库销售员开的票怎么扣减库存管理员怎么把控全局这些都是有讲究的。所以这次写一个 Spring Boot Vue 前后端分离的进销存管理系统重点聊聊“四个角色”这套权限模型怎么落地。如果你正在做类似的中小型管理系统或者准备用 Spring Boot 和 Vue 做项目练手这篇文章应该能帮你少走不少弯路。1. 项目整体设计与角色权限建模1.1 四个角色怎么拆不是拍脑袋定的角色拆分的合理性直接决定了项目后期的开发成本。这个系统里我最终定了四个角色管理员、采购员、销售员、仓管员。管理员管人、管配置、看数据。用户管理、角色分配、商品分类维护、全局数据统计归他管。采购员负责补货。创建采购单、管理供应商、跟踪采购订单状态。销售员负责出货。创建销售单、管理客户信息、查看自己的销售记录。仓管员负责库存实物。执行入库、出库、盘点、查询库存台账。可能有人会问一个小系统而已搞四个角色不是自找麻烦吗直接一个管理员账号全搞定不就行了。说实话如果只是自己用一个超级管理员确实够了。但一旦系统要真正给企业用权限不拆数据全是乱的——采购员要是能改库存数字仓库月底对账能查到怀疑人生销售员要是能看到所有商品成本价那客户谈判就不好做了。拆成四个角色还有一个好处每个角色的工作台Dashboard聚焦自己的核心任务登录进来不迷茫。采购员看采购待审核、在途订单仓管员直接看到今日待入库、待出库销售员看到自己的销售额指标管理员看全盘库存健康度。1.2 为什么选 Spring Boot Vue 这套组合这个选择比较主流但我也想说说背后的实际原因。后端选 Spring Boot说白了就是生态成熟、约定大于配置、招人容易。Spring Boot 的自动装配机制帮我们把大量重复配置吃掉了——引入spring-boot-starter-web就有一套内嵌 Tomcat 的 Web 环境引入spring-boot-starter-data-jpa或 MyBatis-Plus 就能快速操作数据库。尤其是我这种做过好几个项目的Spring Boot 的项目结构已经形成肌肉记忆Controller 收参数、Service 写业务、Mapper 访问数据库分层清晰出问题了也容易定位。有人会纠结用 JPA 还是 MyBatis-Plus。我的建议是如果业务报表复杂、SQL 需要精细控制MyBatis-Plus 更顺手如果模型关系复杂、希望少写 SQLJPA 的 Hibernate 能省不少事。进销存这种系统库存台账、销售统计报表非常多我选了 MyBatis-Plus。前端选 Vue核心是组件化开发和响应式数据绑定让后台管理系统开发效率非常高。一个商品选择器组件写好了采购单、销售单、盘点单都能复用一个表格封装好了所有列表页直接套。而且 Vue 的路由、状态管理Vuex/Pinia都成熟做权限控制很方便。前后端分离最大的好处是权限控制灵活。后端只认 Token 不认 Session前端根据角色动态渲染菜单和按钮两头各管一摊互不干扰。1.3 RBAC 表设计与数据模型四个角色要做权限控制最经典的就是 RBAC基于角色的访问控制模型。这个模型说白了就五张表用户表sys_user账号、密码BCrypt 加密、姓名、手机号、状态。角色表sys_role角色编码admin/purchaser/seller/keeper、角色名称、备注。菜单表sys_menu菜单名称、路由地址、组件路径、权限标识如purchase:order:create、类型目录/菜单/按钮。用户角色关联表sys_user_role一个用户可多角色一个角色可多用户。角色菜单关联表sys_role_menu角色和菜单多对多。菜单表的核心是权限标识perms这是控制按钮级权限的关键。比如“创建采购单”这个按钮权限标识是purchase:order:create采购员角色有这个权限销售员角色没有那销售员登录后页面上根本不显示这个按钮就算手动调接口后端也会拦截 403。数据模型上还有个坑要注意商品表和库存表一定要分开。商品表存商品名称、规格、单位、进价、售价、分类库存表存商品 ID、仓库 ID、当前库存量、锁定库存量、预警阈值。因为一个商品可能在多个仓库有库存不拆开的话后面没法扩展多仓库。2. 后端 Spring Boot 核心实现2.1 工程结构与依赖配置我习惯按功能模块分包而不是按技术分层分包。进销存按模块分大概是这样的com.example.erp ├── common // 通用返回结果、异常处理、工具类 ├── config // 配置Security、MyBatis-Plus、Cors ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 入参出参对象 ├── security // JWT 过滤器、登录认证 └── ErpApplication.java模块包命名上可以按purchase、sale、inventory、system再分一层这样采购相关接口都放在controller/purchase下找代码效率高很多。依赖选型方面核心依赖就这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency版本这里多说一句Spring Boot 版本不是越高越好。Spring Boot 3.x 要求 JDK 17且javax.servlet变成了jakarta.servlet很多老项目升级容易踩坑。如果团队习惯 JDK 8直接用 Spring Boot 2.7.x 最稳妥。我这次用的是 2.7.18配 JDK 8稳得一匹。2.2 JWT 登录与权限控制落地进销存系统登录认证我用了 JWT方案很成熟。原理就不多展开了直接说落地步骤。第一步登录接口生成 Token用户提交用户名密码后端用AuthenticationManager校验密码是否正确。密码加密方式用的是 BCrypt前端传明文后端比对 BCrypt 哈希。千万不要用 MD5——现在 GPU 跑 MD5 碰撞太容易了BCrypt 自带盐且计算慢安全性高得多。校验通过后生成 JWT把用户 ID、用户名、角色编码放进去String token Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(roles, user.getRoles()) .setExpiration(new Date(System.currentTimeMillis() 86400000)) // 24小时 .signWith(secretKey, SignatureAlgorithm.HS256) .compact();第二步JWT 过滤器校验 Token写一个JwtAuthenticationTokenFilter继承OncePerRequestFilter在doFilterInternal里拿请求头Authorization: Bearer xxx解析 Token然后把用户信息放进SecurityContextHolder。String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); Claims claims JwtUtil.parseToken(token); if (claims ! null) { UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(claims.getSubject(), null, getAuthorities(claims.get(roles))); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response);第三步接口权限控制在 SecurityConfig 里放开登录接口和静态资源其余全部要求认证http.authorizeRequests() .antMatchers(/api/auth/login).permitAll() .anyRequest().authenticated();方法级别用 Spring Security 的PreAuthorize注解控制。比如创建采购单PreAuthorize(hasAuthority(purchase:order:create)) PostMapping(/purchase/order) public Result createPurchaseOrder(RequestBody PurchaseOrderDTO dto) { return purchaseOrderService.createOrder(dto); }这样即使有人绕过前端直接调接口后端也会拦住。接口权限是安全底线前端隐藏菜单只是体验优化这个顺序不能搞反。另外提一下 Token 过期策略我设置了 24 小时过期前端在 axios 响应拦截器里发现 401 就跳转登录页。更完善的做法是加一个 refreshToken 实现无感刷新但小系统没必要24 小时重新登录一次完全可以接受。2.3 库存扣减的正确姿势事务与锁进销存系统最核心、最容易出问题的就是库存扣减。进货入库要加库存销售出库要减库存如果多人同时操作很容易出现超卖——库存剩 3 件两个销售员同时开单各卖 2 件一不留神库存变成 -1。方案一数据库乐观锁在库存表加一个version字段更新时带上版本号UPDATE inventory SET quantity quantity - 2, version version 1 WHERE product_id 1001 AND warehouse_id 1 AND version 3;影响行数为 0 就说明版本冲突需要重试。这种方式适合并发不高的场景实现简单。方案二悲观锁行锁查询库存时用SELECT ... FOR UPDATE锁住这行数据事务提交后才释放Inventory inv inventoryMapper.selectForUpdate(productId, warehouseId); if (inv.getQuantity() demand) { throw new BusinessException(库存不足); } inv.setQuantity(inv.getQuantity() - demand); inventoryMapper.updateById(inv);我在销售出库这个环节用的是悲观锁。因为销售出库不仅扣库存还要生成出库流水、记录销售明细、可能还要更新客户欠款涉及多张表更新必须在一个事务里互相可见锁行能保证同一商品不会被并发扣超。关键是要给 Service 方法加Transactional(rollbackFor Exception.class)保证任何一步失败都能回滚。事务和锁要配合使用——只加锁不加事务锁就白锁了只加事务不加锁还是会超卖。2.4 核心接口清单与权限要求以下几类接口是系统中最重要的我把角色权限也列在表里方便你对照设计模块接口功能权限要求认证POST /api/auth/login登录公开商品管理GET /api/product/page分页查询商品所有登录用户商品管理POST /api/product新增商品管理员采购管理POST /api/purchase/order创建采购单采购员采购管理PUT /api/purchase/order/{id}/audit审核采购单管理员采购管理PUT /api/purchase/order/{id}/receipt采购入库仓管员销售管理POST /api/sale/order创建销售单销售员销售管理PUT /api/sale/order/{id}/delivery销售出库仓管员库存管理GET /api/inventory/list库存台账查询仓管员/管理员库存管理GET /api/inventory/warning库存预警列表管理员库存管理POST /api/inventory/check创建盘点单仓管员统计分析GET /api/dashboard/summary首页统计管理员这里有个设计原则业务角色和操作角色分离。采购员可以“创建采购单”但是“采购入库”这个动作一定要仓管员来执行。好处是职责分明采购员负责商务仓管员负责实物两边做一遍校验出错概率就低很多。这在企业管理上叫“不相容职务分离”虽然听起来很正规但本质就是为了防止一个人从头到尾说了算。3. 前端 Vue 实现要点3.1 工程初始化与目录规划前端我用 Vite 构建 Vue 3 项目。对比 Vue CLIVite 启动速度快太多开发体验好不少。创建命令npm create vitelatest erp-web -- --template vue项目目录结构src ├── api // 接口请求封装 │ ├── auth.js │ ├── purchase.js │ ├── sale.js │ └── inventory.js ├── assets ├── components // 公共组件商品选择器、员工选择器等 ├── router // 路由配置 ├── store // Pinia 状态管理 ├── views // 页面 │ ├── dashboard // 工作台 │ ├── purchase // 采购管理 │ ├── sale // 销售管理 │ ├── inventory // 库存管理 │ └── system // 系统管理 ├── utils │ ├── request.js // axios 封装 │ └── auth.js // token 存取 └── App.vue接口请求封装是前端的一个关键点。我统一封装了一个request.js所有请求都走这同一个实例import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } )前端 UI 组件库我选的是 Element Plus。后台管理系统选它最稳表格、表单、弹窗、分页组件开箱即用不需要自己费劲去写基础组件。3.2 动态路由与菜单权限控制这是前端权限控制的核心。思路是登录返回角色和菜单信息前端根据菜单动态注册路由。第一步登录后拉取用户信息和菜单const res await login(username, password) localStorage.setItem(token, res.data.token) const userInfo await getUserInfo() store.setUser(userInfo)第二步根据菜单生成路由后端返回的菜单结构大概是这样的[ { path: /purchase, component: Layout, name: Purchase, meta: { title: 采购管理, icon: ShoppingCart }, children: [ { path: order, component: purchase/order/index, name: PurchaseOrder, meta: { title: 采购订单, perms: [purchase:order:list] } }, { path: supplier, component: purchase/supplier/index, name: Supplier, meta: { title: 供应商管理, perms: [purchase:supplier:list] } } ] } ]前端遍历菜单数组用router.addRoute()动态注册路由。Vue Router 4 里重复添加同名路由会报警告所以要先处理一下// 添加之前先移除同名路由 if (router.hasRoute(route.name)) { router.removeRoute(route.name) } router.addRoute(route)第三步路由守卫控制访问router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.path /login) { next(/) } else { next() } })按钮级权限我用了一个自定义指令v-permission这样页面里写起来很简洁el-button v-permissionpurchase:order:create typeprimary 新建采购单 /el-button指令的实现逻辑是检查当前用户的权限列表里是否包含该权限标识不包含就直接把 DOM 元素移除。3.3 核心页面与状态管理进销存的前端页面虽然多但大部分都是**“列表 搜索表单 新增/编辑弹窗 详情抽屉”**的组合模式。我把几个容易踩坑的点说一下。商品选择器组件这个一定要抽出来。采购单和销售单里都要选商品会涉及到商品编号、名称、规格、单位、当前库存、进价/售价。我把这个组件封装成支持远程搜索的表格弹窗父组件只用传一个v-model绑定选中的商品非常省事。这里面有个细节是销售单选商品时要显示实时可用库存采购单选商品时要显示上次进价两个业务场景的诉求不一样所以组件要支持自定义列配置。computed 的典型使用场景比如采购单明细里数量 × 单价 金额金额自动汇总成总金额这种派生数据非常适合用 computed 而不是在事件里手动算。const totalAmount computed(() { return items.value.reduce((sum, item) { return sum item.quantity * item.price }, 0) })状态管理Pinia我主要存两类数据用户信息含角色和权限列表和全局字典商品分类、单位。字典数据在登录后拉取一次存 Pinia 里页面中直接用避免每个页面都去请求。商品分类这种变更多少的字典可以在用户操作后主动刷新一次。4. 四个角色如何串联业务流4.1 从采购到入库的完整链路进销存的业务流不是孤立的每个角色做的事情都是链条上的一环。我拿“采购补货”来走一遍完整流程。第一步采购员创建采购单采购员发现某商品库存低于预警值或者就是计划要备货选择供应商添加商品明细填写数量、采购价提交采购单。此时采购单状态为“待审核”。第二步管理员审核采购单管理员检查采购单价是否合理、供应商是否正规。审核通过状态变为“已审核待入库”如果审批不通过退回给采购员修改。第三步仓管员执行入库货物到了仓管员打开采购单点“入库”。系统校验数量是否超出采购数量不能超量入库然后执行事务操作更新采购单状态为“已完成”、库存表quantity增加、写入库存流水表。这里关键是要在同一个事务里完成因为采购单状态和库存数量必须保持一致。如果先加库存再更新状态中途发生异常数据库就脏了。4.2 销售出库与退货回滚销售员开销售单时选择客户、添加商品然后点击“提交”。这里有两个方案方案 A销售单提交即扣库存。好处是实时保证库存准确坏处是客户可能反悔销售单还没出库就扣了库存容易引起争议。方案 B销售单提交不扣库存出库时才扣。我选的是方案 B。销售单状态是“待出库”仓管员真正发货时才扣库存。中间还有一道反悔机会。退货场景处理退货单审核通过后库存要加回去同时生成一笔红字销售流水。这里注意退货数量不能大于原单销售数量否则就是恶意操作。4.3 盘点与预警机制盘点单是仓管员的重要工作。系统里创建一个盘点单会列出该仓库的全部商品和账面库存。仓管员填写实盘数量系统自动计算盘盈盘亏。if (checkQty systemQty) { // 盘盈库存增加生成盘盈记录 } else if (checkQty systemQty) { // 盘亏库存减少生成盘亏记录 }盘点单提交后需要管理员审核审核通过才真正更新库存。为什么要审核因为盘亏可能涉及赔偿责任不能仓管员自己说少就少了。预警机制也很简单库存表有个warning_threshold字段定时任务每天扫描一次把库存低于阈值的商品写入预警表。管理员的首页工作台会展示预警列表点击可以直接补货。5. 常见问题与排查技巧实录5.1 真实踩坑记录我把开发过程中遇到的最典型的几个问题列出来了先看表格问题原因解决方案登录后调用接口一直 401JWT 过滤器没放行登录接口或 Token 解析失败确认 SecurityConfig 中 permitAll 配置检查 JWT 密钥是否一致前端登录后刷新页面菜单丢失动态路由是登录后 addRoute 的刷新后 Pinia 状态清空在路由守卫里加“刷新时重新拉取用户信息”逻辑库存扣成负数并发请求导致超卖使用悲观锁SELECT ... FOR UPDATE 事务前端访问后端接口跨域报错前后端分离后端口不同后端允许 CORS 或前端用 Vite proxy 代理项目启动报循环依赖Service 互相注入用Lazy注解或重构让依赖方向单向化采购单状态修改成功但库存没变方法没加事务或跨方法调用加Transactional(rollbackFor Exception.class); 注意自调用不走代理401 问题是最常见的。我排查过一次发现是 JWT 过滤器在 SecurityConfig 里的注册顺序不对过滤器没执行到。解决方案是在配置里显式添加http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);跨域问题开发环境我用 Vite 的代理配置解决// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境我通常让 Nginx 统一转发/api路径到后端服务前端代码里不需要处理跨域。循环依赖印象最深的是采购单服务和供应商服务互相调用启动报错。后来我把公共逻辑抽到了一个独立的PurchaseOrderSupportService里两个服务都只依赖它问题就没了。5.2 容易忽略的设计细节这几个细节新手特别容易忽略我单独拎出来强调一下。第一逻辑删除要统一。用户删除、商品删除、单据删除不要用物理删除都用deleted字段标记。进销存是业务系统单据一旦删除历史对账就缺了一环。MyBatis-Plus 有TableLogic注解配好之后自动带逻辑删除条件。第二单据编号要有业务含义。纯自增主键不适合做单据编号进销存的单据号最好包含日期和流水号比如PO20250218001采购单和SO20250218001销售单。生成规则简单业务上也好查——看到编号就知道是哪天的单子。第三操作日志要留痕。谁在什么时间审核了哪张采购单、谁修改了商品价格这些都要记录。我用 Spring AOP 做了一个操作日志切面拦截带Log注解的方法把操作人、操作类型、操作内容写入日志表。出问题追责的时候特别有用。第四金额计算全部用 BigDecimal。千万不要用 Double 或 Float 做金额运算0.1 0.2 在浮点数世界里是一个尴尬的存在。BigDecimal 虽然写起来啰嗦但这是钱不能丢一分。6. 部署与后续扩展建议6.1 打包部署前端打包npm run build产物在dist目录把静态文件交给 Nginx 托管即可。后端打包mvn clean package -DskipTests生成erp.jar直接跑java -jar erp.jar --spring.profiles.activeprod生产环境数据库连接、Redis 配置放在application-prod.yml里通过 Spring Boot 的多环境配置机制分离。我一般用application-{profile}.yml的方式开发环境一个配置、生产环境一个配置互不干扰。更工程化的做法是打成 Docker 镜像部署。写个 DockerfileJDK 8 基础镜像mvn package之后构建镜像用 Docker Compose 编排 Spring Boot、MySQL、Redis、Nginx 四个容器一套环境就起来了。6.2 后续可以扩展的方向这个系统跑起来之后可以往外扩展的方向不少。扩展一销售统计报表。目前系统有大量明细数据但缺少报表展示。可以加一个统计模块按商品、按客户、按时间段聚合销售额、毛利、销量排行用表格展示导出 Excel 就够用了。扩展二文件上传下载。供应商送货单、客户签收单这些纸质单据可以扫描成附件挂到对应订单下。Spring Boot 处理文件上传下载不算复杂配置好文件存储路径、限制上传大小、做好文件访问权限控制就行。扩展三对接扫码枪/打印机。仓库出入库时用扫码枪扫描商品条码自动填充商品信息效率提升非常大。发货时打印出库单和快递面单也可以接入。扩展四多仓库支持。如果企业有多个仓库库存表需要增加warehouse_id维度采购入库选择目标仓库销售出库选择发货仓库。调拨从 A 仓转到 B 仓也要加进来核心逻辑和出入库类似。做这类管理系统我最大的体会是技术框架只是基础业务理解才是主导。Spring Boot 和 Vue 的语法网上都有教程但“采购单什么时候能改价格、销售单冲销怎么处理、盘点差异谁负责”这种业务规则才是系统真正值钱的地方。代码写不完的业务规则才是骨架。最后再分享一个小技巧这个我用了好几个项目都觉得很值每个表都带上create_time、update_time、create_by、update_by这四个通用字段MyBatis-Plus 的自动填充功能帮我们维护后面做审计、做统计、排查数据问题节省的时间远超出初始的那一点建模成本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价