如何设计一套真正可落地的企业 OA 系统?从门户、流程、单据到消息触达的架构拆解

0 阅读19分钟

如何设计一套真正可落地的企业 OA 系统?从门户、流程、单据到消息触达的架构拆解

🌐 文档地址ruoyioffice.com | 📦 源码1gitcode.com/zhouzhongya… | 📦 源码2gitcode.com/zhouzhongya… | 📦 源码3github.com/yuqing2026/…

很多团队第一次做 OA,习惯从“先画几个表单”开始:请假单、用印单、会议室预约单、出差单、公车申请单。表单很快能跑起来,但上线半年后会发现真正难的不是字段,而是组织权限、审批状态、附件归档、消息触达、审计追踪和跨模块联动。OA 不是表单堆叠,而是企业协同动作的统一运行底座。 oa-system-design-architecture.png

▲ 企业 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 和页面按模块同构后续扩展模块更容易维护

image.png

2.3 一条 OA 单据的标准生命周期

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

image.png

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


三、流程设计:流程不能绑死在业务代码里

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 门户应该让用户“进来就能干活”。 image.png▲ 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 绑定业务单据。 image.png

这样审批详情、移动端详情、归档页面都可以复用同一套附件读取逻辑。

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

OA 流程之所以常常卡住,不是系统没有待办,而是人没有被及时触达。

RuoYi Office 的触达层可以分为三类:

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

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

image.png

这类细节看似很小,但它决定了系统能不能在 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 喜欢用一张“表单数据表”保存所有字段,短期看很灵活,长期会遇到四类问题:

  1. 查询困难:出差按天数统计、公车按车辆统计、公文按文号统计,都需要解析 JSON。
  2. 权限困难:不同模块的数据权限和查看范围不同,很难统一套一条规则。
  3. 性能困难:大量业务都挤在一张表,索引无法针对场景优化。
  4. 审计困难:业务状态和流程状态混在表单 JSON 里,追溯成本高。

所以 RuoYi Office 的选择是:简单审批可以用流程表单,复杂业务必须有自己的业务表。


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

6.1 业务服务实现流程回调

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

对于更复杂的业务,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 支持一下!