Files
tech-forum/models.py
hubian b768424519 feat: 帖子编辑功能(用户限制)
- 发布后24小时内可编辑
- 最多修改5次
- 编辑按钮仅作者可见
- 显示编辑次数和剩余次数
- 数据库新增 edit_count 和 last_edit_at 字段
2026-04-12 18:44:43 +08:00

527 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
技术论坛数据库模型 - 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