资讯详情

基于SpringBoot+Vue+MySQL的工作流程管理系统设计与实现

📅 2026/10/13 20:29:27 | 华诺云谱 👁 阅读
基于SpringBoot+Vue+MySQL的工作流程管理系统设计与实现
去年我完整做了一套前后端分离的工作流程管理系统技术栈就是标题里写的这套SpringBoot Vue MySQL MyBatis。业务上覆盖请假、报销、合同审批这些最典型的场景代码上把流程定义、流程启动、任务审批、驳回、会签、转办全部跑通。我这次不打算只贴几个页面截图而是把从需求拆解到数据库设计、从后端接口到前端交互再到本地部署和排障完整讲一遍。如果你正准备做OA类项目、毕业设计选题或者第一次独立开发这种“有状态流转”的系统这份笔记可以直接当参考。整套系统的核心思路是不用重量级工作流引擎用 Java MySQL 自己实现一套轻量级流程引擎核心只有流程定义、任务、实例几张表。这种方式的最大优势是可控出问题你能直接看SQL分析而不是被引擎的表结构和内部机制带偏。1. 项目概述为什么需要一套工作流程管理系统1.1 先看业务审批场景到底缺什么日常工作里审批几乎是绕不开的。大多数人传统处理方式是什么纸质审批单、聊天软件传文件、邮件往来、共享表格记录。这些方式单独看都能用一旦流程复杂就出问题。最典型的几个痛点审批进度不可见发起人不知道卡在哪个环节。没有统一的任务池审批人可能漏看、误看。审批意见分散在各个聊天记录里事后追溯困难。流程规则靠人记换个人处理就容易出现“以前不是这么审的”这种问题。工作流程管理系统要解决的就是这类问题。它把审批规则固化到系统里让每一步都由流程引擎自动判断下一节点所有任务进入待办池处理完留痕审批人、发起人、管理员各自看到不同的视图。说得直白一点就是把“谁申请、谁审批、按什么顺序审批、不通过怎么办”这几件事用代码和数据库固化下来。1.2 系统角色和功能范围我做的这套系统划分了三个角色角色主要功能普通员工发起申请、查看我的申请、撤回未处理申请审批人处理待办、查看已办、驳回、转办、会签管理员流程定义、节点配置、用户角色管理、全流程监控功能上覆盖了几个核心模块流程管理流程定义的创建、启用、停用。节点配置审批节点、条件节点、开始和结束节点的维护。任务中心待办列表、已办列表、我的申请列表。审批操作同意、驳回、转办、撤回、加签。权限系统基于角色的登录认证和菜单权限控制。从实际使用来看这套范围已经能覆盖绝大多数中小团队的审批需求。复杂到需要自由流程、子流程、会签权重之类的可以在这个骨架上继续扩展但不建议一上来就做太重。2. 技术选型与系统架构设计2.1 为什么是 SpringBoot Vue而不是别的组合选择这套技术栈不是因为它最先进而是因为它在中小型系统里综合成本最低。后端用 SpringBoot理由很简单生态成熟、内嵌容器、配置简化和 MyBatis 配合又很自然。SpringBoot 的自动配置能省掉大量 XML 配置业务代码只需要关注 Controller、Service、Mapper 三层。对于流程管理系统这种“业务规则重、CRUD 多”的项目MyBatis 比 JPA 更直观SQL 可控性也更强。尤其是流程节点查询、子任务统计这种偏复杂的联表查询用 MyBatis 写 SQL 反而清晰。前端用 Vue组件化和响应式数据模型非常适合做后台管理。Vue Router 做页面路由Vuex 或 Pinia 做全局状态配合 Element UI 组件库待办列表、审批弹窗、步骤条这些页面开发效率很高。相比 React 生态Vue 的中文资料更多、上手门槛更低如果是团队项目或者毕设项目这个优势更明显。2.2 前后端分离架构和请求链路系统整体是典型的前后端分离结构浏览器 Vue 前端发起 HTTP 请求后端是 SpringBoot 提供 REST APIMyBatis 负责数据库访问MySQL 存储所有业务数据。前端开发时用 Vite 或 Webpack 代理转发接口请求生产环境可以用 Nginx 托管前端静态文件并反向代理后端地址。身份认证用 JWT Token。用户在登录接口拿到 Token 后前端每次请求都放在 Header 里后端写一个拦截器解析 Token并把当前用户信息放到 ThreadLocal 或者请求上下文后续 Service 层直接取“当前操作人”即可。这个架构的好处是前后端可以分开开发、分开部署。我在实际开发过程中前端和后端甚至可以用不同的开发机器进行联调只要接口约定好基本不互相阻塞。2.3 流程引擎自己写还是用 Activiti/Flowable很多人在做工作流项目时会纠结要不要引入 Activiti 或 Flowable。我的个人选择是这套系统不引入重量级流程引擎自己实现一个轻量引擎原因很直接。Activiti 和 Flowable 功能强大支持 BPMN 规范、子流程、多实例、边界事件但随之而来的是大量数据表和复杂概念。如果只是做请假、报销、合同审批这类线性流程引入这些引擎就像杀鸡用牛刀。你需要去理解它的部署、流程实例、任务、 executions 体系出了问题排查链路很长。自研方案的核心表其实很少流程定义表、节点表、连线表、流程实例表、任务表、审批记录表。代码上用一个状态机思路去驱动任务流转规则写在配置数据里扩展也方便。下面是两种方案的简单对比。维度自研轻量引擎Activiti/Flowable表结构6 张左右容易理解几十张表学习成本高部署成本低纯业务代码高需要熟悉引擎 API复杂流程能力适合顺序、条件、会签支持子流程、事件、多实例等出问题排查直接查业务表和 SQL需要结合引擎内部表分析推荐场景中小型内部系统大型复杂流程平台如果你做的项目未来一定会涉及复杂流程编排那直接用 Flowable 没错。但如果需求边界很明确就是固定表单、固定顺序、少量条件分支自研引擎能让项目整体质量更可控。3. 数据库设计核心表结构与状态流转3.1 实体关系和表清单流程管理系统数据模型围绕“流程定义、实例、任务、审批记录”四个核心概念来建。用户和角色表用于支撑权限这里不过度展开。主要业务表如下表名作用sys_user用户表sys_role角色表wf_process_define流程定义表wf_node流程节点表wf_node_edge节点连线表wf_process_instance流程实例表wf_task任务表wf_task_opinion审批记录表流程定义描述“有哪些节点、节点怎么办”流程实例描述“某一次具体申请走到哪了”任务描述“当前这个节点谁处理”。理解这三者关系整个系统的数据模型就通了。3.2 关键表 DDL 与设计理由这里直接给核心表结构你可以把 SQL 用来建表参考。CREATE TABLE wf_process_define ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_code VARCHAR(64) NOT NULL COMMENT 流程编码, process_name VARCHAR(128) NOT NULL COMMENT 流程名称, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_process_code (process_code) ) COMMENT 流程定义表; CREATE TABLE wf_node ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_id BIGINT NOT NULL COMMENT 所属流程定义ID, node_code VARCHAR(64) NOT NULL COMMENT 节点编码, node_name VARCHAR(128) NOT NULL COMMENT 节点名称, node_type VARCHAR(32) NOT NULL COMMENT start/approval/condition/end, assignee_type VARCHAR(32) COMMENT 角色编码或指定用户, condition_expr VARCHAR(500) COMMENT 条件规则JSON, sort_no INT NOT NULL DEFAULT 0, UNIQUE KEY uk_process_node (process_id, node_code) ) COMMENT 流程节点表; CREATE TABLE wf_process_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_id BIGINT NOT NULL, applicant_id BIGINT NOT NULL COMMENT 发起人, business_key VARCHAR(128) COMMENT 业务单据编号, status VARCHAR(32) NOT NULL COMMENT running/completed/terminated, current_node_code VARCHAR(64), variables TEXT COMMENT 流程变量JSON, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, end_time DATETIME ) COMMENT 流程实例表; CREATE TABLE wf_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, instance_id BIGINT NOT NULL, node_code VARCHAR(64) NOT NULL, node_name VARCHAR(128) NOT NULL, assignee_id BIGINT NOT NULL COMMENT 处理人ID, status VARCHAR(32) NOT NULL COMMENT todo/processed/transferred, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME, KEY idx_instance (instance_id), KEY idx_assignee (assignee_id, status) ) COMMENT 流程任务表; CREATE TABLE wf_task_opinion ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, instance_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, action VARCHAR(32) NOT NULL COMMENT approve/reject/transfer, comment VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 审批记录表;设计的时候有一条原则状态字段用字符串不搞难看的数字魔法值。todo、processed这种值一看就懂排查数据的时候不用查字典表。3.3 节点连线与条件表达式设计节点表负责定义“有哪些节点”连线表负责定义“节点与节点之间怎么走”。这是一张非常关键的表实际结构类似下面这样CREATE TABLE wf_node_edge ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_id BIGINT NOT NULL, from_node_code VARCHAR(64) NOT NULL, to_node_code VARCHAR(64) NOT NULL, condition_expr VARCHAR(500) COMMENT 条件表达式JSON, sort_no INT NOT NULL DEFAULT 0 ) COMMENT 节点连线表;条件表达式的存放方式我用了 JSON 文本。比如一个审批节点有两条出线一条条件是“金额小于等于5000”到主管审批另一条条件是“金额大于5000”到总监审批。判断规则就存在condition_expr字段里格式大致是{field: amount, op: , value: 5000}Service 层拿到流程变量 Map 后按这个表达式解析。至于更复杂的多条件组合可以在 JSON 结构里加and/or嵌套但中小型系统里单条件判断已经够用不必做成规则引擎。4. 后端核心实现SpringBoot MyBatis 工作流业务闭环4.1 认证授权JWT 拦截器与用户上下文后端采用拦截器统一做登录校验。登录接口校验用户名密码后生成 Token其余接口在请求头携带 Token拦截器校验通过后放行。核心类大概是public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (!StringUtils.hasText(token) || !JwtUtil.verify(token)) { response.setStatus(401); return false; } UserContext.setCurrentUserId(JwtUtil.getUserId(token)); return true; } }使用 ThreadLocal 保存当前用户 IDService 层在处理任务时可以直接拿到操作人不需要每个接口都传一遍用户 ID。这里我没有引入 Spring Security因为登录模型简单、权限粒度用角色已经足够拦截器加角色校验反而更直观。4.2 流程启动从流程定义到生成首个任务启动流程是核心入口。发起人提交表单后后端要做三件事创建流程实例、解析开始节点、创建第一个审批任务。Transactional public Long startProcess(String processCode, Long applicantId, MapString, Object variables) { // 1. 查询启用的流程定义 WfProcessDefine define processDefineMapper.findByCode(processCode); if (define null || define.getStatus() ! 1) { throw new BusinessException(流程不存在或已停用); } // 2. 创建流程实例 WfProcessInstance instance new WfProcessInstance(); instance.setProcessId(define.getId()); instance.setApplicantId(applicantId); instance.setStatus(running); instance.setCurrentNodeCode(null); instance.setVariables(JSON.toJSONString(variables)); processInstanceMapper.insert(instance); // 3. 找到开始节点出线指向的第一个审批节点 WfNode startNode nodeMapper.findByProcessAndType(define.getId(), start); String nextNodeCode routeEngine.findNextNodeCode(define.getId(), startNode.getNodeCode(), variables); WfNode nextNode nodeMapper.findByProcessAndCode(define.getId(), nextNodeCode); // 4. 生成待办任务 Long assigneeId assigneeResolver.resolve(nextNode, variables); taskMapper.insert(buildTask(instance.getId(), nextNode, assigneeId)); return instance.getId(); }这个方法加事务很重要。流程实例、任务插入必须同时成功或失败避免出现“实例建了但没有待办任务”这种脏状态。4.3 任务审批推进下一节点审批动作是所有操作里最容易出错的环节。我从一开始就要求自己把“同意”和“驳回”收敛到同一个 Service 方法里代码结构大致如下Transactional public void completeTask(Long taskId, Long operatorId, boolean approved, String comment) { // 1. 行锁锁定当前任务防止并发重复处理 WfTask task taskMapper.selectForUpdate(taskId); if (!task.getAssigneeId().equals(operatorId)) { throw new BusinessException(当前任务不属于该用户); } if (!todo.equals(task.getStatus())) { throw new BusinessException(任务已处理请勿重复操作); } // 2. 写审批意见 taskOpinionMapper.insert(buildOpinion(task, operatorId, approved ? approve : reject, comment)); if (!approved) { // 3. 驳回回退到上一节点或指定节点 String backNodeCode task.getNodeCode(); // 这里根据驳回类型决定回退到哪 TaskUtils.backToNode(task, backNodeCode); return; } // 4. 标记当前任务完成 taskMapper.updateStatus(taskId, processed); // 5. 路由到下一个节点 WfProcessInstance instance processInstanceMapper.findById(task.getInstanceId()); MapString, Object variables JSON.parseObject(instance.getVariables()); String nextNodeCode routeEngine.findNextNodeCode(instance.getProcessId(), task.getNodeCode(), variables); if (routeEngine.isEndNode(instance.getProcessId(), nextNodeCode)) { processInstanceMapper.updateStatus(instance.getId(), completed); } else { WfNode nextNode nodeMapper.findByProcessAndCode(instance.getProcessId(), nextNodeCode); Long assigneeId assigneeResolver.resolve(nextNode, variables); taskMapper.insert(buildTask(instance.getId(), nextNode, assigneeId)); } }注意这里用到了selectForUpdate这是我在实际测试中踩过坑之后加的。没有锁的情况下连续点击两次“同意”按钮可能同一任务被处理两次产生两个下一节点任务。加上行锁之后第二个请求会在第一步卡住等第一个事务提交后任务状态已经更新后续就会抛出“任务已处理”的异常。4.4 驳回、会签、转办与撤回的进阶处理基础审批跑通后我开始补常规流程里必须的进阶功能。驳回逻辑需要区分“驳回上一节点”和“驳回到指定节点”。我实现的方案是在任务审批弹窗里允许选择驳回目标节点后端记录驳回节点编码然后生成一个新的待办任务给目标节点负责人。这里要特别注意任务生成数量驳回不应该产生多条并行任务只创建一条目标节点的任务。会签稍微复杂一点。节点配置里会标记multitrue启动时给多个会签人都创建任务。每个人审批都只把自己的任务变为 processed同时去查同节点下还有没有未完成任务如果没有流程才继续推进。用一句简单 SQL 就能判断SELECT COUNT(*) FROM wf_task WHERE instance_id ? AND node_code ? AND status todo转办最简单本质就是修改任务处理人。我会额外写一条转办记录记录原处理人、新处理人和转办原因方便后续追溯。撤回功能只允许发起人在节点还没被处理时使用。撤回前会检查当前任务状态必须是 todo同时校验操作人等于实例申请人。5. 前端实现Vue 页面与接口对接5.1 页面框架与路由设计前端项目基于 Vue CLI 创建路由分成两类菜单页面和操作页面。基础路由结构如下/login /home /process/list 流程列表 /process/design 流程设计 /apply/start 发起申请 /task/todo 待办中心 /task/done 已办中心 /instance/my 我的申请菜单不是写死的而是登录后根据角色动态生成。前端在路由守卫里判断用户是否登录再根据后端返回的权限数据过滤菜单列表。5.2 发起申请模块的实现方式发起申请页面的挑战在于不同流程需要不同表单字段。我最初想得很简单每个流程写一个页面对应一个组件但后面发现流程数量一多代码重复特别严重。后来改成“动态表单”方案。后端在流程定义里维护字段配置比如请假流程有“请假类型、开始时间、结束时间、事由”报销流程有“报销金额、报销明细、发票附件”。前端根据字段配置循环渲染表单控件配置字段的数据结构类似[ { label: 请假类型, field: leaveType, type: select, options: [事假, 病假, 年假] }, { label: 开始时间, field: startTime, type: date }, { label: 请假事由, field: reason, type: textarea } ]这样新增流程只需要在后台配字段前端不用动代码实用性明显更强。5.3 待办中心与审批操作待办中心是一个表格页接口返回当前用户待处理的任务。主要三列流程名称、发起人、到达时间。点击“处理”按钮弹出审批窗口。审批窗口里我会展示两个关键模块上面的表单详情根据变量 JSON 回显和下方的审批记录。提交时把审批意见、是否通过、任务 ID 传给后端接口async function handleApprove(taskId, approved, comment) { submitLoading.value true try { await axios.post(/api/task/complete, { taskId, approved, comment }) ElMessage.success(操作成功) fetchTodoList() } finally { submitLoading.value false } }提交按钮的 loading 状态很重要。审批操作后端已经加了行锁但前端如果允许重复点击用户体验会很差而且会产生一连串无效请求。这是我在实际联调时特别注意的细节。5.4 流程进度展示流程实例详情页里我用步骤条组件展示当前流程所处阶段。前端把后端返回的节点列表、已完成节点、当前节点换算成步骤条数据就可以让用户直观看到“申请走到第几步、卡在谁那里”。做了这个功能以后内部使用反馈明显变好。很多用户关心的不是流程怎么设计而是“我的单子到底到谁手里了”。这也是工作流系统体验提升的关键点。6. 完整源码结构与本地部署步骤6.1 项目目录结构说明后端工程遵循 SpringBoot 标准分包前端遵循 Vue 标准结构。核心目录大致如下backend/ src/main/java/com/workflow/ controller/ REST接口层 service/ 业务逻辑和工作流核心引擎 mapper/ MyBatis Mapper接口 model/ 实体类 common/ 通用返回结果、异常、JWT工具 src/main/resources/ mapper/ MyBatis XML文件 application.yml 项目配置 frontend/ src/ api/ 接口封装 views/ 登录、首页、流程管理、任务中心等页面 router/ 路由配置 store/ 全局状态管理 main.js 入口源码里我按这个结构组织就是为了让核心流程引擎相关代码都集中在 service 包下改动起来不用到处找。6.2 环境准备与本地启动步骤整套系统本地启动需要以下环境JDK 1.8 或更高版本Maven 3.6MySQL 5.7 或 8.0Node.js 14npm 或 yarn启动步骤我按顺序列一下创建数据库并导入初始化脚本mysql -uroot -p -e CREATE DATABASE wfms DEFAULT CHARACTER SET utf8mb4 mysql -uroot -p wfms init.sql修改后端配置application.yml重点改数据库地址、账号、密码。启动后端mvn spring-boot:run启动前端cd frontend npm install npm run dev本地开发时前端通过代理访问后端接口。Vite 或 Vue CLI 里配置一个 proxy把/api开头的请求转发到http://localhost:8080这样不会出现跨域问题。6.3 初始化数据与测试账号初始化脚本里会预置管理员、普通用户、角色以及一条请假流程定义。测试账号是账号密码角色admin123456管理员alice123456普通员工/审批人bob123456普通员工/审批人登录后用 alice 发起一条请假流程节点会走到主管 bob 那里。bob 登录后能在待办中心看到这条任务处理完以后流程进入下一个节点或结束。7. 常见问题与排查技巧实录7.1 后端启动失败速查表我在开发调试中遇到过的几类启动问题整理成了表格现象原因处理方式报端口被占用8080 被其他进程占用修改server.port或杀掉占用进程数据库连接失败数据库地址、密码配置错误检查application.yml和 MySQL 服务状态找不到数据源驱动Maven 依赖未下载完整mvn clean install后重新启动LocalDateTime 序列化报错Jackson 没配置时间格式引入jackson-datatype-jsr310并配置格式启动问题大多是环境问题排查逻辑从控制台日志往上看第一行报错就够了不用从头到尾读日志。7.2 待办任务重复生成这是工作流系统最常见的隐患。我在刚开始写completeTask时没有加行锁也没有在更新任务状态时加条件判断。前端连续点击“同意”后同一个任务被两个事务先后读取两个事务都认定任务未处理于是生成两条下一节点任务。解决方式就是前面提到的两点第一步用selectForUpdate锁定任务行更新状态时用条件更新int rows taskMapper.updateStatusIfTodo(taskId, processed); if (rows 0) { throw new BusinessException(任务已处理请勿重复操作); }这个updateStatusIfTodo的 SQL 是update idupdateStatusIfTodo UPDATE wf_task SET status #{newStatus}, finish_time NOW() WHERE id #{taskId} AND status todo /update两层保险并发下不会产生重复任务。7.3 前端跨域与 Token 失效本地开发跨域通过 Vite 代理解决。生产环境如果前后端分开部署通常用 Nginx 把/api反向代理到后端服务前端静态资源和后端接口共用同一个对外域名也不会跨域。Token 失效后后端返回 401。前端需要在 axios 响应拦截器里统一处理遇到 401 时清除本地登录状态并跳转登录页。否则用户会看到一堆报错弹窗体验非常差。7.4 流程状态和业务数据不一致比如报销流程审批通过后需要把报销单状态改成“已通过”。如果先修改业务单状态再调用流程审批接口流程失败时业务单已经更新两边就乱了。反过来先审批通过再更新业务单业务单更新失败流程状态又和实际不一致。我建议的做法是把业务状态更新和流程推进放在同一个事务里。流程审批接口接收一个可选回调比如通过 SPI 或事件机制执行业务逻辑或者业务系统在流程审批通过后自己通过监听流程事件来更新业务单。中小型系统里最简单可靠的方式是把业务更新写进审批的 Service 事务里保证强一致。整套系统做下来我最大的感受是工作流系统最难的不是写界面也不是写增删改查而是把任务状态和流程跃迁在并发和回退场景下搞对。只要把“任务从哪来、处理完以后往哪走、并发时怎么办”这几个问题想清楚整个系统的骨架就稳了。如果以后要扩展我大概率会从消息通知和子流程入手。当前版本的消息提醒还是靠待办中心轮询后面可以引入 WebSocket 推送子流程则需要在节点配置里增加子流程类型和映射关系。核心表结构我预留了扩展位改起来不会伤筋动骨。这套系统的设计思路也希望能帮你少走一点弯路。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