Files
admin/dashboard/backend/src/routes/ota.ts
T
2026-07-30 15:08:05 +08:00

352 lines
13 KiB
TypeScript

import { Router, type Request, type Response, type Router as RouterType } from 'express';
import { eq, gt, like, and, desc, count } from 'drizzle-orm';
import multer from 'multer';
import { db } from '../config/database.js';
import { otas } from '../schemas/index.js';
import logger from '../config/logger.js';
import { ApiResponse } from '../utils/response.js';
import { authMiddleware } from '../middleware/auth.js';
import { OtaCreateSchema, OtaUpdateSchema } from '../validators/ota.js';
import {
OTA_MODEL_X8,
OTA_MODEL_X9,
OTA_UPLOAD_MODELS,
readUploadContentAndMd5,
saveX9PackageLocal,
uploadX8PackageToS3,
} from '../services/otaStorage.js';
const router: RouterType = Router();
const upload = multer({ storage: multer.memoryStorage() });
// POST /api/ota/upload-package (需登录)
router.post('/api/ota/upload-package', authMiddleware, upload.single('package_file'), async (req: Request, res: Response) => {
try {
const model = (req.body.model || '').trim();
if (!OTA_UPLOAD_MODELS.has(model)) {
res.json(ApiResponse.error(`当前仅支持为 ${OTA_MODEL_X8}${OTA_MODEL_X9} 上传升级包`));
return;
}
if (!req.file) {
res.json(ApiResponse.error('请选择升级包文件'));
return;
}
logger.info(`Uploading OTA package: model=${model}, filename=${req.file.originalname}`);
const { content, md5Hex } = await readUploadContentAndMd5(req.file.buffer);
if (model === OTA_MODEL_X9) {
const { savedName, downloadUrl } = saveX9PackageLocal(content, md5Hex);
res.json(ApiResponse.success({
md5: md5Hex,
filename: savedName,
url: downloadUrl,
}));
return;
}
if (model === OTA_MODEL_X8) {
const { savedName, downloadUrl, s3Key } = await uploadX8PackageToS3(content, md5Hex);
res.json(ApiResponse.success({
md5: md5Hex,
filename: savedName,
url: downloadUrl,
s3_key: s3Key,
}));
return;
}
res.json(ApiResponse.error('不支持的设备型号'));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('未配置') || msg.includes('S3 上传失败')) {
res.json(ApiResponse.error(msg));
return;
}
logger.error(`Error uploading OTA package: ${msg}`);
res.json(ApiResponse.error(`上传失败:${msg}`));
}
});
// GET /api/ota/latest/check (无需登录 — 设备端调用)
router.get('/api/ota/latest/check', async (req: Request, res: Response) => {
try {
const currentVerCode = parseInt(req.query.currentVerCode as string, 10);
const model = req.query.model as string;
const hw = req.query.hw !== undefined ? parseInt(req.query.hw as string, 10) : null;
logger.info(`Checking latest OTA: currentVerCode=${currentVerCode}, model=${model}, hw=${hw}`);
const conditions = [eq(otas.status, 1), gt(otas.verCode, currentVerCode), eq(otas.model, model)];
if (hw !== null && !isNaN(hw)) {
conditions.push(eq(otas.hw, hw));
}
const [latestOta] = await db
.select()
.from(otas)
.where(and(...conditions))
.orderBy(desc(otas.verCode))
.limit(1);
if (!latestOta) {
logger.info(`No available OTA found for model=${model}, currentVerCode=${currentVerCode}`);
res.json(ApiResponse.noData('empty'));
return;
}
logger.info(`Latest OTA found: verCode=${latestOta.verCode}, verName=${latestOta.verName}`);
res.json(ApiResponse.success(otaToDict(latestOta)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error checking latest OTA: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// 以下接口需登录
router.use('/api/ota', authMiddleware);
// GET /api/ota/
router.get('/api/ota/', async (req: Request, res: Response) => {
try {
const skip = parseInt(req.query.skip as string || '0', 10);
const limit = Math.min(parseInt(req.query.limit as string || '100', 10), 1000);
const verCode = req.query.verCode !== undefined ? parseInt(req.query.verCode as string, 10) : null;
const verName = req.query.verName as string | undefined;
const model = req.query.model as string | undefined;
const status = req.query.status !== undefined ? parseInt(req.query.status as string, 10) : null;
const beta = req.query.beta !== undefined ? parseInt(req.query.beta as string, 10) : null;
const conditions = [];
if (verCode !== null && !isNaN(verCode)) conditions.push(eq(otas.verCode, verCode));
if (verName) conditions.push(like(otas.verName, `%${verName}%`));
if (model) conditions.push(like(otas.model, `%${model}%`));
if (status !== null && !isNaN(status)) conditions.push(eq(otas.status, status));
if (beta !== null && !isNaN(beta)) conditions.push(eq(otas.beta, beta));
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [totalResult] = await db
.select({ value: count() })
.from(otas)
.where(whereClause);
const total = totalResult?.value ?? 0;
const rows = await db
.select()
.from(otas)
.where(whereClause)
.orderBy(desc(otas.id))
.offset(skip)
.limit(limit);
logger.info(`Found ${rows.length} OTA records, total=${total}`);
if (!rows.length) {
res.json(ApiResponse.noData('empty'));
return;
}
const items = rows.map(otaToDict);
res.json(ApiResponse.success({ items, total, skip, limit }));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error getting OTA list: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// GET /api/ota/:ota_id
router.get('/api/ota/:ota_id', async (req: Request, res: Response) => {
try {
const otaId = parseInt(req.params.ota_id as string, 10);
logger.info(`Getting OTA: id=${otaId}`);
const [ota] = await db.select().from(otas).where(eq(otas.id, otaId)).limit(1);
if (!ota) {
logger.warn(`OTA not found: id=${otaId}`);
res.json(ApiResponse.noData('empty'));
return;
}
res.json(ApiResponse.success(otaToDict(ota)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error getting OTA: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// POST /api/ota/
router.post('/api/ota/', async (req: Request, res: Response) => {
try {
const parsed = OtaCreateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
res.json(ApiResponse.error(errors));
return;
}
const data = parsed.data;
logger.info(`Creating OTA: verCode=${data.verCode}, verName=${data.verName}, model=${data.model}`);
// 检查版本号是否已存在
const [existing] = await db
.select()
.from(otas)
.where(and(
eq(otas.verCode, data.verCode),
eq(otas.model, data.model!),
eq(otas.beta, data.beta ?? 0),
))
.limit(1);
if (existing) {
logger.warn(`OTA version already exists: verCode=${data.verCode}, model=${data.model}, beta=${data.beta ?? 0}`);
res.json(ApiResponse.error('该版本已存在(相同版本+灰度状态)'));
return;
}
const result = await db.insert(otas).values({
verCode: data.verCode,
verName: data.verName,
url: data.url,
md5: data.md5,
force: data.force ?? 0,
desc: data.desc ?? null,
model: data.model ?? null,
hw: data.hw ?? 0,
target: data.target ?? 0,
beta: data.beta ?? 0,
startTime: data.startTime ? new Date(data.startTime) : null,
endTime: data.endTime ? new Date(data.endTime) : null,
status: data.status ?? 1,
});
const [dbOta] = await db.select().from(otas).where(eq(otas.id, result[0].insertId)).limit(1);
logger.info(`OTA created successfully: id=${dbOta.id}, verCode=${dbOta.verCode}`);
res.json(ApiResponse.success(otaToDict(dbOta)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error creating OTA: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// PUT /api/ota/:ota_id
router.put('/api/ota/:ota_id', async (req: Request, res: Response) => {
try {
const otaId = parseInt(req.params.ota_id as string, 10);
const parsed = OtaUpdateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
res.json(ApiResponse.error(errors));
return;
}
const data = parsed.data;
logger.info(`Updating OTA: id=${otaId}`);
const [dbOta] = await db.select().from(otas).where(eq(otas.id, otaId)).limit(1);
if (!dbOta) {
logger.warn(`OTA not found: id=${otaId}`);
res.json(ApiResponse.noData('empty'));
return;
}
// 如果更新版本号或灰度状态,检查是否冲突
const newVerCode = data.verCode !== undefined && data.verCode !== null ? data.verCode : dbOta.verCode;
const newModel = data.model !== undefined && data.model !== null ? data.model : dbOta.model;
const newBeta = data.beta !== undefined && data.beta !== null ? data.beta : dbOta.beta;
if (newVerCode !== dbOta.verCode || newModel !== dbOta.model || newBeta !== dbOta.beta) {
const [existing] = await db
.select()
.from(otas)
.where(and(eq(otas.verCode, newVerCode), eq(otas.model, newModel!), eq(otas.beta, newBeta)))
.limit(1);
if (existing) {
logger.warn(`OTA version already exists: verCode=${newVerCode}, model=${newModel}, beta=${newBeta}`);
res.json(ApiResponse.error('该版本已存在(相同版本+灰度状态)'));
return;
}
}
// 更新字段
const updateData: Partial<typeof otas.$inferInsert> = {};
if (data.verCode !== undefined && data.verCode !== null) updateData.verCode = data.verCode;
if (data.verName !== undefined && data.verName !== null) updateData.verName = data.verName;
if (data.url !== undefined && data.url !== null) updateData.url = data.url;
if (data.md5 !== undefined && data.md5 !== null) updateData.md5 = data.md5;
if (data.force !== undefined && data.force !== null) updateData.force = data.force;
if (data.desc !== undefined) updateData.desc = data.desc;
if (data.model !== undefined) updateData.model = data.model;
if (data.hw !== undefined && data.hw !== null) updateData.hw = data.hw;
if (data.target !== undefined && data.target !== null) updateData.target = data.target;
if (data.beta !== undefined && data.beta !== null) updateData.beta = data.beta;
if (data.startTime !== undefined && data.startTime !== null) updateData.startTime = new Date(data.startTime);
if (data.endTime !== undefined && data.endTime !== null) updateData.endTime = new Date(data.endTime);
if (data.status !== undefined && data.status !== null) updateData.status = data.status;
if (Object.keys(updateData).length > 0) {
await db.update(otas).set(updateData).where(eq(otas.id, otaId));
}
logger.info(`OTA updated successfully: id=${otaId}`);
const [updated] = await db.select().from(otas).where(eq(otas.id, otaId)).limit(1);
res.json(ApiResponse.success(otaToDict(updated)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error updating OTA: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// DELETE /api/ota/:ota_id
router.delete('/api/ota/:ota_id', async (req: Request, res: Response) => {
try {
const otaId = parseInt(req.params.ota_id as string, 10);
logger.info(`Deleting OTA: id=${otaId}`);
const [dbOta] = await db.select().from(otas).where(eq(otas.id, otaId)).limit(1);
if (!dbOta) {
logger.warn(`OTA not found: id=${otaId}`);
res.json(ApiResponse.noData('empty'));
return;
}
await db.delete(otas).where(eq(otas.id, otaId));
logger.info(`OTA deleted successfully: id=${otaId}`);
res.json(ApiResponse.success(null, 'success'));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error deleting OTA: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
function otaToDict(ota: typeof otas.$inferSelect) {
return {
id: ota.id,
verCode: ota.verCode,
verName: ota.verName,
url: ota.url,
md5: ota.md5,
force: ota.force,
desc: ota.desc,
model: ota.model,
hw: ota.hw,
target: ota.target,
beta: ota.beta,
startTime: ota.startTime ? ota.startTime.toISOString() : null,
endTime: ota.endTime ? ota.endTime.toISOString() : null,
status: ota.status,
create_at: ota.createAt ? ota.createAt.toISOString() : null,
};
}
export default router;