1. 流程定义保存链路为什么总在联调阶段卡住
Flowable 7.x 的流程定义保存,本质上是把前端画好的 BPMN XML 提交到后端,由后端调用 Flowable 的部署接口完成入库。听起来只有两步,但真正动手时,问题往往不在 Flowable 本身,而在前后端之间的那层“胶水”:前端提交的 JSON 字段名和后端接收对象对不上、雪花 ID 传到浏览器精度丢失、日期格式一个带 T 一个不带、接口鉴权头拼错、跨域预检失败。我见过太多项目在“保存”这个按钮上耗掉一整天。
这篇笔记聚焦的场景很具体:Vue3 前端用 el-drawer 收集流程定义元数据并提交 BPMN XML,SpringBoot3 后端接收后调用 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/console,Key 在https://taotoken.net/api-keys生成。对于 Flowable 这种需要频繁调试接口的场景,统一 Key 的价值在于:你在 Postman 里验证通过的请求,复制到前端代码里不会因为凭证来源不同而失败。
需要说明的是,TaoToken 在这里扮演的是“统一凭证与调用入口”的角色,不是替代你的 SpringBoot 服务,也不是替代 Flowable 引擎。你的流程定义保存接口仍然跑在自己的后端,TaoToken 负责的是联调过程中那些辅助环节的凭证一致性。
配置方式很简单,在项目根目录建一个.env.local(不要提交到 git),写入:
# TaoToken 统一 Key 配置 TAOTOKEN_API_BASE=https://taotoken.net/api TAOTOKEN_API_KEY=sk-your-key-here TAOTOKEN_MODEL_ID=claude-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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 Result<ProcessDefinitionVO> 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) { List<ProcessDefinition> 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 塞进 header:
private 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.post<any>({ 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 = reactive<ProcessDefinitionInOrUpReq>({ 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 xxx - Request 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 Unauthorized:token 没带或过期
日志里通常是:
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 相关报错
error="invalid_grant", error_description="Bad 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下面必须先声明process,process里面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 Plan:https://taotoken.net/coding-plan。
最后说一个实际经验:流程定义保存接口的幂等性很重要。用户连点两次保存按钮,不应该产生两条部署记录。我的做法是在前端按钮上加 loading 状态,后端在 save 方法里用processKey + processVersion做唯一键校验。如果版本号是自增的,那第二次提交会因为版本号相同而被拦截。这个细节在测试阶段很容易被忽略,上线后却会变成数据问题。
下一篇会写流程定义的分页查询,涉及 Flowable 的 ACT_RE_PROCDEF 表和业务表的联合查询,以及如何把 BPMN 的 XML 从 MongoDB 里取出来渲染到前端。如果你在保存这一步还有没解决的问题,可以先检查 ACT_RE_PROCDEF 表里有没有数据——这是判断部署是否成功的最直接依据。