☰
OA源码审批流程引擎:从数据模型到上线排查
2026/10/25 17:07:37 网站建设 项目流程

简介:这套OA办公自动化系统源码面向希望自建审批流程、研究办公系统架构的开发者与中小企业技术团队,提供可二次开发的基础工程。压缩包共2000个文件、约43.54MB,以gif和png等界面图片、cs业务代码、aspx页面、js脚本、css样式及dll程序集、doc说明文档为主,另含数据库与配置文件,覆盖Web端、移动端适配、业务逻辑层和公共工具模块等组成部分。已有6933人学习下载,说明其在同类OA源码中具备一定参考价值。读者可从中了解审批流程如何落到业务规则与页面交互中,学习分层组织、数据库访问、角色权限与表单处理等实现思路,并在此基础上调整流程节点、扩展办公模块,适合作为办公自动化项目起步或改造的代码参照。

1. 一套OA源码真正的分水岭:审批流程能不能由业务自己改

很多团队拿到一套OA源码,第一件事是换登录页的 Logo,第二件事是改首页配色,改完发现真正卡住交付的地方根本不在前端。判断这套代码值不值得深挖,看一个点就够:审批流程是写死在if-else里的,还是抽成了「流程定义 + 节点 + 连线 + 条件表达式」的数据。前一种方案,业务提一句「报销超过五千要加一级总监审批」,你就得改代码、走测试、发版、停机;后一种方案,运维在后台把节点拖一下、把条件表达式改一行,保存即生效,在途单据还按旧版本跑完。这套办公自动化系统源码的价值就在后者——它把审批流程做成了可配置的元数据,而不是一段业务逻辑。适合三类人:要给内部搭一套 OA 的开发者、要交付审批模块的外包团队、想拿一套完整流程引擎当参考实现的中高级工程师。

2. 拆开这套OA的技术骨架:审批引擎怎么把一张流程图变成数据库记录

上手之前先别急着run,花二十分钟把数据模型看清楚,后面配流程、查问题会省掉大量时间。审批引擎的本质只有一句话:把用户在后台画的图,翻译成几张表之间的状态迁移,运行时只认表,不认图。所以真正决定这套 OA 能改多深的,是表的粒度和字段设计,不是前端那个拖拽画布做得多漂亮。

2.1 流程定义、节点、连线:三层数据结构决定流程能改多深

我一般会先翻流程相关的建表脚本,通常能看到这么几张核心表:流程定义表、节点表、连线表、实例表、任务表、审批记录表。前三张是「设计态」,后三张是「运行态」,这条边界必须划清楚——设计态改的是模板,运行态记的是某一张单据走过的痕迹,两者混在一起,改流程就会把在途单据带崩。

设计态三张表的关系是:一个流程定义(oa_process_def)下挂多个节点(oa_process_node),节点之间用连线(oa_process_edge)连接,连线上的条件表达式决定走哪条路。节点表里存的不只是名字,还有节点类型(开始、审批、抄送、条件网关、结束)和审批人策略(指定人、按角色、按部门主管、发起人自选、取表单字段值)。审批人策略这一列是整张表的灵魂,它决定了「谁能审」这个业务问题能不能不改代码就调整。

运行态里,实例表(oa_process_instance)记录一张单据当前停在哪个节点、用的是哪个版本的流程定义,任务表(oa_task)记录每个人的待办,审批记录表(oa_approve_log)记流水。这里有个关键设计:实例表里必须存def_version,而不是只存def_id。只存定义 ID 的系统,业务一改流程,所有在途单据立刻按新流程图走,原来走到第三级的单子可能突然找不到下一节点——这是老系统里最常见的翻车点之一。

表名作用关键字段改动风险
oa_process_def流程模板code、version、status低,新增版本即可
oa_process_node节点与审批人策略node_key、node_type、approver_type中,改策略影响新实例
oa_process_edge连线与条件from_key、to_key、condition_expr中高,条件写错直接断流
oa_process_instance单据实例biz_key、current_node、def_version高,动结构要考虑存量
oa_task待办任务assignee、task_status高,涉及并发与重复生成
oa_approve_log审批流水action、opinion、op_time低,只追加不修改

