1. 前端用户下拉菜单优化 - 改用CSS动画,解决鼠标移动问题 2. 帖子管理增加显示/隐藏开关 3. 帖子详情页可直接删除回复 4. 新增回复管理页面(列表、删除) 5. 用户管理增加编辑功能(用户名、邮箱、手机、简介) 6. 用户管理增加查看帖子按钮 7. 所有页面侧边栏添加回复管理入口 8. 数据库增加 is_hidden 字段
461 lines
17 KiB
Python
461 lines
17 KiB
Python
"""
|
|
技术论坛数据库模型 - SQLite
|
|
"""
|
|
|
|
import sqlite3
|
|
import json
|
|
import uuid
|
|
import datetime
|
|
from pathlib import Path
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from contextlib import contextmanager
|
|
|
|
class Database:
|
|
def __init__(self, db_path='data/tech_forum.db'):
|
|
self.db_path = Path(db_path)
|
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._init_tables()
|
|
|
|
@contextmanager
|
|
def get_conn(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
def _init_tables(self):
|
|
with self.get_conn() as conn:
|
|
# 用户表
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT UNIQUE NOT NULL,
|
|
email TEXT UNIQUE NOT NULL,
|
|
phone TEXT,
|
|
password TEXT NOT NULL,
|
|
avatar TEXT,
|
|
bio TEXT,
|
|
created_at TEXT,
|
|
updated_at TEXT
|
|
)
|
|
''')
|
|
|
|
# 帖子表
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
content TEXT,
|
|
type TEXT DEFAULT 'discussion',
|
|
author_id TEXT,
|
|
tags TEXT,
|
|
likes TEXT DEFAULT '[]',
|
|
views INTEGER DEFAULT 0,
|
|
is_pinned INTEGER DEFAULT 0,
|
|
is_hidden INTEGER DEFAULT 0,
|
|
created_at TEXT,
|
|
updated_at TEXT,
|
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
|
)
|
|
''')
|
|
|
|
# 回复表
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS replies (
|
|
id TEXT PRIMARY KEY,
|
|
post_id TEXT,
|
|
content TEXT,
|
|
author_id TEXT,
|
|
likes TEXT DEFAULT '[]',
|
|
reply_to TEXT,
|
|
created_at TEXT,
|
|
FOREIGN KEY (post_id) REFERENCES posts(id),
|
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
|
)
|
|
''')
|
|
|
|
# 主题表(工具分享)
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS topics (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
icon TEXT DEFAULT '🔧',
|
|
author_id TEXT,
|
|
followers TEXT DEFAULT '[]',
|
|
created_at TEXT,
|
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
|
)
|
|
''')
|
|
|
|
# 子主题表
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS sub_topics (
|
|
id TEXT PRIMARY KEY,
|
|
topic_id TEXT,
|
|
title TEXT,
|
|
content TEXT,
|
|
author_id TEXT,
|
|
created_at TEXT,
|
|
FOREIGN KEY (topic_id) REFERENCES topics(id),
|
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
|
)
|
|
''')
|
|
|
|
# 问题表
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS questions (
|
|
id TEXT PRIMARY KEY,
|
|
topic_id TEXT,
|
|
title TEXT,
|
|
content TEXT,
|
|
author_id TEXT,
|
|
views INTEGER DEFAULT 0,
|
|
created_at TEXT,
|
|
FOREIGN KEY (topic_id) REFERENCES topics(id),
|
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
|
)
|
|
''')
|
|
|
|
# 回答表
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS answers (
|
|
id TEXT PRIMARY KEY,
|
|
question_id TEXT,
|
|
content TEXT,
|
|
author_id TEXT,
|
|
likes TEXT DEFAULT '[]',
|
|
created_at TEXT,
|
|
FOREIGN KEY (question_id) REFERENCES questions(id),
|
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
|
)
|
|
''')
|
|
|
|
conn.commit()
|
|
|
|
|
|
class UserModel:
|
|
def __init__(self, db):
|
|
self.db = db
|
|
|
|
def create(self, username, email, phone, password):
|
|
user_id = str(uuid.uuid4())
|
|
avatar = f'https://api.dicebear.com/7.x/avataaars/svg?seed={username}'
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO users (id, username, email, phone, password, avatar, bio, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, '', ?, ?)
|
|
''', (user_id, username, email, phone, generate_password_hash(password), avatar, now, now))
|
|
conn.commit()
|
|
|
|
return user_id
|
|
|
|
def get_by_id(self, user_id):
|
|
with self.db.get_conn() as conn:
|
|
row = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
def get_by_username(self, username):
|
|
with self.db.get_conn() as conn:
|
|
row = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
def get_by_email(self, email):
|
|
with self.db.get_conn() as conn:
|
|
row = conn.execute('SELECT * FROM users WHERE email = ?', (email,)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
def verify_password(self, user, password):
|
|
return check_password_hash(user['password'], password)
|
|
|
|
def get_all(self):
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT * FROM users ORDER BY created_at DESC').fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
def delete(self, user_id):
|
|
with self.db.get_conn() as conn:
|
|
# 删除用户的所有帖子
|
|
conn.execute('DELETE FROM replies WHERE author_id = ?', (user_id,))
|
|
conn.execute('DELETE FROM posts WHERE author_id = ?', (user_id,))
|
|
conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
|
|
conn.commit()
|
|
|
|
def get_posts_count(self, user_id):
|
|
with self.db.get_conn() as conn:
|
|
return conn.execute('SELECT COUNT(*) FROM posts WHERE author_id = ?', (user_id,)).fetchone()[0]
|
|
|
|
def get_replies_count(self, user_id):
|
|
with self.db.get_conn() as conn:
|
|
return conn.execute('SELECT COUNT(*) FROM replies WHERE author_id = ?', (user_id,)).fetchone()[0]
|
|
|
|
|
|
class PostModel:
|
|
def __init__(self, db):
|
|
self.db = db
|
|
|
|
def create(self, title, content, post_type, author_id, tags):
|
|
post_id = str(uuid.uuid4())
|
|
now = datetime.datetime.now().isoformat()
|
|
tags_json = json.dumps(tags)
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO posts (id, title, content, type, author_id, tags, likes, views, is_pinned, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, '[]', 0, 0, ?, ?)
|
|
''', (post_id, title, content, post_type, author_id, tags_json, now, now))
|
|
conn.commit()
|
|
|
|
return post_id
|
|
|
|
def get_by_id(self, post_id):
|
|
with self.db.get_conn() as conn:
|
|
row = conn.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone()
|
|
if row:
|
|
post = dict(row)
|
|
post['tags'] = json.loads(post['tags'] or '[]')
|
|
post['likes'] = json.loads(post['likes'] or '[]')
|
|
return post
|
|
return None
|
|
|
|
def get_all(self, post_type=None, tag=None, page=1, per_page=20):
|
|
with self.db.get_conn() as conn:
|
|
query = 'SELECT * FROM posts WHERE 1=1'
|
|
params = []
|
|
|
|
if post_type:
|
|
query += ' AND type = ?'
|
|
params.append(post_type)
|
|
|
|
query += ' ORDER BY is_pinned DESC, created_at DESC'
|
|
|
|
rows = conn.execute(query, params).fetchall()
|
|
posts = []
|
|
for row in rows:
|
|
post = dict(row)
|
|
post['tags'] = json.loads(post['tags'] or '[]')
|
|
post['likes'] = json.loads(post['likes'] or '[]')
|
|
posts.append(post)
|
|
|
|
# 分页
|
|
start = (page - 1) * per_page
|
|
return posts[start:start + per_page], len(posts)
|
|
|
|
def increment_views(self, post_id):
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('UPDATE posts SET views = views + 1 WHERE id = ?', (post_id,))
|
|
conn.commit()
|
|
|
|
def add_like(self, post_id, user_id):
|
|
with self.db.get_conn() as conn:
|
|
post = self.get_by_id(post_id)
|
|
likes = post['likes']
|
|
|
|
if user_id in likes:
|
|
likes.remove(user_id)
|
|
liked = False
|
|
else:
|
|
likes.append(user_id)
|
|
liked = True
|
|
|
|
conn.execute('UPDATE posts SET likes = ? WHERE id = ?', (json.dumps(likes), post_id))
|
|
conn.commit()
|
|
|
|
return liked, len(likes)
|
|
|
|
def delete(self, post_id):
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('DELETE FROM replies WHERE post_id = ?', (post_id,))
|
|
conn.execute('DELETE FROM posts WHERE id = ?', (post_id,))
|
|
conn.commit()
|
|
|
|
def toggle_pin(self, post_id):
|
|
with self.db.get_conn() as conn:
|
|
row = conn.execute('SELECT is_pinned FROM posts WHERE id = ?', (post_id,)).fetchone()
|
|
new_pin = 1 if row['is_pinned'] == 0 else 0
|
|
conn.execute('UPDATE posts SET is_pinned = ? WHERE id = ?', (new_pin, post_id))
|
|
conn.commit()
|
|
return new_pin
|
|
|
|
def get_tags_stats(self):
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT tags FROM posts').fetchall()
|
|
tag_counts = {}
|
|
for row in rows:
|
|
tags = json.loads(row['tags'] or '[]')
|
|
for tag in tags:
|
|
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
return sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
class ReplyModel:
|
|
def __init__(self, db):
|
|
self.db = db
|
|
|
|
def create(self, post_id, content, author_id, reply_to=None):
|
|
reply_id = str(uuid.uuid4())
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO replies (id, post_id, content, author_id, likes, reply_to, created_at)
|
|
VALUES (?, ?, ?, ?, '[]', ?, ?)
|
|
''', (reply_id, post_id, content, author_id, reply_to, now))
|
|
conn.commit()
|
|
|
|
return reply_id
|
|
|
|
def get_by_post(self, post_id):
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT * FROM replies WHERE post_id = ? ORDER BY created_at', (post_id,)).fetchall()
|
|
replies = []
|
|
for row in rows:
|
|
reply = dict(row)
|
|
reply['likes'] = json.loads(reply['likes'] or '[]')
|
|
replies.append(reply)
|
|
return replies
|
|
|
|
def delete(self, reply_id):
|
|
"""删除回复"""
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('DELETE FROM replies WHERE id = ?', (reply_id,))
|
|
conn.commit()
|
|
return True
|
|
|
|
def get_all(self, limit=50):
|
|
"""获取所有回复(后台管理用)"""
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT * FROM replies ORDER BY created_at DESC LIMIT ?', (limit,)).fetchall()
|
|
replies = []
|
|
for row in rows:
|
|
reply = dict(row)
|
|
reply['likes'] = json.loads(reply['likes'] or '[]')
|
|
replies.append(reply)
|
|
return replies
|
|
|
|
|
|
class TopicModel:
|
|
def __init__(self, db):
|
|
self.db = db
|
|
|
|
def create(self, name, description, icon, author_id):
|
|
topic_id = str(uuid.uuid4())
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO topics (id, name, description, icon, author_id, followers, created_at)
|
|
VALUES (?, ?, ?, ?, ?, '[]', ?)
|
|
''', (topic_id, name, description, icon, author_id, now))
|
|
conn.commit()
|
|
|
|
return topic_id
|
|
|
|
def get_by_id(self, topic_id):
|
|
with self.db.get_conn() as conn:
|
|
row = conn.execute('SELECT * FROM topics WHERE id = ?', (topic_id,)).fetchone()
|
|
if row:
|
|
topic = dict(row)
|
|
topic['followers'] = json.loads(topic['followers'] or '[]')
|
|
return topic
|
|
return None
|
|
|
|
def get_all(self):
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT * FROM topics ORDER BY created_at DESC').fetchall()
|
|
topics = []
|
|
for row in rows:
|
|
topic = dict(row)
|
|
topic['followers'] = json.loads(topic['followers'] or '[]')
|
|
topics.append(topic)
|
|
return topics
|
|
|
|
def delete(self, topic_id):
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('DELETE FROM answers WHERE question_id IN (SELECT id FROM questions WHERE topic_id = ?)', (topic_id,))
|
|
conn.execute('DELETE FROM questions WHERE topic_id = ?', (topic_id,))
|
|
conn.execute('DELETE FROM sub_topics WHERE topic_id = ?', (topic_id,))
|
|
conn.execute('DELETE FROM topics WHERE id = ?', (topic_id,))
|
|
conn.commit()
|
|
|
|
def add_follower(self, topic_id, user_id):
|
|
with self.db.get_conn() as conn:
|
|
topic = self.get_by_id(topic_id)
|
|
followers = topic['followers']
|
|
|
|
if user_id in followers:
|
|
followers.remove(user_id)
|
|
followed = False
|
|
else:
|
|
followers.append(user_id)
|
|
followed = True
|
|
|
|
conn.execute('UPDATE topics SET followers = ? WHERE id = ?', (json.dumps(followers), topic_id))
|
|
conn.commit()
|
|
|
|
return followed, len(followers)
|
|
|
|
def get_sub_topics(self, topic_id):
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT * FROM sub_topics WHERE topic_id = ? ORDER BY created_at', (topic_id,)).fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
def add_sub_topic(self, topic_id, title, content, author_id):
|
|
sub_id = str(uuid.uuid4())
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO sub_topics (id, topic_id, title, content, author_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', (sub_id, topic_id, title, content, author_id, now))
|
|
conn.commit()
|
|
|
|
return sub_id
|
|
|
|
def get_questions(self, topic_id):
|
|
with self.db.get_conn() as conn:
|
|
rows = conn.execute('SELECT * FROM questions WHERE topic_id = ? ORDER BY created_at DESC', (topic_id,)).fetchall()
|
|
questions = []
|
|
for row in rows:
|
|
q = dict(row)
|
|
# 获取回答
|
|
ans_rows = conn.execute('SELECT * FROM answers WHERE question_id = ? ORDER BY created_at', (q['id'],)).fetchall()
|
|
answers = []
|
|
for ans in ans_rows:
|
|
a = dict(ans)
|
|
a['likes'] = json.loads(a['likes'] or '[]')
|
|
answers.append(a)
|
|
q['answers'] = answers
|
|
questions.append(q)
|
|
return questions
|
|
|
|
def add_question(self, topic_id, title, content, author_id):
|
|
q_id = str(uuid.uuid4())
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO questions (id, topic_id, title, content, author_id, views, created_at)
|
|
VALUES (?, ?, ?, ?, ?, 0, ?)
|
|
''', (q_id, topic_id, title, content, author_id, now))
|
|
conn.commit()
|
|
|
|
return q_id
|
|
|
|
def add_answer(self, question_id, content, author_id):
|
|
ans_id = str(uuid.uuid4())
|
|
now = datetime.datetime.now().isoformat()
|
|
|
|
with self.db.get_conn() as conn:
|
|
conn.execute('''
|
|
INSERT INTO answers (id, question_id, content, author_id, likes, created_at)
|
|
VALUES (?, ?, ?, ?, '[]', ?)
|
|
''', (ans_id, question_id, content, author_id, now))
|
|
conn.commit()
|
|
|
|
return ans_id |