简介:一套完整的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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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, Map<String, 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.Entry<String, 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, Map<String, Object> formData) { List<FlowRoute> 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_type=3,而用户表 dept_id 字段没有维护,或者部门表里没有设置负责人。引擎在解析审批人时遍历了部门 -> 负责人映射,结果得到空集合,于是抛异常并回滚事务。这个配置本身没写错,错的是数据不完整。解决 :处理分三层。一是设计器发布前增加底层校验:凡是 approver_type=3 的节点,至少存在一条部门负责人记录;二是引擎层实现对 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 转总监审批”,财务节点即使不能编辑金额,路由判断时也要能读取该字段的值。如果把路由字段在某个节点禁读,流程就会卡死。我的做法是在保存节点配置时增加一个校验:凡是被条件表达式引用的字段,自动强制 read=true,并提示开发者该字段用于路由判断,不能被权限遮蔽。验证这套权限联动是否正常,我通常跑一个模拟流程:用三个不同角色的账号分别登录,发起一个报销单,观察每个节点能看到的字段和可编辑字段,核心验证点包括节点字段渲染是否正确、被路由引用字段是否强制可读、节点提交后数据是否完整落到 flow_biz_data 中相应字段。这套流程设计器源码里,给我印象最深的就是它把 node_key、版本隔离、条件解析、字段权限四个环节串成了一条完整链路,二开时不需要推翻重写。但边界问题不少:字段类型转换、历史版本兼容、前端节点 key 的稳定性,每一个都可能成为上线后的隐患。希望这份拆解能帮你快速评估这套OA系统源码的可靠性,也给你在二次开发时提供一个最少改动的落地参考。本文还有配套的精品资源,点击获取
简介:一套完整的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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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, Map<String, 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.Entry<String, 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, Map<String, Object> formData) { List<FlowRoute> 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_type=3,而用户表 dept_id 字段没有维护,或者部门表里没有设置负责人。引擎在解析审批人时遍历了部门 -> 负责人映射,结果得到空集合,于是抛异常并回滚事务。这个配置本身没写错,错的是数据不完整。解决 :处理分三层。一是设计器发布前增加底层校验:凡是 approver_type=3 的节点,至少存在一条部门负责人记录;二是引擎层实现对 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 转总监审批”,财务节点即使不能编辑金额,路由判断时也要能读取该字段的值。如果把路由字段在某个节点禁读,流程就会卡死。我的做法是在保存节点配置时增加一个校验:凡是被条件表达式引用的字段,自动强制 read=true,并提示开发者该字段用于路由判断,不能被权限遮蔽。验证这套权限联动是否正常,我通常跑一个模拟流程:用三个不同角色的账号分别登录,发起一个报销单,观察每个节点能看到的字段和可编辑字段,核心验证点包括节点字段渲染是否正确、被路由引用字段是否强制可读、节点提交后数据是否完整落到 flow_biz_data 中相应字段。这套流程设计器源码里,给我印象最深的就是它把 node_key、版本隔离、条件解析、字段权限四个环节串成了一条完整链路,二开时不需要推翻重写。但边界问题不少:字段类型转换、历史版本兼容、前端节点 key 的稳定性,每一个都可能成为上线后的隐患。希望这份拆解能帮你快速评估这套OA系统源码的可靠性,也给你在二次开发时提供一个最少改动的落地参考。本文还有配套的精品资源,点击获取