看懂这张表,你就能预判这套 OA 的上限:节点表如果有approver_type和approver_value两列,说明审批人是可配置的;如果只有approver_id,那所谓「自定义流程」大概率只是自定义节点顺序,人还是得一个个指定,业务量一大就废。

2.2 从源码目录到本地跑通:最小启动路径

目录结构一般长这样,先认几个关键位置,别一上来就全局搜索:

# 典型的分层结构,具体目录名以你手上的包为准 oa-system/ ├── oa-common/ # 工具类、统一返回、常量 ├── oa-framework/ # 权限、拦截器、数据源配置 ├── oa-flow/ # 流程引擎核心:定义解析、任务分发、条件求值 ├── oa-admin/ # 后台管理端接口:流程设计、用户角色 ├── oa-web/ # 前端工程 ├── sql/ # 建表脚本 + 初始化数据 └── doc/ # 部署说明、接口清单

启动顺序我一般固定成四步:建库导脚本、改配置、起后端、起前端。别跳过第一步直接起服务,很多包在启动时会做流程定义的校验,库是空的直接抛异常,报错信息还特别绕。

# 1. 建库 mysql -uroot -p -e "CREATE DATABASE oa_flow DEFAULT CHARSET utf8mb4;" # 2. 导入结构与种子数据,注意顺序:先结构后数据 mysql -uroot -p oa_flow < sql/schema.sql mysql -uroot -p oa_flow < sql/init_data.sql # 3. 改配置文件里的数据源、Redis、文件上传路径 # 重点确认 server.port 和 context-path,前端代理要对得上 # 4. 起后端(Maven 工程) mvn clean package -DskipTests java -jar oa-admin/target/oa-admin.jar --spring.profiles.active=dev
# 5. 起前端,注意接口代理指向后端实际端口 cd oa-web npm install --registry=https://registry.npmmirror.com npm run dev

参数上要盯住三个地方:数据库连接串的时区参数(不加serverTimezone在部分驱动版本下会报时间错乱,审批时间差八小时就是这么来的)、文件上传的临时目录(流程附件走这里,路径不存在会导致提交审批时静默失败)、以及定时任务开关(有些包默认开着任务补偿扫描,本地调试会莫名多出一批任务)。

提示:先跑通「发起一条最简单的单人审批」再动流程设计器。设计器依赖的接口多,一上来就调它,报错分不清是引擎问题还是前端代理问题。

2.3 初始化数据里的角色与用户,别拿来就用

种子数据通常塞了几个内置账号、几个角色、一棵部门树。新手最容易犯的错是直接拿admin跑全流程,跑通了就以为没问题——admin在很多实现里带超级权限,会绕过审批人解析逻辑,等换成普通账号立刻卡在「找不到审批人」。

正确做法是先确认三件事:角色表里有没有「部门主管」这类动态角色的定义;部门树的leader_id字段是否填了值;用户与角色的关联是不是按租户或按组织隔离的。这三样缺一个,后面配「按部门主管审批」就一定解析出空。

-- 检查部门主管是否配齐,返回空说明这条链路会断 SELECT d.id, d.name, d.leader_id, u.name AS leader_name FROM sys_dept d LEFT JOIN sys_user u ON u.id = d.leader_id WHERE d.leader_id IS NULL OR d.leader_id = 0;

这条 SQL 我每次接手新包都会跑一遍,返回哪怕一行,也要先把主管补上再往下走。原因很简单:审批人策略里「按部门主管」是最常用的一个,它取的就是发起人所在部门往上找的第一个有主管的节点,中间断一层,任务就生成不出来,而报错往往只是一句「审批人为空」,不看代码根本不知道断在哪。

3. 画一条能跑通的审批流:节点类型、审批人策略与条件分支

流程设计器是这套 OA 的门面,但真正要理解的是它背后那几个配置项。会配节点类型、会写条件表达式、会选审批人策略,这三件事过关,日常九成的流程需求你都能自己接住,不用回头找开发。

