ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue仓库管理系统(WMS)全栈开发实战

SpringBoot+Vue仓库管理系统(WMS)全栈开发实战 我做毕设导师这几年仓库管理系统这块几乎每年都有人选。原因很简单不管是本科还是专科SpringBoot Vue这套全栈组合写一个仓储管理平台业务清晰、技术主流、可扩展性强既能体现后端功底又能展示前端界面能力答辩时也有故事可讲。更重要的是这套系统的核心业务——库存变动、出入库流程、库位管理——放到真实企业里也完全说得通做完之后不是“纸上谈兵”的玩具项目。这篇文章我按完整的项目复盘来写从技术选型逻辑、数据库建模、核心业务代码、前端交互设计到高频踩坑实录全部拆开来讲。打算做毕设的、想练全栈的、甚至准备拿这套系统去接私活的都可以按这个思路去复现。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot Vue这套组合选技术栈不能只看“流行”得看你这个项目要解决什么问题。仓库管理系统本质上是个信息管理系统核心诉求是数据要可靠、操作要高效、界面要直观。围绕这三点SpringBoot Vue几乎是最稳的组合。后端用SpringBoot一是因为它的自动配置把大量的样板代码砍掉了二是因为它的生态实在成熟。MyBatis-Plus操作数据库、Spring Security或拦截器做权限控制、Redis做缓存预热、POI导出报表这些在仓库管理系统里都是刚需而它们在SpringBoot生态里都有非常成熟的方案。再加上SpringBoot内置Tomcat打包成jar直接跑部署成本极低——这对毕设验收来说太重要了。前端选Vue核心原因是它处理“数据驱动视图”这件事做得干净利落。仓库管理的页面大多是表格、表单、弹窗、状态流转这些场景下Vue的响应式机制让开发体验非常好。你只管维护数据页面自动跟着变。Vue 3的Composition API出来后逻辑复用也方便了不少同一套库存逻辑可以在列表页和详情页复用。说白了这套组合的核心优势就是后端稳定、前端高效、社区资料多。遇到任何问题搜索引擎随便一翻就有答案这对毕业设计来说是潜在的“隐性分数”。1.2 整体架构与目录规划这个项目我建议采用前后端分离结构但不搞微服务——仓库管理系统的体量单体应用前后端分离就是最优解。多做微服务不是加分项反而会因为分布式事务、服务间调用等问题把自己绕进去。后端按照标准的四层结构来组织com.wms ├── config # 配置类跨域、MyBatis-Plus分页、拦截器注册、Redis配置 ├── controller # 接口层接收请求、参数校验、返回统一结果 ├── entity # 实体类对应数据库表 ├── mapper # 数据访问层继承BaseMapper ├── service # 业务层核心业务逻辑都在这里 ├── utils # 工具类JWT工具、统一返回结果封装、异常处理 └── vo # 视图对象给前端用的聚合数据模型前端同样做好结构划分src ├── api # 每个模块的接口请求函数 ├── router # 路由配置 ├── store # 全局状态管理Pinia ├── views # 页面组件 │ ├── dashboard # 数据看板 │ ├── stock # 库存管理 │ ├── order # 出入库管理 │ ├── report # 报表统计 │ └── system # 用户与权限 ├── layout # 主框架布局 └── utils # axios封装、权限指令等这个分层的逻辑要讲清楚Controller只做“翻译”把HTTP请求转成参数传给ServiceService做真正的业务判断比如库存够不够、单据状态能不能操作Mapper就是纯粹的SQL能力提供者。这样分层的好处是业务规则集中在Service层改起来好找测试也好写。2. 数据库建模与核心模块设计2.1 核心表结构设计仓库管理系统的数据库设计是整个项目的地基也是最能在答辩时展示功力的地方。很多人的表设计是东一张西一张逻辑上串不起来。正确的做法是围绕一条核心理念一切库存变动必须有单据、有流水。核心表我建议这样规划表名用途关键字段sys_user系统用户id, username, password(md5加盐), statussys_role角色id, role_key, role_namesys_menu菜单权限id, parent_id, name, path, iconproduct商品信息id, product_code, product_name, category, unit, pricelocation库位id, location_code, warehouse_name, zonestock实时库存id, product_id, location_id, quantity, safety_stockstock_in_order入库单主表id, order_no, supplier, status, create_timestock_in_item入库单明细id, order_id, product_id, quantitystock_out_order出库单主表id, order_no, customer, status, create_timestock_out_item出库单明细id, order_id, product_id, quantitystock_record库存流水表id, product_id, change_type, change_qty, before_qty, after_qty, order_no这里最难的一步也是最容易忽略的一步就是stock_record库存流水表。很多新手做库存系统直接在stock表的quantity字段上加减这在小数据量下没问题但一旦数据多了、操作频繁了就会出现“不知道库存为什么变了”“某天的出入库没法追溯”的问题。流水表的设计思路就像银行的交易流水每次库存变动不只是改数字还要记录这笔变动的前值、后值、来源单据号、操作人、变动类型。这样任意一个商品的库存轨迹都能查到底排查问题的时候一查一个准。2.2 库存操作的核心流程设计库存系统最核心的三个操作是入库、出库和盘点。我建议把这三个流程做成“单据驱动”的模式而不是直接改库存。入库流程创建入库单选择商品、填数量→ 提交 → 审核 → 审核通过才增加库存 → 写流水。审核人填的是虚拟审核逻辑但流程上要体现“报错校验”的功能比如入库数量不能为负数、商品编号不存在要报错。出库流程创建出库单 → 校验库存是否充足 → 扣减库存 → 写流水。这里有一个重要的业务判断如果库存不足系统应该提示“库存不够当前可用xx件”而不是直接扣成负数。这个校验能直接体现你对业务的理解。盘点流程创建盘点单 → 填写实盘数量 → 系统计算盈亏差异 → 确认后调整库存 → 写流水。3. 后端SpringBoot核心功能实现3.1 项目初始化与关键配置创建一个SpringBoot项目引入核心依赖。这里我把pom.xml的关键部分贴出来dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.36/version /dependency /dependencies然后配置application.yml重点要处理几个容易出错的地方server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/wms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里有两个细节要特别注意map-underscore-to-camel-case: true是让数据库的create_time能自动映射到Java类的createTime很多人漏了这一行结果前端拿到的时间字段全是null排查半天才发现是这问题。log-impl: org.apache.ibatis.logging.stdout.StdOutImpl表示开发阶段把SQL打印到控制台这是定位问题最快的手段。等真正上线了再改成不打印。核心配置类里需要注册两个关键东西MyBatis-Plus分页插件和CORS跨域过滤器。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }3.2 库存变动与流水记录的落地实现库存变动是整个系统最核心的业务逻辑一定要用事务保证一致性。这里我给出入库的核心Service代码出库的逻辑对称但多了库存校验Service public class StockInServiceImpl implements StockInService { Autowired private StockInOrderMapper orderMapper; Autowired private StockInItemMapper itemMapper; Autowired private StockMapper stockMapper; Autowired private StockRecordMapper recordMapper; Override Transactional(rollbackFor Exception.class) public void auditStockIn(Long orderId) { // 1. 校验单据状态只有待审核状态才能审核 StockInOrder order orderMapper.selectById(orderId); if (order null || !待审核.equals(order.getStatus())) { throw new RuntimeException(单据不存在或状态已变更请刷新后重试); } // 2. 循环处理明细逐条增加库存 ListStockInItem items itemMapper.selectList( new LambdaQueryWrapperStockInItem() .eq(StockInItem::getOrderId, orderId) ); for (StockInItem item : items) { // 加锁查询当前库存 Stock stock stockMapper.selectForUpdate(item.getProductId()); int beforeQty stock null ? 0 : stock.getQuantity(); int afterQty beforeQty item.getQuantity(); if (stock null) { stock new Stock(); stock.setProductId(item.getProductId()); stock.setQuantity(item.getQuantity()); stockMapper.insert(stock); } else { stock.setQuantity(afterQty); stockMapper.updateById(stock); } // 写入流水 StockRecord record new StockRecord(); record.setProductId(item.getProductId()); record.setChangeType(入库); record.setChangeQty(item.getQuantity()); record.setBeforeQty(beforeQty); record.setAfterQty(afterQty); record.setOrderNo(order.getOrderNo()); record.setCreateTime(new Date()); recordMapper.insert(record); } // 3. 更新单据状态 order.setStatus(已审核); order.setAuditTime(new Date()); orderMapper.updateById(order); } }这段代码有几点值得细说selectForUpdate是MyBatis-Plus里通过悲观锁实现并发控制的关键手段。对应SQL是SELECT * FROM stock WHERE product_id ? FOR UPDATE。如果没有这一步当两个人同时给同一个商品入库时可能出现后写覆盖先写的问题最终库存对不上。这是库存系统最容易出的bug必须在设计阶段就防住。流水记录里的beforeQty和afterQty是用来追溯库存变化轨迹的核心字段。有了它们出了问题就能直接对照流水回放甚至可以在界面上做一个“库存变动轨迹”的查询功能答辩时非常加分。出库的逻辑和入库基本对称但增加了一个关键判断——扣减前先校验余额Transactional(rollbackFor Exception.class) public void auditStockOut(Long orderId) { StockOutOrder order orderMapper.selectById(orderId); if (order null || !待审核.equals(order.getStatus())) { throw new RuntimeException(单据状态异常); } ListStockOutItem items outItemMapper.selectList( new LambdaQueryWrapperStockOutItem() .eq(StockOutItem::getOrderId, orderId) ); for (StockOutItem item : items) { Stock stock stockMapper.selectForUpdate(item.getProductId()); if (stock null || stock.getQuantity() item.getQuantity()) { throw new RuntimeException( 商品ID [ item.getProductId() ] 库存不足当前可用: (stock null ? 0 : stock.getQuantity()) ); } int beforeQty stock.getQuantity(); int afterQty beforeQty - item.getQuantity(); stock.setQuantity(afterQty); stockMapper.updateById(stock); // 同样写流水 saveStockRecord(item.getProductId(), 出库, item.getQuantity(), beforeQty, afterQty, order.getOrderNo()); } order.setStatus(已审核); orderMapper.updateById(order); }3.3 权限控制JWT 拦截器 路由守卫权限设计我建议做成经典的三张表用户表、角色表、菜单表用户和角色关联角色和菜单关联。这个模型虽然老但它简单可靠而且完全符合仓库管理系统的实际场景——管理员有全部权限仓管员有出入库权限财务只有查看权限。后端用JWT做无状态认证。用户登录成功后生成一个包含用户ID和用户名的token返回给前端。前端每次请求把token放在Authorization请求头里后端通过拦截器校验Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new RuntimeException(未登录请先登录); } try { JWTVerifier verifier JWT.require(Algorithm.HMAC256(your-secret-key)).build(); DecodedJWT jwt verifier.verify(token); // 把用户信息放进请求上下文方便业务层获取当前操作人 request.setAttribute(userId, jwt.getClaim(userId).asLong()); request.setAttribute(username, jwt.getClaim(username).asString()); return true; } catch (Exception e) { throw new RuntimeException(登录已过期请重新登录); } } }拦截器要注册到WebMvcConfig里同时配置哪些路径放行、哪些路径需要校验。这里有个实践经验不要把拦截器配置复杂化系统的路径都是有规律的/auth/**放行其他一律校验菜单权限则由前端按钮级指令配合。密码存储一定要做加密处理。明文存储是答辩时最容易被老师当场抓住的问题一般用MD5加盐或者BCrypt。我建议用BCrypt因为它的哈希自带随机盐同样的密码每次加密结果都不一样安全性更高。4. 前端Vue页面与交互实战4.1 路由与动态菜单设计前端我用Vue 3 Vite Pinia Element Plus这套组合。Vite的启动速度比Webpack快一个量级Element Plus的表格、表单、弹窗组件非常适合后台管理系统基本就是为这种项目的UI需求量身定做的。路由分两块静态路由登录页、404页和动态路由根据用户权限从后端拉取的菜单。关键点是登录成功后要根据用户角色动态添加路由并且把菜单渲染成侧边栏。这段逻辑很容易做混乱我建议用一个统一的方法管理// router/index.js const staticRoutes [ { path: /login, component: () import(/views/login/index.vue) }, { path: /404, component: () import(/views/error/404.vue) } ] // 动态路由在登录后由后端返回菜单列表前端转换成router配置 function buildRoutes(menus) { const routes [] menus.forEach(menu { routes.push({ path: menu.path, name: menu.name, component: () import(/views/${menu.component}.vue), meta: { title: menu.title, icon: menu.icon } }) }) return routes }这里有一个坑必须提醒Vite中使用动态import的时候路径必须是静态分析的不支持全动态变量。也就是import(/views/${menu.component}.vue)这种写法在构建时可能报错。解决方案是维护一个组件的映射表把字符串映射到具体的import函数。4.2 核心页面拆解从登录到库存列表库存列表页是整个系统信息密度最大的页面我来说说怎么组织它。页面结构上顶部放搜索条件商品名称、分类、库位中间主体用表格展示库存数据每行显示商品编码、名称、分类、当前库存、安全库存、库位、最后更新时间。库存低于安全库存的行用醒目的颜色标出来这就是“智能预警”的直观体现。表格数据从后端分页获取前端封装请求// api/stock.js import request from /utils/request export function getStockList(params) { return request({ url: /stock/list, method: get, params }) }axios统一封装要处理几个核心逻辑请求拦截器里加token、响应拦截器里统一解包data、遇到401跳登录页、遇到业务错误弹提示。代码写好了后续所有接口都复用同一套逻辑能省大量时间// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router import { useUserStore } from /store/user const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization userStore.token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response?.status 401) { const userStore useUserStore() userStore.logout() router.push(/login) } ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )这里的baseURL用/api前缀开发环境下通过Vite的proxy代理解决跨域// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })生产环境部署时把Vue项目打包后的dist目录里的所有文件复制到SpringBoot的src/main/resources/static/目录下后端直接就托管前端资源了。这样整套系统只是一个jar包终答演示的时候在一台机器上就能跑起来非常省事。4.3 数据看板用ECharts让仓库“可视化”智能仓储管理平台的特点是“看得见”。我用ECharts做数据看板首页展示四个核心指标卡片商品总数、库存总量、今日入库量、今日出库量下方放两个图表一个是最近7天出入库趋势折线图一个是库存分类占比饼图。这个页面的实现难点不在ECharts本身而在于后端要提供聚合查询的数据。比如最近7天出入库趋势需要后端对stock_record表按天分组统计GetMapping(/trend) public Result trend() { // 最近7天 ListMapString, Object list recordMapper.selectTrend( DateUtils.addDays(new Date(), -7), new Date() ); return Result.success(list); }对应的SQL是在mapper里手写的select idselectTrend resultTypejava.util.Map SELECT DATE_FORMAT(create_time, %m-%d) as date, SUM(CASE WHEN change_type 入库 THEN change_qty ELSE 0 END) as inQty, SUM(CASE WHEN change_type 出库 THEN change_qty ELSE 0 END) as outQty FROM stock_record WHERE create_time gt; #{start} AND create_time lt; #{end} GROUP BY DATE_FORMAT(create_time, %m-%d) ORDER BY date /select有些时候MyBatis-Plus的BaseMapper不够用就必须上手写SQL。不要怕混用BaseMapper处理单表CRUD手写SQL处理聚合统计这才是正确的生产方式。5. 高频问题排查与优化实录5.1 前后端联调典型问题前后端分离项目调试阶段最常遇到的问题我按出现的频率排个序每一条都是我实际踩过的坑第一条跨域配置了还是不行。前端用axios请求后端接口报了CORS错误。查了半天发现是后端CORS配置和Spring Security的过滤器顺序冲突了。解决方案很简单在CORS配置类上加上Order(Ordered.HIGHEST_PRECEDENCE)让CORS过滤器在安全过滤器之前执行。第二条时间字段显示成一串数字。前端表格里日期列显示的是1662873600000这样的毫秒时间戳。原因是后端返回的Java Date被Jackson序列化成了时间戳格式。要么在application.yml配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8要么在实体类的日期字段上加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date createTime;我建议两个都加上注解的优先级更高某些特殊字段可以单独指定格式。第三条MyBatis-Plus分页不生效。前端传了pageNum和pageSize结果返回的是全部数据。这个原因很明确没有注册分页插件。上面已经提到了在配置类里加一个PaginationInnerInterceptor就行。这个问题在答辩前必须解决因为分页查询是所有管理系统列表页的标配功能。第四条token验证失败还是能访问接口。这大概率是因为拦截器没有正确注册或者排除路径写错了。排查思路在后端拦截器的preHandle里用System.out输出日志看是否进入校验逻辑。5.2 性能优化与细节打磨仓库管理系统虽然数据量不至于到海量但一些基本的优化意识必须体现在代码里这也是答辩时老师容易追问的点。数据库索引是第一个要考虑的。核心表的外键字段product_id、order_id和查询频繁的字段商品编码product_code、单据号order_no都要加索引ALTER TABLE stock_record ADD INDEX idx_product_id (product_id); ALTER TABLE stock_record ADD INDEX idx_create_time (create_time); ALTER TABLE product ADD INDEX idx_product_code (product_code); ALTER TABLE stock_in_order ADD INDEX idx_order_no (order_no);分页查询必须用MyBatis-Plus的分页插件不能手动写limit去拼。分页插件自动生成count查询处理起来更规范。状态字段建议用int类型比如1表示待审核、2表示已审核、3表示已作废然后在代码里定义一个常量类。不要直接用字符串“待审核”因为一旦有历史数据改起来就是灾难。金额字段要用BigDecimal不能用double。double的二进制浮点表示天生有精度损失0.1 0.2不等于0.3这在商品单价、库存金额计算中是不允许出现的。5.3 答辩前的功能检查清单最后我给做一个功能自检清单在交付前逐项确认检查项是否完成备注登录认证与退出是JWT过期处理要友好用户权限菜单动态生成是不同角色看到的菜单不同商品管理CRUD是支持分页条件查询入库单创建与审核是审核后库存增加并写流水出库单创建与审核是库存不足有提示库存盘点是盘盈盘亏能调整库存库存预警是低于安全库存红色高亮数据看板是出入库趋势库存分类占比操作日志是记录每个用户的关键操作这几个功能在验收时是硬指标缺一个被问到就会有点被动。尤其是库存流水和库存预警这是体现“智能仓储”定位的亮点功能一定要在界面和讲解中突出展示。我在实际做项目时的体会是这套系统完全不要拘泥于我写的代码模板关键要理解每条设计背后的理由为什么要用事务包裹出入库、为什么要记录库存流水、为什么要做库存预警阈值的可配置化。理解了这些问题哪怕老师临时换个场景问“如果有保质期管理怎么加”你也能顺着设计思路给出现成的答案。仓库管理系统是一个非常好的全栈练手项目做完它前后端联调的能力、数据库设计的能力、业务流程梳理的能力都会扎扎实实上一个台阶。
返回列表