SpringBoot+Vue3 实现 OA 公文外来文与归档台账:外部收文、BPM办理、三类公文统一归档
文档地址:http://ruoyioffice.com | 源码1:ruoyi-office-vben | 源码2:ruoyi-office | 源码3:ruoyi-office
在前两篇公文系列文章中,我们分别拆解了公文发文和内部收文。发文解决的是"企业内部正式文件如何产生",收文解决的是"内部发文如何分发到部门办理"。但真实企业办公里,还有一类更容易被忽视的公文:来自上级单位、监管部门、合作单位、外部机构的外来文件。它们不是系统内部发起,却必须被登记、批示、办理、留痕和归档。本文聚焦 RuoYi Office 的 外部收文 + 公文归档台账 设计,看看如何用 Spring Boot + Flowable + Vue3 把三类公文统一收口。
引言:外来文为什么不能只做"附件上传"?
很多企业第一次做 OA 公文模块时,会把外来文简化成一个"文件上传台账":填一下来文单位、标题、上传附件,然后结束。这个方案看似轻巧,但一旦进入真实管理场景,就会暴露出几个问题:
| 业务问题 | 简化方案的后果 | 系统应承担的能力 |
|---|---|---|
| 来文需要领导批示 | 附件上传后无法体现"谁批示、批示什么" | 支持领导批示节点与意见留痕 |
| 来文需要承办部门处理 | 台账只能记录文件,不能追踪办理进展 | 支持承办办理、办理结果、办理期限 |
| 来文需要流程闭环 | 文件状态停留在"已上传",无法区分办理中和已办结 | 建立业务状态 + BPM 状态联动 |
| 来文最终要归档 | 外部收文和内部发文、收文分散在多个列表 | 建立统一归档台账 |
| 管理层要检索历史公文 | 只能按文件名查,缺少文号、密级、责任部门等维度 | 提供结构化检索和导出 |
所以,外部收文不应该只是"上传文件",而应该是一张具备流程属性的业务单据:
text
外部单位来文 → 文秘登记 → 领导批示 → 承办部门办理 → 办结归档 → 统一台账检索
RuoYi Office 的设计重点正是在这里:外部收文独立于内部发文体系,但最终和发文、内部收文一起进入公文归档台账,形成完整的企业公文资产库。
一、业务场景:三类公文如何统一管理?
1.1 公文体系中的三类来源
在 RuoYi Office 中,公文归档台账聚合的不是单一表,而是三类来源:
| 类型 | 来源 | 典型场景 | 归档条件 |
|---|---|---|---|
| 发文 | 企业内部起草并审批签发 | 公司制度、会议纪要、任命通知、对外函件 | processStatus = APPROVE |
| 收文 | 内部发文按部门自动生成,或部门手工登记 | 主送部门签收、领导批示、承办办理 | handleStatus = 3 |
| 外部收文 | 外部单位、监管机构、合作方来文 | 上级通知、监管文件、合作函件、纸质文件登记 | processStatus = APPROVE |
发文强调"生产正式文件",收文强调"内部传达办理",外部收文强调"外部文件进入企业后的处理闭环"。三者的业务动作不同,但最终都要落到同一个问题:哪些公文已经形成可追溯的正式记录?
1.2 外部收文的典型使用场景
外部收文在机关、国企、集团公司、园区企业中非常常见:
- 上级单位下发通知,需要办公室登记后交由领导批示。
- 监管部门发送检查整改文件,需要责任部门在期限内办理。
- 合作单位发送合同函、联系函、会议纪要,需要内部留痕。
- 纸质文件送达企业,需要扫描上传并纳入电子台账。
- 历史公文需要按文号、标题、密级、责任部门进行检索和导出。
如果没有系统承接,这类文件通常散落在邮箱、微信群、纸质档案柜、个人电脑里。短期看还能靠人记,时间一长就会出现"找不到、说不清、追不回"的问题。
1.3 RuoYi Office 的解决思路
RuoYi Office 没有把外来文做成一个孤立表单,而是把它放进完整的公文体系里:

