- 发布后24小时内可编辑 - 最多修改5次 - 编辑按钮仅作者可见 - 显示编辑次数和剩余次数 - 数据库新增 edit_count 和 last_edit_at 字段
527 lines
19 KiB
Python
527 lines
19 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,
|
||
edit_count INTEGER DEFAULT 0,
|
||
last_edit_at TEXT,
|
||
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 can_edit(self, post_id, user_id):
|
||
"""检查用户是否可以编辑帖子"""
|
||
post = self.get_by_id(post_id)
|
||
if not post:
|
||
return False, '帖子不存在'
|
||
|
||
# 检查是否是作者
|
||
if post['author_id'] != user_id:
|
||
return False, '只能编辑自己的帖子'
|
||
|
||
# 检查时间限制(24小时内)
|
||
created_time = datetime.datetime.fromisoformat(post['created_at'])
|
||
now = datetime.datetime.now()
|
||
hours_passed = (now - created_time).total_seconds() / 3600
|
||
|
||
if hours_passed > 24:
|
||
return False, '超过24小时,无法编辑'
|
||
|
||
# 检查修改次数限制(最多5次)
|
||
edit_count = post.get('edit_count', 0) or 0
|
||
if edit_count >= 5:
|
||
return False, '已达到最大修改次数(5次)'
|
||
|
||
return True, None
|
||
|
||
def update_by_user(self, post_id, user_id, title=None, content=None, tags=None):
|
||
"""用户编辑帖子(带限制检查)"""
|
||
can_edit, error = self.can_edit(post_id, user_id)
|
||
if not can_edit:
|
||
return False, error
|
||
|
||
now = datetime.datetime.now().isoformat()
|
||
|
||
with self.db.get_conn() as conn:
|
||
# 获取当前修改次数
|
||
row = conn.execute('SELECT edit_count FROM posts WHERE id = ?', (post_id,)).fetchone()
|
||
edit_count = (row['edit_count'] or 0) + 1
|
||
|
||
update_fields = []
|
||
values = []
|
||
|
||
if title:
|
||
update_fields.append('title = ?')
|
||
values.append(title)
|
||
if content:
|
||
update_fields.append('content = ?')
|
||
values.append(content)
|
||
if tags:
|
||
update_fields.append('tags = ?')
|
||
values.append(json.dumps(tags))
|
||
|
||
update_fields.append('edit_count = ?')
|
||
values.append(edit_count)
|
||
update_fields.append('last_edit_at = ?')
|
||
values.append(now)
|
||
update_fields.append('updated_at = ?')
|
||
values.append(now)
|
||
values.append(post_id)
|
||
|
||
conn.execute(f"UPDATE posts SET {', '.join(update_fields)} WHERE id = ?", values)
|
||
conn.commit()
|
||
|
||
return True, None
|
||
|
||
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 |