feat: add inspection object resource workflow

This commit is contained in:
2026-08-13 16:51:48 +08:00
parent 3db84a7fb2
commit 29a69793db
11 changed files with 2312 additions and 935 deletions

View File

@@ -91,6 +91,7 @@ async function validateRegistrationRunInput(input: {
if (!cad) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的 CAD 文件不存在。" });
}
await validateTaskInputCompatibility(input.taskId, pointcloud, cad);
const registeredChildren = pointcloudFiles
.filter((row) => row.source === "registered" && row.sourcePointcloudFileId === pointcloud.id)
@@ -104,6 +105,29 @@ async function validateRegistrationRunInput(input: {
}
}
async function validateTaskInputCompatibility(
taskId: number,
pointcloud: Awaited<ReturnType<typeof db.listTaskPointcloudFiles>>[number],
cad: Awaited<ReturnType<typeof db.listTaskCadFiles>>[number],
) {
const task = await db.getInspectionTaskById(taskId);
if (!task) throw new TRPCError({ code: "NOT_FOUND", message: "检测任务不存在。" });
if (!task.inspectionObjectId) {
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "历史任务尚未确认检测对象,请联系管理员完成归类后再运行算法。" });
}
if (pointcloud.source === "model_library" && pointcloud.sourceModelPointcloudId) {
const source = await db.getModelPointcloudFileById(pointcloud.sourceModelPointcloudId);
if (!source || source.inspectionObjectId !== task.inspectionObjectId) {
throw new TRPCError({ code: "BAD_REQUEST", message: "点云与当前检测对象不兼容。" });
}
}
if (cad.source === "model_library" && cad.sourceModelCadFileId) {
if (!await db.cadAppliesToInspectionObject(cad.sourceModelCadFileId, task.inspectionObjectId)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "CAD 与当前检测对象不兼容。" });
}
}
}
function latestRegisteredPointcloud(
pointcloudFiles: Awaited<ReturnType<typeof db.listTaskPointcloudFiles>>,
sourcePointcloudFileId: number,
@@ -581,16 +605,88 @@ export const appRouter = router({
}),
}),
// ─── 检测对象配置 ─────────────────────────────────────────────────────────
inspectionObject: router({
list: protectedProcedure
.input(z.object({ modelId: z.number(), activeOnly: z.boolean().optional() }))
.query(({ input }) => db.listInspectionObjects(input.modelId, input.activeOnly ?? false)),
create: adminProcedure
.input(z.object({ modelId: z.number(), name: z.string().trim().min(1).max(100), description: z.string().optional() }))
.mutation(({ input }) => db.createInspectionObject({ ...input, description: input.description ?? null, status: "draft" })),
update: adminProcedure
.input(z.object({ id: z.number(), name: z.string().trim().min(1).max(100).optional(), description: z.string().optional() }))
.mutation(({ input }) => {
const { id, ...data } = input;
return db.updateInspectionObject(id, data);
}),
configureDefaults: adminProcedure
.input(z.object({ id: z.number(), defaultModelPointcloudId: z.number(), defaultModelCadFileId: z.number(), active: z.boolean() }))
.mutation(async ({ input }) => {
try { return await db.configureInspectionObject(input); }
catch (error) { throw new TRPCError({ code: "BAD_REQUEST", message: error instanceof Error ? error.message : "默认输入方案无效" }); }
}),
setStatus: adminProcedure
.input(z.object({ id: z.number(), status: z.enum(["draft", "active"]) }))
.mutation(async ({ input }) => {
if (input.status === "active") {
const object = await db.getInspectionObjectById(input.id);
if (!object?.defaultModelPointcloudId || !object.defaultModelCadFileId) {
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "缺少预置点云或默认 CAD,不能启用检测对象" });
}
return db.configureInspectionObject({
id: object.id,
defaultModelPointcloudId: object.defaultModelPointcloudId,
defaultModelCadFileId: object.defaultModelCadFileId,
active: true,
});
}
return db.updateInspectionObject(input.id, { status: "draft" });
}),
delete: adminProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
try {
await db.deleteInspectionObject(input.id);
return { success: true };
} catch (error) {
if (error instanceof db.InspectionObjectReferencedError) {
throw new TRPCError({ code: "PRECONDITION_FAILED", message: error.message });
}
if (error instanceof Error && error.message === "检测对象不存在") {
throw new TRPCError({ code: "NOT_FOUND", message: error.message });
}
throw error;
}
}),
}),
// ─── 车型 CAD 文件 ─────────────────────────────────────────────────────────
cadFile: router({
list: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listModelCadFiles(input.modelId);
const files = await db.listModelCadFiles(input.modelId);
const objectIds = await db.listCadInspectionObjectIds(files.map(file => file.id));
return files.map(file => ({ ...file, inspectionObjectIds: objectIds[file.id] ?? [] }));
}),
delete: protectedProcedure
listForInspectionObject: protectedProcedure
.input(z.object({ modelId: z.number(), inspectionObjectId: z.number() }))
.query(({ input }) => db.listModelCadFilesForInspectionObject(input.modelId, input.inspectionObjectId)),
setInspectionObjects: adminProcedure
.input(z.object({ id: z.number(), inspectionObjectIds: z.array(z.number()).min(1) }))
.mutation(async ({ input }) => {
const cad = await db.getModelCadFileById(input.id);
if (!cad) throw new TRPCError({ code: "NOT_FOUND", message: "CAD 文件不存在" });
const objects = await Promise.all(input.inspectionObjectIds.map(id => db.getInspectionObjectById(id)));
if (objects.some(object => !object || object.modelId !== cad.modelId)) throw new TRPCError({ code: "BAD_REQUEST", message: "适用检测对象必须属于 CAD 当前车型" });
await db.setCadInspectionObjects(input.id, input.inspectionObjectIds);
return { success: true };
}),
delete: adminProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteModelCadFile(input.id);
@@ -623,6 +719,14 @@ export const appRouter = router({
return db.listModelPointcloudFiles(input.modelId);
}),
listForInspectionObject: protectedProcedure
.input(z.object({ modelId: z.number(), inspectionObjectId: z.number() }))
.query(({ input }) => db.listModelPointcloudFilesForInspectionObject(input.modelId, input.inspectionObjectId)),
assignInspectionObject: adminProcedure
.input(z.object({ id: z.number(), inspectionObjectId: z.number() }))
.mutation(({ input }) => db.assignModelPointcloudInspectionObject(input.id, input.inspectionObjectId)),
rename: adminProcedure
.input(z.object({ id: z.number(), baseName: z.string() }))
.mutation(async ({ input }) => {
@@ -640,7 +744,7 @@ export const appRouter = router({
return db.updateModelPointcloudFileName(row.id, name);
}),
delete: protectedProcedure
delete: adminProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteModelPointcloudFile(input.id);
@@ -720,9 +824,23 @@ export const appRouter = router({
return db.getInspectionTaskById(input.id);
}),
classify: adminProcedure
.input(z.object({ id: z.number(), inspectionObjectId: z.number() }))
.mutation(async ({ input }) => {
const task = await db.getInspectionTaskById(input.id);
const object = await db.getInspectionObjectById(input.inspectionObjectId);
if (!task || !object) throw new TRPCError({ code: "NOT_FOUND", message: "任务或检测对象不存在" });
if (task.modelId !== object.modelId || object.status !== "active") {
throw new TRPCError({ code: "BAD_REQUEST", message: "只能确认当前车型下已启用的检测对象" });
}
await db.classifyInspectionTask(task.id, object.id);
return { success: true };
}),
create: protectedProcedure
.input(z.object({
modelId: z.number(),
inspectionObjectId: z.number(),
name: z.string().trim().min(1).max(255),
inspector: z.string().max(100).optional(),
notes: z.string().optional(),
@@ -734,8 +852,9 @@ export const appRouter = router({
}
try {
return await db.createInspectionTask({
return await db.createInspectionTaskWithDefaults({
modelId: input.modelId,
inspectionObjectId: input.inspectionObjectId,
name: input.name,
inspector: input.inspector ?? null,
notes: input.notes ?? null,
@@ -1212,6 +1331,7 @@ export const appRouter = router({
if (!cad) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的 CAD 文件不存在。" });
}
await validateTaskInputCompatibility(input.taskId, sourcePointcloud, cad);
const registration = await createAndEnqueuePluginJob({
taskId: input.taskId,