▲ 外部收文与归档流程:外部来文登记后进入 BPM 办理流程,流程通过后与发文、内部收文一起进入归档台账
核心思路可以概括为三句话:
- 外部收文是流程单据:登记不是终点,提交后进入 BPM 流程。
- 办理状态由流程驱动:待登记、办理中、已办结不是人工随便改,而是由流程状态回调统一维护。
- 归档台账是查询层聚合:不强行把三类公文塞进一张大表,而是在归档服务中统一投影、排序和分页。
二、系统设计:外部收文与归档台账的边界
2.1 模块分工
这次拆解涉及两个核心后端服务和三组前端页面:
| 层级 | 文件/页面 | 职责 |
|---|---|---|
| 后端服务 | OfficeDocOutsideServiceImpl.java |
外部收文保存、提交、删除、附件维护、流程状态回调 |
| 后端服务 | OfficeDocArchiveServiceImpl.java |
聚合发文、收文、外部收文,输出统一归档列表 |
| PC 列表页 | views/oa/officedoc/outside/list |
外部收文查询、新建、导出、删除 |
| PC 详情页 | views/oa/officedoc/outside/info |
外部收文登记、保存、提交、撤回、上传正式公文和附件 |
| PC 台账页 | views/oa/officedoc/archive/list |
三类公文统一检索、查看详情、PDF 预览、导出 |
这套边界很清晰:外部收文负责"产生并办理一条来文",归档台账负责"把已经完成的公文汇总出来"。
2.2 为什么归档不直接建一张汇总表?
公文归档可以有两种实现方案:
| 方案 | 做法 | 优点 | 风险 |
|---|---|---|---|
| 物理归档表 | 流程结束时复制一份数据到 archive 表 |
查询快,结构统一 | 数据冗余,回写一致性复杂 |
| 查询层聚合 | 查询时分别查三类来源并合并 | 无冗余,来源数据始终真实 | 大数据量下需要优化分页 |
当前 RuoYi Office 采用的是第二种:查询层聚合。
原因很务实:公文归档台账本质上是一个管理查询视图,数据源仍然是发文、收文、外部收文各自的业务表。这样既避免了"业务表改了,归档表没同步"的一致性问题,也能保留每类公文的原始详情入口。
2.3 状态设计:BPM 状态与办理状态分层
外部收文有两个容易混淆的状态:
| 状态字段 | 含义 | 面向对象 | 示例 |
|---|---|---|---|
processStatus |
BPM 流程实例状态 | 流程引擎、审批中心 | 未开始、运行中、通过、驳回、撤回 |
handleStatus |
外部收文业务办理状态 | 业务人员、列表台账 | 待登记、办理中、已办结 |
真实实现中,外部收文办理状态有三个核心值:
| 状态码 | 常量 | 含义 | 触发场景 |
|---|---|---|---|
0 |
HANDLE_STATUS_PENDING |
待登记 | 新建未提交、驳回、取消、撤回、未开始 |
2 |
HANDLE_STATUS_PROCESSING |
办理中 | 提交 BPM 或流程运行中 |
3 |
HANDLE_STATUS_COMPLETED |
已办结 | BPM 审批通过 |
这种分层设计的好处是:BPM 可以保持通用状态,业务页面可以展示更贴近用户语言的"办理状态"。
三、流程设计:从登记到归档的闭环
3.1 外部收文流程节点
外部收文的流程和内部收文类似,重点不是"签发",而是"批示与办理":
text
文秘登记 → 领导批示 → 承办部门办理 → 办结确认 → 流程结束
| 节点 | 角色 | 核心操作 | 数据落点 |
|---|---|---|---|
| 文秘登记 | 办公室/综合部 | 填写来文单位、文号、标题、密级、收文日期,上传附件 | 基础字段、附件 |
| 领导批示 | 部门/公司领导 | 填写批示意见,明确处理方向 | leaderOpinion |
| 承办部门办理 | 责任部门/主办人 | 填写办理结果,补充处理情况 | handleResult |
| 办结确认 | 文秘/流程节点配置角色 | 确认办理完成 | BPM 审批通过 |
外部收文没有内部发文的"套红模板"和"生成正式 PDF"动作,因为它的正式文件通常来自外部单位,系统只需要保存原文件或扫描件,并记录企业内部办理过程。
3.2 状态流转
外部收文的状态流转可以这样理解:
text
待登记(0)
├─ 提交 BPM → 办理中(2)
├─ 流程通过 → 已办结(3)
└─ 驳回/取消/撤回/未开始 → 待登记(0)
注意这里有一个关键点:提交时直接把业务状态置为"办理中"。这意味着只要单据进入 BPM,就不再是普通草稿,而是一条正在办理的外来文。
3.3 节点级保存
前端详情页在审批模式下会识别当前节点名称:
- 当前节点是"领导批示"时,允许编辑
leaderOpinion。 - 当前节点是"承办部门办理"时,允许编辑
handleResult。 - 审批通过前调用保存接口,避免批示或办理结果只停留在前端表单。
这延续了 RuoYi Office 公文收文模块的节点级业务定制思路:流程节点不只是"同意/拒绝"的按钮,还可以承载业务字段的编辑权限。
四、PC 端功能实现:外部收文列表、详情、归档台账
4.1 外部收文列表:面向登记与办理跟踪
外部收文列表页位于 views/oa/officedoc/outside/list,核心能力包括:

