feat: 合并后台到主服务端口19004
- 后台管理整合到 /admin 路径 - 前台保持原有路由 - 统一API路径(后台 /admin/api/xxx) - 删除独立admin服务(保留admin目录作为模板) - 简化部署,只需启动一个服务
This commit is contained in:
@@ -21,23 +21,26 @@
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="mt-6">
|
||||
<a href="/" class="flex items-center gap-3 px-6 py-3 bg-slate-700 text-white">
|
||||
<a href="/admin" class="flex items-center gap-3 px-6 py-3 bg-slate-700 text-white">
|
||||
<i class="ri-dashboard-line"></i><span>仪表盘</span>
|
||||
</a>
|
||||
<a href="/users" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
|
||||
<a href="/admin/users" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
|
||||
<i class="ri-user-line"></i><span>用户管理</span>
|
||||
</a>
|
||||
<a href="/posts" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
|
||||
<a href="/admin/posts" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
|
||||
<i class="ri-file-text-line"></i><span>帖子管理</span>
|
||||
</a>
|
||||
<a href="/topics" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
|
||||
<a href="/admin/topics" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
|
||||
<i class="ri-tools-line"></i><span>主题管理</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t border-slate-700">
|
||||
<a href="http://localhost:19004" target="_blank" class="text-slate-400 hover:text-white text-sm flex items-center gap-2">
|
||||
<a href="/" target="_blank" class="text-slate-400 hover:text-white text-sm flex items-center gap-2">
|
||||
<i class="ri-external-link-line"></i> 访问前台
|
||||
</a>
|
||||
<button onclick="logout()" class="mt-2 text-slate-400 hover:text-red-400 text-sm flex items-center gap-2">
|
||||
<i class="ri-logout-box-line"></i> 退出登录
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -140,8 +143,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检查登录状态
|
||||
async function checkAuth() {
|
||||
const res = await fetch('/admin/api/check-auth');
|
||||
const data = await res.json();
|
||||
if (!data.logged_in) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
}
|
||||
checkAuth();
|
||||
|
||||
// 退出登录
|
||||
async function logout() {
|
||||
await fetch('/admin/api/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
const res = await fetch('/api/stats');
|
||||
const res = await fetch('/admin/api/stats');
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('stat-users').textContent = data.users_count;
|
||||
@@ -155,7 +174,7 @@
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
const res = await fetch('/api/tags');
|
||||
const res = await fetch('/admin/api/tags');
|
||||
const tags = await res.json();
|
||||
|
||||
const container = document.getElementById('tagsList');
|
||||
@@ -172,7 +191,7 @@
|
||||
}
|
||||
|
||||
async function loadRecentPosts() {
|
||||
const res = await fetch('/api/posts');
|
||||
const res = await fetch('/admin/api/posts');
|
||||
const posts = await res.json();
|
||||
|
||||
const container = document.getElementById('recentPosts');
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
window.location.href = '/admin';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
errorEl.classList.remove('hidden');
|
||||
@@ -113,4 +113,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>>
|
||||
310
backend/app.py
310
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/<user_id>', 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/<post_id>')
|
||||
@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/<post_id>', 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/<post_id>/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/<topic_id>')
|
||||
@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/<topic_id>', 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)
|
||||
Reference in New Issue
Block a user