企业办公用品管理系统全流程设计实战:3张表、5种状态、申请→审批→发放→归还全生命周期闭环
📦 源码1:ruoyi-office-vben |📦 源码2:ruoyi-office |📦 源码3:ruoyi-office | 💬 :17156169080
办公用品管理是每家企业的"后勤刚需"——纸笔、墨盒、订书机、投影仪,东西虽小,管不好就是一笔糊涂账。很多企业的用品管理还停留在"Excel台账 + 微信群喊话"的阶段:谁领了什么没记录,库存见底了没人知道,投影仪借走三个月没人还。RuoYi Office 用 3 张表、5 种状态机、1 套 BPM 流程,构建了从台账建档→申请审批→发放扣库存→借用品归还的全生命周期闭环。
引言:办公用品管理到底难在哪?
"不就是买来、发掉、再买吗?"——初次接到需求的开发者大多这么想。但真正动手时会发现,办公用品管理的复杂度远超"进销存"三个字:
品类繁杂,管理方式不同:A4 纸和中性笔是消耗品,发出去就不用管;投影仪和剪刀是借用品,用完要还。两种品类的状态流转和库存逻辑完全不同。
申请与发放分离:员工提交申请不等于立刻拿到东西。申请要走审批流程,审批通过后行政才能发放。申请数量和实际发放数量可能不一致(库存不够时部分发放)。
库存实时性要求高:入库增库存、发放减库存、归还加库存——三个方向的库存变动需要事务保证,还要有库存预警提醒。
归还追踪:借用品发出去多少、还回来多少、还差多少,需要精确到每一行明细。
一张申请单多行物品:员工一次申请可能要 3 种不同的用品,每种用品独立发放、独立归还,状态各不相同。
本文以 RuoYi Office 的办公用品管理模块为例,完整拆解其业务建模、数据结构、流程设计、前后端实现方案。
一、业务设计:从一次领用申请说起
1.1 业务全景
一次完整的办公用品业务涉及三个角色、四个阶段:
| 阶段 | 角色 | 操作 | 系统行为 |
|---|---|---|---|
| 建档 | 行政管理员 | 录入用品信息、入库 | 创建台账记录,库存+N |
| 申请 | 普通员工 | 选择用品、填写数量、提交审批 | 创建申请单+明细行,发起BPM流程 |
| 发放 | 行政管理员 | 审批通过后,逐行或批量发放 | 明细状态流转,库存-N |
| 归还 | 行政管理员 | 借用品到期后确认归还 | 明细状态流转,库存+N |
1.2 消耗品 vs 借用品:两条分叉路
这是本方案最核心的业务抽象——用品的"管理类型"决定了它的生命周期长度:

系统在台账层面定义了每种用品的管理类型(managementType),在发放时自动判断:
- 消耗品(
managementType = 1):发放后状态直接变为「已领用」,不再有归还流程 - 借用品(
managementType ≠ 1):发放后状态变为「已领用待归还」,等待后续归还确认
这种设计避免了"为每种用品写不同的业务逻辑",只需一个字段值就实现了两条截然不同的流转路径。
1.3 五种状态的生命周期
每一行申请明细都有独立的状态,由 SupplyItemStatusEnum 定义:
| 状态码 | 状态名 | 触发条件 | 可执行操作 |
|---|---|---|---|
-1 | 申请中 | 申请单提交BPM审批 | 等待审批 |
0 | 待领用 | BPM审批通过 | 可发放 |
1 | 已领用 | 消耗品发放完成 | 无(终态) |
2 | 已领用待归还 | 借用品发放完成 | 可归还 |
3 | 已归还 | 归还数量达到发放数量 | 无(终态) |
状态流转图:

二、系统设计:三个子模块的协作
2.1 模块组成
RuoYi Office 的办公用品管理位于 OA 办公管理 → 办公用品管理 目录下,由三个紧密协作的子模块组成:
| 子模块 | 菜单名称 | 功能定位 | 面向角色 |
|---|---|---|---|
| 用品台账 | 办公用品台账 | 用品主数据管理:品类、规格、库存、入库 | 行政管理员 |
| 领用申请 | 领用申请管理 | 员工发起领用申请、走BPM审批 | 全体员工 |
| 领用发放 | 领用发放管理 | 审批通过后的发放和归还操作 | 行政管理员 |
2.2 核心设计决策
| 决策点 | 方案 | 理由 |
|---|---|---|
| 数据模型 | 台账+申请单+明细行三张表 | 台账管"有什么",申请单管"谁要",明细行管"要什么和发了没" |
| 库存变动时机 | 仅在发放/归还时变动 | 申请和审批阶段不影响库存,避免"审批中锁库存"的问题 |
| 管理类型 | 台账定义+明细冗余 | 发放时按冗余字段判断,避免反查台账 |
| 审批集成 | FlowBillService标准接口 | 复用BPM框架回调,状态同步零耦合 |
| 我的单据 | 后端强制注入creator | 数据安全,员工只能看到自己的申请 |
三、PC 端功能实现
3.1 办公用品台账
台账页面是用品管理的"数据基石",采用左侧类别导航 + 右侧表格列表的经典布局。

▲ 办公用品台账:左侧按字典 OA_SUPPLY_CATEGORY 动态渲染类别菜单(文具类、打印耗材、生活用品、电脑办公等),点击类别自动筛选;右侧表格展示物品名称、编码、规格型号、库存数量等关键信息
台账设计要点:
- 类别导航联动:左侧
Menu组件绑定selectedCategory状态,切换类别时自动触发gridApi.query(),查询参数中注入category过滤条件 - 库存预警:
renderStockQuantity方法对比stockQuantity(当前库存)和minStock(最低库存),当库存低于阈值时标签变红色警示 - 管理类型标签:通过字典
OA_SUPPLY_MANAGEMENT_TYPE渲染为蓝色标签(消耗品)或橙色标签(借用品) - 入库操作:每行提供「入库」按钮,弹出入库弹窗填写入库数量,提交后库存实时增加
- 导出Excel:支持按当前筛选条件导出用品数据
3.2 领用申请管理
申请列表展示当前用户创建的所有领用申请单,支持新建、查看详情、删除操作。

▲ 领用申请列表:支持按单据编号、单据状态、申请部门、创建时间搜索筛选。列表默认只显示当前登录用户的申请单
列表设计要点:
- 我的单据过滤:后端
getSupplyApplyBillPage在查询时自动注入creator = 当前用户ID,员工只能看到自己的申请 - 单据编号链接:点击编号列自动跳转到详情页,编号格式为
OA108-{YYYYMMDD}{5位流水} - 状态标签:通过
CellDict渲染 BPM 流程状态(草稿/审批中/审批通过/拒绝/已取消),不同状态用不同颜色区分 - 删除约束:删除前通过
BpmProcessInstanceStatusDeleteValue过滤不可删除的状态(如审批中),并给出提示
3.3 领用申请详情
详情页是申请单的核心交互页面,集 基本信息表单、领用明细表格、附件管理 于一体。

▲ 申请详情页(已审批通过):顶部展示单据编号、申请人、日期、所属单位/部门等摘要信息;下方分为基本信息、领用明细、附件信息三个区域
「基本信息」区域:
| 字段 | 组件 | 说明 |
|---|---|---|
| 领用日期 | DatePicker | 默认当天,必填 |
| 使用类型 | Select | 个人使用 / 部门使用(字典 oa_supply_use_type) |
| 领取方式 | Select | 自取 / 配送(字典 oa_supply_pickup_method) |
| 申请事由 | TextArea | 必填,审批人以此判断合理性 |
| 备注 | TextArea | 选填 |
「领用明细」区域:
这是申请单的核心——通过 SupplySelectModal(用品选择弹窗)从台账中勾选需要的办公用品,添加到明细表格中:
- 每行展示物品名称、规格型号、计量单位、管理类型
领用数量列为InputNumber可编辑,默认值为 1- 同一用品不可重复添加(按
supplyId去重) - 只读模式下明细表格不显示操作列和添加按钮

▲ 新建申请单:领用明细为空,点击蓝色「添加办公用品」按钮弹出用品选择弹窗
用品选择弹窗 SupplySelectModal:
弹窗内部是一个独立的分页表格,支持按名称、类别、管理类型搜索过滤。勾选多条用品后点击确认,选中的用品自动添加到明细表格中。弹窗只展示状态为"正常"的用品(status: 0),停用的用品不可被选择。
3.4 领用发放管理
发放管理是行政管理员的工作台,以申请明细行为粒度管理发放和归还。