▲ 外部收文列表:支持按公文标题、来文单位、流程状态查询,展示办理状态和流程状态
列表页的字段设计很贴近文秘日常工作:
| 字段 | 说明 |
|---|---|
| 单据编号 | 系统生成的外部收文业务编号 |
| 公文标题 | 外部来文的标题 |
| 来文单位 | 文件来源单位,如上级单位、合作方、监管部门 |
| 来文字号 | 外部文件自带文号 |
| 密级/紧急程度 | 便于优先级管理 |
| 收文部门 | 文件进入企业后的责任部门 |
| 办理状态 | 待登记、办理中、已办结 |
| 流程状态 | BPM 流程状态 |
列表页的删除逻辑也遵循 BPM 单据规范:只有处于允许删除的流程状态时,删除按钮才可用;运行中的流程单据不可直接删除。
4.2 外部收文详情:登记、提交、上传正式文件
外部收文详情页位于 views/oa/officedoc/outside/info,它不是简单表单,而是一个流程表单页面:
| 区域 | 功能 | 说明 |
|---|---|---|
| 基础信息 | 来文单位、来文字号、公文标题、密级、紧急程度、收文日期 | 登记外来文的结构化元数据 |
| 办理信息 | 收文部门、主办人、领导批示、办理结果、办理期限 | 支撑领导批示和承办办理 |
| 正式公文 | 上传 PDF、Word、扫描件 | 保存外部来文原件 |
| 附件列表 | 上传补充材料 | 保存过程材料、相关附件 |
| 操作按钮 | 保存、提交、撤回、删除 | 与 BPM 状态联动 |

