SpringBoot+Vue+MyBatis+MySQL物资管理系统设计部署实战

发布时间:2026/10/5 7:30:10
SpringBoot+Vue+MyBatis+MySQL物资管理系统设计部署实战 做这个物资综合管理系统起因很俗就是仓库的Excel台账终于撑不住了。当时手头正好要重做一套内部的出入库管理我直接选了SpringBootVueMyBatisMySQL这套前后端分离组合前后端独立开发后端出接口前端管页面最后交付的时候才发现这套方案不仅开发效率高后续接报表、上权限、加移动端都特别顺。这篇就把整个项目的设计思路、核心实现、部署流程和踩坑记录完整拆一遍源码是能直接跑起来的按着步骤来即可。项目适合正在做前后端分离实战练习的同学也适合需要快速落地一套中小型物资管理系统的团队参考。1. 项目整体设计与技术选型拆解1.1 需求梳理这套系统到底管什么很多物资管理系统在学校课设里是标配但真正到现场用起来痛点完全不一样。我一开始就列了三个核心需求谁在什么角色下能看到哪些功能物资从入库、出库到调拨的整个流程要有单据可查库存数量要实时准确月底盘点对得上账。基于这三个需求功能模块拆成认证权限、基础信息、业务单据、库存管理、报表统计五块。权限模型用的是RBAC用户绑定角色角色绑定菜单和按钮权限。前端根据登录用户返回的菜单树动态生成左侧导航后端接口再用拦截器校验角色权限两层控制避免有人绕过前端直接调接口。这个设计在中小型系统中够用也比在代码里写死if判断要灵活得多。业务上最敏感的是出库时库存校验库存不够绝对不能允许出库涉及金额的计算全部用Decimal避免浮点误差。系统在出库单提交时开启事务库存扣减和流水写入同一事务只要一步失败就整体回滚。1.2 技术选型为什么是SpringBootVueMyBatisMySQL后端用SpringBoot 2.7.x没有直接用3.x。原因很实在很多内置依赖在3.x里换了坐标比如javax改成jakarta一些老项目迁移起来容易踩坑。我们这套系统不是从零研究新特性而是求稳2.7.x足够用了而且相关排错资料多遇到问题好查。MyBatis没有选择MyBatis-Plus不是因为Plus不好而是原生MyBatis在复杂多表查询时更可控。物资管理系统里典型场景是“按分类、按时间段、按状态组合查询”动态SQL在XML里写一眼能看明白比代码里拼接字符串或者用Lambda表达式绕来绕去更直观。再加上分页直接用PageHelper插件开发成本也不高。前端为什么选Vue2而不是Vue3因为很多部署环境还停留在Node 14或16Vue2整个生态更成熟Element UI组件齐全网上现成方案多。当然如果你把源码里的依赖升到Vue3和Element Plus改动量也不大但没必要为了新版而新版。MySQL是这套组合里最没有争议的一环。5.7和8.0在语法上基本兼容但8.0的pom依赖驱动名变成了com.mysql.cj.jdbc.Driver时间类型要求更严格。我源码默认兼容MySQL 5.7生产环境也测过8.0只要连接串加上serverTimezone和useSSL两个参数基本没问题。1.3 系统模块与数据库设计思路数据库表设计遵循一个原则流水表只记流水库存表只存快照。比如入库单主表biz_inbound记录单号、操作人、入库时间明细表单独放这样一张单可以录入多种物资库存表不直接存“当前剩余数量”而是每天凌晨跑批生成stock_snapshot快照。日常查询流水时动态汇总报表查询直接读快照两者互不干扰。核心表包括sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu、base_category、base_material、biz_inbound、biz_outbound、biz_transfer、stock_flow_record、stock_snapshot。字段设计上特别注意几点数量字段统一decimal(18,2)金额字段统一decimal(18,2)不要用double所有状态字段用tinyint加注释1有效0无效每张表都带create_time和update_time方便排查数据问题。以物资表base_material为例字段有material_code编码、material_name名称、category_id分类、specification规格、unit单位、lower_limit库存下限、upper_limit库存上限、status状态。编码字段设了唯一索引前端输入时后端要校验重复否则会造成同一物资录入两条记录库存统计直接翻车。2. 后端SpringBootMyBatis核心实现要点2.1 工程结构和Maven依赖搭建工程结构按标准分层来controller接收参数service处理业务mapper定义数据库操作entity对应表结构config放配置类common放统一结果封装和异常处理。启动类上加了SpringBootApplication和MapperScan(com.example.material.mapper)这样不用每个Mapper上写Mapper注解省事很多。pom.xml里核心依赖要卡准版本dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency这里提醒一点mysql-connector-java在SpringBoot 2.x里不用写版本号由父依赖管理但如果MySQL驱动报错可以手动指定5.1.49或8.0.33。Druid连接池的监控页面在开发时很方便但生产环境一定要关闭或加访问认证不然等于把数据库状态暴露出去。2.2 MyBatis配置与XML映射文件设计application.yml里的MyBatis相关配置是这样mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.material.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个属性必须要开否则数据库里material_code查出来不会自动映射到实体的materialCode属性前台显示全是null。log-impl在开发时打印SQL很方便生产环境建议改成slf4j不然日志量太大。XML映射文件里最常用的三个动态SQL场景多条件查询用where加if批量插入用foreach关联查询用resultMap。以物资分页查询为例select idselectMaterialPage resultMapmaterialResultMap select m.*, c.category_name from base_material m left join base_category c on m.category_id c.category_id where if testmaterialName ! null and materialName ! and m.material_name like concat(%, #{materialName}, %) /if if testcategoryId ! null and m.category_id #{categoryId} /if if teststatus ! null and m.status #{status} /if /where order by m.create_time desc /select注意like语句要用concat拼接不要用%${materialName}%后者存在SQL注入风险。业务单的查询会关联用户表查操作人姓名关联分类表查分类名称都用resultMap做映射不用写一堆VO类。2.3 核心接口与业务逻辑含分页、条件查询等后端接口统一返回Result对象结构很简单code、msg、data。分页数据再包一层PageResult包含list、total、pageNum、pageSize。Controller层不写业务逻辑只接收参数、调Service、返回结果。比如入库单提交接口大致是PostMapping(/inbound) public ResultString createInbound(RequestBody Valid InboundDTO dto) { inboundService.createInbound(dto); return Result.success(入库成功); }Service层处理事务入库单新增方法上加了Transactional(rollbackFor Exception.class)。为什么强调rollbackFor因为Spring默认只回滚RuntimeException如果业务代码里抛的是IOException这类受检异常不加rollbackFor的话事务不会回滚库存流水就会出现半截数据。入库单的业务步骤根据计划编号去重校验保存主表拿到单号循环明细表批量插入调用stockService刷新库存流水如果当天已经生成了快照还要重新汇总当前库存。出库单多一步查询现有库存如果小于出库数量直接抛业务异常提示“物资XX库存不足”。分页用的是PageHelper用法有讲究调用PageHelper.startPage(pageNum, pageSize)之后必须紧跟一条Mapper查询中间不能插入其他查询或逻辑否则分页会作用到错误SQL上。查询结束后用PageInfo包装能直接拿到total、pages等分页信息。3. 前端Vue项目搭建与前后端联调3.1 Vue环境配置与项目初始化本地开发最好统一Node版本推荐16.x因为后面要装一些依赖版本太高或太低都容易出现node-sass编译报错。装上Node后全局安装vue/cli然后执行vue create material-ui选择Vue2配置。如果嫌从头建工程麻烦也可以直接拿现成源码在根目录执行npm install就能装完依赖。依赖除了vue、vue-router、axios之外UI框架我用的Element UI。这里有个小坑Element UI的babel插件按需引入需要在babel.config.js里加presets: [vue/cli-plugin-babel/preset]和plugins: [element]不然组件样式会乱。全量引入也行项目不大时不用纠结直接import ElementUI from element-ui。3.2 路由、Axios封装和状态管理路由这块最有价值的是动态路由。登录接口返回当前用户的菜单权限列表前端拿到后递归生成路由表再用router.addRoutes动态挂载。菜单结构一般是树形前端组件映射关系通过meta里面的componentName来对应比如“/material/list”对应MaterialList组件。Axios封装我做了三层创建axios实例设置baseURL为/api请求拦截器从localStorage取token加到请求头响应拦截器判断返回的code401时清空token并跳转登录页非200时直接提示错误信息。这样每个请求不用重复处理错误。这里要说明一点开发环境下baseURL配成/api是因为Vue的devServer里配置了proxy转发到后端8080端口。生产环境下nginx也做了同样的反向代理。这种做法的好处是前后端同源不需要额外处理跨域Cookie也能正常携带。3.3 页面组件实现与联调细节物资列表页是典型的“搜索表单表格分页弹窗”结构。搜索表单绑定一个searchForm对象点击查询时把当前页重置为1然后调用列表接口。表格列里状态列用el-tag显示颜色库存低于下限时显示“库存预警”标签这个逻辑在前端简单判断即可。入库单页面比普通页面复杂因为要做明细行编辑。我用了el-table里的动态行每行有物资选择器、数量输入框、单价输入框自动计算当前行金额并汇总到底部。提交时把明细数组和主表单数据一起发给后端。这个页面主要难点在数据联动切换物资后自动带出分类、单位、默认单价数量输入后校验不能为0和负数。前后端联调时最容易出问题的就是字段命名。后端实体用了驼峰前端传参也用驼峰但数据库字段是下划线MyBatis开了map-underscore-to-camel-case后会自动映射。但如果前端有字段叫materialName后端DTO里也必须有materialName两边对不上就会接收不到值。我习惯把DTO和前端表单字段保持一致减少联调沟通成本。4. 从本地到服务器完整部署教程4.1 环境准备JDK、MySQL、Node等部署前先把环境准备好。JDK必须8以上我推荐OpenJDK 8或11。MySQL建议5.7或8.0安装后统一字符集utf8mb4不然中文会乱码。前端构建需要Node环境如果服务器资源紧张可以在本地构建好dist目录再上传服务器上不装Node也行。创建数据库并导入SQL文件mysql -uroot -p create database material default character set utf8mb4 collate utf8mb4_general_ci; use material; source /data/sql/material.sql;导入时注意SQL文件里的日期格式和分隔符如果报错检查文件是否包含了多余的空格或注释。生产环境最后建议创建一个专用账号只授权material库不要直接用root。4.2 后端打包与启动后端打包前先确认application-prod.yml里的数据库地址、账号密码已经改成生产环境的值。执行mvn clean package -DskipTests构建成功后target目录下会生成material-0.0.1-SNAPSHOT.jar。启动命令用nohupnohup java -jar material-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod /data/logs/material.log 21 一定要指定--spring.profiles.activeprod否则会加载默认的application.yml连到开发库就麻烦了。日志文件单独放排查问题直接用tail -f看。后端启动后先验证接口通不通比如请求登录接口返回正常JSON再部署前端。不要一上来就配nginx容易把前后端问题混在一起。4.3 前端构建与Nginx部署前端构建前检查publicPathvue.config.js里设置publicPath: ./, outputDir: dist, assetsDir: static,如果不设history模式通常还需要history: createWebHistory(process.env.BASE_URL)构建npm run build生成dist目录后把里面的文件上传到服务器比如/data/www/material-ui。Nginx配置如下server { listen 80; server_name your-domain.com; root /data/www/material-ui; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files这一行在history模式下必须要有否则在页面内刷新就会404这是新手最容易踩的问题。location /api/把前端请求转发到后端8080端口后端接口路径不需要改因为Nginx把/api前缀去掉的逻辑取决于proxy_pass末尾是否带路径我用的是不带路径的写法所以后端接口还是/api/login这种不会重复。4.4 数据库初始化与生产环境注意事项SQL文件里包含了建表和初始化数据比如默认管理员账号、基础分类、字典数据。导入后第一件事就是改管理员密码用MD5或BCrypt加密后的字符串替换不要在数据库里存明文密码。生产环境几个必须做的点Druid监控页面关闭或加密码MySQL开启binlog并定期备份后端日志按天切割避免单文件过大服务器防火墙只开放80和22端口后端8080不能对外开放只允许本机nginx访问。这样即便有人扫到8080端口也无法直接调接口。如果同服务器部署多个项目需要修改Nginx的server_name区分后端端口也要相应调整。配置外置这一项我特别推荐把application-prod.yml放在jar包同级目录启动时用--spring.config.additional-location指定这样换环境不用重新打包。5. 部署与使用中踩过的坑常见问题速查5.1 MySQL连接和版本相关部署阶段最常见的就是JDBC连接报错。尤其MySQL 8.0报“SSL connection error”或者“Public Key Retrieval is not allowed”。解决办法是JDBC URL加参数jdbc:mysql://localhost:3306/material?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8mb4如果使用MySQL 5.7驱动连8.0数据库也会报错最好统一用8.0版本的驱动。Maven依赖里mysql-connector-java版本可以直接用8.0.33向下兼容5.7。5.2 跨域和端口问题开发时如果没走proxy直接请求后端会报跨域。前端网络里看不到响应控制台提示CORS。我的习惯是开发时用devServer代理生产时用nginx后端不管跨域逻辑更干净。如果你非要后端处理可以写一个CorsConfig配置类但生产环境nginx代理已经是首选的同源方案。端口冲突也是老问题启动jar报“Port 8080 was already in use”用netstat -tlnp | grep 8080查占用或者直接改server.port。改了后端端口nginx的proxy_pass也要同步改前后端两个配置别对不上。5.3 MyBatis映射和缓存问题查出来字段全为null是MyBatis新手最常见的问题检查两步第一步确认map-underscore-to-camel-case是否设置为true第二步看实体类属性名是否和数据库字段对应。如果用了resultMap就检查resultMap里的column和property漏一个字段就是null。MyBatis缓存这个点也顺便说一下。一级缓存是SqlSession级别同一个Session内重复查询不会重新连接数据库二级缓存是Mapper级别默认关闭如果要开启在XML里加 。但多表JOIN查询时二级缓存很容易出脏数据因为关联表的变更不会自动清空缓存。我在这套系统里的原则是业务流水表不开二级缓存只有base_category这种几乎不变的基础表才开避免为了省一点查询时间带出错误数据。5.4 前端打包、路由刷新和权限失效问题前端部署后还有三个高频问题。第一dist上传后打开空白页面多半是静态资源路径问题检查publicPath是否配置为./。第二页面刷新后404检查Nginx的try_files是否配置。第三登录后菜单不显示用浏览器开发者工具看登录接口返回的menuTree是否为空再检查动态路由的componentName是否和前端组件的路由映射一致。权限这块我遇到过问题是因为后端返回的菜单code有空格前端比较时没trim排查了很久才发现从此养成了接口数据先trim的习惯。还有一个我自己的土办法部署完用curl直接验证后端接口curl -X POST http://127.0.0.1:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:yourpassword}如果返回正常的JSON后端没问题如果403或404再回头查Nginx和防火墙。这个步骤能快速定位问题在前端还是后端省掉了前后端扯皮的功夫。最后分享一个我自己用着很舒服的扩展方向这套物资系统的报表模块目前只做了简单的库存汇总和出入库流水导出后续可以接一个定时任务把每日库存快照推到另一个分析库里做趋势图也可以把动态路由改成菜单管理界面让运营人员自己去配导航。我实际维护下来的体会是前后端分离最大的红利不是开发时的并行而是上线后可以单独改前端页面、单独重启后端服务互不影响。部署这套系统时把这些细节留好后续每次变更都会轻松很多。