feat: add model file library links

This commit is contained in:
2026-07-07 18:15:41 +08:00
parent 7e2e56a50d
commit b5440fa1c3
7 changed files with 358 additions and 6 deletions

View File

@@ -0,0 +1,22 @@
-- 车型点云库,以及任务输入对车型库文件的轻量关联
CREATE TABLE IF NOT EXISTS `model_pointcloud_files` (
`id` INT NOT NULL AUTO_INCREMENT,
`modelId` INT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`fileSize` BIGINT NOT NULL,
`filePath` VARCHAR(512) NOT NULL,
`createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_model_pointcloud_files_modelId` (`modelId`)
);
ALTER TABLE `task_pointcloud_files`
ADD COLUMN `sourceModelPointcloudId` INT NULL
AFTER `sourceResultId`;
ALTER TABLE `task_cad_files`
ADD COLUMN `source` VARCHAR(32) NOT NULL DEFAULT 'uploaded'
AFTER `filePath`,
ADD COLUMN `sourceModelCadFileId` INT NULL
AFTER `source`;

View File

@@ -0,0 +1,5 @@
-- 记录配准产物来自哪个任务点云文件,用于阻止链式配准并检测重复配准
ALTER TABLE `task_pointcloud_files`
ADD COLUMN `sourcePointcloudFileId` INT NULL
AFTER `sourceResultId`;

View File

@@ -62,6 +62,21 @@ export const modelCadFiles = mysqlTable("model_cad_files", {
export type ModelCadFile = typeof modelCadFiles.$inferSelect; export type ModelCadFile = typeof modelCadFiles.$inferSelect;
export type InsertModelCadFile = typeof modelCadFiles.$inferInsert; export type InsertModelCadFile = typeof modelCadFiles.$inferInsert;
// ─── 车型点云文件 ─────────────────────────────────────────────────────────────
// 一个车型可维护多个通用点云文件,供任务按需关联复用
export const modelPointcloudFiles = mysqlTable("model_pointcloud_files", {
id: int("id").autoincrement().primaryKey(),
modelId: int("modelId").notNull(),
name: varchar("name", { length: 255 }).notNull(),
fileSize: bigint("fileSize", { mode: "number" }).notNull(),
filePath: varchar("filePath", { length: 512 }).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type ModelPointcloudFile = typeof modelPointcloudFiles.$inferSelect;
export type InsertModelPointcloudFile = typeof modelPointcloudFiles.$inferInsert;
// ─── 任务 CAD 文件 ───────────────────────────────────────────────────────────── // ─── 任务 CAD 文件 ─────────────────────────────────────────────────────────────
// 每个任务可关联任意个 CAD 文件(与车型级 CAD 独立,用于本次检测的实测参考) // 每个任务可关联任意个 CAD 文件(与车型级 CAD 独立,用于本次检测的实测参考)
@@ -72,6 +87,10 @@ export const taskCadFiles = mysqlTable("task_cad_files", {
format: varchar("format", { length: 32 }).notNull(), format: varchar("format", { length: 32 }).notNull(),
fileSize: bigint("fileSize", { mode: "number" }).notNull(), fileSize: bigint("fileSize", { mode: "number" }).notNull(),
filePath: varchar("filePath", { length: 512 }).notNull(), filePath: varchar("filePath", { length: 512 }).notNull(),
/** 来源:uploaded = 用户上传;model_library = 车型 CAD 库关联 */
source: varchar("source", { length: 32 }).default("uploaded").notNull(),
/** 来源车型 CAD 文件 ID(仅 model_library 时有值) */
sourceModelCadFileId: int("sourceModelCadFileId"),
createdAt: timestamp("createdAt").defaultNow().notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(),
}); });
@@ -105,10 +124,14 @@ export const taskPointcloudFiles = mysqlTable("task_pointcloud_files", {
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
fileSize: bigint("fileSize", { mode: "number" }).notNull(), fileSize: bigint("fileSize", { mode: "number" }).notNull(),
filePath: varchar("filePath", { length: 512 }).notNull(), filePath: varchar("filePath", { length: 512 }).notNull(),
/** 来源:uploaded = 用户上传;registered = 配准算法产出 */ /** 来源:uploaded = 用户上传;registered = 配准算法产出;model_library = 车型点云库关联 */
source: varchar("source", { length: 32 }).default("uploaded").notNull(), source: varchar("source", { length: 32 }).default("uploaded").notNull(),
/** 产出该点云的分析结果 ID(仅 registered 时有值) */ /** 产出该点云的分析结果 ID(仅 registered 时有值) */
sourceResultId: int("sourceResultId"), sourceResultId: int("sourceResultId"),
/** 生成该配准产物的原始任务点云 ID(仅 registered 时有值) */
sourcePointcloudFileId: int("sourcePointcloudFileId"),
/** 来源车型点云文件 ID(仅 model_library 时有值) */
sourceModelPointcloudId: int("sourceModelPointcloudId"),
createdAt: timestamp("createdAt").defaultNow().notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(),
}); });

