首页 / 资讯中心 / 文章详情

OA系统源码拆解:从数据库到引擎的审批流程自定义设计

OA系统源码拆解:从数据库到引擎的审批流程自定义设计 ★ FEATURED ARTICLE
简介一套完整的OA系统源码专为需要部署或二次开发办公自动化系统的开发者和企业信息化人员准备。项目围绕审批流程定制这一核心需求展开覆盖请假、报销等常见业务场景支持根据实际管理流程调整规则。压缩包共2000个文件约43.54MB包含619个cs源码文件、498个aspx页面、129个js脚本以及大量gif/png/jpg图片资源同时含db数据库文件和dll程序集基本呈现Web前端、业务逻辑层、数据访问层及移动端界面的完整结构。源码中还区分了Web端、移动端和公共工具模块便于按需学习与改造。已有6931人学习下载。通过学习这套源码可掌握OA系统的分层设计思路、审批流程的状态流转实现以及如何将自身管理规则编码到系统中尤其适合希望从零搭建或快速定制办公自动化平台的技术人员。1. 一套OA系统源码如何设计审批流程从固定流程到拖拉拽自定义拿到这套OA系统源码时我第一时间看的不是登录页有多炫也不是报表模块多花哨而是那个标注着“流程设计器”的模块。原因是办公自动化里最硬的需求从来不是考勤打卡而是业务表单在不同审批节点之间的流转方式完全随组织架构而变有的部门两审就行有的部门必须会签加终审。这套源码内建了流程定义、节点路由、表单绑定和版本发布的闭环支持通过拖拽方式配置审批链改完直接生效。后文逐层拆它的数据库模型、设计器交互和审批引擎的实现细节每一步都对照真实部署场景来讲看完就能判断这套OA系统源码值不值得直接落进自己的项目里。2. 审批流程的表单与节点模型先搞清数据库要存什么拿到源码第一件事不是跑前端而是翻开数据库脚本目录看它到底用几张表支撑“自定义审批流程”。如果连流程定义都没法脱离代码保存那所谓“自己设计审批流程”就是空话。这套源码用四张核心表把流程、节点、路由和业务数据分开设计思路很常规但关键字段处理得比较干净值得逐一说明。2.1 核心数据表流程定义、节点表、路由表的字段设计先看流程定义主表我习惯叫它 flow_definition。这张表承载一个流程的元信息包括流程编码、名称、版本号、状态以及两张关键的 JSON 大字段form_json 存表单定义route_json 存流程节点和连线的完整描述。源码中初始化脚本大致是下面这样CREATE TABLE flow_definition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_key VARCHAR(64) NOT NULL UNIQUE COMMENT 流程唯一标识比如 leave_flow、expense_flow, flow_name VARCHAR(128) NOT NULL COMMENT 流程业务名比如请假审批、报销审批, version_no INT NOT NULL DEFAULT 1 COMMENT 流程版本号每次发布自增, form_json TEXT COMMENT 动态表单定义JSON数组记录字段名、类型、是否必填, route_json TEXT COMMENT 流程图定义JSON对象记录节点列表和连线列表, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已发布 2已停用, created_by BIGINT NOT NULL, created_time DATETIME NOT NULL, updated_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批流程定义表;路由和节点也拆开了。node 表负责定义每个节点的类型、审批人来源、角色绑定route 表负责定义从哪个节点到哪个节点以及条件表达式。源码里这两张表的 DDL 大概是CREATE TABLE flow_node ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_id BIGINT NOT NULL COMMENT 关联flow_definition.id, node_key VARCHAR(64) NOT NULL COMMENT 节点内部编码如 start_node、approve_1、end_node前端绘图也引用这个值, node_type TINYINT NOT NULL COMMENT 1开始节点 2审批节点 3条件分支 4会签节点 5抄送节点 6结束节点, node_name VARCHAR(128) COMMENT 节点显示名称如部门主管审批, approver_type TINYINT COMMENT 1指定人员 2指定角色 3部门负责人 4发起人自选, approver_ref VARCHAR(512) COMMENT 人员ID、角色编码或部门字段路径多个用英文逗号分隔, form_perm TEXT COMMENT 该节点对Form表单字段的读写权限JSON格式, create_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批流程节点表; CREATE TABLE flow_route ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_id BIGINT NOT NULL, from_node_key VARCHAR(64) NOT NULL COMMENT 起始节点node_key, to_node_key VARCHAR(64) NOT NULL COMMENT 目标节点node_key, condition_expr VARCHAR(1024) COMMENT 条件表达式如 amount 5000解析后决定走哪条线, priority_no INT DEFAULT 0 COMMENT 同一from_node下多条连线的优先级数值越小越先判断, create_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批流程连线表;从字段设计能看出两点。第一node_key 是显式的字符串编码不是数据库的自增 id这是为了让前端设计器保存、刷新、回显时不丢失节点引用第二approver_ref 存的是“角色编码用户ID部门字段”混合引用引擎层需要根据 approver_type 来解析到底谁是当前节点的审批人。如果你把这两点忽略了后面大概率会踩到节点丢失、审批人匹配不到这种坑。2.2 动态表单数据存储从业务表到 KV 快照流程定义里存了 form_json这只解决了“表单长什么样”的问题业务数据怎么存是另一回事。这套OA系统源码没有为每个流程单独建业务表而是用了一张通用的业务数据快照表 flow_biz_data。它的设计好处是新流程不需要改表结构改起来非常像把表单字段摊开成 KV 对。CREATE TABLE flow_biz_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_instance_id BIGINT NOT NULL COMMENT 流程实例ID关联flow_instance表, biz_key VARCHAR(64) NOT NULL COMMENT 业务单据号如 BX20250501_00001作为业务隔离的持久化键, field_key VARCHAR(128) NOT NULL COMMENT 表单字段名对应form_json里的字段key, field_value TEXT COMMENT 字段值统一存字符串需要数字比较时在引擎里做类型转换, field_type TINYINT DEFAULT 0 COMMENT 0字符串 1数字 2日期 3布尔, create_time DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程业务数据表;这块最容易被忽略的是 biz_key 的作用。一次审批发起时生成一个 biz_key这个单据从第一步到终审结束的所有表单快照都在同一个 biz_key 下。如果二次发起时重新生成 biz_key旧数据就失去关联如果沿用旧的 biz_key提交时又可能覆盖第一条。源码里在 service 层专门做了 bizKey 与 flow_instance_id 的双向绑定后续审批引擎拿 bizKey 就能读取全部表单数据不用垮表 join。表单数据之所以用 KV 而不是横表核心原因是动态表单字段无法预知横表会在增加字段时频繁改结构。KV 模式的缺点也有比如查询效率低、复合条件统计麻烦但在这类轻量级 OA 源码场景里可维护性优先级明显高于查询性能。2.3 建表脚本与初始化数据的部署路径源码包里数据库脚本一般放在 project_root/sql/ 目录常见文件名是 01_schema.sql、02_init_data.sql、03_permission.sql。部署时直接导入即可mysql -uroot -p -h127.0.0.1 core_oa 01_schema.sql mysql -uroot -p -h127.0.0.1 core_oa 02_init_data.sql mysql -uroot -p -h127.0.0.1 core_oa 03_permission.sql导入后建议执行下面这条 SQL 检查四张核心表是否都建成功SHOW TABLES LIKE flow_%;正常情况应该看到 flow_definition、flow_node、flow_route、flow_instance、flow_biz_data 五张表。如果只看到前面三张说明你拉到的源码可能是简化版审批引擎缺少实例层流程并发和撤回功能大概率不完整。这套源码我在本地复现时是五张表齐全的后面章节以完整版为准继续拆解。3. 流程设计器前端拖拽画布与 JSON 模型的双向同步自定义审批流程真正价值体现的位置在“流程设计器”。打开设计器左侧是节点类型面板中间是可拖拽画布右侧是节点属性编辑区。左侧面板提供开始节点、审批节点、条件分支、会签节点、抄送节点、结束节点六个类型。设计器保存的数据是 JSON后端只负责持久化不做图形计算这是目前 OA 源码里最主流的实现方式。3.1 画布拖拽与节点渲染的实现思路源码前端的流程设计器基于 Vue 技术栈实现使用 SVG 画布渲染节点与连线。节点拖拽的核心逻辑并不复杂本质是监听 drop 事件在鼠标落点创建节点实例并写入画布数据模型。简化后的核心代码如下// FlowDesigner.vue export default { data() { return { nodeList: [], linkList: [], currentFlowId: this.$route.query.flowId } }, methods: { onDropNode(e) { const nodeType e.dataTransfer.getData(node_type) const rect this.$refs.canvas.getBoundingClientRect() const pos { x: e.clientX - rect.left, y: e.clientY - rect.top } const nodeKey ${nodeType}_${this.nodeList.length 1} this.nodeList.push({ key: nodeKey, type: nodeType, name: this.getDefaultName(nodeType), x: pos.x, y: pos.y }) } } }nodeKey 的生成规则直接使用 nodeType 加递增序号这样做比自增数字 id 更稳因为 nodeType 前缀能保证节点类型可读、可回溯。画布上的线条不是手动绘制的而是用户先按住节点侧边的小圆点拖到另一个节点上松开后触发 addLink 方法把 fromNodeKey 和 toNodeKey 记录进 linkList。这套设计器的巧妙之处在于它始终维护 nodeList 和 linkList 两个内存数组绘制和保存都基于这两份数据。保存时直接序列化整个数据结构打开流程时反序列化恢复不需要服务端渲染图片兼容性也更好。3.2 节点属性面板审批人、条件与权限的配置入口点击某个节点后右侧面板会显示对应的属性表单。审批节点的属性包括节点名称、审批人类型、审批人引用、字段权限列表条件分支节点则多一个条件表达式输入框。这个面板的输入直接绑定节点对象属性改动即生效最终随保存动作一次性提交。// NodePanel.vue 里的审批人配置片段 onApproverTypeChange(type) { this.currentNode.approverType type if (type 1) { this.approverRefPlaceholder 请输入人员ID多个用逗号分隔 } else if (type 2) { this.approverRefPlaceholder 请输入角色编码 } else if (type 3) { this.approverRefPlaceholder 请选择部门字段路径如 dept.managerId } }这里建议在面板层直接做表达式预校验。条件表达式是流程路由正确性的关键但很多源码把这个校验放在后端导致前端画图时并发热和分支条件错误直到发起人发起申请时才暴露。我自己复现时会主动在 NodePanel 的保存流程里加入一个 validateConditionExpr 方法用正则先匹配操作符和数字格式不对直接前端拦截不给后端制造脏数据。3.3 保存草稿与正式发布版本管理的代码视角这套OA系统源码把“保存”和“发布”分成了两个动作设计器上方有两个按钮保存草稿将表单和流程 JSON 写入 flow_definition 的 form_json 与 route_json 字段状态保持为 0发布则更新 version_no把状态改为 1并生成一份不可变的流程版本快照。async saveDraft() { const payload { flowId: this.currentFlowId, formJson: JSON.stringify(this.formDesigner.exportForm()), routeJson: JSON.stringify({ nodes: this.nodeList, links: this.linkList }) } await this.$http.post(/api/flow/design/save, payload) this.$message.success(草稿已保存) } async publishFlow() { const confirmResult await this.$confirm(发布后新的审批单将按新版流程执行是否继续) if (confirmResult) { const res await this.$http.post(/api/flow/design/publish/${this.currentFlowId}) if (res.data.code 0) { this.$message.success(流程已发布) } } }注意一个细节发布时前端只传 flowId后端通过查数据库拿 latest routeJson 与 formJson 生成新版前端不必把整个 JSON 再传一遍。这样做避免了前端数据与后端数据不一致的概率但代价是如果设计器里草稿还没有保存就点发布后端发布的是上一次保存的 JSON容易让用户产生困惑。建议实际使用时在发布接口里加一道校验最近是否有未保存的改动有的话先提示用户保存草稿再发布。4. 审批引擎核心发起、同意、驳回、转交的状态流转设计器画完只是开始真正决定一套OA系统源码能不能用的是审批引擎对节点状态的处理。引擎核心是状态机加事务控制涉及 FlowInstance 状态、当前节点游标、审批记录历史、条件路由判断四块逻辑。我拉源码建完库后第一件事就是打开 FlowEngine.java理解它的状态流转顺序。4.1 发起申请到首个审批节点的流转逻辑发起申请时的后端处理流程可以概括为校验流程已发布、创建流程实例、写入业务数据快照、推进状态到第一个审批节点。其中最关键的是提取开始节点出线并判断是否有条件分支。源码里核心方法大致如下// FlowEngine.java Transactional(rollbackFor Exception.class) public String startFlow(Long flowId, Long userId, MapString, Object formData) { FlowDefinition def flowDefMapper.selectById(flowId); if (def.getStatus() ! FlowStatus.PUBLISHED) { throw new BizException(流程未发布禁止发起); } FlowInstance inst new FlowInstance(); inst.setFlowId(flowId); inst.setVersionNo(def.getVersionNo()); inst.setBizKey(genBizKey()); inst.setState(FlowState.RUNNING); inst.setCurrNodeKey(start_node); flowInstMapper.insert(inst); for (Map.EntryString, Object entry : formData.entrySet()) { bizDataMapper.insert(inst.getBizKey(), entry.getKey(), String.valueOf(entry.getValue()), detectType(entry.getValue())); } // 从开始节点找到第一条有效连线 FlowRoute route findFirstRoute(def.getId(), start_node, formData); // 推进到目标节点 moveToNode(inst.getId(), route.getToNodeKey()); return inst.getBizKey(); }这段代码有两个关键设计。其一findFirstRoute会把 formData 里的值解析成 Map 传给条件判断器按 priority_no 顺序逐条尝试匹配命中即返回其二moveToNode负责更新流程实例的当前节点游标并生成一条待办任务队列。发起流程常被忽略的一个问题是条件表达式里的类型转换比如表单里填的是字符串“5000”而条件表达式是 amount 5000后端必须先把字段值转为数值再比较否则所有大于判断都会失效。源码里在 condition 解析器中用 JsonPath 读取字段值并按 flow_biz_data 的 field_type 转换类型这点做得比较周全。4.2 条件分支与并发会签的实现方式条件分支本质是“从同一节点引出多条连线按表达式匹配后只走一条”实现上依赖 flow_route 表的 from_node_key 和 priority_no。承接上文的 findFirstRoute核心判断大体是private FlowRoute findFirstRoute(Long flowId, String fromNodeKey, MapString, Object formData) { ListFlowRoute routes routeMapper.selectByFromNode(flowId, fromNodeKey); // 按priority_no升序排列 routes.sort(Comparator.comparingInt(FlowRoute::getPriorityNo)); for (FlowRoute route : routes) { if (route.getConditionExpr() null || route.getConditionExpr().isEmpty() || conditionParser.eval(route.getConditionExpr(), formData)) { return route; } } return routes.get(routes.size() - 1); }会签节点实现思路稍有不同。会签要求多人同时审批且每个人都完成任务后流程才汇聚到下一步。源码采用的方式是会签节点落到分实例表 flow_node_task每个人一条任务记录只有当前节点下所有任务都完成节点状态才置为完成。这个方案能精准表示“谁还没批”代价是查询待办时要做一次子查询组装复杂度略高。我在复现时会建议给 flow_node_task 表加一个联合索引 (node_id, status, assignee_id)否则组织人数达到几百人后待办查询会肉眼可见地变慢。4.3 驳回、转交与撤回的边界处理审批动作的正确性通常比流程本身的逻辑更考验源码质量。这个系统中同意动作的具体实现大概可以概括为回写当前节点的审批结论判断当前节点类型。如果是会签节点则减掉一个任务实例等待全部处理完如果是普通节点则按路由走到下一个节点。驳回则分为两种驳回到上一步和驳回至发起人。两者的代码区别只在 moveToNode 时传入的 targetNodeKey 不同但业务含义完全不同。发起人撤回必须有个限制条件只有当前流程处于第一个待办节点、且尚未被审批人处理时发起人才能撤回。这个判断在源码里用当前节点任务快照中是否有任何人处理过来实现逻辑上可靠。转交动作则偏技巧性审批人点转交时原任务标记为转交状态新任务推送给被转交人系统生成一条审计流水。这块源码没有单独做转交记录表而是复用了 flow_task_log 表操作类型字段填 5方便后续追责。边界处理上最容易出问题的场景是发起人撤回了流程但该流程的某条分支任务已经在处理这时如果直接更新流程实例状态为取消不处理分支任务那么被转交人仍然能在待办列表看到这条单子。源码里做了级联更新撤回时把所有 flow_node_task 中未完成任务批量置为已取消这一点务必在评估源码时确认否则线上会经常出现“待办里躺着早已撤销的假单”。5. 审批流程高频翻车点四个典型问题与排查路径我自己每次拿到OA源码都会先做一轮“暴力测试”专门针对设计器、引擎、数据表之间容易断链的地方。这里总结四个高频问题每条都按“现象 → 原因 → 解决”展开对应你在源码二次开发中最可能遇到的场景。5.1 发起后找不到审批人流程挂起在第一个节点现象 流程发起成功但流程实例一直停在第一个审批节点后台日志报错内容大致是“Approvor Not Found: deque_manager”。原因 审批节点配置的是“按部门负责人”审批也就是 approver_type3而用户表 dept_id 字段没有维护或者部门表里没有设置负责人。引擎在解析审批人时遍历了部门 - 负责人映射结果得到空集合于是抛异常并回滚事务。这个配置本身没写错错的是数据不完整。解决 处理分三层。一是设计器发布前增加底层校验凡是 approver_type3 的节点至少存在一条部门负责人记录二是引擎层实现对 approver_ref 的兜底逻辑可以约定如果部门负责人查不到自动转给该部门直属上级或转给全局管理员三是在用户管理界面强制 console 提醒用户维护部门字段。源码中自带的是第一种后两种需要二开建议无论如何都补上否则线上换组织架构时审批流会集体失效。5.2 保存流程再打开连线全部丢失或乱跳现象 设计器里画好节点和连线保存后点击“编辑流程”重新进入发现节点位置正常但连线全部丢失或者部分连线指向错误的节点。原因 前端设计器保存时把画布组件的内部实例 id 当作 nodeKey 传给后端而后端读出来时前端重新渲染画布实例 id 已被 Vue 重新分配旧连线引用的节点对象不存在了。这不是数据库丢数据而是前后端对“节点唯一标识”的理解不一致。解决 强制约定一个不可变标识。我改这套源码时把每个节点的 key 统一用 nodeType 加后缀形式例如 approve_3、branch_1连线里保存 fromNodeKey 和 toNodeKey画布渲染时通过 key 去 nodeList 里找对应节点对象。修复后连续做了三轮“保存-刷新-重开”测试连线稳定不再丢失。5.3 多次提交同一种申请历史审批数据被覆盖现象 同一员工当天提交了三份请假单前两份已经审批完成但查看历史记录时发现第一份的单据内容变成了第三份的内容。原因 flow_biz_data 表设计时把 flow_instance_id 当作唯一维度而发起流程时没生成独立的 biz_key每次提交都往同一个 flow_instance_id 下面插入字段记录相当于同一把钥匙开了三个房间的锁后写的数据覆盖了前面的字段。源码里业务层在 saveDraft 时直接复用旧的 flow_instance_id没有新建实例。解决 发起动作必须生成新 biz_key 并关联新的 flow_instance_id草稿要等提交时才创建实例。这里需要区分“草稿”和“正式发起”两个动作。我在这套源码的业务层加了一个 draftBizKey 字段草稿阶段只把表单内容存进草稿表点正式提交才生成新 biz_key、新 flow_instance_id并把草稿数据整体迁移过去这样就杜绝了覆盖问题。5.4 发布新版后旧审批单异常提示流程节点不存在现象 流程从版本 1 更新到版本 2 并且新审批单都正常但版本 1 遗留下来的未审批单据在点击“同意”时直接报错提示找不到目标节点。原因 引擎执行历史单据时从 flow_definition 读的是最新版本的 route_json而旧版本单据的 current_node_key 可能在最新版流程里已经不存在了。新版删掉了一个审批节点旧单此时正好停在那个节点上引擎自然就找不到当前节点到下一步的路由。解决 两条路缺一不可。一是引擎加载流程实例时关联快照版本也就是把 flow_instance.version_no 作为关键索引加入查询解析节点路由只用对应版本号的路由数据二是设计器做“下线旧版本”功能发布新版本时可以配置旧未完成单的继续处理策略比如“等待旧单完成后再允许发布”或“发布后未完成单按旧版逻辑继续跑”。源码里已经实现了前者后者需要你在项目里额外加一个开关。从那以后我每次做流程引擎都要先确认版本隔离这一步有没有做不然新改一个流程历史单就变成定时炸弹。6. 进阶技巧动态表单与审批节点的数据权限联动流程能跑通不等于能上线真正决定体验的是节点粒度的表单字段权限。比如差旅报销单里财务审批节点应该只能看到金额和发票号不应该看到申请人的私人备注部门主管节点可以看到全部字段并修改金额。这套OA系统源码的 flow_node 表里设计了 form_perm 字段专门用来做这件事。用好的话一个流程可以适配多岗位协作场景这也是它比普通静态表单 OA 系统源码更值得复现的地方。form_perm 的数据格式是 JSON 数组每个元素包含字段 key、读取权限、编辑权限、是否必填。比如[ {key: apply_reason, read: true, edit: false, required: false}, {key: amount, read: true, edit: true, required: true}, {key: private_note, read: false, edit: false, required: false} ]引擎在拿到当前节点审批人身份后按该节点的 form_perm 规则过滤动态表单字段前端渲染表单时只显示有 read 权限的字段编辑权限则控制是否可输入。实现时有一个关键点节点切换后前一个节点的编辑数据要能连贯保存下来。源码的处理是按 biz_key 每次追加快照也就是当前节点保存时把整份 formData 写入 flow_biz_data 的同一条记录里而不是每个节点单独存一份。这样设计的好处是审批链条上每个节点看到的数据始终是同一份底稿只是可见字段范围不同。字段权限上还有一个容易被忽视的细节条件分支路由依赖的字段即使当前节点没有编辑权限也必须传递到路由引擎。比如分支持“金额大于 5000 转总监审批”财务节点即使不能编辑金额路由判断时也要能读取该字段的值。如果把路由字段在某个节点禁读流程就会卡死。我的做法是在保存节点配置时增加一个校验凡是被条件表达式引用的字段自动强制 readtrue并提示开发者该字段用于路由判断不能被权限遮蔽。验证这套权限联动是否正常我通常跑一个模拟流程用三个不同角色的账号分别登录发起一个报销单观察每个节点能看到的字段和可编辑字段核心验证点包括节点字段渲染是否正确、被路由引用字段是否强制可读、节点提交后数据是否完整落到 flow_biz_data 中相应字段。这套流程设计器源码里给我印象最深的就是它把 node_key、版本隔离、条件解析、字段权限四个环节串成了一条完整链路二开时不需要推翻重写。但边界问题不少字段类型转换、历史版本兼容、前端节点 key 的稳定性每一个都可能成为上线后的隐患。希望这份拆解能帮你快速评估这套OA系统源码的可靠性也给你在二次开发时提供一个最少改动的落地参考。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站