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