3.1 节点类型与审批人策略的参数对照

节点类型不要贪多,先把五种用熟:开始、审批、条件网关、抄送、结束。很多包还会提供并行网关、子流程、定时节点,那些属于进阶,第一版流程别碰,出问题排查成本太高。

审批人策略是重点,常见实现里大概有这么几类,我按使用频率排:

策略标识含义取值来源典型场景
USER指定具体人用户 ID 列表财务复核、法务审核
ROLE按角色角色编码财务角色审报销
DEPT_LEADER部门主管发起人部门树请假、外出
FORM_FIELD取表单字段表单里的用户字段报销单填的「项目负责人」
STARTER_SELECT发起时自选运行时选择临时加签、指定协作人
INITIATOR发起人本人当前登录人销假确认、补录

选策略有个原则:能用FORM_FIELD就别用USER。指定具体人的流程,人一离职流程就废;绑到表单字段上,谁填单谁负责选人,责任清晰还不会过期。部门主管同理,绑部门树比绑人稳得多。

节点上除了审批人,还有两个参数要留意:multi_mode(会签还是或签)和allow_transfer(是否允许转办)。这两个在节点配置里往往是个下拉框,选错了要到运行阶段才暴露,配的时候就得定下来。

3.2 条件表达式怎么写才不会把流程写死

条件网关的连线表达式是整套系统最容易出事故的地方。表达式求值一般走两种实现:一种是内置的表达式引擎(常见是 SpEL 或 Aviator 这类),另一种是前端拼好的 JSON 条件树。不管哪种,写的时候守住三条:只引用表单里确实存在的字段、只做简单比较、每个分支都有兜底。

{ "nodeKey": "gw_amount", "nodeType": "CONDITION", "edges": [ { "to": "node_manager", "priority": 1, "conditionExpr": "#amount <= 5000" }, { "to": "node_director", "priority": 2, "conditionExpr": "#amount > 5000 && #amount <= 50000" }, { "to": "node_ceo", "priority": 99, "conditionExpr": "true" } ] }

这段配置里,priority是匹配顺序,引擎从上往下求值,命中第一条就走,所以兜底那条必须放最后且条件恒真。conditionExpr里用#amount引用表单变量,变量名要和表单字段的field_key完全一致,大小写都不能差。我见过最典型的一类事故,是表单字段叫totalAmount,表达式里写成#total_amount,求值返回 null,所有单子都掉进兜底分支,全公司报销直达 CEO——上线当天就被投诉。

表达式能力要克制。不要在条件里调方法、算日期差、查数据库,那些看着方便,实际是把业务逻辑埋进了配置里,别人接手时根本找不到。日期区间、金额汇总这类判断,在表单提交时就算好存成字段,条件里只做数值比较,这是最省心的做法。

3.3 一次完整的请假流程配置实操

拿请假走一遍,把前面的东西串起来。需求是:三天以内主管审批,三到七天主管加人事,七天以上再加一级总监,结束前抄送考勤员。

第一步建流程定义,填code、name、绑定请假表单,状态先存草稿。第二步拖节点:开始 → 主管审批 → 条件网关 → 人事审批 → 条件网关2 → 总监审批 → 抄送 → 结束。第三步配连线条件。

[ { "from": "start", "to": "node_leader", "conditionExpr": "true" }, { "from": "node_leader", "to": "gw_days", "conditionExpr": "true" }, { "from": "gw_days", "to": "node_hr", "conditionExpr": "#days > 3", "priority": 1 }, { "from": "gw_days", "to": "node_cc", "conditionExpr": "true", "priority": 99 }, { "from": "node_hr", "to": "gw_days2", "conditionExpr": "true" }, { "from": "gw_days2", "to": "node_director", "conditionExpr": "#days > 7", "priority": 1 }, { "from": "gw_days2", "to": "node_cc", "conditionExpr": "true", "priority": 99 } ]

