refactor(db): 统一数据库 schema — 软删除、命名规范、约束标准化

- 全表新增 deleted_at 列,统一软删除替代硬删除+archived_at
- models.model_id 重命名为 external_id,消除语义混淆
- conversations.model_id 改为可空(模型为建议而非绑定)
- messages 新增 updated_at,移除 CASCADE 改为 DAO 层级联
- 移除 DB 层 UNIQUE 约束,改为应用层检查(配合软删除)
- 新增 helpers.ts(baseColumns + 构造层防御)、ESLint 规则、契约测试
- 迁移 0004 补全 CHECK 约束(providers.type/materials.status/messages.role)
- DAO 层全面重写:级联软删除、应用层唯一、provider 删除保护
- 路由/前端/测试全量适配 externalId 重命名及类型变更
This commit is contained in:
2026-06-05 01:02:23 +08:00
parent e25b2537fd
commit db40d04dc5
37 changed files with 1564 additions and 324 deletions

View File

@@ -20,9 +20,14 @@
SQLite + bun:sqlite + Drizzle ORM。
- `src/server/db/schema.ts`Drizzle 表结构,列名 snake_caseTS 类型 camelCase。
- `src/server/db/connection.ts``createDatabase(dataDir, logger)` 打开 `alfred.db`PRAGMAforeign_keys=ON、journal_mode=WAL、busy_timeout=5000。`wrap(db)` 转为 Drizzle 实例。`paginateQuery()` 分页工具
- Migration开发期 `drizzle-kit generate` 产出到 `drizzle/`;生产期嵌入可执行文件,启动时自动应用。备份到 `<dataDir>/backups/`,事务中执行,失败回滚
- `src/server/db/schema.ts`Drizzle 表结构,列名 snake_caseTS 类型 camelCase。所有业务表通过 `helpers.ts``baseColumns` 获取 id/created_at/updated_at/deleted_at。
- `src/server/db/helpers.ts``baseColumns` 常量id、createdAt、updatedAt、deletedAt+ Drizzle 构建器再导出。`src/server/db/` 内禁止直接从 `drizzle-orm/sqlite-core` 导入 `sqliteTable`ESLint 强制)
- `src/server/db/connection.ts``createDatabase(dataDir, logger)` 打开 `alfred.db`PRAGMAforeign_keys=ON、journal_mode=WAL、busy_timeout=5000。`wrap(db)` 转 Drizzle 实例(`DrizzleDB` 类型)。工具函数:`timestamp()``notDeleted(table)``softDeleteRecord(db, table, id)``paginateQuery()`(支持 `softDelete` 参数自动过滤已删除行)
- Migration开发期 `drizzle-kit generate` 产出到 `drizzle/`;生产期嵌入可执行文件,启动时自动应用。备份到 `<dataDir>/backups/`,事务中执行(迁移期间临时关闭外键检查),失败回滚。
### 软删除
所有业务表projects、providers、models、conversations、materials、messages使用 `deleted_at` 列实现软删除,不暴露给 API 层。DAO 查询通过 `notDeleted(table)``paginateQuery({ softDelete })` 自动过滤已删除行。唯一性校验在应用层完成(同名 + `deleted_at IS NULL`),无数据库级 UNIQUE 约束。级联软删除:删除项目 → 级联软删除会话(→ 消息)+ 素材;删除会话 → 级联软删除消息;删除供应商 → 需无未删除模型。
### 数据访问函数
@@ -38,7 +43,7 @@ SQLite + bun:sqlite + Drizzle ORM。
## AI 服务层
- `src/server/ai/types.ts``AIProviderConfig`name、type、baseUrl、apiKey`AIModelConfig`providerId、modelId、capabilities
- `src/server/ai/types.ts``AIProviderConfig`name、type、baseUrl、apiKey`AIModelConfig`providerId、modelId、capabilitiesAI 层 `modelId` 对应 DB 层 `Model.externalId`
- `src/server/ai/registry.ts`
- `buildProviderRegistry(db)` — 从 DB 查询供应商构建 AI SDK Provider Registry每次调用重建不缓存。通过 `registry.languageModel('providerId:modelId')` 获取模型实例。
- `testProviderConnection(config, logger)` — 测试 Base URL 可达性 + `/models` 接口
@@ -57,7 +62,7 @@ SQLite + bun:sqlite + Drizzle ORM。
### 连通性测试
- `POST /api/providers/test` — 用未保存配置测试,不写入 DB不阻止保存。Base URL 不可达或 API Key 无效返回 `ok: false``/models` 不支持返回 `ok: true` + 提示。
- `POST /api/models/test` — 用模型关联供应商 + modelId 测试。
- `POST /api/models/test` — 用模型关联供应商 + externalId 测试。
## 素材 API
@@ -66,9 +71,9 @@ SQLite + bun:sqlite + Drizzle ORM。
| GET | `/api/projects/:id/materials` | 列出项目下素材(分页) |
| POST | `/api/projects/:id/materials` | 创建素材 |
| GET | `/api/projects/:id/materials/:mid` | 获取素材详情 |
| DELETE | `/api/projects/:id/materials/:mid` | 删除素材(删除) |
| DELETE | `/api/projects/:id/materials/:mid` | 删除素材(删除) |
校验description 必填非空associatedDate 必填 YYYY-MM-DD项目须存在且 active素材归属校验不匹配返回 403。
校验description 必填非空associatedDate 必填 YYYY-MM-DD项目须存在且 active 且未删除,素材归属校验不匹配返回 403。
## 聊天 API