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 = {}; 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;