第四步配审批人:node_leader用DEPT_LEADER,node_hr用ROLE绑人事角色,node_director用USER指定具体人但在生产环境建议换成角色。第五步发布,版本号走v1。

保存后先别急着用真实账号测,用测试库造三个人:发起人、主管、人事。发起一条days=2的单子,看是否停在主管节点;再发days=5的,看主管审完是否生成人事任务;最后发days=10的走完全链路。三条都通,这条流程才叫配好了。

注意:条件网关后面如果有两条连线条件同时为真且priority相同,不同引擎的处理不一样,有的抛异常,有的随便走一条。发布前把优先级显式写清楚,别依赖默认值。

4. 流转的核心动作:任务生成、会签或签、回退与转办

流程配好只是把路铺出来,真正决定这套 OA 好不好用的是运行时那几个动作:审批通过后任务怎么生成、多个审批人是并行还是串行、审错了怎么退回去、人出差了怎么转给别人。这几块逻辑写得糙的系统,业务用一周就开始骂人。

4.1 任务分发与并发控制:别让同一条单子生成两份待办

审批通过后的核心动作是「找下一个节点 → 解析审批人 → 生成任务 → 更新实例当前节点」,这四步必须在同一个事务里。我见过不少包为了性能把这四步拆开,结果用户手快点了两次通过,任务表里出现两条相同的待办,两个人各审一遍,单据直接跳过一级。

// 任务完成与流转的核心骨架,重点是事务边界和幂等控制 @Transactional(rollbackFor = Exception.class) public void completeTask(Long taskId, String action, String opinion, String operator) { // 1. 悲观锁住任务行,防止并发重复提交 OaTask task = taskMapper.selectForUpdate(taskId); if (task == null || !TaskStatus.PENDING.equals(task.getTaskStatus())) { throw new BizException("任务已处理,请刷新后重试"); } // 2. 写审批流水,先落痕再改状态,出了问题能追溯 approveLogMapper.insert(buildLog(task, action, opinion, operator)); // 3. 改任务状态,不用物理删除,保留处理记录 taskMapper.updateStatus(taskId, TaskStatus.DONE); // 4. 解析下一节点并生成任务 ProcessInstance inst = instanceMapper.selectById(task.getInstanceId()); List<String> nextNodes = flowResolver.resolveNext(inst, task.getNodeKey(), task.getVariables()); for (String nodeKey : nextNodes) { List<String> assignees = assigneeResolver.resolve(inst, nodeKey); if (assignees.isEmpty()) { throw new BizException("节点[" + nodeKey + "]未解析到审批人,请检查角色或部门主管配置"); } taskService.batchCreate(inst, nodeKey, assignees); } // 5. 更新实例当前节点,多节点并行时不做覆盖,改成列表 instanceMapper.updateCurrentNode(inst.getId(), nextNodes); }

selectForUpdate这一行是关键,少了它就是并发双审。assigneeResolver解析为空时直接抛异常而不是静默跳过,这是让问题在测试阶段暴露、而不是上线后被业务发现。第五步在多节点并行时不能用单个字段覆盖,很多包在这里偷懒,只存一个current_node,一遇到并行网关状态就乱,改起来还得动表结构。

批量生成任务时还有个细节:去重。同一节点同一处理人如果因为配置重复解析出两次,batchCreate里要按instance_id + node_key + assignee做一次去重,否则待办列表里会出现两条一模一样的记录。

4.2 会签、或签、加签:三种多人审批的差别

多人审批这块,配置项少但语义差别大,配错了不是报错,是流程卡住或者被少数人通过。

会签(ALL)是所有人都要审,有一条驳回就整节点驳回,全部通过才往下走。实现上通常是给每个人生成一条任务,实例上记一个计数器,全部完成才流转。或签(ANY)是任一人处理即可,其余任务作废,实现上要么生成多条任务靠抢锁,要么只生成一条共享任务。

加签是在已生成的任务上追加处理人,分前加签和后加签:前加签是让指定人先审,审完再回到原审批人;后加签是原审批人审完再让指定人看。这套逻辑实现成本不低,很多源码包只做了「追加任务」没做「回到原位」,用起来会有一种怪怪的感觉——加签人审完流程直接往下走了,原审批人反而没审。

