如何设计一套真正可落地的企业 OA 系统?从门户、流程、单据到消息触达的架构拆解
🌐 文档地址:ruoyioffice.com | 📦 源码1:gitcode.com/zhouzhongya… | 📦 源码2:gitcode.com/zhouzhongya… | 📦 源码3:github.com/yuqing2026/…
很多团队第一次做 OA,习惯从“先画几个表单”开始:请假单、用印单、会议室预约单、出差单、公车申请单。表单很快能跑起来,但上线半年后会发现真正难的不是字段,而是组织权限、审批状态、附件归档、消息触达、审计追踪和跨模块联动。OA 不是表单堆叠,而是企业协同动作的统一运行底座。
▲ 企业 OA 系统架构:以统一门户承接工作入口,以业务单据承载数据,以 BPM 流程驱动协同,以附件中心沉淀材料,以通知与 IM 完成触达,最后通过审计追踪形成闭环
引言:OA 系统到底难在哪?
OA 的英文是 Office Automation,但企业真正需要的不是“办公自动化”这个名词,而是把日常协同动作变成可管理、可追踪、可复盘的系统能力。
一个看似简单的用印申请,背后至少涉及:
- 谁可以发起用印,是否受部门、岗位、角色限制。
- 用什么印章,印章管理员是谁,是否需要法人或总经理审批。
- 附件是否必须上传,审批时能不能预览材料。
- 审批通过后是否自动通知印章管理员安排盖章。
- 被拒绝、撤回、退回时,业务状态如何回滚。
- 半年后审计时,能否查到谁在什么时间批准了什么材料。
如果只把 OA 当成“动态表单 + 审批流”,这些问题都会在后期变成补丁。
| 常见做法 | 上线初期表现 | 后期问题 |
|---|---|---|
| 每个业务各写一套审批逻辑 | 开发快 | 状态字段不统一,流程回调散落在各处 |
| 所有业务塞进一张万能表 | 配置灵活 | 复杂业务难查询、难统计、难做权限 |
| 审批和业务状态分离 | 流程能跑 | 审批通过但业务未生效,数据不一致 |
| 只做站内待办 | 功能完整 | 用户收不到提醒,流程长期挂起 |
| 只记录最新状态 | 页面清爽 | 审计时无法还原历史过程 |
本文不讲单个模块,而是从系统设计视角拆解一套企业 OA 应该如何落地:业务场景怎么拆、总体架构怎么搭、流程和单据怎么解耦、消息怎么触达、数据结构怎么设计,以及 RuoYi Office 在这些问题上的工程实现。
一、业务场景:OA 不是一个模块,而是一组协同场景
1.1 企业 OA 的业务全景
RuoYi Office 的 OA 模块覆盖的是企业日常办公中最典型的一组场景:
| 场景 | 业务模块 | 典型动作 |
|---|---|---|
| 公文流转 | 发文、收文、外部收文、归档 | 拟稿、核稿、签发、收文登记、办理、归档 |
| 行政资源 | 会议室、公车、用印、办公用品 | 预约、申请、审批、发放、归还、状态回写 |
| 员工协同 | 出差、日程、工作汇报 | 发起、审批、提醒、汇总、追踪 |
| 文件资产 | 企业云盘、附件中心 | 上传、预览、共享、归档、权限控制 |
| 消息协同 | 通知公告、即时通讯 | 待办提醒、审批触达、会话沟通、在线状态 |
这些场景的共同点是:都有一个业务对象,有一段生命周期,有一套参与角色,有一组需要留痕的动作。
所以 OA 的第一层抽象不应该是“表单”,而应该是“业务单据”。
1.2 为什么“表单堆叠”一定会失控?
表单解决的是“用户填什么”,但 OA 还要解决“填完之后发生什么”。
比如会议室预约,表单字段很简单:会议室、开始时间、结束时间、参会人员。但真正影响系统可用性的,是同一时间是否冲突、审批通过后是否占用资源、取消后是否释放会议室、参会人是否收到通知。
再看公车申请,字段也不复杂:车辆、用车时间、目的地、事由。难点在于车辆是互斥资源,审批中的申请也可能造成未来冲突,还车动作最好拆成独立单据,否则用车和还车状态会混在一起。
因此,企业 OA 至少要围绕 6 个核心问题设计:
| 设计维度 | 要回答的问题 |
|---|---|
| 组织 | 谁能发起、谁能审批、谁能查看 |
| 权限 | 菜单、按钮、数据范围如何控制 |
| 流程 | 谁审批、按什么条件分支、能否变更 |
| 状态机 | 草稿、审批中、通过、拒绝、撤回后业务怎么变化 |
| 消息触达 | 待办、站内信、WebSocket、IM 如何协同 |
| 审计追踪 | 过程、附件、意见、状态变更是否可还原 |
二、OA 系统总体设计:门户 + 单据 + 流程 + 触达
2.1 总体架构分层
RuoYi Office 采用的是典型的前后端分离架构:后端基于 Spring Boot 3.5 + Flowable 7 + MyBatis-Plus + Redis + WebSocket,前端基于 Vue3 + Vben Admin + Ant Design Vue。
从 OA 设计角度看,可以拆成 6 层:
| 层次 | 作用 | 关键能力 |
|---|---|---|
| 门户层 | 统一工作入口 | 工作台、待办、日程、消息、常用应用 |
| 业务层 | 承载具体场景 | 公文、会议室、用印、公车、出差、云盘、汇报 |
| 单据层 | 统一业务抽象 | Bill 编号、业务主键、状态、附件、创建人 |
| 流程层 | 驱动协同流转 | Flowable、BPMN、流程变量、任务、回调 |
| 触达层 | 让人及时处理 | 站内通知、WebSocket、IM 会话、在线状态 |
| 基础层 | 支撑企业级能力 | 组织、权限、数据权限、多租户、审计、文件存储 |
这种分层的好处是:业务模块可以快速扩展,但流程、附件、通知、权限这些共性能力不需要重复造轮子。
2.2 核心设计决策
| 决策点 | RuoYi Office 的做法 | 价值 |
|---|---|---|
| 业务建模 | 每类业务保留自己的单据表 | 支持复杂查询、统计、权限和业务规则 |
| 流程接入 | 单据提交 BPM,流程实例绑定 businessKey | 流程和业务弱耦合 |
| 状态回写 | FlowBillService 接收流程事件 | 审批结果自动驱动业务状态 |
| 附件处理 | 附件中心按单据类型和业务 ID 绑定 | 上传、预览、归档统一 |
| 消息触达 | 通知 + WebSocket + IM | 待办不只停留在列表里 |
| 前端组织 | API 和页面按模块同构 | 后续扩展模块更容易维护 |

