Files
ggl/api/routes/categories.ts
2025-09-23 07:35:11 +00:00

431 lines
10 KiB
TypeScript

/**
* 分类管理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;