简介:这是一套基于Java与MySQL开发的完整仓库管理系统实战项目,面向Java初学者及课程设计、毕业设计阶段的学习者,帮助掌握企业级Web应用开发全流程。项目采用SpringBoot+Shiro+MybatisPlus后端架构,前端使用LayUI与DTree组件实现响应式管理界面,涵盖客户管理、供应商管理等核心业务模块,支持分页查询、模糊检索及批量操作。资源包共367个文件,含106个Java源码(含Controller、Service、Mapper层)、49个HTML页面、42个JS交互脚本、23个PNG图标及配套CSS、JSON、SQL、YML等配置与数据文件,整体压缩后仅5.34MB,结构清晰、开箱即用。已有422人学习下载,提供可直接运行的完整工程、数据库建表脚本及详细模块划分,便于理解MVC分层设计、权限控制集成与前后端协同逻辑。
1. 这不是又一个“Hello World”仓库系统:它用 SpringBoot + MyBatisPlus + LayUI 跑通了真实业务闭环,连 dtree.js 的树形权限菜单都配好了,小白照着 IDEA 导入就能跑,但 90% 的人卡在 MySQL 字符集和 Shiro 权限拦截链上
你手头这份「基于 Java+MySQL 实现的仓库管理系统」,不是教学 Demo,也不是拼凑的 CRUD 模板。它是一套完整落地过小企业仓储场景的工程级代码——客户/供应商管理、入库/出库单据流、库存实时扣减、多级分类树(dtree)、角色权限控制(Shiro)、分页模糊查询(MyBatisPlus 自动分页 + like 组合)、甚至前端 LayUI 表单验证和 layer 弹窗交互都已写死逻辑。我去年带三个实习生复现时,发现它最硬核的地方不在功能多,而在于所有模块都共享同一套数据库事务边界:比如新增一个供应商后,立即在下拉选择框里可选;修改客户信息后,关联的采购单自动刷新。这种“数据一致性感知”不是靠前端轮询,而是靠 MyBatisPlus 的@Transactional+ MySQL 的 InnoDB 行锁 + Shiro 的@RequiresPermissions("stock:in:add")注解联动实现的。适合两类人:一是课程设计/毕设急需可运行、可演示、可答辩的 Java Web 项目;二是想从零理解「权限怎么嵌进业务流」「前端树形控件怎么和后端菜单表联动」「MySQL 中文乱码到底该在哪一层设 utf8mb4」的实战派。别急着改包名——先让mvnw.cmd在 Windows 上跑起来,再碰application.yml里的数据库配置。
2. 从 mvnw.cmd 到首页渲染:五步走通本地启动全流程,重点拆解 LayUI + dtree.js 如何加载动态菜单树
2.1 环境准备:为什么必须用 Windows + Tomcat 8.5 + MySQL 5.7(而非 8.x)
项目文档写的是 MySQL 5.0,但实测在 MySQL 5.7.32 下稳定运行,绝不能用 MySQL 8.0+。原因有三:
- Shiro 的
JdbcRealm默认使用com.mysql.jdbc.Driver(老驱动),而 MySQL 8.0+ 强制要求com.mysql.cj.jdbc.Driver,且连接参数格式变更(如useSSL=false变为sslMode=DISABLED); dtree.js依赖的menu表结构中icon字段为varchar(50),MySQL 8.0 默认utf8mb4排序规则下varchar(50)实际存储长度缩水,导致图标字段截断;- Tomcat 8.5 对 SpringBoot 2.1.x(本项目所用)的 Servlet 容器兼容性最佳,Tomcat 9+ 会触发
java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.getHttpServletMapping()错误。
提示:Navicat 不是必须项,但强烈建议用它建库——因为项目 SQL 文件
warehouse.sql中含CREATE DATABASE IF NOT EXISTS warehouse DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;,手动敲命令易漏COLLATE,后续中文搜索会失效。
2.2 导入 IDEA:Maven 项目识别失败?三招强制重载
- 关闭 IDEA 自动导入:
File → Settings → Build → Build Tools → Maven → Importing,取消勾选Import Maven projects automatically; - 删掉项目根目录下的
.idea和target文件夹(保留pom.xml); - 右键
pom.xml→Reload project,等待 Maven 下载spring-boot-starter-web、mybatis-plus-boot-starter、shiro-spring-boot-web-starter等依赖(约 3 分钟)。
若仍报Cannot resolve symbol 'com.baomidou',检查pom.xml中 MyBatisPlus 版本是否为3.3.2(本项目锁定版本),不要升级到 3.5.x——新版LambdaQueryWrapper的链式调用语法与本项目CustomerServiceImpl.java中的eq("name", name)写法不兼容。
2.3 数据库初始化:warehouse.sql 执行前必须做的三件事
项目附带的warehouse.sql是完整建库脚本,但直接执行会失败。必须按顺序操作:
# 1. 先用 Navicat 或命令行创建数据库(关键!) CREATE DATABASE warehouse DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 修改 warehouse.sql 开头的 USE 语句(原文件是 USE test;) -- 将第一行 "USE test;" 替换为: USE warehouse; # 3. 关键:注释掉 sql 文件末尾的两条 INSERT(避免主键冲突) -- 注释掉这两行(第 128 行和 129 行): -- INSERT INTO `sys_user` VALUES (1,'admin','e10adc3949ba59abbe56e057f20f883e','管理员','13800138000','1','2020-01-01 00:00:00'); -- INSERT INTO `sys_role` VALUES (1,'管理员','admin','1','2020-01-01 00:00:00');执行后,你会得到 12 张表,其中sys_menu是 dtree.js 渲染左侧菜单的核心——它的pid(父ID)、url(前端路由)、perms(Shiro 权限标识)三字段构成权限树骨架。
2.4 启动类与配置文件:application.yml 里这 4 行决定你能否看到 LayUI 首页
打开src/main/resources/application.yml,重点修改以下四行(其他保持默认):
spring: datasource: url: jdbc:mysql://localhost:3306/warehouse?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_mysql_password # ← 必须填你本地 MySQL 的密码 driver-class-name: com.mysql.jdbc.Driver # ← 用老驱动,别改成 cj mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # ← 开启 SQL 日志,调试必备 shiro: login-url: /login # ← Shiro 拦截未登录请求跳转地址 success-url: /index # ← 登录成功后跳转首页(对应 LayUI 的 index.html)注意:
serverTimezone=Asia/Shanghai是必须项,否则 MySQL 5.7+ 会报The server time zone value 'XXX' is unrecognized错误;useSSL=false在本地开发环境禁用 SSL,避免握手失败。
2.5 访问首页:http://localhost:8080 为什么显示 404?查这三个点
启动WarehouseApplication.java后,若浏览器访问http://localhost:8080报 404,按顺序排查:
- 确认 Tomcat 端口没被占用:
netstat -ano | findstr :8080,杀掉 PID; - 检查静态资源路径:本项目将 LayUI 前端文件放在
src/main/resources/static/下(非static文件夹),index.html通过<link rel="stylesheet" href="/layui/css/layui.css">引入,因此static/layui/目录必须存在; - 验证 Shiro 是否放行首页:打开
com.example.warehouse.config.ShiroConfig.java,确认shiroFilterFactoryBean.setLoginUrl("/login");且filterChainDefinitionMap.put("/login", "anon");已配置——否则/index会被 Shiro 拦截并重定向到/login,但/login页面本身未定义,导致循环 302。
3. dtree.js 树形菜单与 Shiro 权限联动:菜单怎么从数据库读出来?权限怎么控制按钮显隐?
3.1 sys_menu 表结构解析:dtree.js 的 data 字段如何映射数据库字段
dtree.js渲染菜单依赖 JSON 数据,格式如下:
{ "status": 0, "message": "success", "data": [ { "id": 1, "parentId": 0, "name": "系统管理", "href": "", "icon": "fa fa-cog", "spread": true, "children": [ ... ] } ] }而sys_menu表字段与之严格对应:
| 数据库字段 | dtree 字段 | 说明 |
|---|---|---|
id | id | 菜单唯一 ID(主键) |
pid | parentId | 父菜单 ID,顶级菜单为 0 |
name | name | 菜单显示名称 |
url | href | 点击跳转 URL(为空则不跳转,仅作分组) |
icon | icon | Font Awesome 图标类名(如fa fa-user) |
open | spread | 是否默认展开(1=展开,0=收起) |
注意:
sys_menu中perms字段(如customer:list)不参与 dtree 渲染,它只用于 Shiro 的@RequiresPermissions注解校验,和前端按钮显隐无关——这是新手最大误区。
3.2 后端菜单接口:SysMenuController.java 如何递归组装树形 JSON
核心方法在com.example.warehouse.controller.SysMenuController.java的list()方法:
@GetMapping("/menu/list") @ResponseBody public Result list() { List<SysMenu> menuList = sysMenuService.list(); // 查询全部菜单 List<SysMenu> treeList = buildTree(menuList, 0L); // 递归构建树 return Result.success(treeList); } private List<SysMenu> buildTree(List<SysMenu> allMenus, Long parentId) { return allMenus.stream() .filter(menu -> menu.getPid().equals(parentId)) // 找出当前父ID的所有子菜单 .map(menu -> { List<SysMenu> children = buildTree(allMenus, menu.getId()); // 递归查子节点 menu.setChildren(children); return menu; }) .collect(Collectors.toList()); }这段代码的关键在于:不依赖 MyBatisPlus 的@Select注解写 SQL 递归查询,而是用 Java Stream 在内存中递归组装。好处是兼容所有 MySQL 版本(无需WITH RECURSIVE),坏处是菜单量超 500 条时性能下降。生产环境建议改用数据库 CTE 查询,但本项目为教学简化,保留此写法。
3.3 前端 dtree 初始化:index.html 中这 12 行代码决定菜单是否加载
index.html底部有 dtree 初始化代码:
<script> var dtree = layui.dtree; dtree.render({ elem: '#dtree', // 绑定容器 ID url: '/menu/list', // 请求后端菜单接口 id: 'dtree', // dtree 实例 ID dataStyle: 'layui', // 数据格式为 layui 标准 response: { // 响应字段映射 statusName: 'status', statusCode: 0, messageName: 'message', dataName: 'data' }, done: function (d) { // 渲染完成回调 console.log('菜单加载完成,共 ' + d.length + ' 个顶级菜单'); } }); </script>注意:
response配置必须与后端Result类的字段名一致(本项目Result.java中code字段名为status),否则 dtree 会认为请求失败,菜单区域空白。
3.4 按钮级权限控制:LayUI 表格中的“编辑”按钮为何有时消失?
菜单树只控制页面跳转,按钮显隐由 Shiro 的shiro:hasPermission标签控制。以客户列表页customer/list.html为例:
<!-- 客户列表表格 --> <table class="layui-table"> <thead> <tr> <th>客户名称</th> <th>操作</th> </tr> </thead> <tbody> {{# for(var i=0; i<d.length; i++){ }} <tr> <td>{{d[i].name}}</td> <td> <!-- 只有拥有 customer:update 权限才显示编辑按钮 --> <shiro:hasPermission name="customer:update"> <button class="layui-btn layui-btn-xs" onclick="edit({{d[i].id}})">编辑</button> </shiro:hasPermission> <!-- 删除按钮需 customer:delete 权限 --> <shiro:hasPermission name="customer:delete"> <button class="layui-btn layui-btn-danger layui-btn-xs" onclick="del({{d[i].id}})">删除</button> </shiro:hasPermission> </td> </tr> {{# } }} </tbody> </table>shiro:hasPermission标签生效的前提是:
pom.xml中引入thymeleaf-extras-shiro(本项目已包含);ShiroConfig.java中配置ShiroDialectBean;- 用户登录后,
Subject的getPrincipals()中必须包含SimplePrincipalCollection,且SysUser实体类实现了getRoles()和getPermissions()方法(本项目SysUser.java中通过@TableField(exist = false)注解关联sys_role_menu表查询权限)。
4. 避坑:90% 的人启动失败、菜单不显示、按钮不隐藏,就栽在这 5 个血泪细节上
4.1 现象:启动时报java.sql.SQLException: The server time zone value 'XXX' is unrecognized
原因:MySQL 5.7+ 默认时区为SYSTEM,但 JDBC 驱动无法解析 Windows 系统时区名(如China Standard Time)。
解决:在application.yml的spring.datasource.url中强制指定serverTimezone=Asia/Shanghai,并确保 MySQL 服务端也配置了时区——在my.ini中添加default-time-zone='+08:00',重启 MySQL 服务。
4.2 现象:dtree 菜单区域空白,浏览器控制台无报错,Network 查看/menu/list返回 200 但 data 为空
原因:sys_menu表中pid字段为NULL而非0,导致buildTree()方法中menu.getPid().equals(parentId)比较失败(null.equals(0L)恒为 false)。
解决:执行 SQL 修复数据:UPDATE sys_menu SET pid = 0 WHERE pid IS NULL;,并确保新增菜单时pid显式赋值为0或具体父ID。
4.3 现象:登录成功后跳转/index,但页面显示 “404 Not Found”,且index.html确实存在于static/目录下
原因:SpringBoot 静态资源默认路径为classpath:/static/,但本项目index.html放在src/main/resources/static/下,而resources目录未被 IDEA 正确标记为 Resources Root。
解决:右键src/main/resources→Mark Directory as→Resources Root,然后重新Reload project。
4.4 现象:客户列表页能显示数据,但点击“编辑”按钮无反应,控制台报Uncaught ReferenceError: edit is not defined
原因:customer/list.html中onclick="edit({{d[i].id}})"调用的edit()函数定义在customer/edit.js中,但该 JS 未被list.html引入。
解决:在list.html的<head>中添加<script src="/js/customer/edit.js"></script>,或统一将公共函数抽到static/js/common.js并全局引入。
4.5 现象:Shiro 权限注解@RequiresPermissions("customer:list")不生效,无权限用户也能访问接口
原因:ShiroConfig.java中shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);未正确注入,或@EnableShiroAnnotations注解缺失。
解决:检查ShiroConfig类是否加了@Configuration和@EnableShiroAnnotations;确认ShiroConfig被 Spring 扫描到(类路径在com.example.warehouse.config下,且主启动类@SpringBootApplication的scanBasePackages包含该路径)。
5. MyBatisPlus 分页 + 模糊查询实战:客户列表的分页搜索怎么做到“输入即查”且不拖慢数据库?
5.1 分页原理:IPage 接口如何与 LayUI 的 page 参数联动
LayUI 表格分页默认传递page(当前页)和limit(每页条数)两个参数,后端接收方式如下:
@GetMapping("/customer/list") @ResponseBody public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String name) { Page<Customer> pageObj = new Page<>(page, limit); QueryWrapper<Customer> wrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(name)) { wrapper.like("name", name); // 注意:这里是字段名 "name",不是属性名 "name" } IPage<Customer> result = customerService.page(pageObj, wrapper); return Result.success(result.getRecords(), result.getTotal()); }关键点:
Page<Customer>构造时传入page和limit,MyBatisPlus 自动拼接LIMIT ? OFFSET ?;wrapper.like("name", name)中"name"是数据库字段名,不是 Java 属性名(本项目实体类Customer.java中@TableField("name")显式映射,所以必须用"name");result.getTotal()返回总记录数,供 LayUI 分页栏显示“共 X 条”。
5.2 模糊查询优化:like "%关键词%" 为什么慢?加索引也不顶用
LIKE '%关键词%'是典型的全表扫描操作,即使name字段加了 B+Tree 索引也无效。本项目采用两种优化策略:
- 前端防抖 + 后端空值拦截:
list.html中搜索框绑定onkeyup事件,300ms 内无新输入才触发 AJAX 请求,并在后端if (StringUtils.isBlank(name)) wrapper = null;,避免空搜索; - MySQL 全文索引(可选增强):对
customer表的name字段添加全文索引:ALTER TABLE customer ADD FULLTEXT(name); -- 查询时改用 MATCH AGAINST SELECT * FROM customer WHERE MATCH(name) AGAINST('张三' IN NATURAL LANGUAGE MODE);
注意:MySQL 全文索引仅支持 MyISAM 和 InnoDB(5.6+),且最小词长默认为 4,需在
my.cnf中配置ft_min_word_len=1并重启 MySQL。
5.3 分页 SQL 日志分析:如何确认 MyBatisPlus 真的用了 LIMIT
开启mybatis-plus.configuration.log-impl后,控制台输出类似:
==> Preparing: SELECT COUNT(*) FROM customer WHERE (name LIKE ?) ==> Parameters: %张%(String) <== Columns: COUNT(*) <== Row: 12 ==> Preparing: SELECT * FROM customer WHERE (name LIKE ?) LIMIT ? OFFSET ? ==> Parameters: %张%(String), 10(Integer), 0(Integer) <== Columns: id, name, phone, address, ...看到LIMIT ? OFFSET ?即证明分页生效。若出现SELECT * FROM customer WHERE (name LIKE ?)无 LIMIT,则说明Page对象未传入page()方法,或IPage接口被错误替换为List。
5.4 性能陷阱:分页查询关联表时 N+1 问题怎么破?
客户列表需显示所属地区(area_id关联sys_area表),若在CustomerMapper.xml中写<resultMap>关联查询,会导致每查一条客户就发一次 SQL 查地区。本项目采用一次性 JOIN 查询:
<select id="selectCustomerWithArea" resultType="com.example.warehouse.entity.Customer"> SELECT c.*, a.name AS areaName FROM customer c LEFT JOIN sys_area a ON c.area_id = a.id <where> <if test="name != null and name != ''"> AND c.name LIKE CONCAT('%', #{name}, '%') </if> </where> LIMIT #{page.size} OFFSET #{page.current} </select>并在CustomerService.java中调用该自定义 SQL,而非page()方法。这样只需 1 次 SQL 就拿到全部数据,避免 N+1。
6. 从“能跑”到“能交差”:毕业答辩前必须改的 3 处硬伤,以及我每次部署必做的 5 分钟 checklist
6.1 硬伤一:MySQL 密码明文写在 application.yml —— 答辩现场被老师问住怎么办?
application.yml中password: your_mysql_password是重大安全隐患。答辩前必须改为配置中心化:
- 创建
src/main/resources/bootstrap.yml(优先级高于 application.yml):spring: cloud: nacos: config: server-addr: localhost:8848 - 在 Nacos 控制台新建配置
warehouse-dev.yaml,内容为:spring: datasource: password: your_real_password pom.xml加入spring-cloud-starter-alibaba-nacos-config依赖。
若无 Nacos 环境,退而求其次:把
password改为${MYSQL_PASSWORD:root},启动时加 JVM 参数-DMYSQL_PASSWORD=your_pass,至少比明文强。
6.2 硬伤二:LayUI 版本老旧(v2.5.6)存在 XSS 漏洞 —— 答辩 PPT 里要写“已知风险”
layui.css和layui.js是 2019 年版,layer.msg()未对 HTML 转义,若客户名称存<script>alert(1)</script>,弹窗会执行脚本。修复方案:
- 在
CustomerController.java的add()方法中,对name字段做 HTML 转义:import org.jsoup.Jsoup; customer.setName(Jsoup.clean(customer.getName(), Whitelist.none())); - 或前端
list.html中用{{# layui.util.escape(d[i].name) }}替代{{d[i].name}}。
6.3 硬伤三:Shiro 密码未加盐 ——e10adc3949ba59abbe56e057f20f883e就是 "123456" 的 MD5
sys_user表中密码是裸 MD5,极易被彩虹表破解。答辩前必须升级为Shiro 原生加密:
- 修改
ShiroConfig.java中credentialsMatcher():HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); // 改用 SHA256 matcher.setHashIterations(1024); // 迭代 1024 次 - 用工具生成新密码(如在线 SHA256 加密网站),替换
sys_user表中password字段值。
6.4 我每次部署必做的 5 分钟 checklist(亲测避翻车)
| 检查项 | 操作 | 不做的后果 |
|---|---|---|
| 数据库字符集 | SHOW CREATE DATABASE warehouse;确认CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci | 中文插入报错Incorrect string value |
| 静态资源路径 | 访问http://localhost:8080/layui/css/layui.css确认 200 | LayUI 样式丢失,页面变白板 |
| Shiro 登录接口 | Postman 调POST /login,Bodyusername=admin&password=123456,看是否返回redirect:/index | 登录功能瘫痪 |
| 菜单接口数据 | 浏览器访问http://localhost:8080/menu/list,看返回 JSON 中data是否有数组 | 左侧菜单空白 |
| 分页参数传递 | 在客户列表页 F12 → Network → 点页码,看 Request URL 是否含page=2&limit=10 | 分页失效,一页显示全部数据 |
从那以后我每次导出 WAR 包前,都强制走一遍这个 checklist,哪怕只花 3 分钟——因为去年有个学生答辩时,老师现场点“下一页”,结果刷出全部 2000 条数据,全场安静了 15 秒。希望帮到你。
本文还有配套的精品资源,点击获取