企业办公用品管理系统全流程设计实战:3张表、5种状态、申请→审批→发放→归还全生命周期闭环

0 阅读20分钟

企业办公用品管理系统全流程设计实战:3张表、5种状态、申请→审批→发放→归还全生命周期闭环

📦 源码1ruoyi-office-vben |📦 源码2ruoyi-office |📦 源码3ruoyi-office | 💬 :17156169080

办公用品管理是每家企业的"后勤刚需"——纸笔、墨盒、订书机、投影仪,东西虽小,管不好就是一笔糊涂账。很多企业的用品管理还停留在"Excel台账 + 微信群喊话"的阶段:谁领了什么没记录,库存见底了没人知道,投影仪借走三个月没人还。RuoYi Office 用 3 张表、5 种状态机、1 套 BPM 流程,构建了从台账建档→申请审批→发放扣库存→借用品归还的全生命周期闭环。

引言:办公用品管理到底难在哪?

"不就是买来、发掉、再买吗?"——初次接到需求的开发者大多这么想。但真正动手时会发现,办公用品管理的复杂度远超"进销存"三个字:

品类繁杂,管理方式不同:A4 纸和中性笔是消耗品,发出去就不用管;投影仪和剪刀是借用品,用完要还。两种品类的状态流转和库存逻辑完全不同。

申请与发放分离:员工提交申请不等于立刻拿到东西。申请要走审批流程,审批通过后行政才能发放。申请数量和实际发放数量可能不一致(库存不够时部分发放)。

库存实时性要求高:入库增库存、发放减库存、归还加库存——三个方向的库存变动需要事务保证,还要有库存预警提醒。

归还追踪:借用品发出去多少、还回来多少、还差多少,需要精确到每一行明细。

一张申请单多行物品:员工一次申请可能要 3 种不同的用品,每种用品独立发放、独立归还,状态各不相同。

本文以 RuoYi Office 的办公用品管理模块为例,完整拆解其业务建模、数据结构、流程设计、前后端实现方案。


一、业务设计:从一次领用申请说起

1.1 业务全景

一次完整的办公用品业务涉及三个角色、四个阶段:

阶段角色操作系统行为
建档行政管理员录入用品信息、入库创建台账记录,库存+N
申请普通员工选择用品、填写数量、提交审批创建申请单+明细行,发起BPM流程
发放行政管理员审批通过后,逐行或批量发放明细状态流转,库存-N
归还行政管理员借用品到期后确认归还明细状态流转,库存+N

1.2 消耗品 vs 借用品:两条分叉路

这是本方案最核心的业务抽象——用品的"管理类型"决定了它的生命周期长度image.png

系统在台账层面定义了每种用品的管理类型(managementType),在发放时自动判断:

  • 消耗品managementType = 1):发放后状态直接变为「已领用」,不再有归还流程
  • 借用品managementType ≠ 1):发放后状态变为「已领用待归还」,等待后续归还确认

这种设计避免了"为每种用品写不同的业务逻辑",只需一个字段值就实现了两条截然不同的流转路径。

1.3 五种状态的生命周期

每一行申请明细都有独立的状态,由 SupplyItemStatusEnum 定义:

状态码状态名触发条件可执行操作
-1申请中申请单提交BPM审批等待审批
0待领用BPM审批通过可发放
1已领用消耗品发放完成无(终态)
2已领用待归还借用品发放完成可归还
3已归还归还数量达到发放数量无(终态)

状态流转图: image.png


二、系统设计:三个子模块的协作

2.1 模块组成

RuoYi Office 的办公用品管理位于 OA 办公管理 → 办公用品管理 目录下,由三个紧密协作的子模块组成:

子模块菜单名称功能定位面向角色
用品台账办公用品台账用品主数据管理:品类、规格、库存、入库行政管理员
领用申请领用申请管理员工发起领用申请、走BPM审批全体员工
领用发放领用发放管理审批通过后的发放和归还操作行政管理员

2.2 核心设计决策

决策点方案理由
数据模型台账+申请单+明细行三张表台账管"有什么",申请单管"谁要",明细行管"要什么和发了没"
库存变动时机仅在发放/归还时变动申请和审批阶段不影响库存,避免"审批中锁库存"的问题
管理类型台账定义+明细冗余发放时按冗余字段判断,避免反查台账
审批集成FlowBillService标准接口复用BPM框架回调,状态同步零耦合
我的单据后端强制注入creator数据安全,员工只能看到自己的申请

三、PC 端功能实现

3.1 办公用品台账

台账页面是用品管理的"数据基石",采用左侧类别导航 + 右侧表格列表的经典布局。 supply-info-list.png

