import { Router, Request, Response } from 'express'; import multer from 'multer'; import sharp from 'sharp'; import path from 'path'; import fs from 'fs'; import { authMiddleware } from '../admin/auth.service'; import { updateSystemConfig } from '../admin/system-config.service'; const router = Router(); // ============ Upload ============ /** * POST /api/admin/upload-fallback-image * Upload a fallback cover image for search results without covers. * Recommended: 320×180 JPEG/PNG (16:9), max 2MB. */ const uploadDir = path.resolve('/app/uploads/fallback'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const fallbackStorage = multer.diskStorage({ destination: (_req, _file, cb) => cb(null, uploadDir), filename: (_req, _file, cb) => { const ext = '.jpg'; cb(null, `fallback_cover_tmp${ext}`); }, }); const upload = multer({ storage: fallbackStorage, limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max fileFilter: (_req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('仅支持图片文件(JPEG/PNG)')); } }, }); router.post('/admin/upload-fallback-image', authMiddleware, upload.single('image'), async (req: Request, res: Response) => { try { if (!req.file) { res.status(400).json({ error: '请选择要上传的图片' }); return; } // 压缩:最大宽度320px,JPEG quality 80 const outPath = path.resolve(uploadDir, 'fallback_cover.jpg'); await sharp(req.file.path) .resize(320, undefined, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 80 }) .toFile(outPath); // 删除原始上传文件(如果路径不同) if (req.file.path !== outPath) { fs.unlink(req.file.path, () => {}); } const url = `/api/uploads/fallback/fallback_cover.jpg`; updateSystemConfig('search_fallback_image', url); const stat = fs.statSync(outPath); res.json({ success: true, url, message: `✅ 兜底图已压缩上传 (${(stat.size / 1024).toFixed(1)}KB)` }); } catch (err: any) { res.status(500).json({ error: err.message || '上传失败' }); } }); /** * POST /api/admin/upload-logo * Upload a site logo image displayed on search page (home link) and homepage. * Recommended: 320×60 or similar wide/banner ratio, JPEG/PNG/WebP, max 2MB. */ const logoUploadDir = path.resolve('/app/uploads/logo'); if (!fs.existsSync(logoUploadDir)) { fs.mkdirSync(logoUploadDir, { recursive: true }); } const logoStorage = multer.diskStorage({ destination: (_req, _file, cb) => cb(null, logoUploadDir), filename: (_req, _file, cb) => { cb(null, `site_logo_tmp.png`); }, }); const logoUpload = multer({ storage: logoStorage, limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max fileFilter: (_req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('仅支持图片文件(JPEG/PNG/WebP)')); } }, }); router.post('/admin/upload-logo', authMiddleware, logoUpload.single('image'), async (req: Request, res: Response) => { try { if (!req.file) { res.status(400).json({ error: '请选择要上传的图片' }); return; } // 压缩:最大宽度640px,PNG格式 const outPath = path.resolve(logoUploadDir, 'site_logo.png'); await sharp(req.file.path) .resize(640, undefined, { fit: 'inside', withoutEnlargement: true }) .png({ compressionLevel: 9 }) .toFile(outPath); if (req.file.path !== outPath) { fs.unlink(req.file.path, () => {}); } const url = `/api/uploads/logo/site_logo.png`; updateSystemConfig('site_logo', url); const stat = fs.statSync(outPath); res.json({ success: true, url, message: `✅ 站点图标已压缩上传 (${(stat.size / 1024).toFixed(1)}KB)` }); } catch (err: any) { res.status(500).json({ error: err.message || '上传失败' }); } }); import { startQrLogin, getQrLoginStatus, cancelQrLogin } from '../cloud/qr-login.service'; // ===== 夸克扫码登录 (不需要 auth,用户未登录时也需要能用) ===== export default router;