今年整理项目源码时翻出这套早期做的企业级物资管理系统,基于 SpringBoot + Vue + MyBatis + MySQL 的经典前后端分离架构,当时用于企业防疫物资的入库、领用、库存预警和台账统计。虽然现在这套系统被我改成了通用版物资管理,但核心代码结构没变,拿来当 SpringBoot 全栈练手项目或者直接改造复用都合适。
这篇文章不打算只贴代码结构和功能列表,我把自己当时从设计表结构、开发后端接口、写前端页面到部署上线的整个过程,包括踩过的坑、优化过的细节,全都梳理一遍。如果你是刚学完 SpringBoot 和 Vue 想找个完整项目练手,或者公司需要一套物资管理系统但预算有限想自己折腾,这篇值得认真看完。
1. 这套源码解决的真实痛点与技术选型逻辑
1.1 为什么市面上模板代码很多,真正能落地的不多
很多人拿到一个所谓“完整管理系统源码”,打开一看是 MyEclipse 时代的 SSH 项目,或者前端还是 JSP,想改点东西都费劲。还有一种是代码结构完全混乱,Controller 里写 SQL,业务逻辑全部堆在页面里,根本没法维护。
这套系统的原始需求很明确:企业要管理口罩、消毒液、防护服这类物资的库存,需要记录每一次入库和领用记录,实时掌握当前库存数量,并能在库存低于阈值时自动预警。看起来简单,但真做起来有不少细节——物资种类多、批次不一、领用频繁,还要按部门统计,数据要有追溯性。
我当时的判断是:这类企业内部管理系统,业务逻辑复杂但技术难度不算高,关键是要结构清晰、易于二次开发。用 SpringBoot 负责后端接口,Vue 负责前端页面,MyBatis 负责数据持久层,MySQL 存储数据,这是最稳妥也最容易被团队接手的搭配。
1.2 SpringBoot+Vue+MyBatis+MySQL 组合的底层合理性
先聊后端。SpringBoot 简化了 Spring 的配置流程,内嵌 Tomcat,打包成 jar 就能直接跑,这对中小企业的部署环境特别友好——不需要单独配置外部容器,服务器上装个 JDK 就完事。MyBatis 相比 JPA/Hibernate 在这类场景的优势是 SQL 完全可控,物资管理里经常涉及多表关联查询、按时间范围聚合统计、动态条件筛选,这些用 MyBatis 的 XML 映射文件写起来非常顺手,排查问题时也能直接拿出一条 SQL 来分析。
比如统计某个月各部门领用数量,大概是这样的查询逻辑:
SELECT department_name, material_name, SUM(quantity) AS total_quantity FROM stock_record WHERE record_type = 'OUT' AND create_time BETWEEN #{startTime} AND #{endTime} GROUP BY department_name, material_name ORDER BY total_quantity DESC这种带条件的动态统计,在 MyBatis 里用<where>和<if>标签可以灵活拼装,还能用 include 抽取重复的 SQL 片段。
前端选 Vue 是因为它组件化开发非常适合管理系统这类页面。像物资台账、出入库表单、库存看板,每个功能模块封装成独立组件,互不干扰。配合 Element UI 组件库,表格、弹窗、表单校验这些后台管理的常见交互都能快速搞定,不用从零写 CSS 和交互逻辑。如果你用的 Vue 3,新版本还支持 Composition API,代码复用性和 TypeScript 支持都有明显提升,但这套老系统用的是 Vue 2 + Element UI,主要考虑是当时生态最成熟。
这套技术栈的另一个优势是招聘市场上会的人多,接手维护成本低,对企业来说这意味着后续修改功能不需要依赖源码作者。
2. 从业务梳理到数据库设计:物资台账与出入库流转
2.1 核心业务域拆解
开发前第一件事不是建工程写代码,而是把业务捋清楚。我把物资管理系统拆成了四个核心域:
- 库存域:物资信息、当前库存、库存预警阈值。
- 流转域:入库单、领用单、调拨单,每次流转都会留下一条台账记录。
- 组织域:部门、用户、角色,不同角色权限不同。
- 统计域:按时间、部门、物资种类多维度的数据汇总。
这四个域对应到数据库里,就是一组相互关联的表。其中最关键的设计决策是:库存表只存当前数量,所有历史变动全部落在流转记录表里。这样做的好处是任何一个数量变化都能追溯到具体的操作记录,对后期审计非常有利。
2.2 表结构设计要点与索引策略
核心表我设计了下面六张,这里直接列出关键字段:
- material:物资表。id、name、specification、unit、stock_quantity、lower_limit、status。
- stock_record:出入库记录表。id、material_id、record_type(IN 或 OUT)、quantity、department_id、operator_id、remark、create_time。
- department:部门表。id、name、parent_id。
- sys_user:用户表。id、username、password、real_name、department_id、role_id。
- sys_role:角色表。id、role_code、role_name。
- 另外还有一张字典表,用来维护物资分类、计量单位等通用数据。
表结构设计中有几个容易被忽略的点,我单独说一下。
第一,物资表里冗余了 stock_quantity 字段。正常情况下,库存量可以通过出入库记录实时计算出来,但每次都做全表聚合查询性能开销很大。保留冗余字段,每次出入库操作后在事务里同步更新这个值,查询时就能直接取,单条 SQL 搞定。
第二,stock_record 表必须建联合索引。我建的是idx_material_time(material_id, create_time),这样按物资查询时间范围内的出入库记录时能走索引,避免全表扫描。还要单独建一个idx_record_type(record_type)索引,因为按出库类型统计是最常见的查询条件。
第三,所有金额、数量字段都用 DECIMAL 而不是 FLOAT/DOUBLE。虽然物资管理的数量一般不会出现二进制浮点误差问题,但规范一点没坏处,而且 DECIMAL 在报表统计时不会出现小数点后面一堆 9 的情况。
CREATE TABLE stock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', material_id BIGINT NOT NULL COMMENT '物资ID', record_type VARCHAR(10) NOT NULL COMMENT 'in-入库,out-出库', quantity INT NOT NULL COMMENT '数量', department_id BIGINT COMMENT '领用部门ID', operator_id BIGINT NOT NULL COMMENT '操作人ID', remark VARCHAR(255) COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', KEY idx_material_time (material_id, create_time), KEY idx_record_type (record_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出入库记录表';这里要注意,MySQL 如果版本比较老,utf8mb4 的索引长度限制会导致建索引失败,建议建表前先把字符集和排序规则统一设置好。
2.3 几个必须处理的边界数据场景
表结构设计完,有几个边界场景必须在编码前想清楚,否则后面写业务逻辑时很容易出漏洞。
场景一是删除约束。部门可能被删除,但历史出入库记录还引用着这个部门的 ID,如果直接物理删除,历史数据就断了。我的做法是给部门表加一个deleted逻辑删除标记,物理数据永远保留,查询时只过滤有效数据。
场景二是物资下架。某类物资不再使用,不能直接从物资表删除,因为历史记录还关联着它。做法和部门一样,用状态字段标记为“停用”,前端展示时过滤掉,但历史数据仍然完整可查。
场景三是负数库存。库存扣减时如果不对数量做校验,很容易出现并发请求导致库存变为负数。这个问题我在后面专门讲解,表结构层面能做的限制是给stock_quantity字段加UNSIGNED约束,但更关键的还是在业务代码层做控制。
3. 后端实现细节:事务、并发扣减与权限控制
3.1 标准分层结构与关键配置
后端代码我严格按照 Controller → Service → Mapper 三层结构组织,严禁在 Controller 里写业务逻辑。Controller 只做参数接收和结果封装,Service 处理业务规则和数据事务,Mapper 负责数据库操作。这样做的直接好处是,假如要把 Service 层拆成微服务,只需要把接口暴露出去就行,改动量很小。
application.yml里有几个配置值得注意:
spring: datasource: url: jdbc:mysql://localhost:3306/material_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 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.StdOutImpl这个配置里有几个关键项:第一个是时区,之前不设置serverTimezone经常出现时间差 8 小时的问题;第二个是map-underscore-to-camel-case,数据库字段下划线命名可以自动映射成 Java 的驼峰属性,免去大量 resultMap 配置;第三个是 SQL 日志打印,开发阶段打开方便调试,生产环境建议关闭。
3.2 库存扣减的并发安全实现
这是整个系统技术含量最高的地方,值得单独拉出来讲。
想一想这个场景:库存剩余 10 件,两个员工同时提交领用申请,各领 8 件。如果没有并发控制,两个请求都可能先查到库存是 10,然后各自扣减 8,最后库存变成 2 而不是负数,但实际库存已经被超卖了。
我采用了乐观锁方案,在物资表增加一个version字段,每次更新库存时附带版本号条件:
UPDATE material SET stock_quantity = stock_quantity - #{quantity}, version = version + 1 WHERE id = #{materialId} AND version = #{version} AND stock_quantity >= #{quantity}这段 SQL 的含义是:只有当库存数量足够且版本号没变时,才执行扣减。MyBatis 中受影响行数为 0 时抛出异常或重试。我把核心操作放在 Service 层的@Transactional事务里,同时完成扣减库存和插入出入库记录,保证两个操作要么同时成功要么同时失败。
还有一个容易被忽略的细节:stock_quantity字段的精度。如果物资按重量管理(比如消毒液按升),浮点数参与加法减法误差会累积,所以我把数量字段统一设计为整数(最小计量单位),例如 1.5 升按 1500 毫升管理。这个设计初期可能不觉得重要,但用到复杂报表统计时就体现价值了。
3.3 基于 RBAC 的权限拦截与数据隔离
企业管理系统一般都要求不同角色看到不同内容。我实现的是最经典的 RBAC(基于角色的访问控制)模型:用户 → 角色 → 权限。
权限拦截是通过 SpringBoot 拦截器做的,核心代码如下:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token == null || !TokenUtil.verify(token)) { response.setStatus(401); return false; } // 解析 token 获取用户信息,放入 ThreadLocal 供业务层使用 UserContext.set(TokenUtil.parse(token)); return true; } }登录成功时签发一个 Token 返回前端,前端把 Token 存在本地,每次请求放在请求头里。后端拦截器统一校验,没有 Token 直接返回 401 状态码让前端跳转登录页。
这里我踩过一个坑:接口权限只做了登录拦截,没有细粒度到菜单和按钮级权限。后来业务提需求说“普通员工不能看到库存预警设置”,我不得不在前端再做一层路由守卫和菜单过滤,同时在每个接口上加@RequiresPermission注解,总共改了两三天。如果你打算直接拿这套源码改造成公司内部系统,强烈建议先把“接口权限”和“菜单权限”的数据模型设计好,Role 表关联权限表,再关联接口和按钮标识。
用户数据隔离方面,普通用户只能看到自己部门相关操作记录,管理员可以查看全部,我在stock_record查询接口中加入当前用户的部门 ID 过滤条件。这个设计不是最优的(复杂报表查询时需要跨部门汇总),但优点是实现简单、不容易越权。
4. 前端实现细节:Vue 工程化与核心页面交互
4.1 工程目录与鉴权链路
前端用 Vue 脚手架创建,工程目录按功能模块划分,主干如下:
src/api:封装所有后端接口请求。src/router:路由配置,包含动态路由和路由守卫。src/store:Vuex 状态管理,存储用户信息、Token、菜单权限。src/views:页面组件,按模块分目录。src/utils:封装的 axios 实例、工具函数等。
关键的鉴权链路在router/index.js里,用全局前置守卫拦截未登录的访问:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })axios 封装时加请求拦截器,统一在请求头携带 Token;响应拦截器处理后端返回的统一结构,拿到 HTTP 401 时清理本地 Token 并跳转登录页。这样每写一个接口不需要重复判断登录状态,代码干净不少。
4.2 出入库操作、库存看板等核心页面拆解
系统页面按业务场景拆分为三个核心:物资台账、出入库登记、统计看板。
物资台账页面用 Element UI 的el-table分页展示,表格上方提供搜索条件——物资名称、分类、状态。这里有一个被我反复调优的交互细节:搜索按钮触发后必须重置分页页码为 1,否则在搜索条件下用户在第 5 页搜索,拿到的是第 5 页数据,看起来就像搜不到结果,这个问题很隐蔽但极易出现。
出入库登记页是操作最频繁的页面。表单里有物资选择、数量、部门、备注等字段。注意两个细节:一是物资选择下拉框如果物资种类多(几百个),一次性拉全部选项会卡死,前端应该使用远程搜索组件,输入关键字再去后端查;二是数量输入框必须校验必须为正整数,我在前端用rules校验,后端再校验一遍,防止绕过前端直接调接口导致脏数据。
库存看板部分我用了 ECharts 来做柱状图,展示库存数量和预警情况。这个页面还有一个功能:库存低于预警阈值的物资,在台账页列表中用红色高亮显示。当时看到这个高亮效果时,我意识到“数据可视化对于物资管理类系统真的很关键,管理者需要一眼看出问题,而不是从表格里一个个找”。
Vue 前端在首次加载时还会根据当前用户的角色动态渲染菜单,这部分是通过前端代码模拟的menuList过滤实现的,没有做真正的动态路由。
5. 完整源码跑通指南:从本地环境到实际部署
5.1 环境准备与初始化脚本
拿到源码之后第一件事不是运行,而是把环境准备好。需要安装 JDK 1.8 以上、Maven 3.6 以上、MySQL 5.7 以上、Node.js 14 以上。
MySQL 安装完,有两个地方新手容易踩坑。一个是 root 密码认证插件问题,MySQL 8.x 默认使用caching_sha2_password,老版本 JDBC 驱动不兼容,运行报错找不到驱动或连接被拒绝。解决办法是下载最新版 MySQL Connector/J,或者用下面命令把认证方式改回mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;另一个是数据库字符集,建库时建议显式指定:
CREATE DATABASE material_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后把项目里的material_db.sql导入。这个脚本里包含建表语句和部分测试数据,测试数据包含用户、部门、物资、出入库记录等,能保证运行起来就能看到效果。后端配置里数据库连接信息需要改成你自己的用户名密码,改完启动 SpringBoot 应用,项目默认端口 8080。
前端在项目根目录执行npm install,这一步如果网络环境不稳,建议配置 npm 镜像源,然后执行npm run dev启动开发服务器。默认端口是 3000。
5.2 SpringBoot 与 Vue 的联调与部署
开发模式下前端和后端端口不同,需要配置跨域,否则浏览器会拦截接口请求。我后端加了一个全局 CORS 配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:3000") .allowedMethods("GET", "POST", "PUT", "DELETE"); } }如果后端日志或浏览器控制台显示“No 'Access-Control-Allow-Origin' header is present”,基本都是这个配置没生效。
生产环境部署时,前端执行npm run build打包,生成dist目录下的静态文件。把静态文件放在 Nginx 的 html 目录下,同时配置反向代理,把/api路径转发到 SpringBoot 服务:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 解决刷新404问题 } location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }后端打成 jar 包,用 nohup 启动:
nohup java -jar material-system.jar --spring.profiles.active=prod > app.log 2>&1 &5.3 首次启动最常见的四个报错
这部分是我根据自己带新同事、朋友拿源码跑时遇到的真实问题整理的,新手经常在这几个位置卡住。
报错一:数据库密码包含特殊字符。比如密码是abc@123,在 yml 里直接写的话 @ 符号会被当成参数占位符解析,导致连接失败。解决方法是把密码用单引号包起来,或者改用环境变量注入,避免明文写在配置里。
报错二:MyBatis 绑定异常,提示Invalid bound statement (not found)。一般有三种情况:mapper-locations配置的路径和 XML 实际位置不一致;XML 文件没在 Maven 打包时被拷贝到 classpath;接口和 XML 命名空间没对应上。检查第一个的时候注意,路径写成classpath*:mapper/*.xml更安全,能兼容多模块场景。
报错三:启动端口被占用。SpringBoot 默认 8080,本地已经跑着别的应用时启动失败。临时解决可以加启动参数--server.port=8081,也可以把端口配置写到 yml 里。
报错四:前端依赖装不上。npm install 报各种奇怪错误,大概率是 node-sass 这类依赖需要编译,和本地 Node 版本不兼容。建议直接用镜像源,然后检查 Node 版本是否在项目要求的范围内。如果项目里是 node-sass,我后面已经改成 sass(dart-sass)了,问题彻底解决。
6. 企业级项目的进阶优化方向与源码阅读建议
6.1 性能与扩展性的演进路线
这套系统用在小规模场景非常流畅,但你要是想把它改造升级成支撑更大规模业务的系统,有几个方向值得考虑。
第一个是缓存层。目前所有查询都直连 MySQL,在高并发场景下数据库压力很大。比如库存看板这种所有人都要看的页面,可以引入 Redis 缓存,把聚合查询结果缓存 30 秒,大大减少数据库压力。具体实现可以基于注解做框架封装,但这套系统里没有引入 Redis,需要你自己扩展。
第二个是异步化。出入库操作本身要求事务一致性,但操作成功后的短信通知、消息推送、日志审计不需要同步完成。把这些动作改成发消息到 MQ 或者 Spring 自带的异步事件处理,可以显著缩短接口响应时间。
第三个是数据库层面的考虑。当数据量达到百万级,单表查询会明显变慢,可以考虑按时间分表,比如 stock_record 按月分表,或者引入读写分离。不过对绝大多数使用场景来说,加索引和 SQL 优化足够解决 99% 的性能问题,不用一上来就整这些复杂的架构。
6.2 安全加固方向
企业级系统上线前必须做安全检查,至少以下三个位置不能有漏洞。
第一是 SQL 注入。MyBatis 框架用#{}传参时是预编译,天然防注入,但有人写成${}直接拼字符串,那就是个大漏洞。搜索项目里所有${}的用法,除了 ORDER BY 字段名和列名这种无法用预编译的场景,其他全部改掉。ORDER BY 字段名也不能乱拼,最好是前端传字段白名单,后端做映射,不合法就拒绝请求。
第二是越权访问。现在接口只校验“是否登录”,没区分“这个用户有没有权限操作这个数据”。例如一个普通用户拿到了领用记录的接口 URL,直接绕过前端就能调接口无限领用。建议做数据权限:用户只能查询自己部门的数据,管理员全部数据,这个我在前面 RBAC 部分讲过了,改造的时候优先做这个。
第三是密码安全。现在用户表存的是密码明文或简单哈希,这是非常危险的。改成 BCrypt 加密存储,增加随机盐。注册或修改密码时,前端拿到明文,后端用 BCrypt 加密后存库。登录校验时用matches方法比对,不要自己发明加密算法。
6.3 拿到源码后怎么读代码最快
如果你刚拿到这套源码,我建议不要从启动开始读,也不要按 package 顺序从上往下读,效率太低。
我的建议是先跑起来,然后用一个完整业务串联代码。比如“入库一批口罩”这个操作,从前端点击“入库”按钮开始,跟踪到接口请求怎么发出、后端 Controller 怎么接收、Service 怎么实现事务逻辑、Mapper 怎么更新数据,一条线读完就掌握了整个系统的核心链路。
第二遍读“登录 + 权限”这条线,弄明白 Token 怎么签发、拦截器怎么拦截、权限怎么控制。这两条链弄明白,这套系统的骨架就拿下了。剩下其他模块都是围绕相似逻辑在重复,阅读速度会快很多。
读代码的时候重点关注几个关键类:TokenUtil、StockService、AuthInterceptor、MaterialController,这几个类是系统中复用率最高的核心模块。
最后再说一个心态问题。很多人拿到完整项目源码第一反应是“我把它运行起来就好了”,然后跑起来就不知道怎么继续学了。真正让你技术提升的不是跑起来那个瞬间,而是把代码改坏再修好的过程。你可以试试把库存扣减里的乐观锁去掉,然后并发请求测试,感受一下数据错乱是什么样子,再改回来,这个过程比看十遍代码都有用——一定不要只停留在“能运行”的层面,要主动去改、去拆、去踩坑,技术才能变成自己的。