▲ 办公用品台账:左侧按字典 OA_SUPPLY_CATEGORY 动态渲染类别菜单(文具类、打印耗材、生活用品、电脑办公等),点击类别自动筛选;右侧表格展示物品名称、编码、规格型号、库存数量等关键信息

台账设计要点

  • 类别导航联动:左侧 Menu 组件绑定 selectedCategory 状态,切换类别时自动触发 gridApi.query(),查询参数中注入 category 过滤条件
  • 库存预警renderStockQuantity 方法对比 stockQuantity(当前库存)和 minStock(最低库存),当库存低于阈值时标签变红色警示
  • 管理类型标签:通过字典 OA_SUPPLY_MANAGEMENT_TYPE 渲染为蓝色标签(消耗品)或橙色标签(借用品)
  • 入库操作:每行提供「入库」按钮,弹出入库弹窗填写入库数量,提交后库存实时增加
  • 导出Excel:支持按当前筛选条件导出用品数据

3.2 领用申请管理

申请列表展示当前用户创建的所有领用申请单,支持新建、查看详情、删除操作。 supply-apply-list.png

▲ 领用申请列表:支持按单据编号、单据状态、申请部门、创建时间搜索筛选。列表默认只显示当前登录用户的申请单

列表设计要点

  • 我的单据过滤:后端 getSupplyApplyBillPage 在查询时自动注入 creator = 当前用户ID,员工只能看到自己的申请
  • 单据编号链接:点击编号列自动跳转到详情页,编号格式为 OA108-{YYYYMMDD}{5位流水}
  • 状态标签:通过 CellDict 渲染 BPM 流程状态(草稿/审批中/审批通过/拒绝/已取消),不同状态用不同颜色区分
  • 删除约束:删除前通过 BpmProcessInstanceStatusDeleteValue 过滤不可删除的状态(如审批中),并给出提示

3.3 领用申请详情

详情页是申请单的核心交互页面,集 基本信息表单、领用明细表格、附件管理 于一体。 supply-apply-detail-data.png

▲ 申请详情页(已审批通过):顶部展示单据编号、申请人、日期、所属单位/部门等摘要信息;下方分为基本信息、领用明细、附件信息三个区域

「基本信息」区域

