官网开发 0804

This commit is contained in:
eafonyang
2026-08-04 19:18:35 +08:00
parent f37b6451ef
commit 816281bf2d
31 changed files with 1177 additions and 543 deletions
+75 -11
View File
@@ -17,16 +17,33 @@ const UPLOAD_DIR = path.resolve(process.cwd(), 'uploads');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
const storage = multer.diskStorage({
destination: (_req, _file, cb) => {
const now = new Date();
const subDir = path.join(UPLOAD_DIR, `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}`);
fs.mkdirSync(subDir, { recursive: true });
cb(null, subDir);
destination: (req, _file, cb) => {
const body = req.body as Record<string, string>;
const customDir = body.dir?.trim();
if (customDir) {
const dirPath = path.join(UPLOAD_DIR, customDir);
fs.mkdirSync(dirPath, { recursive: true });
cb(null, dirPath);
} else {
const now = new Date();
const subDir = path.join(UPLOAD_DIR, `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}`);
fs.mkdirSync(subDir, { recursive: true });
cb(null, subDir);
}
},
filename: (_req, file, cb) => {
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const name = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`;
cb(null, name);
const body = req.body as Record<string, string>;
// auto_name 默认 true(不传或传 "true" 均视为自动生成)
const autoName = body.auto_name !== 'false';
if (autoName) {
cb(null, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`);
} else {
const customName = (body.custom_filename || 'file').trim();
// 去除用户可能误输入的扩展名
const cleanName = path.basename(customName, path.extname(customName));
cb(null, `${cleanName}${ext}`);
}
},
});
@@ -41,6 +58,22 @@ function getFileType(mimetype: string): string {
return 'file';
}
/** 彻底删除素材:DB 记录 + 标签关联 + 磁盘文件 */
async function deleteMediaCompletely(row: typeof wwwMedia.$inferSelect) {
await db.delete(wwwMediaTagMap).where(eq(wwwMediaTagMap.mediaId, row.id));
await db.delete(wwwMedia).where(eq(wwwMedia.id, row.id));
// 删除磁盘文件:file_path 形如 /uploads/xxx,拼接到 UPLOAD_DIR 并做路径安全校验
const rel = row.filePath.replace(/^\/uploads\//, '');
const abs = path.normalize(path.join(UPLOAD_DIR, rel));
if (abs.startsWith(UPLOAD_DIR + path.sep) && fs.existsSync(abs)) {
try {
fs.unlinkSync(abs);
} catch (err) {
logger.error(`[www/media] 删除磁盘文件失败 ${abs}: ${err instanceof Error ? err.message : err}`);
}
}
}
// ===== 6. 素材库 =====
// GET /api/www/media?category=product&tag=xxx&keyword=xxx&file_type=image
@@ -117,8 +150,10 @@ router.post('/api/www/media/upload', upload.single('file'), async (req: Request,
const category = (req.body.category as string) || 'other';
const relativePath = `/uploads/${path.relative(UPLOAD_DIR, file.path)}`;
// 显示名(不含扩展名),DB 中不存扩展名
const displayName = path.parse(file.filename).name;
const result = await db.insert(wwwMedia).values({
filename: file.originalname,
filename: displayName,
filePath: relativePath,
fileType,
mimeType: file.mimetype,
@@ -184,6 +219,35 @@ router.post('/api/www/media/upload-batch', upload.array('files', 20), async (req
}
});
// DELETE /api/www/media/batch(批量物理删除,跳过被引用的素材;放在 :id 前面避免路由冲突)
router.delete('/api/www/media/batch', async (req: Request, res: Response) => {
try {
const ids = req.body.ids as number[];
if (!Array.isArray(ids) || ids.length === 0) {
res.json(ApiResponse.error('ids 为必填项'));
return;
}
const deleted: number[] = [];
const skipped: number[] = [];
for (const id of ids) {
const [existing] = await db.select().from(wwwMedia).where(eq(wwwMedia.id, id));
if (!existing || existing.refCount > 0) {
skipped.push(id);
continue;
}
await deleteMediaCompletely(existing);
deleted.push(id);
}
res.json(ApiResponse.success(
{ deleted, skipped },
`成功删除 ${deleted.length} 个素材${skipped.length ? `,跳过 ${skipped.length} 个(不存在或被引用)` : ''}`
));
} catch (e: unknown) {
logger.error(`[www/media/batch] DELETE error: ${e instanceof Error ? e.message : e}`);
res.json(ApiResponse.error('批量删除失败'));
}
});
// GET /api/www/media/tags(放在 :id 前面避免路由冲突)
router.get('/api/www/media/tags', async (_req: Request, res: Response) => {
try {
@@ -319,7 +383,7 @@ router.put('/api/www/media/:id', async (req: Request, res: Response) => {
}
});
// DELETE /api/www/media/:id软删除
// DELETE /api/www/media/:id物理删除:DB 记录 + 磁盘文件
router.delete('/api/www/media/:id', async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
@@ -334,7 +398,7 @@ router.delete('/api/www/media/:id', async (req: Request, res: Response) => {
return;
}
await db.update(wwwMedia).set({ deletedAt: new Date() }).where(eq(wwwMedia.id, id));
await deleteMediaCompletely(existing);
res.json(ApiResponse.success(null, '删除成功'));
} catch (e: unknown) {
logger.error(`[www/media/:id] DELETE error: ${e instanceof Error ? e.message : e}`);