RuoyiOffice之 OA 公文收文管理——发文-收文自动联动、签收认领、领导批示、承办办理全流程拆解
🌐 文档地址:http://ruoyioffice.com | 📦 源码1:ruoyi-office-vben |📦 源码2:ruoyi-office |📦 源码3:ruoyi-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 的公文收文有两种创建方式,覆盖了不同的业务场景:
| 来源 | 说明 | 特点 |
|---|---|---|
| 发文自动生成 | 内部发文审批通过后系统自动创建 | 带 sendBillId、creator 为空(需签收)、数据从发文同步 |
| 手动创建 | 收文部门人员手动新建(类似外部收文登记) | 无 sendBillId、由创建人直接填写所有字段 |
为什么需要手动创建? 虽然大多数内部收文来自发文联动,但有时收文部门需要登记一些非系统流转的来文(如收到纸质文件需要在系统中登记办理)。手动创建提供了这种灵活性。
二、流程设计:从签收到归档的四级流转
公文收文的审批流程与发文截然不同——发文聚焦"审核签发",收文聚焦"批示办理"。
2.1 审批流程节点

▲ 公文收文四级流转:签收后进入领导批示环节,批示指定承办人后由承办人办理,最终办结归档
| 节点 | 角色 | 核心操作 | 设计考量 |
|---|---|---|---|
| 收文签收 | 收文部门文员 | 确认签收、补充收文信息 | 进入流程的第一步,明确责任人 |
| 领导批示 | 部门领导 | 批示处理意见、指定承办人 | 领导可编辑"领导批示"字段,指明办理方向 |
| 承办部门办理 | 承办人 | 实际办理公文事项、填写办理结果 | 承办人可编辑"办理结果"字段,记录办理过程 |
| 办结归档 | 部门文员 | 确认办结、归档存查 | 最终确认环节,流程结束后状态变为"已办结" |
2.2 节点级业务定制
与发文类似,收文流程也在关键节点嵌入了精准的业务逻辑:
领导批示节点:只有在此节点,"领导批示"文本框才可编辑。领导的批示意见通常包括"请XX部门阅处"、"同意,请尽快落实"等指示。审批前自动保存表单,确保批示内容不丢失。
承办部门办理节点:只有在此节点,"办理结果"文本框才可编辑。承办人需要记录实际办理情况。审批前同样自动保存。
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 签收认领:收文流转的"第一公里"
对于发文自动生成的收文,最关键的第一步是签收认领。签收的本质是"将当前用户设为该收文记录的负责人"。
在列表页中,未签收的收文显示"签收"按钮,已签收的显示"已签收":
<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,
},
]"
/>签收确认弹窗:
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();
},
});
}签收设计的三个关键约束:
- 只有发文生成的收文才需要签收:通过
!!row.sendBillId判断,手动创建的收文已有创建人,无需签收 - 先到先得:签收使用了乐观锁机制(
isNull("creator")),同一条收文只有第一个签收的人能成功 - 签收不可逆:一旦签收,该收文即归属该用户,不可再转让
3.2 收文表单字段设计
收文表单的字段设计分为两大类:发文同步字段和收文专属字段。
| 字段 | 组件 | 发文生成时 | 手动创建时 | 特殊说明 |
|---|---|---|---|---|
| 收文类型 | Select(固定选项) | 自动填充(主送/抄送) | 不显示 | disabled,不可编辑 |
| 发文单位 | Input(只读) | 从发文同步 | 不显示 | 仅发文生成的收文可见 |
| 发文日期 | Input(只读) | 从发文同步 | 不显示 | 仅发文生成的收文可见 |
| 签发人 | Input(只读) | 从发文同步 | 不显示 | 仅发文生成的收文可见 |
| 公开类别 | Select(只读) | 从发文同步 | 不显示 | 仅发文生成的收文可见 |
| 来文字号 | Input | 从发文同步 | 手动填写 | 对应发文的 docNumber |
| 公文标题 | Input(全宽) | 从发文同步 | 手动填写 | 必填 |
| 密级 | Select(字典驱动) | 从发文同步 | 手动选择 | — |
| 紧急程度 | Select(字典驱动) | 从发文同步 | 手动选择 | — |
| 收文日期 | DatePicker | 自动填充当天 | 自动填充当天 | 必填 |
| 收文部门 | Input | 自动填充 | 手动填写 | — |
| 主办人 | Input | 手动指定 | 手动指定 | 通常在领导批示时指定 |
| 领导批示 | Textarea(全宽) | — | — | 仅"领导批示"节点可编辑 |
| 办理结果 | Textarea(全宽) | — | — | 仅"承办部门办理"节点可编辑 |
| 办理期限 | DatePicker | 手动选择 | 手动选择 | 带时分秒 |
| 内容摘要 | Textarea(全宽) | 从发文同步 | 手动填写 | — |
表单字段的动态显隐是一个设计亮点。发文同步字段(发文单位、发文日期、签发人、公开类别)通过 dependencies 配置,只有当 sendBillId 存在时才显示:
{
fieldName: 'sendDeptName',
label: '发文单位',
component: 'Input',
componentProps: { disabled: true },
dependencies: {
triggerFields: ['sendBillId'],
show: (values) => !!values.sendBillId,
},
},领导批示和办理结果的节点级权限控制同样值得关注。它们的 disabled 属性是一个函数,根据当前审批节点名称动态判断:
{
fieldName: 'leaderOpinion',
label: '领导批示',
component: 'Textarea',
formItemClass: 'col-span-full',
componentProps: {
placeholder: '请输入领导批示',
disabled: () => {
if (nodeKeyName?.value === '领导批示') return false;
return readonly?.value;
},
},
},这个设计让同一个表单在不同审批节点呈现不同的编辑能力——领导看到的表单中只有"领导批示"可以编辑,承办人看到的表单中只有"办理结果"可以编辑,其他字段一律只读。
3.3 正式公文预览与关联发文查看
收文详情页的"正式公文"区域根据数据来源呈现三种状态:
状态一:发文生成 + 已有 PDF
<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 未生成
<template v-else-if="formData.sendBillId">
<Alert type="info" message="正式公文尚未生成,请联系办公室文秘" />
<Button type="link" @click="handleViewSendBill">查看关联发文单</Button>
</template>这种情况通常出现在发文流程尚未走到"办公室编号发文"节点(PDF 在该节点生成)。
状态三:手动创建的收文
<template v-else>
<div class="flex flex-col items-center">
<p>请上传正式公文文件(支持 PDF、Word 格式)</p>
<Button type="primary" @click="handleUploadDoc">上传正式公文</Button>
</div>
</template>手动创建的收文没有关联发文,用户可以自行上传正式公文文件。
关联发文跳转的实现非常简洁:
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 条件显示 |
收文列表的数据隔离值得注意——查询时自动附加 companyId 和 receiveDeptId 过滤条件,确保用户只能看到自己公司、自己部门的收文:
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_id | template_id(继承) | 收文继承发文的模板引用 |
| 文号 | doc_mark + doc_year + doc_sequence → doc_number | 仅 doc_number(来文字号) | 收文无需拆分文号组成 |
| 部门 | send_dept_id/name、main_receive_dept_ids/names、cc_dept_ids/names | receive_dept_id/name | 发文一对多,收文单一部门 |
| 签发 | signer_id/name | 无(通过关联发文查看) | 签发人信息存储在发文 |
| 办理 | 无 | handler_id/name、leader_opinion、handle_result、handle_deadline | 收文特有的办理流转字段 |
| 办理状态 | 无 | handle_status(0-3) | 收文特有的四态状态机 |
doc_file_url(服务端生成) | doc_file_url(从发文同步或手动上传) | 收文的 PDF 来自发文同步 | |
| 关联 | 无 | send_bill_id → 发文 | 核心关联字段 |
4.3 设计思考:为什么收文不复用发文的预览组件?
在发文模块中,我们实现了一个精美的 DocPreview 实时预览组件,支持 GB/T 9704 标准渲染。那为什么收文没有复用它?
原因很务实:
- 收文的核心诉求不同:发文需要"边填边看"的实时预览体验(因为发文人在构建公文内容),而收文部门只需要"查看已生成的正式公文"
- 正式公文 PDF 已生成:发文审批通过时 PDF 已经生成,收文直接用 iframe 展示 PDF 即可,无需再次渲染
- 手动创建的收文无模板:手动登记的收文没有关联套红模板,无法使用
DocPreview渲染
架构设计的原则之一是"避免过度设计"。收文用 iframe + PDF 已经完美满足需求,没必要为了"技术一致性"而强行复用预览组件。
五、核心后端代码解析
5.1 签收认领:CAS 风格的乐观锁
签收是收文模块最关键的原子操作——它必须保证"同一条收文只能被一个人签收":
@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);
}
}三层防护:
- 业务校验:只有发文生成的收文(
sendBillId != null)才允许签收操作 - 乐观锁:UPDATE 条件中
isNull("creator")确保只有第一个签收的人能成功 - 异常提示:如果
updated == 0(已被他人签收),抛出"已被签收"的友好提示
这种 CAS(Compare-And-Swap)风格的实现方式比悲观锁(
SELECT FOR UPDATE)更轻量,适合签收这种低并发但需要原子性的场景。
5.2 收文提交:发起 BPM 流程
签收后,收文部门的文员可以提交收文,进入 BPM 审批流程:
@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();
}关键设计点:
- 状态联动:提交时同时将
processStatus设为运行中、handleStatus设为"办理中" - 标准 BPM 接入:使用与发文完全相同的
BpmProcessVariableUtils.buildBillVariables()构建流程变量 - 流程定义 Key:通过
OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE.getProcessDefinitionKey()获取,与菜单配置的流程定义 Key 一致
5.3 FlowBillService:流程状态与办理状态的联动
收文服务同样实现了 FlowBillService<OaBillTypeEnum> 接口,但它的 updateProcessStatus 方法比发文更复杂——因为需要同步维护办理状态:
@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 自动生成收文的权限控制
在收文详情页加载数据时,有一段重要的权限控制逻辑:
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_SEND | OA_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 发文同步附件(只读)
当收文来自发文自动生成时,附件从发文复制而来:
<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 为空)可以自由上传和管理附件:
<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 渲染器展示:
{
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_id | SaaS 场景开箱即用 |
十、快速体验
在线体验
- 演示地址:http://ruoyioffice.com/web/
- 账号密码:admin / admin123
- 操作路径:OA 协同办公 → 公文管理 → 公文收文
推荐体验路径
- 先到"公文发文"创建一条发文,主送 2 个部门
- 走完发文审批流程
- 到"公文收文"列表查看自动生成的收文记录
- 点击"签收"认领收文
- 提交收文,体验领导批示 → 承办办理 → 办结归档的完整流程
源码获取
| 平台 | 仓库地址 |
|---|---|
| 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 Cloud | 3.5 |
| 流程引擎 | Flowable | 7.0.1 |
| 前端框架 | Vue 3 + TypeScript | 3.5 |
| UI 框架 | Vben Admin + Ant Design Vue | 5.x / 4.x |
结语
如果说发文是公文流转的"起点",那么收文就是公文流转的"终点"。 从发文审批通过的那一刻起,系统自动为每个收文部门推送待签收记录;收文部门签收认领后,进入领导批示、承办办理、办结归档的完整流转——整个过程无需人工传递、无需线下沟通,系统自动驱动。
RuoYi Office 的公文收文模块,用签收认领的"拉"模式对接发文的"推"模式,用四态状态机同步办理进度,用节点级权限控制保证数据的精准编辑——在"自动化"和"可控性"之间找到了恰当的平衡。
发文 + 收文的组合,让 RuoYi Office 的公文管理从"单向发布"升级为"闭环流转"。 如果你正在寻找一套完整的公文管理解决方案,两篇文章结合阅读,相信会给你不少启发。
💡 觉得有价值?
⭐ Star 仓库:https://gitee.com/yqzy1688/ruoyi-office
💬 技术交流:添加微信 17156169080,备注「RuoYi Office」,加入技术交流群
📚 更多文章:http://ruoyioffice.com