:以 `spec.parent` 為基準的 Console 樹 API 與單次位置更新設(shè)計)
Halo 控制臺分類樹管理重構(gòu)以spec.parent為基準的 Console 樹 API 與單次位置更新設(shè)計【免費下載鏈接】haloHalo 是一款強大易用的開源建站工具從個人博客、知識庫到企業(yè)官網(wǎng)、在線商城Halo 都能助您輕松實現(xiàn)一站式滿足您的多樣化建站需求。項目地址: https://gitcode.com/GitHub_Trending/ha/halo分類目錄樹是 Halo 內(nèi)容管理的核心交互之一從拖拽排序、多級嵌套到共享的分類選擇器都依賴一套可編輯的分類層級。Halo 早期的實現(xiàn)把層級邏輯大量留在前端 Vue 工具中——先拉平分類列表、本地構(gòu)建可編輯樹、拖拽后前端自行重算所有兄弟節(jié)點priority、再把整棵樹拍平為一批 JSON Patch 并發(fā)提交。本文基于 spec.md 的需求基線并結(jié)合當(dāng)前倉庫中的源碼完整講解 Halo 如何把這條職責(zé)邊界后移到后端新增返回規(guī)范化canonical分類樹的 Console 樹 API以及一次僅移動一個分類的position更新 API讓前端只保留交互狀態(tài)。讀完你將掌握這兩類新 API 的路徑、請求語義、后端校驗與優(yōu)先級重算規(guī)則以及前端如何圍繞它們重構(gòu)。一、重構(gòu)背景把層級所有權(quán)從 Vue 工具交還給后端在本次改動之前Console 分類管理的調(diào)用鏈大致是列出全部分類flat 列表在前端Vue本地構(gòu)建可編輯樹拖拽后由前端遍歷差異為受影響兄弟列表重算每個spec.priority將整棵樹扁平化回多個 Category并對這些分類并發(fā)發(fā)送層次 JSON Patch 請求。design.md對該問題給出了明確的判定前端擁有過多層級行為。這種做法的隱患是規(guī)范排序規(guī)則被復(fù)制到了 UI 層且一次拖拽可能產(chǎn)生多個部分寫入存在部分保存失敗的失敗模式。與此相對Console 菜單層級menu hierarchy的改造已經(jīng)建立了更合理的邊界——后端 Console API 返回規(guī)范化樹數(shù)據(jù)并接受單個相對位移請求前端只保留交互狀態(tài)。本次簡化 Console 分類樹管理改動正是讓分類管理遵循同一模型只是分類沒有 menu 那樣的歸屬字段menu 由 owning menu field 定位層級因此需要專門的分類樹接口詳見 design.md。二、數(shù)據(jù)模型前提spec.parent與spec.priority是唯一層次寫入點本次 Console 改造并非憑空發(fā)明新字段它建立在分類層級以Category.spec.parent為運行時唯一事實來源這一更大的數(shù)據(jù)模型遷移之上。在 Category.java 中可以看到該模型的關(guān)鍵設(shè)計spec.parent父分類的metadata.name根分類不設(shè)置此字段注釋明確 Root categories leave this unsetspec.priority同級排序優(yōu)先級默認值0spec.children保留的舊字段已被Deprecated(since 2.26.0)標(biāo)記并在 schema 上聲明deprecated true層級不再從它推導(dǎo)常量 HIERARCHY_MIGRATED_LABELcontent.halo.run/category-hierarchy-migrated用于標(biāo)記已完成遷移的分類供遷移組件判斷與重試。在本次 Console 重構(gòu)的需求邊界內(nèi)所有關(guān)于把分類放到哪里、排在哪位的寫操作都必須收斂為對spec.parent與spec.priority的更新并且這些計算只能發(fā)生在后端。舊的spec.children在本改動中既不會被寫入、也不會被重算見 design.md 的 Non-Goals。三、讀取側(cè)Console 分類樹 API 返回規(guī)范化層級3.1 端點定義需求 Console category tree APIs provide canonical hierarchy 要求系統(tǒng)提供讀取與更新可編輯分類層級的 Console API。它落地為兩條自定義端點定義在 CategoryEndpoint.java 中方法路徑operationId職責(zé)GETapis/api.console.halo.run/v1alpha1/categories/-/treeListCategoryTree將分類以規(guī)范化樹返回供 Console 分類管理使用PUTapis/api.console.halo.run/v1alpha1/categories/{name}/positionUpdateCategoryPosition在 Console 樹內(nèi)移動一個分類選擇position位移端點 返回整棵樹而不是直接 PUT 一整棵樹design.md給出了理由拖拽在語義上是一次單一用戶動作專用位置端點比接受整棵樹更清晰design.md Decision 1。3.2 響應(yīng)節(jié)點形狀CategoryTreeNode樹響應(yīng)不是復(fù)用主題側(cè) VO而是專門的 Console DTO CategoryTreeNode.javaCategoryTreeNode { Category category; ListCategoryTreeNode children; }設(shè)計文檔對比了備選方案復(fù)用CategoryTreeVo或直接在 Category 擴展對象里塞children。兩者都被否決CategoryTreeVo面向主題渲染含parentName、文章計數(shù)投影等主題輸出關(guān)切在 API 響應(yīng)里直接給 Category 加children則會模糊擴展?fàn)顟B(tài)與可編輯樹視圖數(shù)據(jù)的界限。因此新增的CategoryTreeNode節(jié)點包含原始 Category 擴展與只讀子節(jié)點列表design.md Decision 2。3.3children是視圖數(shù)據(jù)不是存儲數(shù)據(jù)spec 中有一個極易混淆的要點見 spec.md返回的樹節(jié)點里確實叫children但它是視圖數(shù)據(jù)view data絕不寫回Category.spec.children。也就是說這個children與已棄用的存儲字段同名卻不同義存儲的層次關(guān)系完全在spec.parent上表達樹中的嵌套只是后端按parent組裝出來的投影。需求原文措辭 SHALL be view data and SHALL NOT write toCategory.spec.children 正是在防止實現(xiàn)者順手把樹又拍平回舊字段。3.4 建樹容錯無效父引用一律按根節(jié)點渲染真實生產(chǎn)數(shù)據(jù)可能被插件或歷史導(dǎo)入污染。為此樹構(gòu)建必須容錯渲染。需求 Console category tree handles invalid parent referencesspec.md要求當(dāng)某個分類存在缺失父、自引用、循環(huán)父鏈時受影響分類應(yīng)被渲染為根分類其余鏈條合法的后代仍正常返回。這一邏輯在 CategoryConsoleService.listToTree 中實現(xiàn)其算法分三步validParentMap()只登記父存在、且父名不等于自身的邊L156-L165缺失父與自引用自然被過濾cyclicNames()沿著父鏈做環(huán)檢測將處于環(huán)中的節(jié)點名集合標(biāo)記出來L167-L183組裝子樹后只有parentMap中不存在父、或?qū)儆诃h(huán)的節(jié)點被提升為根L146-L153從而保證 Console 樹在異常數(shù)據(jù)下依然可用。3.5 規(guī)范化排序規(guī)則需求 Console category tree is ordered canonicallyspec.md規(guī)定同一父下多個分類依次按priority、創(chuàng)建時間戳、metadata.name排序。這正是 defaultCategoryComparator() 的鏈式比較器隨后sortTree遞歸應(yīng)用到每一層L185-L188。對priority缺省的分類取0L203-L207創(chuàng)建時間用nullsFirst兜底。換句話說同級的先后順序從此只有后端一處實現(xiàn)前端無需再復(fù)制任何排序口徑。3.6 共享分類選擇器統(tǒng)一走樹需求 Category select uses canonical treespec.md面向console-src下的共享categorySelect組件渲染選項、鍵盤導(dǎo)航、搜索結(jié)果路徑都必須使用 Console 樹 API 返回的樹。spec 同時允許前端在本地把樹拉平flatten用于搜索與選中值解析——這體現(xiàn)了明確的邊界樹的來源與結(jié)構(gòu)由后端權(quán)威給出扁平化只是本地索引型視圖。四、寫入側(cè)一次移動一個分類的 position API4.1 相對位置請求parentNamebeforeName移動語義的關(guān)鍵在請求體設(shè)計。CategoryPositionRequest是只有兩個可空字段的 recordCategoryPositionRequest.javarecord CategoryPositionRequest(Nullable String parentName, Nullable String beforeName) {}兩個字段的語義組合完整覆蓋了三種移動這些場景被逐條固化為 spec 需求parentNamebeforeName效果spec 場景目標(biāo)父名目標(biāo)前一兄弟名移動到該父下、指定兄弟之前Console moves a category by relative position目標(biāo)父名未設(shè)置/null追加到該父兄弟列表末尾Category position update appends to a sibling list未設(shè)置/null任意服務(wù)端不校驗移除spec.parent成為根分類追加到根兄弟列表末尾Category position update moves category to root實現(xiàn)入口在 CategoryConsoleService.updatePosition真正執(zhí)行的是applyMoveL62-L122。一次成功的位移會返回更新后的完整規(guī)范化樹前端直接以該樹替換本地狀態(tài)因此位置語義是相對位移、絕對返回。4.2 服務(wù)端校驗四類拒絕spec 用四個場景明確了 position 更新的非法輸入均以ServerWebInputExceptionHTTP 400拒絕逐條對應(yīng)applyMove中的檢查無效相對對象parentName或beforeName指向不存在的分類 → 拒絕L79-L89被移動的分類本身不存在則返回 404L70-L73目標(biāo)同級不一致beforeName在應(yīng)用移動后的目標(biāo)父兄弟列表中找不到 → 拒絕L101-L105成環(huán)把分類移到自己或自己的后代之下 → 拒絕。實現(xiàn)用isDescendant()沿父鏈上溯檢測L215-L230其中自身作為父L76-L78也單獨攔截附帶地若目標(biāo)父本身已處于環(huán)鏈中也會拋異常拒絕。4.3 兄弟優(yōu)先級重算連續(xù)整數(shù) 最小持久化spec Category position update recalculates sibling prioritiesspec.md規(guī)定了寫入規(guī)則與前端自算 priority 批量 patch的舊模式形成鮮明對比目標(biāo)兄弟列表被賦予從 0 開始的連續(xù)整數(shù)priorityassignPriorities按新順序下標(biāo)逐位寫入L249-L258若父級發(fā)生變化原兄弟列表同樣重算為從 0 開始的連續(xù)整數(shù)L110-L112避免留下空洞只持久化spec.parent或spec.priority確實發(fā)生變化的分類先對每個分類快照原始(parentName, priority)HierarchyStaterecordL272再經(jīng)hasHierarchyChanged()過濾出差異集后逐個client.updateL114-L121。這從設(shè)計上把寫什么、寫多少完全收歸后端前端不再需要推導(dǎo)任何持久化用的 priority 數(shù)值。4.4 并發(fā)沖突樂觀鎖重試 409分類層級允許多人同時編輯后端寫操作按擴展機制攜帶版本號并發(fā)沖突會拋OptimisticLockingFailureException。處理策略對應(yīng) updatePosition是退避重試1 次Retry.backoff(1, Duration.ofMillis(100))重試耗盡后映射為409 Conflict響應(yīng)體注明 Category position update conflicted.前端收到失敗后重取規(guī)范化樹見下節(jié)讓雙方狀態(tài)重新對齊。五、前端改造只保留交互狀態(tài)本次改動的需求集中條目 Console category management writes parent references 從加載創(chuàng)建根/子分類拖拽保存保存失敗移到根等維度約束了 Console 行為spec.md。5.1 狀態(tài)入口usePostCategory 消費樹 API分類管理的數(shù)據(jù)入口 composable use-post-category.ts 與需求一一對應(yīng)通過生成的 Console API client 調(diào)用consoleApiClient.content.category.listCategoryTree()獲取樹queryKey 為[post-categories]setCategoriesTree同步維護三份狀態(tài)權(quán)威樹categoriesTree、拖拽前的樹快照previousCategoriesTreecloneDeep深拷貝、供過濾/搜索/選中解析使用的拉平數(shù)組categoriesL16-L20樹中若存在帶刪除時間戳或尚無permalinkstatus 未就緒的異常分類則以 1 秒間隔自動輪詢刷新L29-L35。spec 中 Console SHALL NOT build the editable tree from a flat Category list 由此落實本地只做拉平索引flattenCategoryTreeNodes位于 categories/utils/index.ts絕不再本地拼接可編輯樹。5.2 拖拽保存 派生一條 position 請求Console saves drag-and-drop hierarchy 場景spec.md定義了拖拽保存的理想流程管理員把分類拖到新位置Console 發(fā)送單次position 更新含目標(biāo)父與目標(biāo)前一兄弟前端不自行計算spec.priority持久化值前端不用層級 JSON Patch 批量 patch 分類前端用后端返回的規(guī)范化樹替換本地樹。previousCategoriesTree快照正是為步驟 2 服務(wù)的比較拖拽前后兩棵樹推導(dǎo)出哪一個分類、移動到哪個 parent、插在哪個 before 之前的唯一移動請求。若差異無法用一個單一移動解釋例如出現(xiàn)意外的多節(jié)點變化設(shè)計文檔的風(fēng)險章節(jié)給出的對策是放棄推測、直接重取樹絕不以模糊的本地狀態(tài)作為持久化結(jié)果design.md Risks。5.3 失敗回退重載權(quán)威樹Console handles drag-and-drop save failurespec.md與 5.2 共同組成一致性閉環(huán)position 請求一旦失敗Console 必須重新加載規(guī)范化樹不得保留未確認的本地拖拽狀態(tài)。同樣的原則也覆蓋移到根管理員將分類拖到根層時Console 發(fā)送parentName為 null 的 position 更新而不再通過前端 JSON Patch 移除/spec/parentspec.md——補丁式寫層次的做法在此被整體移除。5.4 編輯彈窗中更改父級更早的 spec 版本還細化了編輯分類彈窗改父級的交互在本 archive 對應(yīng)的 category-hierarchy/spec.md 通用需求 之外的openspec/specs正式版本中需求 Console edits category parents 與此一脈相承編輯既有分類時展示父分類下拉含無父選項候選來自權(quán)威樹且必須排除被編輯分類自身及其所有后代防止成環(huán)更換父級保存時發(fā)送parentName為選中父、beforeName為 null 的 position 更新追加到目標(biāo)兄弟末尾未更改父級則不發(fā)送 position 更新保留既有層級位置保存字段成功但移動失敗時前端上報失敗并刷新權(quán)威樹。這驗證了一個更普適的設(shè)計結(jié)論凡是會產(chǎn)生層級變化的寫操作無論入口是拖拽還是編輯彈窗最終都收斂為同一個 position 更新端點。六、權(quán)限與范圍邊界RBAC 層面design 文檔要求分類角色模板補充categories/tree與categories/position兩個 Console 資源design.md Decision 6。同時明確這是本改動的 Non-GoalConsole UI 仍沿用system:posts:*權(quán)限字符串切換到system:categories:*屬于獨立的授權(quán)清理工作不在此次范圍內(nèi)不新增 Console 專屬分類創(chuàng)建 API分類創(chuàng)建依舊走核心 Category API初始 priority 的前端計算保留到后續(xù)專門的 Console create API 中解決不刪除或改寫已棄用的Category.spec.children不改變分類刪除語義無數(shù)據(jù)遷移本次為純代碼級重構(gòu)既有spec.parent存儲格式不變回滾僅需回退代碼design.md Migration Plan Rollback。七、落地順序與驗證design 文檔給出的實施順序是先補后端 DTO、服務(wù)、端點、RBAC 規(guī)則與測試 → 重新生成 OpenAPI 文檔與 UI API 客戶端 → 更新usePostCategory()及各消費方 → 用單次 position 更新替換批量層級保存 → 刪除不再使用的前端層級持久化工具并更新單元測試design.md Migration Plan。倉庫中可直接核驗的產(chǎn)物包括后端單元/端點測試CategoryConsoleServiceTest.java、CategoryEndpointTest.java覆蓋建樹容錯、移動校驗、優(yōu)先級重算等 spec 場景數(shù)據(jù)遷移測試CategoryHierarchyMigrationTest.java驗證從舊children到spec.parent的安全遷移屬于該模型的更早一環(huán)實現(xiàn)位于 CategoryHierarchyMigration.java生成的客戶端與契約category-v1alpha1-console-api.ts 與 category-position-request.ts以及 OpenAPI 文檔 apis_console.api_v1alpha1.json前端工具測試categories/utils/__tests__/index.spec.ts。八、小結(jié)一條可復(fù)用的職責(zé)邊界把本次改動的核心契約壓縮成一句話樹只能從后端讀GET/categories/-/tree層級只能通過一次相對位移寫PUT/categories/{name}/positionspec.parent與spec.priority的重算、校驗、排序與最小化持久化全部由服務(wù)端承擔(dān)前端只負責(zé)用返回值刷新權(quán)威狀態(tài)。這套規(guī)范化讀 單點相對寫 響應(yīng)替換 失敗重載的模式同樣被 Console 菜單層級管理采用是 Halo Console 處理樹形數(shù)據(jù)的一類樣板方案。理解它也就理解了如何為 Console 設(shè)計既簡單又強一致的樹形資源接口?!久赓M下載鏈接】haloHalo 是一款強大易用的開源建站工具從個人博客、知識庫到企業(yè)官網(wǎng)、在線商城Halo 都能助您輕松實現(xiàn)一站式滿足您的多樣化建站需求。項目地址: https://gitcode.com/GitHub_Trending/ha/halo創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考