2.3 一条 OA 单据的标准生命周期
创建草稿
↓
保存业务单据和明细
↓
提交 Flowable 流程,绑定 businessKey
↓
流程进入待办,触发通知 / WebSocket / IM
↓
审批人处理任务
↓
FlowBillService 回调业务模块
↓
更新业务状态、资源占用、附件归档和审计记录

只要这个生命周期稳定,后续新增“合同申请”“资产领用”“项目立项”等场景,本质上都能复用这套模式。
三、流程设计:流程不能绑死在业务代码里
3.1 流程设计的三个层次
企业 OA 的流程设计至少有三层:
| 层次 | 关注点 | 示例 |
|---|---|---|
| 流程模型 | 谁审批、谁抄送、如何分支 | 部门经理 → 行政 → 总经理 |
| 流程变量 | 分支判断依据 | 金额、天数、部门、申请类型 |
| 业务回调 | 审批结果如何影响业务 | 用印通过后通知印章管理员,公车通过后占用车辆 |
很多系统只实现第一层,所以看起来“有审批”,但审批结果无法真正驱动业务。
RuoYi Office 通过 Flowable 7 承载 BPMN 流程,用业务模块提供流程变量,用 FlowBillService 处理流程回调。流程引擎只关心任务流转,业务模块只关心自己的状态变化。
3.2 FlowBillService:流程与业务之间的桥
FlowBillService 的关键不是代码复杂,而是职责边界清晰:流程事件到了业务模块,只传业务主键和流程状态,具体怎么更新由业务自己决定。
public interface FlowBillService<T extends BillTypeEnum> {
/**
* 当前服务支持的单据类型,例如 OA_BUSINESS_TRIP、OA_WORK_REPORT。
*/
T getSupportedBillType();
/**
* 流程状态变更时,回写业务单据状态。
*/
void updateProcessStatus(String businessKey, Integer status);
default void onProcessApproved(String businessKey) {
// 审批通过后,业务模块可执行资源占用、归档、发放等动作
}
default void onProcessRejected(String businessKey) {
// 审批拒绝后,业务模块可释放资源或回滚状态
}
default void onProcessCancelled(String businessKey) {
// 流程撤回后,业务模块可回到草稿或待提交状态
}
}
这比在流程监听器里写一堆 if (billType == xxx) 更稳。流程模块不需要理解每个业务的细节,业务模块也不需要侵入 Flowable 的内部实现。
3.3 流程变量要来自业务,而不是来自页面临时判断
流程分支通常依赖业务字段,例如:
- 出差天数超过 3 天,需要更高级别审批。
- 用印类型为合同章,需要法务参与。
- 办公用品金额超过预算,需要财务审批。
- 外部收文涉及领导批示,需要追加办理节点。
这些条件应该在提交流程时进入流程变量,而不是让前端页面临时判断。原因很简单:流程变量属于流程审计的一部分,未来追溯时必须能解释“当时为什么走了这条审批路径”。
四、功能实现:从门户入口到消息触达
4.1 门户不是首页,而是工作入口
一个可落地的 OA 门户至少要承接 5 类信息:
| 入口 | 解决的问题 |
|---|---|
| 待办任务 | 我现在要处理什么 |
| 已办 / 发起 | 我处理过什么、发起过什么 |
| 日程提醒 | 今天有哪些会议、事项和截止时间 |
| 常用应用 | 快速进入用印、会议室、云盘、公文等高频模块 |
| 消息会话 | 审批沟通、系统提醒、同事协作 |
如果门户只展示几张统计卡片,它更像 BI 看板;真正的 OA 门户应该让用户“进来就能干活”。
▲ RuoyiOffice 可以自定义首页,内置多个通用组件
4.2 业务单据提交 BPM 的标准模式
下面是 RuoYi Office 里 OA 单据常见的提交模式:先保存业务单据和明细,再提交流程,最后把流程实例 ID 回写到业务表。
@Transactional(rollbackFor = Exception.class)
public Long createBusinessTrip(BusinessTripSaveReqVO reqVO) {
BusinessTripDO bill = BeanUtils.toBean(reqVO, BusinessTripDO.class)
.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
businessTripMapper.insertOrUpdate(bill);
saveItems(bill.getId(), reqVO.getItems());
Map<String, Object> variables = BpmProcessVariableUtils.buildBillVariables(reqVO);
variables.put("totalDays", reqVO.getTotalDays());
variables.put("estimatedCost", reqVO.getEstimatedCost());
String processInstanceId = processInstanceApi.submitProcessInstance(
Long.valueOf(reqVO.getCreator()),
new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(OaBillTypeEnum.OA_BUSINESS_TRIP.getProcessDefinitionKey())
.setVariables(variables)
.setBusinessKey(String.valueOf(bill.getId()))
).getCheckedData();
businessTripMapper.updateById(new BusinessTripDO()
.setId(bill.getId()).setProcessInstanceId(processInstanceId));
attachmentService.saveAttachmentList(OaBillTypeEnum.OA_BUSINESS_TRIP.getTypeCode(),
bill.getId(), reqVO.getAttachments());
return bill.getId();
}
这段代码体现了一个重要原则:业务数据先落库,流程实例再绑定业务主键。流程可以驱动状态,但不能替代业务表。
4.3 状态机必须由流程回调驱动
OA 系统里最容易出问题的是“流程状态”和“业务状态”不一致。例如审批已经通过,但会议室还没占用;还车单审批被拒,但原用车单仍显示还车中。
正确做法是把状态回写集中在流程回调里:
| 流程事件 | 通用流程状态 | 业务侧动作 |
|---|---|---|
| 提交 | 审批中 | 单据进入运行态,产生待办 |
| 通过 | 已通过 | 资源占用、生效、归档、发放 |
| 拒绝 | 已拒绝 | 回滚占用、释放资源、记录原因 |
| 撤回 | 已取消 | 回到草稿或待提交状态 |
| 删除流程 | 已删除 | 按需删除业务单据或清理关联 |
状态机的价值不是多几个枚举,而是让每一种异常路径都有明确结果。
4.4 附件中心:不要让附件散落在各业务表
OA 单据几乎都需要附件:
- 公文:正文、套红文件、批示材料、归档文件。
- 用印:合同扫描件、授权材料、盖章后回传文件。
- 出差:行程单、预算说明、报销凭证。
- 工作汇报:日报、周报、图片、补充说明。
- 企业云盘:制度、模板、会议纪要、知识材料。
如果每个业务各自设计附件字段,很快会出现“一个表三个附件 URL”的混乱。更合理的做法是建立统一附件中心,用 billType + businessId 绑定业务单据。

