做过物资管理系统这类项目的朋友应该都有体会:表面上就是一套“登记-审批-发放”的流程,但真正落地的时候,你会发现库存对不上、审批链混乱、发放记录查不清楚,各种麻烦接踵而至。这个基于 JavaWeb 和 MySQL 的 SpringBoot 物资发放管理系统,技术栈集中在 java + springboot + html + thymeleaf + layui + mysql + maven,正好解决的问题就是“用什么记录、怎么审批、如何统计”这三件事。它适合两类人看:一类是刚学完 Spring Boot 基础、准备找一个完整的 CRUD 项目练手的学生;另一类是单位或企业内部确实需要快速搭一套物资发放台账的开发者。本文我会从项目分层设计、核心模块拆解、数据库落地到部署踩坑,把整个系统的实现逻辑从头到尾捋一遍。
先说这套系统的定位。它不属于高并发、分布式那种宏大架构,而是典型的中小型信息管理系统,核心目标是一件事:把物资从入库、申领、审批到发放的完整链路管起来,让每一步都有据可查。所以不要幻想里面有多复杂的并发方案或者微服务拆分,它就讲究四个字:清晰、够用。下面我从项目设计层面开始讲。
1. 整体设计与技术选型
1.1 为什么选 SpringBoot + Thymeleaf 而不是前后端分离
现在很多新项目一上来就是 Vue + Spring Boot 做成前后端分离,但对于物资发放这种内部管理系统,SpringBoot + Thymeleaf 的服务器端渲染方案依然是很务实的选择。原因很简单:系统功能以表单提交、列表展示、状态流转为主,不存在大量复杂交互,Thymeleaf 直接在 HTML 里写th:each循环渲染列表、th:if控制按钮显隐,开发效率比前后端分离高得多,少写一堆接口文档和跨域配置。
另外,Layui 作为前端 UI 框架非常契合这种项目。它对后端开发者极其友好,不需要懂什么 npm、webpack、组件化,直接下载静态文件放到项目里就能用。表格用table.render渲染,弹窗用layer.open,表单用form.on('submit')监听,每一个组件都是标准的“引入 JS + 初始化配置”模式。这套组合拳下来,一个人从数据库设计到页面成型,工作量可以压缩到极短时间。
1.2 标准的三层架构分层
这套系统的包结构建议按照常见的三层架构来拆:
controller层:接收前端请求,调用 service,返回视图名称或 JSON 数据。service层:承载核心业务逻辑,比如库存扣减、发放单状态流转、权限校验。mapper层(或 dao 层):通过 MyBatis 或 JPA 操作 MySQL 数据库。
我在实际项目里还会额外加一层vo包,专门放视图对象,比如UserVO、MaterialStockVO,用来把关联查询的结果封装成前端表格可以直接绑定的结构。这样做的好处是避免在 HTML 模板里频繁使用user.getDept().getName()这种深层次取值,模板代码会干净很多。
1.3 角色权限的初步设计
物资发放系统要不要引入 Shiro 或 Spring Security?我的建议是:看用户规模。如果就是一个单位内部几十人使用,完全没有必要引入安全框架,直接在拦截器里写一个简单的会话判断就够了。但如果你准备把这个项目作为毕设作品展示,或者有扩展预期,我推荐加一层基于 Spring Security 的简易权限控制,并把用户角色设计成“系统管理员、仓库管理员、普通申领人”三种。
管理员负责用户管理和系统配置,仓库管理员负责物资入库、发放单审核、出库登记,普通申领人只能提交申领申请和查看自己的历史记录。权限控制的核心逻辑很简单:在自定义拦截器中判断session.getAttribute("loginUser")是否为空,再根据user.getRole()判断是否有访问某个 URL 的权限。配置文件里把需要拦截的路径写清楚,比如/admin/**只能由系统管理员访问。
2. 核心功能模块解析
2.1 登录认证与会话管理
登录功能是这套系统所有权限控制的地基。实操上我建议把登录接口设计成/login接收 POST 请求,参数为用户名、密码。密码存储一定不要明文,至少要用 MD5 加盐,或者更推荐 BCrypt 加密。虽然内部系统的安全要求不用跟互联网产品对齐,但这是基本职业素养。
登录成功后,把用户 ID、用户名、角色 ID 放进 session,跳转到/index。这里有一个经常被忽略的坑:HTML 里的静态资源——CSS、JS、图片——如果被拦截器拦截,页面打开就会是一片裸样式,样式全丢。所以拦截器配置必须放行静态资源路径,Thymeleaf 项目中通常用th:href="@{/layui/css/layui.css}"这种写法生成带上下文路径的完整 URL。
2.2 物资档案与库存管理模块
物资档案是这个系统的数据底座。每一件物资需要登记的信息包括:物资编码(唯一)、名称、规格型号、计量单位、分类(如办公耗材、劳保用品、应急物资)、当前库存量、库存预警阈值、存放位置。
库存管理还有一招比较实用:在物资列表页加一个“库存状态”的颜色标记。当库存数量高于预警值时显示正常,低于预警值但大于 0 时显示“低库存”的黄色标签,等于 0 时显示灰色“已断货”标签。这个功能通过 Layui 表格的自定义模板列实现,判断逻辑写在模板字符串里,非常简单,但对使用体验的提升却很明显。
2.3 申领审批与发放核销流程
这是整个系统最有业务含量的部分。我设计的是“申领单”作为核心流转实体,一条完整的发放链路要经过下面五个状态:
- 待审核:申领人填写物资名称、申领数量、用途说明,提交后生成申领单。
- 审核通过:管理员确认申请合理,库存数量充足,允许发放。
- 已发放:仓库管理员执行出库操作,库存扣减,生成发放记录。
- 已驳回:申请被退回,申领人可以看到驳回原因。
- 已取消:申领人主动撤回未审核的申请单。
这个流程中有一个业务细节特别容易出错:库存扣减的时机。我见过不少项目在“审核通过”时就扣库存,结果申领人后来没有来领物资,库存就莫名其妙短了。正确做法是,在“已发放”这个环节才真正执行库存扣减。审核通过只是锁定预留,出库时才变更库存表数据,同时记录发放人和发放时间。这样库存流和单据流的每一步都能对上。
2.4 统计报表与发放记录查询
报表模块的价值在于让“发了多少、还剩多少、发给谁了”一目了然。我常用的实现方案有三种:
- 按物资维度统计:汇总每一种物资的入库总量、发放总量、当前库存。
- 按部门维度统计:汇总各部门申领物资的种类数和总数量。
- 按时间维度统计:查询某一时间段内每天发放的物资金额总量。
数据库层面用GROUP BY加聚合函数就能实现,关键是把查询结果封装成报表专用的 VO 类。展示端我比较推荐两个方式:表格呈现明细数据,ECharts 画柱状图和饼图。ECharts 在 Thymeleaf 中也很好接入,无非就是引入 JS 文件,然后在模板里定义一个div容器,再用th:inline="javascript"把后端传来的统计数据输出为 JS 数组。
3. 数据库设计与核心 SQL
3.1 核心表结构设计
数据库是整个系统最不能省功夫的部分。我要强调一点:字段设计直接影响后续开发的效率和系统维护的难度,宁可在建表时多思考十分钟,也不要等代码写了一半再回来改表。
我设计的核心表至少有下面这六张:
sys_user用户表:id、username、password、real_name、role_id、dept_name、phone、status、create_time。material_info物资档案表:id、material_code、material_name、specification、unit、category、quantity、warning_line、location、remark。material_stock_log库存流水表:id、material_id、change_type(入库/出库/盘点调整)、change_quantity、balance_quantity、operator_id、create_time。apply_order申领单主表:id、order_no、applicant_id、dept_name、apply_reason、total_quantity、status、apply_time、audit_time、audit_user、audit_remark。apply_order_item申领单明细表:id、order_id、material_id、apply_quantity、deliver_quantity。deliver_record发放记录表:id、order_id、material_id、deliver_quantity、unit_price、deliver_user_id、deliver_time、remark。
申领单主表和明细表是一对多的关系。为什么一定要拆成两张表?因为一张申领单可能包含多种物资,如果只用一个字段去存物资 ID,想用 SQL 统计每种物资的发放总量时会无比痛苦。拆成子表之后,apply_order_item相当于一个关联实体,每一行代表“某张单子里的某一种物资要发多少”。
3.2 库存扣减的事务处理
这里的核心是事务问题。当一个申领单包含多种物资时,发放动作会同时更新库存流水表和发放记录表。如果某一笔物资出库成功,另一笔因为库存不足失败了,绝对不能出现“第一笔扣了库存、第二笔没扣成”的脏数据。
解决办法是在 Service 层方法上标注@Transactional(rollbackFor = Exception.class)。回滚逻辑必须注意:MyBatis 的update语句返回的是受影响行数而不是实体,所以在扣减库存的 SQL 里要加一个条件quantity >= apply_quantity。如果返回值是 0,说明库存不足,直接抛出运行时异常,让事务回滚。这条 SQL 我写出来你就明白了:
UPDATE material_info SET quantity = quantity - #{applyQuantity} WHERE id = #{materialId} AND quantity >= #{applyQuantity}条件里带上quantity >=是防止并发情况下的超卖问题。不要觉得内部系统并发低就不用管,一旦两个人同时审批同一个单子,这种边界情况就会冒出来。
3.3 分页查询与动态 SQL
列表页的分页查询是此类系统的标配需求。一个物资列表往往有几百条数据,一次性渲染到页面上既不美观也影响查询效率。所以要么用 MyBatis 的 PageHelper 插件,要么手写 LIMIT 分页。
手写分页其实也不复杂,核心在于两个接口方法:一个是查询总记录数,一个是查询当前页数据。前端 Layui 表格的page参数会传来page(页码)和limit(每页条数),后端接收后计算偏移量:
offset = (page - 1) * limit然后拼接 SQL:
SELECT * FROM material_info WHERE material_name LIKE CONCAT('%', #{keyword}, '%') ORDER BY create_time DESC LIMIT #{offset}, #{limit}我给一个建议:分页查询和动态条件查询往往同时出现,所以 Mapper XML 里务必用<where>标签配合<if>来拼接条件,否则用户一输入搜索关键词,SQL 就会因为多了一个WHERE AND直接报错。
4. 从零到一的构建过程与部署经验
4.1 开发环境准备与初始化
如果你是从零开始搭这个项目,按下面这个清单准备环境基本不会出错:
- JDK 1.8 或更高版本(建议用 1.8,兼容性最稳)。
- Maven 3.6 以上,用来管理依赖。
- MySQL 5.7 或 8.0,建库时统一用 UTF-8 字符集:
CREATE DATABASE material_db DEFAULT CHARACTER SET utf8mb4; - 开发工具用一个称手的即可,导入 Maven 项目后等待依赖下载完成。
Spring Boot 的版本选择上我建议用 2.x 系列,比如 2.7.x。不要一上来直接用 Spring Boot 3.x,因为 Thymeleaf 和 Layui 的静态资源路径写法在老项目里早已验证过坑最少,Spring Boot 2.7 配合 JDK 8 是“零意外组合”。
4.2 关键配置文件的坑
application.yml中有几个配置项很容易被新手忽略,我这里列出来当参考模板:
spring: thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html datasource: url: jdbc:mysql://localhost:3306/material_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truethymeleaf.cache: false只在开发阶段用,改完模板刷新浏览器就能看到效果,不用反复重启项目,效率提升非常明显。上线部署时再改成 true,性能会好一点。map-underscore-to-camel-case: true这个配置也很关键,它能让数据库的apply_order_no自动映射成实体类的applyOrderNo属性,省去大量手写 resultMap 的麻烦。
4.3 打包发布与运行截图注意事项
项目完成后,在 Maven 面板执行package命令打包,果一切正常,target目录下会出现一个.jar文件。部署时可以这样启动:
java -jar material-system.jar --spring.datasource.password=你的密码如果你想部署到独立的 Tomcat,需要把 Spring Boot 的打包方式改成war,并且继承SpringBootServletInitializer,然后重写configure方法。不过我更推荐直接用内置 Tomcat 跑,简单省事。有一点要提醒:服务器上 MySQL 的时区问题很常见,报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized错误时,在 JDBC 连接串里加上serverTimezone=Asia/Shanghai就能解决,这个参数容易漏,漏了就是折磨。
5. 常见问题与排查技巧实录
5.1 页面模板加载出来全是纯文本
这个现象大多是引入 CSS 和 JS 的方式写错了。用 Thymeleaf 模板时,静态资源路径一定要带上th:href和th:src属性,让框架自动拼接上下文路径。如果写成href="/layui/css/layui.css",在本地 IDE 跑可能没事,但部署到带项目名的 Tomcat 后就全部 404。
5.2 列表查询时报 SQL 语法错误
大多数原因是数据库字段名和实体属性名对不上。比如数据库字段是material_code,实体属性是materialCode,如果 MyBatis 没开启驼峰映射,返回的结果里这个字段就一直是 null。还有一种情况是动态 SQL 拼接问题,<where>标签前直接写WHERE conditions导致多余的关键字。
5.3 发放记录丢失但库存已经扣减
出现这种问题几乎可以肯定没有加事务控制。记住一句话:涉及“改库存 + 写流水 + 更新单状态”三个动作的方法,必须使用@Transactional。还有,事务不能只加在 Controller 层而要在 Service 层加,因为 Controller 方法往往调用了多个 Service 方法,事务粒度控制不当就会部分提交。
5.4 常见问题速查表
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| 登录后页面闪回登录页 | session 没写入或拦截器逻辑颠倒 | 检查登录成功的 redirect 路径以及拦截器 exclude 配置 |
| 表格数据空白但接口有返回 | Layui 表格要求的 json 格式不对 | 返回的数据必须包含code、msg、count、data四个字段 |
| 时间日期显示为英文格式 | 未配置时间序列化格式 | 在实体类的LocalDateTime字段加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| 部署后无法连接数据库 | 防火墙未放行 3306 端口或 JDBC URL 配错 | 测试telnet ip 3306,并检查 yml 配置 |
| 修改页面代码后不生效 | Thymeleaf 缓存未关闭 | 开发时将cache: false;若仍不生效则强制刷新浏览器或重启应用 |
5.5 一点实际经验的补充
这些坑我基本上都亲自踩过。印象最深的是有一次排查半天,发现 Layui 表格一直提示“数据接口请求异常”,原因居然是我在实体类里使用了大写开头的Integer类型作字段,但数据库字段直接用了number,而 MySQL 里number并不是保留字,MyBatis 却因为映射关系没写对把 SQL 生成成了number列名。从那以后我给自己立了一条规矩:所有数据库表字段和实体类字段在设计阶段就统一命名风格,避免后续手工映射,能省下大量无意义的排查时间。
6. 项目演示与后续扩展方向
系统跑通之后,你可以从两个方向继续扩展。第一个方向是加上物资批次和有效期管理,比如某些物资有保质期,发放时要遵循先进先出原则,这需要在库存流水表里增加批次号字段,通过GROUP BY batch_no获取每个批次的剩余数量;第二个方向是引入简单的消息通知,比如申领人被驳回时在系统首页收到一条待办提醒,实现方案可以是在数据库加消息表,登录后在 Thymeleaf 模板中读取未读数量,用 Layui 的dropdown组件做一个下拉通知栏。
我个人在实际项目中的习惯是,先保证核心的“物资档案 + 申领审批 + 库存流水”三个环节跑通且数据准确,再去考虑报表可视化、消息推送这些锦上添花的功能。因为物资管理系统的灵魂是数据和流程可靠,而不是界面多花哨。很多项目做到一半推倒重来,不是技术问题,是流程混乱导致数据对不上,这一点才是大家真正要小心的。
最后再分享一个小技巧:开发阶段记得频繁使用浏览器控制台看网络请求,Layui 表格的每一次数据请求都能在 Network 面板里看到返回的 JSON 结构。遇到问题先定位是前端渲染问题还是后端数据问题,不要上来就改 Java 代码。学会这一招,排查问题的速度能提升一大半。