在数字化转型浪潮中,企业OA系统早已不是简单的“审批工具”,而是承载协同、驱动流程、沉淀数据的关键基础设施。本文基于RuoYi Office的实践,从架构设计视角拆解一套真正可落地的企业OA系统:如何解耦流程与业务、如何设计消息触达层、如何保证审计可追溯。无论你是架构师还是技术负责人,都能从中找到可复用的设计思路。

一、OA系统设计的核心挑战:从“表单堆叠”到“业务协同”

很多团队把OA理解为“动态表单+审批流”,但一个用印申请背后至少涉及:发起权限、印章管理、附件预览、审批后通知、状态回滚、审计追溯。如果只关注表单字段,后期会不断打补丁。

核心问题:OA系统需要围绕6个关键点设计——谁可以发起、资源是否互斥、流程变量如何传递、状态如何回写、消息如何触达、审计如何留痕。

常见做法上线初期表现后期问题
每个业务各写一套审批逻辑开发快状态字段不统一,流程回调散落在各处
所有业务塞进一张万能表配置灵活复杂业务难查询、难统计、难做权限
审批和业务状态分离流程能跑审批通过但业务未生效,数据不一致
只做站内待办功能完整用户收不到提醒,流程长期挂起
只记录最新状态页面清爽审计时无法还原历史过程

企业OA的业务场景包括:公文管理、用印申请、出差审批、会议室预约、公车管理、工作汇报等。这些场景的共同点是:都有一个业务对象、一段生命周期、一套参与角色、一组需留痕的动作。因此,OA的第一层抽象不应该是“表单”,而应该是“业务单据”。

二、总体架构:门户+单据+流程+触达四层模型

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 会话、在线状态
基础层支撑企业级能力组织、权限、数据权限、多租户、审计、文件存储

这种分层的好处是:业务模块可快速扩展,而流程、附件、通知、权限等共性能力无需重复开发。核心设计决策包括:

  • 流程与业务解耦:流程引擎只负责任务流转,业务状态由回调驱动
  • 统一附件中心:避免附件散落在各业务表
  • 消息触达分层:站内信、WebSocket、IM即时通讯组合触达
  • 单体/微服务双模式:从小团队到集团化演进

一条OA单据的标准生命周期如下:

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

只要这个生命周期稳定,后续新增“合同申请”“资产领用”等场景,都能复用这套模式。

三、流程设计:如何实现流程与业务的解耦

企业OA的流程设计至少有三层:

层次关注点示例
流程模型谁审批、谁抄送、如何分支部门经理 → 行政 → 总经理
流程变量分支判断依据金额、天数、部门、申请类型
业务回调审批结果如何影响业务用印通过后通知印章管理员,公车通过后占用车辆

很多系统只实现了第一层,导致审批结果无法驱动业务。RuoYi Office通过Flowable 7承载BPMN流程,用业务模块提供流程变量,用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) {
  // 流程撤回后,业务模块可回到草稿或待提交状态
  }
  }

⚠️ 关键实践:流程变量必须来自业务字段,而不是前端临时判断。例如出差天数超过3天需更高级别审批,这个条件应在提交流程时写入流程变量,以便审计追溯。

四、功能实现:从门户入口到消息触达

4.1 门户:不只是首页,更是工作入口

一个可落地的OA门户至少要承接5类信息:待办任务、通知公告、快捷入口、数据看板、日程日历。image.png

4.2 单据提交:业务数据先落库,流程再绑定

业务单据提交的标准模式:先保存业务单据和明细,再提交流程,最后把流程实例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单据几乎都需要附件:公文正文、用印材料、出差凭证等。如果每个业务各自设计附件字段,很快会出现混乱。统一附件中心用billType + businessId绑定业务单据。image.png

4.5 消息触达层:待办不应该只躺在列表里

OA流程卡住,往往不是因为系统没有待办,而是人没有被及时触达。触达层分为三类:站内信、WebSocket推送、IM即时通讯。

触达方式适合场景
站内通知重要但不要求实时的系统提醒
WebSocket待办数量、在线消息、审批提醒实时推送
IM 即时通讯审批上下文沟通、同事会话、多人协同
在这里插入图片描述

前端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();
}
image.png

五、数据结构设计:服务于状态、权限和审计

不同OA单据字段不同,但核心字段高度相似:

字段含义设计原因
业务主键与流程 绑定
单据编号便于业务检索和人工沟通
流程实例 ID关联 Flowable 流程详情
流程状态列表筛选、业务状态展示
创建人数据权限、发起人追踪
创建时间审计和统计
更新时间变更追踪
逻辑删除保留历史和审计空间

