/** * 专题管理API路由 * Handle topic CRUD operations */ import { Router, type Request, type Response } from 'express'; import { query, queryOne, execute } from '../config/database.js'; import { authenticateToken, optionalAuth } from '../middleware/auth.js'; const router = Router(); /** * 获取所有专题列表 * GET /api/topics */ router.get('/', optionalAuth, async (req: Request, res: Response): Promise => { try { const topics = await query(` SELECT t.id, t.name, t.description, t.cover_image, t.sort_order, t.status, t.created_at, COUNT(vt.video_id) as video_count FROM topics t LEFT JOIN video_topics vt ON t.id = vt.topic_id WHERE t.status = 'active' GROUP BY t.id, t.name, t.description, t.cover_image, t.sort_order, t.status, t.created_at ORDER BY t.sort_order ASC, t.created_at DESC `); res.json({ code: 200, message: '获取成功', data: topics }); } catch (error) { console.error('获取专题列表失败:', error); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); /** * 获取专题详情及其视频列表 * GET /api/topics/:id */ router.get('/:id', optionalAuth, async (req: Request, res: Response): Promise => { try { const topicId = parseInt(req.params.id); const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 12; const offset = (page - 1) * limit; if (isNaN(topicId)) { res.status(400).json({ code: 400, message: '无效的专题ID' }); return; } // 获取专题信息 const topic = await queryOne(` SELECT t.id, t.name, t.description, t.cover_image, t.sort_order, t.status, t.created_at, COUNT(vt.video_id) as video_count FROM topics t LEFT JOIN video_topics vt ON t.id = vt.topic_id WHERE t.id = ? AND t.status = 'active' GROUP BY t.id, t.name, t.description, t.cover_image, t.sort_order, t.status, t.created_at `, [topicId]); if (!topic) { res.status(404).json({ code: 404, message: '专题不存在' }); return; } // 获取专题下的视频列表 const videos = await query(` SELECT v.id, v.title, v.description, v.file_path, v.video_url, v.cover_image, v.duration, v.file_size, v.created_at, u.username as uploader, COALESCE(vs.views, 0) as views, COALESCE(vs.likes, 0) as likes FROM videos v INNER JOIN video_topics vt ON v.id = vt.video_id LEFT JOIN users u ON v.user_id = u.id LEFT JOIN video_stats vs ON v.id = vs.video_id WHERE vt.topic_id = ? AND v.status = 'active' ORDER BY v.created_at DESC LIMIT ? OFFSET ? `, [topicId, limit, offset]); // 获取总数 const totalResult = await queryOne(` SELECT COUNT(*) as total FROM videos v INNER JOIN video_topics vt ON v.id = vt.video_id WHERE vt.topic_id = ? AND v.status = 'active' `, [topicId]); const total = totalResult.total; const totalPages = Math.ceil(total / limit); res.json({ code: 200, message: '获取成功', data: { topic, videos: { list: videos, total, page, limit, totalPages, hasNext: page < totalPages, hasPrev: page > 1 } } }); } catch (error) { console.error('获取专题详情失败:', error); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); /** * 创建新专题(管理员功能) * POST /api/topics */ router.post('/', authenticateToken, async (req: Request, res: Response): Promise => { try { const { name, description, cover_image, sort_order } = req.body; if (!name) { res.status(400).json({ code: 400, message: '专题名称不能为空' }); return; } // 检查专题名称是否已存在 const existingTopic = await queryOne('SELECT id FROM topics WHERE name = ?', [name]); if (existingTopic) { res.status(400).json({ code: 400, message: '专题名称已存在' }); return; } const result = await execute(` INSERT INTO topics (name, description, cover_image, sort_order, created_at) VALUES (?, ?, ?, ?, datetime('now')) `, [name, description || '', cover_image || '', sort_order || 0]); res.status(201).json({ code: 201, message: '专题创建成功', data: { id: result.lastID, name, description, cover_image, sort_order } }); } catch (error) { console.error('创建专题失败:', error); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); /** * 更新专题信息(管理员功能) * PUT /api/topics/:id */ router.put('/:id', authenticateToken, async (req: Request, res: Response): Promise => { try { const topicId = parseInt(req.params.id); const { name, description, cover_image, sort_order, status } = req.body; if (isNaN(topicId)) { res.status(400).json({ code: 400, message: '无效的专题ID' }); return; } if (!name) { res.status(400).json({ code: 400, message: '专题名称不能为空' }); return; } // 检查专题是否存在 const existingTopic = await queryOne('SELECT id FROM topics WHERE id = ?', [topicId]); if (!existingTopic) { res.status(404).json({ code: 404, message: '专题不存在' }); return; } // 检查名称是否与其他专题冲突 const nameConflict = await queryOne('SELECT id FROM topics WHERE name = ? AND id != ?', [name, topicId]); if (nameConflict) { res.status(400).json({ code: 400, message: '专题名称已存在' }); return; } await execute(` UPDATE topics SET name = ?, description = ?, cover_image = ?, sort_order = ?, status = ?, updated_at = datetime('now') WHERE id = ? `, [name, description || '', cover_image || '', sort_order || 0, status || 'active', topicId]); res.json({ code: 200, message: '专题更新成功' }); } catch (error) { console.error('更新专题失败:', error); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); /** * 删除专题(管理员功能) * DELETE /api/topics/:id */ router.delete('/:id', authenticateToken, async (req: Request, res: Response): Promise => { try { const topicId = parseInt(req.params.id); if (isNaN(topicId)) { res.status(400).json({ code: 400, message: '无效的专题ID' }); return; } // 检查专题是否存在 const existingTopic = await queryOne('SELECT id FROM topics WHERE id = ?', [topicId]); if (!existingTopic) { res.status(404).json({ code: 404, message: '专题不存在' }); return; } // 软删除:将状态设置为 inactive await execute(` UPDATE topics SET status = 'inactive', updated_at = datetime('now') WHERE id = ? `, [topicId]); res.json({ code: 200, message: '专题删除成功' }); } catch (error) { console.error('删除专题失败:', error); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); /** * 获取视频的专题列表 * GET /api/topics/video/:videoId */ router.get('/video/:videoId', optionalAuth, async (req: Request, res: Response): Promise => { try { const videoId = parseInt(req.params.videoId); if (isNaN(videoId)) { res.status(400).json({ code: 400, message: '无效的视频ID' }); return; } const topics = await query(` SELECT t.id, t.name, t.description, t.cover_image, t.sort_order FROM topics t INNER JOIN video_topics vt ON t.id = vt.topic_id WHERE vt.video_id = ? AND t.status = 'active' ORDER BY t.sort_order ASC, t.name ASC `, [videoId]); res.json({ code: 200, message: '获取成功', data: topics }); } catch (error) { console.error('获取视频专题失败:', error); res.status(500).json({ code: 500, message: '服务器内部错误' }); } }); export default router;