397 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			397 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | const express = require('express'); | ||
|  | const { getDB } = require('../database'); | ||
|  | const { auth, adminAuth } = require('../middleware/auth'); | ||
|  | const dayjs = require('dayjs'); | ||
|  | 
 | ||
|  | const router = express.Router(); | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | router.get('/', auth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { page = 1, limit = 10, status, type, keyword } = req.query; | ||
|  |     const offset = (page - 1) * limit; | ||
|  |      | ||
|  |     let whereClause = 'WHERE 1=1'; | ||
|  |     const params = []; | ||
|  |      | ||
|  |     if (status) { | ||
|  |       whereClause += ' AND status = ?'; | ||
|  |       params.push(status); | ||
|  |     } | ||
|  |      | ||
|  |     if (type) { | ||
|  |       whereClause += ' AND type = ?'; | ||
|  |       params.push(type); | ||
|  |     } | ||
|  |      | ||
|  |     if (keyword) { | ||
|  |       whereClause += ' AND (title LIKE ? OR content LIKE ?)'; | ||
|  |       params.push(`%${keyword}%`, `%${keyword}%`); | ||
|  |     } | ||
|  |      | ||
|  |     // 获取总数
 | ||
|  |     const countQuery = `SELECT COUNT(*) as total FROM announcements ${whereClause}`; | ||
|  |     const [countResult] = await db.execute(countQuery, params); | ||
|  |     const total = countResult[0].total; | ||
|  |      | ||
|  |     // 获取公告列表(包含用户阅读状态)
 | ||
|  |     const limitValue = Math.max(1, Math.min(100, parseInt(limit))); | ||
|  |     const offsetValue = Math.max(0, parseInt(offset)); | ||
|  |      | ||
|  |     const query = `
 | ||
|  |       SELECT a.*, u.username as creator_name, | ||
|  |              uar.is_read, | ||
|  |              uar.read_at, | ||
|  |              CASE  | ||
|  |                WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 | ||
|  |                ELSE 0 | ||
|  |              END as is_expired | ||
|  |       FROM announcements a  | ||
|  |       LEFT JOIN users u ON a.created_by = u.id  | ||
|  |       LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? | ||
|  |       ${whereClause}  | ||
|  |       ORDER BY a.is_pinned DESC, a.created_at DESC  | ||
|  |       LIMIT ${limitValue} OFFSET ${offsetValue} | ||
|  |     `;
 | ||
|  |      | ||
|  |     const [announcements] = await db.execute(query, [req.user.id, ...params]); | ||
|  |      | ||
|  | 
 | ||
|  |      | ||
|  |     const expiredUnreadAnnouncements = announcements.filter(a => a.is_expired && !a.is_read); | ||
|  |      | ||
|  |     if (expiredUnreadAnnouncements.length > 0) { | ||
|  |       const expiredIds = expiredUnreadAnnouncements.map(a => a.id); | ||
|  |       await db.execute(`
 | ||
|  |         INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||
|  |         VALUES ${expiredIds.map(() => '(?, ?, TRUE, NOW())').join(', ')} | ||
|  |         ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||
|  |       `, expiredIds.flatMap(id => [req.user.id, id]));
 | ||
|  |        | ||
|  |       // 更新返回数据中的阅读状态
 | ||
|  |       expiredUnreadAnnouncements.forEach(a => { | ||
|  |         a.is_read = true; | ||
|  |         a.read_at = new Date(); | ||
|  |       }); | ||
|  |     } | ||
|  |      | ||
|  |     res.json({ | ||
|  |       success: true, | ||
|  |       data: { | ||
|  |         announcements, | ||
|  |         total, | ||
|  |         page: parseInt(page), | ||
|  |         limit: parseInt(limit), | ||
|  |         totalPages: Math.ceil(total / limit) | ||
|  |       } | ||
|  |     }); | ||
|  |   } catch (error) { | ||
|  |     console.error('获取公告列表失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '获取公告列表失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | 
 | ||
|  | router.get('/:id', auth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { id } = req.params; | ||
|  |      | ||
|  |     const query = `
 | ||
|  |       SELECT a.*, u.username as creator_name, | ||
|  |              uar.is_read, | ||
|  |              uar.read_at, | ||
|  |              CASE  | ||
|  |                WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 | ||
|  |                ELSE 0 | ||
|  |              END as is_expired | ||
|  |       FROM announcements a  | ||
|  |       LEFT JOIN users u ON a.created_by = u.id  | ||
|  |       LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? | ||
|  |       WHERE a.id = ? | ||
|  |     `;
 | ||
|  |      | ||
|  |     const [result] = await db.execute(query, [req.user.id, id]); | ||
|  |      | ||
|  |     if (result.length === 0) { | ||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||
|  |     } | ||
|  |      | ||
|  |     const announcement = result[0]; | ||
|  |      | ||
|  |     // 如果公告未读或已过期但未标记为已读,则标记为已读
 | ||
|  |     if (!announcement.is_read || (announcement.is_expired && !announcement.is_read)) { | ||
|  |       await db.execute(`
 | ||
|  |         INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||
|  |         VALUES (?, ?, TRUE, NOW()) | ||
|  |         ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||
|  |       `, [req.user.id, id]);
 | ||
|  |        | ||
|  |       announcement.is_read = true; | ||
|  |       announcement.read_at = new Date(); | ||
|  |     } | ||
|  |      | ||
|  |     res.json({ success: true, data: announcement }); | ||
|  |   } catch (error) { | ||
|  |     console.error('获取公告详情失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '获取公告详情失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | 
 | ||
|  | router.post('/', auth, adminAuth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { | ||
|  |       title, | ||
|  |       content, | ||
|  |       type = 'system', | ||
|  |       priority = 'medium', | ||
|  |       status = 'draft', | ||
|  |       is_pinned = false, | ||
|  |       publish_time, | ||
|  |       expire_time | ||
|  |     } = req.body; | ||
|  |      | ||
|  |     if (!title || !content) { | ||
|  |       return res.status(400).json({ success: false, message: '标题和内容不能为空' }); | ||
|  |     } | ||
|  |      | ||
|  |     const query = `
 | ||
|  |       INSERT INTO announcements ( | ||
|  |         title, content, type, priority, status, is_pinned,  | ||
|  |         publish_time, expire_time, created_by, created_at, updated_at | ||
|  |       ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) | ||
|  |     `;
 | ||
|  |      | ||
|  |     const [result] = await db.execute(query, [ | ||
|  |       title, | ||
|  |       content, | ||
|  |       type, | ||
|  |       priority, | ||
|  |       status, | ||
|  |       is_pinned, | ||
|  |       publish_time || null, | ||
|  |       expire_time || null, | ||
|  |       req.user.id | ||
|  |     ]); | ||
|  |      | ||
|  |     res.status(201).json({ | ||
|  |       success: true, | ||
|  |       message: '公告创建成功', | ||
|  |       data: { id: result.insertId } | ||
|  |     }); | ||
|  |   } catch (error) { | ||
|  |     console.error('创建公告失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '创建公告失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | 
 | ||
|  | router.put('/:id', auth, adminAuth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { id } = req.params; | ||
|  |     let { | ||
|  |       title, | ||
|  |       content, | ||
|  |       type, | ||
|  |       priority, | ||
|  |       status, | ||
|  |       is_pinned, | ||
|  |       publish_time, | ||
|  |       expire_time | ||
|  |     } = req.body; | ||
|  |      | ||
|  |     // 检查公告是否存在
 | ||
|  |     const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); | ||
|  |     if (existing.length === 0) { | ||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||
|  |     } | ||
|  |      | ||
|  |     const updates = []; | ||
|  |     const params = []; | ||
|  |      | ||
|  |     if (title !== undefined) { | ||
|  |       updates.push('title = ?'); | ||
|  |       params.push(title); | ||
|  |     } | ||
|  |     if (content !== undefined) { | ||
|  |       updates.push('content = ?'); | ||
|  |       params.push(content); | ||
|  |     } | ||
|  |     if (type !== undefined) { | ||
|  |       updates.push('type = ?'); | ||
|  |       params.push(type); | ||
|  |     } | ||
|  |     if (priority !== undefined) { | ||
|  |       updates.push('priority = ?'); | ||
|  |       params.push(priority); | ||
|  |     } | ||
|  |     if (status !== undefined) { | ||
|  |       updates.push('status = ?'); | ||
|  |       params.push(status); | ||
|  |     } | ||
|  |     if (is_pinned !== undefined) { | ||
|  |       updates.push('is_pinned = ?'); | ||
|  |       params.push(is_pinned); | ||
|  |     } | ||
|  |     if (publish_time !== undefined) { | ||
|  |       updates.push('publish_time = ?'); | ||
|  |       publish_time = dayjs(publish_time).format('YYYY-MM-DD'); | ||
|  |       params.push(publish_time); | ||
|  |     } | ||
|  |     if (expire_time !== undefined) { | ||
|  |       updates.push('expire_time = ?'); | ||
|  |       params.push(expire_time); | ||
|  |     } | ||
|  |      | ||
|  |     if (updates.length === 0) { | ||
|  |       return res.status(400).json({ success: false, message: '没有要更新的字段' }); | ||
|  |     } | ||
|  |      | ||
|  |     updates.push('updated_at = NOW()'); | ||
|  |     params.push(id); | ||
|  |      | ||
|  |     const query = `UPDATE announcements SET ${updates.join(', ')} WHERE id = ?`; | ||
|  |     await db.execute(query, params); | ||
|  |      | ||
|  |     res.json({ success: true, message: '公告更新成功' }); | ||
|  |   } catch (error) { | ||
|  |     console.error('更新公告失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '更新公告失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | 
 | ||
|  | router.delete('/:id', auth, adminAuth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { id } = req.params; | ||
|  |      | ||
|  |     // 检查公告是否存在
 | ||
|  |     const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); | ||
|  |     if (existing.length === 0) { | ||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||
|  |     } | ||
|  |      | ||
|  |     await db.execute('DELETE FROM announcements WHERE id = ?', [id]); | ||
|  |      | ||
|  |     res.json({ success: true, message: '公告删除成功' }); | ||
|  |   } catch (error) { | ||
|  |     console.error('删除公告失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '删除公告失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | 
 | ||
|  | router.get('/public/list', async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { limit = 5 } = req.query; | ||
|  |     const limitValue = Math.max(1, Math.min(50, parseInt(limit))); | ||
|  |      | ||
|  |     const query = `
 | ||
|  |       SELECT id, title, content, type, priority, publish_time, created_at | ||
|  |       FROM announcements  | ||
|  |       WHERE status = 'published'  | ||
|  |         AND (expire_time IS NULL OR expire_time > NOW()) | ||
|  |         AND (publish_time IS NULL OR publish_time <= NOW()) | ||
|  |       ORDER BY is_pinned DESC, created_at DESC  | ||
|  |       LIMIT ${limitValue} | ||
|  |     `;
 | ||
|  |      | ||
|  |     const [announcements] = await db.execute(query, []); | ||
|  |      | ||
|  |     res.json({ success: true, data: announcements }); | ||
|  |   } catch (error) { | ||
|  |     console.error('获取公开公告失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '获取公开公告失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | // 标记公告为已读
 | ||
|  | router.post('/:id/read', auth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { id } = req.params; | ||
|  |      | ||
|  |     // 检查公告是否存在
 | ||
|  |     const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); | ||
|  |     if (existing.length === 0) { | ||
|  |       return res.status(404).json({ success: false, message: '公告不存在' }); | ||
|  |     } | ||
|  |      | ||
|  |     // 标记为已读
 | ||
|  |     await db.execute(`
 | ||
|  |       INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||
|  |       VALUES (?, ?, TRUE, NOW()) | ||
|  |       ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||
|  |     `, [req.user.id, id]);
 | ||
|  |      | ||
|  |     res.json({ success: true, message: '已标记为已读' }); | ||
|  |   } catch (error) { | ||
|  |     console.error('标记公告已读失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '标记公告已读失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | // 获取用户未读公告数量
 | ||
|  | router.get('/unread/count', auth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |      | ||
|  |     const query = `
 | ||
|  |       SELECT COUNT(*) as unread_count | ||
|  |       FROM announcements a | ||
|  |       LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? | ||
|  |       WHERE a.status = 'published'  | ||
|  |         AND (a.publish_time IS NULL OR a.publish_time <= NOW()) | ||
|  |         AND (a.expire_time IS NULL OR a.expire_time > NOW()) | ||
|  |         AND (uar.is_read IS NULL OR uar.is_read = FALSE) | ||
|  |     `;
 | ||
|  |      | ||
|  |     const [result] = await db.execute(query, [req.user.id]); | ||
|  |      | ||
|  |     res.json({  | ||
|  |       success: true,  | ||
|  |       data: {  | ||
|  |         unread_count: result[0].unread_count  | ||
|  |       }  | ||
|  |     }); | ||
|  |   } catch (error) { | ||
|  |     console.error('获取未读公告数量失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '获取未读公告数量失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | // 批量标记公告为已读
 | ||
|  | router.post('/batch/read', auth, async (req, res) => { | ||
|  |   try { | ||
|  |     const db = getDB(); | ||
|  |     const { announcement_ids } = req.body; | ||
|  |      | ||
|  |     if (!announcement_ids || !Array.isArray(announcement_ids) || announcement_ids.length === 0) { | ||
|  |       return res.status(400).json({ success: false, message: '请提供有效的公告ID列表' }); | ||
|  |     } | ||
|  |      | ||
|  |     // 批量标记为已读
 | ||
|  |     const values = announcement_ids.map(() => '(?, ?, TRUE, NOW())').join(', '); | ||
|  |     const params = announcement_ids.flatMap(id => [req.user.id, id]); | ||
|  |      | ||
|  |     await db.execute(`
 | ||
|  |       INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) | ||
|  |       VALUES ${values} | ||
|  |       ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() | ||
|  |     `, params);
 | ||
|  |      | ||
|  |     res.json({ success: true, message: '批量标记已读成功' }); | ||
|  |   } catch (error) { | ||
|  |     console.error('批量标记公告已读失败:', error); | ||
|  |     res.status(500).json({ success: false, message: '批量标记公告已读失败' }); | ||
|  |   } | ||
|  | }); | ||
|  | 
 | ||
|  | module.exports = router; |