習(xí)筆記(六)Vue3+SpringBoot3實現(xiàn)流程定義保存功能:TaoToken統(tǒng)一Key打通前后端聯(lián)調(diào))
1. 流程定義保存鏈路為什么總在聯(lián)調(diào)階段卡住Flowable 7.x 的流程定義保存本質(zhì)上是把前端畫好的 BPMN XML 提交到后端由后端調(diào)用 Flowable 的部署接口完成入庫。聽起來只有兩步但真正動手時問題往往不在 Flowable 本身而在前后端之間的那層“膠水”前端提交的 JSON 字段名和后端接收對象對不上、雪花 ID 傳到瀏覽器精度丟失、日期格式一個帶 T 一個不帶、接口鑒權(quán)頭拼錯、跨域預(yù)檢失敗。我見過太多項目在“保存”這個按鈕上耗掉一整天。這篇筆記聚焦的場景很具體Vue3 前端用 el-drawer 收集流程定義元數(shù)據(jù)并提交 BPMN XMLSpringBoot3 后端接收后調(diào)用 Flowable 的 RepositoryService 完成部署同時把流程定義記錄寫入業(yè)務(wù)表。為了讓前后端聯(lián)調(diào)不再被環(huán)境配置和鑒權(quán)問題打斷我會用 TaoToken 的統(tǒng)一 Key 來管理模型調(diào)用與接口調(diào)試環(huán)節(jié)的憑證把注意力留給業(yè)務(wù)邏輯本身。適合誰看已經(jīng)搭好 Flowable 7.x 基礎(chǔ)工程、寫過流程定義列表頁、現(xiàn)在要補(bǔ)“新增/保存”功能的開發(fā)者。你需要對 Vue3 的 ref/reactive、SpringBoot3 的 RestController、MyBatis-Plus 有基本了解。全文的代碼都可以直接復(fù)制路徑和包名按你自己的工程調(diào)整即可。先說清楚一個容易混淆的點Flowable 的“部署”和業(yè)務(wù)表的“保存”是兩件事。部署是把 BPMN XML 解析成流程定義寫入 ACT_RE_* 系列表業(yè)務(wù)表的保存是記錄流程的 key、名稱、版本、狀態(tài)、描述這些管理信息。兩者要么放在同一個事務(wù)里要么先部署拿到 deploymentId 再寫業(yè)務(wù)表。我選擇后者因為部署失敗時業(yè)務(wù)表不該留下臟數(shù)據(jù)。聯(lián)調(diào)階段最典型的報錯是 401 和 403。401 是沒帶 token 或 token 過期403 是帶了 token 但接口權(quán)限沒配。這兩個錯誤在瀏覽器 Network 面板里長得幾乎一樣但排查方向完全不同。下面我會把這兩類問題的定位方法拆開講。2. TaoToken 統(tǒng)一 Key 在聯(lián)調(diào)環(huán)境里的定位與配置在講具體代碼之前先解決一個環(huán)境問題。前后端聯(lián)調(diào)時我們經(jīng)常需要在多個工具之間切換瀏覽器調(diào)接口、Postman 測后端、IDE 里跑單元測試、有時候還要用 AI 輔助生成 BPMN 片段或排查報錯。每個工具都配一遍 Key改一次環(huán)境就要同步一遍很容易出現(xiàn)“瀏覽器能通、Postman 報 401”這種問題。TaoToken 的做法是提供一個統(tǒng)一的 API 入口和 Key 管理把模型對話、編碼輔助、接口調(diào)試這些場景的憑證收斂到一處。它的 API 地址是https://taotoken.net/api控制臺在https://taotoken.net/consoleKey 在https://taotoken.net/api-keys生成。對于 Flowable 這種需要頻繁調(diào)試接口的場景統(tǒng)一 Key 的價值在于你在 Postman 里驗證通過的請求復(fù)制到前端代碼里不會因為憑證來源不同而失敗。需要說明的是TaoToken 在這里扮演的是“統(tǒng)一憑證與調(diào)用入口”的角色不是替代你的 SpringBoot 服務(wù)也不是替代 Flowable 引擎。你的流程定義保存接口仍然跑在自己的后端TaoToken 負(fù)責(zé)的是聯(lián)調(diào)過程中那些輔助環(huán)節(jié)的憑證一致性。配置方式很簡單在項目根目錄建一個.env.local不要提交到 git寫入# TaoToken 統(tǒng)一 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 要成對出現(xiàn)Model ID 按你實際使用的模型填。這三件套Base URL Key Model ID是接入任何兼容 Anthropic 協(xié)議的工具時都必須完整的缺一個就會報認(rèn)證失敗或模型不存在。對于 Cline、Cursor 這類支持 MCP 的工具配置里同樣需要這三項。我試過只填 Key 不填 Base URL結(jié)果請求打到了默認(rèn)地址報了一個很迷惑的 404。后來把三項補(bǔ)齊就正常了。提示.env.local和settings.json都要加入.gitignore。Key 泄露的后果不用我多說尤其是團(tuán)隊協(xié)作時一個提交就可能讓 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 無關(guān)但它是后面所有聯(lián)調(diào)工作的基礎(chǔ)——憑證不通后面全是白費。3. 可復(fù)制的后端配置與前端請求片段這一節(jié)是全文的核心給出可以直接粘貼的配置和代碼。先看后端。3.1 SpringBoot3 的 Flowable 與數(shù)據(jù)源配置application.yml里需要配置 Flowable 的數(shù)據(jù)庫和異步執(zhí)行器。Flowable 7.x 默認(rèn)使用 Spring Boot 3 的自動配置但有幾個參數(shù)必須顯式指定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會在啟動時自動創(chuàng)建 ACT_ 系列表開發(fā)環(huán)境很方便生產(chǎn)環(huán)境建議改成false并用 SQL 腳本手動建表。async-executor-activate: false在聯(lián)調(diào)階段關(guān)掉異步執(zhí)行器避免定時任務(wù)干擾日志。check-process-definitions: false防止 Flowable 去掃描resources/processes目錄因為我們的流程定義是從前端提交的不是預(yù)置文件。3.2 流程定義保存接口的完整實現(xiàn)Controller 層接收前端提交的 JSON包含 BPMN XML 字符串和元數(shù)據(jù)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(參數(shù)異常: {}, e.getMessage(), e); return Result.error(參數(shù)異常 e.getMessage()); } catch (Exception e) { log.error(保存失敗: {}, e.getMessage(), e); return Result.error(保存失敗 e.getMessage()); } } }Service 層的 save 方法要處理兩件事調(diào)用 Flowable 部署 BPMN XML以及寫業(yè)務(wù)表。部署部分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的第二個參數(shù)是資源名必須以.bpmn20.xml結(jié)尾否則 Flowable 不會解析。這是很多人第一次部署時踩的坑——文件傳上去了但 ACT_RE_PROCDEF 表里沒有記錄。3.3 前端 axios 封裝與請求片段前端的https.ts負(fù)責(zé)請求攔截和響應(yīng)攔截。請求攔截器把 token 塞進(jìn) 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 }響應(yīng)攔截器統(tǒng)一處理 401 跳轉(zhuǎn)和錯誤提示。這里有個細(xì)節(jié)handleResponse直接返回res.data所以業(yè)務(wù)代碼里拿到的就是后端Result對象不用再.data一層。保存接口的定義export function saveProcessDefinition(data: ProcessDefinitionInOrUpReq) { return request.postany({ url: /pm-process/api/v1/definition/save, data, }) }/pm-process是網(wǎng)關(guān)前綴實際請求會打到http://gateway/pm-process/api/v1/definition/save。如果你的工程沒有網(wǎng)關(guān)把 baseURL 直接指向后端端口即可。3.4 前端抽屜表單的關(guān)鍵參數(shù)el-drawer 里的表單參數(shù)用 reactive 定義校驗規(guī)則用 reactive 定義const processDefinitionParams reactiveProcessDefinitionInOrUpReq({ id: 0, processKey: , processName: , processVersion: 1, processStatus: 0, description: , deleted: false, bpmnXml: , })bpmnXml字段是新增的用來承載前端 BPMN 編輯器產(chǎn)出的 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. 驗證請求與成功結(jié)果確認(rèn)配置寫完之后必須驗證整條鏈路。我習(xí)慣分三步先驗后端接口再驗前端請求最后驗數(shù)據(jù)庫。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 }返回應(yīng)該是{ success: true, code: 200, message: 操作成功, data: { id: 1234567890123456789, processKey: leave_apply, processName: 請假申請, processVersion: 1, deploymentId: 1, processStatus: 0 } }注意id是字符串這是JsonSerialize(using ToStringSerializer.class)的效果。如果返回的是數(shù)字且末尾變成 000說明雪花 ID 精度丟了前端拿到的 id 是錯的。4.2 檢查 Flowable 的部署表部署成功后ACT_RE_DEPLOYMENT 和 ACT_RE_PROCDEF 表里應(yīng)該有記錄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 前端聯(lián)調(diào)時的 Network 檢查打開瀏覽器 DevTools 的 Network 面板點保存按鈕看請求Request URL 是否正確網(wǎng)關(guān)前綴有沒有拼錯Request Headers 里 Authorization 是否存在且格式為Bearer xxxRequest Payload 里 bpmnXml 是否完整有時候編輯器返回的 XML 被轉(zhuǎn)義了Response 的 code 是否為 200如果 Response 是 401檢查 token 是否過期如果是 403檢查PreAuthorize的權(quán)限字符串是否和用戶角色匹配。4.4 數(shù)據(jù)庫最終確認(rèn)業(yè)務(wù)表process_definition里應(yīng)該有對應(yīng)記錄deployment_id字段和 ACT_RE_DEPLOYMENT 的 ID_ 一致。這一步確認(rèn)了業(yè)務(wù)表和 Flowable 引擎表的數(shù)據(jù)一致性。5. 聯(lián)調(diào)常見報錯排查對照這一節(jié)列出我實際遇到過的報錯以及定位方法。每個報錯都給出真實日志片段和解決方向。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 默認(rèn)有效期通常很短最后看 token 的類型前綴是不是Bearer。如果用的是 TaoToken 的 Key 調(diào)模型接口401 通常是 Key 寫錯或 Base URL 不匹配。5.2 403 Forbidden權(quán)限字符串不匹配后端日志org.springframework.security.access.AccessDeniedException: 不允許訪問這個錯誤的根源是PreAuthorize(hasAnyAuthority(/api/v1/definition/save))里的權(quán)限字符串和用戶實際擁有的權(quán)限不一致。解決方法是去權(quán)限管理表里給當(dāng)前角色分配這個權(quán)限或者臨時把注解注釋掉驗證邏輯。5.3 local proxy failed網(wǎng)關(guān)或跨域問題前端控制臺報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這是跨域問題。開發(fā)環(huán)境用 Vite 的 proxy 解決// vite.config.ts server: { proxy: { /pm-process: { target: http://127.0.0.1:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/pm-process/, ), }, }, }生產(chǎn)環(huán)境由 Nginx 處理不要在后端加CrossOrigin了事那樣會帶來安全隱患。5.4 reading choices響應(yīng)結(jié)構(gòu)解析失敗前端報TypeError: Cannot read properties of undefined (reading choices)這個錯誤通常出現(xiàn)在調(diào)用模型接口時響應(yīng)結(jié)構(gòu)和預(yù)期不符。比如你期望的是 OpenAI 格式的choices數(shù)組但實際返回的是 Anthropic 格式的content數(shù)組。檢查你的請求體里 model 字段和接口協(xié)議是否匹配。用 TaoToken 時Base URL 決定了協(xié)議格式/api/v1/messages是 Anthropic 格式/api/v1/chat/completions是 OpenAI 格式兩者不能混用。5.5 OAuth2 相關(guān)報錯errorinvalid_grant, error_descriptionBad credentials這是 OAuth2 密碼模式登錄時用戶名或密碼錯誤。檢查請求體里的 username/password以及 client_id/client_secret 是否正確。如果是授權(quán)碼模式檢查 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 對元素順序有嚴(yán)格要求definitions下面必須先聲明processprocess里面startEvent要在sequenceFlow之前。建議用 bpmn-js 的校驗功能先在前端過濾一遍。5.7 雪花 ID 精度丟失前端拿到的 id 是1234567890123456800末尾幾位變成 0。這是 JavaScript Number 類型最大安全整數(shù)是 2^53-1 導(dǎo)致的。解決方法是在 VO 的 id 字段加JsonSerialize(using ToStringSerializer.class)讓后端序列化成字符串。前端 TS 類型定義里 id 也要聲明為string而不是number。6. 把保存鏈路固化成可復(fù)用的聯(lián)調(diào)模板走到這里流程定義保存的鏈路應(yīng)該已經(jīng)跑通了。但聯(lián)調(diào)不是一次性的工作下次加新接口時還會遇到同樣的問題。我的做法是把這套配置固化成一個模板新項目直接復(fù)制。模板包含四部分后端的 Flowable 配置片段、前端的 axios 封裝、TaoToken 的三件套配置、以及一份排查清單。排查清單按錯誤碼分類401 查 token、403 查權(quán)限、跨域查 proxy、XML 報錯查命名空間、精度丟失查序列化注解。這份清單貼在團(tuán)隊 wiki 上新人遇到問題先自查能省掉大量重復(fù)溝通。關(guān)于 TaoToken 的使用我的建議是把它當(dāng)成聯(lián)調(diào)環(huán)境的“憑證中樞”。模型對話、編碼輔助、接口調(diào)試這些場景共用一套 Key切換工具時不用重新配置。需要生成 Key 的話去https://taotoken.net/api-keys接入文檔在https://taotoken.net/doc模型對話入口在https://taotoken.net/chat。如果你在做長期的 Flowable 工作流開發(fā)涉及大量 Agent 編排和編碼任務(wù)可以看看 Coding Planhttps://taotoken.net/coding-plan。最后說一個實際經(jīng)驗流程定義保存接口的冪等性很重要。用戶連點兩次保存按鈕不應(yīng)該產(chǎn)生兩條部署記錄。我的做法是在前端按鈕上加 loading 狀態(tài)后端在 save 方法里用processKey processVersion做唯一鍵校驗。如果版本號是自增的那第二次提交會因為版本號相同而被攔截。這個細(xì)節(jié)在測試階段很容易被忽略上線后卻會變成數(shù)據(jù)問題。下一篇會寫流程定義的分頁查詢涉及 Flowable 的 ACT_RE_PROCDEF 表和業(yè)務(wù)表的聯(lián)合查詢以及如何把 BPMN 的 XML 從 MongoDB 里取出來渲染到前端。如果你在保存這一步還有沒解決的問題可以先檢查 ACT_RE_PROCDEF 表里有沒有數(shù)據(jù)——這是判斷部署是否成功的最直接依據(jù)。