这样审批详情、移动端详情、归档页面都可以复用同一套附件读取逻辑。
4.5 消息触达层:待办不应该只躺在列表里
OA 流程之所以常常卡住,不是系统没有待办,而是人没有被及时触达。
RuoYi Office 的触达层可以分为三类:
| 触达方式 | 适合场景 |
|---|---|
| 站内通知 | 重要但不要求实时的系统提醒 |
| WebSocket | 待办数量、在线消息、审批提醒实时推送 |
| IM 即时通讯 | 审批上下文沟通、同事会话、多人协同 |

▲ IM 即时通讯页面:OA 的消息触达不只是“发一条通知”,更重要的是把审批、协同、沟通放回同一个工作上下文里
前端 WebSocket 地址构建也要考虑生产环境子路径部署,例如前端部署在 /web 下,但 WebSocket 需要连接站点根路径代理到后端。
export function buildWebSocketUrl(path: string, token?: string) {
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
const baseUrl = import.meta.env.VITE_BASE_URL as string;
const origin = /^https?:\/\//.test(baseUrl)
? new URL(baseUrl).origin
: window.location.origin;
const url = new URL(normalizedPath, origin);
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
if (token) {
url.searchParams.set('token', token);
}
return url.toString();
}

这类细节看似很小,但它决定了系统能不能在 Nginx、HTTPS、子路径部署、网关代理等真实生产环境中稳定运行。
五、数据结构:OA 表设计要服务于状态、权限和审计
5.1 业务单据的通用字段
不同 OA 单据字段不同,但核心字段高度相似:
| 字段 | 含义 | 设计原因 |
|---|---|---|
id | 业务主键 | 与流程 businessKey 绑定 |
bill_code | 单据编号 | 便于业务检索和人工沟通 |
process_instance_id | 流程实例 ID | 关联 Flowable 流程详情 |
process_status | 流程状态 | 列表筛选、业务状态展示 |
creator | 创建人 | 数据权限、发起人追踪 |
create_time | 创建时间 | 审计和统计 |
update_time | 更新时间 | 变更追踪 |
deleted | 逻辑删除 | 保留历史和审计空间 |
这组字段可以视为 OA 单据的“基础骨架”。业务模块在此基础上增加自己的领域字段。
5.2 不同模块的数据建模重点
| 模块 | 关键表设计 | 建模重点 |
|---|---|---|
| 公文管理 | 发文、收文、外部收文、归档表 | 文号、正文、流转、办理、归档 |
| 会议室 | 会议室主表、预约单 | 时间冲突、参会人、提醒 |
| 用印管理 | 印章主表、用印申请单 | 印章类型、管理员、盖章材料 |
| 公车管理 | 车辆台账、用车单、还车单 | 资源互斥、双单联动、归还状态 |
| 出差管理 | 出差单、行程明细 | 多行程、天数、预算、审批变量 |
| 办公用品 | 用品台账、申请单、明细 | 库存、发放、归还、消耗 |
| 企业云盘 | 文件表、权限表、收藏表 | 文件元数据、共享权限、空间控制 |
| 工作汇报 | 汇报单、明细 | 日报周报、接收人、汇总 |
| 即时通讯 | 会话、消息、成员、在线状态 | 实时消息、已读未读、会话上下文 |
5.3 为什么建议保留业务独立表?
很多低代码 OA 喜欢用一张“表单数据表”保存所有字段,短期看很灵活,长期会遇到四类问题:
- 查询困难:出差按天数统计、公车按车辆统计、公文按文号统计,都需要解析 JSON。
- 权限困难:不同模块的数据权限和查看范围不同,很难统一套一条规则。
- 性能困难:大量业务都挤在一张表,索引无法针对场景优化。
- 审计困难:业务状态和流程状态混在表单 JSON 里,追溯成本高。
所以 RuoYi Office 的选择是:简单审批可以用流程表单,复杂业务必须有自己的业务表。
六、核心代码:流程事件如何驱动业务闭环
6.1 业务服务实现流程回调
当 Flowable 流程状态变化后,业务模块通过 FlowBillService 更新自己的状态。以出差单这类业务为例,回调只接收 businessKey 和 status,避免流程模块感知业务表结构。
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
log.info("[updateProcessStatus] 更新出差申请单流程状态,id: {}, status: {}", id, status);
BusinessTripDO bill = businessTripMapper.selectById(id);
if (bill == null) {
throw exception(BUSINESS_TRIP_NOT_EXISTS);
}
BusinessTripDO updateObj = new BusinessTripDO();
updateObj.setId(id);
updateObj.setProcessStatus(status);
businessTripMapper.updateById(updateObj);
if (BpmTaskStatusEnum.APPROVE.getStatus().equals(status)) {
notifyTripApproved(bill);
}
log.info("[updateProcessStatus] 出差申请单流程状态更新成功,id: {}", id);
}
对于更复杂的业务,onProcessApproved 可以继续处理资源占用、库存扣减、归档文件生成、原单状态回写等动作。
6.2 消息触达不等于发送一条文本
OA 消息要带上下文,至少要包含业务类型、业务主键、流程实例、标题、接收人和跳转地址。否则用户收到“你有一条待办”以后,还要自己去找是什么单据。
public void pushApprovalTodo(Long userId, OaBillMessage message) {
NotifyMessageCreateReqDTO notify = new NotifyMessageCreateReqDTO()
.setUserId(userId)
.setTemplateCode("oa_approval_todo")
.setTemplateParams(Map.of(
"billType", message.getBillType(),
"billTitle", message.getTitle(),
"starter", message.getStarterName()
));
notifyMessageApi.createNotifyMessage(notify);
WebSocketMessage wsMessage = new WebSocketMessage()
.setType("oa_approval_todo")
.setBusinessId(message.getBusinessId())
.setProcessInstanceId(message.getProcessInstanceId())
.setUrl(message.getDetailUrl());
webSocketMessageSender.send(userId, wsMessage);
}
这就是“触达层”的设计重点:不是多接几个渠道,而是让消息和业务上下文绑定。
七、RuoyiOffice(RuoYi Office)的创新设计
7.1 Bill + BPM:既保留业务表,又复用流程引擎
RuoYi Office 没有把所有 OA 都做成一张万能表,也没有让每个业务各写一套审批。它采用的是“业务单据 Bill + BPM 流程”的双层模型:
| 模型 | 负责内容 |
|---|---|
| Bill | 业务字段、明细、附件、业务状态、统计查询 |
| BPM | 流程定义、任务流转、审批意见、参与人、流程历史 |
这让业务模块有足够表达能力,同时又能复用统一流程能力。
7.2 FlowBillService:流程回调标准化
流程通过、拒绝、撤回、删除这些事件在所有业务里都会出现,但每个业务的后续动作不同。FlowBillService 把共性入口固定下来,把差异留给业务实现。
这是一种很适合企业系统的设计:接口稳定,业务可扩展。
7.3 附件中心:让材料成为可治理资产
OA 不是只有字段,很多关键证据都在附件里。公文正文、用印材料、出差凭证、会议纪要、工作汇报附件,如果散落在各个模块,会直接影响归档和审计。
统一附件中心让文件和单据通过 billType + businessId 绑定,既能服务审批详情,也能服务归档、预览和移动端复用。
7.4 通知 + IM:把“待办”变成真正可达
传统 OA 的待办只是一个列表,用户不点进去就不知道。RuoYi Office 把通知、WebSocket 和即时通讯组合起来,让审批提醒、会话沟通、在线状态形成一个触达层。
这也是内置 IM 对企业 OA 的价值:审批不是孤立动作,很多时候需要上下文沟通。
7.5 单体 / 微服务双模式:从小团队到集团化演进
OA 项目很容易从小范围试点开始,后续扩展到全公司、集团、多租户和多端。RuoYi Office 支持单体和微服务两种部署模式,让企业可以先用单体快速落地,再按业务规模演进。
八、快速体验:从一个流程看完整闭环
如果想快速理解这套 OA 架构,可以按下面路径体验:
| 步骤 | 操作 | 观察重点 |
|---|---|---|
| 1 | 登录系统工作台 | 看待办、消息、常用应用入口 |
| 2 | 进入 OA 模块 | 查看公文、会议室、用印、公车、出差、云盘等菜单 |
| 3 | 新建一张出差或用印申请 | 观察业务字段、明细和附件上传 |
| 4 | 提交审批 | 观察流程实例和业务单据如何绑定 |
| 5 | 切换审批人处理待办 | 观察流程任务、审批意见和状态变化 |
| 6 | 查看单据详情 | 观察附件、流程记录、业务状态是否一致 |
| 7 | 打开 IM 或消息中心 | 观察审批提醒和会话触达 |
| 8 | 回到列表筛选状态 | 验证审批中、已通过、已拒绝等状态查询 |
推荐从“出差申请”“用印管理”“公车申请”“工作汇报”这几类单据开始,因为它们能同时体现业务表、流程变量、附件和状态回调。
九、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 统一门户 | 工作台 + 待办 + 消息 + 常用应用 | 用户进入系统即可处理工作 |
| 业务单据 | 每类复杂业务保留独立 Bill 表 | 查询、统计、权限和审计更可靠 |
| 流程驱动 | Flowable 7 + BPMN 流程模型 | 流程可配置、可追踪、可扩展 |
| 回调解耦 | FlowBillService 标准接口 | 流程事件和业务状态稳定衔接 |
| 附件中心 | billType + businessId 统一绑定 | 审批材料、归档材料可复用 |
| 消息触达 | 通知 + WebSocket + IM | 待办实时可达,减少流程卡滞 |
| 权限体系 | RBAC + 数据权限 + 多租户 | 支撑企业组织边界和数据安全 |
| 前端架构 | Vue3 + Vben Admin + Ant Design Vue | 页面、表格、表单和路由规范统一 |
| 部署架构 | 单体 / 微服务双模式 | 小团队快速上线,大规模平滑演进 |
结语
一套真正可落地的企业 OA,不应该从“我有多少张表单”开始设计,而应该从“企业有哪些协同动作,动作如何被流程驱动,结果如何触达和留痕”开始设计。
RuoYi Office 的核心思路可以概括为一句话:以业务单据承载事实,以 BPM 流程驱动协同,以附件和消息补齐上下文,以权限和审计守住企业边界。
这种架构不仅适用于 OA,也适用于 HRM 入职转正、合同审批、资产领用、项目立项、CRM 回款等企业管理场景。一套代码,通吃多端,关键不是“页面能不能做出来”,而是“业务能不能长期跑下去”。
💡 RuoYi Office —— 一个平台,管好整个企业
🌐 在线演示:ruoyioffice.com/web/(账号 admin / admin123)
💬 微信:添加 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!
