feat: 添加公告功能

This commit is contained in:
2025-12-13 18:40:42 +08:00
parent d7d878dc08
commit 7015de0055
4 changed files with 584 additions and 3 deletions

View File

@@ -54,7 +54,7 @@ config = get_config()
DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理)
DB_VERSION = 5
DB_VERSION = 6
# ==================== 时区处理工具函数 ====================
# Bug fix: 统一时区处理,避免混用导致的问题
@@ -219,6 +219,30 @@ def init_database():
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
''')
# 公告表
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 公告永久关闭记录表(用户维度)
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcement_dismissals (
user_id INTEGER NOT NULL,
announcement_id INTEGER NOT NULL,
dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, announcement_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE
)
''')
# 用户定时任务表
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_schedules (
@@ -263,6 +287,11 @@ def init_database():
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_user_id ON bug_feedbacks(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_status ON bug_feedbacks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_created_at ON bug_feedbacks(created_at)')
# 公告表索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)')
# 用户定时任务表索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_schedules_user_id ON user_schedules(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled ON user_schedules(enabled)')
@@ -346,6 +375,10 @@ def migrate_database():
_migrate_to_v5(conn)
current_version = 5
if current_version < 6:
_migrate_to_v6(conn)
current_version = 6
# 更新版本号
cursor.execute('UPDATE db_version SET version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
(DB_VERSION,))
@@ -514,6 +547,46 @@ def _migrate_to_v5(conn):
conn.commit()
def _migrate_to_v6(conn):
"""迁移到版本6 - 添加公告功能相关表"""
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcements'")
if not cursor.fetchone():
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
print(" ✓ 创建 announcements 表 (公告)")
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)')
print(" ✓ 创建 announcements 表索引")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcement_dismissals'")
if not cursor.fetchone():
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcement_dismissals (
user_id INTEGER NOT NULL,
announcement_id INTEGER NOT NULL,
dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, announcement_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE
)
''')
print(" ✓ 创建 announcement_dismissals 表 (公告永久关闭记录)")
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)')
print(" ✓ 创建 announcement_dismissals 表索引")
conn.commit()
# ==================== 管理员相关 ====================
def ensure_default_admin():
@@ -1713,6 +1786,115 @@ def get_feedback_stats():
return dict(row) if row else {'total': 0, 'pending': 0, 'replied': 0, 'closed': 0}
# ==================== 公告管理 ====================
def create_announcement(title, content, is_active=True):
"""创建公告(默认启用;启用时会自动停用其他公告)"""
title = (title or '').strip()
content = (content or '').strip()
if not title or not content:
return None
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
if is_active:
cursor.execute('UPDATE announcements SET is_active = 0, updated_at = ? WHERE is_active = 1', (cst_time,))
cursor.execute('''
INSERT INTO announcements (title, content, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
''', (title, content, 1 if is_active else 0, cst_time, cst_time))
conn.commit()
return cursor.lastrowid
def get_announcement_by_id(announcement_id):
"""根据ID获取公告"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM announcements WHERE id = ?', (announcement_id,))
row = cursor.fetchone()
return dict(row) if row else None
def get_announcements(limit=50, offset=0):
"""获取公告列表(管理员用)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM announcements
ORDER BY created_at DESC, id DESC
LIMIT ? OFFSET ?
''', (limit, offset))
return [dict(row) for row in cursor.fetchall()]
def set_announcement_active(announcement_id, is_active):
"""启用/停用公告;启用时会自动停用其他公告"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
if is_active:
cursor.execute('UPDATE announcements SET is_active = 0, updated_at = ? WHERE is_active = 1', (cst_time,))
cursor.execute('''
UPDATE announcements
SET is_active = 1, updated_at = ?
WHERE id = ?
''', (cst_time, announcement_id))
else:
cursor.execute('''
UPDATE announcements
SET is_active = 0, updated_at = ?
WHERE id = ?
''', (cst_time, announcement_id))
conn.commit()
return cursor.rowcount > 0
def delete_announcement(announcement_id):
"""删除公告(同时清理用户关闭记录)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('DELETE FROM announcement_dismissals WHERE announcement_id = ?', (announcement_id,))
cursor.execute('DELETE FROM announcements WHERE id = ?', (announcement_id,))
conn.commit()
return cursor.rowcount > 0
def get_active_announcement_for_user(user_id):
"""获取当前用户应展示的启用公告(已永久关闭的不再返回)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT a.*
FROM announcements a
LEFT JOIN announcement_dismissals d
ON d.announcement_id = a.id AND d.user_id = ?
WHERE a.is_active = 1 AND d.announcement_id IS NULL
ORDER BY a.created_at DESC, a.id DESC
LIMIT 1
''', (user_id,))
row = cursor.fetchone()
return dict(row) if row else None
def dismiss_announcement_for_user(user_id, announcement_id):
"""用户永久关闭某条公告(幂等)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute('''
INSERT OR IGNORE INTO announcement_dismissals (user_id, announcement_id, dismissed_at)
VALUES (?, ?, ?)
''', (user_id, announcement_id, cst_time))
conn.commit()
return cursor.rowcount >= 0
# ==================== 用户定时任务管理 ====================
def get_user_schedules(user_id):