详情页初始化新单据时,会自动带入当前用户的公司、部门、创建人、默认密级、紧急程度和收文日期:
typescript
const now = new Date();
formData.value = {
creator: userStore.userInfo?.id,
companyId: userStore.userInfo?.companyId || 0,
companyName: userStore.userInfo?.companyName || '',
deptId: userStore.userInfo?.deptId || 0,
deptName: userStore.userInfo?.deptName || '',
processStatus: BpmProcessInstanceStatus.NOT_START,
secretLevel: 0,
urgencyLevel: 0,
handleStatus: 0,
sourceOrg: '',
title: '',
receiveDate: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`,
docFileUrl: '',
attachments: [],
};
这种默认值设计的价值在于减少文秘录入成本:系统知道当前用户属于哪个公司、哪个部门,没必要让用户每次重复填写。
4.3 归档台账:三类公文统一入口
归档台账页面位于 views/oa/officedoc/archive/list,它面向的是管理查询而不是单据办理:

▲ 公文归档台账:统一展示发文、收文、外部收文,支持按归档类型、标题、文号、密级、归档时间查询
归档台账的前端设计有三个细节:
| 设计点 | 实现方式 | 用户价值 |
|---|---|---|
| 归档类型区分 | 发文蓝色、收文绿色、外部收文橙色标签 | 一眼识别来源 |
| 详情跳转 | 根据 archiveType 跳转到发文、收文、外部收文详情页 |
保留原始业务上下文 |
| PDF 预览 | 有 docFileUrl 时展示"预览"按钮,用弹窗 iframe 打开 |
不离开台账即可查看正式文件 |
前端跳转逻辑非常直接:
typescript
function handleViewDetail(row: OfficeDocArchiveApi.OfficeDocArchive) {
const pathMap: Record<string, string> = {
send: '/oa/officedoc/send-info',
receive: '/oa/officedoc/receive-info',
outside: '/oa/officedoc/outside-info',
};
const path = pathMap[row.archiveType || ''];
if (path) {
router.push({ path, query: { id: row.id, viewType: 'done' } });
}
}
这说明归档台账不是复制出来的"静态档案",而是一个统一入口。用户在台账中看到的是归档视图,点击进去仍然能回到原始业务单据。
五、后端核心代码:提交、回调、聚合
5.1 外部收文提交:保存单据并发起 BPM
外部收文提交的关键动作有四步:
- 如果没有单号,生成业务单据编号。
- 保存外部收文主表,并将
processStatus设为运行中。 - 将
handleStatus设为办理中。 - 提交 BPM 流程实例,回写
processInstanceId,保存附件。
java
@Override
public Long submitOfficeDocOutside(OfficeDocOutsideSaveReqVO saveReqVO) {
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.OA, OaBillTypeEnum.OA_OFFICE_DOC_OUTSIDE));
}
OfficeDocOutsideDO docOutside = BeanUtils.toBean(saveReqVO, OfficeDocOutsideDO.class)
.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus())
.setHandleStatus(HANDLE_STATUS_PROCESSING);
officeDocOutsideMapper.insertOrUpdate(docOutside);
Map<String, Object> variables = BpmProcessVariableUtils.buildBillVariables(saveReqVO);
Long submitUserId = StringUtils.isNotBlank(saveReqVO.getCreator())
? Long.valueOf(saveReqVO.getCreator()) : SecurityFrameworkUtils.getLoginUserId();
String processInstanceId = processInstanceApi.submitProcessInstance(submitUserId,
new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(OaBillTypeEnum.OA_OFFICE_DOC_OUTSIDE.getProcessDefinitionKey())
.setVariables(variables).setBusinessKey(String.valueOf(docOutside.getId()))
).getCheckedData();
officeDocOutsideMapper.updateById(new OfficeDocOutsideDO().setId(docOutside.getId())
.setProcessInstanceId(processInstanceId));
if (saveReqVO.getAttachments() != null) {
attachmentService.saveAttachmentList(OaBillTypeEnum.OA_OFFICE_DOC_OUTSIDE.getTypeCode(),
docOutside.getId(), saveReqVO.getAttachments());
}
return docOutside.getId();
}
这段代码体现了 RuoYi Office 流程单据的通用模式:业务表先落库,BPM 以业务表 ID 作为 businessKey,流程实例启动后再回写到业务表。
5.2 流程状态回调:BPM 驱动办理状态
外部收文实现了 FlowBillService<OaBillTypeEnum>,因此流程状态变化时会回调业务服务。核心逻辑如下:
java
@Override
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
log.info("[updateProcessStatus] 外部收文,id: {}, status: {}", id, status);
validateExists(id);
OfficeDocOutsideDO updateObj = new OfficeDocOutsideDO();
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_PENDING);
}
officeDocOutsideMapper.updateById(updateObj);
}
这段代码是外部收文状态机的核心。它没有让前端直接传"已办结",而是把办理状态收敛到后端流程回调里。这样可以避免用户手动篡改状态,也能确保审批中心和业务列表的状态一致。
5.3 归档聚合查询:三类来源合并后分页
归档服务的入口是 getArchivePage。它根据查询条件决定是否查询发文、收文、外部收文,然后把结果合并、按归档时间倒序排序,并在内存中分页:
java
@Override
public PageResult<OfficeDocArchiveRespVO> getArchivePage(OfficeDocArchivePageReqVO reqVO) {
String archiveType = reqVO.getArchiveType();
List<OfficeDocArchiveRespVO> allRecords = new ArrayList<>();
if (archiveType == null || TYPE_SEND.equals(archiveType)) {
allRecords.addAll(querySendArchives(reqVO));
}
if (archiveType == null || TYPE_RECEIVE.equals(archiveType)) {
allRecords.addAll(queryReceiveArchives(reqVO));
}
if (archiveType == null || TYPE_OUTSIDE.equals(archiveType)) {
allRecords.addAll(queryOutsideArchives(reqVO));
}
allRecords.sort(Comparator.comparing(
OfficeDocArchiveRespVO::getArchiveTime,
Comparator.nullsLast(Comparator.reverseOrder())));
int pageNo = reqVO.getPageNo() != null ? reqVO.getPageNo() : 1;
int pageSize = reqVO.getPageSize() != null ? reqVO.getPageSize() : 20;
int start = (pageNo - 1) * pageSize;
int end = Math.min(start + pageSize, allRecords.size());
List<OfficeDocArchiveRespVO> pageList = start < allRecords.size()
? allRecords.subList(start, end) : new ArrayList<>();
return new PageResult<>(pageList, (long) allRecords.size());
}
这就是"统一归档视图"的核心:不是让三类公文改变自己的业务模型,而是把它们投影成统一的 OfficeDocArchiveRespVO。
5.4 外部收文归档条件
外部收文进入归档台账的条件是 processStatus = APPROVE。归档投影时,会把外部收文字段映射成统一字段:
java
private List<OfficeDocArchiveRespVO> queryOutsideArchives(OfficeDocArchivePageReqVO reqVO) {
LambdaQueryWrapperX<OfficeDocOutsideDO> wrapper = new LambdaQueryWrapperX<OfficeDocOutsideDO>()
.eq(OfficeDocOutsideDO::getProcessStatus, BpmProcessInstanceStatusEnum.APPROVE.getStatus())
.eqIfPresent(OfficeDocOutsideDO::getCompanyId, reqVO.getCompanyId())
.likeIfPresent(OfficeDocOutsideDO::getTitle, reqVO.getTitle())
.likeIfPresent(OfficeDocOutsideDO::getDocNumber, reqVO.getDocNumber())
.eqIfPresent(OfficeDocOutsideDO::getSecretLevel, reqVO.getSecretLevel())
.betweenIfPresent(OfficeDocOutsideDO::getUpdateTime, reqVO.getArchiveTime());
return officeDocOutsideMapper.selectList(wrapper).stream().map(out -> {
OfficeDocArchiveRespVO vo = new OfficeDocArchiveRespVO();
vo.setId(out.getId());
vo.setArchiveType(TYPE_OUTSIDE);
vo.setArchiveTypeName("外部收文");
vo.setBillCode(out.getBillCode());
vo.setDocNumber(out.getDocNumber());
vo.setTitle(out.getTitle());
vo.setDocDate(out.getReceiveDate());
vo.setDeptName(out.getReceiveDeptName());
vo.setHandlerName(out.getHandlerName());
vo.setArchiveTime(out.getUpdateTime());
return vo;
}).toList();
}
注意这里外部收文的 docDate 对应的是 receiveDate,deptName 对应的是 receiveDeptName,handlerName 对应的是 handlerName。统一字段名不代表丢失业务语义,而是在归档视图中把不同来源转换成可比较、可展示的共同语言。
六、数据结构:外部收文与归档视图
6.1 外部收文主表字段
外部收文主表对应 OfficeDocOutsideDO,表名为 oa_office_doc_outside。核心字段可以分为五组:
| 字段 | 类型含义 | 说明 |
|---|---|---|
id |
主键 | 外部收文记录 ID |
billCode |
单据编号 | 系统生成,作为业务单号 |
processInstanceId |
流程实例 ID | 关联 BPM 流程实例 |
processStatus |
流程状态 | BPM 状态 |
handleStatus |
办理状态 | 待登记、办理中、已办结 |
| 字段 | 类型含义 | 说明 |
|---|---|---|
sourceOrg |
来文单位 | 外部文件来源单位 |
docNumber |
来文字号 | 外部公文自带文号 |
title |
公文标题 | 外部来文标题 |
secretLevel |
密级 | 公开、内部、秘密等 |
urgencyLevel |
紧急程度 | 普通、紧急等 |
receiveDate |
收文日期 | 企业收到文件的日期 |
| 字段 | 类型含义 | 说明 |
|---|---|---|
receiveDeptId / receiveDeptName |
收文部门 | 负责接收和办理的部门 |
handlerId / handlerName |
主办人 | 具体承办人 |
leaderOpinion |
领导批示 | 领导处理意见 |
handleResult |
办理结果 | 承办部门处理结果 |
handleDeadline |
办理期限 | 用于督办和时效管理 |
| 字段 | 类型含义 | 说明 |
|---|---|---|
content |
内容摘要 | 文件主要内容 |
docFileUrl |
正式公文地址 | 外部文件 PDF、Word 或扫描件 |
companyId / companyName |
公司信息 | 多组织隔离与展示 |
deptId / deptName |
创建部门 | 登记人所在部门 |
remark |
备注 | 补充说明 |
6.2 归档响应视图
归档台账使用 OfficeDocArchiveRespVO 作为统一返回结构:
| 字段 | 统一含义 | 发文映射 | 收文映射 | 外部收文映射 |
|---|---|---|---|---|
id |
原始记录 ID | 发文 ID | 收文 ID | 外部收文 ID |
archiveType |
归档类型 | send |
receive |
outside |
archiveTypeName |
类型名称 | 发文 | 收文 | 外部收文 |
billCode |
单据编号 | 发文单号 | 收文单号 | 外部收文单号 |
docNumber |
文号 | 发文字号 | 来文字号 | 来文字号 |
title |
公文标题 | 发文标题 | 收文标题 | 外部来文标题 |
docDate |
公文日期 | 发文日期 | 收文日期 | 收文日期 |
deptName |
责任部门 | 发文部门 | 收文部门 | 收文部门 |
handlerName |
经办人 | 签发人 | 主办人 | 主办人 |
archiveTime |
归档时间 | 更新时间 | 更新时间 | 更新时间 |
这张视图表不是数据库表,而是"查询返回模型"。它的价值在于把不同来源的字段压平到一套列表结构,前端不需要关心每类公文背后的表结构差异。
6.3 附件设计
外部收文附件没有直接塞进主表,而是通过通用附件服务维护:
| 附件类型 | 存储方式 | 说明 |
|---|---|---|
| 正式公文 | docFileUrl |
适合单个主文件,如 PDF、Word、扫描件 |
| 补充附件 | AttachmentService |
适合多个佐证材料、过程附件 |
这种设计区分了"主文件"和"附件"。主文件用于台账预览和正式文件展示,附件用于保存过程材料。
七、RuoyiOffice 创新设计
7.1 业务状态不等于流程状态
很多系统会直接把 BPM 状态展示给业务用户,比如"运行中、已通过、已驳回"。这对开发者很清楚,但对文秘和业务部门不够友好。
RuoYi Office 做了一层业务状态映射:
| BPM 状态 | 外部收文办理状态 | 用户理解 |
|---|---|---|
| 未开始、驳回、取消、撤回 | 待登记 | 还没有形成有效办理流程 |
| 运行中 | 办理中 | 已进入内部处理 |
| 通过 | 已办结 | 文件处理完成,可归档 |
这样列表页既能展示流程状态,也能展示办理状态,既满足审批系统的准确性,也满足业务用户的可读性。
7.2 归档台账不是"复制数据",而是"统一视图"
传统 OA 往往会在归档时复制一份数据,久而久之就出现两份数据不一致的问题。RuoYi Office 当前采用查询层聚合,保证归档台账看到的是业务源数据的最新状态。
这对中小企业很友好:表结构更简单,维护成本更低,数据链路更容易理解。
7.3 三类公文保留各自详情入口
归档台账中点击详情,不是进入一个抽象的"归档详情页",而是根据类型跳回原始单据:
| 归档类型 | 详情入口 |
|---|---|
| 发文 | /oa/officedoc/send-info |
| 收文 | /oa/officedoc/receive-info |
| 外部收文 | /oa/officedoc/outside-info |
这意味着归档台账既是统一检索入口,又不牺牲各类公文自己的业务上下文。
7.4 PC 端与 BPM 审批中心共用表单
外部收文详情页既可以从业务列表打开,也可以在 BPM 审批中心作为流程表单打开。通过 props.isApproval、props.nodeKeyName、viewType 等参数,页面可以自动判断当前是编辑、查看还是审批场景。
这套设计让业务页面和审批页面复用同一份 Vue 组件,避免维护两套表单。
7.5 附件与流程生命周期联动
外部收文删除时,不仅删除主表,还会清理流程实例和附件:
- 如果存在
processInstanceId,尝试删除 BPM 流程实例。 - 调用附件服务删除业务附件。
- 删除外部收文主记录。
这避免了"单据删了,流程还在;附件没人引用,却占用存储"的数据垃圾问题。
八、技术亮点总结
| 设计要点 | 实现方式 | 业务价值 |
|---|---|---|
| 外部收文流程化 | 提交时创建 BPM 流程实例 | 外来文不止登记,还能办理闭环 |
| 状态分层 | processStatus + handleStatus |
兼顾流程准确性和业务可读性 |
| 状态回调 | FlowBillService.updateProcessStatus |
BPM 状态变化自动同步业务状态 |
| 附件管理 | AttachmentService + docFileUrl |
区分正式文件和补充材料 |
| 归档聚合 | 发文、收文、外部收文分别查询后合并 | 保留源数据,不额外复制 |
| 统一响应 VO | OfficeDocArchiveRespVO |
前端列表无需感知多表差异 |
| 类型化跳转 | archiveType 映射详情路径 |
台账可回溯原始单据 |
| PDF 预览 | docFileUrl + iframe 弹窗 |
台账页面快速查看正式文件 |
| 多组织过滤 | companyId、deptId 条件 |
支持集团/多部门数据隔离 |
| 导出能力 | 列表导出 Excel | 满足线下档案移交和统计 |
九、快速体验路径
如果你想在系统中完整体验外部收文与归档台账,可以按下面的顺序操作:
- 进入 OA 办公 → 公文管理 → 外部收文。
- 点击"新增外部收文",填写来文单位、来文字号、公文标题、收文日期。
- 上传正式公文或附件,点击保存,确认草稿数据正常。
- 点击提交,外部收文进入 BPM 办理流程,办理状态变为"办理中"。
- 在审批中心完成领导批示和承办办理。
- 流程审批通过后,外部收文办理状态变为"已办结"。
- 进入 OA 办公 → 公文管理 → 公文归档台账。
- 选择归档类型"外部收文",查看刚才办结的记录,也可以切换"全部"查看发文、收文、外部收文混合台账。
源码模块对应关系如下:
| 体验功能 | 前端路径 | 后端服务 |
|---|---|---|
| 外部收文列表 | views/oa/officedoc/outside/list |
OfficeDocOutsideServiceImpl |
| 外部收文详情 | views/oa/officedoc/outside/info |
OfficeDocOutsideServiceImpl |
| 公文归档台账 | views/oa/officedoc/archive/list |
OfficeDocArchiveServiceImpl |
结语
外部收文看起来只是公文管理的一个补充模块,但它其实补齐了企业公文体系中非常关键的一环:不是所有文件都从企业内部发起,但所有进入企业的重要文件都应该被登记、办理和归档。
RuoYi Office 的实现没有把外来文做成孤立附件库,而是通过 BPM 流程、办理状态机、附件服务和统一归档台账,把外部来文纳入企业公文闭环。发文、收文、外部收文三类来源最终汇聚到同一个归档视图中,既保留各自业务差异,又形成统一查询入口。
这也是 RuoYi Office 在 OA 模块设计中的一贯思路:业务可以复杂,但系统边界要清晰;流程可以灵活,但数据状态要可控。OA · HRM · CRM · ERP,一站打通,最终服务的都是同一个目标:让企业的每一类业务都能形成可追踪、可复盘、可沉淀的数字化资产。
RuoYi Office ------ 一个平台,管好整个企业
在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
技术咨询:添加 17156169080,备注「RuoYi Office」
如果觉得不错,请给个 Star 支持一下。