feat: 合并后台到主服务端口19004

- 后台管理整合到 /admin 路径
- 前台保持原有路由
- 统一API路径(后台 /admin/api/xxx)
- 删除独立admin服务(保留admin目录作为模板)
- 简化部署,只需启动一个服务
This commit is contained in:
2026-04-12 17:05:01 +08:00
parent cb4b7d5363
commit 47bf6f48d5
3 changed files with 328 additions and 21 deletions

View File

@@ -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');

View File

@@ -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>>

View File

@@ -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)