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; |