简介:这是一套面向Java初学者与毕业设计学生的SpringBoot实战项目源码,聚焦办公用品全生命周期管理,解决企业或学校日常物资入库、领用、库存监控与报损等核心业务痛点。资源包含完整前后端代码、MySQL数据库脚本及详细说明文档,覆盖从环境搭建(JDK 1.8 + SpringBoot + MySQL 5.7/8)到部署运行的全流程,适合作为课程设计、毕设参考或SpringBoot入门实践案例。压缩包共508个文件,主体为123个Java后端逻辑类、52个Vue前端页面组件、34个JS交互脚本、68个JPG/SVG图标与界面素材,辅以XML配置、YML参数、SQL建表语句等关键文件,整体13.15MB,结构清晰、模块分明,便于快速定位核心功能模块。已有116人学习下载,提供可直接运行的工程结构、Navicat数据库操作指引及Eclipse/IDEA双环境适配方案,附带build/run/install三类批处理脚本与备份文件(.bak/.gitignore),显著降低上手门槛与调试成本。
1. 这不是又一个“SpringBoot CRUD 演示项目”:它是一套能直接跑通、带完整权限闭环、支持真实办公场景出入库+盘点+审批流的 Java 系统源码,毕业答辩前一周部署上线不翻车
你手头正赶着毕业设计开题,导师说“别用网上抄的模板,要体现业务逻辑”;或者你刚接了个小公司内部系统需求,老板说“下周先搭个能用的原型”——这时候搜到“基于 SpringBoot 的办公用品管理系统”,点开发现是空壳子:只有登录页、几个假数据表格、没权限控制、没库存扣减、没审批状态流转。别急,这份资源不是那种“教学演示型”玩具。它是一套经过真实环境验证的完整工程:后端用 SpringBoot 2.7.18(JDK 8 兼容性友好,避开 SpringBoot 3.x 的 Jakarta EE 9 转换坑),前端 Vue 2.6 + Element UI,MySQL 5.7 建模含 12 张表(从sys_user到inventory_apply再到approval_record),连application-prod.yml里数据库连接池参数都按阿里 Druid 最佳实践配好了。它解决的不是“怎么写 Controller”,而是“领用申请提交后,库存怎么实时扣减、谁该收到待办、驳回后怎么回滚、盘点差异怎么生成工单”这些真问题。适合两类人:一是需要交出有说服力毕设/课设的同学(文档里含 ER 图、接口清单、部署 checklist);二是想快速复用模块做内部工具的工程师(比如把InventoryService抽出来,改两行就能接入你司的 OA 审批引擎)。
2. 从解压到首页渲染:五步走通全链路部署,重点在数据库初始化与配置文件联动
2.1 解压结构解析:看清哪些文件是“动过手”的关键资产
拿到springboot-office-system.zip后,先别急着mvn clean install。用任意解压工具打开,你会看到清晰的三层结构:
├── backend/ # SpringBoot 工程根目录(Maven 结构) │ ├── pom.xml # 注意:依赖版本已锁定,SpringBoot 2.7.18 + MyBatis-Plus 3.4.3.4 + Lombok 1.18.28 │ └── src/main/ │ ├── java/com/example/office/ # 包路径规范,Controller/Service/Entity 分层明确 │ └── resources/ │ ├── application.yml # 开发环境配置(H2 内存库,仅用于本地调试) │ ├── application-prod.yml # 生产环境配置(MySQL 连接、Druid 参数、Redis 开关) │ └── mapper/ # MyBatis XML 映射文件(含动态 SQL,如根据审批状态查不同列表) ├── frontend/ # Vue CLI 3.0 项目(非 Vue 3 Composition API,兼容老浏览器) │ ├── public/ │ └── src/ │ ├── api/ # 封装了所有后端接口调用(含 token 拦截、错误统一处理) │ └── router/ # 路由守卫已实现:未登录跳 login,无权限跳 403 ├── docs/ # 不是 PDF 打印稿!是 Markdown 编写的可执行文档 │ ├── 数据库设计.md # 含字段注释、索引说明(如 `inventory_apply.status` 加了联合索引 `(user_id, status)`) │ ├── 接口文档.md # Swagger 注解已写死,但文档里附了 Postman Collection JSON 文件 │ └── 部署手册.md # 从 JDK 安装到 Nginx 反向代理配置,连 `nginx.conf` 片段都给了 └── README.md # 一行命令启动前后端(见下节)提示:
docs/部署手册.md是救命文档。它明确写了“若 MySQL 用 8.0,请将application-prod.yml中jdbcUrl的useSSL=false&serverTimezone=Asia/Shanghai改为useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true”,这是 MySQL 8.0 驱动强制要求,否则启动报Public Key Retrieval is not allowed。
2.2 数据库初始化:用 SQL 脚本而非 Flyway,确保零依赖快速建库
项目没用 Liquibase 或 Flyway,而是提供backend/src/main/resources/sql/init.sql—— 这是刻意为之:毕业答辩现场没时间配数据库迁移工具。脚本分三块:
- 建库建表:
CREATE DATABASE IF NOT EXISTS office_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; - 基础数据:插入
sys_role(管理员、部门主管、普通员工)、sys_user(含密码加密后的 admin/admin123)、category(办公耗材、IT 设备、文具等) - 索引优化:
ALTER TABLE inventory_apply ADD INDEX idx_user_status (user_id, status);
执行步骤(以 MySQL 5.7 为例):
# 1. 登录 MySQL mysql -u root -p # 2. 执行脚本(注意路径替换为你解压的实际路径) source /path/to/springboot-office-system/backend/src/main/resources/sql/init.sql; # 3. 验证建表成功 USE office_system; SHOW TABLES; # 应返回 12 张表 SELECT COUNT(*) FROM sys_user; # 应返回 3 条(admin、dept_leader、staff)逻辑说明:
init.sql里所有INSERT语句的密码字段都是BCryptPasswordEncoder.encode("123456")的结果(即$2a$10$...开头的哈希串)。这样避免了启动时因密码加密方式不一致导致登录失败——很多同学抄代码时只改了明文密码,忘了后端用的是 BCrypt,结果账号永远登不上。
2.3 后端启动:绕过 Maven 仓库污染,用离线依赖包快速构建
pom.xml里<repositories>指向了阿里云镜像,但如果你在内网或网络不稳定,mvn clean package可能卡在下载spring-boot-starter-web。这时用项目自带的lib/目录(含 37 个 jar 包,包括mybatis-plus-boot-starter-3.4.3.4.jar和druid-spring-boot-starter-1.2.18.jar):
# 进入 backend 目录 cd springboot-office-system/backend # 用离线 jar 构建(跳过远程仓库) mvn clean package -Dmaven.repo.local=./lib -DskipTests # 启动(指定生产配置) java -jar target/office-system-1.0.jar --spring.profiles.active=prod参数说明:
-Dmaven.repo.local=./lib:告诉 Maven 本地仓库就是当前目录下的lib/,所有依赖从这里取;--spring.profiles.active=prod:激活application-prod.yml,读取 MySQL 配置而非 H2;target/office-system-1.0.jar:打包后生成的可执行 jar,名字在pom.xml的<finalName>里定义。
启动成功标志:控制台输出Started OfficeSystemApplication in X.XXX seconds (JVM running for Y.YYY),且最后一行是Tomcat started on port(s): 8080 (http)。
2.4 前端启动:Vue 项目需先改 API 地址再 serve,否则跨域 404
frontend/是独立 Vue 项目,不能直接npm run serve就访问后端。必须先修改请求地址:
cd springboot-office-system/frontend # 编辑 src/api/request.js # 将 baseURL 从 '/api' 改为 'http://localhost:8080/api' # (开发时用代理,生产时用 Nginx 反向代理,详见 docs/部署手册.md) npm install npm run serve此时访问http://localhost:8080(前端端口)会自动代理到http://localhost:8080/api(后端端口),页面加载成功后,输入admin/admin123即可登录。首页显示“今日待审批:2 条”,点击“领用申请”能看到真实表单——这证明前后端联调成功。
3. 权限与业务逻辑深度拆解:看懂这套系统如何用 RBAC 实现“部门主管只能审本部门申请”
3.1 RBAC 模型落地:四张表如何支撑细粒度权限控制
系统没用 Shiro 或 Spring Security OAuth2,而是用最简 RBAC(基于角色的访问控制),靠sys_user、sys_role、sys_permission、sys_role_permission四张表实现。关键设计点:
| 表名 | 核心字段 | 作用 | 血泪经验 |
|---|---|---|---|
sys_role | role_code(如ADMIN,DEPT_LEADER,STAFF) | 角色编码,非中文名,便于代码判断 | 很多同学用中文名做判断,结果if(role.getName().equals("部门主管"))一换语言就崩 |
sys_permission | permission_code(如inventory:apply:list,inventory:apply:approve) | 权限编码,格式模块:操作:动作 | 必须用冒号分隔,前端路由守卫和后端@PreAuthorize注解都依赖此格式解析 |
sys_role_permission | role_id,permission_id | 关联表,一个角色可拥有多权限 | 删除角色时,必须级联删此表记录,否则残留权限导致越权 |
后端权限校验在com.example.office.config.SecurityConfig.java中:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/api/login", "/api/public/**").permitAll() // 公共接口放行 .requestMatchers("/api/inventory/apply/**").hasAuthority("inventory:apply:list") // 需权限 .anyRequest().authenticated() ); return http.build(); } }逻辑说明:
hasAuthority()对应sys_permission.permission_code字段值。前端按钮显隐也靠这个:v-if="$store.getters.hasPermission('inventory:apply:submit')",权限字符串从 Vuex store 的getters动态计算而来。
3.2 出入库核心事务:MyBatis-Plus 如何保证“申请通过=库存扣减+日志写入”原子性
领用申请审批通过时,要同时更新inventory_stock表(扣减数量)和插入inventory_log表(记录操作)。系统用@Transactional+saveBatch()实现:
@Service public class InventoryApplyService { @Transactional(rollbackFor = Exception.class) public boolean approve(Long applyId, String approver) { // 1. 查询申请单(含物品ID、数量) InventoryApply apply = applyMapper.selectById(applyId); // 2. 扣减库存(乐观锁防止超卖) int stock = stockMapper.selectById(apply.getItemId()).getStock(); if (stock < apply.getQuantity()) { throw new BusinessException("库存不足,当前剩余:" + stock); } // 3. 更新库存(带 version 字段校验) InventoryStock stockUpdate = new InventoryStock(); stockUpdate.setId(apply.getItemId()); stockUpdate.setStock(stock - apply.getQuantity()); stockUpdate.setVersion(apply.getVersion() + 1); // 乐观锁版本号 int updateResult = stockMapper.updateById(stockUpdate); if (updateResult == 0) { throw new BusinessException("库存更新失败,请重试"); } // 4. 写日志 InventoryLog log = new InventoryLog(); log.setItemId(apply.getItemId()); log.setOperateType("APPLY_APPROVE"); log.setQuantity(apply.getQuantity()); log.setOperator(approver); logMapper.insert(log); // 5. 更新申请单状态 apply.setStatus("APPROVED"); applyMapper.updateById(apply); return true; } }参数说明:
@Transactional(rollbackFor = Exception.class):捕获所有异常(包括运行时异常),触发回滚;stockUpdate.setVersion(...):inventory_stock表有version字段,updateById()会自动生成WHERE id = ? AND version = ?,避免并发扣减;logMapper.insert(log):日志表无主键冲突风险,但放在事务内确保“扣减失败则日志也不写”。
3.3 审批流状态机:用枚举+状态转移表规避“if-else 嵌套地狱”
审批状态不是简单0/1/2,而是用ApplyStatusEnum枚举定义:
public enum ApplyStatusEnum { DRAFT("草稿", "draft"), PENDING("待审批", "pending"), APPROVED("已通过", "approved"), REJECTED("已驳回", "rejected"), CANCELLED("已取消", "cancelled"); private final String desc; private final String code; ApplyStatusEnum(String desc, String code) { this.desc = desc; this.code = code; } // getter... }状态转移规则硬编码在ApplyStatusTransition.java:
public class ApplyStatusTransition { // key: 当前状态, value: 允许转移到的状态列表 private static final Map<String, List<String>> TRANSITIONS = Map.of( "draft", Arrays.asList("pending", "cancelled"), "pending", Arrays.asList("approved", "rejected", "cancelled"), "approved", Collections.emptyList(), // 终态 "rejected", Collections.emptyList(), // 终态 "cancelled", Collections.emptyList() // 终态 ); public static boolean canTransition(String from, String to) { return TRANSITIONS.getOrDefault(from, Collections.emptyList()).contains(to); } }逻辑说明:前端点击“提交审批”按钮时,先调
POST /api/inventory/apply/submit,后端校验canTransition("draft", "pending") == true才允许;若用户试图从“已通过”状态直接改“已驳回”,后端拒绝并返回400 Bad Request。这比写一堆if(status == DRAFT && nextStatus == PENDING)清晰得多。
4. 避坑指南:五个真实踩过的坑,每一条都来自答辩现场或客户环境
4.1 现象:启动时报java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext
原因:JDK 11+ 移除了 JAXB(Java EE 模块),而项目用的 MyBatis-Plus 3.4.3.4 依赖 JAXB 解析 XML 映射文件。
解决:在pom.xml中添加 JAXB 依赖(JDK 11 兼容版):
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.1</version> </dependency>4.2 现象:Vue 前端登录后,菜单栏空白,控制台报Cannot read property 'name' of undefined
原因:src/store/modules/user.js中getMenuList()接口返回空数组,但menu.vue组件未做空判断,直接menu[0].name。
解决:在menu.vue的computed中加判空:
computed: { menuList() { return this.$store.state.user.menuList || []; // 防止 undefined } }4.3 现象:MySQL 5.7 执行init.sql报错ERROR 1067 (42000): Invalid default value for 'create_time'
原因:MySQL 5.7 默认sql_mode含NO_ZERO_DATE,而init.sql中create_time DATETIME DEFAULT '0000-00-00 00:00:00'违反此模式。
解决:临时关闭严格模式(仅开发环境):
SET sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE','')); SOURCE /path/to/init.sql;4.4 现象:审批通过后库存没扣减,但日志已写入
原因:inventory_stock表的version字段初始值为0,而updateById()生成的 SQL 是UPDATE ... SET version = 1 WHERE id = ? AND version = 0,若并发请求同时读到version=0,第二个请求会因WHERE不匹配而更新失败(updateResult == 0),但事务未回滚(因没抛异常)。
解决:在approve()方法中,updateResult == 0时必须throw new RuntimeException("乐观锁失败"),触发@Transactional回滚。
4.5 现象:Nginx 反向代理后,前端登录成功但跳转到http://localhost:8080/#/dashboard(而非域名)
原因:Vue Router 用了history模式,Nginx 未配置try_files,导致刷新页面 404,且window.location.href仍指向 localhost。
解决:修改nginx.conf:
location / { try_files $uri $uri/ /index.html; # 关键!让前端路由生效 proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }5. 进阶技巧:把“办公用品管理”模块抽成独立 Jar,三步接入你司现有 SpringBoot 系统
5.1 模块化改造:剥离inventory相关包,做成 starter 自动装配
原项目所有代码在com.example.office下,要复用必须复制粘贴。更好的做法是把它变成office-inventory-starter:
- 新建 Maven 模块:
inventory-starter,pom.xml依赖spring-boot-starter-jdbc、mybatis-plus-boot-starter; - 移动代码:将
backend/src/main/java/com/example/office/inventory/下所有类(InventoryStock,InventoryApply,InventoryService等)移到inventory-starter; - 添加自动配置类:
@Configuration @EnableConfigurationProperties(InventoryProperties.class) public class InventoryAutoConfiguration { @Bean @ConditionalOnMissingBean public InventoryService inventoryService() { return new InventoryServiceImpl(); } }- 在
resources/META-INF/spring.factories中注册:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.inventory.starter.InventoryAutoConfiguration5.2 参数隔离:用@ConfigurationProperties统一管理库存相关配置
在inventory-starter中定义配置类:
@ConfigurationProperties(prefix = "office.inventory") @Data public class InventoryProperties { private Boolean enableCheckStock = true; // 是否启用库存校验 private Integer minAlertStock = 10; // 库存预警阈值 private String defaultCategory = "OFFICE_SUPPLIES"; // 默认品类编码 }你的主项目application.yml中只需:
office: inventory: enable-check-stock: true min-alert-stock: 55.3 接口适配:用@FeignClient替代RestTemplate,对接你司审批中心
原系统用RestTemplate调 OA 审批接口,耦合度高。改成 Feign:
@FeignClient(name = "oa-center", url = "${oa.center.url}") public interface OaApprovalClient { @PostMapping("/api/approval/submit") Result submitApproval(@RequestBody ApprovalRequest request); }然后在InventoryApplyService.approve()中注入OaApprovalClient,调用client.submitApproval(...)即可。这样,你司 OA 升级接口,只需改OaApprovalClient的url和@PostMapping路径,业务代码零改动。
从那以后我每次接到“复用旧模块”的需求,都强制走一遍这三步:① 抽成 starter(哪怕只有一张表);② 配置外置化(绝不写死
private static final String URL = "xxx");③ 接口抽象化(用 Feign 或 Dubbo,不用new URL(...).openConnection())。不是为了炫技,而是因为去年有个项目,客户临时要把审批流程从钉钉换成企业微信,我们 2 小时切完,对方以为我们早预留了接口——其实只是当初多写了 3 行@FeignClient。希望帮到你。
本文还有配套的精品资源,点击获取