Vue3+SpringBoot3实现流程定义保存功能:TaoToken统一Key打通前后端联调)
1. 流程定义保存链路为什么总在联调阶段卡住Flowable 7.x 的流程定义保存本质上是把前端画好的 BPMN XML 提交到后端由后端调用 Flowable 的部署接口完成入库。听起来只有两步但真正动手时问题往往不在 Flowable 本身而在前后端之间的那层“胶水”前端提交的 JSON 字段名和后端接收对象对不上、雪花 ID 传到浏览器精度丢失、日期格式一个带 T 一个不带、接口鉴权头拼错、跨域预检失败。我见过太多项目在“保存”这个按钮上耗掉一整天。这篇笔记聚焦的场景很具体Vue3 前端用 el-drawer 收集流程定义元数据并提交 BPMN XMLSpringBoot3 后端接收后调用 Flowable 的 RepositoryService 完成部署同时把流程定义记录写入业务表。为了让前后端联调不再被环境配置和鉴权问题打断我会用 TaoToken 的统一 Key 来管理模型调用与接口调试环节的凭证把注意力留给业务逻辑本身。适合谁看已经搭好 Flowable 7.x 基础工程、写过流程定义列表页、现在要补“新增/保存”功能的开发者。你需要对 Vue3 的 ref/reactive、SpringBoot3 的 RestController、MyBatis-Plus 有基本了解。全文的代码都可以直接复制路径和包名按你自己的工程调整即可。先说清楚一个容易混淆的点Flowable 的“部署”和业务表的“保存”是两件事。部署是把 BPMN XML 解析成流程定义写入 ACT_RE_* 系列表业务表的保存是记录流程的 key、名称、版本、状态、描述这些管理信息。两者要么放在同一个事务里要么先部署拿到 deploymentId 再写业务表。我选择后者因为部署失败时业务表不该留下脏数据。联调阶段最典型的报错是 401 和 403。401 是没带 token 或 token 过期403 是带了 token 但接口权限没配。这两个错误在浏览器 Network 面板里长得几乎一样但排查方向完全不同。下面我会把这两类问题的定位方法拆开讲。2. TaoToken 统一 Key 在联调环境里的定位与配置在讲具体代码之前先解决一个环境问题。前后端联调时我们经常需要在多个工具之间切换浏览器调接口、Postman 测后端、IDE 里跑单元测试、有时候还要用 AI 辅助生成 BPMN 片段或排查报错。每个工具都配一遍 Key改一次环境就要同步一遍很容易出现“浏览器能通、Postman 报 401”这种问题。TaoToken 的做法是提供一个统一的 API 入口和 Key 管理把模型对话、编码辅助、接口调试这些场景的凭证收敛到一处。它的 API 地址是https://taotoken.net/api控制台在https://taotoken.net/consoleKey 在https://taotoken.net/api-keys生成。对于 Flowable 这种需要频繁调试接口的场景统一 Key 的价值在于你在 Postman 里验证通过的请求复制到前端代码里不会因为凭证来源不同而失败。需要说明的是TaoToken 在这里扮演的是“统一凭证与调用入口”的角色不是替代你的 SpringBoot 服务也不是替代 Flowable 引擎。你的流程定义保存接口仍然跑在自己的后端TaoToken 负责的是联调过程中那些辅助环节的凭证一致性。配置方式很简单在项目根目录建一个.env.local不要提交到 git写入# TaoToken 统一 Key 配置 TAOTOKEN_API_BASEhttps://taotoken.net/api TAOTOKEN_API_KEYsk-your-key-here TAOTOKEN_MODEL_IDclaude-sonnet-4-20250514如果你用的是 Claude Code 或类似的编码辅助工具可以在项目里放一个settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-your-key-here, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }注意 Base URL 和 Key 要成对出现Model ID 按你实际使用的模型填。这三件套Base URL Key Model ID是接入任何兼容 Anthropic 协议的工具时都必须完整的缺一个就会报认证失败或模型不存在。对于 Cline、Cursor 这类支持 MCP 的工具配置里同样需要这三项。我试过只填 Key 不填 Base URL结果请求打到了默认地址报了一个很迷惑的 404。后来把三项补齐就正常了。提示.env.local和settings.json都要加入.gitignore。Key 泄露的后果不用我多说尤其是团队协作时一个提交就可能让 Key 暴露在仓库历史里。配置完成后你可以用一条 curl 验证 Key 是否可用curl -X POST https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H x-api-key: sk-your-key-here \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 100, messages: [{role: user, content: ping}] }返回里有content字段就说明 Key 和 Base URL 都对。这一步看起来和 Flowable 无关但它是后面所有联调工作的基础——凭证不通后面全是白费。3. 可复制的后端配置与前端请求片段这一节是全文的核心给出可以直接粘贴的配置和代码。先看后端。3.1 SpringBoot3 的 Flowable 与数据源配置application.yml里需要配置 Flowable 的数据库和异步执行器。Flowable 7.x 默认使用 Spring Boot 3 的自动配置但有几个参数必须显式指定spring: datasource: url: jdbc:mysql://127.0.0.1:3306/flowable_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver flowable: database-schema-update: true async-executor-activate: false history-level: full check-process-definitions: falsedatabase-schema-update: true会在启动时自动创建 ACT_ 系列表开发环境很方便生产环境建议改成false并用 SQL 脚本手动建表。async-executor-activate: false在联调阶段关掉异步执行器避免定时任务干扰日志。check-process-definitions: false防止 Flowable 去扫描resources/processes目录因为我们的流程定义是从前端提交的不是预置文件。3.2 流程定义保存接口的完整实现Controller 层接收前端提交的 JSON包含 BPMN XML 字符串和元数据RestController RequestMapping(/api/v1/definition) RequiredArgsConstructor Slf4j public class ProcessDefinitionController { private final IProcessDefinitionService processDefinitionService; private final RepositoryService repositoryService; PostMapping(/save) public ResultProcessDefinitionVO save(RequestBody ProcessDefinitionInOrUpReq req) { try { ProcessDefinitionVO vo processDefinitionService.save(req); return Result.success(vo); } catch (IllegalArgumentException e) { log.error(参数异常: {}, e.getMessage(), e); return Result.error(参数异常 e.getMessage()); } catch (Exception e) { log.error(保存失败: {}, e.getMessage(), e); return Result.error(保存失败 e.getMessage()); } } }Service 层的 save 方法要处理两件事调用 Flowable 部署 BPMN XML以及写业务表。部署部分Override public ProcessDefinitionVO save(ProcessDefinitionInOrUpReq req) { if (req null || StringUtils.isEmpty(req.getProcessKey())) { throw new IllegalArgumentException(processKey 不能为空); } ProcessDefinition entity new ProcessDefinition(); BeanUtil.copyProperties(req, entity, CopyOptions.create().ignoreNullValue()); // 如果带了 BPMN XML先部署到 Flowable if (StringUtils.isNotBlank(req.getBpmnXml())) { Deployment deployment repositoryService.createDeployment() .name(req.getProcessName()) .key(req.getProcessKey()) .addString(req.getProcessKey() .bpmn20.xml, req.getBpmnXml()) .deploy(); entity.setDeploymentId(deployment.getId()); log.info(流程部署成功, deploymentId{}, deployment.getId()); } // 唯一键校验processKey processVersion if (entity.getId() null) { ListProcessDefinition exist lambdaQuery() .eq(ProcessDefinition::getProcessKey, entity.getProcessKey()) .eq(ProcessDefinition::getProcessVersion, entity.getProcessVersion()) .list(); if (!exist.isEmpty()) { throw new BusinessException(存在相同流程定义 entity.getProcessKey() - entity.getProcessVersion()); } } saveOrUpdate(entity); return ProcessDefinitionStructMapper.INSTANCE.toVO(entity); }注意addString的第二个参数是资源名必须以.bpmn20.xml结尾否则 Flowable 不会解析。这是很多人第一次部署时踩的坑——文件传上去了但 ACT_RE_PROCDEF 表里没有记录。3.3 前端 axios 封装与请求片段前端的https.ts负责请求拦截和响应拦截。请求拦截器把 token 塞进 headerprivate handleRequest (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig { const userStore useUserStore() const token userStore.token if (token config.url ! /oauth2/token) { const { token_type, access_token } JSON.parse(token) as Token config.headers!.Authorization ${token_type} ${access_token} } return config }响应拦截器统一处理 401 跳转和错误提示。这里有个细节handleResponse直接返回res.data所以业务代码里拿到的就是后端Result对象不用再.data一层。保存接口的定义export function saveProcessDefinition(data: ProcessDefinitionInOrUpReq) { return request.postany({ url: /pm-process/api/v1/definition/save, data, }) }/pm-process是网关前缀实际请求会打到http://gateway/pm-process/api/v1/definition/save。如果你的工程没有网关把 baseURL 直接指向后端端口即可。3.4 前端抽屉表单的关键参数el-drawer 里的表单参数用 reactive 定义校验规则用 reactive 定义const processDefinitionParams reactiveProcessDefinitionInOrUpReq({ id: 0, processKey: , processName: , processVersion: 1, processStatus: 0, description: , deleted: false, bpmnXml: , })bpmnXml字段是新增的用来承载前端 BPMN 编辑器产出的 XML 字符串。如果你的 BPMN 编辑器是 bpmn-js可以用modeler.saveXML({ format: true })拿到 XML赋值给这个字段。保存方法async function onSaveProcessDefinition() { try { await operatePDFormRef.value.validate() const result: any await saveProcessDefinition(processDefinitionParams) if (result.success result.code 200) { showProcessDefinitionDrawer.value false ElMessage({ message: 操作成功, type: success }) } else { ElMessage({ message: result.message, type: error }) } } catch (error) { ElMessage({ message: 保存失败, type: error }) } }4. 验证请求与成功结果确认配置写完之后必须验证整条链路。我习惯分三步先验后端接口再验前端请求最后验数据库。4.1 用 curl 直接打后端接口绕过前端直接用 curl 测试后端能快速定位问题出在哪一层curl -X POST http://127.0.0.1:8080/api/v1/definition/save \ -H Content-Type: application/json \ -H Authorization: Bearer your_access_token \ -d { processKey: leave_apply, processName: 请假申请, processVersion: 1, processStatus: 0, description: 员工请假流程, bpmnXml: ?xml version\1.0\ encoding\UTF-8\?definitions xmlns\http://www.omg.org/spec/BPMN/20100524/MODEL\ targetNamespace\http://flowable.org/processdef\process id\leave_apply\ name\请假申请\startEvent id\start\/sequenceFlow id\flow1\ sourceRef\start\ targetRef\end\/endEvent id\end\//process/definitions }返回应该是{ success: true, code: 200, message: 操作成功, data: { id: 1234567890123456789, processKey: leave_apply, processName: 请假申请, processVersion: 1, deploymentId: 1, processStatus: 0 } }注意id是字符串这是JsonSerialize(using ToStringSerializer.class)的效果。如果返回的是数字且末尾变成 000说明雪花 ID 精度丢了前端拿到的 id 是错的。4.2 检查 Flowable 的部署表部署成功后ACT_RE_DEPLOYMENT 和 ACT_RE_PROCDEF 表里应该有记录SELECT ID_, NAME_, KEY_, VERSION_, DEPLOYMENT_ID_ FROM ACT_RE_PROCDEF WHERE KEY_ leave_apply;如果 ACT_RE_PROCDEF 为空但 ACT_RE_DEPLOYMENT 有记录说明 BPMN XML 解析失败。常见原因是 XML 里缺少targetNamespace或者 process 的 id 和文件名不匹配。4.3 前端联调时的 Network 检查打开浏览器 DevTools 的 Network 面板点保存按钮看请求Request URL 是否正确网关前缀有没有拼错Request Headers 里 Authorization 是否存在且格式为Bearer xxxRequest Payload 里 bpmnXml 是否完整有时候编辑器返回的 XML 被转义了Response 的 code 是否为 200如果 Response 是 401检查 token 是否过期如果是 403检查PreAuthorize的权限字符串是否和用户角色匹配。4.4 数据库最终确认业务表process_definition里应该有对应记录deployment_id字段和 ACT_RE_DEPLOYMENT 的 ID_ 一致。这一步确认了业务表和 Flowable 引擎表的数据一致性。5. 联调常见报错排查对照这一节列出我实际遇到过的报错以及定位方法。每个报错都给出真实日志片段和解决方向。5.1 401 Unauthorizedtoken 没带或过期日志里通常是Resolved [org.springframework.security.access.AccessDeniedException: Access Denied]或者前端 Network 里看到{error:invalid_token,error_description:Token has expired}排查顺序先看请求头有没有 Authorization再看 token 是否过期OAuth2 的 access_token 默认有效期通常很短最后看 token 的类型前缀是不是Bearer。如果用的是 TaoToken 的 Key 调模型接口401 通常是 Key 写错或 Base URL 不匹配。5.2 403 Forbidden权限字符串不匹配后端日志org.springframework.security.access.AccessDeniedException: 不允许访问这个错误的根源是PreAuthorize(hasAnyAuthority(/api/v1/definition/save))里的权限字符串和用户实际拥有的权限不一致。解决方法是去权限管理表里给当前角色分配这个权限或者临时把注解注释掉验证逻辑。5.3 local proxy failed网关或跨域问题前端控制台报Error: local proxy failed或者Access to XMLHttpRequest at http://localhost:8080/api/v1/definition/save from origin http://localhost:5173 has been blocked by CORS policy这是跨域问题。开发环境用 Vite 的 proxy 解决// vite.config.ts server: { proxy: { /pm-process: { target: http://127.0.0.1:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/pm-process/, ), }, }, }生产环境由 Nginx 处理不要在后端加CrossOrigin了事那样会带来安全隐患。5.4 reading choices响应结构解析失败前端报TypeError: Cannot read properties of undefined (reading choices)这个错误通常出现在调用模型接口时响应结构和预期不符。比如你期望的是 OpenAI 格式的choices数组但实际返回的是 Anthropic 格式的content数组。检查你的请求体里 model 字段和接口协议是否匹配。用 TaoToken 时Base URL 决定了协议格式/api/v1/messages是 Anthropic 格式/api/v1/chat/completions是 OpenAI 格式两者不能混用。5.5 OAuth2 相关报错errorinvalid_grant, error_descriptionBad credentials这是 OAuth2 密码模式登录时用户名或密码错误。检查请求体里的 username/password以及 client_id/client_secret 是否正确。如果是授权码模式检查 redirect_uri 是否在注册列表里。5.6 BPMN XML 解析失败后端日志org.flowable.bpmn.exceptions.XMLException: cvc-complex-type.2.4.a: Invalid content was found starting with element process这是 BPMN XML 格式问题。最常见的是缺少命名空间声明或者元素顺序不对。BPMN 2.0 对元素顺序有严格要求definitions下面必须先声明processprocess里面startEvent要在sequenceFlow之前。建议用 bpmn-js 的校验功能先在前端过滤一遍。5.7 雪花 ID 精度丢失前端拿到的 id 是1234567890123456800末尾几位变成 0。这是 JavaScript Number 类型最大安全整数是 2^53-1 导致的。解决方法是在 VO 的 id 字段加JsonSerialize(using ToStringSerializer.class)让后端序列化成字符串。前端 TS 类型定义里 id 也要声明为string而不是number。6. 把保存链路固化成可复用的联调模板走到这里流程定义保存的链路应该已经跑通了。但联调不是一次性的工作下次加新接口时还会遇到同样的问题。我的做法是把这套配置固化成一个模板新项目直接复制。模板包含四部分后端的 Flowable 配置片段、前端的 axios 封装、TaoToken 的三件套配置、以及一份排查清单。排查清单按错误码分类401 查 token、403 查权限、跨域查 proxy、XML 报错查命名空间、精度丢失查序列化注解。这份清单贴在团队 wiki 上新人遇到问题先自查能省掉大量重复沟通。关于 TaoToken 的使用我的建议是把它当成联调环境的“凭证中枢”。模型对话、编码辅助、接口调试这些场景共用一套 Key切换工具时不用重新配置。需要生成 Key 的话去https://taotoken.net/api-keys接入文档在https://taotoken.net/doc模型对话入口在https://taotoken.net/chat。如果你在做长期的 Flowable 工作流开发涉及大量 Agent 编排和编码任务可以看看 Coding Planhttps://taotoken.net/coding-plan。最后说一个实际经验流程定义保存接口的幂等性很重要。用户连点两次保存按钮不应该产生两条部署记录。我的做法是在前端按钮上加 loading 状态后端在 save 方法里用processKey processVersion做唯一键校验。如果版本号是自增的那第二次提交会因为版本号相同而被拦截。这个细节在测试阶段很容易被忽略上线后却会变成数据问题。下一篇会写流程定义的分页查询涉及 Flowable 的 ACT_RE_PROCDEF 表和业务表的联合查询以及如何把 BPMN 的 XML 从 MongoDB 里取出来渲染到前端。如果你在保存这一步还有没解决的问题可以先检查 ACT_RE_PROCDEF 表里有没有数据——这是判断部署是否成功的最直接依据。