ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的翡翠仓库进销存管理系统设计与实践

基于SpringBoot+Vue的翡翠仓库进销存管理系统设计与实践 做翡翠贸易这行最怕的不是行情波动而是货和账对不上。原石、毛料、成品镯子摆了一仓库谁经手、谁出库、卖了多少、还剩多少靠Excel来回传月底一核对全是窟窿。前阵子我帮朋友做了一套“基于SpringBoot的翡翠仓库进销存管理系统”前端选了Vue后端用SpringBoot整体按四个业务角色来做权限和流程隔离算是把这一摊事捋顺了。今天把这套系统的设计与实操过程完整拆出来给正在研究SpringBootVue进销存开发的朋友做个参考。这套系统解决的核心问题很直接仓库里每一件翡翠的入库、出库、调拨、盘点都有据可查采购、销售、仓管、管理层各管各的环节但所有数据最终都在同一个平台上汇合。对于有类似需求的中小规模珠宝贸易商、玉石加工厂、零售门店或者拿这个当毕业设计和面试项目的开发者这套方案都有很强的移植性。下面从架构设计、数据库、后端实现、前端实现到踩坑记录按实际开发顺序从头讲。1. 整体架构与四角色设计思路1.1 为什么锁定SpringBoot Vue这套组合先说技术选型的事。市面上做管理系统能用的组合很多Python的Django、PHP的Laravel、Go的Gin都有人用但回到“翡翠仓库进销存”这个具体场景SpringBootVue几乎是风险最低的选择。原因不复杂。第一SpringBoot在后端领域的基础设施太成熟了Spring Security做认证权限、MyBatis-Plus操作数据库、Redis做缓存和会话管理这些组件随便一拼就是一套完整的后端服务开发效率很高。第二Vue在前端生态里上手门槛低组件化开发对进销存这种页面多、表单密、表格繁重的业务非常友好Element UI或者Ant Design Vue一套组件库就能把后台管理界面搭得又快又整齐。第三前后端分离这个模式在中小型项目里已经成了默认选项后端只出接口前端只管页面团队协作或者一个人单干都好维护。实际开发中我还有一个很实在的感受SpringBoot的问题在网上几乎都能搜到答案。翡翠进销存系统虽然业务上有点行业特性但底层仍然是标准的增删改查加库存计算用这套组合遇到卡点排查成本比冷门框架低一个量级。这一点在项目后期尤其重要因为业务逻辑写完之后真正的耗时大头都花在权限控制、数据一致性、并发处理这些细节上框架本身的稳定性直接决定你要不要加班。1.2 四个角色到底怎么划分权限进销存系统的角色划分不能拍脑袋。很多项目上来就做“管理员”和“普通用户”两个角色放到翡翠仓库这个场景里根本不够用。仓库管理、采购进货、门店销售、老板巡查这四类人的需求互相冲突销售只想看到货品信息和价格不想也不能改库存采购需要掌握补货节奏但不该看到销售成本和利润细节仓管负责实物的进出库必须对数量负责但无权调整售价老板需要全局视角又不能陷入具体单据的录入。基于这个矛盾我最终把系统拆成了四个角色角色核心职责菜单与操作范围数据可见范围系统管理员系统配置、用户管理、数据维护全部菜单含角色权限配置、基础数据管理全局数据仓库管理员入库、出库、调拨、盘点、库存查询库存管理、入出库单据、盘点模块全部货品与库存记录采购人员供应商管理、采购订单、补货建议采购模块、供应商管理、采购入库单供应商与采购相关数据销售人员客户管理、销售订单、销售出库销售模块、客户管理、销售出库单可售货品与售价不含成本价这个权限矩阵看起来简单但落地时有一层容易忽视的细节除了页面菜单级别的权限还要做数据级别的隔离。比如销售人员能查到“库存可用数量”但不能看到“库存成本单价”采购人员能看到当前库存水位但不能覆盖仓库的盘点权限。菜单权限用路由来控制数据权限则要在后端接口的参数里带上角色条件比如查询库存列表时销售角色的接口只返回上架状态并且过滤掉利润敏感字段。这两层权限叠加起来才是完整的角色隔离。2. 数据库设计与进销存核心模型2.1 围绕“一物一码”设计货品主数据翡翠货品的管理和普通标准品不一样。一箱螺丝钉可以按统一规格建SKU但两只同样标称“冰种飘花手镯”的翡翠实际质量、尺寸、价格可能完全不同。所以这套系统的货品主数据必须采用“一物一码”的思路每一件入仓的翡翠都要建立独立的货品档案相当于给每件货一个唯一的身份证号。货品档案表我命名为t_goods关键字段包括货品编号唯一编码、类目原石/毛料/成品、品名、种水、颜色、重量、尺寸、图片路径、供应商ID、成本单价、销售单价、存放库位ID和当前状态。种水颜色这些字段在翡翠行业里是定价的核心依据必须单独建字段而不是塞进备注里因为后续要根据这些属性做库存统计和销售分析。类目我这里刻意分成了三级一级类目原石、半成品、成品二级类目比如成品下分手镯、挂件、摆件、戒面三级属性用标签字段来记录比如“冰种”“糯种”“满绿”“飘花”。这种设计的好处是库存汇总时可以随时切换统计维度想看“所有手镯一共多少件”还是“冰种手镯多少件”一条SQL就能解决不用改表结构。2.2 用流水表加余额表解决库存准确性进销存系统最核心的设计决策在于库存数据不能只存一个“当前数量”。很多初学者把库存数量直接做成货品表里的一个字段入库加、出库减表面上看没问题但一旦遇到单据作废、修改、重复提交这个字段就会变得不可追溯。更稳妥的做法是拆成两张表库存余额表和库存流水表。库存余额表t_stock保存每个货品在每个库位的实时数量只有入库、出库、调拨、盘点确认这四个操作能修改它而且修改必须发生在同一个数据库事务里。库存流水表t_stock_log则记录每一次库存变动的明细包含货品ID、变动类型采购入库/销售出库/调拨入/调拨出/盘盈/盘亏、变动数量、变动前后的库存快照、关联单据号、操作人和操作时间。这两张表配合起来有个实际作用就是任何一个时间点的库存数据都可以重放验证。比如月底发现某件货数量不对不用去猜是哪一单出了问题直接查流水表按时间轴一条条核对很快就能定位到具体操作和责任人。这也是翡翠这类高价值货品特别需要的能力——账实不符的时候追责和复盘必须有效率。2.3 采购、销售、盘点三大单据的主从表设计进销存的业务动作最终都要落到单据上单证分离是标准化管理的核心。我设计了采购订单、采购入库单、销售订单、销售出库单和盘点单五大类单据全部采用“主表明细表”的结构。以采购入库单为例主表t_purchase_in保存单据编号、供应商ID、入库仓库、入库时间、经办人、审核状态和备注明细表t_purchase_in_item保存该单下每一件货品的信息包括货品ID、数量、入库单价、货品类目等。为什么要拆主从表因为一张入库单可以包含多件不同货品如果只做一张大宽表数据冗余会非常严重而且后续扩展每件货品的独立属性时根本没法设计字段。主从表在代码层面也对应了事务边界保存采购入库单时主表和明细表要么一起写入要么一起回滚绝不允许出现有头无尾的脏数据。这个过程我在后端统一封装在Transactional事务方法里后面会详细讲具体实现。3. SpringBoot后端核心实现3.1 项目结构划分与依赖配置后端的工程结构直接决定了后续开发体验。我按照业务边界做了一个相对标准的划分避免所有Controller堆在一起com.jade.stock ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis-Plus数据访问层 ├── entity // 数据库实体 ├── dto // 接口参数对象 ├── vo // 返回视图对象 ├── common // 通用工具、返回值封装、异常处理 ├── config // 配置类安全、Redis、跨域等 └── utils // 工具类依赖方面我选的是SpringBoot 2.7.x搭配MyBatis-Plus 3.5.x、Spring Security、Redis和MySQL 8.0。为什么不用SpringBoot 3.x这个问题我在开发前也犹豫过但考虑到很多生产环境依赖的组件对jakarta命名空间的兼容还需要踩坑2.7.x在稳定性和资料丰富度上更有优势。如果你的项目是全新启动且没有历史包袱直接上3.x也没问题但最好确认一下MyBatis-Plus和Spring Security的版本配套。3.2 登录认证与四角色权限控制权限这块我直接用的Spring Security加JWT方案没有引入更重的Shiro也没有用Spring Cloud那种级别的微服务权限体系因为这个项目的规模决定了简单就是最好的维护性。登录认证的流程不复杂用户提交账号密码后端校验通过后生成JWT令牌令牌里除了用户ID、用户名还声明了一个关键字段roleCode用来区分是管理员、仓库、采购还是销售。前端拿到令牌后存到本地每次请求在请求头里带Authorization: Bearer xxx后端通过过滤器解析令牌把用户信息放到SecurityContext里供后续使用。角色权限控制分了两层。第一层是接口级别的权限校验使用Spring Security的PreAuthorize(hasRole(ADMIN))注解不同角色的Controller方法上标注不同的访问要求。第二层是数据级别的过滤比如查询销售出库单时销售角色只能看到自己创建的订单// 销售角色查询订单列表自动追加操作人条件 public PageResultSaleOrderVO querySaleOrder(SaleOrderQuery query) { // 从SecurityContext中取当前登录用户 LoginUser user SecurityUtils.getLoginUser(); if (user.isSales()) { query.setCreateBy(user.getUserId()); } PageSaleOrder page saleOrderMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), buildQueryWrapper(query) ); return PageResult.of(page); }这层逻辑放在Service层而不是Controller层确保每个入口的权限过滤都统一收口不会漏掉某个接口造成越权。数据权限的bug通常不是显性的安全性问题而是销售看到了不该看的采购成本价这种问题一旦发生业务上的信任感很难修复。3.3 采购入库、销售出库的事务实现进销存业务里事务管理是绝对的红线。采购入库和销售出库都涉及多张表的联动操作任何一个环节出问题库存数据就会变成一笔糊涂账。这里我直接贴一段采购入库的核心Service代码把关键点拆开讲Transactional(rollbackFor Exception.class) public PurchaseInResult createPurchaseIn(PurchaseInDTO dto) { // 1. 校验供应商状态 Supplier supplier supplierMapper.selectById(dto.getSupplierId()); if (supplier null || supplier.getStatus() ! 1) { throw new BusinessException(供应商不存在或已禁用); } // 2. 生成单据编号并保存主表 PurchaseIn purchaseIn new PurchaseIn(); purchaseIn.setOrderNo(generateOrderNo(PI)); purchaseIn.setSupplierId(dto.getSupplierId()); purchaseIn.setWarehouseId(dto.getWarehouseId()); purchaseIn.setStatus(0); // 待审核 purchaseInMapper.insert(purchaseIn); // 3. 循环保存明细并同步库存余额和流水 for (PurchaseInItemDTO item : dto.getItems()) { PurchaseInItem detail new PurchaseInItem(); detail.setPurchaseInId(purchaseIn.getId()); detail.setGoodsId(item.getGoodsId()); detail.setQuantity(item.getQuantity()); detail.setPrice(item.getPrice()); purchaseInItemMapper.insert(detail); // 锁定库存行防止并发超卖 Stock stock stockMapper.selectByGoodsIdAndWarehouseForUpdate( item.getGoodsId(), dto.getWarehouseId()); if (stock null) { stock new Stock(); stock.setGoodsId(item.getGoodsId()); stock.setWarehouseId(dto.getWarehouseId()); stock.setQuantity(item.getQuantity()); stockMapper.insert(stock); } else { stock.setQuantity(stock.getQuantity() item.getQuantity()); stockMapper.updateById(stock); } // 写库存流水 insertStockLog(item.getGoodsId(), dto.getWarehouseId(), 1, item.getQuantity(), stock.getQuantity(), purchaseIn.getOrderNo()); } return new PurchaseInResult(purchaseIn.getId()); }这段代码里有两个关键点值得单独说。第一是Transactional(rollbackFor Exception.class)必须指定rollbackFor为Exception.class因为Spring事务默认只回滚运行时异常RuntimeException如果业务代码里抛的是受检异常不指定的话事务不会回滚库存就白白改了。第二是操作库存余额时用了selectByGoodsIdAndWarehouseForUpdate也就是加了FOR UPDATE行级锁。这个锁的作用是防止两个人同时入库同一件货导致库存叠加计算出现丢失更新。虽然翡翠仓库这种业务并发量一般不大但作为一套要长期稳定运行的系统这个保险必须加。3.4 盘点流程的数据一致性处理盘点这个功能在进销存里属于“纠偏”环节逻辑上比普通出入库要复杂。实物数量经仓管清点后系统里记录的账面数量可能不一致这就需要盘盈盘亏的确认操作。盘点单的流程我设计为先创建盘点单并冻结当前库存快照然后仓管录入实际盘点数量系统自动对比账面数并计算盈亏最后提交审核时一次性更新库存余额并生成盈亏流水。这个过程中最关键的一点是“冻结”动作也就是创建盘点单的那一刻要把货品的当前数量复制到盘点单明细里后续所有对比都基于这个快照而不是实时查库存表。这样做的好处是如果盘点过程中有其他出库单在走盘点结果不会被中途变化干扰。盘点的库存更新同样放在事务中而且我先检查盘点单的状态机只有“已审核”状态的单据才允许修改库存防止同一张盘点单被重复提交两次造成库存凭空翻倍或归零。4. Vue前端实现要点4.1 前端工程结构与路由设计前端这边我用的是Vue 3加ViteUI库选择Ant Design Vue状态管理用的Pinia。Vite的启动速度比Webpack时代的体验好太多日常开发热更新几乎是毫秒级响应对进销存这种大量表单联调的场景帮助很大。工程结构上按模块拆成views、router、store、api、components几个目录。views下面按角色业务再分子目录admin、warehouse、purchase、sales、system每个模块下的页面只放自己相关的文件。api目录则对应后端的Controller接口每个模块一个JS文件统一封装axios请求。路由设计是整个权限控制的前端入口。我在路由表里给每个路由声明了meta信息包括roles数组比如{ path: stock/purchase-in, name: PurchaseIn, component: () import(/views/purchase/PurchaseIn.vue), meta: { title: 采购入库, roles: [ADMIN, PURCHASE] } }用户登录成功后前端拿到当前用户的角色用Router.beforeEach做一个前置守卫遍历所有路由表把不包含当前角色的路由全部过滤掉。这个过滤逻辑不仅实现了菜单的动态显示还从根本上防止了用户直接在URL地址栏输入路径绕过菜单访问无权页面。4.2 动态菜单与角色视图切换动态菜单的实现思路不算复杂但有不少细节。我的做法是后端登录接口返回用户信息和角色编码前端根据角色编码在前端本地维护一份角色-菜单映射表渲染对应的侧边栏菜单。这里有一个关键的选型取舍就是菜单配置放前端还是后端。我最终选择了前端维护映射原因是这个项目的菜单和角色是相对稳定的不需要运营人员在后端动态配置权限菜单。把映射写在前端省掉了一次查询菜单表的网络请求页面加载更轻快。如果业务发展到需要管理员后台自定义菜单再放后端也不迟前期完全没必要增加复杂度。菜单渲染的核心代码如下const menuTree computed(() { const role authStore.userInfo.roleCode return filterMenu(menuConfig, role) })filterMenu就是递归遍历菜单配置根据每个菜单项的meta.roles判断是否保留。这个方案改起来也直观要给某个角色加一个新菜单项改一行配置就行不用动页面代码。4.3 核心页面的交互设计进销存系统的核心页面绕不开三个货品列表、入库单创建、出库单创建。货品列表页我用的是Ant Design Vue的Table组件配合SearchForm做筛选条件。翡翠货品筛选条件比较多类目、种水、颜色、重量区间、库存状态都要支持但搜索区域不能太挤所以我把一级筛选条件做成顶栏下拉高级条件放到“展开”面板里。表格列方面货品图片使用缩略图成本价和售价字段根据角色动态隐藏这个逻辑对应后端返回的字段权限标识。入库单创建页是整个系统里最容易让用户抱怨的页面。采购人员录入一单多件货品时如果每件货都要开新页面录入效率极低。所以我用了“明细表格动态行编辑”的交互方式主表信息在上方中间是明细行每行可以选择货品并填入数量、单价底部有“添加一行”按钮一次可以录入几十件货再统一提交。这个交互的实现要特别注意行数据的校验漏填数量、选了重复货品都要在提交前拦截。4.4 与后端的接口联调规范联调阶段经常出现前端拿到的数据格式和后端不一致或者时间字段类型没法直接渲染等问题。我把联调规范定成了几条硬性约定后端返回对象统一包裹在{ code, message, data }结构里分页数据固定用{ total, records }格式日期时间字段统一返回字符串并指定yyyy-MM-dd HH:mm:ss格式。这样前端axios响应拦截器只需要处理一次结构解包后面所有页面都不用关心数据是封装了多少层。还有一个容易被忽略但实际很影响体验的问题文件上传。翡翠货品需要上传图片而且图片数量不少我用了前端直传对象存储的方案后端接口只负责接收上传后返回的URL。这样避免了后端Tomcat存储大图片再返回二进制流的性能问题页面加载货品列表时的压力也小很多。5. 常见问题与排查技巧实录5.1 高频问题与快速排查表前面把架构和实现都过了一遍这段分享一下实际调试中最常遇到的一批问题按症状、原因和解决办法整理成一张速查表方便大家遇到类似情况时直接定位。问题症状常见原因排查与解决登录后前端反复重定向到登录页JWT过期时间设置太短或前端axios拦截器误判响应码检查Token有效期确认后端未登录返回码统一为401前端拦截器只在401时清除登录态我明明配了接口权限但另一个角色也能访问PreAuthorize注解放在私有方法上或配置类中放行了该接口Spring AOP对私有方法不生效把注解放到Controller的public方法上并检查SecurityConfig的permitAll列表多行明细提交时只有部分明细保存成功主表和明细表没在同一个事务中或者自增主键尚未回填给Service方法加Transactional(rollbackFor Exception.class)入库后立即获取主表ID再写明细两个人同时入库库存数量比实际少了库存更新语句没有加行级锁两个会话同时读到同一旧值使用SELECT FOR UPDATE锁行或者用UPDATE ... SET quantity quantity #{num}的原子SQL写法前端菜单和角色不匹配路由表meta的roles写错或后端登录接口没有返回角色编码核对路由meta与后端角色枚举值一致并在Vue Devtools里查看登录态存储的角色值盘点单提交后发现库存被重置错乱没有用盘点快照而是实时读取库存或者盘点单重复提交创建盘点单时保存库存快照字段审核时用快照对比实盘数审核状态加幂等判断图片上传后前端无法预览文件对象存储的访问域名和业务域名跨域在对象存储的Bucket权限里配置跨域规则或者统一使用对象存储的默认域名5.2 并发场景下如何验证库存正确性进销存系统的并发问题平时不太容易暴露但一到月底盘点就会现原形。我在联调时专门写过一个并发测试脚本模拟多个线程同时提交入库和出库单然后核对最终的库存余额是否与流水一致。方法是开启20个线程每个线程随机执行入库或出库操作最后用SQL把该货品的所有流水变动量累加对比库存余额表。如果不一致说明事务控制或锁实现存在问题。这个验证脚本只花了一个下午的时间就发现了两个bug一个是乐观锁版本号没有叠加另一个是某些更新操作直接用实体对象更新时把库存字段为空的记录覆盖成0了。写进销存的库存逻辑时建议把“库存余额所有流水累计变动”这个等式当成单元测试的硬性校验每次调整业务代码后跑一遍能省掉不少上线后的脏数据麻烦。5.3 我的几个核心避坑心得最后的避坑心得其实都是血泪教训换来的。第一条权限设计一定要从第一天就做不要觉得系统简单就先不做权限等页面多了再补到时候每个接口都要回头改改动量是前期的十倍。第二条翡翠货品的图片路径和唯一编号千万不要让用户手工输入编号必须由系统自动生成图片路径从上传接口返回否则数据质量和录入效率都没谱。第三条所有的删除操作都别用物理删除一律用逻辑删除标记字段。这个原则在进销存里尤其重要因为单据关联了库存流水物理删除会直接把历史的审计链路撕断后期查账和追责都会变成一件不可能完成的事。第四条数据库表字段的注释一定要写清楚特别是货品属性这一块种水、颜色、尺寸这些字段每个团队叫法可能都不一样你不写注释三个月后自己来看也要猜半天。6. 个人经验收尾这套翡翠仓库进销存系统从需求梳理到上线前后花了大概四周的时间。我最大的体会是进销存系统的复杂程度不在于CRUD本身而在于库存这种有状态的数据在多人协作、多角色并发的场景下如何保持一致性。做权限设计时多花点心思后面能省掉大量扯皮和返工。技术选型上SpringBoot加Vue这套组合对这类传统业务管理系统来说依然是最省心的选择社区成熟、招人容易、解决问题快不存在“技术太老”的问题。如果你也要做类似项目我建议先把四角色的权限矩阵和数据看板梳理清楚再动手写第一行代码。单据、流水、余额这三层数据结构虽然前期要多建几张表但上线之后你就知道这个设计有多值钱。最后再分享一个小工具上的细节前端调试时我强烈建议装上Vue Devtools配合路由白名单排查菜单权限问题效率会比盲改代码高很多。
返回列表