first commit
This commit is contained in:
356
api/routes/topics.ts
Executable file
356
api/routes/topics.ts
Executable file
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* 专题管理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;
|
||||
Reference in New Issue
Block a user