ARTICLE DETAIL

资讯详情

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

仓库物资采购管理系统实战:基于Vue与Spring Boot的前后端分离开发

仓库物资采购管理系统实战:基于Vue与Spring Boot的前后端分离开发 写这个仓库物资采购管理系统之前我其实纠结了一阵子技术选型。这几年接手的项目里十有七八还停留在JSP Servlet JQuery 的阶段前端页面嵌着Java代码改个按钮样式都得小心翼翼生怕动了不该动的逻辑。如果继续用老一套这套系统做出来最多算“能用”根本谈不上好维护。但真要完全拥抱微服务、分布式那一套对这个场景来说又纯属过度设计——一个仓库物资采购管理核心是流程清晰、库存准确、操作顺手没必要上来就把架构堆成航母。所以最终定下的方案是后端用 JavaWeb 体系里的 Spring Boot 作为基础框架前端用 Vue 2 Element UI 搭界面前后端完全分离通过 RESTful API 通信。做完之后回过头看这个选择无论是从开发效率、代码可维护性还是从后续扩展空间来说都很合适。这篇文章我会把整个系统的设计思路、核心模块实现、数据库设计以及我在实操中踩过的坑都整理出来给正在做类似管理系统或者准备毕业设计的同学做个参考。1. 项目整体设计与技术选型思路1.1 为什么选 Vue JavaWeb 这套组合先说结论这套组合是“管理系统”类项目里性价比最高的搭配没有之一。JavaWeb 这边的 Spring Boot 解决的是后端业务的规范化和自动化问题。比如物资入库、采购审批、库存扣减这些操作本质上都是对数据库的增删改查但难点在于流程校验、状态流转、并发控制。Spring Boot 的 Starter 机制让整合 MyBatis、MySQL、Redis 变得非常省事而且它的依赖管理和自动配置能减少大量重复的 XML 配置。Vue 这边解决的是页面交互复杂度的问题。仓库物资管理系统的页面形态很固定无非是表格、表单、弹窗、树形菜单但业务操作路径多——采购单要从创建走到审批、入库、结算每一步都要展示不同的数据状态。用 Vue 的双向绑定和组件化机制这部分代码写起来很顺手数据变了页面自动更新不用像 JQuery 那样手动操作 DOM。我做了一个简单的对比能说明为什么不用其他方案方案优势劣势适用场景JSP Servlet JQuery上手门槛低前后端耦合、维护成本高小型页面快速出活Vue JavaWeb(Spring Boot)前后端分离、开发效率高、组件复用强需要同时掌握前端工程化和后端框架中后台管理系统Vue Spring Cloud 微服务扩展性强、服务治理完善架构复杂、部署成本高高并发、多团队协作的大型系统这个项目选择中间那档核心原因很简单仓库物资采购管理系统的业务规模决定它不需要微服务但它的功能复杂度又决定了不能再用 JSP 那种传统方式。1.2 系统功能拆解一个完整的物资采购闭环我把整个系统的功能拆成了五个核心模块物资管理、供应商管理、采购管理、入库管理、系统管理。五个模块之间不是孤立的而是构成一个完整的业务闭环先有物资档案再有供应商档案然后创建采购订单订单审批通过后执行入库操作入库同时更新库存数据。物资管理是基础数据层负责维护物资的基础信息包括物资编码、名称、规格型号、计量单位、分类、库存上下限等。供应商管理维护供应商档案和供货关系这里有个容易被忽略的点供应商应该和物资建立关联关系一个物资可以对应多个供应商这样才能在创建采购单时智能推荐合作过的供应商。采购管理是整个系统的核心覆盖从采购申请到采购订单、再到采购入库的全流程。我把它分成两步采购申请单需求提出和采购订单正式采购。采购申请单由仓库人员根据库存预警或实际需求发起经过部门主管审批后转成采购订单采购订单由采购员执行到货后生成入库单。入库管理是库存数据的来源物资到货后仓库人员核对采购订单号确认物资信息和数量无误后执行入库操作。这里要处理一个关键逻辑入库单必须关联采购单否则库存账目和采购账目就对不上。系统管理负责用户、角色、菜单权限的管理同时包含操作日志的记录。权限模型我用了最简单也最实用的 RBAC基于角色的访问控制思路但后面你会发现RBAC 在前端路由控制上的落地方式值得仔细思考。1.3 为什么前后端分离是这次改造的关键决策在真正动手写代码前我把数据交互方式定死为前后端完全分离即前端只通过 JSON 格式的接口和后端通信。这样做有个直接好处前端开发和后端开发可以并行推进我作为一个人开发时虽然体现不出并行优势但代码结构清晰很多。更重要的原因是前端分离之后我在维护这套系统时可以独立更新前端组件而不影响后端逻辑也可以对后端接口做单元测试而不需要打开页面。比如后来我给物资列表加了“批量导入”功能前端只需要新增一个组件、调用一个上传接口后端增加一个接收 Excel 文件的接口改起来非常干净。前后端分离的通信格式我统一约定为{ code: 200, message: 操作成功, data: {} }所有的接口返回都是这个结构前端封装 axios 拦截器统一处理。code 为 200 时正常返回数据为 401 时跳转登录页为 500 时弹出错误提示。这个约定在后续开发中帮了大忙因为采购审批、入库等很多操作的前端提示逻辑是一致的统一封装后不用每个页面都写一遍错误处理。2. 数据库设计表结构如何支撑业务流程2.1 核心表设计与字段规划数据库我用的是 MySQL 5.7存储引擎统一 InnoDB字符集 utf8mb4。设计表结构时我坚持三个原则一是每张业务表都加 id主键、create_time创建时间、update_time更新时间三个公共字段二是金额相关字段一律用 decimal绝不用 float 或 double不然算采购金额时会有精度问题三是逻辑删除用 deleted 字段标记不物理删除数据。整个系统我建了 12 张表核心的几张表设计如下物资信息表material包含 id、material_code物资编码、material_name物资名称、specification规格型号、unit计量单位、category_id分类ID、stock_quantity当前库存、min_stock库存下限、max_stock库存上限、status状态、deleted、create_time、update_time。供应商表supplier包含 id、supplier_code、supplier_name、contact_person联系人、contact_phone、address、status、deleted 等字段。采购订单表purchase_order包含 id、order_no订单编号、supplier_id供应商ID、order_status状态0待审批 1已审批 2已入库 3已取消、total_amount总金额、apply_remark申请备注、audit_remark审批备注、create_by申请人、audit_by审批人、create_time、update_time。采购订单明细表purchase_order_item包含 id、order_id关联订单主键、material_id物资ID、purchase_quantity采购数量、purchase_price采购单价、amount小计金额。入库单表stock_in包含 id、in_no入库单号、order_id关联采购订单ID、material_id、in_quantity入库数量、in_price入库单价、operator操作人、remark、create_time。字段设计上最难处理的是金额和数量同步的问题。采购订单的主表记录总金额明细表记录每一行的单价和数量当我修改明细时主表的总金额必须同步更新。这个逻辑如果在业务代码里散落处理很容易漏我最后是在 Service 层封装了一个专门的方法统一处理明细变更和主表金额更新。2.2 采购订单与入库单的关系甄别这是我踩坑最多的地方。一开始我把采购订单和入库单做成了简单的一对一关系一个采购订单入库一次性完成。但实际业务中经常出现分批到货的情况——采购了 500 个物料供应商先送了 300隔了两天再送 200。为了解决分批入库我在入库单表加了 order_id 关联采购订单同时增加了一个字段记录“当前订单已入库数量”。在 Service 层做入库操作时先查询该采购订单已入库的总数量加上本次入库数量后校验是否超过采购数量如果超过了就提示“入库数量超出采购数量”。每次入库完成后同步更新采购订单主表的已入库数量字段并且检查状态如果已入库数量等于采购总数量订单状态更新为“已入库”否则保持“已审批”状态允许后续继续入库。这个逻辑在系统里是一个完整的事务操作任何一个步骤失败都会回滚。2.3 库存扣减与数据一致性处理库存数据是整个系统最敏感的数据不能出现负数也不能出现超卖。在这里我做了两件事第一库存数量的更新必须在数据库层面做原子操作不能先查出来再在 Java 里减掉再写回去因为这样在并发场景下会丢失更新。正确的写法是直接用 SQL 更新语句UPDATE material SET stock_quantity stock_quantity - #{quantity} WHERE id #{materialId} AND stock_quantity #{quantity}这条 SQL 能保证两个效果一是扣减是原子性的二是在库存不足时影响行数为 0业务层通过判断影响行数来决定是否回滚避免了脏读和超卖。入库时则是增加库存相对简单但也用同样的原子更新方式。另外我在物资表设计了库存上下限字段每次入库或出库完成后业务层检查当前库存是否低于 min_stock如果低于则自动生成一条采购建议记录提醒仓库人员补货。这个功能看似简单但在实际使用中非常受欢迎因为它能主动发现问题而不是等人去查报表。3. 后端核心业务实现3.1 登录认证与权限控制的实现登录认证我用的 JWTJSON Web Token方案。用户登录成功后后端签发一个 token 返回给前端前端保存在 localStorage 里每次请求时在请求头中携带 Authorization: Bearer token。后端通过拦截器验证 token 的合法性并且从 token 中解析出用户ID和角色信息。权限控制是系统里一个重要模块我采用的是 RBAC 模型用户表、角色表、菜单表用户和角色多对多角色和菜单多对多。菜单表里每个菜单记录对应前端路由的路径和按钮的操作标识比如物资管理下的“新增”按钮对应一个权限标识 material:add。前端拿到用户信息后会连同角色拥有的权限标识列表一并返回。前端路由守卫中判断用户是否拥有访问该菜单的权限没有权限就跳转到 403 页面。按钮级别的权限则通过 Vue 自定义指令实现比如我们封装了一个 v-permission 指令传入权限标识如果用户没有该权限自动移除对应的 DOM 元素。// 前端自定义指令示例 Vue.directive(permission, { inserted(el, binding) { const permissionList store.getters.permissions const required binding.value if (!permissionList.includes(required)) { el.parentNode.removeChild(el) } } })页面上使用时就很简单了el-button v-permissionmaterial:add新增物资/el-button3.2 采购审批流程的状态机设计采购流程是整个系统最复杂的业务逻辑从采购申请到审批、再到订单生成和入库涉及多个状态的变化。我用一个状态机来管理避免状态跳转混乱。采购订单状态0待审批新建或修改后提交1已审批审批通过等待入库2已入库所有物料全部入库完成3已取消审批不通过或手动取消状态流转规则在 Service 层统一校验。比如只有状态为“待审批”的订单才允许审批操作只有“已审批”的订单才允许入库操作状态不符合的直接抛异常。这个状态机虽然简单但保证了业务操作的合法性防止了前端绕过页面直接调接口造成的数据错乱。审批操作还有一个细节审批人字段记录审批人ID审批时间单独记录。采购订单创建人和审批人不能是同一个人我在代码里做了校验防止自己审自己的情况出现。采购审批通过后系统自动生成一份采购订单编号编号规则采用“PO 年月日 四位序号”例如 PO202506060001。这个编号在订单生成时通过 Redis 自增序号生成保证并发场景下不会重复。3.3 物资入库与库存同步的原子性操作入库操作是典型的“多步操作但必须全部成功或全部失败”的场景我用 Spring 的事务注解管理保证原子性。入库流程是这样的校验采购订单状态是否为“已审批”校验入库数量不超过未入库数量生成入库单记录更新采购订单的已入库数量和订单状态原子更新物资表的库存数量记录操作日志这六步操作任何一个抛异常整个事务回滚库存不会出现多入或者少入的情况。这里我特别强调的是第 5 步的原子更新也就是前面说的那条 update 语句这是并发安全的关键保证。还有一个实际使用中发现的细节入库单价记录的是采购单价而不是手工填写的单价。这样做的目的是保持采购成本和入库成本一致后续做财务核算时不会出现两套价格数据。4. 前端 Vue 项目开发实录4.1 前端项目结构与路由设计前端项目我用 Vue CLI 4 初始化目录结构按照业务模块划分而不是按照文件类型划分这样维护时查找代码非常快。项目的目录结构大致如下src/ ├── api/ # 接口请求定义 │ ├── material.js │ ├── purchase.js │ └── supplier.js ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── Pagination.vue # 分页组件 │ └── ImageUpload.vue # 图片上传组件 ├── router/ # 路由配置 ├── store/ # Vuex 状态管理 ├── views/ # 页面视图 │ ├── material/ # 物资管理页面 │ ├── purchase/ # 采购管理页面 │ ├── supplier/ # 供应商管理页面 │ └── stock/ # 入库管理页面 ├── utils/ # 工具函数 │ ├── request.js # axios 封装 │ └── auth.js # token 存取 ├── App.vue └── main.js路由设计上我采用了动态路由的方式。用户在登录成功后后端返回该用户可访问的菜单列表前端根据菜单列表动态注册路由。这样的好处是用户登录后只能看到自己有权限访问的页面未授权页面即使手动输入 URL 也无法进入路由守卫会拦截。4.2 基于 axios 的请求封装与拦截器配置axios 封装是 VUE 项目里最能提升开发效率的部分之一。所有页面都不直接调用 axios而是调用 api 目录下定义好的方法方法内部统一走 request.js 的封装。// utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器自动带上 token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) }) // 响应拦截器统一处理返回码 service.interceptors.response.use(response { const res response.data if (res.code 200) { return res } else if (res.code 401) { localStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) } else { Message.error(res.message || 系统错误) return Promise.reject(new Error(res.message)) } }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) }) export default service请求封装好在哪第一不用每个页面都写 token 获取逻辑拦截器统一处理第二接口报错时整个系统有统一的口径不会出现一个页面弹红框、一个页面弹黑框的问题第三登录过期自动跳转这个在后来的实际使用中避免了好几次“用户明明登录过却被卡在页面里”的尴尬情况。4.3 采购流程页面中的联动与状态渲染前端实现采购订单页面时踩了一个不少 Vue 新手容易遇到的坑级联选择器和表格数据的联动。创建采购单的页面结构是先选择供应商然后动态添加采购明细行每次选择物资后自动带出该物资的规格型号和计量单位填入采购数量后系统自动计算金额。这个效果的核心是监听当前行的物资选中事件然后去物资列表中匹配对应信息。el-table :dataorderItemList el-table-column label物资名称 template slot-scopescope el-select v-modelscope.row.materialId filterable changehandleMaterialChange(scope.$index, $event) el-option v-foritem in materialList :keyitem.id :labelitem.materialName :valueitem.id /el-option /el-select /template /el-table-column el-table-column label规格型号 template slot-scopescope{{ scope.row.specification }}/template /el-table-column el-table-column label采购数量 template slot-scopescope el-input-number v-modelscope.row.quantity :min1 changehandleQuantityChange(scope.$index)/el-input-number /template /el-table-column el-table-column label金额 template slot-scopescope{{ scope.row.amount }}/template /el-table-column /el-table这里最关键的是不能用 ES6 的 forEach 去修改 scope.row 的属性。一开始我直接在handleMaterialChange里这样写this.orderItemList[this.orderItemList.findIndex(item item.id index)] newData结果发现页面不更新。原因是下标变化发生在数组遍历内部Vue 能监听到数组元素替换但监听不到对象拦截的某些属性新增。后来我改用Vue.set或者直接修改scope.row的属性赋值页面响应就正常了。这个经验提醒我在 Vue 2 里操作对象的属性时新增属性一定要用this.$set否则响应式系统跟踪不到。4.4 组件复用库存预警与物资选择的公共封装实际开发中我发现物资选择的场景特别多创建采购单要选物资制作入库单要选物资物资库存查询也要选物资条件审批详情页要展示物资清单。所以我将物资选择封装成了公共组件 MaterialSelect通过 props 接收当前已选中的物资ID通过 emit 向父组件传递选中结果。此外库存预警也是多个页面共用的功能。仓库首页要用物资列表要显示采购建议生成后也要展示。我封装了一个 StockWarning 组件接收物资列表数据自动筛选出库存低于下限的物资并展示建议补货数量补货量 上限 - 当前库存。这个组件在物资管理列表页和仓库仪表盘页面都被复用代码量省了不少。5. 项目中的典型问题与排查过程5.1 前后端联调时出现的跨域问题项目开发中前后端联调时碰到最常见的坑就是跨域。前端跑在 localhost:8080后端跑在 localhost:8081前端请求后端接口时浏览器直接报错Access to XMLHttpRequest has been blocked by CORS policy。解决跨域有两种常用方式我后端用的是 Spring Boot 的 CORS 配置。写一个 WebMvcConfigurer 配置类统一处理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:*, http://127.0.0.1:*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有个细节allowCredentials(true) 时allowedOrigin 不能写成 *必须明确指定来源否则浏览器会拒绝这个响应。最初没注意到这个排查了好久等短时间接入了 Vue 后这个坑尤其明显。5.2 重复提交与接口幂等性的处理在做新增提交时用户连续点击两次“提交”按钮采购单被创建了两条这个问题是真实出现过的被仓库管理员当场吐槽“系统有 bug”。排查下来不是后端逻辑问题而是前端没有做防重复提交。解决思路分两层前端在提交按钮点击后立即禁用按钮等请求返回后再恢复后端接口做幂等校验比如用数据库唯一索引约束。实际我采用的是前端禁用按钮 后端订单号唯一索引的组合方式。前端禁用很简单给提交按钮加一个 loading 状态el-button typeprimary :loadingsubmitLoading clicksubmitForm 提交审批 /el-button提交时this.submitLoading true请求完成后this.submitLoading false这样用户就不会重复点击了。后端的唯一索引作为兜底万一前端被绕过数据库层面也能挡住重复订单。5.3 导出 Excel 时文件名乱码的坑系统里采购订单列表要支持导出 Excel 功能我用的是 Apache POI 生成 xlsx 文件。本地测试一切正常部署到服务器后用着用着发现问题通过 Chromium 内核的浏览器下载时文件名出现乱码。排查后确认是响应头中的 HTTP header 不支持中文文件名需要做编码转换。正确写法是String fileName URLEncoder.encode(采购订单明细.xlsx, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment; filename*UTF-8 fileName);第一版我用了无编码的写法文件名直接塞中文导致在部分浏览器上解析失败。这个问题也提醒我凡是涉及文件下载响应头里的文件名都要做 URL 编码处理并且要兼容 RFC 5987 规范。5.4 前端打包部署后的接口地址问题开发环境下前端 api 的 baseURL 是/dev-api通过 vue.config.js 里的 devServer proxy 代理到后端的 8081 端口。但打包部署时静态页面放在 Nginx 下接口地址如果还写成相对路径/dev-api就会 404。最终的方案是用环境变量区分。项目里放两个环境文件.env.development和.env.production分别配置不同的接口前缀。生产环境的 Nginx 配置中把所有/prod-api前缀的请求反向代理到后端服务location /prod-api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这样前后端分离部署时前端只管静态文件和页面路由后端只管业务接口两边职责很清楚。第一次部署时我把这步想简单了直接改成了完整的后端地址来发布上线后跨域问题又冒出来了改成同域名反代之后才彻底解决。6. 项目运行与部署注意事项6.1 本地开发环境搭建如果你想把这套系统跑起来本地的环境要求如下依赖项版本要求说明JDK1.8JavaWeb 体系兼容性最好的版本MySQL5.7支持 utf8mb4 和事务Maven3.6后端依赖管理Node.js14前端构建环境npm/yarn任意前端依赖安装后端启动步骤创建数据库并导入 SQL 脚本修改 application.yml 中的数据库账号密码运行 Spring Boot 启动类。前端启动步骤npm install 安装依赖npm run dev 启动开发服务器。这里要提醒一句npm install 的时候别直接用 root 用户跑会有权限和依赖兼容性问题。我自己在 Mac 上遇到过一次 node-sass 编译卡死的情况后来统一换成了 dart-sass 才顺利安装。国内网络环境下建议配置 npm 镜像源否则有些包下载速度会让人崩溃。6.2 部署到服务器时的关键配置生产环境部署我采用的方案是前端构建后扔给 Nginx 托管静态文件后端打成 jar 包通过 systemd 守护进程运行。Redis 主要用于 JWT 黑名单和订单编号自增当然这里为简化也可以直接只用数据库的自增看实际需求。部署时还有三个容易遗漏的点。第一是 MySQL 连接串要加上时区配置serverTimezoneAsia/Shanghai否则时间字段会差 8 个小时。第二是 JWT 的密钥和过期时间要写在配置文件中别硬编码在代码里方便后续修改。第三是文件上传大小限制采购附件动辄几 MB 甚至几十 MBSpring Boot 默认只有 1MB必须在配置文件中调大spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB6.3 初期上线后需要关注的运维指标系统上线后除了正常使用功能还有一个容易被忽略的事情数据库慢查询日志。因为采购订单明细表、物资表的数据会随着使用时间增长如果没有索引查询会越来越慢。我在上线一周后习惯性查了下慢查询日志发现物资列表页的一个模糊查询走了全表扫描加了个组合索引之后立即恢复正常。索引设计上建议给这几张表的关联字段都加上索引采购订单明细表的 order_id 和 material_id入库单表的 order_id以及物资表的 material_code。这些字段是高频查询的过滤条件缺索引会直接影响页面响应速度。另外定时备份数据库是必须的。我写了一个简单的 shell 脚本每天凌晨用 mysqldump 全量备份数据库文件保留最近 7 天的备份然后通过 cron 定时执行。做系统就是这样平时看不出区别一旦出现问题备份就是救命稻草。7. 这个项目的经验复盘与可扩展方向这个项目从需求梳理到最终部署上线前后大概花了两周半的时间。如果只总结一句最深的体会我想说管理系统的核心从来不是技术栈有多新而是业务流程是否清晰、数据流转是否正确、状态控制是否严密。Vue 和 JavaWeb 都只是工具真正需要下功夫的是把采购、入库、库存这些环节的边界定义清楚。从代码量来看后端大约 3000 行 Java前端大约 2000 行 Vue不算多但整个系统的设计上是完整的。这个项目做完后我个人的习惯是每个模块写完代码后立刻补上对应的接口测试用例尤其是采购审批和入库这两个核心模块的接口后续每次改动代码时都能快速回归验证。后续扩展上如果业务量增长明显可以考虑引入消息队列比如 RabbitMQ做采购通知的异步推送或者引入 Redis 做库存热点数据的缓存。但如果只是中小型仓库场景保持当前架构就完全够用没必要为了炫技引入额外复杂度。最后再分享一个小技巧开发这种管理系统时尽量让每个表单页面的字段顺序与数据库表字段顺序保持一致这样接口联调时双方口头沟通效率会高很多。我在这个项目里吃过一次亏前端字段顺序和后端不一致两边对着表格调了半天后来统一了字段顺序并让前端直接使用后端返回的字段名做 v-model 绑定后续再没有出过类似的低级问题。如果你正准备做一个类似的仓库管理系统或者正在为毕业设计选题纠结完全可以参考这个项目的思路先梳理清业务流程再确定技术选型然后分模块开发每一步都做扎实。选择 Vue JavaWeb 这套技术栈既能学到前后端分离的开发模式又不会陷入微服务那种过于复杂的架构中是很稳妥的路线。等到项目跑起来、机器上线、采购单在系统里正常流转的那一刻你会觉得之前所有加班调试都是值得的。
返回列表