在数字化转型浪潮中,企业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 回调业务模块
↓
更新业务状态、资源占用、附件归档和审计记录
只要这个生命周期稳定,后续新增“合同申请”“资产领用”等场景,都能复用这套模式。
三、流程设计:如何实现流程与业务的解耦
企业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类信息:待办任务、通知公告、快捷入口、数据看板、日程日历。
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绑定业务单据。
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();
}
五、数据结构设计:服务于状态、权限和审计
不同OA单据字段不同,但核心字段高度相似:
| 字段 | 含义 | 设计原因 |
|---|---|---|
| 业务主键 | 与流程 绑定 | |
| 单据编号 | 便于业务检索和人工沟通 | |
| 流程实例 ID | 关联 Flowable 流程详情 | |
| 流程状态 | 列表筛选、业务状态展示 | |
| 创建人 | 数据权限、发起人追踪 | |
| 创建时间 | 审计和统计 | |
| 更新时间 | 变更追踪 | |
| 逻辑删除 | 保留历史和审计空间 |
不同模块的数据建模重点各异:
| 模块 | 关键表设计 | 建模重点 |
|---|---|---|
| 公文管理 | 发文、收文、外部收文、归档表 | 文号、正文、流转、办理、归档 |
| 会议室 | 会议室主表、预约单 | 时间冲突、参会人、提醒 |
| 用印管理 | 印章主表、用印申请单 | 印章类型、管理员、盖章材料 |
| 公车管理 | 车辆台账、用车单、还车单 | 资源互斥、双单联动、归还状态 |
| 出差管理 | 出差单、行程明细 | 多行程、天数、预算、审批变量 |
| 办公用品 | 用品台账、申请单、明细 | 库存、发放、归还、消耗 |
| 企业云盘 | 文件表、权限表、收藏表 | 文件元数据、共享权限、空间控制 |
| 工作汇报 | 汇报单、明细 | 日报周报、接收人、汇总 |
| 即时通讯 | 会话、消息、成员、在线状态 | 实时消息、已读未读、会话上下文 |
⚠️ 为什么建议保留业务独立表?很多低代码OA喜欢用一张“表单数据表”保存所有字段,短期灵活但长期会遇到查询困难、权限困难、性能困难、审计困难四类问题。RuoYi Office的选择是:简单审批用流程表单,复杂业务必须有自己的业务表。
六、核心代码实现:流程事件驱动业务闭环
当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);
}消息触达要带上下文:业务类型、业务主键、流程实例、标题、接收人和跳转地址。
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
浙公网安备 33010602011771号