{ "nodeKey": "node_finance", "nodeType": "APPROVAL", "multiMode": "ALL", "rejectStrategy": "ONE_VOTE_VETO", "allowAddSign": true, "addSignType": "BEFORE", "approverType": "ROLE", "approverValue": "finance" }

rejectStrategy这一项要看引擎实现,有的叫ONE_VOTE_VETO(一票否决),有的叫BACK_TO_INITIATOR(直接退回发起人),名字不同语义差很远,配之前翻一遍求值代码,别信前端那个下拉框的文案。

4.3 回退、撤回、转办的边界条件

回退是审错了退给上一步,撤回是发起人自己把单子收回来,转办是把待办转给别人。这三个动作看着简单,边界条件一堆:

回退要判断目标节点是否存在且在当前路径上。有些实现允许退到任意历史节点,挺好用但风险高——退到一个已经过期的分支上,后续条件求值可能拿不到当时的变量,流程就悬在那了。稳妥的做法是只允许退到上一步,或者退到发起人。

撤回要判断下一节点是否已被处理。已经有人审过了就不能撤回,否则等于绕过审批。这个校验必须在服务端做,前端置灰不算数。

转办要判断接收人是否有该节点的审批权限。转给一个没有对应角色的人,任务能生成但点开报无权限,用户会以为系统坏了。

-- 撤回前的前置校验:确认当前节点没有已完成的后续任务 SELECT COUNT(1) FROM oa_task t JOIN oa_process_instance i ON i.id = t.instance_id WHERE t.instance_id = #{instanceId} AND t.task_status = 'DONE' AND t.node_key <> #{currentNodeKey};

这条查询返回大于零,就说明后面已经有人审过,撤回按钮必须禁用。把这段做成服务端的强校验,比在前端藏按钮靠谱得多——接口是能被直接调的。

5. 上线前必查的五类问题:审批流跑不通时的排查清单

流程跑不通,报错信息往往只有一句「操作失败」,光看日志很难定位。下面这五类是我在不同项目里反复遇到的,按现象、原因、解决三段记,遇到时照着对一遍,比盲翻代码快。

5.1 现象:提交成功但待办列表空的,审批人解析返回 null

现象是单据提交回显成功,流程实例也建了,但谁的待办里都看不到。原因是审批人策略解析出空集合,引擎拿到空列表后没抛错,直接跳过了任务生成,实例停在第一个审批节点上。

解决分三步:先查实例表的current_node是否为空,为空说明连节点都没定下来,看流程定义的开始节点连线有没有配;不为空就查该节点配置的approver_type,逐类核对取值——ROLE看角色编码是否存在、DEPT_LEADER看部门树的leader_id、FORM_FIELD看表单里那个字段提交的值是不是空的。最后在assigneeResolver里加一条空集合抛异常的兜底,让问题提前暴露。

5.2 现象:会签节点只生成一条任务

节点明明配了会签,审批人解析出三个人,但任务表里只有一条。原因通常是任务生成的去重逻辑写错了维度,把instance_id + node_key当成了唯一键,同一个节点只允许一条任务,后面两个人直接被丢掉。

解决是把去重维度改成instance_id + node_key + assignee,同时检查任务表上有没有(instance_id, node_key, assignee)的唯一索引——有的话插入第二条会直接抛主键冲突,没抛就说明代码层做了静默忽略。另外确认multi_mode的取值在解析时真的被读到了,有些包前端传的是all,后端枚举是大写ALL,大小写不一致就默认走了或签。

5.3 现象:流程改版后,在途单据点开报「节点不存在」

业务流程从三级审批改成两级,新版发布后,改版前提交、还停在第三级的单据全部打不开。原因是实例表只存了流程定义 ID,没存版本号,运行时按 ID 去取最新版本的节点配置,自然找不到旧节点。

