Skip to content

RuoyiOffice之 OA 公文收文管理——发文-收文自动联动、签收认领、领导批示、承办办理全流程拆解

🌐 文档地址http://ruoyioffice.com | 📦 源码1ruoyi-office-vben |📦 源码2ruoyi-office |📦 源码3ruoyi-office | 💬 :17156169080(备注「RuoYi Office」)

在上一篇《公文发文管理》中,我们完整拆解了从套红模板、GB/T 9704 标准预览到审批签发、PDF 生成的全链路实现。文章最后提到"发文审批通过后,系统自动为每个主送/抄送部门生成收文记录"——但"生成"只是起点,收文部门拿到这条记录后,怎么签收?领导怎么批示?承办人怎么办理?办理完了怎么归档? 这些才是公文流转真正"落地"的关键环节。本文聚焦公文收文模块,从发文-收文的联动机制讲起,完整拆解签收认领、领导批示、承办办理、办结归档的设计与实现。

上篇回顾:发文模块的核心产出

在进入收文之前,先快速回顾发文模块的三大核心产出——它们正是收文模块的"输入":

产出说明对收文的意义
收文记录发文审批通过后,系统按主送/抄送部门自动创建 OfficeDocReceiveDO收文的数据来源
正式公文 PDF办公室编号发文节点生成的 GB/T 9704 标准 PDF收文可直接查看/下载正式公文
附件同步发文附件自动复制到每条收文记录收文部门无需另行索取

发文的 onProcessApproved 方法为每个主送部门创建了一条"待签收"的收文记录(handleStatus = 0),为每个抄送部门创建了一条"仅需知悉"的收文记录。但这些记录的 creator 字段被刻意清空——因为收文还没有人签收认领

这个设计的精妙之处在于:发文是"推",收文是"拉"。发文审批通过后系统主动推送收文记录,但具体由谁来处理,需要收文部门的人主动签收认领。


一、业务设计:收文在公文体系中的角色

1.1 发文与收文的联动关系

发文-收文自动联动架构:发文审批通过后按部门拆分生成收文记录

▲ 发文审批通过后,系统按主送/抄送部门自动拆分生成收文记录,每个部门各自独立签收、批示、办理

核心联动规则

  • 一对多拆分:一条发文可能主送 3 个部门、抄送 2 个部门,系统为每个部门各生成一条独立的收文记录
  • 数据继承:收文继承发文的标题、来文字号、正文摘要、密级、紧急程度、正式公文 PDF、附件
  • 关联追溯:收文通过 sendBillId 关联原始发文,可跳转查看原始发文详情
  • 独立流转:各部门的收文相互独立,一个部门办结不影响其他部门的办理进度

1.2 收文的两种来源

RuoYi Office 的公文收文有两种创建方式,覆盖了不同的业务场景:

来源说明特点
发文自动生成内部发文审批通过后系统自动创建sendBillIdcreator 为空(需签收)、数据从发文同步
手动创建收文部门人员手动新建(类似外部收文登记)sendBillId、由创建人直接填写所有字段

为什么需要手动创建? 虽然大多数内部收文来自发文联动,但有时收文部门需要登记一些非系统流转的来文(如收到纸质文件需要在系统中登记办理)。手动创建提供了这种灵活性。


二、流程设计:从签收到归档的四级流转

公文收文的审批流程与发文截然不同——发文聚焦"审核签发",收文聚焦"批示办理"。

2.1 审批流程节点

公文收文审批流程:签收 → 领导批示 → 承办部门办理 → 办结归档

▲ 公文收文四级流转:签收后进入领导批示环节,批示指定承办人后由承办人办理,最终办结归档

节点角色核心操作设计考量
收文签收收文部门文员确认签收、补充收文信息进入流程的第一步,明确责任人
领导批示部门领导批示处理意见、指定承办人领导可编辑"领导批示"字段,指明办理方向
承办部门办理承办人实际办理公文事项、填写办理结果承办人可编辑"办理结果"字段,记录办理过程
办结归档部门文员确认办结、归档存查最终确认环节,流程结束后状态变为"已办结"

2.2 节点级业务定制

