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

@@ -13,7 +13,7 @@ import {
updateConversation,
updateConversationTimestamp,
} from "../../db/conversations";
import { getModelWithProvider } from "../../db/models";
import { getModelWithProvider, listModels } from "../../db/models";
import { createApiError, jsonResponse } from "../../helpers";
import { validateIdParam } from "../../middleware";
@@ -79,13 +79,23 @@ export async function handleSendChat(req: Request, db: Database, mode: RuntimeMo
let model;
try {
const result = getModelWithProvider(db, conversation.modelId);
let effectiveModelId = conversation.modelId;
if (!effectiveModelId) {
const fallback = listModels(db, { page: 1, pageSize: 1 });
const firstModel = fallback.items[0];
if (!firstModel) {
return jsonResponse(createApiError("没有可用的模型,请先配置模型", 400), { mode, status: 400 });
}
effectiveModelId = firstModel.id;
}
const result = getModelWithProvider(db, effectiveModelId);
if ("error" in result) {
return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status });
}
const registry = buildProviderRegistry(db);
model = registry.languageModel(`${result.provider.id}:${result.model.modelId}`);
model = registry.languageModel(`${result.provider.id}:${result.model.externalId}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return jsonResponse(createApiError(`模型初始化失败:${msg}`, 500), { mode, status: 500 });