feat: initial commit - massive-cloud point cloud processing system

This commit is contained in:
2026-05-27 09:51:42 +00:00
commit 16811100d1
391 changed files with 78338 additions and 0 deletions

544
server/routers.ts Normal file
View File

@@ -0,0 +1,544 @@
import { COOKIE_NAME } from "@shared/const";
import { getSessionCookieOptions } from "./_core/cookies";
import { systemRouter } from "./_core/systemRouter";
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import * as db from "./db";
import { enqueueAssemblyInspectionJob } from "./assemblyInspectionJob.js";
import { enqueueDeviationHeatmapJob } from "./deviationHeatmapJob.js";
import { enqueueSmoothHeatmapJob } from "./smoothHeatmapJob.js";
import { stepParseCacheDir } from "./stepParse.js";
import { listPlugins, getPlugin } from "./plugins/registry.js";
import { enqueueAlgorithmJob, algorithmJobStats } from "./algorithmJob.js";
export const appRouter = router({
system: systemRouter,
auth: router({
me: publicProcedure.query(opts => opts.ctx.user),
logout: publicProcedure.mutation(({ ctx }) => {
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
return { success: true } as const;
}),
}),
// ─── 车系 ──────────────────────────────────────────────────────────────────
vehicleSeries: router({
list: protectedProcedure.query(async () => {
return db.listVehicleSeries();
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
return db.createVehicleSeries({
name: input.name,
description: input.description ?? null,
});
}),
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().min(1).max(100).optional(),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateVehicleSeries(id, data);
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteVehicleSeries(input.id);
return { success: true };
}),
}),
// ─── 车型 ──────────────────────────────────────────────────────────────────
vehicleModel: router({
listBySeries: protectedProcedure
.input(z.object({ seriesId: z.number() }))
.query(async ({ input }) => {
return db.listVehicleModelsBySeries(input.seriesId);
}),
get: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return db.getVehicleModelById(input.id);
}),
create: protectedProcedure
.input(z.object({
seriesId: z.number(),
name: z.string().min(1).max(100),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
return db.createVehicleModel({
seriesId: input.seriesId,
name: input.name,
description: input.description ?? null,
});
}),
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().min(1).max(100).optional(),
description: z.string().optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateVehicleModel(id, data);
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteVehicleModel(input.id);
return { success: true };
}),
}),
// ─── 车型 CAD 文件 ─────────────────────────────────────────────────────────
cadFile: router({
list: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listModelCadFiles(input.modelId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteModelCadFile(input.id);
return { success: true };
}),
/** STEP 解析后的 manifest(零件列表与 STL 相对路径) */
assemblyManifest: protectedProcedure
.input(z.object({ modelId: z.number(), cadFileId: z.number() }))
.query(async ({ input }) => {
const row = await db.getModelCadFileById(input.cadFileId);
if (!row || row.modelId !== input.modelId) {
throw new TRPCError({ code: "NOT_FOUND", message: "CAD 文件不存在" });
}
const manPath = path.join(stepParseCacheDir(input.modelId, input.cadFileId), "manifest.json");
if (!fs.existsSync(manPath)) {
return { ready: false as const, manifest: null as null };
}
const manifest = JSON.parse(fs.readFileSync(manPath, "utf-8")) as Record<string, unknown>;
return { ready: true as const, manifest };
}),
}),
// ─── 任务 CAD 文件 ────────────────────────────────────────────────────────────
taskCadFile: router({
list: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
return db.listTaskCadFiles(input.taskId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteTaskCadFile(input.id);
return { success: true };
}),
}),
// ─── 检测任务 ──────────────────────────────────────────────────────────────
inspectionTask: router({
listByModel: protectedProcedure
.input(z.object({ modelId: z.number() }))
.query(async ({ input }) => {
return db.listInspectionTasksByModel(input.modelId);
}),
get: protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return db.getInspectionTaskById(input.id);
}),
create: protectedProcedure
.input(z.object({
modelId: z.number(),
name: z.string().min(1).max(255),
inspector: z.string().max(100).optional(),
notes: z.string().optional(),
}))
.mutation(async ({ input }) => {
return db.createInspectionTask({
modelId: input.modelId,
name: input.name,
inspector: input.inspector ?? null,
notes: input.notes ?? null,
status: "pending",
});
}),
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().max(255).optional(),
inspector: z.string().max(100).optional(),
notes: z.string().optional(),
status: z.enum(["pending", "processing", "completed", "error"]).optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateInspectionTask(id, data);
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteInspectionTask(input.id);
return { success: true };
}),
}),
// ─── 任务点云文件 ────────────────────────────────────────────────────────────
pointcloudFile: router({
list: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
return db.listTaskPointcloudFiles(input.taskId);
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteTaskPointcloudFile(input.id);
return { success: true };
}),
}),
// ─── 任务分析结果 ────────────────────────────────────────────────────────────
analysisResult: router({
list: protectedProcedure
.input(z.object({ taskId: z.number() }))
.query(async ({ input }) => {
return db.listTaskAnalysisResults(input.taskId);
}),
runAnalysis: protectedProcedure
.input(z.object({
taskId: z.number(),
type: z.enum([
"error_analysis",
"measurement",
"compression",
"smooth_heatmap",
"assembly_inspection",
"obj_deviation",
]),
/** 点云采样预算:平滑热力默认 40 万,偏差热力(GPU)可提高到千万级 */
totalPoints: z.number().min(50_000).max(20_000_000).optional(),
/** 装配检测:指定 STEP 的 model_cad_files.id;不填则取该车型下第一个 STEP */
cadFileId: z.number().optional(),
/** 装配检测:点云采样点数,默认 10 万 */
samplePoints: z.number().min(10_000).max(500_000).optional(),
/** 装配检测:合格阈值(mm) */
toleranceMm: z.number().positive().max(100).optional(),
/** 装配检测:是否生成每零件顶点热力缓存 */
includeVertexHeatmap: z.boolean().optional(),
}))
.mutation(async ({ input }) => {
if (input.type === "assembly_inspection") {
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "assembly_inspection",
status: "processing",
});
enqueueAssemblyInspectionJob(record.id, input.taskId, {
cadFileId: input.cadFileId,
samplePoints: input.samplePoints ?? 100_000,
toleranceMm: input.toleranceMm ?? 0.3,
includeVertexHeatmap: input.includeVertexHeatmap ?? false,
});
return { success: true as const, id: record.id, kind: "assembly_inspection" as const };
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
(msg.includes("Incorrect") && msg.includes("column 'type'"))
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库缺少 assembly_inspection 类型枚举,请应用迁移 drizzle/0005_task_analysis_assembly_inspection.sql 后重启。",
});
}
throw e;
}
}
if (input.type === "smooth_heatmap") {
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "smooth_heatmap",
status: "processing",
});
enqueueSmoothHeatmapJob(record.id, input.taskId, input.totalPoints ?? 400_000);
return { success: true as const, id: record.id, kind: "smooth_heatmap" as const };
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
msg.includes("Incorrect") && msg.includes("column 'type'")
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库 task_analysis_results.type 枚举缺少 smooth_heatmap。请重启服务以应用迁移 drizzle/0004_task_analysis_smooth_heatmap_enum.sql,或在 MySQL 执行 scripts/alter-task-analysis-type-enum.sql。",
});
}
throw e;
}
}
if (input.type === "obj_deviation") {
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "obj_deviation",
status: "processing",
});
enqueueDeviationHeatmapJob(record.id, input.taskId, input.totalPoints ?? 5_000_000);
return { success: true as const, id: record.id, kind: "obj_deviation" as const };
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
(msg.includes("Incorrect") && msg.includes("column 'type'"))
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库 task_analysis_results.type 枚举缺少 obj_deviation。请应用迁移 drizzle/0006_task_analysis_obj_deviation.sql 后重启。",
});
}
throw e;
}
}
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: input.type,
status: "processing",
});
await new Promise(resolve => setTimeout(resolve, 1500));
const errorStats = {
min: 0.001,
max: 2.543,
mean: 0.856,
stdDev: 0.423,
totalPoints: 1000000,
withinTolerance: 892345,
outOfTolerance: 107655,
};
const measurements = [
{ name: "车体长度", value: 25700.4, unit: "mm", tolerance: 5, status: "pass" },
{ name: "车体宽度", value: 3380.1, unit: "mm", tolerance: 3, status: "pass" },
{ name: "车体高度", value: 3700.2, unit: "mm", tolerance: 3, status: "pass" },
{ name: "轮对距D1", value: 1435.8, unit: "mm", tolerance: 1, status: "warning" },
{ name: "转向架轴距", value: 2500.3, unit: "mm", tolerance: 2, status: "pass" },
];
await db.updateTaskAnalysisResult(record.id, {
status: "completed",
errorStats,
measurements,
});
await db.updateInspectionTask(input.taskId, {
status: "completed",
completedAt: new Date(),
});
return { success: true, id: record.id, errorStats, measurements };
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
await db.deleteTaskAnalysisResult(input.id);
return { success: true };
}),
}),
// ─── 通用算法插件(Route B) ──────────────────────────────────────────────
//
// 不为每类算法做独立界面:前端按 manifest.inputs 动态渲染表单 + 多选并发执行;
// 后端按 manifest.runtime 启动子进程或返回 mock fixture;
// 结果统一落到 task_analysis_results.type = 'plugin',pluginId 区分。
algorithm: router({
/** 列出所有已注册插件(前端 AlgorithmRunPanel 用) */
list: protectedProcedure.query(() => {
return {
plugins: listPlugins().map((m) => ({
id: m.id,
name: m.name,
shortName: m.shortName,
description: m.description,
version: m.version,
schemaVersion: m.schemaVersion,
category: m.category,
inputs: m.inputs ?? [],
requiresCad: m.requiresCad ?? false,
requiresPointCloud: m.requiresPointCloud ?? true,
runtimeKind: m.runtime.kind,
})),
scheduler: algorithmJobStats(),
};
}),
/** 单插件执行:创建一条 'plugin' 类型的分析结果记录,丢进并发池 */
run: protectedProcedure
.input(
z.object({
taskId: z.number(),
pluginId: z.string().min(1),
params: z.record(z.string(), z.unknown()).optional(),
/** 可选:只处理选中的点云文件 ID(空/缺省 = 任务下全部文件) */
pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(),
}),
)
.mutation(async ({ input }) => {
const manifest = getPlugin(input.pluginId);
if (!manifest) {
throw new TRPCError({
code: "NOT_FOUND",
message: `未注册的算法插件:${input.pluginId}`,
});
}
try {
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "plugin",
pluginId: manifest.id,
schemaVersion: manifest.schemaVersion,
status: "processing",
});
void enqueueAlgorithmJob(
record.id,
input.taskId,
manifest.id,
input.params ?? {},
input.pointCloudFileIds,
input.cadFileIds,
);
return {
success: true as const,
id: record.id,
pluginId: manifest.id,
schemaVersion: manifest.schemaVersion,
};
} catch (e: unknown) {
const err = e as { errno?: number; message?: string };
const msg = err.message ?? "";
if (
err.errno === 1265 ||
err.errno === 1366 ||
msg.includes("Data truncated") ||
(msg.includes("Incorrect") && msg.includes("column 'type'")) ||
msg.includes("Unknown column 'pluginId'")
) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"数据库缺少 type='plugin' 枚举或 pluginId/schemaVersion 列。请应用迁移 drizzle/0007_task_analysis_plugin_columns.sql 后重启。",
});
}
throw e;
}
}),
/** 多插件并发执行:每个插件都通过相同的 run 流程入池 */
runBatch: protectedProcedure
.input(
z.object({
taskId: z.number(),
/** 可选:只处理选中的点云文件 ID(空/缺省 = 任务下全部文件) */
pointCloudFileIds: z.array(z.number()).optional(),
/** 可选:任务级 CAD 文件 ID(空/缺省 = 不传 CAD) */
cadFileIds: z.array(z.number()).optional(),
jobs: z
.array(
z.object({
pluginId: z.string().min(1),
params: z.record(z.string(), z.unknown()).optional(),
}),
)
.min(1)
.max(16),
}),
)
.mutation(async ({ input }) => {
const created: { id: number; pluginId: string }[] = [];
for (const job of input.jobs) {
const manifest = getPlugin(job.pluginId);
if (!manifest) continue;
const record = await db.createTaskAnalysisResult({
taskId: input.taskId,
type: "plugin",
pluginId: manifest.id,
schemaVersion: manifest.schemaVersion,
status: "processing",
});
void enqueueAlgorithmJob(
record.id,
input.taskId,
manifest.id,
job.params ?? {},
input.pointCloudFileIds,
input.cadFileIds,
);
created.push({ id: record.id, pluginId: manifest.id });
}
return { success: true as const, jobs: created, scheduler: algorithmJobStats() };
}),
}),
});
export type AppRouter = typeof appRouter;