与发文类似,收文流程也在关键节点嵌入了精准的业务逻辑:

领导批示节点:只有在此节点,"领导批示"文本框才可编辑。领导的批示意见通常包括"请XX部门阅处"、"同意,请尽快落实"等指示。审批前自动保存表单,确保批示内容不丢失。

承办部门办理节点:只有在此节点,"办理结果"文本框才可编辑。承办人需要记录实际办理情况。审批前同样自动保存。

typescript
async function beforeApproval(): Promise<boolean> {
  try {
    if (
      props.isApproval &&
      (props.nodeKeyName === '领导批示' ||
        props.nodeKeyName === '承办部门办理') &&
      basicFormRef.value
    ) {
      const formValues = await basicFormRef.value.getFormValues();
      const data = { ...formData.value, ...formValues };
      await saveOfficeDocReceive(data);
    }
    return true;
  } catch {
    message.error($t('ui.actionMessage.operationFailed'));
    return false;
  }
}

注意这段代码的精妙之处:它只在"领导批示"和"承办部门办理"两个特定节点自动保存。其他节点(如收文签收、办结归档)不需要额外的保存逻辑,因为它们没有特殊的可编辑字段。

2.3 办理状态机

收文的办理状态独立于 BPM 流程状态,形成了一个四态状态机

待签收(0) ──签收──→ 已签收(1) ──提交流程──→ 办理中(2) ──流程通过──→ 已办结(3)
                        │                        │
                        └──流程驳回/撤回──→ 已签收(1)
状态码状态名含义触发条件
0待签收发文自动生成的收文,等待部门人员签收发文 onProcessApproved 创建
1已签收已有人签收认领,可以起草提交claimOfficeDocReceive 方法
2办理中BPM 流程运行中submitOfficeDocReceive 方法
3已办结流程审批通过,办理完成updateProcessStatus 回调

办理状态与流程状态的联动是在 FlowBillService.updateProcessStatus() 方法中自动完成的——流程通过则标记"已办结",流程驳回/撤回则回退到"已签收"。


三、功能实现:签收认领 + 收文表单

3.1 签收认领:收文流转的"第一公里"

对于发文自动生成的收文,最关键的第一步是签收认领。签收的本质是"将当前用户设为该收文记录的负责人"。

在列表页中,未签收的收文显示"签收"按钮,已签收的显示"已签收":

vue
<TableAction
  :actions="[
    {
      label: '签收',
      type: 'link',
      ifShow: () => !!row.sendBillId && !row.creator,
      auth: ['oa:office-doc-receive:create'],
      onClick: () => handleClaim(row),
    },
    {
      label: '已签收',
      type: 'link',
      ifShow: () => !!row.sendBillId && !!row.creator,
      disabled: true,
    },
  ]"
/>

签收确认弹窗:

typescript
function handleClaim(row: OfficeDocReceiveApi.OfficeDocReceive) {
  Modal.confirm({
    title: '签收确认',
    content: `确定签收公文【${row.title || row.billCode}】吗?
              签收后该公文将由您负责后续办理。`,
    okText: '确定签收',
    cancelText: '取消',
    async onOk() {
      await claimOfficeDocReceive(row.id as number);
      message.success('签收成功');
      onRefresh();
    },
  });
}

签收设计的三个关键约束

  1. 只有发文生成的收文才需要签收:通过 !!row.sendBillId 判断,手动创建的收文已有创建人,无需签收
  2. 先到先得:签收使用了乐观锁机制(isNull("creator")),同一条收文只有第一个签收的人能成功
  3. 签收不可逆:一旦签收,该收文即归属该用户,不可再转让

3.2 收文表单字段设计

收文表单的字段设计分为两大类:发文同步字段收文专属字段

