feat: v1.1.0 安全重构

- 后台添加登录验证(Session + JWT双重验证)
- JSON存储改为SQLite数据库,解决并发问题
- API密钥移至config.py,支持环境变量覆盖
- SECRET_KEY改为随机生成
- 新增管理员登录页面
- 修复README.md乱码
- 更新.gitignore忽略敏感配置
This commit is contained in:
2026-04-12 16:56:35 +08:00
parent 301c286b8e
commit cb4b7d5363
8 changed files with 972 additions and 521 deletions

View File

@@ -1,63 +1,31 @@
"""
技术论坛与技术分享网站 - 后端API
技术论坛与技术分享网站 - 后端API (重构版)
"""
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
import datetime
import json
import uuid
import os
from pathlib import Path
# 导入配置和模型
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import SECRET_KEY, LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, DATABASE_PATH, BACKEND_PORT
from models import Database, UserModel, PostModel, ReplyModel, TopicModel
app = Flask(__name__, static_folder='../frontend', static_url_path='')
CORS(app)
# 配置
SECRET_KEY = 'tech-forum-secret-2026'
LLM_BASE_URL = 'http://192.168.2.5:1234/v1'
LLM_API_KEY = 'sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j'
LLM_MODEL = 'qwen3.5-4b'
# 初始化数据库
db = Database(DATABASE_PATH)
user_model = UserModel(db)
post_model = PostModel(db)
reply_model = ReplyModel(db)
topic_model = TopicModel(db)
# 数据目录
DATA_DIR = Path(__file__).parent.parent / 'data'
USERS_FILE = DATA_DIR / 'users.json'
POSTS_FILE = DATA_DIR / 'posts.json'
TOPICS_FILE = DATA_DIR / 'topics.json'
UPLOAD_DIR = Path(__file__).parent.parent / 'uploads'
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# 初始化数据文件
def init_data():
if not USERS_FILE.exists():
USERS_FILE.write_text(json.dumps({}, ensure_ascii=False))
if not POSTS_FILE.exists():
POSTS_FILE.write_text(json.dumps({}, ensure_ascii=False))
if not TOPICS_FILE.exists():
TOPICS_FILE.write_text(json.dumps({}, ensure_ascii=False))
init_data()
# 辅助函数
def load_users():
return json.loads(USERS_FILE.read_text(encoding='utf-8'))
def save_users(users):
USERS_FILE.write_text(json.dumps(users, ensure_ascii=False, indent=2), encoding='utf-8')
def load_posts():
return json.loads(POSTS_FILE.read_text(encoding='utf-8'))
def save_posts(posts):
POSTS_FILE.write_text(json.dumps(posts, ensure_ascii=False, indent=2), encoding='utf-8')
def load_topics():
return json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
def save_topics(topics):
TOPICS_FILE.write_text(json.dumps(topics, ensure_ascii=False, indent=2), encoding='utf-8')
# ============ 辅助函数 ============
def generate_token(user_id):
return jwt.encode({
@@ -78,8 +46,7 @@ def get_current_user():
data = verify_token(token)
if not data:
return None
users = load_users()
return users.get(data['user_id'])
return user_model.get_by_id(data['user_id'])
# ============ 页面路由 ============
@@ -133,32 +100,15 @@ def api_register():
if password != confirm_password:
return jsonify({'error': '两次密码不一致'}), 400
users = load_users()
# 检查是否已存在
for uid, user in users.items():
if user['username'] == username:
return jsonify({'error': '用户名已存在'}), 400
if user['email'] == email:
return jsonify({'error': '邮箱已注册'}), 400
if user_model.get_by_username(username):
return jsonify({'error': '用户名已存在'}), 400
if user_model.get_by_email(email):
return jsonify({'error': '邮箱已注册'}), 400
# 创建用户
user_id = str(uuid.uuid4())
users[user_id] = {
'id': user_id,
'username': username,
'email': email,
'phone': phone,
'password': generate_password_hash(password),
'avatar': f'https://api.dicebear.com/7.x/avataaars/svg?seed={username}',
'bio': '',
'created_at': datetime.datetime.now().isoformat(),
'posts': [],
'replies': [],
'likes': []
}
save_users(users)
user_id = user_model.create(username, email, phone, password)
user = user_model.get_by_id(user_id)
token = generate_token(user_id)
@@ -169,7 +119,7 @@ def api_register():
'id': user_id,
'username': username,
'email': email,
'avatar': users[user_id]['avatar']
'avatar': user['avatar']
}
})
@@ -183,27 +133,30 @@ def api_login():
if not login_name or not password:
return jsonify({'error': '请输入用户名和密码'}), 400
users = load_users()
# 查找用户
user = user_model.get_by_username(login_name)
if not user:
user = user_model.get_by_email(login_name)
for user_id, user in users.items():
if user['username'] == login_name or user['email'] == login_name:
if check_password_hash(user['password'], password):
token = generate_token(user_id)
return jsonify({
'success': True,
'token': token,
'user': {
'id': user_id,
'username': user['username'],
'email': user['email'],
'avatar': user['avatar'],
'bio': user.get('bio', '')
}
})
else:
return jsonify({'error': '密码错误'}), 400
if not user:
return jsonify({'error': '用户不存在'}), 400
return jsonify({'error': '用户不存在'}), 400
if not user_model.verify_password(user, password):
return jsonify({'error': '密码错误'}), 400
token = generate_token(user['id'])
return jsonify({
'success': True,
'token': token,
'user': {
'id': user['id'],
'username': user['username'],
'email': user['email'],
'avatar': user['avatar'],
'bio': user.get('bio', '')
}
})
@app.route('/api/user')
def api_current_user():
@@ -218,32 +171,28 @@ def api_current_user():
'phone': user.get('phone', ''),
'avatar': user['avatar'],
'bio': user.get('bio', ''),
'posts_count': len(user.get('posts', [])),
'replies_count': len(user.get('replies', [])),
'posts_count': user_model.get_posts_count(user['id']),
'replies_count': user_model.get_replies_count(user['id']),
'created_at': user['created_at']
})
@app.route('/api/user/<user_id>')
def api_user_profile(user_id):
users = load_users()
posts = load_posts()
topics = load_topics()
user = users.get(user_id)
user = user_model.get_by_id(user_id)
if not user:
return jsonify({'error': '用户不存在'}), 404
# 获取用户的帖子
posts, total = post_model.get_all()
user_posts = []
for post_id in user.get('posts', []):
if post_id in posts:
post = posts[post_id]
for post in posts:
if post['author_id'] == user_id:
user_posts.append({
'id': post_id,
'id': post['id'],
'title': post['title'],
'type': post['type'],
'likes': len(post.get('likes', [])),
'replies': len(post.get('replies', [])),
'likes': len(post['likes']),
'replies': len(reply_model.get_by_post(post['id'])),
'created_at': post['created_at']
})
@@ -253,8 +202,8 @@ def api_user_profile(user_id):
'username': user['username'],
'avatar': user['avatar'],
'bio': user.get('bio', ''),
'posts_count': len(user.get('posts', [])),
'replies_count': len(user.get('replies', [])),
'posts_count': user_model.get_posts_count(user['id']),
'replies_count': user_model.get_replies_count(user['id']),
'created_at': user['created_at']
},
'posts': user_posts
@@ -264,24 +213,18 @@ def api_user_profile(user_id):
@app.route('/api/posts')
def api_posts():
posts = load_posts()
users = load_users()
post_type = request.args.get('type') # discussion, share
post_type = request.args.get('type')
tag = request.args.get('tag')
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 20))
posts, total = post_model.get_all(post_type, tag, page, per_page)
post_list = []
for pid, post in posts.items():
if post_type and post['type'] != post_type:
continue
if tag and tag not in post.get('tags', []):
continue
author = users.get(post['author_id'], {})
for post in posts:
author = user_model.get_by_id(post['author_id']) or {}
post_list.append({
'id': pid,
'id': post['id'],
'title': post['title'],
'type': post['type'],
'content_preview': post['content'][:150] + '...' if len(post['content']) > 150 else post['content'],
@@ -290,24 +233,17 @@ def api_posts():
'username': author.get('username', '未知'),
'avatar': author.get('avatar', '')
},
'tags': post.get('tags', []),
'likes': len(post.get('likes', [])),
'replies': len(post.get('replies', [])),
'views': post.get('views', 0),
'tags': post['tags'],
'likes': len(post['likes']),
'replies': len(reply_model.get_by_post(post['id'])),
'views': post['views'],
'created_at': post['created_at'],
'is_pinned': post.get('is_pinned', False)
'is_pinned': post['is_pinned']
})
# 排序:置顶在前,然后按时间
post_list.sort(key=lambda x: (not x['is_pinned'], x['created_at']), reverse=True)
# 分页
start = (page - 1) * per_page
end = start + per_page
return jsonify({
'posts': post_list[start:end],
'total': len(post_list),
'posts': post_list,
'total': total,
'page': page,
'per_page': per_page
})
@@ -322,7 +258,7 @@ def api_create_post():
title = data.get('title', '').strip()
content = data.get('content', '').strip()
post_type = data.get('type', 'discussion') # discussion, share
post_type = data.get('type', 'discussion')
tags = data.get('tags', [])
if not title or len(title) < 5:
@@ -330,31 +266,7 @@ def api_create_post():
if not content or len(content) < 10:
return jsonify({'error': '内容至少10个字符'}), 400
posts = load_posts()
users = load_users()
post_id = str(uuid.uuid4())
posts[post_id] = {
'id': post_id,
'title': title,
'content': content,
'type': post_type,
'author_id': user['id'],
'tags': tags,
'likes': [],
'replies': [],
'views': 0,
'is_pinned': False,
'created_at': datetime.datetime.now().isoformat(),
'updated_at': datetime.datetime.now().isoformat()
}
save_posts(posts)
# 更新用户的帖子列表
if post_id not in users[user['id']]['posts']:
users[user['id']]['posts'].append(post_id)
save_users(users)
post_id = post_model.create(title, content, post_type, user['id'], tags)
return jsonify({
'success': True,
@@ -363,24 +275,22 @@ def api_create_post():
@app.route('/api/posts/<post_id>')
def api_post_detail(post_id):
posts = load_posts()
users = load_users()
post = posts.get(post_id)
post = post_model.get_by_id(post_id)
if not post:
return jsonify({'error': '帖子不存在'}), 404
# 增加浏览量
post['views'] = post.get('views', 0) + 1
save_posts(posts)
post_model.increment_views(post_id)
post['views'] += 1
author = users.get(post['author_id'], {})
author = user_model.get_by_id(post['author_id']) or {}
# 获取回复
replies = []
for reply in post.get('replies', []):
reply_author = users.get(reply['author_id'], {})
replies.append({
replies = reply_model.get_by_post(post_id)
reply_list = []
for reply in replies:
reply_author = user_model.get_by_id(reply['author_id']) or {}
reply_list.append({
'id': reply['id'],
'content': reply['content'],
'author': {
@@ -388,7 +298,7 @@ def api_post_detail(post_id):
'username': reply_author.get('username', '未知'),
'avatar': reply_author.get('avatar', '')
},
'likes': len(reply.get('likes', [])),
'likes': len(reply['likes']),
'created_at': reply['created_at'],
'reply_to': reply.get('reply_to')
})
@@ -404,10 +314,10 @@ def api_post_detail(post_id):
'avatar': author.get('avatar', ''),
'bio': author.get('bio', '')
},
'tags': post.get('tags', []),
'likes': len(post.get('likes', [])),
'tags': post['tags'],
'likes': len(post['likes']),
'views': post['views'],
'replies': replies,
'replies': reply_list,
'created_at': post['created_at'],
'updated_at': post.get('updated_at', post['created_at'])
})
@@ -420,36 +330,16 @@ def api_reply_post(post_id):
data = request.json
content = data.get('content', '').strip()
reply_to = data.get('reply_to') # 回复的评论ID
reply_to = data.get('reply_to')
if not content:
return jsonify({'error': '回复内容不能为空'}), 400
posts = load_posts()
users = load_users()
post = posts.get(post_id)
post = post_model.get_by_id(post_id)
if not post:
return jsonify({'error': '帖子不存在'}), 404
reply_id = str(uuid.uuid4())
reply = {
'id': reply_id,
'content': content,
'author_id': user['id'],
'likes': [],
'reply_to': reply_to,
'created_at': datetime.datetime.now().isoformat()
}
post['replies'].append(reply)
post['updated_at'] = datetime.datetime.now().isoformat()
save_posts(posts)
# 更新用户的回复列表
if post_id not in users[user['id']]['replies']:
users[user['id']]['replies'].append(post_id)
save_users(users)
reply_id = reply_model.create(post_id, content, user['id'], reply_to)
return jsonify({
'success': True,
@@ -462,39 +352,29 @@ def api_like_post(post_id):
if not user:
return jsonify({'error': '请先登录'}), 401
posts = load_posts()
post = posts.get(post_id)
post = post_model.get_by_id(post_id)
if not post:
return jsonify({'error': '帖子不存在'}), 404
if user['id'] in post['likes']:
post['likes'].remove(user['id'])
liked = False
else:
post['likes'].append(user['id'])
liked = True
save_posts(posts)
liked, likes_count = post_model.add_like(post_id, user['id'])
return jsonify({
'success': True,
'liked': liked,
'likes_count': len(post['likes'])
'likes_count': likes_count
})
# ============ API: 工具分享主题 ============
@app.route('/api/topics')
def api_topics():
topics = load_topics()
users = load_users()
topics = topic_model.get_all()
topic_list = []
for tid, topic in topics.items():
author = users.get(topic['author_id'], {})
for topic in topics:
author = user_model.get_by_id(topic['author_id']) or {}
topic_list.append({
'id': tid,
'id': topic['id'],
'name': topic['name'],
'description': topic['description'][:100] + '...' if len(topic.get('description', '')) > 100 else topic.get('description', ''),
'icon': topic.get('icon', '🔧'),
@@ -502,9 +382,9 @@ def api_topics():
'id': topic['author_id'],
'username': author.get('username', '未知')
},
'sub_topics_count': len(topic.get('sub_topics', [])),
'questions_count': len(topic.get('questions', [])),
'followers': len(topic.get('followers', [])),
'sub_topics_count': len(topic_model.get_sub_topics(topic['id'])),
'questions_count': len(topic_model.get_questions(topic['id'])),
'followers': len(topic['followers']),
'created_at': topic['created_at']
})
@@ -527,22 +407,7 @@ def api_create_topic():
if not name:
return jsonify({'error': '主题名称不能为空'}), 400
topics = load_topics()
topic_id = str(uuid.uuid4())
topics[topic_id] = {
'id': topic_id,
'name': name,
'description': description,
'icon': icon,
'author_id': user['id'],
'sub_topics': [],
'questions': [],
'followers': [],
'created_at': datetime.datetime.now().isoformat()
}
save_topics(topics)
topic_id = topic_model.create(name, description, icon, user['id'])
return jsonify({
'success': True,
@@ -551,33 +416,33 @@ def api_create_topic():
@app.route('/api/topics/<topic_id>')
def api_topic_detail(topic_id):
topics = load_topics()
users = load_users()
topic = topics.get(topic_id)
topic = topic_model.get_by_id(topic_id)
if not topic:
return jsonify({'error': '主题不存在'}), 404
author = users.get(topic['author_id'], {})
author = user_model.get_by_id(topic['author_id']) or {}
# 获取子主题
sub_topics = []
for st in topic.get('sub_topics', []):
sub_topics.append({
sub_topics = topic_model.get_sub_topics(topic_id)
sub_list = []
for st in sub_topics:
st_author = user_model.get_by_id(st['author_id']) or {}
sub_list.append({
'id': st['id'],
'title': st['title'],
'content': st['content'],
'author': users.get(st['author_id'], {}),
'author': st_author,
'created_at': st['created_at']
})
# 获取问题
questions = []
for q in topic.get('questions', []):
q_author = users.get(q['author_id'], {})
questions = topic_model.get_questions(topic_id)
q_list = []
for q in questions:
q_author = user_model.get_by_id(q['author_id']) or {}
answers = []
for a in q.get('answers', []):
a_author = users.get(a['author_id'], {})
for a in q['answers']:
a_author = user_model.get_by_id(a['author_id']) or {}
answers.append({
'id': a['id'],
'content': a['content'],
@@ -586,10 +451,10 @@ def api_topic_detail(topic_id):
'username': a_author.get('username', '未知'),
'avatar': a_author.get('avatar', '')
},
'likes': len(a.get('likes', [])),
'likes': len(a['likes']),
'created_at': a['created_at']
})
questions.append({
q_list.append({
'id': q['id'],
'title': q['title'],
'content': q.get('content', ''),
@@ -613,9 +478,9 @@ def api_topic_detail(topic_id):
'username': author.get('username', '未知'),
'avatar': author.get('avatar', '')
},
'sub_topics': sub_topics,
'questions': questions,
'followers': len(topic.get('followers', [])),
'sub_topics': sub_list,
'questions': q_list,
'followers': len(topic['followers']),
'created_at': topic['created_at']
})
@@ -632,24 +497,13 @@ def api_add_subtopic(topic_id):
if not title:
return jsonify({'error': '标题不能为空'}), 400
topics = load_topics()
topic = topics.get(topic_id)
topic = topic_model.get_by_id(topic_id)
if not topic:
return jsonify({'error': '主题不存在'}), 404
subtopic = {
'id': str(uuid.uuid4()),
'title': title,
'content': content,
'author_id': user['id'],
'created_at': datetime.datetime.now().isoformat()
}
subtopic_id = topic_model.add_sub_topic(topic_id, title, content, user['id'])
topic['sub_topics'].append(subtopic)
save_topics(topics)
return jsonify({'success': True, 'subtopic_id': subtopic['id']})
return jsonify({'success': True, 'subtopic_id': subtopic_id})
@app.route('/api/topics/<topic_id>/question', methods=['POST'])
def api_add_question(topic_id):
@@ -664,26 +518,13 @@ def api_add_question(topic_id):
if not title:
return jsonify({'error': '问题标题不能为空'}), 400
topics = load_topics()
topic = topics.get(topic_id)
topic = topic_model.get_by_id(topic_id)
if not topic:
return jsonify({'error': '主题不存在'}), 404
question = {
'id': str(uuid.uuid4()),
'title': title,
'content': content,
'author_id': user['id'],
'answers': [],
'views': 0,
'created_at': datetime.datetime.now().isoformat()
}
question_id = topic_model.add_question(topic_id, title, content, user['id'])
topic['questions'].append(question)
save_topics(topics)
return jsonify({'success': True, 'question_id': question['id']})
return jsonify({'success': True, 'question_id': question_id})
@app.route('/api/topics/<topic_id>/question/<question_id>/answer', methods=['POST'])
def api_answer_question(topic_id, question_id):
@@ -697,34 +538,13 @@ def api_answer_question(topic_id, question_id):
if not content:
return jsonify({'error': '回答内容不能为空'}), 400
topics = load_topics()
topic = topics.get(topic_id)
topic = topic_model.get_by_id(topic_id)
if not topic:
return jsonify({'error': '主题不存在'}), 404
# 找到问题
question = None
for q in topic['questions']:
if q['id'] == question_id:
question = q
break
answer_id = topic_model.add_answer(question_id, content, user['id'])
if not question:
return jsonify({'error': '问题不存在'}), 404
answer = {
'id': str(uuid.uuid4()),
'content': content,
'author_id': user['id'],
'likes': [],
'created_at': datetime.datetime.now().isoformat()
}
question['answers'].append(answer)
save_topics(topics)
return jsonify({'success': True, 'answer_id': answer['id']})
return jsonify({'success': True, 'answer_id': answer_id})
@app.route('/api/topics/<topic_id>/follow', methods=['POST'])
def api_follow_topic(topic_id):
@@ -732,40 +552,23 @@ def api_follow_topic(topic_id):
if not user:
return jsonify({'error': '请先登录'}), 401
topics = load_topics()
topic = topics.get(topic_id)
topic = topic_model.get_by_id(topic_id)
if not topic:
return jsonify({'error': '主题不存在'}), 404
if user['id'] in topic['followers']:
topic['followers'].remove(user['id'])
followed = False
else:
topic['followers'].append(user['id'])
followed = True
save_topics(topics)
followed, followers_count = topic_model.add_follower(topic_id, user['id'])
return jsonify({
'success': True,
'followed': followed,
'followers_count': len(topic['followers'])
'followers_count': followers_count
})
# ============ API: 标签 ============
@app.route('/api/tags')
def api_tags():
posts = load_posts()
tag_counts = {}
for post in posts.values():
for tag in post.get('tags', []):
tag_counts[tag] = tag_counts.get(tag, 0) + 1
tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
tags = post_model.get_tags_stats()
return jsonify([{'name': t[0], 'count': t[1]} for t in tags])
# ============ API: 搜索 ============
@@ -777,17 +580,14 @@ def api_search():
if not query:
return jsonify({'posts': [], 'topics': []})
posts = load_posts()
topics = load_topics()
users = load_users()
# 搜索帖子
posts, _ = post_model.get_all()
matched_posts = []
for pid, post in posts.items():
for post in posts:
if query in post['title'].lower() or query in post['content'].lower():
author = users.get(post['author_id'], {})
author = user_model.get_by_id(post['author_id']) or {}
matched_posts.append({
'id': pid,
'id': post['id'],
'title': post['title'],
'type': post['type'],
'author': author.get('username', '未知'),
@@ -795,11 +595,12 @@ def api_search():
})
# 搜索主题
topics = topic_model.get_all()
matched_topics = []
for tid, topic in topics.items():
for topic in topics:
if query in topic['name'].lower() or query in topic.get('description', '').lower():
matched_topics.append({
'id': tid,
'id': topic['id'],
'name': topic['name'],
'icon': topic.get('icon', '🔧')
})
@@ -809,11 +610,17 @@ def api_search():
'topics': matched_topics[:20]
})
# ============ API: 健康检查 ============
@app.route('/api/health')
def api_health():
return jsonify({'status': 'ok', 'service': 'tech-forum', 'port': BACKEND_PORT})
if __name__ == '__main__':
print("=" * 50)
print("技术论坛与技术分享网站")
print("=" * 50)
print(f"访问地址: http://localhost:19004")
print(f"访问地址: http://localhost:{BACKEND_PORT}")
print("=" * 50)
app.run(host='0.0.0.0', port=19004, debug=True)
app.run(host='0.0.0.0', port=BACKEND_PORT, debug=True)