Files
lipan 29f1d45d66 feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
2026-06-25 08:22:20 +00:00

410 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { eq, desc, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import type { ResultSetHeader } from "mysql2";
import {
users, InsertUser,
vehicleSeries, InsertVehicleSeries, VehicleSeries,
vehicleModels, InsertVehicleModel, VehicleModel,
modelCadFiles, InsertModelCadFile, ModelCadFile,
taskCadFiles, InsertTaskCadFile, TaskCadFile,
inspectionTasks, InsertInspectionTask, InspectionTask,
taskPointcloudFiles, InsertTaskPointcloudFile, TaskPointcloudFile,
taskAnalysisResults, InsertTaskAnalysisResult, TaskAnalysisResult,
} from "../drizzle/schema";
import { ENV } from './_core/env';
let _db: ReturnType<typeof drizzle> | null = null;
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
/** drizzle mysql2 insert resolves to mysql2 ResultSetHeader with insertId (not result[0].insertId). */
function getMysqlInsertId(result: unknown): number {
const r = result as ResultSetHeader & { 0?: ResultSetHeader };
if (typeof r.insertId === "number" && r.insertId > 0) return r.insertId;
if (r[0] && typeof r[0].insertId === "number") return r[0].insertId;
throw new Error("insertId missing from insert result");
}
/**
* Clears the cached pool so the next getDb() creates a new connection.
* Call after ECONNREFUSED / PROTOCOL_CONNECTION_LOST so retries can recover.
*/
export function invalidateDb() {
_db = null;
}
/**
* Returns the drizzle DB instance, creating it if needed.
* Retries the initial TCP probe while MySQL is still starting (common in Docker).
*/
export async function getDb() {
if (_db) return _db;
if (!process.env.DATABASE_URL) return null;
const maxAttempts = 40;
const delayMs = 500;
// One drizzle instance / pool; retry execute only (avoids leaking a pool per attempt).
const instance = drizzle(process.env.DATABASE_URL);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await instance.execute(sql`SELECT 1`);
_db = instance;
if (attempt > 1) {
console.log(`[Database] Connected after ${attempt} attempt(s).`);
}
return _db;
} catch (error: any) {
const msg = error?.message ?? String(error);
const retriable =
error?.code === "ECONNREFUSED" ||
error?.code === "ETIMEDOUT" ||
error?.code === "ENOTFOUND" ||
msg.includes("connect");
if (attempt < maxAttempts && retriable) {
if (attempt === 1 || attempt % 5 === 0) {
console.warn(`[Database] Waiting for MySQL... (${attempt}/${maxAttempts}) ${msg}`);
}
await sleep(delayMs);
continue;
}
console.error("[Database] Giving up:", msg);
_db = null;
return null;
}
}
return null;
}
// ─── 用户 ─────────────────────────────────────────────────────────────────────
export async function upsertUser(user: InsertUser): Promise<void> {
if (!user.openId) throw new Error("User openId is required for upsert");
const db = await getDb();
if (!db) {
console.warn("[Database] Cannot upsert user: database not available");
return;
}
try {
const values: InsertUser = { openId: user.openId };
const updateSet: Record<string, unknown> = {};
const textFields = ["name", "email", "loginMethod"] as const;
type TextField = (typeof textFields)[number];
const assignNullable = (field: TextField) => {
const value = user[field];
if (value === undefined) return;
const normalized = value ?? null;
values[field] = normalized;
updateSet[field] = normalized;
};
textFields.forEach(assignNullable);
if (user.lastSignedIn !== undefined) {
values.lastSignedIn = user.lastSignedIn;
updateSet.lastSignedIn = user.lastSignedIn;
}
if (user.role !== undefined) {
values.role = user.role;
updateSet.role = user.role;
} else if (user.openId === ENV.ownerOpenId) {
values.role = 'admin';
updateSet.role = 'admin';
}
if (!values.lastSignedIn) values.lastSignedIn = new Date();
if (Object.keys(updateSet).length === 0) updateSet.lastSignedIn = new Date();
await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet });
} catch (error) {
console.error("[Database] Failed to upsert user:", error);
throw error;
}
}
export async function getUserByOpenId(openId: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
return result[0];
}
// ─── 车系 ─────────────────────────────────────────────────────────────────────
export async function createVehicleSeries(data: InsertVehicleSeries): Promise<VehicleSeries> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(vehicleSeries).values(data);
const [row] = await db.select().from(vehicleSeries).where(eq(vehicleSeries.id, getMysqlInsertId(result)));
return row;
}
export async function listVehicleSeries(): Promise<VehicleSeries[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(vehicleSeries).orderBy(desc(vehicleSeries.createdAt));
}
export async function updateVehicleSeries(id: number, data: Partial<InsertVehicleSeries>): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(vehicleSeries).set(data).where(eq(vehicleSeries.id, id));
}
export async function deleteVehicleSeries(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(vehicleSeries).where(eq(vehicleSeries.id, id));
}
// ─── 车型 ─────────────────────────────────────────────────────────────────────
export async function createVehicleModel(data: InsertVehicleModel): Promise<VehicleModel> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(vehicleModels).values(data);
const [row] = await db.select().from(vehicleModels).where(eq(vehicleModels.id, getMysqlInsertId(result)));
return row;
}
export async function listVehicleModelsBySeries(seriesId: number): Promise<VehicleModel[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(vehicleModels)
.where(eq(vehicleModels.seriesId, seriesId))
.orderBy(desc(vehicleModels.createdAt));
}
export async function getVehicleModelById(id: number): Promise<VehicleModel | undefined> {
const db = await getDb();
if (!db) return undefined;
const [row] = await db.select().from(vehicleModels).where(eq(vehicleModels.id, id));
return row;
}
export async function updateVehicleModel(id: number, data: Partial<InsertVehicleModel>): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(vehicleModels).set(data).where(eq(vehicleModels.id, id));
}
export async function deleteVehicleModel(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(vehicleModels).where(eq(vehicleModels.id, id));
}
// ─── 车型 CAD 文件 ─────────────────────────────────────────────────────────────
export async function createModelCadFile(data: InsertModelCadFile): Promise<ModelCadFile> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(modelCadFiles).values(data);
const [row] = await db.select().from(modelCadFiles).where(eq(modelCadFiles.id, getMysqlInsertId(result)));
return row;
}
export async function listModelCadFiles(modelId: number): Promise<ModelCadFile[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(modelCadFiles)
.where(eq(modelCadFiles.modelId, modelId))
.orderBy(desc(modelCadFiles.createdAt));
}
export async function getModelCadFileById(id: number): Promise<ModelCadFile | undefined> {
const db = await getDb();
if (!db) return undefined;
const [row] = await db.select().from(modelCadFiles).where(eq(modelCadFiles.id, id));
return row;
}
export async function deleteModelCadFile(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(modelCadFiles).where(eq(modelCadFiles.id, id));
}
// ─── 任务 CAD 文件 ─────────────────────────────────────────────────────────────
export async function createTaskCadFile(data: InsertTaskCadFile): Promise<TaskCadFile> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(taskCadFiles).values(data);
const [row] = await db.select().from(taskCadFiles).where(eq(taskCadFiles.id, getMysqlInsertId(result)));
return row;
}
export async function listTaskCadFiles(taskId: number): Promise<TaskCadFile[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(taskCadFiles)
.where(eq(taskCadFiles.taskId, taskId))
.orderBy(taskCadFiles.createdAt);
}
export async function getTaskCadFileById(id: number): Promise<TaskCadFile | undefined> {
const db = await getDb();
if (!db) return undefined;
const [row] = await db.select().from(taskCadFiles).where(eq(taskCadFiles.id, id));
return row;
}
export async function deleteTaskCadFile(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(taskCadFiles).where(eq(taskCadFiles.id, id));
}
// ─── 检测任务 ─────────────────────────────────────────────────────────────────
export async function createInspectionTask(data: InsertInspectionTask): Promise<InspectionTask> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(inspectionTasks).values(data);
const [row] = await db.select().from(inspectionTasks).where(eq(inspectionTasks.id, getMysqlInsertId(result)));
return row;
}
export async function listInspectionTasksByModel(modelId: number): Promise<InspectionTask[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(inspectionTasks)
.where(eq(inspectionTasks.modelId, modelId))
.orderBy(desc(inspectionTasks.createdAt));
}
export async function getInspectionTaskById(id: number): Promise<InspectionTask | undefined> {
const db = await getDb();
if (!db) return undefined;
const [row] = await db.select().from(inspectionTasks).where(eq(inspectionTasks.id, id));
return row;
}
export async function updateInspectionTask(id: number, data: Partial<InsertInspectionTask>): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(inspectionTasks).set(data).where(eq(inspectionTasks.id, id));
}
export async function deleteInspectionTask(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(inspectionTasks).where(eq(inspectionTasks.id, id));
}
// ─── 任务点云文件 ──────────────────────────────────────────────────────────────
export async function createTaskPointcloudFile(data: InsertTaskPointcloudFile): Promise<TaskPointcloudFile> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(taskPointcloudFiles).values(data);
const [row] = await db.select().from(taskPointcloudFiles).where(eq(taskPointcloudFiles.id, getMysqlInsertId(result)));
return row;
}
/** 把配准算法产出的点云注册为任务点云(source='registered')。
* filePath 必须是绝对路径,文件必须已存在于磁盘。
*/
export async function createRegisteredPointcloudFile(
taskId: number,
name: string,
filePath: string,
sourceResultId: number,
): Promise<TaskPointcloudFile> {
const fs = await import('node:fs');
const fileSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
return createTaskPointcloudFile({
taskId,
name,
filePath,
fileSize,
source: 'registered',
sourceResultId,
} as InsertTaskPointcloudFile);
}
export async function listTaskPointcloudFiles(taskId: number): Promise<TaskPointcloudFile[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(taskPointcloudFiles)
.where(eq(taskPointcloudFiles.taskId, taskId))
.orderBy(taskPointcloudFiles.createdAt);
}
export async function deleteTaskPointcloudFile(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(taskPointcloudFiles).where(eq(taskPointcloudFiles.id, id));
}
// ─── 任务分析结果 ──────────────────────────────────────────────────────────────
export async function createTaskAnalysisResult(data: InsertTaskAnalysisResult): Promise<TaskAnalysisResult> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(taskAnalysisResults).values(data);
const [row] = await db.select().from(taskAnalysisResults).where(eq(taskAnalysisResults.id, getMysqlInsertId(result)));
return row;
}
export async function listTaskAnalysisResults(taskId: number): Promise<TaskAnalysisResult[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(taskAnalysisResults)
.where(eq(taskAnalysisResults.taskId, taskId))
.orderBy(desc(taskAnalysisResults.createdAt));
}
export async function getTaskAnalysisResultById(id: number): Promise<TaskAnalysisResult | undefined> {
const db = await getDb();
if (!db) return undefined;
const [row] = await db.select().from(taskAnalysisResults).where(eq(taskAnalysisResults.id, id));
return row;
}
export async function updateTaskAnalysisResult(id: number, data: Partial<InsertTaskAnalysisResult>): Promise<void> {
const db = await getDb();
if (!db) return;
await db.update(taskAnalysisResults).set(data).where(eq(taskAnalysisResults.id, id));
}
/**
* 非破坏性局部更新 task_analysis_results.resultData。
* 与 updateTaskAnalysisResult(resultData) 不同——后者会把整个 JSON 列覆写,
* 适合「最终结果」;本函数把 patch 合并到现有 JSON 上,适合
* 子进程频繁上报进度的场景(例如算法插件 progress / stage)。
*
* 实现策略:先读取一次再 set 写回;并发更新时按「最后写入者获胜」处理,
* 对进度上报来说足够安全(覆盖丢失最多 1~2 个心跳)。
*/
export async function patchTaskAnalysisResultData(
id: number,
patch: Record<string, unknown>,
): Promise<void> {
const db = await getDb();
if (!db) return;
const existing = await getTaskAnalysisResultById(id);
if (!existing) return;
const prev = (existing.resultData as Record<string, unknown> | null) ?? {};
const merged: Record<string, unknown> = { ...prev };
for (const [k, v] of Object.entries(patch)) {
if (v === undefined) continue;
merged[k] = v;
}
await db
.update(taskAnalysisResults)
.set({ resultData: merged })
.where(eq(taskAnalysisResults.id, id));
}
export async function deleteTaskAnalysisResult(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(taskAnalysisResults).where(eq(taskAnalysisResults.id, id));
}