字段组件发文生成时手动创建时特殊说明
收文类型Select(固定选项)自动填充(主送/抄送)不显示disabled,不可编辑
发文单位Input(只读)从发文同步不显示仅发文生成的收文可见
发文日期Input(只读)从发文同步不显示仅发文生成的收文可见
签发人Input(只读)从发文同步不显示仅发文生成的收文可见
公开类别Select(只读)从发文同步不显示仅发文生成的收文可见
来文字号Input从发文同步手动填写对应发文的 docNumber
公文标题Input(全宽)从发文同步手动填写必填
密级Select(字典驱动)从发文同步手动选择
紧急程度Select(字典驱动)从发文同步手动选择
收文日期DatePicker自动填充当天自动填充当天必填
收文部门Input自动填充手动填写
主办人Input手动指定手动指定通常在领导批示时指定
领导批示Textarea(全宽)仅"领导批示"节点可编辑
办理结果Textarea(全宽)仅"承办部门办理"节点可编辑
办理期限DatePicker手动选择手动选择带时分秒
内容摘要Textarea(全宽)从发文同步手动填写

表单字段的动态显隐是一个设计亮点。发文同步字段(发文单位、发文日期、签发人、公开类别)通过 dependencies 配置,只有当 sendBillId 存在时才显示:

typescript
{
  fieldName: 'sendDeptName',
  label: '发文单位',
  component: 'Input',
  componentProps: { disabled: true },
  dependencies: {
    triggerFields: ['sendBillId'],
    show: (values) => !!values.sendBillId,
  },
},

领导批示和办理结果的节点级权限控制同样值得关注。它们的 disabled 属性是一个函数,根据当前审批节点名称动态判断:

typescript
{
  fieldName: 'leaderOpinion',
  label: '领导批示',
  component: 'Textarea',
  formItemClass: 'col-span-full',
  componentProps: {
    placeholder: '请输入领导批示',
    disabled: () => {
      if (nodeKeyName?.value === '领导批示') return false;
      return readonly?.value;
    },
  },
},

这个设计让同一个表单在不同审批节点呈现不同的编辑能力——领导看到的表单中只有"领导批示"可以编辑,承办人看到的表单中只有"办理结果"可以编辑,其他字段一律只读。

3.3 正式公文预览与关联发文查看

收文详情页的"正式公文"区域根据数据来源呈现三种状态:

状态一:发文生成 + 已有 PDF

vue
<template v-if="formData.docFileUrl">
  <a :href="formData.docFileUrl" target="_blank">下载正式公文</a>
  <Button v-if="formData.sendBillId" type="link" @click="handleViewSendBill">
    查看关联发文单
  </Button>
  <iframe :src="formData.docFileUrl" style="height: 600px" />
</template>

用户可以直接在页面内预览 PDF 公文、下载公文、跳转查看关联的原始发文单。

状态二:发文生成 + PDF 未生成

vue
<template v-else-if="formData.sendBillId">
  <Alert type="info" message="正式公文尚未生成,请联系办公室文秘" />
  <Button type="link" @click="handleViewSendBill">查看关联发文单</Button>
</template>

这种情况通常出现在发文流程尚未走到"办公室编号发文"节点(PDF 在该节点生成)。

状态三:手动创建的收文

vue
<template v-else>
  <div class="flex flex-col items-center">
    <p>请上传正式公文文件(支持 PDF、Word 格式)</p>
    <Button type="primary" @click="handleUploadDoc">上传正式公文</Button>
  </div>
</template>

手动创建的收文没有关联发文,用户可以自行上传正式公文文件。

关联发文跳转的实现非常简洁:

typescript
function handleViewSendBill() {
  if (formData.value.sendBillId) {
    router.push({
      path: '/oa/officedoc/send-info',
      query: { id: formData.value.sendBillId, viewType: 'done' },
    });
  }
}

viewType: 'done' 表示以"已办"模式打开发文详情,所有字段只读——收文部门只能查看原始发文,不能修改。

3.4 收文列表页

收文列表页在标准的业务单据列表模式基础上,增加了签收管理功能:

说明技术实现
单据编号系统自动生成,可点击跳转详情createRouterLinkColumn
公文标题最长 200 字符普通文本列
来文字号如"无办发〔2026〕1号"普通文本列
密级公开/内部/秘密/机密/绝密CellDict 字典渲染
紧急程度普通/急件/特急CellDict 字典渲染
收文类型主送/抄送自定义 formatter
收文部门收文归属部门普通文本列
主办人负责办理的人员普通文本列
办理状态待签收/已签收/办理中/已办结CellDict 字典渲染
流程状态BPM 流程状态CellDict 字典渲染
操作签收/已签收/删除TableAction 条件显示

