简介这套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.activedev# 5. 起前端注意接口代理指向后端实际端口 cd oa-web npm install --registryhttps://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_LEADERnode_hr用ROLE绑人事角色node_director用USER指定具体人但在生产环境建议换成角色。第五步发布版本号走v1。保存后先别急着用真实账号测用测试库造三个人发起人、主管、人事。发起一条days2的单子看是否停在主管节点再发days5的看主管审完是否生成人事任务最后发days10的走完全链路。三条都通这条流程才叫配好了。注意条件网关后面如果有两条连线条件同时为真且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()); ListString nextNodes flowResolver.resolveNext(inst, task.getNodeKey(), task.getVariables()); for (String nodeKey : nextNodes) { ListString 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 现象本地一切正常部署后审批时间差八小时本地跑得好好的上服务器后审批记录的时间全偏了八小时排序全乱。原因是数据库连接的时区参数没配或者应用容器的默认时区是 UTCJava 侧new Date()和 MySQL 的NOW()差了八个小时。解决是两头对齐连接串加serverTimezoneAsia/ShanghaiJVM 启动参数加-Duser.timezoneAsia/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 取模命中灰度组的走新版本其余走旧版本。这样引擎本身不用改出问题把分流开关一关就回去了比在引擎里加一堆判断干净得多。回归验证这块手工点太慢我一般写一个脚本直接调接口建单、审批、查状态把每条分支跑一遍断言最终节点和任务数量。这套脚本跟着版本走每次发布前跑一次比人肉点强太多。血泪经验是有一回图省事直接在已发布的流程定义上改了条件表达式的值没走版本。改完十分钟正在审批中的单子按新条件重新求值一批本该走总监的单子直接跳到结束事后靠审批流水一条条往回捞。从那以后我每次改流程定义不管改的是一个字段还是一行表达式都强制走「复制新版本 → 改 → 灰度 → 全量」这条链路一次都没再跳过。流程配置看着是数据可它承载的是审批责任改错了不是显示问题是有人该审的没审。希望帮到你。本文还有配套的精品资源点击获取