Files
Alfred/src/server/db/materials.ts
lanyuanxiaoyao 21b557c255 feat(inbox): 素材持久化 CRUD — 数据库表 + API + 前端接入
- 新增 materials 表(id/projectId/description/associatedDate/status/createdAt/updatedAt)
- 新增 4 个后端 API 路由(list/create/get/delete)+ 13 个测试
- 新增 use-materials hooks(TanStack Query)
- 收集箱页面重构为三层架构(InboxPage + MaterialSidebar + MaterialDetailPanel)
- MaterialCard: Popconfirm 删除确认 + 粗粒度时间格式
- MaterialContent: 展示状态标签 + createdAt
- 更新开发文档 backend.md / frontend.md
2026-06-03 14:53:23 +08:00

108 lines
3.3 KiB
TypeScript

import type Database from "bun:sqlite";
import { desc, eq } from "drizzle-orm";
import type { CreateMaterialRequest, Material, MaterialStatus } from "../../shared/api";
import type { Logger } from "../logger";
import { paginateQuery, wrap } from "./connection";
import { materials, projects } from "./schema";
export function createMaterial(
raw: Database,
projectId: string,
request: CreateMaterialRequest,
_logger: Logger,
): { error: string; status: number } | { material: Material } {
const db = wrap(raw);
const project = db.select().from(projects).where(eq(projects.id, projectId)).get();
if (!project) return { error: "项目不存在", status: 404 };
if (project.status === "archived") return { error: "已归档项目不可操作", status: 409 };
const description = request.description.trim();
if (!description) return { error: "描述不能为空", status: 400 };
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(request.associatedDate)) {
return { error: "associatedDate 格式错误,要求 YYYY-MM-DD", status: 400 };
}
const id = crypto.randomUUID();
const now = new Date().toISOString();
db.insert(materials)
.values({
associatedDate: request.associatedDate,
createdAt: now,
description,
id,
projectId,
status: "pending",
updatedAt: now,
})
.run();
const row = db.select().from(materials).where(eq(materials.id, id)).get();
return { material: toMaterial(row!) };
}
export function deleteMaterial(
raw: Database,
projectId: string,
materialId: string,
_logger: Logger,
): { error: string; status: number } | { success: true } {
const db = wrap(raw);
const row = db.select().from(materials).where(eq(materials.id, materialId)).get();
if (!row) return { error: "素材不存在", status: 404 };
if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 };
db.delete(materials).where(eq(materials.id, materialId)).run();
return { success: true };
}
export function getMaterial(
raw: Database,
projectId: string,
materialId: string,
): { error: string; status: number } | { material: Material } {
const db = wrap(raw);
const row = db.select().from(materials).where(eq(materials.id, materialId)).get();
if (!row) return { error: "素材不存在", status: 404 };
if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 };
return { material: toMaterial(row) };
}
export function listMaterials(
raw: Database,
projectId: string,
options: { page: number; pageSize: number; status?: MaterialStatus },
): { items: Material[]; page: number; pageSize: number; total: number } {
const conditions = [eq(materials.projectId, projectId)];
if (options.status) {
conditions.push(eq(materials.status, options.status));
}
return paginateQuery(raw, materials, {
conditions,
mapRow: toMaterial,
orderBy: () => desc(materials.createdAt),
page: options.page,
pageSize: options.pageSize,
});
}
function toMaterial(row: typeof materials.$inferSelect): Material {
return {
associatedDate: row.associatedDate,
createdAt: row.createdAt,
description: row.description,
id: row.id,
projectId: row.projectId,
status: row.status,
updatedAt: row.updatedAt,
};
}