收文列表的数据隔离值得注意——查询时自动附加 companyIdreceiveDeptId 过滤条件,确保用户只能看到自己公司、自己部门的收文:

typescript
proxyConfig: {
  ajax: {
    query: async ({ page }, formValues) => {
      return await getOfficeDocReceivePage({
        pageNo: page.currentPage,
        pageSize: page.pageSize,
        ...formValues,
        companyId: userStore.userInfo?.companyId,
        receiveDeptId: userStore.userInfo?.deptId,
      });
    },
  },
},

四、表结构设计

4.1 收文表核心字段

┌───────────────────────────────────────────────┐
│          oa_office_doc_receive                │
│          (公文收文表)                           │
│                                               │
│  ┌─ 关联字段 ─────────────────────────────┐   │
│  │  send_bill_id  → 关联发文单 ID          │   │
│  │  template_id   → 套红模板 ID            │   │
│  │  receive_type  → 收文类型(主送/抄送)     │   │
│  └─────────────────────────────────────────┘   │
│                                               │
│  ┌─ 公文信息 ─────────────────────────────┐   │
│  │  doc_number     来文字号                │   │
│  │  title          公文标题                │   │
│  │  secret_level   密级                    │   │
│  │  urgency_level  紧急程度                │   │
│  │  content        内容摘要                │   │
│  │  doc_file_url   正式公文文件             │   │
│  └─────────────────────────────────────────┘   │
│                                               │
│  ┌─ 收文处理 ─────────────────────────────┐   │
│  │  receive_date      收文日期             │   │
│  │  receive_dept_id   收文部门 ID          │   │
│  │  receive_dept_name 收文部门名称          │   │
│  │  handler_id        主办人 ID            │   │
│  │  handler_name      主办人姓名            │   │
│  │  leader_opinion    领导批示              │   │
│  │  handle_status     办理状态              │   │
│  │  handle_result     办理结果              │   │
│  │  handle_deadline   办理期限              │   │
│  └─────────────────────────────────────────┘   │
│                                               │
│  ┌─ 流程/公共字段 ────────────────────────┐   │
│  │  process_instance_id  流程实例 ID       │   │
│  │  process_status       流程状态           │   │
│  │  bill_code            单据编号           │   │
│  │  tenant_id / creator / ...              │   │
│  └─────────────────────────────────────────┘   │
└───────────────────────────────────────────────┘

4.2 收文表与发文表的字段对比

字段类别发文表(send)收文表(receive)说明
模板关联template_idtemplate_id(继承)收文继承发文的模板引用
文号doc_mark + doc_year + doc_sequencedoc_numberdoc_number(来文字号)收文无需拆分文号组成
部门send_dept_id/namemain_receive_dept_ids/namescc_dept_ids/namesreceive_dept_id/name发文一对多,收文单一部门
签发signer_id/name无(通过关联发文查看)签发人信息存储在发文
办理handler_id/nameleader_opinionhandle_resulthandle_deadline收文特有的办理流转字段
办理状态handle_status(0-3)收文特有的四态状态机
PDFdoc_file_url(服务端生成)doc_file_url(从发文同步或手动上传)收文的 PDF 来自发文同步
关联send_bill_id → 发文核心关联字段

4.3 设计思考:为什么收文不复用发文的预览组件?

在发文模块中,我们实现了一个精美的 DocPreview 实时预览组件,支持 GB/T 9704 标准渲染。那为什么收文没有复用它?

原因很务实

  1. 收文的核心诉求不同:发文需要"边填边看"的实时预览体验(因为发文人在构建公文内容),而收文部门只需要"查看已生成的正式公文"
  2. 正式公文 PDF 已生成:发文审批通过时 PDF 已经生成,收文直接用 iframe 展示 PDF 即可,无需再次渲染
  3. 手动创建的收文无模板:手动登记的收文没有关联套红模板,无法使用 DocPreview 渲染

架构设计的原则之一是"避免过度设计"。收文用 iframe + PDF 已经完美满足需求,没必要为了"技术一致性"而强行复用预览组件。


