mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
177 lines
8.3 KiB
Plaintext
177 lines
8.3 KiB
Plaintext
---
|
||
description:
|
||
globs: src/services/**/*,src/database/**/*,src/server/**/*
|
||
alwaysApply: false
|
||
---
|
||
|
||
# LobeChat 后端技术架构指南
|
||
|
||
本指南旨在阐述 LobeChat 项目的后端分层架构,重点介绍各核心目录的职责以及它们之间的协作方式。
|
||
|
||
## 目录结构映射
|
||
|
||
```
|
||
src/
|
||
├── server/
|
||
│ ├── routers/ # tRPC API 路由定义
|
||
│ └── services/ # 业务逻辑服务层
|
||
│ └── */impls/ # 平台特定实现
|
||
├── database/
|
||
│ ├── models/ # 数据模型 (单表 CRUD)
|
||
│ ├── repositories/ # 仓库层 (复杂查询/聚合)
|
||
│ └── schemas/ # Drizzle ORM 表定义
|
||
└── services/ # 客户端服务 (调用 tRPC 或直接访问 Model)
|
||
```
|
||
|
||
## 核心架构分层
|
||
|
||
LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应不同的运行环境(如浏览器端 PGLite、服务端远程 PostgreSQL 以及 Electron 桌面应用)。
|
||
|
||
其主要分层如下:
|
||
|
||
1. 客户端服务层 (`src/services`):
|
||
- 位于 src/services/。
|
||
- 这是客户端业务逻辑的核心层,负责封装各种业务操作和数据处理逻辑。
|
||
- 环境适配: 根据不同的运行环境,服务层会选择合适的数据访问方式:
|
||
- 本地数据库模式: 直接调用 `Model` 层进行数据操作,适用于浏览器 PGLite 和本地 Electron 应用。
|
||
- 远程数据库模式: 通过 `tRPC` 客户端调用服务端 API,适用于需要云同步的场景。
|
||
- 类型转换: 对于简单的数据类型转换,直接在此层进行类型断言,如 `this.pluginModel.query() as Promise<LobeTool[]>`
|
||
- 每个服务模块通常包含 `client.ts`(本地模式)、`server.ts`(远程模式)和 `type.ts`(接口定义)文件,在实现时应该确保本地模式和远程模式业务逻辑实现一致,只是数据库不同。
|
||
|
||
2. API 接口层 (`TRPC`):
|
||
- 位于 src/server/routers/
|
||
- 使用 `tRPC` 构建类型安全的 API。Router 根据运行时环境(如 Edge Functions, Node.js Lambda)进行组织。
|
||
- 负责接收客户端请求,并将其路由到相应的 `Service` 层进行处理。
|
||
- 新建 lambda 端点时可以参考 src/server/routers/lambda/\_template.ts
|
||
|
||
3. 仓库层 (`Repositories`):
|
||
- 位于 src/database/repositories/。
|
||
- 主要处理复杂的跨表查询和数据聚合逻辑,特别是当需要从多个 `Model` 获取数据并进行组合时。
|
||
- 与 `Model` 层不同,`Repository` 层专注于复杂的业务查询场景,而不涉及简单的领域模型转换。
|
||
- 当业务逻辑涉及多表关联、复杂的数据统计或需要事务处理时,会使用 `Repository` 层。
|
||
- 如果数据操作简单(仅涉及单个 `Model`),则通常直接在 `src/services` 层调用 `Model` 并进行简单的类型断言。
|
||
|
||
4. 模型层 (`Models`):
|
||
- 位于 src/database/models/ (例如 src/database/models/plugin.ts 和 src/database/models/document.ts)。
|
||
- 提供对数据库中各个表(由 src/database/schemas/ 中的 Drizzle ORM schema 定义)的基本 CRUD (创建、读取、更新、删除) 操作和简单的查询能力。
|
||
- `Model` 类专注于单个数据表的直接操作,不涉及复杂的领域模型转换,这些转换通常在上层的 `src/services` 中通过类型断言完成。
|
||
- model(例如 Topic) 层接口经常需要从对应的 schema 层导入 NewTopic 和 TopicItem
|
||
- 创建新的 model 时可以参考 src/database/models/\_template.ts
|
||
|
||
5. 数据库 (`Database`):
|
||
- 客户端模式 (浏览器/PWA): 使用 PGLite (基于 WASM 的 PostgreSQL),数据存储在用户浏览器本地。
|
||
- 服务端模式 (云部署): 使用远程 PostgreSQL 数据库。
|
||
- Electron 桌面应用:
|
||
- Electron 客户端会启动一个本地 Node.js 服务。
|
||
- 本地服务通过 `tRPC` 与 Electron 的渲染进程通信。
|
||
- 数据库选择依赖于是否开启云同步功能:
|
||
- 云同步开启: 连接到远程 PostgreSQL 数据库。
|
||
- 云同步关闭: 使用 PGLite (通过 Node.js 的 WASM 实现) 在本地存储数据。
|
||
|
||
## 数据流向说明
|
||
|
||
### 浏览器/PWA 模式
|
||
|
||
```
|
||
UI (React) → Zustand action -> Client Service → Model Layer → PGLite (本地数据库)
|
||
```
|
||
|
||
### 服务端模式
|
||
|
||
```
|
||
UI (React) → Zustand action → Client Service -> TRPC Client → TRPC Routers → Repositories/Models → Remote PostgreSQL
|
||
```
|
||
|
||
### Electron 桌面应用模式
|
||
|
||
```
|
||
UI (Electron Renderer) → Zustand action → Client Service -> TRPC Client → 本地 Node.js 服务 → TRPC Routers → Repositories/Models → PGLite/Remote PostgreSQL (取决于云同步设置)
|
||
```
|
||
|
||
## 服务层 (Server Services)
|
||
|
||
- 位于 src/server/services/。
|
||
- 核心职责是封装独立的、可复用的业务逻辑单元。这些服务应易于测试。
|
||
- 平台差异抽象: 一个关键特性是通过其内部的 `impls` 子目录(例如 src/server/services/file/impls 包含 s3.ts 和 local.ts)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
|
||
- 目标是使 `tRPC` router 层的逻辑尽可能纯粹,专注于请求处理和业务流程编排。
|
||
- 服务可能会调用 `Repository` 层或直接调用 `Model` 层进行数据持久化和检索,也可能调用其他服务。
|
||
|
||
## 最佳实践 (Best Practices)
|
||
|
||
### 数据库操作封装原则
|
||
|
||
**连续的数据库操作应该封装到 Model 层**
|
||
|
||
当业务逻辑涉及多个相关的数据库操作时,建议将这些操作封装到 Model 层中,而不是在上层(Service 或 Router 层)中进行多次数据库调用。
|
||
|
||
**优势:**
|
||
|
||
- **代码复用**: Client DB 环境的 service 实现和 Server DB 的 lambda 层实现可以复用相同的 Model 方法
|
||
- **事务一致性**: 相关的数据库操作可以在同一个方法中管理,便于维护数据一致性
|
||
- **性能优化**: 减少数据库连接次数,提高查询效率
|
||
- **职责清晰**: Model 层专注数据访问,上层专注业务协调
|
||
|
||
**示例:**
|
||
|
||
```typescript
|
||
// ✅ 推荐:在 Model 层封装连续的数据库操作
|
||
class GenerationBatchModel {
|
||
async delete(id: string): Promise<{ deletedBatch: BatchItem; thumbnailUrls: string[] }> {
|
||
// 1. 查询相关数据
|
||
const batchWithGenerations = await this.db.query.generationBatches.findFirst({...});
|
||
|
||
// 2. 收集需要处理的数据
|
||
const thumbnailUrls = [...];
|
||
|
||
// 3. 执行删除操作
|
||
const [deletedBatch] = await this.db.delete(generationBatches)...;
|
||
|
||
return { deletedBatch, thumbnailUrls };
|
||
}
|
||
}
|
||
|
||
// ✅ 上层使用简洁
|
||
const { thumbnailUrls } = await model.delete(id);
|
||
await fileService.deleteFiles(thumbnailUrls);
|
||
```
|
||
|
||
### 文件操作与数据库操作的执行顺序
|
||
|
||
**删除操作原则:数据库删除在前,文件删除在后**
|
||
|
||
当业务逻辑同时涉及数据库记录和文件系统操作时,应该遵循"数据库优先"的原则。
|
||
|
||
**原因:**
|
||
|
||
- **用户体验优先**: 如果先删除文件再删除数据库记录,可能出现文件已删除但数据库记录仍存在的情况,用户访问时会遇到文件不存在的错误
|
||
- **影响程度较小**: 如果先删除数据库记录再删除文件,即使文件删除失败,用户也看不到这个记录,只是造成一些存储空间浪费,对用户体验影响更小
|
||
- **数据一致性**: 数据库记录是业务逻辑的核心,应该优先保证其一致性
|
||
|
||
**示例:**
|
||
|
||
```typescript
|
||
// ✅ 推荐:先删除数据库记录,再删除文件
|
||
async deleteGeneration(id: string) {
|
||
// 1. 先删除数据库记录
|
||
const deletedGeneration = await generationModel.delete(id);
|
||
|
||
// 2. 再删除相关文件
|
||
if (deletedGeneration.asset?.thumbnailUrl) {
|
||
await fileService.deleteFile(deletedGeneration.asset.thumbnailUrl);
|
||
}
|
||
}
|
||
|
||
// ❌ 不推荐:先删除文件
|
||
async deleteGeneration(id: string) {
|
||
const generation = await generationModel.findById(id);
|
||
|
||
// 如果这里删除成功,但后面数据库删除失败,用户会遇到访问错误
|
||
await fileService.deleteFile(generation.asset.thumbnailUrl);
|
||
await generationModel.delete(id); // 可能失败
|
||
}
|
||
```
|
||
|
||
**创建操作原则:数据库创建在前,文件操作在后**
|
||
|
||
创建操作同样应该优先处理数据库记录,确保数据的一致性和完整性。
|