字段组件说明
领用日期DatePicker默认当天,必填
使用类型Select个人使用 / 部门使用(字典 oa_supply_use_type
领取方式Select自取 / 配送(字典 oa_supply_pickup_method
申请事由TextArea必填,审批人以此判断合理性
备注TextArea选填

「领用明细」区域

这是申请单的核心——通过 SupplySelectModal(用品选择弹窗)从台账中勾选需要的办公用品,添加到明细表格中:

  • 每行展示物品名称、规格型号、计量单位、管理类型
  • 领用数量 列为 InputNumber 可编辑,默认值为 1
  • 同一用品不可重复添加(按 supplyId 去重)
  • 只读模式下明细表格不显示操作列和添加按钮 image.png

▲ 新建申请单:领用明细为空,点击蓝色「添加办公用品」按钮弹出用品选择弹窗

用品选择弹窗 SupplySelectModal

弹窗内部是一个独立的分页表格,支持按名称、类别、管理类型搜索过滤。勾选多条用品后点击确认,选中的用品自动添加到明细表格中。弹窗只展示状态为"正常"的用品(status: 0),停用的用品不可被选择。

3.4 领用发放管理

发放管理是行政管理员的工作台,以申请明细行为粒度管理发放和归还。 supply-record-list.png

▲ 领用发放管理:顶部分段器(全部/申请中/待发放/已领用/待归还/已归还)切换筛选;表格展示申请单号、申请人、物品名称、管理类型、申请数量、实发数量、已归还数量等信息

发放管理设计要点

  • 分段器筛选:6 种状态对应 Segmented 组件的选项,切换时自动以 itemStatus 条件查询;选择"全部"时不带状态条件
  • 发放操作:仅「待领用」状态的行可操作。弹出发放弹窗,显示物品名称和申请数量,填写实发数量后提交。发放同时扣减台账库存
  • 批量发放:勾选多行「待领用」记录,点击「批量发放」一次性全部发放,实发数量默认等于申请数量
  • 归还操作:仅「已领用待归还」状态的行可操作。弹窗显示实发数量和已归还数量,填写本次归还数量(不得超过未归还数量),提交后恢复台账库存
  • 勾选限制checkMethod 禁止勾选「申请中」状态的记录,避免误操作

四、流程设计:BPM 审批驱动状态流转

4.1 流程编排

办公用品领用申请使用 Flowable 引擎编排,流程定义 Key 为 oa_supply_apply_bill。流程设计相对简洁:

  1. 员工填写申请信息 + 选择用品明细 → 提交流程
  2. 负责人审批
  3. 行政审批
  4. 审批通过,明细状态从「申请中」→「待领用」
  5. 行政管理员在领用发放管理中执行发放

与请假销假等"长生命周期流程"不同,办公用品的 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(用品台账)

字段类型说明
idbigint主键
namevarchar(200)物品名称
codevarchar(100)物品编码
categorytinyint物品类别(字典 oa_supply_category
management_typetinyint管理类型(1消耗品 / 其他为借用品)
specvarchar(200)规格型号
unitvarchar(50)计量单位
unit_pricedecimal(10,2)参考单价
stock_quantityint当前库存数量
min_stockint最低库存(预警阈值)
pic_urlvarchar(500)物品图片
company_id / company_namebigint / varchar所属公司
statustinyint状态(0正常 / 1停用)
sortint排序
tenant_idbigint租户编号

7.2 表结构:oa_supply_apply_bill(领用申请单)

字段类型说明
idbigint主键
bill_codevarchar(50)单据编号(OA108-YYYYMMDD00001
process_instance_idvarchar(64)Flowable 流程实例 ID
process_statustinyint流程状态(0草稿 1审批中 2通过 3拒绝 4已取消)
apply_reasonvarchar(500)申请事由
apply_datedate领用日期
use_typetinyint使用类型(个人/部门)
pickup_methodtinyint领取方式(自取/配送)
creator_namevarchar(100)申请人姓名
dept_id / dept_namebigint / varchar所属部门
company_id / company_namebigint / varchar所属公司
remarkvarchar(500)备注
tenant_idbigint租户编号

7.3 表结构:oa_supply_apply_item(领用明细行)

字段类型说明
idbigint主键
apply_bill_idbigint所属申请单 ID(外键)
supply_idbigint关联台账用品 ID(外键)
supply_namevarchar(200)物品名称(冗余)
supply_specvarchar(200)规格型号(冗余)
supply_unitvarchar(50)计量单位(冗余)
management_typetinyint管理类型(冗余,发放时判断)
apply_quantityint申请数量
issue_quantityint实发数量
return_quantityint已归还数量
item_statustinyint明细状态(-1/0/1/2/3)
issue_timedatetime发放时间
issue_byvarchar(100)发放人
issue_remarkvarchar(500)发放备注
return_timedatetime归还时间
return_byvarchar(100)归还确认人
return_remarkvarchar(500)归还备注

7.4 设计要点

  • 冗余存储:明细行冗余了台账的 namespecunitmanagementType,避免列表查询时 JOIN 台账表。即使台账信息后续修改,已有明细的历史数据不受影响
  • 桥接模型:明细行同时通过 apply_bill_id 关联申请单、通过 supply_id 关联台账,是两张主表的交汇点
  • 操作留痕:发放和归还都记录了操作人(issue_by / return_by)和操作时间,便于审计追溯
  • 多租户隔离:全表 tenant_id,SaaS 场景开箱即用

八、技术亮点总结

设计要点实现方式价值
3 表分层模型台账+申请单+明细行职责清晰,扩展性强
5 种状态机SupplyItemStatusEnum 驱动明细流转精确跟踪每行物品的全生命周期
消耗品/借用品分流managementType 一个字段控制两条路径一套代码覆盖两种业务
发放时才扣库存申请/审批阶段不影响库存库存数字始终反映真实可用量
分批归还累计归还数量 vs 实发数量支持真实业务中的部分归还场景
库存预警前端 renderStockQuantity 红/绿标签管理员一眼发现需补货物品
类别导航联动字典驱动菜单 + 查询/导出/新建联动可扩展、交互流畅
批量发放勾选多行一键发放减少重复操作,提升效率
FlowBillService 标准化统一接口接收 BPM 回调新增单据只需实现接口
我的单据过滤后端强制注入 creator数据安全,用户只看自己的
删除级联清理流程+附件+明细+主表数据一致性,无孤儿记录
多租户隔离全表 tenant_idSaaS 场景开箱即用

九、快速体验

操作路径:OA → 办公用品管理

推荐体验流程

  1. 建台账:进入「办公用品台账」,新增几种用品(如 A4 纸/消耗品、投影仪/借用品),设置库存和最低库存
  2. 入库:点击台账行的「入库」按钮,录入入库数量,观察库存变化
  3. 发起申请:进入「领用申请管理」,点击「新建」,选择用品、填写数量和事由,提交审批
  4. 审批:到流程中心的待办任务中找到这条申请,审批通过
  5. 发放:进入「领用发放管理」,找到状态为「待领用」的记录,点击「发放」
  6. 观察分流:消耗品发放后状态变为「已领用」(终态);借用品变为「已领用待归还」
  7. 归还:对借用品执行「归还」操作,观察库存恢复和状态变化
  8. 库存预警:调低某种用品的最低库存阈值,观察列表中的红色预警标签

源码仓库

平台地址
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)