From 47bf6f48d5c3592dc959bdbd4c01a1f102c60c16 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Sun, 12 Apr 2026 17:05:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=88=E5=B9=B6=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=88=B0=E4=B8=BB=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=8F=A319004?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后台管理整合到 /admin 路径 - 前台保持原有路由 - 统一API路径(后台 /admin/api/xxx) - 删除独立admin服务(保留admin目录作为模板) - 简化部署,只需启动一个服务 --- admin/templates/index.html | 35 ++++- admin/templates/login.html | 4 +- backend/app.py | 310 +++++++++++++++++++++++++++++++++++-- 3 files changed, 328 insertions(+), 21 deletions(-) diff --git a/admin/templates/index.html b/admin/templates/index.html index 953a882..c6e3ea4 100644 --- a/admin/templates/index.html +++ b/admin/templates/index.html @@ -21,23 +21,26 @@
- + 访问前台 +
@@ -140,8 +143,24 @@ - \ No newline at end of file +> \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index 7bca900..5b24b49 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,9 +1,11 @@ """ -技术论坛与技术分享网站 - 后端API (重构版) +技术论坛与技术分享网站 - 后端API (整合版) +主服务 + 后台管理 统一端口 19004 """ -from flask import Flask, request, jsonify, send_file +from flask import Flask, request, jsonify, send_file, render_template, redirect, session from flask_cors import CORS +from functools import wraps import jwt import datetime import os @@ -12,11 +14,15 @@ 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 config import SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD, DATABASE_PATH, BACKEND_PORT from models import Database, UserModel, PostModel, ReplyModel, TopicModel -app = Flask(__name__, static_folder='../frontend', static_url_path='') +app = Flask(__name__, + static_folder='../frontend', + static_url_path='', + template_folder='../admin/templates') CORS(app) +app.secret_key = SECRET_KEY # 初始化数据库 db = Database(DATABASE_PATH) @@ -48,7 +54,30 @@ def get_current_user(): return None return user_model.get_by_id(data['user_id']) -# ============ 页面路由 ============ +# 后台管理员验证装饰器 +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # 检查 session + if not session.get('admin_logged_in'): + # 检查 Authorization header + token = request.headers.get('Authorization', '').replace('Bearer ', '') + if token: + try: + data = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + if data.get('admin'): + return f(*args, **kwargs) + except: + pass + + # API请求返回401,页面请求跳转登录 + if request.path.startswith('/admin/api/'): + return jsonify({'error': '请先登录', 'code': 401}), 401 + return redirect('/admin/login') + return f(*args, **kwargs) + return decorated_function + +# ============ 前台页面路由 ============ @app.route('/') def index(): @@ -78,7 +107,266 @@ def user_page(user_id): def create_page(): return send_file('../frontend/create.html') -# ============ API: 用户认证 ============ +# ============ 后台管理页面路由 ============ + +@app.route('/admin/login') +def admin_login_page(): + return render_template('login.html') + +@app.route('/admin') +@admin_required +def admin_index(): + return render_template('index.html') + +@app.route('/admin/users') +@admin_required +def admin_users_page(): + return render_template('users.html') + +@app.route('/admin/posts') +@admin_required +def admin_posts_page(): + return render_template('posts.html') + +@app.route('/admin/topics') +@admin_required +def admin_topics_page(): + return render_template('topics.html') + +# ============ 后台管理 API ============ + +@app.route('/admin/api/login', methods=['POST']) +def admin_api_login(): + data = request.json + + username = data.get('username', '').strip() + password = data.get('password', '') + + if not username or not password: + return jsonify({'error': '请输入用户名和密码'}), 400 + + if username != ADMIN_USERNAME or password != ADMIN_PASSWORD: + return jsonify({'error': '用户名或密码错误'}), 400 + + # 设置session + session['admin_logged_in'] = True + session['admin_username'] = username + + # 生成token + token = jwt.encode({ + 'admin': True, + 'username': username, + 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) + }, SECRET_KEY, algorithm='HS256') + + return jsonify({ + 'success': True, + 'token': token, + 'message': '登录成功' + }) + +@app.route('/admin/api/logout', methods=['POST']) +def admin_api_logout(): + session.pop('admin_logged_in', None) + session.pop('admin_username', None) + return jsonify({'success': True, 'message': '已退出登录'}) + +@app.route('/admin/api/check-auth') +def admin_api_check_auth(): + if session.get('admin_logged_in'): + return jsonify({ + 'logged_in': True, + 'username': session.get('admin_username') + }) + return jsonify({'logged_in': False}) + +@app.route('/admin/api/stats') +@admin_required +def admin_api_stats(): + users = user_model.get_all() + posts, posts_total = post_model.get_all() + topics = topic_model.get_all() + + # 统计回复数 + total_replies = 0 + for post in posts: + total_replies += len(reply_model.get_by_post(post['id'])) + + today = datetime.datetime.now().strftime('%Y-%m-%d') + today_posts = sum(1 for p in posts if p.get('created_at', '').startswith(today)) + today_users = sum(1 for u in users if u.get('created_at', '').startswith(today)) + + # 帖子类型统计 + discussion_count = sum(1 for p in posts if p.get('type') == 'discussion') + share_count = sum(1 for p in posts if p.get('type') == 'share') + + return jsonify({ + 'users_count': len(users), + 'posts_count': posts_total, + 'topics_count': len(topics), + 'messages_count': total_replies, + 'today_posts': today_posts, + 'today_users': today_users, + 'discussion_count': discussion_count, + 'share_count': share_count, + }) + +@app.route('/admin/api/users') +@admin_required +def admin_api_users(): + users = user_model.get_all() + + user_list = [] + for user in users: + user_list.append({ + 'id': user['id'], + 'username': user.get('username', ''), + 'email': user.get('email', ''), + 'phone': user.get('phone', ''), + 'posts_count': user_model.get_posts_count(user['id']), + 'replies_count': user_model.get_replies_count(user['id']), + 'created_at': user.get('created_at', ''), + }) + + return jsonify(user_list) + +@app.route('/admin/api/users/', methods=['DELETE']) +@admin_required +def admin_api_delete_user(user_id): + user_model.delete(user_id) + return jsonify({'success': True}) + +@app.route('/admin/api/posts') +@admin_required +def admin_api_posts(): + post_type = request.args.get('type') + + posts, total = post_model.get_all(post_type) + + post_list = [] + for post in posts: + author = user_model.get_by_id(post['author_id']) or {} + post_list.append({ + 'id': post['id'], + 'title': post['title'], + 'type': post['type'], + 'author': author.get('username', '未知'), + 'author_id': post['author_id'], + 'likes': len(post['likes']), + 'replies': len(reply_model.get_by_post(post['id'])), + 'views': post['views'], + 'is_pinned': post['is_pinned'], + 'created_at': post['created_at'], + }) + + return jsonify(post_list) + +@app.route('/admin/api/posts/') +@admin_required +def admin_api_post_detail(post_id): + post = post_model.get_by_id(post_id) + if not post: + return jsonify({'error': '帖子不存在'}), 404 + + author = user_model.get_by_id(post['author_id']) or {} + + # 获取回复 + 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'][:100] + '...' if len(reply['content']) > 100 else reply['content'], + 'author': reply_author.get('username', '未知'), + 'likes': len(reply['likes']), + 'created_at': reply['created_at'], + }) + + return jsonify({ + 'id': post_id, + 'title': post['title'], + 'content': post['content'], + 'type': post['type'], + 'author': author.get('username', '未知'), + 'tags': post['tags'], + 'likes': len(post['likes']), + 'replies': reply_list, + 'views': post['views'], + 'is_pinned': post['is_pinned'], + 'created_at': post['created_at'], + }) + +@app.route('/admin/api/posts/', methods=['DELETE']) +@admin_required +def admin_api_delete_post(post_id): + post_model.delete(post_id) + return jsonify({'success': True}) + +@app.route('/admin/api/posts//pin', methods=['POST']) +@admin_required +def admin_api_pin_post(post_id): + new_pin = post_model.toggle_pin(post_id) + return jsonify({ + 'success': True, + 'is_pinned': new_pin + }) + +@app.route('/admin/api/topics') +@admin_required +def admin_api_topics(): + topics = topic_model.get_all() + + topic_list = [] + for topic in topics: + author = user_model.get_by_id(topic['author_id']) or {} + topic_list.append({ + 'id': topic['id'], + 'name': topic['name'], + 'icon': topic.get('icon', '🔧'), + 'author': author.get('username', '未知'), + 'sub_topics_count': len(topic_model.get_sub_topics(topic['id'])), + 'questions_count': len(topic_model.get_questions(topic['id'])), + 'followers_count': len(topic['followers']), + 'created_at': topic['created_at'], + }) + + return jsonify(topic_list) + +@app.route('/admin/api/topics/') +@admin_required +def admin_api_topic_detail(topic_id): + topic = topic_model.get_by_id(topic_id) + if not topic: + return jsonify({'error': '主题不存在'}), 404 + + author = user_model.get_by_id(topic['author_id']) or {} + + return jsonify({ + 'id': topic_id, + 'name': topic['name'], + 'description': topic.get('description', ''), + 'icon': topic.get('icon', '🔧'), + 'author': author.get('username', '未知'), + 'sub_topics': topic_model.get_sub_topics(topic_id), + 'questions': topic_model.get_questions(topic_id), + 'followers_count': len(topic['followers']), + 'created_at': topic['created_at'], + }) + +@app.route('/admin/api/topics/', methods=['DELETE']) +@admin_required +def admin_api_delete_topic(topic_id): + topic_model.delete(topic_id) + return jsonify({'success': True}) + +@app.route('/admin/api/tags') +@admin_required +def admin_api_tags(): + tags = post_model.get_tags_stats() + return jsonify([{'name': t[0], 'count': t[1]} for t in tags[:20]]) + +# ============ 前台用户 API ============ @app.route('/api/register', methods=['POST']) def api_register(): @@ -564,15 +852,13 @@ def api_follow_topic(topic_id): 'followers_count': followers_count }) -# ============ API: 标签 ============ +# ============ API: 标签和搜索 ============ @app.route('/api/tags') def api_tags(): tags = post_model.get_tags_stats() return jsonify([{'name': t[0], 'count': t[1]} for t in tags]) -# ============ API: 搜索 ============ - @app.route('/api/search') def api_search(): query = request.args.get('q', '').strip().lower() @@ -610,7 +896,7 @@ def api_search(): 'topics': matched_topics[:20] }) -# ============ API: 健康检查 ============ +# ============ 健康检查 ============ @app.route('/api/health') def api_health(): @@ -620,7 +906,9 @@ if __name__ == '__main__': print("=" * 50) print("技术论坛与技术分享网站") print("=" * 50) - print(f"访问地址: http://localhost:{BACKEND_PORT}") + print(f"前台地址: http://localhost:{BACKEND_PORT}") + print(f"后台地址: http://localhost:{BACKEND_PORT}/admin") + print(f"后台账号: {ADMIN_USERNAME} / {ADMIN_PASSWORD}") print("=" * 50) app.run(host='0.0.0.0', port=BACKEND_PORT, debug=True) \ No newline at end of file