
简介一套完整的OA系统源码面向需要灵活定制审批流程的开发者与企业用户覆盖请假、报销、用印等典型办公自动化场景。资源包共2000个文件压缩后仅43.54MB核心文件类型包括ASP.NET页面、C#业务逻辑、JavaScript交互脚本、Word文档说明以及图片素材其中演示动图对功能操作有直观展示另附Excel表格模板和数据库备份同时提供移动端与Web端两套入口配套数据访问类、业务逻辑层、公共工具模块等分层结构。目前已有6931人学习下载。源码系从51aspx购买后分享架构相对完整开发者可重点研读审批流程引擎、角色权限控制和数据库访问层实现借助示例数据库快速搭建测试环境按需修改节点配置与表单设计内容预览可见系统登录、图片上传、角色管理等典型模块既可直接用于项目二次开发也适合作为OA系统的教学参考。1. 一套OA系统源码真正值钱的地方审批流程能不能让业务自己改做协同办公的人应该都有这种经历行政拿来一张纸质请假单说“帮我在OA里加个流程”你打开代码一翻发现审批节点写死在业务代码里改一条流程要动前后端、重新发版。到头来一个OA系统最耗开发资源的不是考勤、不是会议而是没完没了的审批流程调整。所谓“一套完整的OA系统源码可自己设计审批流程”核心卖点并不是源码本身而是源码里那套可视化流程设计器。有了它配置审批流的操作从“找研发排期”变成“行政自己在画布上拖两个节点”业务部门自己维护流程IT只负责兜底。这篇文章围绕同一个目标展开如何基于一套开源或自研的OA源码把审批流程从“硬编码”改造成“可配置”。先拆清流程引擎的建模思路再落到数据库设计、前后端设计器实现、运行时引擎解析最后记录几个让新手最容易翻车的真实排错现场。如果你正准备二次开发一套OA源码或者想在公司内部重做审批中心,这篇文章能帮你把值钱的部分看清楚。2. 选型与拆分自研轻量流程引擎还是外接重型工作流框架2.1 为什么我不建议直接套用重型工作流框架市面上成熟的工作流引擎很多选型时团队容易直接奔着功能全的去。但拖进OA源码里试过之后会发现重型框架的学习成本和改造成本远高于预期。它确实把会签、或签、条件分支、撤回、委派这些概念全都做了可你要让它跟现有组织架构、角色体系、数据权限打通就得写一堆适配层。最尴尬的是你只是需要审批流不是要一个完整的工作流平台。我反复对比后的结论是OA系统的审批流适合用自研的轻量状态机引擎只实现开始、审批、条件分支、会签、或签、抄送、结束这几种节点。理由有四点。第一OA审批流的节点类型非常固定不需要复杂的流程编排第二轻量引擎的核心代码量控制在2000行以内团队任何一个人都能看懂第三出问题时能直接定位到自己的代码不需要去翻框架源码第四审批流的运行数据要跟业务表单强关联自研引擎可以按自己的表结构控制数据不需要迁就框架的通用模型。2.2 OA源码的模块划分与核心数据流向一套能自己设计审批流程的OA源码从工程上拆成五个模块组织权限模块、表单模块、流程设计器、流程引擎、消息与待办中心。组织权限提供部门和人员树表单模块负责动态生成字段流程设计器把用户在画布上拖出来的节点保存成JSON定义流程引擎负责读取定义、推进节点、生成待办消息与待办中心负责把待办推给对应审批人。数据流向是一个闭环业务人员发起审批时前端提交表单数据和流程定义ID后端创建流程实例然后把表单数据绑定到流程实例上引擎根据流程定义的第一个节点开始执行判断节点类型、解析审批人、生成审批任务审批人处理完后引擎根据路由规则找到下一个节点直到走到结束节点。整套逻辑里最核心的就是那个流程定义JSON它既是设计器的产物又是引擎的输入也是后续流程版本管理的基本单位。2.3 技术栈组合与部署形态我经手的几个OA二次开发项目后端用Java Spring Boot的比例最高前端用Vue3数据库MySQL缓存Redis用于待办计数和会话。工作流引擎部分不引第三方自主实现。如果团队是PHP或Node技术栈思路完全一样只是语言不同核心表结构和状态机逻辑可以平迁。部署上要特别注意不要把流程设计器功能和审批执行功能拆成两个服务。流程定义需要频繁修改而流程实例运行时需要读取旧版本定义拆成微服务会引入版本同步的复杂度。单体应用、单库、按模块拆包是这个体量最稳的形态。等流程实例量日均过万再考虑拆分。3. 核心建模审批流程的表结构设计决定上层能走多远3.1 核心表结构流程定义、流程实例、审批任务各自的职责流程定义表负责存储设计器产出的JSON和基础元数据流程实例表是某一次具体审批的所有上下文审批任务表是当前正在等待处理的待办。它们之间的关系是一对多一个定义可以被发起多个实例一个实例按顺序产生多个任务。设计时注意不要把表单数据直接存在流程实例表里表单字段是动态的存在单独的表或JSON列里更合适。核心表结构建议按下面这张表建字段名和类型可以直接复用表名关键字段职责flow_definitionid、name、version、status、definition_json、create_time存储流程画布JSON一个流程可多个版本flow_instanceid、definition_id、definition_version、form_data_json、current_node_id、status、initiator_id、create_time存储一次审批的运行时上下文包含表单数据flow_taskid、instance_id、node_id、task_name、assignee_id、status、create_time、finish_time审批任务每个待办一条记录flow_task_historyid、instance_id、node_id、operator_id、action_type、comment、create_time审批历史用于追溯和回显这里我踩过一次坑把审批历史直接写在flow_task里任务结束就更新状态导致审批记录丢失。正确做法是任务表只保留当前待办一旦处理完就把记录写进历史表任务表删除或标记完成。否则后续做审批轨迹回显时数据残缺还得从头补。3.2 节点类型与路由规则的建模方式流程定义JSON里节点类型必须有明确枚举。我通常用这么几种start发起人、approval单人审批、countersign会签需多人审、orsign或签一人通过即可、condition条件分支、cc抄送、end结束。这七种基本覆盖了OA里90%的场景再多就是给自己找麻烦。路由规则分两种。一种是线路由节点之间靠边连接边上可以挂条件表达式匹配下一个节点另一种是节点内路由比如条件节点内挂多个分支。经验之谈只做线路由不做节点内路由会省掉一大波解析逻辑。画布上虽然看起来是节点连节点但存储时每条连线就是一条路由记录目标节点只可能来自连线指向。在线路由的建模上我的习惯是在边对象上带from和to两个字段。审批流走到某个节点时引擎收集所有from等于当前节点的边逐个判断条件表达式找到第一个表达式为true的边把to作为下一个节点。没有条件表达式的边就是默认路由必须保证每个节点最多有一条默认路由否则引擎会随机选线上必出事故。3.3 表单与流程的绑定方式表单数据和流程引擎不能完全解耦因为条件分支需要根据表单字段取值做路由判断。最省事的做法流程定义里带一个formFields数组声明哪些表单字段参与路由判断引擎在创建实例时从表单数据中提取这些字段到实例上下文。设计中还有两个细节值得注意。一是表单字段的name要稳定前端可以改label但不能随意改name否则历史实例回显会错位。二是发起人、部门、金额、日期这类内置字段在引擎上下文里要能直接引用。我处理的方式是表单数据统一存JSON列但发起人ID、发起人部门ID、发起时间会冗余到flow_instance的独立字段上路由表达式既能取表单字段也能取这些内置字段这样条件分支写起来才够灵活。4. 从设计器到执行引擎审批流程自研的关键代码实现4.1 前端设计器的数据结构与画布保存逻辑流程设计器的前端本质是一个可视化编辑器。节点是拖拽组件连线用svg或canvas绘制每次拖拽和连线都在操作一份内存中的JSON。这个JSON的格式要尽早固定下来前端按这个格式生成后端按这个格式解析字段名错一个字符解析就全乱。保存时前端传回的数据结构可以参照下面的格式{ processName: 请假审批, nodes: [ { id: node_start, type: start, name: 发起人 }, { id: node_lead, type: approval, name: 直属领导审批, approver: { type: dept_leader }, rejectPolicy: back_to_initiator, allowAddSign: true }, { id: node_admin, type: approval, name: 行政确认, approver: { type: role, value: admin_role_id }, rejectPolicy: back_previous }, { id: node_end, type: end, name: 结束 } ], edges: [ { id: edge_1, from: node_start, to: node_lead, condition: null, isDefault: true }, { id: edge_2, from: node_lead, to: node_admin, condition: ${days} 3, isDefault: false }, { id: edge_3, from: node_lead, to: node_end, condition: ${days} 3, isDefault: false } ] }这里有三个协议约定需要在设计器里写死。第一所有节点ID必须全局唯一由前端生成生成规则用前端前缀加时间戳不允许后端再生成否则后端解析时连线指向会错乱。第二每条边最多一个isDefault为true的边保存时前端就要校验默认边唯一性。第三condition表达式统一使用${字段名}引用表单字段运算符只支持大于、小于、大于等于、小于等于、等于、不等于、包含这些常规操作避免引入太复杂的表达式语法。4.2 后端流程定义的解析与校验后端拿到JSON后不能直接入库要先做一轮完整性校验。校验内容包括是否刚好一个开始节点和一个结束节点所有边引用的from和to节点是否真实存在条件边的表达式能否解析非默认边是否都带条件节点是否全部可达。这五项校验缺一不可否则线上会出现流程走到一半找不到下一个节点的“死流程”。我一般用下面这段Java逻辑处理节点引用的完整性校验public void validateDefinition(FlowDefinition def) { MapString, FlowNode nodeMap def.getNodes().stream() .collect(Collectors.toMap(FlowNode::getId, n - n)); ListFlowEdge edges def.getEdges(); if (nodeMap.values().stream().filter(n - start.equals(n.getType())).count() ! 1) { throw new BizException(流程必须包含且仅包含一个发起节点); } if (nodeMap.values().stream().filter(n - end.equals(n.getType())).count() ! 1) { throw new BizException(流程必须包含且仅包含一个结束节点); } for (FlowEdge e : edges) { if (!nodeMap.containsKey(e.getFrom()) || !nodeMap.containsKey(e.getTo())) { throw new BizException(连线存在悬空节点: e.getId()); } } // 随后校验可达性从 start 节点做一次广度优先遍历 // 若存在节点未被访问说明该节点没有入口路由属于不可达节点 }参数说明nodeMap用于快速索引节点时间复杂度O(1)避免校验边时反复遍历节点列表。每条边都检查from和to是否真实存在能拦截掉设计器因删除节点未同步删除连线导致的问题。可达性校验用的广度优先遍历从start节点出发沿着边走到不能再走为止若还有节点没被访问到直接拒绝发布新版本。4.3 审批人路由与兜底策略引擎最核心的执行逻辑校验通过后流程定义会被发布到正式环境。运行时引擎拿到一个流程实例和当前节点核心要回答两个问题这个节点该由谁来审批处理完之后下一跳往哪去。下一跳由边的条件表达式决定上一节已经说明这里重点写审批人解析逻辑。审批人的配置不能只有“指定某人”还得支持角色、部门负责人、发起人自选、表单字段指定人这几种方式。角色用于跨部门的固定审批节点部门负责人用于常见的“直属领导审批”发起人自选用于报销这类流程里财务审批人由发起人临时指定。下面是解析器的核心逻辑public ListString resolveApprovers(FlowNode node, FlowInstance instance) { ApproverConfig config node.getApprover(); ListString userIds new ArrayList(); String type config.getType(); switch (type) { case user: userIds.add(config.getValue()); break; case role: ListString ids userRoleService.getUserIdsByRoleId(config.getValue()); if (ids.isEmpty()) { throw new BizException(角色[ config.getValue() ]下没有用户请联系管理员); } userIds.addAll(ids); break; case dept_leader: String leaderId deptService.getLeaderIdByDeptId(instance.getInitiatorDeptId()); if (StringUtils.isBlank(leaderId)) { throw new BizException(发起人所在部门未设置负责人); } userIds.add(leaderId); break; case self_select: JSONArray selected instance.getFormData().getJSONArray(config.getValue()); if (selected null || selected.isEmpty()) { throw new BizException(未指定审批人流程已退回发起人); } selected.forEach(item - userIds.add((String) item)); break; } // 去重防止同一人被角色和部门负责人两种方式重复命中 return userIds.stream().distinct().collect(Collectors.toList()); }参数说明dept_leader和self_select是运行时解析必须挂在流程实例上下文上user和role是静态解析可在发布定义时预加载。最重要的一点解析完审批人为空时必须抛出明确异常并且引擎捕获后把流程实例状态置为挂起同时给管理员发通知。千万不要静默通过否则这一节点没有审批人流程会卡死而经办人那边完全不知道发生了什么。这种问题在测试环境很难发现因为测试数据里部门和角色都是全的生产环境才会暴露。5. 审批流程自研的常见坑条件分支、会签逻辑与驳回重报的真实排查记录5.1 条件分支解析翻车数字类型与表达式上下文引发的玄学问题现象是真实而典型的流程配置了金额大于5000走总经理审批小于等于5000走部门负责人审批测试时单笔金额5000以内没问题一旦超过5000就抛表达式解析失败。翻日志一看异常信息是类型不匹配表达式期望数字实际传入的是字符串。原因是表单页面输入框默认输出字符串后端存JSON时金额字段存成了9000而不是9000Spring表达式解析字符串与整数比较时直接报错。解决办法在流程引擎创建实例时做一个类型感知转换——参与条件判断的字段会先按流程定义里声明的fieldType做类型转换数字类型转成BigDecimal日期类型转成Date然后再放进表达式上下文。这里有另一个注意点前端把金额改成输入框后如果用户输入了千分位逗号转换时要把逗号去掉否则解析照样失败。5.2 会签与或签的判定错误通过率参数的隐藏问题会签节点的参数有个通过率比如“超过50%即通过”之类。踩过的坑是审批人总数为3人时2人通过就应该通过但代码实现时如果按百分比乘以总数再取整可能算出1.5取整为1导致1人通过就算会签成功。这就是用int类型处理带小数的通过率导致的严重误判。解决方式也简单通过率逻辑统一改成分数判断分子是已同意人数分母是总审批人数比较时用分子乘以100除以分母再跟通过率阈值比较。另外需要注意会签节点的审批人解析结果不应该去重因为并发审批时每个审批人独立操作去重会导致任务减少直接改变了审批基数。这个地方设计时务必写清楚。5.3 驳回再提交后的流程实例状态错乱一次典型的报障审批人驳回后发起人修改表单再次提交发现流程实例还是停留在原节点审批人列表里却同时出现了多个旧节点的待办记录。顺着数据看下来问题出在代码里发起人重新提交时new了一个审批任务却没有把旧的待办任务标记为取消。正确的处理方式驳回再提交时先把这个实例下所有状态为pending的任务全部置为cancel再重新生成新的审批任务。这样既不会出现重复待办也保证了审批历史链条清晰。如果驳回目标是“退回发起人”还要顺便把流程实例状态置为draft或者waiting_submit让发起人能重新编辑表单数据。5.4 兜底策略缺失导致的审批中断这个坑很典型某个流程的部门负责人节点把审批人配置为“发起人所在部门主管”但生产环境有部门没设置主管结果流程发起到这个节点直接中断。查看流程实例状态node_id停在这个节点但没有任务产生。问题根源是审批人解析器抛出的异常没有被处理引擎直接捕获异常后停止推进也没有任何降级方案。我要给出的解决办法是每个节点设置兜底人审批人解析失败时任务自动转到系统管理员同时把异常信息写入flow_instance的error_msg字段。这里梳理一下三种常用的兜底策略转管理员、自动通过、挂起等待人工介入。业务上最常用的是转管理员自动通过有风险静默挂起则会导致流程卡住。6. 进阶实践用流程仿真回放和版本灰度给审批流上双保险模拟仿真这个办法值得认真对待。所谓流程仿真就是在流程定义发布前用最近三个月的真实历史工单数据跑一遍回放读取某条已完结的工单提取它的表单数据套进新流程定义走一遍完整的节点解析逻辑最后对比模拟产生的审批路径与真实路径的重合度。重合度低于80%就有风险应当驳回定义变更高于95%基本可放心发布。这个机制能直接拦截掉条件分支配置错乱这类问题。仿真功能实现上不用太复杂引擎启动时加一个simulateMode开关开启后流程实例不落库、不产生真实任务只输出节点走向的日志。同一批历史数据的回放通过后再走版本灰度。流程定义表里要保留version字段发布新版本时旧版本继续运行已经发起的历史实例按版本号就低不就高——发起时用哪个版本就走到哪个版本完新发起才走新版本。上线初期只让测试部门用新版本业务高峰期不发布新流程定义这两个规矩务必守住。还有几个常用的习惯需要交代待办转办、加签流程、会签中途撤回这些功能要在设计器里预留开关位而不是等上线后需求来了再改数据结构。审批人配置里的兜底策略每个节点都要有不是可有可无的参数。发起人自选审批人的场景表单校验要提前拦住空值别让异常流到引擎层。这些经验和坑基本都是从踩坑记录里捞出来的。一次生产环境的事故让我彻底接受了“仿真回放必须走完一整轮”这个规矩某次流程变更因为条件分支的边界条件写错导致一批跨部门工单审批路径全部走错事后回滚流程定义花了三天才处理完所有受影响实例。自从仿真回放成了硬性规则这类问题再也没在发布后出现过。希望这些思路能帮你少走弯路。本文还有配套的精品资源点击获取