拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+Vue+MySQL+MyBatis构建智能无人仓库管理系统

最近帮一个做仓储的团队落地了一套智能无人仓库管理系统用的就是标题里那套经典组合——SpringBoot做后端、Vue搭前端、MySQL存数据、MyBatis管持久层。整套项目做下来最大的感触是所谓智能和无人真正的门槛根本不在什么高深算法上而在于库存模型设计得够不够严谨、出入库流程能不能扛住并发、权限模型是不是真的清晰。这篇就把整个项目的设计思路、核心代码逻辑和那些文档里不会写的坑逐一拆开聊聊希望能给正在做类似系统或者准备拿这类题目做毕设的同学一些参考。1. 项目整体设计与技术选型拆解1.1 先搞清楚无人仓库系统到底在管什么很多人在动手写仓库管理系统之前根本没有想清楚仓库这个业务场景到底在流转什么。我习惯用一个生活化的类比来解释仓库管理系统本质上就是个超级家庭收纳账本。你家里的衣柜、储物间就是库区和库位衣服就是货物你把某件衣服放进衣柜是入库拿出来穿是出库每季度整理一次就是盘点。家庭收纳只需要脑子记但仓库的规模放大一千倍以后就必须靠系统了。具体到智能无人仓库系统核心闭环是这么一条线供应商送货到达仓库系统生成入库单货物按规则分配到库位库存数据累加下游业务需要领料或者发货系统校验库存充足后生成出库单库存数据扣减过程中任何账实不符的情况通过盘点操作来校准。整个闭环里无人不是指没有工作人员而是指减少人工记录、人工核对、人工决策这些低价值重复劳动让系统自动完成数据流转和异常预警。所以功能模块就可以顺着这条闭环拆出来基础数据模块用户、角色、菜单、供应商、货物管理模块货物档案、分类、包装规格、仓库布局模块库区、库位、出入库作业模块入库单、出库单、审核流程、库存管理模块库存台账、库存预警、调整、盘点模块盘点单、盘点差异、统计报表模块库存报表、出入库流水、周转分析。这一套模块拆完整个系统的业务边界就非常清晰了。1.2 技术选型SpringBootVueMySQLMyBatis的取舍逻辑技术选型这件事我向来不太喜欢追新。做业务系统第一原则是团队最容易维护、生态最成熟、能找到最多参考资料的组合。SpringBootVueMySQLMyBatis这套组合在中小企业后台管理系统里几乎是标准答案不是没有道理。先看SpringBoot。它解决了传统Spring配置地狱的问题内嵌Tomcat容器一个jar包就能跑起来自动配置机制把大量样板式配置收拢了。做仓库系统这种典型CRUD业务规则的项目SpringBoot的开发效率确实没得说。关键是它几乎没有上手门槛任何一个会Java的同学都能快速进入状态。再看Vue。仓库管理系统的特点是表单密集、表格密集、交互状态多Vue这种基于组件的响应式框架配合Element UI这类成熟组件库开发管理后台页面非常顺手。而且前后端分离架构下前端只需要调后端接口业务逻辑完全被隔离在后端接口层职责清晰分工明确。MySQL和MyBatis就更不用多说了。MySQL在中型数据量下性能、稳定性、社区支持的综合表现最均衡。而MyBatis这类半自动ORM的最大优势是SQL是我们自己手写的遇到联表查询、动态条件、复杂报表SQL的时候自由度非常高。JPA虽然开发快但复杂查询的调优空间很有限。这个系统里大量涉及多表关联统计比如按库区汇总库存、按月统计出入库流水手写SQL反而最可控。为什么不选微服务说句实话一个仓库管理系统撑死了几十个接口、几百张表的规模强行拆微服务只会引入分布式事务、服务治理一堆额外复杂度。对于这种单体就够用的项目过度设计就是最大的坑。1.3 项目工程结构与开发环境规划后端工程是标准Maven结构我按功能模块分包而不是按技术层分包这样代码的聚合度更高。大致结构是controller层只做参数接收和结果包装service层承载业务逻辑mapper层是MyBatis的接口定义配合XML文件放SQL语句。对于一个仓库系统来说这个分层足够清晰没有引入DDD那种更重的架构。前端工程用Vue CLI构建主要目录包括views放页面组件、router放路由配置、store放Vuex状态、utils放axios封装和工具函数、api目录统一放接口方法。前后端开发联调时我在vue.config.js里配了一个devServer代理把/api前缀的请求转发到后端8080端口避免开发阶段频繁处理跨域。数据库规划我用的是MySQL 8.0字符集统一utf8mb4因为要支持货物名称里的特殊符号排序规则用utf8mb4_general_ci就够没有必要上utf8mb4_unicode_ci。连接池用HikariCPSpringBoot 2.x默认自带最大连接数设置为20对这类并发规模完全够用。2. 数据库设计与MyBatis持久层细节2.1 MySQL核心表结构与关键字段设计数据库设计是整个系统的地基。有些同学一上来就急着建表写代码结果做到库存模块发现字段不够用再回头改表结构代价非常大。我建议先花半天时间把所有表关系和关键字段梳理清楚再动手。这个系统里我建了大概20张表核心的是这么几张用户表、角色表、菜单权限表、货物表、库区表、库位表、库存表、入库单主表和明细表、出库单主表和明细表、盘点单主表和明细表。挑几个重点字段设计来讲。首先是库存表这张表我专门加了唯一约束组合字段是库位id加货物id。为什么要加唯一约束因为同一个库位不可能存在两行同一货物的库存记录如果业务上来了两条说明数据被写脏了。这个约束不是可有可无的装饰它是最后一道数据防线。第二数量字段类型。库存数量、出入库数量一律用DECIMAL(12,2)不要用Integer。仓库里有大量非整数计量的货物比如某种原材料按公斤入库重量可能就是0.75这样一个数。用整数类型会导致四舍五入误差日积月累下来库存账目就乱了。金额字段用DECIMAL(12,2)就不用赘述了。第三逻辑删除字段。每张业务表我都预留一个deleted字段默认0。虽然占了一点存储但是换来了数据可追溯和误删可恢复。尤其是出入库单这种业务单据逻辑删除是必须的物理删除等于销毁证据。第四数量相关的索引设计。出入库明细表里查询频率最高的条件是货物id、出入库单id和时间范围所以我把货物id和创建时间做成了联合索引(goods_id, create_time)。这样按货物查流水、按时间截断统计都能直接命中索引不用回表。我把核心表的关系做了个汇总表名用途关键字段关联关系t_user用户id, username, password, role_id多对一角色t_role角色id, role_name, remark一对多用户t_goods货物档案id, goods_name, spec, unit, safety_stock一对多库存t_warehouse_area库区id, area_name, area_type一对多库位t_warehouse_location库位id, location_code, area_id多对一库区t_stock库存台账id, goods_id, location_id, quantity, version唯一约束(goods_id, location_id)t_inbound_order入库单主表id, order_no, supplier_id, status, inbound_time一对多明细t_inbound_order_item入库单明细id, order_id, goods_id, quantity, location_id多对一货物t_outbound_order出库单主表id, order_no, customer_id, status, outbound_time一对多明细t_stock_check_order盘点单id, check_no, status, check_time一对多明细2.2 MyBatis动态SQL与复杂查询实践MyBatis是这个系统里数据访问层的主力其中用得最频繁的就是动态SQL。举一个典型场景入库单明细批量插入。仓库一次入库可能包含几十种货物如果用for循环单条insert性能差不说数据库连接压力也大。我用foreach标签做批量插入一次提交整个批次的明细实测插入效率提升了十几倍。insert idbatchInsertItems parameterTypelist INSERT INTO t_inbound_order_item ( order_id, goods_id, quantity, location_id, create_time ) VALUES foreach collectionlist itemitem separator, (#{item.orderId}, #{item.goodsId}, #{item.quantity}, #{item.locationId}, NOW()) /foreach /insert另外一个高频场景是库存查询的多条件组合。用户在前端页面上可能同时指定货物名称、库区、库位、库存区间等条件但这些条件都是可选的直接用一条固定SQL没法应付。MyBatis的if标签正好解决这个问题条件为空就不拼入SQL。这样做还有个额外好处就是SQL本身能明确看到不同条件组合下的执行计划方便后续优化。分页方面我用了PageHelper这个MyBatis分页插件。用法非常简单在查询前调用PageHelper.startPage(pageNum, pageSize)紧跟其后的第一条查询就会被自动拦截并分页。但这里有个非常容易踩的坑如果你在多表联查时使用了嵌套结果映射PageHelper的count查询可能会统计出错。因为插件生成的count语句是把你的原查询包一层如果原查询里带group by或者distinct统计行数就不对会导致分页总条数异常。我最终的解决方案是复杂统计类查询不走PageHelper手写count查询再把总条数回填到分页结果对象里确保总条数准确。2.3 MyBatis缓存机制的经验总结MyBatis的缓存知识点这系统里也用到了但我的经验是能不开二级缓存就不开。MyBatis的一级缓存是SqlSession级别的在同一个SqlSession内重复查询会命中缓存但Spring管理的事务环境下SqlSession的创建和关闭非常频繁一级缓存很难跨请求命中所以不用太指望它。二级缓存是Mapper级别的全局缓存确实能减少数据库查询压力但代价是数据一致性风险。库存系统的数据变更非常频繁任何一次入库、出库、盘点都会修改库存表如果开启了二级缓存就必须保证所有修改路径都执行刷新缓存的逻辑。一旦某条路径漏了用户看到的库存数据就是陈旧的这在仓库场景下是很严重的问题。我的建议是仓库系统的核心表一律不开启二级缓存把数据库本身的能力吃透通过合理索引和SQL优化来解决性能问题。事实也证明索引建好之后单表百万级数据量下查询都是毫秒级的根本用不上缓存。3. SpringBoot后端核心流程与安全实现3.1 后端工程搭建与分层设计后端工程我用的SpringBoot 2.7.x版本JDK 1.8Maven管理依赖。选这个版本是因为2.7是2.x的稳定版本不会像3.x那样要求JDK 17对大多数人的本地方便得多。创建工程的方式很简单直接用Spring Initializr生成勾选Web、MySQL驱动、MyBatis依赖后续再加一个PageHelper分页插件和一个Hutool工具包。分层这块我坚持一个原则Controller层绝对不写业务逻辑。你看到的controller层方法就三行代码——接收参数、调用service、返回统一包装结果。业务逻辑全部下沉到service层这样好处是接口层薄了后期做单元测试、做接口文档都会省很多事。统一返回结果类是这个系统里一个不大但很重要的设计。我定义了一个Result类包含code、message、data三个字段全部接口都返回这个结构。前端axios封装里统一判断code是否为200不是200就直接弹出el-message提示。这样前后端交互协议非常统一不存在某个接口返回格式不一样、前端需要特判的情况。全局异常处理也是这个系统里必须的一部分。我写了一个GlobalExceptionHandler用RestControllerAdvice注解标记分门别类处理业务异常、参数校验异常、空指针异常和兜底的Exception异常。业务异常是我自定义的BizException比如库存不足、单据状态不对这类异常在service层直接throw统一在全局处理器里转成code为500的Result返回给前端。这样不会让后端报错的堆栈信息直接暴露给用户也方便前端统一弹出错误提示。3.2 库存事务与并发扣减的核心实现这个系统的核心业务就四个大字出入库。而这份核心业务里最关键的技术点是事务和并发控制。我先把入库流程拆出来讲。入库操作在service层是一个完整的事务方法流程是校验入库单是否存在且处于待审核状态然后遍历入库单明细对每个明细里的货物先查询目标库位上的库存记录是否存在。如果不存在就新建一条库存记录初始数量等于本次入库数量如果存在就在原有数量基础上累加。所有明细处理完成后把入库单状态改成已入库同时写入一条库存流水记录。整个流程用Transactional注解包起来任何一步抛出异常前面所有写操作全部回滚。这样才能保证入库单不会出现单子状态更新了但库存没加或者反过来的半截数据。出库流程比入库多一道关卡——库存够不够和并发扣减。如果你的出库逻辑是先查询库存判断数量充足再执行update扣减那么在高并发下一定会出问题。两个请求同时查询到库存还剩10件都判断可以出库5件然后都执行update结果就是超卖。正确的做法是把判断放入SQL语句中用一次原子update完成扣减和校验。UPDATE t_stock SET quantity quantity - #{outQty}, update_time NOW() WHERE id #{stockId} AND quantity - #{outQty} 0这条update语句的关键是where子句里带着扣减后数量必须不小于0的条件。如果影响行数为0说明库存不足或者数据状态不对这时候事务直接回滚前端拿到明确提示。这条语句本身就是乐观锁思路完全不需要额外的select和代码层判断这也是我对库存扣减最推荐的做法简洁、高效、安全。说个Transactional容易踩坑的细节。Spring的声明式事务默认只有遇到RuntimeException和Error才会回滚如果方法里抛的是受检异常比如Exception事务是不会回滚的。我见过不少同事在这个地方栽过跟头。要么在异常类设计上统一用运行时异常要么在Transactional注解的rollbackFor属性里显式指定。我的习惯是直接把rollbackFor Exception.class写上宁可过度回滚也不能出现脏数据。另一个坑是事务方法内部调用自身方法导致事务失效。比如OutboundServiceImpl里方法A调用同类的方法B而B上面标了Transactional原理上B的事务是AOP代理机制生成的内部this调用走的不是代理对象所以B的事务注解根本不会生效。遇到这种场景要么把B拆到另一个类里要么在类上注入自身代理对象绕过this调用。3.3 基于JWT的轻量级权限控制权限控制这个点上我没有引入Spring Security或者Shiro而是用JWT加拦截器做了一套轻量的方案。做这个选择是被实际项目教训催出来的。之前维护过一个系统用了Spring Security配置类几百行权限表达式写得到处都是结果真正的业务模块只需要登录才能访问管理员部分功能限制这么简单的需求。为了这点需求背一个重框架还要处理它自己一套的过滤器链、认证管理器、异常处理属实不划算。JWT方案的逻辑很直白用户登录时验证用户名密码校验通过后生成一个token里面封装了用户id、用户名、角色编码设置有效期我设的是2小时也可以根据业务调整。这个token返回给前端前端存储起来之后每次请求在请求头里带上Authorization字段。后端写一个拦截器在请求进入controller之前取出token、验证签名、解析出用户信息重新放回请求上下文里后面的业务代码直接取用。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token)) { Claims claims JwtUtil.parseToken(token); if (claims ! null) { request.setAttribute(userId, claims.get(userId)); request.setAttribute(username, claims.get(username)); return true; } } response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; }角色层面的权限校验我用一个自定义注解RequireRole(ADMIN)标在需要管理员权限的controller方法上。拦截器里解析出角色编码后检查是否匹配。这种模式处理仓库系统的权限维度已经足够了因为实际业务角色就仓库管理员、操作员、报表查看者这几类不会复杂到需要动态权限矩阵。前端配合这套方案在axios请求拦截器里统一把token塞进请求头在vue-router的全局前置守卫里判断用户是否登录。没登录就跳转登录页登录了但访问了没有权限的路由就拦截下来给个403页面。整个权限链路做下来大约两百行代码但完全够用而且任何新接手的人都能快速看懂。4. Vue前端页面与交互实现4.1 前端基础工程与配套模块选型前端的技术栈我用的是Vue 2.6加Element UI 2.15。说一句实话Vue 3已经非常成熟但Element UI在Vue 2下的生态积累最深网上踩坑案例最多对做一个仓库管理系统来说反而最省事。如果你非要用Vue 3那就搭配Element Plus问题也不大只是部分组件API用法有细微差异。我身边有人两套都写过结论一致管系统类的项目用稳定熟的那套就够了。前端工程创建用的是Vue CLI也就是npm install -g vue/cli之后执行vue create warehouse-web。创建时我勾选了Router和Vuex其他默认。然后手动安装Element UI、Axios、ECharts和Sass。ECharts这个依赖用来做后面要讲的统计报表图表Sass只是因为个人写样式喜欢嵌套语法纯粹的习惯问题不装也不影响。路由配置上我做了两层设计。第一层是登录页面不需要任何权限就能访问。第二层是主布局页包含左侧菜单、顶部导航和内容区三个区域进入主布局之后的所有页面都以children子路由的方式挂载。菜单导航数据是从后端接口动态获取的这样不同角色登录后能看到不同菜单。实现方式就是登录成功后前端调一次菜单接口拿到该角色的菜单树存到Vuex里再动态生成侧边栏菜单组件。这个设计让系统天然具备用户看到的功能由角色决定的权限模型。4.2 axios封装和关键业务页面实现axios的封装是整个前端复用的基础。我在utils目录下创建request.js核心做了三件事baseURL统一配置为/api请求拦截器从localStorage里取token塞进请求头响应拦截器统一处理后端返回的Result结构。拦截器里的核心逻辑是当后端返回code等于401时说明token过期或者未登录自动清空本地登录状态并跳转登录页。这种统一处理避免在每一个页面的接口回调里重复写登录失效的逻辑。具体页面里最有代表性的三个业务模块是货物管理、出入库管理和盘点管理。货物管理页面是典型的数据表格加搜索表单加分页组合。搜索字段包括货物名称、分类、状态表格列展示货物编码、名称、规格、单位、安全库存等信息。操作列放编辑和删除按钮。这类页面用Element UI的el-table和el-pagination组件组合配合后端接口的分页参数pageNum和pageSize做起来非常顺手。出入库管理页面稍微复杂一点。入库单列表加了新建入库单的按钮点击后弹出对话框对话框里动态维护一个明细表格每一行选择货物、填写数量、选择目标库位。前端在提交前做完整性校验比如明细行里货物不能为空、数量必须大于0。这个校验看起来简单但是一定要做不然脏数据到了后端直接触发异常体验比前端拦截差得多。盘点模块我用了一个小设计点击生成盘点单按钮时前端调用后端接口后端自动把当前所有库位的库存快照生成一张盘点单并返回待填的盘点数量列表。前端展示为一张表格用户填入实际盘点数量提交后由后端逐条比对差异生成盈亏明细。这个设计能让盘点操作和库存数据完全隔离——盘点前先冻结快照盘点中不影响正常出入库非常好用。4.3 数据可视化与ECharts报表展示报表模块是这个系统比较出彩的部分。我用ECharts做了三个图表库存总量柱状图、库存周转率折线图、出入库数量趋势图。这三个图表的数据都来自后端专门的统计接口。后端接口需要返回图表友好的数据结构也就是一组按类目分组的键值对比如按月统计出库数量返回一个包含月份和数量的数组。在实现上前端在页面mounted周期里调用统计接口拿到数据后初始化ECharts实例并设置option。图表的交互相对简单但有一个细节值得注意图表容器需要一个固定高度的div如果高度为0ECharts初始化出来的是空白画布。这个坑我踩过一次后来统一给图表容器设置了400px的最小高度再结合resize事件监听来适配窗口大小变化。需要销毁图表时调用dispose方法否则页面多次跳转ECharts实例累积会导致内存泄漏。4.4 前后端联调中的几个高频坑前后端联调是整个项目周期里问题最集中的阶段我把几个高频问题集中说一下。第一个是跨域问题。开发阶段我已经在vue.config.js里配了代理生产环境用Nginx做了反向代理把前端静态资源和后端接口放在同一个域名下从根源上规避跨域。这里有第二个容易踩的坑如果你把后端接口配置为允许所有跨域并且Nginx也做了跨域配置两个配置同时生效会冲突反而导致某些请求失败。我最终的做法是后端不做任何跨域配置全部交给Nginx统一处理。第二个是时间格式问题。后端接口返回的时间格式是java.util.Date序列化后的时间戳前端拿到后直接显示成一串数字。我在application.yml里配置了Jackson的全局日期格式统一为yyyy-MM-dd HH:mm:ss从源头解决。配置如下spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai第三个是路由模式的404问题。Vue Router默认是hash模式URL里带#号刷新不会404。但如果你把它改成history模式URL变得好看了但直接刷新页面时后端Nginx找不到对应路由会返回404。必须在Nginx配置里加一句try_files指令把所有请求都重写到index.html。这个配置可以说几乎是所有前后端分离项目的标配。5. 常见问题与排坑实录5.1 直接可抄的问题排查表问题现象根因分析解决方案页面表格数据一直转圈不显示前端vue.config.js代理未生效确认代理配置里target端口和后端一致重启devServer后端接口报中文乱码数据库连接URL缺少字符集参数JDBC URL加useUnicodetruecharacterEncodingutf8登录后请求返回401token存储错误或请求头未带确认前端axios请求拦截器统一加了Authorization头刷新页面404Vue Router history模式未配置Nginx回退加try_files $uri $uri/ /index.html;分页总条数不对PageHelper和group by冲突复杂统计查询改手写count查询库存被库存量为负数扣减库存未做原子UPDATE校验改为UPDATE ... WHERE quantity - outQty 0事务没回滚捕获了异常但异常未抛出catch块里必须抛出RuntimeException或者设置rollbackFor5.2 实战经验和优化建议第一个经验是关于SQL性能。库存流水表会持续累积半年之后查出入库流水明显变慢。我给流水查询加了联合索引(goods_id, create_time)并按月度分区。月度分区的好处是超过半年的流水可以整体归档到历史表主表查询更轻快。在这个系统里我建议至少给库存流水表和出入库明细表加上分区设计一开始就做好省得后面数据量上来了再迁移。第二个经验是关于日志。库存系统所有关键操作都要留痕。我专门写了一个操作流水表记录谁在什么时间做了什么操作操作前后数据的变化。这个表在排障时极其有价值。有一次系统里出现库存对不上账我顺着流水表把每一步操作追出来发现是导数据时把一批初始化数据重复导入了一次很快就定位清楚了。这类日志其实实现成本非常低就是一个切面注解的事但价值非常高。第三个经验是关于数据库备份。仓库系统的数据就是企业的资产我建议至少每天做一次全量备份关键业务时段再做增量备份。备份这件事在开发阶段看不出价值多存一个文件又不费事。真到了数据误删、被人为篡改、服务器硬盘损坏的那个时刻才知道备份有多救命。说实话项目扩展到这个阶段还可以继续做的方向很多。比如对接条码扫描枪让入库出库的数据采集更接近无人化后端加一个定时任务每天自动生成库存预警报表发给管理员如果仓库规模更大还能引入RFID批量识别那才是真正意义上的无人仓库。但不管怎么扩展SpringBoot加Vue这套基础架子都不用推倒重来这恰恰是当初把地基打扎实的最大回报。最后分享一个小经验项目结构命名和注释别偷懒。这个仓库系统的代码量不算大但接手维护的人看了代码能快速定位每个模块。我的习惯是controller和service的接口命名跟业务单据一一对应入库单接口就是InboundOrderController方法名就是create、audit、confirm这些动词。简单直接的命名比长篇注释有用得多毕竟代码才是最终的执行结果。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门