first commit
This commit is contained in:
431
api/routes/categories.ts
Normal file
431
api/routes/categories.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 分类管理API路由
|
||||
* Handle category 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/categories
|
||||
*/
|
||||
router.get('/', optionalAuth, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const categories = await query(`
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.created_at as createdAt,
|
||||
c.sort_order,
|
||||
COUNT(v.id) as videoCount
|
||||
FROM categories c
|
||||
LEFT JOIN videos v ON c.id = v.category
|
||||
GROUP BY c.id, c.name, c.description, c.created_at, c.sort_order
|
||||
ORDER BY c.sort_order ASC, c.created_at DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '获取分类列表成功',
|
||||
data: categories
|
||||
});
|
||||
} catch (error) {
|
||||
// 获取分类列表失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取分类列表失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取分类详情
|
||||
* GET /api/categories/:id
|
||||
*/
|
||||
router.get('/:id', optionalAuth, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const category = await queryOne(`
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.created_at as createdAt,
|
||||
COUNT(v.id) as videoCount
|
||||
FROM categories c
|
||||
LEFT JOIN videos v ON c.id = v.category
|
||||
WHERE c.id = ?
|
||||
GROUP BY c.id, c.name, c.description, c.created_at
|
||||
`, [id]);
|
||||
|
||||
if (!category) {
|
||||
res.status(404).json({
|
||||
code: 404,
|
||||
message: '分类不存在',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '获取分类详情成功',
|
||||
data: category
|
||||
});
|
||||
} catch (error) {
|
||||
// 获取分类详情失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '获取分类详情失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建分类
|
||||
* POST /api/categories
|
||||
*/
|
||||
router.post('/', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { name, description = '' } = req.body;
|
||||
|
||||
if (!name || name.trim() === '') {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '分类名称不能为空',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查分类名称是否已存在
|
||||
const existingCategory = await queryOne(
|
||||
'SELECT id FROM categories WHERE name = ?',
|
||||
[name.trim()]
|
||||
);
|
||||
|
||||
if (existingCategory) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '分类名称已存在',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建分类
|
||||
const result = await execute(
|
||||
'INSERT INTO categories (name, description, created_at) VALUES (?, ?, datetime("now"))',
|
||||
[name.trim(), description.trim()]
|
||||
);
|
||||
|
||||
// 获取创建的分类信息
|
||||
const newCategory = await queryOne(
|
||||
'SELECT id, name, description, created_at as createdAt FROM categories WHERE id = ?',
|
||||
[result.lastID]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
code: 201,
|
||||
message: '创建分类成功',
|
||||
data: newCategory
|
||||
});
|
||||
} catch (error) {
|
||||
// 创建分类失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '创建分类失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新分类
|
||||
* PUT /api/categories/:id
|
||||
*/
|
||||
router.put('/:id', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description = '' } = req.body;
|
||||
|
||||
if (!name || name.trim() === '') {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '分类名称不能为空',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查分类是否存在
|
||||
const existingCategory = await queryOne(
|
||||
'SELECT id FROM categories WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!existingCategory) {
|
||||
res.status(404).json({
|
||||
code: 404,
|
||||
message: '分类不存在',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查分类名称是否已被其他分类使用
|
||||
const duplicateCategory = await queryOne(
|
||||
'SELECT id FROM categories WHERE name = ? AND id != ?',
|
||||
[name.trim(), id]
|
||||
);
|
||||
|
||||
if (duplicateCategory) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '分类名称已存在',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新分类
|
||||
await execute(
|
||||
'UPDATE categories SET name = ?, description = ? WHERE id = ?',
|
||||
[name.trim(), description.trim(), id]
|
||||
);
|
||||
|
||||
// 获取更新后的分类信息
|
||||
const updatedCategory = await queryOne(
|
||||
'SELECT id, name, description, created_at as createdAt FROM categories WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '更新分类成功',
|
||||
data: updatedCategory
|
||||
});
|
||||
} catch (error) {
|
||||
// 更新分类失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '更新分类失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
* DELETE /api/categories/:id
|
||||
*/
|
||||
router.delete('/:id', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查分类是否存在
|
||||
const existingCategory = await queryOne(
|
||||
'SELECT id FROM categories WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!existingCategory) {
|
||||
res.status(404).json({
|
||||
code: 404,
|
||||
message: '分类不存在',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有视频使用该分类
|
||||
const videosUsingCategory = await queryOne(
|
||||
'SELECT COUNT(*) as count FROM videos WHERE category = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (videosUsingCategory.count > 0) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: `无法删除分类,还有 ${videosUsingCategory.count} 个视频使用该分类`,
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
await execute('DELETE FROM categories WHERE id = ?', [id]);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '删除分类成功',
|
||||
data: null
|
||||
});
|
||||
} catch (error) {
|
||||
// 删除分类失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '删除分类失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 批量删除分类
|
||||
* DELETE /api/categories/batch
|
||||
*/
|
||||
router.delete('/batch', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { categoryIds } = req.body;
|
||||
|
||||
if (!Array.isArray(categoryIds) || categoryIds.length === 0) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '请选择要删除的分类',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有视频使用这些分类
|
||||
const videosUsingCategories = await query(
|
||||
`SELECT category, COUNT(*) as count FROM videos WHERE category IN (${categoryIds.map(() => '?').join(',')}) GROUP BY category`,
|
||||
categoryIds
|
||||
);
|
||||
|
||||
if (videosUsingCategories.length > 0) {
|
||||
const categoryNames = await query(
|
||||
`SELECT id, name FROM categories WHERE id IN (${categoryIds.map(() => '?').join(',')})`,
|
||||
categoryIds
|
||||
);
|
||||
|
||||
const usedCategories = videosUsingCategories.map(vc => {
|
||||
const category = categoryNames.find(cn => cn.id === vc.category);
|
||||
return `${category?.name || '未知分类'}(${vc.count}个视频)`;
|
||||
});
|
||||
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: `无法删除分类,以下分类还有视频在使用:${usedCategories.join(', ')}`,
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量删除分类
|
||||
await execute(
|
||||
`DELETE FROM categories WHERE id IN (${categoryIds.map(() => '?').join(',')})`,
|
||||
categoryIds
|
||||
);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: `成功删除 ${categoryIds.length} 个分类`,
|
||||
data: null
|
||||
});
|
||||
} catch (error) {
|
||||
// 批量删除分类失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '批量删除分类失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新分类顺序
|
||||
* PUT /api/categories/:id/order
|
||||
*/
|
||||
router.put('/:id/order', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const categoryId = parseInt(req.params.id);
|
||||
const { direction } = req.body; // 'up' 或 'down'
|
||||
|
||||
if (isNaN(categoryId)) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '分类ID必须是有效的数字',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!direction || !['up', 'down'].includes(direction)) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: '方向参数必须是 up 或 down',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前分类信息
|
||||
const currentCategory = await queryOne('SELECT * FROM categories WHERE id = ?', [categoryId]);
|
||||
if (!currentCategory) {
|
||||
res.status(404).json({
|
||||
code: 404,
|
||||
message: '分类不存在',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取需要交换的分类
|
||||
let targetCategory;
|
||||
if (direction === 'up') {
|
||||
// 获取排序值小于当前分类且最接近的分类
|
||||
targetCategory = await queryOne(`
|
||||
SELECT * FROM categories
|
||||
WHERE sort_order < ?
|
||||
ORDER BY sort_order DESC
|
||||
LIMIT 1
|
||||
`, [currentCategory.sort_order]);
|
||||
} else {
|
||||
// 获取排序值大于当前分类且最接近的分类
|
||||
targetCategory = await queryOne(`
|
||||
SELECT * FROM categories
|
||||
WHERE sort_order > ?
|
||||
ORDER BY sort_order ASC
|
||||
LIMIT 1
|
||||
`, [currentCategory.sort_order]);
|
||||
}
|
||||
|
||||
if (!targetCategory) {
|
||||
res.status(400).json({
|
||||
code: 400,
|
||||
message: direction === 'up' ? '已经是第一个分类' : '已经是最后一个分类',
|
||||
data: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 交换两个分类的sort_order值
|
||||
const tempOrder = currentCategory.sort_order;
|
||||
await execute('UPDATE categories SET sort_order = ? WHERE id = ?', [targetCategory.sort_order, categoryId]);
|
||||
await execute('UPDATE categories SET sort_order = ? WHERE id = ?', [tempOrder, targetCategory.id]);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: '分类顺序更新成功',
|
||||
data: null
|
||||
});
|
||||
} catch (error) {
|
||||
// 更新分类顺序失败
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: '更新分类顺序失败',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user