资讯详情

基于自研轻量流程引擎的OA系统源码:审批流程可视化配置与落地实践

📅 2026/10/25 14:40:06 | 华诺云谱 👁 阅读
基于自研轻量流程引擎的OA系统源码:审批流程可视化配置与落地实践
简介这套OA办公自动化源码面向需要自建或二次开发审批流程的开发者与技术团队提供一套可运行、可改造的完整实现涵盖请假、报销等日常审批业务的流程设计与权限配置思路。压缩包约43.54MB共近2000个文件以gif图片、C#源码.cs、aspx页面为主辅以js脚本、css样式、dll程序集与doc说明文档另有csproj、sln工程文件。目录按典型三层架构拆分Web与MobileWeb分别对应桌面端和移动端界面TMOA.BLL承载审批流转与业务规则TMOA.Common提供公共工具类TMOA.DBUtility负责数据库访问DB_51aspx存放示例数据。目前已有6933人学习。读者可据此理清OA系统的模块划分与分层调用关系掌握审批流程的编码方式并在其基础上替换业务字段、扩展流程节点搭建贴合自身管理需求的办公平台。1. 需求切入审批流程一变就得改代码这套 OA 源码要解决的就是这件事某公司的行政专员每个季度都要调整一次报销审批链路——部门经理换了、金额阈值调了、新增了财务复核节点每次都得提需求给开发改代码、走测试、等发版一来一回两周。一年下来光审批流程变更就压了十几张工单。这类需求在办公自动化场景里非常典型业务规则天天变但代码写死了。一套完整的 OA 系统源码真正的价值不在于功能清单有多长而在于审批流程能不能由业务人员自己画、自己发布、自己验证。这套源码围绕「审批流程可视化配置」展开适合正在做 OA 毕设或课程设计的学生也适合要给中小团队交付一套可维护办公系统的开发者。下面从架构讲到实现再到怎么验收。2. 总体架构设计分层结构与流程引擎的职责边界在动手写第一行代码之前有一件事必须先确定审批流程的「可变部分」和「不变部分」到底怎么切。如果流程引擎和业务代码混在一起那所谓「自己设计审批流程」就只是一句口号最后还是会退化成每加一个审批节点就动一次 Java 代码。这一章先把分层和边界定清楚后面所有模块都围绕它展开。2.1 技术选型为什么用轻量自研引擎而不是直接上 Flowable先看一张对比表把两条路的取舍摆到台面上。维度自研轻量流程引擎Flowable / Activiti学习成本只需要理解自己定义的 JSON 模型需要掌握 BPMN 2.0 规范与引擎 API源码可控性全部逻辑可见改一行就生效引擎内部黑盒较多出问题排查链路长二次开发与业务表直接关联扩展方便需要理解引擎表结构扩展有耦合适用场景中小团队 OA、毕设课设、流程相对固定大型企业、复杂并行网关、跨系统编排演示效果能讲清每一步怎么流转容易变成「调 API」讲不出设计对一套要交作业、要交付给中小团队的 OA 系统来说自研轻量引擎的优势非常明显所有状态都在自己的数据库里任何一个审批动作都能顺着表查下去答辩时能把「为什么这么设计」讲透。Flowable 不是不好而是它的复杂度在中小 OA 里属于过度设计玄学问题一多反而压不住。技术栈上前端用 Vue 3 Element Plus流程设计器基于画布组件自行封装后端 Spring Boot 2.7 MyBatis-Plus数据库 MySQL 8缓存用 Redis 存待办计数和会话。整体分四层接入层Controller 做参数校验与权限拦截不写业务逻辑应用层Service 编排流程发布、任务流转、消息通知领域层流程定义、流程实例、任务三个聚合根各自有独立的状态机基础设施层Mapper、Redis、站内信通道。这个分层的价值在于流程引擎只依赖领域层换数据库、换前端框架都不影响核心流转逻辑。2.2 模块划分与核心数据表设计按职责把系统拆成七个模块每个模块对应一组表认证权限用户、部门、角色、菜单权限表单中心表单模板的增删改查与版本管理流程定义中心流程建模、校验、发布、停用流程运行中心实例启动、节点流转、任务创建待办中心我的待办、已办、我发起的通知中心站内信、邮件可选审计日志所有审批动作留痕。真正撑起「可自定义」的是下面这几张表字段设计直接决定后面代码好不好写。表名关键字段作用oa_form_templateform_key, name, schema_json, version, status表单模板schema_json 存字段定义oa_process_defprocess_key, name, version, definition_json, form_key, status流程定义definition_json 存整棵节点树oa_process_instanceprocess_def_id, business_key, initiator, status, current_node流程实例一条业务单据对应一条oa_taskinstance_id, node_key, assignee, assignee_type, status待办任务会签时一个节点多条oa_task_recordtask_id, operator, action, comment审批记录用于追溯和退回oa_approver_rulerule_key, rule_type, rule_config审批人解析规则可复用建表时有两个细节容易忽略。第一definition_json用 JSON 类型存储而不是拆成多张节点表因为流程定义一旦发布就不应该再改拆表反而增加联表成本第二oa_task里必须冗余node_name否则历史待办在流程定义升级后会显示错乱。核心建表语句大致如下CREATE TABLE oa_process_def ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_key VARCHAR(64) NOT NULL COMMENT 流程标识如 expense_apply, name VARCHAR(128) NOT NULL COMMENT 流程名称, version INT NOT NULL DEFAULT 1 COMMENT 版本号发布一次加一, definition_json JSON NOT NULL COMMENT 节点树与连线, form_key VARCHAR(64) NOT NULL COMMENT 绑定的表单模板, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已发布 2已停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_key_version (process_key, version) ) COMMENT 流程定义表;process_key version做唯一索引是为了保证同一流程的版本不重复实例启动时锁定具体版本后续流程定义再怎么改都不影响在途单据。这一点在答辩时经常被问到属于必须讲清楚的设计点。3. 审批流程建模节点、条件分支与审批人规则架构定完接下来是整套系统最核心的部分——怎么用一份数据把「谁在什么条件下审什么」描述清楚。建模做得好后面代码就是按图索骥建模做不好条件分支和审批人规则会缠成一团改一处崩三处。3.1 流程定义模型用一棵 JSON 树描述整条审批链节点类型只保留五种够用且不易失控start发起、approval审批、condition条件分支、cc抄送、end结束。每个节点带nodeKey、nodeName、type审批节点额外带approverRule。节点之间用nextNodeKey串联条件节点用branches数组表达多路。{ processKey: expense_apply, nodes: [ { nodeKey: start, type: start, nextNodeKey: dept_leader }, { nodeKey: dept_leader, type: approval, nodeName: 部门经理审批, approverRule: { type: DEPT_LEADER }, nextNodeKey: amount_condition }, { nodeKey: amount_condition, type: condition, branches: [ { expression: ${amount 5000}, nextNodeKey: finance }, { expression: default, nextNodeKey: end } ] }, { nodeKey: finance, type: approval, nodeName: 财务复核, approverRule: { type: ROLE, roleKey: finance }, nextNodeKey: end }, { nodeKey: end, type: end } ] }这份 JSON 有几个设计取舍值得说。第一用nextNodeKey单向链表而不是邻接矩阵是因为 OA 流程绝大多数是串行加少量分支链表足够表达且渲染简单第二条件节点的expression用${}包裹解析时只支持字段比较和逻辑运算不引入完整表达式引擎避免执行任意代码的安全问题第三default分支必须存在防止条件全部不满足时流程卡死。参数上amount这类变量来自表单数据启动实例时会把表单字段平铺进上下文条件判断直接读上下文。3.2 条件分支与审批人解析让「谁来审」变成可配置规则审批人规则是整个建模里最容易被低估的部分。很多毕设做到这里就写死「部门经理」结果一演示就露馅。合格的规则至少要覆盖五类USER指定具体用户适合法务、财务等固定岗位ROLE按角色匹配一个角色多人时进入候选池DEPT_LEADER取发起人所在部门的负责人FORM_FIELD审批人由表单里选的人决定适合「指定证明人」场景INITIATOR_SELECT发起时自己选适合临时加签。解析逻辑放在独立的ApproverResolver里每种类型对应一个策略实现新增类型只需要加一个实现类不改主流程。下面是一段解析核心代码public interface ApproverResolver { String getType(); ListLong resolve(ApproverRule rule, ProcessContext ctx); } Component public class DeptLeaderResolver implements ApproverResolver { Override public String getType() { return DEPT_LEADER; } Override public ListLong resolve(ApproverRule rule, ProcessContext ctx) { // 从上下文拿到发起人再查其部门负责人 Long initiator ctx.getInitiator(); Long deptId userMapper.selectDeptIdByUserId(initiator); Long leaderId deptMapper.selectLeaderByDeptId(deptId); if (leaderId null) { throw new BizException(部门未配置负责人流程无法流转); } return Collections.singletonList(leaderId); } }ProcessContext里带着发起人、表单数据、当前实例解析器只读不写保证幂等。这里有个血泪经验部门负责人为空时必须显式抛异常而不是返回空列表让流程静默卡住——前者在待办页能看到错误提示后者只能靠人肉查库排查成本差十倍。3.3 表单与流程解耦字段驱动流转而不是流程写死字段表单和流程必须解耦。做法是表单模板独立维护流程定义只存一个form_key启动实例时按 key 取当前启用版本的表单 schema把用户填的值存进oa_process_instance.form_dataJSON 字段。这样同一套「报销流程」可以绑定不同表单同一张表单也能被多条流程复用。解耦带来的直接好处是条件表达式可以统一从form_data取值。比如${amount 5000}中的amount就是表单里的字段名建模时在设计器里下拉选择字段避免手写字段名拼错。字段类型也要约束只有数字和日期类型允许参与大小比较字符串只支持等值判断枚举走in判断。这些约束写进流程发布的校验器里发布前就拦住非法表达式比运行时报错友好得多。4. 落地实现从流程发布到任务流转的前后端关键代码建模是纸面设计这一章把它变成能跑的系统。顺序上先讲后端发布与流转再讲前端设计器最后补会签、或签、退回这些容易被跳过的细节。每一段代码都给关键注释方便直接对照改。4.1 流程定义发布校验、存版本、生成节点快照发布接口做三件事校验 JSON 合法性、写入新版本、把节点树展开成快照缓存。校验包括节点 key 唯一、nextNodeKey必须存在、条件节点必须有 default 分支、审批节点的审批人规则必须能解析出至少一种策略。Transactional(rollbackFor Exception.class) public Long publish(String processKey, String definitionJson, String formKey) { // 1. 解析并校验节点树 ProcessDefinition def JSON.parseObject(definitionJson, ProcessDefinition.class); validator.validate(def); // 节点唯一性、连线完整性、规则可解析性 // 2. 取当前最大版本号加一后落库 Integer maxVersion processDefMapper.selectMaxVersion(processKey); int newVersion (maxVersion null ? 0 : maxVersion) 1; ProcessDefPO po new ProcessDefPO(); po.setProcessKey(processKey); po.setVersion(newVersion); po.setDefinitionJson(definitionJson); po.setFormKey(formKey); po.setStatus(1); processDefMapper.insert(po); // 3. 旧版本置为停用但保留在途实例继续使用 processDefMapper.disableOtherVersions(processKey, newVersion); return po.getId(); }关键点在第三步旧版本不能删因为在途实例还引用着它。停用只影响新发起的单据这是版本管理里最容易翻车的地方很多人图省事直接覆盖结果在途单据流转到一半找不到节点定义。参数上processKey在整个系统内唯一version自增status用 0/1/2 三态而不是布尔值是为了给「草稿」留位置——设计器保存但不发布时走 0 状态。4.2 任务流转核心下一节点计算与审批人解析审批动作的入口只有一个complete(taskId, action, comment)。它要根据当前节点算出下一节点解析审批人生成新任务或结束实例。核心逻辑拆成三步下面这段是主干Transactional(rollbackFor Exception.class) public void complete(Long taskId, String action, String comment) { OaTask task taskMapper.selectById(taskId); ProcessInstance inst instanceMapper.selectById(task.getInstanceId()); ProcessDefinition def definitionCache.get(inst.getProcessDefId()); // 1. 记录本次审批动作退回和同意都走这里 recordService.save(task, action, comment); // 2. 根据动作决定下一节点同意走 nextNodeKey退回走历史节点 String nextNodeKey REJECT.equals(action) ? historyService.findPrevNode(inst.getId(), task.getNodeKey()) : resolveNext(def, task.getNodeKey(), inst.getFormData()); // 3. 到达结束节点则关闭实例否则解析审批人并建任务 if (end.equals(nextNodeKey)) { instanceMapper.finish(inst.getId()); } else { ProcessNode node def.getNode(nextNodeKey); ListLong approvers approverService.resolve(node.getApproverRule(), inst); taskService.createTasks(inst, node, approvers); instanceMapper.updateCurrentNode(inst.getId(), nextNodeKey); } taskMapper.finish(taskId); }resolveNext处理条件节点时会把form_data平铺进上下文逐条求值命中即返回createTasks内部按审批人数量决定建一条还是多条会签场景在下一节展开。这里要强调事务边界记录、流转、建任务必须在同一个事务里中途失败要整体回滚否则会出现「有审批记录但任务没生成」的脏数据这类问题在演示时特别致命。4.3 前端可视化设计器拖拽生成 JSON 并回显设计器不追求功能大而全把五类节点和连线做稳就够了。实现上左侧节点面板、中间画布、右侧属性面板三段式画布用绝对定位 SVG 画连线。拖入节点时生成nodeKey拖拽结束时计算nextNodeKey属性面板修改审批人规则时实时更新 JSON。// 节点拖拽结束后重建连线关系并同步到 store function rebuildEdges(nodes, edges) { const map new Map(nodes.map(n [n.id, n])); edges.forEach(e { const from map.get(e.source); const to map.get(e.target); if (!from || !to) return; // 条件节点用 branches 表达多出边普通节点直接接 nextNodeKey if (from.type condition) { from.branches from.branches || []; from.branches.push({ expression: e.expression || default, nextNodeKey: to.nodeKey }); } else { from.nextNodeKey to.nodeKey; } }); return nodes; }这段逻辑的关键是把画布上的「边」翻译成数据模型里的「下一节点」。条件节点有多条出边用branches数组承载普通节点只有一条出边直接写nextNodeKey。回显时反向操作遍历branches和nextNodeKey画出连线。设计器保存前调用后端的校验接口把错误标红在节点上避免用户点了发布才发现问题。4.4 会签、或签、退回与加签的处理方式这四种动作是 OA 的「深水区」处理不好前面所有设计都会白费。会签一个节点解析出多个审批人时建多条oa_task节点状态挂一个counter字段记录「已完成/总数」全部通过才流转任一驳回则整个节点驳回或签同样建多条任务但任意一条完成即关闭同节点其余任务用 Redis 分布式锁防止两人同时点通过退回不删记录从oa_task_record里找到上一个审批节点重新生成任务实例的current_node回退但保留历史记录加签在当前节点之前插入一个临时审批任务用parent_task_id标记归属完成后回到原节点继续。或签的并发控制要单独说一句。两个人同时打开待办点通过如果不加锁会出现两条任务都完成、节点被流转两次的情况。做法是用instance_id node_key作为 Redis 锁的 key抢到锁的线程执行流转没抢到的直接返回「该任务已被他人处理」。5. 避坑与常见问题排查五类最容易翻车的点流程引擎这类系统功能演示时往往很顺一到并发或者版本切换就原形毕露。下面五条是实际项目里踩出来的每条按「现象 → 原因 → 解决」写对照排查能省不少时间。5.1 待办列表显示旧节点名现象流程定义升级后历史待办页显示的节点名称变成乱码或空白。原因oa_task只存了node_key展示时去当前最新版流程定义里查节点名而新版本可能已经删掉了该节点。解决任务创建时把node_name冗余进任务表展示直接读冗余字段。流程定义只作为流转依据不作为展示依据。同理form_key也要在实例上冗余一份版本号。5.2 条件表达式解析出空指针现象发起单据时报NullPointerException日志指向表达式求值。原因表达式里的字段名和表单字段对不上上下文取值为 null直接参与比较就炸了。解决求值前先做字段存在性校验缺失字段一律视为不满足分支走 default。发布时校验表达式里的字段必须在绑定表单的 schema 中存在把错误拦在发布阶段。5.3 会签节点永远不结束现象会签节点所有人都审完了流程还是停在那里。原因counter字段更新用了「先查后改」的非原子操作并发下少加了一次或者审批人列表里有重复用户 ID导致任务数与预期不一致。解决counter用UPDATE ... SET done done 1 WHERE id ?原子自增创建任务前对审批人列表去重结束判断用done total而不是done total留一点容错空间。5.4 退回后重新提交进入死循环现象单据退回给发起人改完重新提交又回到了错误的节点。原因重新提交时复用了原实例的current_node而这个值在退回时改成了发起节点重新提交没有按流程定义重新计算。解决退回只改实例状态为「已退回」重新提交时新建一次流转从 start 节点重新走一遍历史记录通过parent_instance_id关联。虽然多一条实例记录但状态清晰不会绕晕。5.5 停用流程导致在途单据报错现象管理员停用了旧版本流程在途单据点审批时提示「流程定义不存在」。原因流转时按process_key取最新启用版本而不是按实例绑定的process_def_id取。解决实例启动时锁定process_def_id后续所有流转都用这个 ID 查定义与流程是否停用无关。停用只影响新发起不影响在途。这条是版本管理里最容易写错的地方代码 review 时要专门看一眼。6. 验收与答辩怎么证明「流程可自定义」真的成立验收这套系统别只演示一次「发起→审批→结束」那证明不了可自定义。真正有说服力的验收是现场改一条流程不改代码、不重启服务让流程立刻生效。下面这张清单可以照着走一遍验收项操作通过标准新增审批节点在设计器里加一个「总监审批」并发布新发起的单据多出该节点旧单据不受影响修改条件阈值把金额阈值从 5000 改成 3000 并发布3000 以上的单据走财务分支切换审批人规则把部门经理改为按角色匹配审批人变化且历史记录仍显示原审批人退回重提发起后被退回修改后重新提交从发起节点重新走历史记录完整保留停用旧版本停用 v1 后审批在途单据在途单据正常流转新单据走 v2答辩时最容易被追问的三个问题提前准备好答案一是「流程定义改了在途单据怎么办」答版本锁定二是「两个人同时审批同一个节点会怎样」答分布式锁 原子计数三是「条件表达式会不会被执行恶意代码」答只支持字段比较、不引入表达式引擎。这三个问题答得出来基本就说明设计想清楚了。有个习惯我后来一直保留每次演示前先用一套造好的测试数据把上面五条验收项跑一遍尤其是退回重提和版本停用这两条最容易在演示现场翻车。有一次就是没跑退回现场点下去流程卡住只能尴尬地说「这是网络问题」。从那以后我每次交付前都强制走一遍验收清单不再靠临场发挥。希望这套方案和清单能帮到你把这套 OA 系统稳稳地交出去。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