简介:一份面向Java毕业设计场景的办公用品管理系统参考论文,适合计算机相关专业学生及初级开发者借鉴。论文基于SpringBoot+Vue框架与MySQL数据库设计,系统涵盖采购、库存、分发、统计等核心功能,并从前端页面到后台接口、数据表设计均有说明。正文从绪论、相关技术及环境说明、需求分析等方面展开,包含研究背景、国内外现状、可行性分析、总体设计、编码实现与软件测试等内容,结构规范、层次清楚,可作为撰写毕业设计论文或课程报告的模板参考。压缩包内为1个docx文件,大小1.96MB,便于阅读和编辑。该资料已有91人学习,适合用于补充系统设计思路与文档撰写细节,帮助提升毕业设计效率。
1. 为什么办公用品管理系统会同时选中 SpringBoot 与 Vue
办公用品管理系统是 Java 毕设里最常见的题目,但很多初学项目做到一半就卡在“库存对不上”和“审批状态乱”这两个地方。原因在于这个系统本质上是一个带并发写入的流程系统,不是简单的增删改查。Spring Boot 负责把库存扣减、申请审批、事务回滚这类后端逻辑收拢成独立服务,Vue 则专门处理申请页面的多步交互、审批按钮和统计图表展示。前后端分离后,业务边界清楚,也可以独立部署。接下来的内容会沿着数据模型、Spring Boot 接口、Vue 页面、测试部署这条主线,把新手容易翻车的地方逐一拆开。
2. 办公用品管理系统的数据模型设计:先定领用流程,再拆表结构
2.1 从办公用品领用流程反推业务对象
办公用品管理系统的最小闭环是:员工登录→查看办公用品库存→发起领用申请→管理员审批→出库或驳回。再往下扩展,还会有采购入库、供应商管理、部门领用统计、低库存预警。做第一版时,我一般不建议一上来就把权限系统做得特别重,而是先把四个核心对象定清楚:用户(User)、办公用品(Item)、领用申请(Apply)、审批记录(Approval)。普通员工和管理员用role字段区分即可,等系统跑通后再引入 Spring Security 的完整 RBAC。
这样定义后,状态流转就会非常清晰。领用申请的状态设计为:PENDING(待审批)、APPROVED(已通过)、REJECTED(已驳回)、OUTBOUND(已出库)。这里特别要区分 APPROVED 和 OUTBOUND:审批通过只代表管理员同意,实物还没被领走,所以不能扣库存;只有出库操作才真正减库存。很多毕设库存对不上,正是因为把“审批通过”和“库存扣减”绑在了同一个动作里,用户申请通过后一直不来领,库存就提前少了。
2.2 核心表结构与字段设计
下面是一个最小可用的建表模型,MySQL 8.0 直接执行即可。我把办公用品表t_item和库存表合并为一对一关系,这样查询用品列表时不需要联表就能拿到库存数量;代价是如果需要记录每一次入库流水,需要再建一张流水表。对于第一版论文系统,这种设计足够清晰。
CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '0-管理员 1-普通员工', department VARCHAR(50) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, spec VARCHAR(128) DEFAULT NULL COMMENT '规格型号', unit VARCHAR(16) NOT NULL COMMENT '单位:盒/支/个', stock INT NOT NULL DEFAULT 0, low_stock INT NOT NULL DEFAULT 5 COMMENT '低库存预警阈值', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE t_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, item_id BIGINT NOT NULL, apply_count INT NOT NULL COMMENT '申请数量', status VARCHAR(20) NOT NULL DEFAULT 'PENDING', reason VARCHAR(255) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_apply_user (user_id), KEY idx_apply_status (status) ); CREATE TABLE t_approval ( id BIGINT PRIMARY KEY AUTO_INCREMENT, apply_id BIGINT NOT NULL UNIQUE, approver_id BIGINT NOT NULL, action VARCHAR(20) NOT NULL COMMENT 'APPROVE/REJECT', comment VARCHAR(255) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );表结构里最需要关注的是t_apply.status和t_item.stock这两个字段,它们的组合关系决定了库存是否正确。
| 状态/字段 | 示例值 | 说明 |
|---|---|---|
t_apply.status | PENDING | 初始状态,前端显示“待审批” |
t_apply.status | APPROVED | 管理员同意,未出库,不扣库存 |
t_apply.status | OUTBOUND | 已出库,此时库存才扣减 |
t_apply.status | REJECTED | 已驳回,不产生库存变化 |
t_item.stock | 12 | 当前剩余数量 |
t_item.low_stock | 5 | 小于等于该值触发预警 |
状态流转可以这样约束:PENDING 只能转为 APPROVED 或 REJECTED;APPROVED 只能转为 OUTBOUND;REJECTED 和 OUTBOUND 是终态。这个约束不需要写在数据库里,而是在 Service 层用if判断,避免用数据库触发器增加维护成本。另外还有一个容易忽略的校验:申请数量不能超过库存。如果这个校验只做在前端,绕过页面直接调接口就失效了,所以后端 Service 必须重复校验。但只有这一层校验还不够,两个并发请求同时读取到库存为 5,同时申请 5 个都会通过校验,最后库存变成负数。并发问题放到第 3 章详细讲。
2.3 用 MyBatis-Plus 还是 Spring Data JPA
这个选型直接影响后面代码量。Spring Data JPA 上手快,实体类加注解后自动建表,适合快速原型,但复杂统计要写 JPQL,比如“每个部门领用排行”这种需求就不好写。MyBatis-Plus 更贴近 SQL 习惯,LambdaQueryWrapper直接拼条件,代码生成器也能生成单表 CRUD,目前是大多数 Java 毕设和中小型管理系统的默认选型。如果这个系统后面要加多表关联的领用统计,我会选择 MyBatis-Plus。
一个反直觉的结论:无论选哪个,都不建议在 Controller 里直接操作多个 Mapper 或 Repository 做跨表事务。正确做法是在 Service 层加@Transactional,把“更新库存”和“生成出库记录”放在同一个事务方法内。这样可以保证库存扣减成功的同时记录一定存在,不会出现扣了库存但记录丢了的情况。
3. 用 Spring Boot 实现库存扣减与领用审批的后端服务
3.1 项目初始化与依赖选型:避开 SpringBoot 版本太高的问题
初始化 Spring Boot 项目时,直接用 IDEA 的 Spring Initializr 生成工程,依赖选择spring-boot-starter-web、MySQL、MyBatis-Plus 等。很多新手喜欢选最新版本,结果遇到 MyBatis-Plus 或某些第三方 starter 尚未适配的情况,报错找不到数据源配置。实际项目里,我会选择最新稳定版的“上一个次版本”,比如当前稳定版是 3.3.x,我可能选 3.2.x 来保证兼容。这个问题在springboot 面试题里也常被问到:版本太高时,第一件事不是改代码,而是检查 starter 的兼容矩阵。
pom.xml里关键的依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>注意 Spring Boot 3.x 必须引入mybatis-plus-spring-boot3-starter,旧版的mybatis-plus-boot-starter只适配 Spring Boot 2.x,这是最常见的兼容错误。spring-boot-starter-parent已统一管理依赖版本,所以 MySQL 驱动不需要写version。后端目录我按controller/service/mapper/entity四层拆分,Controller 只做参数接收和返回,Service 写业务事务,Mapper 继承 MyBatis-Plus 的 BaseMapper。这个分层结构画成图,可以直接放进论文的“系统设计”章节。
3.2 领用申请审批与出库的事务实现
核心功能分为两个操作:申请创建和出库扣减。申请创建相对简单,只插入一条PENDING记录,最多做一个数量上限校验。真正容易错的是出库,因为它要同时修改t_apply状态和t_item.stock。下面用一个@Transactional方法完成。
@Service public class ApplyService { @Resource private ApplyMapper applyMapper; @Resource private ItemMapper itemMapper; @Resource private ApprovalMapper approvalMapper; @Transactional(rollbackFor = Exception.class) public void outbound(Long applyId, Long approverId) { Apply apply = applyMapper.selectById(applyId); if (apply == null || !"APPROVED".equals(apply.getStatus())) { throw new BusinessException("当前状态不能出库"); } // 锁定库存行,防止并发超发 Item item = itemMapper.selectByIdForUpdate(apply.getItemId()); if (item.getStock() < apply.getApplyCount()) { throw new BusinessException("库存不足"); } item.setStock(item.getStock() - apply.getApplyCount()); itemMapper.updateById(item); apply.setStatus("OUTBOUND"); applyMapper.updateById(apply); Approval approval = new Approval(); approval.setApplyId(applyId); approval.setApproverId(approverId); approval.setAction("OUTBOUND"); approvalMapper.insert(approval); } }核心是selectByIdForUpdate,它对应 SQL 中的SELECT ... FOR UPDATE,在事务内锁住该物品行,第二个并发请求会等待,直到第一个事务提交。@Transactional保证item扣减、apply状态、approval记录要么全部成功,要么全部回滚。selectByIdForUpdate需要在ItemMapper中自己定义:
@Select("SELECT * FROM t_item WHERE id = #{id} FOR UPDATE") Item selectByIdForUpdate(Long id);如果使用 JPA,可以用@Lock(LockModeType.PESSIMISTIC_WRITE)达到同样效果。还有一个常见错误:把库存判断放在 Controller 或 Service 方法外面,先selectById判断,再调用outbound,这样判断和扣减就不是原子操作,并发时照样超发。所以库存判断和扣减必须在同一个事务方法内。
3.3 压测验证与锁策略选择
给这个接口做并发验证时,我用 JMeter 创建 50 个线程,同时对一个库存为 10 的物品提交申请,每个申请数量 1。最终t_apply中OUTBOUND的记录数应该是 10,库存为 0。如果出现第 11 条成功记录,说明锁没有生效。不同锁策略的适用场景可以简单对比如下:
| 锁方式 | 实现 | 适合场景 |
|---|---|---|
| 乐观锁 version 字段 | UPDATE t_item SET stock = stock - 1 WHERE id = ? AND stock >= 1 | 冲突较少 |
SELECT ... FOR UPDATE | 悲观行锁 | 高并发扣库存 |
| Redis 分布式锁 | SET key value NX EX 10 | 多实例部署 |
第一版毕设系统用行锁最简单,也最容易解释清楚。如果以后要拆成多个后端实例,再把 Redis 分布式锁加上,锁的 key 可以设计为item:lock:{itemId}。
3.4 用 Redis 缓存库存列表,降低数据库压力
办公用品列表是首页高频查询,但库存实时性要求并不高,可以加一层 Redis 缓存。加入spring-boot-starter-data-redis后,通过StringRedisTemplate手动实现缓存,这样能更直观地控制缓存键和过期时间。
@Service public class ItemService { @Resource private StringRedisTemplate stringRedisTemplate; @Resource private ItemMapper itemMapper; public List<Item> listItems() { String key = "item:list"; String cached = stringRedisTemplate.opsForValue().get(key); if (cached != null) { return JSON.parseArray(cached, Item.class); } List<Item> items = itemMapper.selectList(null); stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(items), 60, TimeUnit.SECONDS); return items; } }缓存过期时间不要设置为 1 秒或 1 小时,前者起不到缓存效果,后者会导致缺货了用户还在申请。我一般设置 30~60 秒,并在出库时主动删除item:list缓存键,这样下次查询能读到最新库存。
3.5 多环境配置与参数管理
Spring Boot 支持用application-{profile}.yml区分开发、测试、生产环境。在application.yml中指定激活项:
spring: profiles: active: @profiles.active@@profiles.active@是 Maven 构建时替换的占位符,需要在pom.xml中定义 profile。开发环境application-dev.yml会打印 SQL 日志,生产环境则关闭。这个配置过程也是springboot 配置里的高频考点。对于低库存预警阈值一类的自定义业务参数,可以放在custom节点下:
custom: low-stock-threshold: 5再用@ConfigurationProperties(prefix = "custom")读取,这样论文里可以写“系统业务参数支持外部化配置”,比硬编码在代码里更有说服力。
4. 用 Vue 搭建前端页面:路由、状态管理与打包布局处理
4.1 使用 Vite 创建 Vue 3 项目并配置开发环境
前端我使用 Vue 3 + Vite + Element Plus。创建项目的命令如下:
npm create vite@latest office-supply-web -- --template vue cd office-supply-web npm install npm install vue-router@4 pinia axios element-plusNode 版本建议 18+,否则 Vite 会提示Unsupported engine。这是vue安装及环境配置最常见的坑,先看 Node 版本再动手。如果npm install慢,可以配置国内镜像,速度会明显提升。项目创建后,我会先清掉src/components/HelloWorld.vue,然后按views、router、store、utils四个目录组织代码。
4.2 开发代理与 Axios 统一封装
Vite 开发服务器默认跑在 5173,后端接口在 8080,所以需要配置代理。在vite.config.js中设置:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })后端所有接口都以/api开头,这样前端请求/api/item/list会被转发到http://localhost:8080/api/item/list。changeOrigin必须为true,否则后端收到的 Host 头还是前端域名,个别请求会异常。Axios 封装时,我会新建src/utils/request.js:
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { // 放在单独的 auth.js 中,避免和 router 文件循环依赖 window.location.href = '/login' } else { ElMessage.error(error.response?.data?.message || '请求失败') } return Promise.reject(error) } ) export default request这里有一个容易踩的坑:直接在拦截器里import router后调router.push,可能因为循环依赖导致router是undefined。最简单的处理方式是window.location.href强制跳转,或者把跳转逻辑放到一个单独模块中。
4.3 用 Pinia 管理领用申请流程
领用申请在页面上是多步操作:选择物品、填写数量、提交。把当前申请单放在 Pinia store 中,多个组件共享状态时不用层层传递 props。新建src/store/applyStore.js:
import { defineStore } from 'pinia' export const useApplyStore = defineStore('apply', { state: () => ({ item: null, count: 1, submitting: false }), actions: { async submit() { this.submitting = true try { const res = await request.post('/apply', { itemId: this.item.id, applyCount: this.count }) return res } finally { this.submitting = false } } } })Pinia 相比 Vuex 少了 mutations,可以直接在 action 里改 state,中小型管理系统开发效率高很多。注意在setup外使用useApplyStore()时,必须先执行app.use(pinia)。否则会报getActivePinia was called with no active Pinia,这个问题在 Vue 相关面试里也经常被拿出来问。
4.4 路由模式与打包后布局异常的处理
开发模式下一切正常,但npm run build部署到 Nginx 后,刷新页面会 404,或者资源加载不出来。这要看路由模式:hash 模式 URL 带#,刷新不会请求后端;history 模式 URL 没有#,刷新时 Nginx 会去磁盘找真实文件,找不到就 404。
| 路由模式 | URL 示例 | 部署要求 |
|---|---|---|
| hash | /#/apply/list | 无特殊配置 |
| history | /apply/list | Nginx try_files 回退到 index.html |
使用 history 模式时,Nginx 配置这样写:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }关于vue 打包后布局异常,大部分是静态资源路径使用了绝对路径/assets/xxx.js,部署到子路径后找不到资源。解决办法是在vite.config.js里设置base: './',让构建产出使用相对路径。这个参数很多人忽略,评审演示时却会直接暴露。
5. 用测试数据和部署记录让办公用品管理系统达到验收标准
5.1 后端接口冒烟测试:用 JUnit 验证事务回滚
演示或答辩时,最怕被问“怎么证明系统可靠”。与其口头描述,不如写一个接口测试。我习惯用 Spring Boot 的 MockMvc 测核心流程:先准备一条库存为 1 的物品,提交申请数量 2,断言接口返回“库存不足”,并验证t_apply中没有新增记录。
@Test @Transactional void stockNotEnoughShouldReject() throws Exception { mockMvc.perform(post("/api/apply") .param("itemId", "1") .param("applyCount", "2")) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(false)); }测试类加@Transactional后,测试结束数据自动回滚,不会污染开发库。这个测试同时覆盖了业务校验和事务机制,是性价比最高的验证手段。
5.2 用 Docker Compose 做本地演示部署
为了确保换一台电脑也能跑起来,我会用 Docker Compose 一键拉起 MySQL、Redis、后端和前端。容器服务名直接作为主机名访问,后端连接 MySQL 不能写localhost,要写服务名mysql,否则会报Communications link failure。部署后验证用两条 SQL 就够:
SELECT stock FROM t_item WHERE id = 1; SELECT COUNT(*) FROM t_apply WHERE status = 'OUTBOUND';5.3 并发验证的一个具体技巧
在 JMeter 里模拟 50 个用户同时申请库存为 10 的物品时,除了看响应时间,更要在请求结束后查数据库:确认库存数 + OUTBOUND 记录数 = 初始库存。如果出现库存为负,就要重点检查selectByIdForUpdate是否真的进入了事务方法,以及 Service 调用链上有没有被非代理方法拦截。这个小测试能直接支撑论文里“系统支持并发库存扣减”的结论,而不用凭空说“系统并发能力好”。
本文还有配套的精品资源,点击获取