first commit

This commit is contained in:
2025-09-23 07:35:11 +00:00
commit a5dd3f1335
110 changed files with 46108 additions and 0 deletions

356
api/routes/topics.ts Executable file
View 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;