Files
tech-forum/models.py
hubian cb4b7d5363 feat: v1.1.0 安全重构
- 后台添加登录验证(Session + JWT双重验证)
- JSON存储改为SQLite数据库,解决并发问题
- API密钥移至config.py,支持环境变量覆盖
- SECRET_KEY改为随机生成
- 新增管理员登录页面
- 修复README.md乱码
- 更新.gitignore忽略敏感配置
2026-04-12 16:56:35 +08:00

442 lines
16 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,
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
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