- 新增 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
108 lines
3.3 KiB
TypeScript
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,
|
|
};
|
|
}
|