解决要在数据层做两件事:实例表补def_version字段,发起时写入当时发布的版本号;节点查询按def_id + version双条件取,不要只按def_id。存量数据补版本号时,按实例创建时间去比对发布记录,取当时生效的版本,别一刀切填成当前版本。这件事没有后悔药,只能在设计阶段就想好。

5.4 现象:回退之后条件分支走错方向

单据从人事节点退回主管,主管重新通过后,本该走「三天以内直接结束」的分支,结果又进了人事。原因是回退时清空了流程变量,或者回退后重新求值时用的是节点局部变量而不是实例级变量,#days取不到值,条件全部不成立,掉进了兜底分支。

解决是先确认变量存在哪一层,实例级变量在回退时不能清,节点级变量可以清。条件表达式里的变量统一改成实例级引用,表单提交时一次性写入,中间节点只读不写。如果引擎支持表达式调试,把回退后那次求值的上下文打出来看一眼,比猜快得多。

5.5 现象:本地一切正常,部署后审批时间差八小时

本地跑得好好的,上服务器后审批记录的时间全偏了八小时,排序全乱。原因是数据库连接的时区参数没配,或者应用容器的默认时区是 UTC,Java 侧new Date()和 MySQL 的NOW()差了八个小时。

解决是两头对齐:连接串加serverTimezone=Asia/Shanghai,JVM 启动参数加-Duser.timezone=Asia/Shanghai,建表脚本里时间字段统一datetime而不是timestamp(后者会二次转换)。改完记得把历史数据的时区也核对一遍,别只测新数据。

注意:这五条里前三条属于设计问题,改起来要动表或动代码;后两条属于配置问题,改完就能生效。接手一套新包,先按顺序把五条过一遍,能省掉上线后至少一半的返工。

6. 把流程交给业务自己改:版本管理、灰度发布与回归验证

流程能配之后,下一个问题很快会来:业务部门开始自己改流程,改完直接在线上发布,第二天出问题找不到是谁改的。这时候需要补的不是功能,是流程发布本身的规范。

我一般会强制三件事。第一,流程定义只允许新增版本,不允许原地改已发布的定义,改完的版本状态是草稿,走一次「发布」动作才生效,发布人要留痕。第二,版本切换做灰度:同一流程编码下允许多个版本并存,按发起人所属部门或指定用户组分流,先让一个部门跑新版本,跑三天没问题再全量。第三,每次发布前跑一遍回归用例,至少覆盖三类单据:金额落在每个条件分支边界的、会签节点有人驳回的、发起后撤回的。

-- 发布前检查:是否有实例仍停在旧版本,评估改版影响面 SELECT def_version, COUNT(1) AS running_cnt FROM oa_process_instance WHERE def_code = 'leave' AND status = 'RUNNING' GROUP BY def_version ORDER BY def_version;

这条查询用来判断新旧版本并存的规模。如果旧版本还有几十条在途,新版本一上线就得保证旧版本的节点配置不被清掉——很多包在发布时会把旧版本的节点数据逻辑删除,结果在途单据跑到一半找不到下一节点,只能人工补数据。

灰度分流我通常不放在流程引擎里做,而是放在发起入口:按starter的部门 ID 取模,命中灰度组的走新版本,其余走旧版本。这样引擎本身不用改,出问题把分流开关一关就回去了,比在引擎里加一堆判断干净得多。

回归验证这块,手工点太慢,我一般写一个脚本直接调接口:建单、审批、查状态,把每条分支跑一遍,断言最终节点和任务数量。这套脚本跟着版本走,每次发布前跑一次,比人肉点强太多。

血泪经验是有一回图省事,直接在已发布的流程定义上改了条件表达式的值,没走版本。改完十分钟,正在审批中的单子按新条件重新求值,一批本该走总监的单子直接跳到结束,事后靠审批流水一条条往回捞。从那以后我每次改流程定义,不管改的是一个字段还是一行表达式,都强制走「复制新版本 → 改 → 灰度 → 全量」这条链路,一次都没再跳过。流程配置看着是数据,可它承载的是审批责任,改错了不是显示问题,是有人该审的没审。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询