不同模块的数据建模重点各异:

模块关键表设计建模重点
公文管理发文、收文、外部收文、归档表文号、正文、流转、办理、归档
会议室会议室主表、预约单时间冲突、参会人、提醒
用印管理印章主表、用印申请单印章类型、管理员、盖章材料
公车管理车辆台账、用车单、还车单资源互斥、双单联动、归还状态
出差管理出差单、行程明细多行程、天数、预算、审批变量
办公用品用品台账、申请单、明细库存、发放、归还、消耗
企业云盘文件表、权限表、收藏表文件元数据、共享权限、空间控制
工作汇报汇报单、明细日报周报、接收人、汇总
即时通讯会话、消息、成员、在线状态实时消息、已读未读、会话上下文

⚠️ 为什么建议保留业务独立表?很多低代码OA喜欢用一张“表单数据表”保存所有字段,短期灵活但长期会遇到查询困难、权限困难、性能困难、审计困难四类问题。RuoYi Office的选择是:简单审批用流程表单,复杂业务必须有自己的业务表。

六、核心代码实现:流程事件驱动业务闭环

当Flowable流程状态变化后,业务模块通过FlowBillService更新自己的状态。回调只接收businessKeystatus,避免流程模块感知业务表结构。

@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);
}

消息触达要带上下文:业务类型、业务主键、流程实例、标题、接收人和跳转地址。

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);
}

这就是“触达层”的设计重点:不是多接几个渠道,而是让消息和业务上下文绑定。

七、RuoYi Office的创新设计:Bill + BPM双层模型

RuoYi Office采用“业务单据Bill + BPM流程”的双层模型:

模型负责内容
Bill业务字段、明细、附件、业务状态、统计查询
BPM流程定义、任务流转、审批意见、参与人、流程历史

核心创新点包括:

  • FlowBillService:流程回调标准化,共性入口固定,差异留给业务实现
  • 附件中心:让材料成为可治理资产,支持归档、预览和移动端复用
  • 通知+IM:把“待办”变成真正可达,审批提醒与上下文沟通结合
  • 单体/微服务双模式:支持从小团队到集团化演进

[AFFILIATE_SLOT_1]

八、快速体验:从一个流程看完整闭环

如果想快速理解这套OA架构,可以按以下路径体验:

步骤操作观察重点
1登录系统工作台看待办、消息、常用应用入口
2进入 OA 模块查看公文、会议室、用印、公车、出差、云盘等菜单
3新建一张出差或用印申请观察业务字段、明细和附件上传
4提交审批观察流程实例和业务单据如何绑定
5切换审批人处理待办观察流程任务、审批意见和状态变化
6查看单据详情观察附件、流程记录、业务状态是否一致
7打开 IM 或消息中心观察审批提醒和会话触达
8回到列表筛选状态验证审批中、已通过、已拒绝等状态查询

推荐从“出差申请”“用印管理”“公车申请”“工作汇报”开始,因为它们能同时体现业务表、流程变量、附件和状态回调。

九、技术亮点总结

设计要点实现方式价值
统一门户工作台 + 待办 + 消息 + 常用应用用户进入系统即可处理工作
业务单据每类复杂业务保留独立 Bill 表查询、统计、权限和审计更可靠
流程驱动Flowable 7 + BPMN 流程模型流程可配置、可追踪、可扩展
回调解耦 标准接口流程事件和业务状态稳定衔接
附件中心 统一绑定审批材料、归档材料可复用
消息触达通知 + WebSocket + IM待办实时可达,减少流程卡滞
权限体系RBAC + 数据权限 + 多租户支撑企业组织边界和数据安全
前端架构Vue3 + Vben Admin + Ant Design Vue页面、表格、表单和路由规范统一
部署架构单体 / 微服务双模式小团队快速上线,大规模平滑演进

[AFFILIATE_SLOT_2]

结语

一套真正可落地的企业OA,核心思路可以概括为:以业务单据承载事实,以BPM流程驱动协同,以附件和消息补齐上下文,以权限和审计守住企业边界。这种架构不仅适用于OA,也适用于HRM入职转正、合同审批、资产领用、项目立项等企业管理场景。关键不是“页面能不能做出来”,而是“业务能不能长期跑下去”。

RuoYi Office —— 一个平台,管好整个企业

在线演示http://ruoyioffice.com/web/(账号 admin / admin123)

微信:添加 17156169080,备注「RuoYi Office」

如果觉得不错,请给个 Star 支持一下!

idbusinessKeybill_codeprocess_instance_idprocess_statuscreatorcreate_timeupdate_timedeletedFlowBillServicebillType + businessId