▲ 领用发放管理:顶部分段器(全部/申请中/待发放/已领用/待归还/已归还)切换筛选;表格展示申请单号、申请人、物品名称、管理类型、申请数量、实发数量、已归还数量等信息
发放管理设计要点:
- 分段器筛选:6 种状态对应
Segmented组件的选项,切换时自动以itemStatus条件查询;选择"全部"时不带状态条件 - 发放操作:仅「待领用」状态的行可操作。弹出发放弹窗,显示物品名称和申请数量,填写实发数量后提交。发放同时扣减台账库存
- 批量发放:勾选多行「待领用」记录,点击「批量发放」一次性全部发放,实发数量默认等于申请数量
- 归还操作:仅「已领用待归还」状态的行可操作。弹窗显示实发数量和已归还数量,填写本次归还数量(不得超过未归还数量),提交后恢复台账库存
- 勾选限制:
checkMethod禁止勾选「申请中」状态的记录,避免误操作
四、流程设计:BPM 审批驱动状态流转
4.1 流程编排
办公用品领用申请使用 Flowable 引擎编排,流程定义 Key 为 oa_supply_apply_bill。流程设计相对简洁:
- 员工填写申请信息 + 选择用品明细 → 提交流程
- 负责人审批
- 行政审批
- 审批通过,明细状态从「申请中」→「待领用」
- 行政管理员在领用发放管理中执行发放
与请假销假等"长生命周期流程"不同,办公用品的 BPM 流程在审批通过后即结束。后续的发放和归还是线下操作 + 系统记录,不再依赖流程引擎驱动。
4.2 FlowBillService 回调驱动状态变化
申请单服务实现了 FlowBillService<OaBillTypeEnum> 接口,BPM 引擎在流程状态变化时自动回调 updateProcessStatus:
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
SupplyApplyBillDO bill = supplyApplyBillMapper.selectById(id);
if (bill == null) {
throw exception(SUPPLY_APPLY_BILL_NOT_EXISTS);
}
// 更新主表流程状态
SupplyApplyBillDO updateObj = new SupplyApplyBillDO();
updateObj.setId(id);
updateObj.setProcessStatus(status);
supplyApplyBillMapper.updateById(updateObj);
if (APPROVE.getStatus().equals(status)) {
// 审批通过 → 明细变为「待领用」,可在领用发放管理中操作
supplyApplyItemMapper.updateItemStatusByBillId(id,
SupplyItemStatusEnum.PENDING_ISSUE.getStatus());
} else if (REJECT.getStatus().equals(status) || CANCEL.getStatus().equals(status)
|| RETURN.getStatus().equals(status) || WITHDRAW.getStatus().equals(status)) {
// 拒绝/取消/退回/撤回 → 清空明细状态,从领用发放管理中移除
supplyApplyItemMapper.updateItemStatusByBillId(id, null);
}
}
关键设计:
- 审批通过时,所有明细行从
APPLYING(-1)变为PENDING_ISSUE(0),此时行政管理员在领用发放页面才能看到这些记录并执行发放 - 审批拒绝/撤回时,明细行状态置为
null,相当于从领用发放管理中"消失"——这是一种优雅的"软删除"设计 - 整个状态流转由 BPM 框架通过
processDefinitionKey匹配FlowBillService实现类,零硬编码
五、后端核心实现
5.1 三层数据模型
办公用品管理使用 3 张表实现完整的业务数据存储:
oa_supply(用品台账)
├── 1:N ──▶ oa_supply_apply_item(领用明细行)
│ 通过 supply_id 关联
│
oa_supply_apply_bill(领用申请单)
└── 1:N ──▶ oa_supply_apply_item(领用明细行)
通过 apply_bill_id 关联
明细行 oa_supply_apply_item 是桥接表,同时关联台账和申请单,承载了"谁申请了什么、发了多少、还了多少"的全部信息。
5.2 单据编号生成
每张申请单都有唯一的单据编号,格式为 OA108-{YYYYMMDD}{5位流水}(如 OA108-2026032200001)。编号在首次保存或提交时自动生成:
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.OA, OaBillTypeEnum.OA_SUPPLY_APPLY_BILL));
}
108 是 OA 模块中办公用品领用申请单的类型编码,由 OaBillTypeEnum.OA_SUPPLY_APPLY_BILL 定义。流水号基于 Redis 自增,key 为 bill_code:OA:108:{yyyyMMdd},过期时间 2 天。
5.3 提交流程
提交申请时,后端做四件事:
@Override
@Transactional(rollbackFor = Exception.class)
public Long submitSupplyApplyBill(SupplyApplyBillSaveReqVO saveReqVO) {
// 1. 生成单据编号
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.OA, OaBillTypeEnum.OA_SUPPLY_APPLY_BILL));
}
// 2. 保存申请单主表(状态设为 RUNNING)
SupplyApplyBillDO bill = BeanUtils.toBean(saveReqVO, SupplyApplyBillDO.class)
.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
supplyApplyBillMapper.insertOrUpdate(bill);
// 3. 保存明细行(全量删插)
saveItems(bill.getId(), saveReqVO.getItems());
// 4. 发起 BPM 流程实例
Map<String, Object> variables = BpmProcessVariableUtils.buildBillVariables(saveReqVO);
String processInstanceId = processInstanceApi.submitProcessInstance(
Long.valueOf(saveReqVO.getCreator()),
new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey("oa_supply_apply_bill")
.setVariables(variables)
.setBusinessKey(String.valueOf(bill.getId()))
).getCheckedData();
// 5. 回写流程实例ID + 明细状态设为「申请中」
supplyApplyBillMapper.updateById(
new SupplyApplyBillDO().setId(bill.getId())
.setProcessInstanceId(processInstanceId));
supplyApplyItemMapper.updateItemStatusByBillId(
bill.getId(), SupplyItemStatusEnum.APPLYING.getStatus());
return bill.getId();
}
注意:提交流程时不扣减库存。库存仅在管理员执行发放操作时才会变动,避免了"审批中锁库存导致其他人无法申请"的问题。
5.4 库存管理三板斧
SupplyServiceImpl 提供三个库存变动方法,分别对应入库、发放、归还三种场景:
入库(+N):
@Transactional(rollbackFor = Exception.class)
public void instockSupply(SupplyInstockReqVO instockReqVO) {
SupplyDO supply = supplyMapper.selectById(instockReqVO.getId());
if (supply == null) throw exception(SUPPLY_NOT_EXISTS);
SupplyDO updateObj = new SupplyDO();
updateObj.setId(instockReqVO.getId());
updateObj.setStockQuantity(supply.getStockQuantity() + instockReqVO.getQuantity());
supplyMapper.updateById(updateObj);
}
发放扣库存(-N):
@Transactional(rollbackFor = Exception.class)
public void deductStock(Long supplyId, Integer quantity) {
SupplyDO supply = supplyMapper.selectById(supplyId);
if (supply == null) throw exception(SUPPLY_NOT_EXISTS);
if (supply.getStockQuantity() < quantity) {
throw exception(SUPPLY_STOCK_NOT_ENOUGH); // 库存不足
}
SupplyDO updateObj = new SupplyDO();
updateObj.setId(supplyId);
updateObj.setStockQuantity(supply.getStockQuantity() - quantity);
supplyMapper.updateById(updateObj);
}
归还恢复库存(+N):
@Transactional(rollbackFor = Exception.class)
public void restoreStock(Long supplyId, Integer quantity) {
SupplyDO supply = supplyMapper.selectById(supplyId);
if (supply == null) throw exception(SUPPLY_NOT_EXISTS);
SupplyDO updateObj = new SupplyDO();
updateObj.setId(supplyId);
updateObj.setStockQuantity(supply.getStockQuantity() + quantity);
supplyMapper.updateById(updateObj);
}
三个方法都加了 @Transactional,确保库存变动与业务操作在同一个事务中。
5.5 发放:消耗品 vs 借用品的分流
发放操作是整个业务中逻辑最密集的环节——需要校验状态、扣减库存、判断管理类型、更新明细状态,全部在一个事务中完成:
@Transactional(rollbackFor = Exception.class)
public void issueItem(SupplyApplyItemIssueReqVO issueReqVO) {
SupplyApplyItemDO item = supplyApplyItemMapper.selectById(issueReqVO.getId());
if (item == null) throw exception(SUPPLY_APPLY_ITEM_NOT_EXISTS);
// 只有「待领用」状态才可发放
if (!SupplyItemStatusEnum.PENDING_ISSUE.getStatus().equals(item.getItemStatus())) {
throw exception(SUPPLY_ITEM_STATUS_INVALID);
}
// 扣减台账库存
supplyService.deductStock(item.getSupplyId(), issueReqVO.getIssueQuantity());
// 消耗品 → 已领用(终态);借用品 → 已领用待归还
Integer newStatus;
if (item.getManagementType() != null && item.getManagementType() == 1) {
newStatus = SupplyItemStatusEnum.ISSUED.getStatus();
} else {
newStatus = SupplyItemStatusEnum.ISSUED_PENDING_RETURN.getStatus();
}
SupplyApplyItemDO updateObj = new SupplyApplyItemDO();
updateObj.setId(item.getId());
updateObj.setIssueQuantity(issueReqVO.getIssueQuantity());
updateObj.setItemStatus(newStatus);
updateObj.setIssueTime(LocalDateTime.now());
updateObj.setIssueBy(getCurrentUserName());
updateObj.setIssueRemark(issueReqVO.getIssueRemark());
supplyApplyItemMapper.updateById(updateObj);
}
5.6 归还:累计归还与状态翻转
归还操作支持分批归还——一台投影仪发了 3 台,可以先还 1 台,再还 2 台。系统累计归还数量,当累计归还数量 ≥ 实发数量时,状态自动翻转为「已归还」:
@Transactional(rollbackFor = Exception.class)
public void confirmReturn(SupplyApplyItemReturnReqVO returnReqVO) {
SupplyApplyItemDO item = supplyApplyItemMapper.selectById(returnReqVO.getId());
// 只有「已领用待归还」才可归还
if (!SupplyItemStatusEnum.ISSUED_PENDING_RETURN.getStatus().equals(item.getItemStatus())) {
throw exception(SUPPLY_ITEM_STATUS_INVALID);
}
// 累计归还不得超过实发数量
int newReturnQty = (item.getReturnQuantity() == null ? 0 : item.getReturnQuantity())
+ returnReqVO.getReturnQuantity();
if (newReturnQty > item.getIssueQuantity()) {
throw exception(SUPPLY_ITEM_RETURN_EXCEED);
}
// 恢复台账库存
supplyService.restoreStock(item.getSupplyId(), returnReqVO.getReturnQuantity());
// 全部归还 → 状态翻转为「已归还」;部分归还 → 保持「已领用待归还」
Integer newStatus = (newReturnQty >= item.getIssueQuantity())
? SupplyItemStatusEnum.RETURNED.getStatus()
: SupplyItemStatusEnum.ISSUED_PENDING_RETURN.getStatus();
SupplyApplyItemDO updateObj = new SupplyApplyItemDO();
updateObj.setId(item.getId());
updateObj.setReturnQuantity(newReturnQty);
updateObj.setItemStatus(newStatus);
updateObj.setReturnTime(LocalDateTime.now());
updateObj.setReturnBy(getCurrentUserName());
updateObj.setReturnRemark(returnReqVO.getReturnRemark());
supplyApplyItemMapper.updateById(updateObj);
}
5.7 删除时清理流程与关联数据
删除申请单时,需要同时清理 BPM 流程实例、附件和明细行:
@Transactional(rollbackFor = Exception.class)
public void deleteSupplyApplyBill(Long id) {
SupplyApplyBillDO bill = supplyApplyBillMapper.selectById(id);
if (bill == null) throw exception(SUPPLY_APPLY_BILL_NOT_EXISTS);
// 清理 BPM 流程实例(尽力清理,不阻塞删除)
if (StringUtils.isNotBlank(bill.getProcessInstanceId())) {
try {
processInstanceApi.deleteProcessInstance(
Long.valueOf(bill.getCreator()), bill.getProcessInstanceId());
} catch (Exception e) {
log.warn("[deleteSupplyApplyBill] 清理流程实例失败: {}", e.getMessage());
}
}
// 清理附件、明细行、主表
attachmentService.deleteAttachmentByBusiness(
OaBillTypeEnum.OA_SUPPLY_APPLY_BILL.getTypeCode(), id);
supplyApplyItemMapper.deleteByBillId(id);
supplyApplyBillMapper.deleteById(id);
}
六、RuoYi Office 的创新设计
6.1 类别导航 + 表格联动
传统的用品管理通常只提供一个下拉筛选器。RuoYi Office 在台账页面设计了左侧类别菜单 + 右侧表格联动的布局,类别通过数据字典 OA_SUPPLY_CATEGORY 动态渲染,无需修改代码即可扩展新类别。
选择类别后,表格查询、导出、新建(默认类别)三个操作同时联动,用户体验一气呵成。
6.2 库存预警可视化
台账列表中,库存数量不是简单的数字展示,而是通过 renderStockQuantity 方法动态渲染为带颜色的标签:
- 库存 ≥ 最低库存:绿色标签,表示安全
- 库存 < 最低库存:红色标签 + 文字警示,提醒及时采购
这种设计让管理员在浏览列表时就能一眼发现需要补货的物品。
6.3 申请阶段不锁库存
很多系统在员工提交申请时就"预扣"库存,导致审批周期内其他人看到的库存不准确。RuoYi Office 选择了发放时才扣减的策略:
- 优点:库存数字永远反映真实可用量,不受审批时长影响
- 代价:审批通过后发放时可能发现库存不足——但这种情况在实际业务中通常由管理员协调解决(部分发放或补货后再发放)
6.4 明细行级别的独立状态机
一张申请单可能包含 5 种用品,每种用品独立跟踪发放/归还状态。管理员可以先发放墨盒(消耗品,直接完成),再发放投影仪(借用品,等待归还),两者互不干扰。
6.5 批量发放提效
管理员可以勾选多行「待领用」记录一键批量发放,系统自动按申请数量逐行发放并扣减库存,大幅减少重复操作。
七、数据结构
7.1 表结构:oa_supply(用品台账)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
name | varchar(200) | 物品名称 |
code | varchar(100) | 物品编码 |
category | tinyint | 物品类别(字典 oa_supply_category) |
management_type | tinyint | 管理类型(1消耗品 / 其他为借用品) |
spec | varchar(200) | 规格型号 |
unit | varchar(50) | 计量单位 |
unit_price | decimal(10,2) | 参考单价 |
stock_quantity | int | 当前库存数量 |
min_stock | int | 最低库存(预警阈值) |
pic_url | varchar(500) | 物品图片 |
company_id / company_name | bigint / varchar | 所属公司 |
status | tinyint | 状态(0正常 / 1停用) |
sort | int | 排序 |
tenant_id | bigint | 租户编号 |
7.2 表结构:oa_supply_apply_bill(领用申请单)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
bill_code | varchar(50) | 单据编号(OA108-YYYYMMDD00001) |
process_instance_id | varchar(64) | Flowable 流程实例 ID |
process_status | tinyint | 流程状态(0草稿 1审批中 2通过 3拒绝 4已取消) |
apply_reason | varchar(500) | 申请事由 |
apply_date | date | 领用日期 |
use_type | tinyint | 使用类型(个人/部门) |
pickup_method | tinyint | 领取方式(自取/配送) |
creator_name | varchar(100) | 申请人姓名 |
dept_id / dept_name | bigint / varchar | 所属部门 |
company_id / company_name | bigint / varchar | 所属公司 |
remark | varchar(500) | 备注 |
tenant_id | bigint | 租户编号 |
7.3 表结构:oa_supply_apply_item(领用明细行)
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
apply_bill_id | bigint | 所属申请单 ID(外键) |
supply_id | bigint | 关联台账用品 ID(外键) |
supply_name | varchar(200) | 物品名称(冗余) |
supply_spec | varchar(200) | 规格型号(冗余) |
supply_unit | varchar(50) | 计量单位(冗余) |
management_type | tinyint | 管理类型(冗余,发放时判断) |
apply_quantity | int | 申请数量 |
issue_quantity | int | 实发数量 |
return_quantity | int | 已归还数量 |
item_status | tinyint | 明细状态(-1/0/1/2/3) |
issue_time | datetime | 发放时间 |
issue_by | varchar(100) | 发放人 |
issue_remark | varchar(500) | 发放备注 |
return_time | datetime | 归还时间 |
return_by | varchar(100) | 归还确认人 |
return_remark | varchar(500) | 归还备注 |
7.4 设计要点
- 冗余存储:明细行冗余了台账的
name、spec、unit、managementType,避免列表查询时 JOIN 台账表。即使台账信息后续修改,已有明细的历史数据不受影响 - 桥接模型:明细行同时通过
apply_bill_id关联申请单、通过supply_id关联台账,是两张主表的交汇点 - 操作留痕:发放和归还都记录了操作人(
issue_by/return_by)和操作时间,便于审计追溯 - 多租户隔离:全表
tenant_id,SaaS 场景开箱即用
八、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 3 表分层模型 | 台账+申请单+明细行 | 职责清晰,扩展性强 |
| 5 种状态机 | SupplyItemStatusEnum 驱动明细流转 | 精确跟踪每行物品的全生命周期 |
| 消耗品/借用品分流 | managementType 一个字段控制两条路径 | 一套代码覆盖两种业务 |
| 发放时才扣库存 | 申请/审批阶段不影响库存 | 库存数字始终反映真实可用量 |
| 分批归还 | 累计归还数量 vs 实发数量 | 支持真实业务中的部分归还场景 |
| 库存预警 | 前端 renderStockQuantity 红/绿标签 | 管理员一眼发现需补货物品 |
| 类别导航联动 | 字典驱动菜单 + 查询/导出/新建联动 | 可扩展、交互流畅 |
| 批量发放 | 勾选多行一键发放 | 减少重复操作,提升效率 |
| FlowBillService 标准化 | 统一接口接收 BPM 回调 | 新增单据只需实现接口 |
| 我的单据过滤 | 后端强制注入 creator | 数据安全,用户只看自己的 |
| 删除级联清理 | 流程+附件+明细+主表 | 数据一致性,无孤儿记录 |
| 多租户隔离 | 全表 tenant_id | SaaS 场景开箱即用 |
九、快速体验
操作路径:OA → 办公用品管理
推荐体验流程:
- 建台账:进入「办公用品台账」,新增几种用品(如 A4 纸/消耗品、投影仪/借用品),设置库存和最低库存
- 入库:点击台账行的「入库」按钮,录入入库数量,观察库存变化
- 发起申请:进入「领用申请管理」,点击「新建」,选择用品、填写数量和事由,提交审批
- 审批:到流程中心的待办任务中找到这条申请,审批通过
- 发放:进入「领用发放管理」,找到状态为「待领用」的记录,点击「发放」
- 观察分流:消耗品发放后状态变为「已领用」(终态);借用品变为「已领用待归还」
- 归还:对借用品执行「归还」操作,观察库存恢复和状态变化
- 库存预警:调低某种用品的最低库存阈值,观察列表中的红色预警标签
源码仓库:
| 平台 | 地址 |
|---|---|
| GitCode(后端) | gitcode.com/zhouzhongya… |
| GitCode(前端) | gitcode.com/zhouzhongya… |
| GitHub(后端) | github.com/yuqing2026/… |
结语
办公用品管理看似是 OA 系统中最"不起眼"的模块,但做好了是行政管理效率的倍增器。3 张表的分层模型让数据职责清晰,5 种状态机让每件物品都有据可查,消耗品/借用品的自动分流让一套代码覆盖两种场景,发放时才扣库存让数字永远可信。
这套设计模式不仅适用于办公用品,还可以推广到其他"申请→审批→发放→归还"类的资产管理场景(如固定资产领用、工具借还、设备调拨等)。核心思想是:让状态机驱动业务流转,让 BPM 负责审批,让库存操作只发生在物理交付时刻。
如果你正在设计类似的物资管理模块,或者对状态机驱动的业务设计感兴趣,欢迎参考源码实现。
> 💡 **觉得有价值?**
>
> ⭐ **Star 仓库**:[ruoyi-office](https://gitcode.com/zhouzhongyan/ruoyi-office.git)
>
> 💬 **技术交流**: 添加💬**17156169080**,备注「RuoYi Office」,加入技术交流群
>
> 📚 **演示地址**:[http://ruoyioffice.com](http://ruoyioffice.com)