范實戰(zhàn):從 DDD 分層目錄到 TypeScript 與 Zod 工程實踐的全面解析)
FastGPT 代碼規(guī)范實戰(zhàn)從 DDD 分層目錄到 TypeScript 與 Zod 工程實踐的全面解析【免費下載鏈接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.項目地址: https://gitcode.com/GitHub_Trending/fa/FastGPTFastGPT 是一個基于 LLM 的知識庫問答平臺核心能力涵蓋數(shù)據(jù)處理、RAG 檢索與可視化 AI 工作流編排。其前端與后端共享大量類型與常量代碼規(guī)模橫跨packages/global、packages/service、packages/web等多個包因此建立統(tǒng)一、可維護的代碼規(guī)范尤為重要。本文以倉庫內(nèi) .agents/code/syntax.md 為骨架結(jié)合 packages/service/core/app 等真實源碼系統(tǒng)講解 FastGPT 的 DDD 目錄組織、文件職責劃分、層級依賴約束以及 TypeScript 類型體系、Zod 校驗與 OpenAPI 風格的落地方式幫助你寫出符合社區(qū)規(guī)范、易于評審與維護的代碼?;A(chǔ)代碼組織模式按業(yè)務(wù)域 → 子功能 → 固定文件三層劃分FastGPT 采用 DDD領(lǐng)域驅(qū)動設(shè)計架構(gòu)包內(nèi)代碼按業(yè)務(wù)域 → 子功能 → 固定文件三層劃分。以packages/為例核心劃分如下packages/ ├── global/core/ # 類型、常量前后端共享 │ ├── app/ │ │ ├── type.ts # 頂層聚合類型 │ │ ├── constants.ts │ │ ├── workflow/ │ │ │ ├── type.ts │ │ │ └── constants.ts │ │ ├── version/ │ │ │ └── type.ts │ │ └── evaluation/ │ │ └── type.ts │ ├── chat/ │ ├── dataset/ │ └── plugin/ │ └── service/core/ # 后端業(yè)務(wù)邏輯不可在前端引用 ├── app/ │ ├── schema.ts # App 主表 Mongoose Schema │ ├── entity.ts # findById / create / updateById 等基礎(chǔ)操作封裝 │ ├── service.ts # 聚合業(yè)務(wù)邏輯跨子功能協(xié)調(diào)不允許互相引用只允許單向依賴 │ ├── auth.ts # 鑒權(quán)相關(guān)如有 │ ├── utils.ts # 純函數(shù)工具無副作用可獨立單測 │ ├── version/ │ │ ├── schema.ts │ │ ├── entity.ts │ │ ├── service.ts │ │ └── utils.ts │ ├── evaluation/ │ │ ├── schema.ts # 合并多個 schema 到單文件 │ │ ├── entity.ts │ │ ├── service.ts │ │ └── utils.ts │ ├── logs/ │ └── tool/ │ ├── service.ts │ └── utils.ts ├── chat/ ├── dataset/ └── plugin/在倉庫中可以看到這一約定已落實到實際代碼packages/service/core/app 目錄下存在schema.ts、controller.ts、utils.ts以及version/、evaluation/、tool/等子目錄與文檔中的目錄骨架一一對應(yīng)。葉子目錄固定文件說明每個葉子目錄不再細分子功能的目錄統(tǒng)一放置四個固定文件職責嚴格分離文件職責schema.tsMongoose Schema 定義導出 Model 和 SchemaTypeentity.ts數(shù)據(jù)訪問封裝findById、create、updateById等基礎(chǔ)操作service.ts業(yè)務(wù)邏輯調(diào)用 entity跨模塊協(xié)調(diào)處理業(yè)務(wù)規(guī)則utils.ts純函數(shù)工具無副作用可獨立單測entity.ts與service.ts的分工示例// entity.ts 示例 —— 只做數(shù)據(jù)訪問不含業(yè)務(wù)判斷 export const findAppById (id: string) MongoApp.findById(id).lean(); export const createApp (data: AppCreateParams, session?: ClientSession) MongoApp.create([data], { session }); // service.ts 示例 —— 調(diào)用 entity處理業(yè)務(wù)規(guī)則 export const createAppAndInitVersion async (data: AppCreateParams, session?: ClientSession) { const app await createApp(data, session); await createVersion({ appId: app._id, ... }, session); return app; }; // service 需協(xié)同通過 props 傳入另一個 service 或者衍生方法。 const service1 xxxx const service2 (props: {id:string; service1: typeof service1 }) { const data findAppById(id) return props.service1(data); };核心原則是entity.ts只做數(shù)據(jù)訪問、不含業(yè)務(wù)判斷service.ts調(diào)用 entity 并處理業(yè)務(wù)規(guī)則utils.ts保持純函數(shù)、無副作用因此可以脫離數(shù)據(jù)庫獨立做單元測試。層級約束global/core/只放類型和常量禁止引入 mongoose、服務(wù)端 SDKservice/core/只在服務(wù)端使用禁止被packages/web/或前端頁面直接引用子功能目錄不超過3 層嵌套一個目錄內(nèi)無需拆子功能時直接放schema.tsentity.tsservice.tsutils.ts多個 schema 文件如evalSchema.tsevalItemSchema.ts合并到單個schema.ts。從 packages/service/core/app/schema.ts 可以看到AppSchema定義了parentId、teamId、tmbId、name、type、version等字段并通過AppCollectionName apps統(tǒng)一管理集合名符合schema 只定義數(shù)據(jù)模型的定位。代碼風格貫穿類型安全與可讀性的具體約定禁止 re-export禁止使用export { ... } from ...、export type { ... } from ...或export * from ...轉(zhuǎn)導其他模塊的成員包括index.tsbarrel、目錄聚合入口和兼容舊路徑的轉(zhuǎn)發(fā)文件。每個導出只能由其實際定義文件提供使用方直接從定義模塊導入index.ts可以包含自身的實現(xiàn)和定義但不能聚合導出其他文件移動定義時直接修改所有使用方的 import確認舊路徑無引用后刪除舊文件不為縮短 import 路徑、隱藏目錄結(jié)構(gòu)或兼容舊路徑新增轉(zhuǎn)導層避免依賴來源不明確、循環(huán)依賴和無效模塊加載。// ? 不好的實踐通過目錄入口轉(zhuǎn)導其他模塊 export { createLLMResponse } from ./createLLMResponse; export type { LLMResponse } from ./type; export * from ./constants; // ? 好的實踐使用方直接引用成員的定義模塊 import { createLLMResponse } from fastgpt/service/core/ai/llm/createLLMResponse; import type { LLMResponse } from fastgpt/global/core/ai/llm/type;使用type進行類型聲明不使用interface// ? 不好的實踐 interface User { id: string; name: string; } // ? 好的實踐 type User { id: string; name: string; }使用type的考量在于聯(lián)合類型、交叉類型、映射類型等能力只有type具備且統(tǒng)一使用type可以避免interface聲明合并帶來的隱式行為。使用 IIFE 寫法取代 if/else 進行變量條件賦值// ? 不好的實踐 if (condition) { value true; } else { value false; } // ? 好的實踐 const value (() { if (condition) { return true; } return false; })();IIFE 將條件分支收斂在表達式內(nèi)部變量聲明與賦值在一條語句中完成避免先聲明后賦值造成的中間狀態(tài)與作用域泄漏。類型推導Zod schema 同時承擔校驗和類型用z.infer從 schema 推導類型不重復手寫相同結(jié)構(gòu)的 type避免校驗邏輯與類型定義出現(xiàn)兩處維護點導致漂移。// ? 不好的實踐 type MessageParam { role: user | assistant; content: string }; const MessageParamSchema z.object({ role: z.enum([user, assistant]), content: z.string() }); // ? 好的實踐 export const MessageParamSchema z.discriminatedUnion(role, [...]); export type MessageParam z.infertypeof MessageParamSchema;Zod Schema 與 OpenAPI 風格一套 schema 三處復用Zod schema 在 FastGPT 中同時承擔運行時校驗、TypeScript 類型推導和 OpenAPI 生成來源。新增或調(diào)整 API 時按以下規(guī)則組織業(yè)務(wù)通用結(jié)構(gòu)放在業(yè)務(wù)歸屬目錄例如packages/global/core/app/type.ts、packages/global/core/workflow/type/node.ts、packages/global/support/permission/**/controller.ts。packages/global/openapi/**只聲明接口 query/body/response/path、接口專用兼容處理和 OpenAPI 文檔信息不把通用配置類型、權(quán)限對象、工具配置等只為文檔復制到 openapi 目錄。OpenAPI schema 優(yōu)先復用業(yè)務(wù) schema。需要字段說明時優(yōu)先在業(yè)務(wù) schema 上補齊meta只屬于某個接口視角的說明可以在 openapi schema 里用SomeSchema.shape.field.meta(...)補充。不要重復建立同構(gòu) schema也不要用export const A B這種重命名 alias 當作新 schema 導出。只有接口邊界確實需要特殊兼容時才在 openapi 目錄定義專用 wrapper例如把{}兼容為undefined、或去掉 JSON Schema 不支持的 function 字段。此類 wrapper 附近要寫清楚原因。API response schema 默認聲明業(yè)務(wù)data結(jié)構(gòu)不重復聲明統(tǒng)一響應(yīng) envelope例如code、statusText。只有路由實際直接返回這些字段時才把它們寫進 schema。只為實際存在的業(yè)務(wù)入?yún)⒑蜆I(yè)務(wù)出參定義 Schema。請求完全沒有 query、body 或 params 時不創(chuàng)建z.object({})占位 Schema也不調(diào)用parseApiInputOpenAPI 省略對應(yīng)的requestParams/requestBody。沒有業(yè)務(wù)返回數(shù)據(jù)的成功響應(yīng)不創(chuàng)建z.undefined()、z.null()或z.object({})占位 Schemahandler 直接不返回值或返回undefinedOpenAPI 只保留狀態(tài)碼和說明統(tǒng)一響應(yīng)中間件會把undefined包成data: null。存在實際業(yè)務(wù)數(shù)據(jù)時仍必須在 API 邊界執(zhí)行 Zod 校驗。每個對外字段補齊description關(guān)鍵入?yún)⒑头祷刂笛aexample。如果字段語義屬于業(yè)務(wù)通用結(jié)構(gòu)優(yōu)先把meta寫到通用 schema如果只是某接口視角寫到 openapi schema。API 入?yún)ⅰ⒖蛻舳藗鬏斀Y(jié)構(gòu)和需要容錯解析的配置字段優(yōu)先使用packages/global/common/zod里的BoolSchema、NumSchema、IntSchema。不要直接寫z.coerce.number()普通數(shù)值用NumSchema非負整數(shù)、數(shù)量、分頁、limit 用IntSchema布爾配置和查詢參數(shù)用BoolSchema。只有明確需要嚴格拒絕字符串/數(shù)字形式時才保留z.number()或z.boolean()。廢棄字段用.meta({ deprecated: true })標記可同時保留業(yè)務(wù)說明例如description: 舊版團隊標簽。不要只寫/** deprecated */也不要只把已廢棄寫進description。Tag 歸屬按能力復用判斷通用模塊接口被業(yè)務(wù)模塊使用時同時加通用模塊 tag 和業(yè)務(wù)模塊 tag業(yè)務(wù)模塊自己的狀態(tài)查詢或狀態(tài)操作只加業(yè)務(wù)模塊 tag。API key 文檔只給實際開放接口加 apikey 專用 tag不開放的接口不要為了分類加 tag。僅客戶端使用的 API 也要以客戶端實際傳參為準定義 schema避免schema.parse因number、boolean的字符串形態(tài)導致業(yè)務(wù)不可用。理解 BoolSchema / NumSchema / IntSchema 的容錯語義上述規(guī)則提到的三個通用 Schema 定義在 packages/global/common/zod/index.ts 中其實現(xiàn)揭示了為什么不要手寫z.coerce.number()import z from zod; import { stripUrlTrailingSlash } from ../string/url; const truthyBoolStrs [true, 1, yes, y, on]; export const BoolSchema z.preprocess((val) { if (typeof val boolean) return val; if (typeof val string) { return truthyBoolStrs.includes(val.trim().toLowerCase()); } if (typeof val number) { if (val 1) return true; if (val 0) return false; } return val; }, z.boolean()); export const NumSchema z.coerce.numbernumber(); export const IntSchema NumSchema.int().nonnegative(); export const UrlSchema z.string().url().transform(stripUrlTrailingSlash);可以看到BoolSchema通過z.preprocess把字符串true/1/yes/y/on忽略大小寫與首尾空格和數(shù)字1/0統(tǒng)一歸一化為布爾值避免查詢參數(shù)以字符串形態(tài)傳入時校驗失敗NumSchema是對z.coerce.number()的統(tǒng)一封裝負責把字符串數(shù)字安全轉(zhuǎn)換為 numberIntSchema在NumSchema基礎(chǔ)上追加.int().nonnegative()專用于非負整數(shù)場景如數(shù)量、分頁、limit只有明確需要嚴格拒絕字符串/數(shù)字形式時才直接使用z.number()或z.boolean()。在實際 API 定義中的用法如下import { BoolSchema, IntSchema, NumSchema } from fastgpt/global/common/zod; export const UpdateConfigSchema z.object({ enabled: BoolSchema.meta({ description: 是否啟用 }), limit: IntSchema.optional().meta({ description: 最大數(shù)量 }), temperature: NumSchema.optional().meta({ description: 溫度參數(shù) }), teamTags: z.array(z.string()).optional().meta({ description: 舊版團隊標簽, deprecated: true }) });其他 TypeScript 編碼約定可選鏈調(diào)用回調(diào)用?.()調(diào)用可選回調(diào)取代if (fn) fn()的冗余寫法。// ? 不好的實踐 if (onProgress) { onProgress({ phase: creatingContainer }); } // ? 好的實踐 onProgress?.({ phase: creatingContainer });空值合并取默認值用??取代||處理默認值避免0、false、被錯誤覆蓋。// ? 不好的實踐 const version lastVersion?.version || 0; // version 為 0 時被誤覆蓋 const text item?.value || ; // ? 好的實踐 const version (lastVersion?.version ?? -1) 1; const text item?.value ?? ;這是||的經(jīng)典陷阱0、、false都是合法值卻被||當作假值錯誤替換成默認值??只在null/undefined時兜底語義更精確。解構(gòu)重命名同名變量來自多個來源時解構(gòu)時重命名避免命名沖突。// ? 不好的實踐 const r1 await getSkillGuidance(...); const r2 await createLLMResponse(...); const inputTokens r1.usage.inputTokens r2.usage.inputTokens; // ? 好的實踐 const { usage: guidanceUsage } await getSkillGuidance(...); const { usage: generateUsage } await createLLMResponse(...); const inputTokens guidanceUsage.inputTokens generateUsage.inputTokens;類型守衛(wèi)用is關(guān)鍵字收窄unknown/any類型替代強制斷言。// ? 不好的實踐 function process(value: unknown) { const n value as number; // 不安全 } // ? 好的實踐 const isValidNumber (value: unknown): value is number typeof value number Number.isFinite(value); if (isValidNumber(value)) { // 此處 value 安全收窄為 number }value is number是 TypeScript 的類型謂詞語法配合Number.isFinite可同時排除NaN、Infinity比裸as number更安全。非關(guān)鍵清理用.catch()鏈次要的清理操作不影響主流程用.catch()吞掉錯誤不污染主 try/catch。// ? 不好的實踐 try { await client.delete(); } catch { // 清理失敗主流程中斷 } // ? 好的實踐 await client.delete().catch(() {});適用于資源釋放、臨時文件刪除、日志上報等非關(guān)鍵路徑避免清理失敗把主流程的異常處理邏輯攪亂。函數(shù)參數(shù)不超過 2 個多參數(shù)用對象傳遞獨立參數(shù)不超過 2 個超過時改為對象參數(shù)便于擴展且無需關(guān)心順序。// ? 不好的實踐 function createVersion(skillId: string, teamId: string, tmbId: string, version: number) {} // ? 好的實踐 function createVersion(data: { skillId: string; teamId: string; tmbId: string; version: number }) {}數(shù)據(jù)寫操作函數(shù)支持可選 session 參數(shù)涉及數(shù)據(jù)庫寫操作的函數(shù)統(tǒng)一支持可選的session參數(shù)便于上層組合事務(wù)。事務(wù)統(tǒng)一通過mongoSessionRun發(fā)起內(nèi)部自動處理 startTransaction / commit / abort / retry。import { mongoSessionRun } from fastgpt/service/common/mongo/sessionRun; import { type ClientSession } from fastgpt/service/common/mongo; // entity.ts —— 基礎(chǔ)操作透傳 session export const createVersion (data: CreateVersionData, session?: ClientSession) MongoAppVersion.create([data], { session }); // service.ts —— 需要事務(wù)時用 mongoSessionRun 包裹外部已有 session 時直接傳入 export const createAppAndInitVersion async ( data: AppCreateParams, session?: ClientSession ) { const create async (session: ClientSession) { const app await createApp(data, session); await createVersion({ appId: app._id, version: 0 }, session); return app; }; if (session) { return create(session); } else { return mongoSessionRun(create); } };這一約定與底層實現(xiàn) packages/service/common/mongo/sessionRun.ts 完全對應(yīng)mongoSessionRun通過connectionMongo.startSession()開啟會話調(diào)用session.withTransaction()執(zhí)行事務(wù)回調(diào)MongoDB driver 會處理TransientTransactionError事務(wù)級重試與UnknownTransactionCommitResult并設(shè)置maxCommitTimeMS超時上限60 秒針對 ACL 增量寫入的并發(fā)沖突定義了MongoTransactionConflictError在maxConflictRetries3 次范圍內(nèi)記錄 warn 日志并用新 session 重試業(yè)務(wù)錯誤則保持原樣拋出最后在finally中endSession()釋放會話。使用該模式時需注意entity.ts層面的寫操作只需透傳session不自行開啟事務(wù)service.ts需要組合多個寫操作時優(yōu)先檢查外部是否已傳入session避免嵌套事務(wù)否則用mongoSessionRun自建事務(wù)事務(wù)適合多表一致性寫入場景例如創(chuàng)建 App 后同步初始化 version 記錄若中途失敗可整體回滾??偨Y(jié)FastGPT 的代碼規(guī)范是一套面向大型 monorepo 的實戰(zhàn)工程約定DDD 三層目錄與固定文件職責劃分保證了類型共享、服務(wù)端隔離的清晰邊界type、IIFE、可選鏈、??、解構(gòu)重命名、類型守衛(wèi)等 TypeScript 細節(jié)約定統(tǒng)一了團隊代碼風格Zod 單源 schema 同時服務(wù)運行時校驗、類型推導與 OpenAPI 生成配合BoolSchema/NumSchema/IntSchema的容錯語義與meta描述讓 API 邊界健壯且文檔自洽而session透傳與mongoSessionRun事務(wù)封裝則讓復雜業(yè)務(wù)可以在不引入嵌套事務(wù)的前提下安全組合寫操作。無論是為 FastGPT 貢獻代碼還是在類似規(guī)模的項目中制定工程規(guī)范這套約定都值得作為參考基線。如需深入了解目錄落地情況可繼續(xù)閱讀 packages/service/core/app/schema.tsApp 主表 Schema 與集合名定義、packages/global/common/zod/index.ts通用容錯 Schema 實現(xiàn)以及 packages/service/common/mongo/sessionRun.ts事務(wù)運行器實現(xiàn)。【免費下載鏈接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.項目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考