View File

@@ -43,6 +43,26 @@ function ensureDir(dir: string) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
function formatRegistrationTimestamp(date: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return [
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate()),
'-',
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds()),
].join('');
}
function registeredPointcloudDisplayName(sourceName: string | undefined, derivedFile: string, resultId: number): string {
const sourceParsed = path.parse(sourceName || 'pointcloud');
const stem = sourceParsed.name || 'pointcloud';
const ext = path.extname(derivedFile) || sourceParsed.ext || '.ply';
return `${stem}_已配准_${formatRegistrationTimestamp(new Date())}_R${resultId}${ext}`;
}
/** 把 process / mock cmd 模板里的占位符替换成实际路径。 */ /** 把 process / mock cmd 模板里的占位符替换成实际路径。 */
function resolvePlaceholders(value: string, ctx: Record<string, string>): string { function resolvePlaceholders(value: string, ctx: Record<string, string>): string {
return value.replace(/\{(\w+)\}/g, (_, key) => ctx[key] ?? `{${key}}`); return value.replace(/\{(\w+)\}/g, (_, key) => ctx[key] ?? `{${key}}`);
@@ -91,7 +111,7 @@ async function gatherInputs(
} }
} }
return { task, pointCloudPaths, cadPath }; return { task, pointCloudRecords, pointCloudPaths, cadPath };
} }
async function writeInputJson( async function writeInputJson(
@@ -282,7 +302,12 @@ export async function runAlgorithmJob(
try { try {
await patchProgress(resultId, pluginId, 0.02, '准备输入'); await patchProgress(resultId, pluginId, 0.02, '准备输入');
const { pointCloudPaths, cadPath } = await gatherInputs(taskId, manifest, pointCloudFileIds, cadFileIds); const { pointCloudRecords, pointCloudPaths, cadPath } = await gatherInputs(
taskId,
manifest,
pointCloudFileIds,
cadFileIds,
);
const inputJsonPath = await writeInputJson( const inputJsonPath = await writeInputJson(
workDir, workDir,
taskId, taskId,
@@ -350,13 +375,16 @@ export async function runAlgorithmJob(
continue; continue;
} }
try { try {
const sourcePointcloud = pointCloudRecords[0];
const displayName = registeredPointcloudDisplayName(sourcePointcloud?.name, d.file, resultId);
await db.createRegisteredPointcloudFile( await db.createRegisteredPointcloudFile(
taskId, taskId,
d.name ?? d.file, displayName,
absPath, absPath,
resultId, resultId,
sourcePointcloud?.id ?? null,
); );
console.log(`[AlgorithmJobRunner] registered derived pointcloud: ${d.name ?? d.file}`); console.log(`[AlgorithmJobRunner] registered derived pointcloud: ${displayName}`);
} catch (err) { } catch (err) {
console.error('[AlgorithmJobRunner] failed to register derived pointcloud:', err); console.error('[AlgorithmJobRunner] failed to register derived pointcloud:', err);
} }

View File

@@ -6,6 +6,7 @@ import {
vehicleSeries, InsertVehicleSeries, VehicleSeries, vehicleSeries, InsertVehicleSeries, VehicleSeries,
vehicleModels, InsertVehicleModel, VehicleModel, vehicleModels, InsertVehicleModel, VehicleModel,
modelCadFiles, InsertModelCadFile, ModelCadFile, modelCadFiles, InsertModelCadFile, ModelCadFile,
modelPointcloudFiles, InsertModelPointcloudFile, ModelPointcloudFile,
taskCadFiles, InsertTaskCadFile, TaskCadFile, taskCadFiles, InsertTaskCadFile, TaskCadFile,
inspectionTasks, InsertInspectionTask, InspectionTask, inspectionTasks, InsertInspectionTask, InspectionTask,
taskPointcloudFiles, InsertTaskPointcloudFile, TaskPointcloudFile, taskPointcloudFiles, InsertTaskPointcloudFile, TaskPointcloudFile,
@@ -229,6 +230,37 @@ export async function deleteModelCadFile(id: number): Promise<void> {
await db.delete(modelCadFiles).where(eq(modelCadFiles.id, id)); await db.delete(modelCadFiles).where(eq(modelCadFiles.id, id));
} }
// ─── 车型点云文件 ─────────────────────────────────────────────────────────────
export async function createModelPointcloudFile(data: InsertModelPointcloudFile): Promise<ModelPointcloudFile> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(modelPointcloudFiles).values(data);
const [row] = await db.select().from(modelPointcloudFiles).where(eq(modelPointcloudFiles.id, getMysqlInsertId(result)));
return row;
}
export async function listModelPointcloudFiles(modelId: number): Promise<ModelPointcloudFile[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(modelPointcloudFiles)
.where(eq(modelPointcloudFiles.modelId, modelId))
.orderBy(desc(modelPointcloudFiles.createdAt));
}
export async function getModelPointcloudFileById(id: number): Promise<ModelPointcloudFile | undefined> {
const db = await getDb();
if (!db) return undefined;
const [row] = await db.select().from(modelPointcloudFiles).where(eq(modelPointcloudFiles.id, id));
return row;
}
export async function deleteModelPointcloudFile(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(modelPointcloudFiles).where(eq(modelPointcloudFiles.id, id));
}
// ─── 任务 CAD 文件 ───────────────────────────────────────────────────────────── // ─── 任务 CAD 文件 ─────────────────────────────────────────────────────────────
export async function createTaskCadFile(data: InsertTaskCadFile): Promise<TaskCadFile> { export async function createTaskCadFile(data: InsertTaskCadFile): Promise<TaskCadFile> {
@@ -260,6 +292,46 @@ export async function deleteTaskCadFile(id: number): Promise<void> {
await db.delete(taskCadFiles).where(eq(taskCadFiles.id, id)); await db.delete(taskCadFiles).where(eq(taskCadFiles.id, id));
} }
export async function linkTaskCadFilesFromModel(taskId: number, modelCadFileIds: number[]): Promise<TaskCadFile[]> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const task = await getInspectionTaskById(taskId);
if (!task) throw new Error("Task not found");
const existing = await listTaskCadFiles(taskId);
const existingSourceIds = new Set(
existing
.map((row) => row.sourceModelCadFileId)
.filter((id): id is number => typeof id === "number"),
);
const inserted: TaskCadFile[] = [];
for (const id of modelCadFileIds) {
if (existingSourceIds.has(id)) continue;
const source = await getModelCadFileById(id);
if (!source) throw new Error("Model CAD file not found");
if (source.modelId !== task.modelId) {
throw new Error("Cannot link CAD file from another model");
}
const record = await createTaskCadFile({
taskId,
name: source.name,
format: source.format,
fileSize: source.fileSize,
filePath: source.filePath,
source: "model_library",
sourceModelCadFileId: source.id,
} as InsertTaskCadFile);
inserted.push(record);
existingSourceIds.add(id);
}
return inserted;
}
// ─── 检测任务 ───────────────────────────────────────────────────────────────── // ─── 检测任务 ─────────────────────────────────────────────────────────────────
export async function createInspectionTask(data: InsertInspectionTask): Promise<InspectionTask> { export async function createInspectionTask(data: InsertInspectionTask): Promise<InspectionTask> {
@@ -315,6 +387,7 @@ export async function createRegisteredPointcloudFile(
name: string, name: string,
filePath: string, filePath: string,
sourceResultId: number, sourceResultId: number,
sourcePointcloudFileId?: number | null,
): Promise<TaskPointcloudFile> { ): Promise<TaskPointcloudFile> {
const fs = await import('node:fs'); const fs = await import('node:fs');
const fileSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; const fileSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
@@ -325,6 +398,7 @@ export async function createRegisteredPointcloudFile(
fileSize, fileSize,
source: 'registered', source: 'registered',
sourceResultId, sourceResultId,
sourcePointcloudFileId: sourcePointcloudFileId ?? null,
} as InsertTaskPointcloudFile); } as InsertTaskPointcloudFile);
} }
@@ -342,6 +416,48 @@ export async function deleteTaskPointcloudFile(id: number): Promise<void> {
await db.delete(taskPointcloudFiles).where(eq(taskPointcloudFiles.id, id)); await db.delete(taskPointcloudFiles).where(eq(taskPointcloudFiles.id, id));
} }
export async function linkTaskPointcloudFilesFromModel(
taskId: number,
modelPointcloudFileIds: number[],
): Promise<TaskPointcloudFile[]> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const task = await getInspectionTaskById(taskId);
if (!task) throw new Error("Task not found");
const existing = await listTaskPointcloudFiles(taskId);
const existingSourceIds = new Set(
existing
.map((row) => row.sourceModelPointcloudId)
.filter((id): id is number => typeof id === "number"),
);
const inserted: TaskPointcloudFile[] = [];
for (const id of modelPointcloudFileIds) {
if (existingSourceIds.has(id)) continue;
const source = await getModelPointcloudFileById(id);
if (!source) throw new Error("Model pointcloud file not found");
if (source.modelId !== task.modelId) {
throw new Error("Cannot link pointcloud file from another model");
}
const record = await createTaskPointcloudFile({
taskId,
name: source.name,
fileSize: source.fileSize,
filePath: source.filePath,
source: "model_library",
sourceModelPointcloudId: source.id,
} as InsertTaskPointcloudFile);
inserted.push(record);
existingSourceIds.add(id);
}
return inserted;
}
// ─── 任务分析结果 ────────────────────────────────────────────────────────────── // ─── 任务分析结果 ──────────────────────────────────────────────────────────────
export async function createTaskAnalysisResult(data: InsertTaskAnalysisResult): Promise<TaskAnalysisResult> { export async function createTaskAnalysisResult(data: InsertTaskAnalysisResult): Promise<TaskAnalysisResult> {

View File

@@ -24,7 +24,9 @@ ensureDir(TASKS_DIR);
const diskStorage = multer.diskStorage({ const diskStorage = multer.diskStorage({
destination: (req, _file, cb) => { destination: (req, _file, cb) => {
let dir: string; let dir: string;
if (req.params.modelId) { if (req.params.modelId && req.path.includes('/model-pointcloud/')) {
dir = path.join(MODELS_DIR, String(parseInt(req.params.modelId, 10)), 'pointcloud');
} else if (req.params.modelId) {
dir = path.join(MODELS_DIR, String(parseInt(req.params.modelId, 10)), 'cad'); dir = path.join(MODELS_DIR, String(parseInt(req.params.modelId, 10)), 'cad');
} else if (req.params.taskId && req.path.includes('/task-cad/')) { } else if (req.params.taskId && req.path.includes('/task-cad/')) {
dir = path.join(TASKS_DIR, String(parseInt(req.params.taskId, 10)), 'cad'); dir = path.join(TASKS_DIR, String(parseInt(req.params.taskId, 10)), 'cad');
@@ -92,6 +94,35 @@ router.post('/upload/cad/:modelId', diskUpload.single('file'), async (req, res)
}); });
}); });
// ─── 车型点云文件上传 ────────────────────────────────────────────────────────
// POST /api/files/upload/model-pointcloud/:modelId
// multipart/form-data field: file
router.post('/upload/model-pointcloud/:modelId', diskUpload.single('file'), async (req, res) => {
const modelId = parseInt(req.params.modelId, 10);
if (!req.file || isNaN(modelId)) {
return res.status(400).json({ success: false, error: 'Missing file or modelId' });
}
const destPath = req.file.path;
const record = await db.createModelPointcloudFile({
modelId,
name: req.file.originalname,
fileSize: req.file.size,
filePath: destPath,
});
res.json({
success: true,
file: {
id: record.id,
name: record.name,
size: formatFileSize(record.fileSize),
filePath: record.filePath,
},
});
});
// ─── 任务 CAD 文件上传 ──────────────────────────────────────────────────────── // ─── 任务 CAD 文件上传 ────────────────────────────────────────────────────────
// POST /api/files/upload/task-cad/:taskId // POST /api/files/upload/task-cad/:taskId
@@ -173,6 +204,17 @@ router.get('/read/cad/:modelId/:filename', (req, res) => {
res.sendFile(filePath); res.sendFile(filePath);
}); });
// ─── 读取车型点云文件 ─────────────────────────────────────────────────────────
// GET /api/files/read/model-pointcloud/:modelId/:filename
router.get('/read/model-pointcloud/:modelId/:filename', (req, res) => {
const pcDir = path.join(MODELS_DIR, req.params.modelId, 'pointcloud');
const filePath = safeResolve(pcDir, req.params.filename);
if (!filePath) return res.status(403).json({ error: 'Access denied' });
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'File not found' });
res.sendFile(filePath);
});
// ─── 读取点云文件 ───────────────────────────────────────────────────────────── // ─── 读取点云文件 ─────────────────────────────────────────────────────────────
// GET /api/files/read/pointcloud/:taskId/:filename // GET /api/files/read/pointcloud/:taskId/:filename

