官网开发 0804
This commit is contained in:
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user