356 lines
8.6 KiB
TypeScript
Executable File
356 lines
8.6 KiB
TypeScript
Executable File
/**
|
|
* 专题管理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<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
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; |