View File

@@ -14,6 +14,52 @@ import { stepParseCacheDir } from "./stepParse.js";
import { listPlugins, getPlugin } from "./plugins/registry.js"; import { listPlugins, getPlugin } from "./plugins/registry.js";
import { enqueueAlgorithmJob, algorithmJobStats, cancelAlgorithmJob, cancelAlgorithmJobsByTask } from "./algorithmJob.js"; import { enqueueAlgorithmJob, algorithmJobStats, cancelAlgorithmJob, cancelAlgorithmJobsByTask } from "./algorithmJob.js";
async function validateRegistrationRunInput(input: {
taskId: number;
pointCloudFileIds?: number[];
cadFileIds?: number[];
allowDuplicateRegistration?: boolean;
}) {
const pointCloudFileIds = input.pointCloudFileIds ?? [];
const cadFileIds = input.cadFileIds ?? [];
if (pointCloudFileIds.length !== 1 || cadFileIds.length !== 1) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "整车配准必须恰好选择 1 个点云文件和 1 个 CAD 文件。",
});
}
const pointcloudFiles = await db.listTaskPointcloudFiles(input.taskId);
const pointcloud = pointcloudFiles.find((row) => row.id === pointCloudFileIds[0]);
if (!pointcloud) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的点云文件不存在。" });
}
if (pointcloud.source === "registered") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "已配准点云不能再次作为配准输入,请选择原始点云进行配准。",
});
}
const cadFiles = await db.listTaskCadFiles(input.taskId);
const cad = cadFiles.find((row) => row.id === cadFileIds[0]);
if (!cad) {
throw new TRPCError({ code: "NOT_FOUND", message: "选中的 CAD 文件不存在。" });
}
const registeredChildren = pointcloudFiles
.filter((row) => row.source === "registered" && row.sourcePointcloudFileId === pointcloud.id)
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
if (registeredChildren.length > 0 && !input.allowDuplicateRegistration) {
const latest = registeredChildren[0];
throw new TRPCError({
code: "CONFLICT",
message: `点云「${pointcloud.name}」已有配准产物「${latest.name}」,重复配准需要确认。`,
});
}
}
export const appRouter = router({ export const appRouter = router({
system: systemRouter, system: systemRouter,
@@ -147,6 +193,23 @@ export const appRouter = router({
}), }),
}), }),
// ─── 车型点云文件 ─────────────────────────────────────────────────────────
modelPointcloudFile: router({
list: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listModelPointcloudFiles(input.modelId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteModelPointcloudFile(input.id);
return { success: true };
}),
}),
// ─── 任务 CAD 文件 ──────────────────────────────────────────────────────────── // ─── 任务 CAD 文件 ────────────────────────────────────────────────────────────
taskCadFile: router({ taskCadFile: router({
@@ -163,6 +226,27 @@ export const appRouter = router({
return { success: true }; return { success: true };
}), }),
linkFromModel: protectedProcedure
.input(z.object({
taskId: z.number(),
modelCadFileIds: z.array(z.number()).min(1),
}))
.mutation(async ({ input }) => {
try {
const files = await db.linkTaskCadFilesFromModel(input.taskId, input.modelCadFileIds);
return { success: true, files };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
if (message.includes("another model")) {
throw new TRPCError({ code: "BAD_REQUEST", message: "只能关联当前车型的 CAD 库文件" });
}
if (message.includes("not found")) {
throw new TRPCError({ code: "NOT_FOUND", message: "任务或 CAD 库文件不存在" });
}
throw e;
}
}),
// 检查 CAD 文件是否已有三角化采样缓存(.cad_sample_cache/ 目录下存在对应 PLY) // 检查 CAD 文件是否已有三角化采样缓存(.cad_sample_cache/ 目录下存在对应 PLY)
hasSampleCache: protectedProcedure hasSampleCache: protectedProcedure
.input(z.object({ cadFileIds: z.array(z.number()) })) .input(z.object({ cadFileIds: z.array(z.number()) }))
@@ -257,6 +341,27 @@ export const appRouter = router({
await db.deleteTaskPointcloudFile(input.id); await db.deleteTaskPointcloudFile(input.id);
return { success: true }; return { success: true };
}), }),
linkFromModel: protectedProcedure
.input(z.object({
taskId: z.number(),
modelPointcloudFileIds: z.array(z.number()).min(1),
}))
.mutation(async ({ input }) => {
try {
const files = await db.linkTaskPointcloudFilesFromModel(input.taskId, input.modelPointcloudFileIds);
return { success: true, files };
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
if (message.includes("another model")) {
throw new TRPCError({ code: "BAD_REQUEST", message: "只能关联当前车型的点云库文件" });
}
if (message.includes("not found")) {
throw new TRPCError({ code: "NOT_FOUND", message: "任务或点云库文件不存在" });
}
throw e;
}
}),
}), }),
// ─── 任务分析结果 ──────────────────────────────────────────────────────────── // ─── 任务分析结果 ────────────────────────────────────────────────────────────
@@ -467,6 +572,8 @@ export const appRouter = router({
pointCloudFileIds: z.array(z.number()).optional(), pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */ /** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(), cadFileIds: z.array(z.number()).optional(),
/** 配准:允许同一原始点云再次生成配准产物 */
allowDuplicateRegistration: z.boolean().optional(),
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -477,6 +584,9 @@ export const appRouter = router({
message: `未注册的算法插件:${input.pluginId}`, message: `未注册的算法插件:${input.pluginId}`,
}); });
} }
if (manifest.category === "配准") {
await validateRegistrationRunInput(input);
}
try { try {
const record = await db.createTaskAnalysisResult({ const record = await db.createTaskAnalysisResult({
@@ -529,6 +639,8 @@ export const appRouter = router({
pointCloudFileIds: z.array(z.number()).optional(), pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */ /** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(), cadFileIds: z.array(z.number()).optional(),
/** 配准:允许同一原始点云再次生成配准产物 */
allowDuplicateRegistration: z.boolean().optional(),
jobs: z jobs: z
.array( .array(
z.object({ z.object({
@@ -542,6 +654,10 @@ export const appRouter = router({
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const created: { id: number; pluginId: string }[] = []; const created: { id: number; pluginId: string }[] = [];
const hasRegistrationJob = input.jobs.some((job) => getPlugin(job.pluginId)?.category === "配准");
if (hasRegistrationJob) {
await validateRegistrationRunInput(input);
}
for (const job of input.jobs) { for (const job of input.jobs) {
const manifest = getPlugin(job.pluginId); const manifest = getPlugin(job.pluginId);
if (!manifest) continue; if (!manifest) continue;