五、核心后端代码解析

5.1 签收认领:CAS 风格的乐观锁

签收是收文模块最关键的原子操作——它必须保证"同一条收文只能被一个人签收":

java
@Override
public void claimOfficeDocReceive(Long id) {
    OfficeDocReceiveDO docReceive = officeDocReceiveMapper.selectById(id);
    if (docReceive == null) {
        throw exception(OFFICE_DOC_RECEIVE_NOT_EXISTS);
    }
    if (docReceive.getSendBillId() == null) {
        throw exception(OFFICE_DOC_RECEIVE_CLAIM_NOT_ALLOWED);
    }
    Long userId = SecurityFrameworkUtils.getLoginUserId();
    int updated = officeDocReceiveMapper.update(null,
            Wrappers.<OfficeDocReceiveDO>update()
                    .set("creator", String.valueOf(userId))
                    .set("handle_status", HANDLE_STATUS_CLAIMED)
                    .isNull("creator")  // 乐观锁:只有 creator 为空才能签收
                    .eq("id", id));
    if (updated == 0) {
        throw exception(OFFICE_DOC_RECEIVE_ALREADY_CLAIMED);
    }
}

三层防护

  1. 业务校验:只有发文生成的收文(sendBillId != null)才允许签收操作
  2. 乐观锁:UPDATE 条件中 isNull("creator") 确保只有第一个签收的人能成功
  3. 异常提示:如果 updated == 0(已被他人签收),抛出"已被签收"的友好提示

这种 CAS(Compare-And-Swap)风格的实现方式比悲观锁(SELECT FOR UPDATE)更轻量,适合签收这种低并发但需要原子性的场景。

5.2 收文提交:发起 BPM 流程

签收后,收文部门的文员可以提交收文,进入 BPM 审批流程:

java
@Override
public Long submitOfficeDocReceive(OfficeDocReceiveSaveReqVO saveReqVO) {
    if (StringUtils.isBlank(saveReqVO.getBillCode())) {
        saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
            SystemEnum.OA, OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE));
    }
    OfficeDocReceiveDO docReceive = BeanUtils.toBean(saveReqVO, OfficeDocReceiveDO.class)
            .setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus())
            .setHandleStatus(HANDLE_STATUS_PROCESSING);
    officeDocReceiveMapper.insertOrUpdate(docReceive);

    Map<String, Object> processInstanceVariables =
        BpmProcessVariableUtils.buildBillVariables(saveReqVO);
    String processInstanceId = processInstanceApi.submitProcessInstance(
        Long.valueOf(saveReqVO.getCreator()),
        new BpmProcessInstanceCreateReqDTO()
            .setProcessDefinitionKey(
                OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE.getProcessDefinitionKey())
            .setVariables(processInstanceVariables)
            .setBusinessKey(String.valueOf(docReceive.getId()))
    ).getCheckedData();
    officeDocReceiveMapper.updateById(
        new OfficeDocReceiveDO().setId(docReceive.getId())
            .setProcessInstanceId(processInstanceId));

    if (saveReqVO.getAttachments() != null) {
        attachmentService.saveAttachmentList(
            OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE.getTypeCode(),
            docReceive.getId(), saveReqVO.getAttachments());
    }
    return docReceive.getId();
}

关键设计点

  1. 状态联动:提交时同时将 processStatus 设为运行中、handleStatus 设为"办理中"
  2. 标准 BPM 接入:使用与发文完全相同的 BpmProcessVariableUtils.buildBillVariables() 构建流程变量
  3. 流程定义 Key:通过 OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE.getProcessDefinitionKey() 获取,与菜单配置的流程定义 Key 一致

5.3 FlowBillService:流程状态与办理状态的联动

收文服务同样实现了 FlowBillService<OaBillTypeEnum> 接口,但它的 updateProcessStatus 方法比发文更复杂——因为需要同步维护办理状态:

java
@Override
public void updateProcessStatus(String businessKey, Integer status) {
    Long id = Long.parseLong(businessKey);
    validateExists(id);
    OfficeDocReceiveDO updateObj = new OfficeDocReceiveDO();
    updateObj.setId(id);
    updateObj.setProcessStatus(status);

    if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(status)) {
        updateObj.setHandleStatus(HANDLE_STATUS_COMPLETED);  // 流程通过 → 已办结
    } else if (BpmProcessInstanceStatusEnum.RUNNING.getStatus().equals(status)) {
        updateObj.setHandleStatus(HANDLE_STATUS_PROCESSING); // 流程运行 → 办理中
    } else if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(status)
            || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(status)
            || BpmProcessInstanceStatusEnum.WITHDRAW.getStatus().equals(status)
            || BpmProcessInstanceStatusEnum.NOT_START.getStatus().equals(status)) {
        updateObj.setHandleStatus(HANDLE_STATUS_CLAIMED);    // 驳回/撤回 → 已签收
    }
    officeDocReceiveMapper.updateById(updateObj);
}

状态映射规则

BPM 流程状态办理状态业务含义
APPROVE(通过)已办结(3)流程走完,公文已办理完成
RUNNING(运行中)办理中(2)流程正在审批中
REJECT / CANCEL / WITHDRAW / NOT_START已签收(1)流程异常结束,回退到可重新提交状态

为什么驳回后回退到"已签收"而不是"待签收"? 因为签收是一个不可逆操作——一旦某人签收了这条收文,它就归属该用户。流程被驳回后,仍然由签收人负责修改重新提交。

5.4 自动生成收文的权限控制

在收文详情页加载数据时,有一段重要的权限控制逻辑:

typescript
async function loadData() {
  // ...
  const data = await getOfficeDocReceive(id);
  formData.value = { ...data };
  readonly.value = computeBusinessFormReadonly(
    props.viewType, props.isApproval,
    formData.value.processStatus as number,
  );

  // 自动生成的收文:未签收或非本人签收时只读且隐藏操作按钮
  if (
    formData.value.sendBillId &&
    (!formData.value.creator ||
      String(formData.value.creator) !== String(userStore.userInfo?.id))
  ) {
    readonly.value = true;
    hideFooter.value = true;
  }
  // ...
}

这段逻辑确保:

  • 未签收的收文!formData.value.creator):任何人都只能查看,不能操作
  • 他人签收的收文creator !== currentUser):同部门其他人只能查看,不能干预
  • 自己签收的收文:拥有完整的编辑和提交权限

六、FlowBillService:收文与发文的接入对比

收文和发文都实现了同一个 FlowBillService 接口,但它们的实现重点截然不同:

方法发文实现收文实现
getSupportedBillType()OA_OFFICE_DOC_SENDOA_OFFICE_DOC_RECEIVE
updateProcessStatus()仅更新 processStatus更新 processStatus + 同步 handleStatus
onProcessApproved()自动生成收文记录无(默认空实现)
deleteBill()删除发文 + 附件删除收文 + 附件

发文的核心联动在 onProcessApproved(审批通过后生成收文),收文的核心联动在 updateProcessStatus(流程状态与办理状态的双向同步)。同一个接口,不同的实现侧重——这就是面向接口编程的魅力。

事件驱动流转全景

Flowable 引擎 → BpmProcessInstanceEventListener → Spring ApplicationEvent
    → OaLocalNotificationListener → OaFlowBillServiceFactory
    → OfficeDocReceiveServiceImpl.updateProcessStatus()
    → 自动同步办理状态(待签收/已签收/办理中/已办结)

收文的事件驱动链路与发文完全一致——BPM 框架通过 Spring Event 机制,将流程状态变更事件分发到对应的业务服务。业务模块只需关注自己的状态同步逻辑,完全不需要关心 Flowable 的底层 API。


七、附件管理的双重模式

收文的附件管理根据数据来源分为两种模式:

7.1 发文同步附件(只读)

当收文来自发文自动生成时,附件从发文复制而来:

vue
<AttachmentList
  ref="attachmentListRef"
  v-model="formData.attachments"
  :readonly="readonly || !!formData.sendBillId"
  :max-count="10"
  :max-size="20"
  :hide-upload-button="true"
/>

注意 :readonly="readonly || !!formData.sendBillId" 这个判断——只要是发文生成的收文,附件始终只读,不允许增删。这保证了"原始发文附件的完整性"。

