const express = require('express'); const router = express.Router(); const {Article, Category} = require('../../models') const {Op} = require('sequelize') /* 查询文章列表 GET /admin/articles */ router.get('/', async function(req, res, next) { try { const query = req.query //当前是第几页,如果不传,那就是第一页 const currentPage = Math.abs(Number(query.currentPage)) || 1 //每页显示多少条数据,如果不传,那就显示10条 const pageSize = Math.abs(Number(query.pageSize)) || 10 //计算 offset const offset = (currentPage - 1) * pageSize const condition = { order:[['id','DESC']], limit:pageSize, offset, include: [{ model: Category, as: 'categoryInfo', attributes: ['id', 'name', 'level', 'parentId'], required: false // LEFT JOIN,即使没有分类也能返回文章 }] } // 构建查询条件 const whereConditions = {}; // 标题搜索 if(query.title){ whereConditions.title = { [Op.like]:`%${query.title}%` } } // 分类筛选 - 支持多选和包含子分类 if(query.categoryIds){ let categoryIds = []; // 处理categoryIds参数(支持逗号分隔的多个ID) if(typeof query.categoryIds === 'string'){ categoryIds = query.categoryIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); } else if(Array.isArray(query.categoryIds)){ categoryIds = query.categoryIds.map(id => parseInt(id)).filter(id => !isNaN(id)); } else { categoryIds = [parseInt(query.categoryIds)].filter(id => !isNaN(id)); } if(categoryIds.length > 0){ // 获取所有选中的分类及其子分类的ID const allCategoryIds = await getAllCategoryIdsWithChildren(categoryIds); whereConditions.category = { [Op.in]: allCategoryIds }; } } // 如果有查询条件,添加到condition中 if(Object.keys(whereConditions).length > 0){ condition.where = whereConditions; } const {count ,rows} = await Article.findAndCountAll(condition) res.json({ status:true, message:'成功', data:{ articles:rows, pagination:{ total:count, currentPage, pageSize } } }); }catch(error){ res.status(500).json({ status:false, message:'失败', errors:[error.message] }); } }); /* 查询文章详情 GET /admin/articles/:id */ router.get('/:id', async function(req, res, next) { try { //获取文章id const {id} = req.params //查询文章 const article = await Article.findByPk(id, { include: [{ model: Category, as: 'categoryInfo', attributes: ['id', 'name', 'level', 'parentId'], required: false }] }) if(article){ res.json({ status:true, message:'成功', data:article }); }else{ res.status(404).json({ status:false, message:'文章未找到', }); } }catch(error){ res.status(500).json({ status:false, message:'失败', errors:[error.message] }); } }); /* 创建文章 POST /admin/articles/ */ router.post('/', async function(req, res, next) { try { // 添加请求日志 console.log('=== 创建文章请求开始 ==='); console.log('请求体大小:', JSON.stringify(req.body).length); console.log('Content字段长度:', req.body.content ? req.body.content.length : 0); console.log('Title字段长度:', req.body.title ? req.body.title.length : 0); //白名单过滤 const body = filterBody(req) console.log('过滤后的数据:', { titleLength: body.title ? body.title.length : 0, contentLength: body.content ? body.content.length : 0, hasImage: !!body.img, type: body.type }); const article = await Article.create(body) console.log('文章创建成功, ID:', article.id); console.log('=== 创建文章请求结束 ==='); res.status(201).json({ status:true, message:'成功', data:article }); }catch(error){ // 添加详细的错误日志 console.error('=== 创建文章错误 ==='); console.error('错误名称:', error.name); console.error('错误消息:', error.message); console.error('错误堆栈:', error.stack); console.error('请求体大小:', JSON.stringify(req.body).length); console.error('请求体:', JSON.stringify(req.body, null, 2)); if(error.message === '标题不能为空' || error.message === '内容不能为空' || error.message.includes('长度不能超过') || error.message.includes('不允许的脚本标签')){ res.status(400).json({ status:false, message:'请求参数错误', errors:[error.message] }); }else if(error.name === 'SequelizeValidationError'){ const errors = error.errors.map(e => e.message) res.status(400).json({ status:false, message:'数据验证失败', errors }); }else if(error.name === 'SequelizeDatabaseError'){ console.error('数据库错误详情:', error.original); res.status(500).json({ status:false, message:'数据库错误', errors:['数据库操作失败,请稍后重试'] }); }else if(error.name === 'SequelizeConnectionError'){ res.status(500).json({ status:false, message:'数据库连接错误', errors:['数据库连接失败,请稍后重试'] }); }else{ res.status(500).json({ status:false, message:'服务器内部错误', errors:['服务器处理请求时发生错误,请稍后重试'] }); } } }); /* 删除文章 GET /admin/articles/:id */ router.delete('/:id', async function(req, res, next) { try { //获取文章id const {id} = req.params //查询文章 const article = await Article.findByPk(id) if(article){ await article.destroy() res.json({ status:true, message:'成功', }); }else{ res.status(404).json({ status:false, message:'文章未找到', }); } }catch(error){ res.status(500).json({ status:false, message:'失败', errors:[error.message] }); } }); /* 更新文章 GET /admin/articles/:id */ router.put('/:id', async function(req, res, next) { try { //获取文章id const {id} = req.params //查询文章 const article = await Article.findByPk(id) //白名单过滤 const body = filterBody(req) if(article){ await article.update(body) res.json({ status:true, message:'成功', data:article }); }else{ res.status(404).json({ status:false, message:'文章未找到', }); } }catch(error){ res.status(500).json({ status:false, message:'失败', errors:[error.message] }); } }); /** * 获取分类及其所有子分类的ID列表 * @param {Array} categoryIds - 分类ID数组 * @returns {Promise} 包含所有分类ID的数组 */ async function getAllCategoryIdsWithChildren(categoryIds) { try { let allIds = [...categoryIds]; // 递归获取所有子分类 async function getChildrenIds(parentIds) { if (parentIds.length === 0) return []; const children = await Category.findAll({ where: { parentId: { [Op.in]: parentIds } }, attributes: ['id'] }); const childrenIds = children.map(child => child.id); if (childrenIds.length > 0) { allIds = allIds.concat(childrenIds); // 递归获取子分类的子分类 const grandChildrenIds = await getChildrenIds(childrenIds); allIds = allIds.concat(grandChildrenIds); } return childrenIds; } await getChildrenIds(categoryIds); // 去重并返回 return [...new Set(allIds)]; } catch (error) { console.error('获取分类ID列表错误:', error); // 如果出错,返回原始ID列表 return categoryIds; } } function filterBody(req){ try { // 数据清理和验证 const body = { title: req.body.title ? String(req.body.title).trim() : null, content: req.body.content ? String(req.body.content) : null, type: req.body.type ? parseInt(req.body.type) : null, img: req.body.img ? String(req.body.img).trim() : null, date: req.body.date ? new Date(req.body.date) : null, author: req.body.author ? String(req.body.author).trim() : null, category: req.body.category ? parseInt(req.body.category) : null, crop: req.body.crop ? parseInt(req.body.crop) : null, seoKeyword: req.body.seoKeyword ? String(req.body.seoKeyword).trim() : null, seoDescription: req.body.seoDescription ? String(req.body.seoDescription).trim() : null }; // 验证必填字段 if (!body.title) { throw new Error('标题不能为空'); } if (!body.content) { throw new Error('内容不能为空'); } // 验证标题长度 - 放宽限制以适应富文本编辑器 if (body.title.length > 500) { throw new Error('标题长度不能超过500个字符'); } // 验证内容长度 - 防止过大的内容 if (body.content.length > 5000000) { // 5MB限制 throw new Error('内容过长,请减少内容长度'); } // 检查富文本内容是否包含危险标签或脚本 const dangerousTags = /]*>.*?<\/script>/gi; if (dangerousTags.test(body.content)) { throw new Error('内容包含不允许的脚本标签'); } return body; } catch (error) { console.error('filterBody错误:', error); throw error; } } module.exports = router;