Files
massive-cloud/server/_core/index.ts
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

169 lines
5.2 KiB
TypeScript
Raw 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 "dotenv/config";
import express from "express";
import { createServer } from "http";
import net from "net";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import mysql from "mysql2/promise";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { registerOAuthRoutes } from "./oauth";
import { appRouter } from "../routers";
import { createContext } from "./context";
import { serveStatic, setupVite } from "./vite";
import { fileRouter } from "../fileRoutes";
const SERVER_ROOT = path.dirname(fileURLToPath(import.meta.url));
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Run SQL migrations using a raw mysql2 connection.
* Drizzle's execute() requires tagged template sql``; using mysql2 directly
* is safer for running arbitrary migration statements.
*/
async function runMigrations() {
if (!process.env.DATABASE_URL) return;
// Wait for MySQL to be ready (up to 40 s)
let conn: mysql.Connection | null = null;
for (let i = 1; i <= 20; i++) {
try {
conn = await mysql.createConnection(process.env.DATABASE_URL);
console.log("[Migration] Database ready.");
break;
} catch {
console.log(`[Migration] Waiting for database... (${i}/20)`);
await sleep(2000);
}
}
if (!conn) {
console.warn("[Migration] Database not available after 40 s, skipping migrations.");
return;
}
try {
const migrationsDir = path.resolve(SERVER_ROOT, "../../drizzle");
if (!fs.existsSync(migrationsDir)) return;
const sqlFiles = fs.readdirSync(migrationsDir)
.filter(f => f.endsWith(".sql"))
.sort();
// Tracking table
await conn.execute(`
CREATE TABLE IF NOT EXISTS \`_migrations\` (
\`name\` VARCHAR(255) PRIMARY KEY,
\`appliedAt\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
for (const file of sqlFiles) {
const [rows] = await conn.execute(
`SELECT name FROM \`_migrations\` WHERE name = ?`, [file]
) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
if (rows.length > 0) continue; // already applied
const sqlText = fs.readFileSync(path.join(migrationsDir, file), "utf8");
// Split on ";". Do NOT drop statements that start with "--": each CREATE block
// begins with comment lines, and filtering those skipped all tables (bug).
const statements = sqlText
.split(";")
.map(s => s.trim())
.filter(s => s.length > 0);
for (const stmt of statements) {
try {
await conn.execute(stmt);
} catch (e: any) {
if (!e.message?.includes("already exists")) {
console.error(`[Migration] Error in ${file}:`, e.message);
}
}
}
await conn.execute(
`INSERT IGNORE INTO \`_migrations\` (name) VALUES (?)`, [file]
);
console.log(`[Migration] Applied: ${file}`);
}
} finally {
await conn.end();
}
}
function isPortAvailable(port: number): Promise<boolean> {
return new Promise(resolve => {
const server = net.createServer();
server.listen(port, () => {
server.close(() => resolve(true));
});
server.on("error", () => resolve(false));
});
}
async function findAvailablePort(startPort: number = 3000): Promise<number> {
for (let port = startPort; port < startPort + 20; port++) {
if (await isPortAvailable(port)) {
return port;
}
}
throw new Error(`No available port found starting from ${startPort}`);
}
async function startServer() {
try {
await runMigrations();
} catch (e) {
console.error("[Migration] Unexpected error, server will still start:", e);
}
const app = express();
const server = createServer(app);
// Configure body parser with larger size limit for file uploads
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));
// 文件上传路由单独设置超长超时(50 GB 点云,按 100 MB/s 约需 8 分钟,留 60 分钟余量)
server.timeout = 0; // 禁用 Node http.Server 全局超时
server.keepAliveTimeout = 0; // 长连接不超时
server.headersTimeout = 0; // 读请求头不超时
// OAuth callback under /api/oauth/callback
registerOAuthRoutes(app);
// 禁止代理(NPS 等)缓存 API 响应
app.use('/api', (_req, res, next) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
res.setHeader('Pragma', 'no-cache');
next();
});
// File API for local file browsing
app.use('/api/files', fileRouter);
// tRPC API
app.use(
"/api/trpc",
createExpressMiddleware({
router: appRouter,
createContext,
})
);
// development mode uses Vite, production mode uses static files
if (process.env.NODE_ENV === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
const preferredPort = parseInt(process.env.PORT || "3000");
const port = await findAvailablePort(preferredPort);
if (port !== preferredPort) {
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
}
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}/`);
});
}
startServer().catch(console.error);