7.2 手动上传附件(可编辑)

手动创建的收文(sendBillId 为空)可以自由上传和管理附件:

vue
<template #extra>
  <Button
    v-if="!readonly && !formData.sendBillId"
    type="primary"
    @click="attachmentListRef?.handleTriggerUpload()"
  >
    上传附件
  </Button>
</template>

发文同步的附件是"原件"的副本,只读保护确保收文部门不会误删原始材料;手动创建的收文则给予完全的附件管理自由度。


八、字典数据:收文新增的办理状态字典

在发文已有的字典(密级、紧急程度、公开类别)基础上,收文新增了办理状态字典:

字典类型字典值使用场景
office_doc_handle_status待签收(0)、已签收(1)、办理中(2)、已办结(3)收文列表的"办理状态"列渲染
office_doc_secret_level公开、内部、秘密、机密、绝密密级选择(复用发文字典)
office_doc_urgency_level普通、急件、特急紧急程度(复用发文字典)

办理状态字典在列表页中通过 CellDict 渲染器展示:

typescript
{
  field: 'handleStatus',
  title: '办理状态',
  width: 100,
  cellRender: {
    name: 'CellDict',
    props: { type: 'office_doc_handle_status' },
  },
},

九、总结:收文模块的技术亮点

能力实现方式价值
发文-收文联动onProcessApproved 自动生成 + sendBillId 关联一次发文,多方自动收文
签收认领CAS 乐观锁 + 权限隔离先到先得,职责明确
办理状态机四态状态机与 BPM 流程联动业务状态与流程状态双向同步
节点级权限beforeApproval + 动态 disabled不同节点不同编辑能力
正式公文预览iframe + PDF / 关联发文跳转直接查看/下载正式公文
数据隔离companyId + receiveDeptId 过滤部门只看自己的收文
附件双模式发文同步只读 / 手动创建可编辑保护原件,灵活扩展
事件驱动解耦FlowBillService + Spring Event业务模块与 BPM 零耦合
多租户隔离全表带 tenant_idSaaS 场景开箱即用

十、快速体验

在线体验

  • 演示地址http://ruoyioffice.com/web/
  • 账号密码:admin / admin123
  • 操作路径:OA 协同办公 → 公文管理 → 公文收文

推荐体验路径

  1. 先到"公文发文"创建一条发文,主送 2 个部门
  2. 走完发文审批流程
  3. 到"公文收文"列表查看自动生成的收文记录
  4. 点击"签收"认领收文
  5. 提交收文,体验领导批示 → 承办办理 → 办结归档的完整流程

源码获取

平台仓库地址
GitCode(后端)https://gitcode.com/zhouzhongyan/ruoyi-office.git
GitCode(前端)https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git
GitHub(后端)https://github.com/yuqing2026/ruoyi-office.git

技术栈速览

层次技术版本
后端框架Spring Boot / Spring Cloud3.5
流程引擎Flowable7.0.1
前端框架Vue 3 + TypeScript3.5
UI 框架Vben Admin + Ant Design Vue5.x / 4.x

结语

如果说发文是公文流转的"起点",那么收文就是公文流转的"终点"。 从发文审批通过的那一刻起,系统自动为每个收文部门推送待签收记录;收文部门签收认领后,进入领导批示、承办办理、办结归档的完整流转——整个过程无需人工传递、无需线下沟通,系统自动驱动

RuoYi Office 的公文收文模块,用签收认领的"拉"模式对接发文的"推"模式,用四态状态机同步办理进度,用节点级权限控制保证数据的精准编辑——在"自动化"和"可控性"之间找到了恰当的平衡。

发文 + 收文的组合,让 RuoYi Office 的公文管理从"单向发布"升级为"闭环流转"。 如果你正在寻找一套完整的公文管理解决方案,两篇文章结合阅读,相信会给你不少启发。


💡 觉得有价值?

Star 仓库https://gitee.com/yqzy1688/ruoyi-office

💬 技术交流:添加微信 17156169080,备注「RuoYi Office」,加入技术交流群

📚 更多文章http://ruoyioffice.com