Files
pdf-translate-web/admin.py
coder 236b202e3e 添加后台管理功能
功能:
- 数据概览仪表盘
- 用户管理(列表/编辑/删除)
- 翻译记录管理
- 缓存管理
- 统计报表(图表)
- 操作日志

管理员账号: admin / admin123
后台地址: /admin/
2026-04-07 23:08:49 +08:00

428 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
后台管理模块
"""
from datetime import datetime, date, timedelta
from functools import wraps
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, flash
from sqlalchemy import func, desc
from models import db, User, Translation, TranslationCache, GuestTranslation, SystemConfig, OperationLog
from config import USER_LIMITS, MEMBERSHIP_PLANS
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
# ==================== 权限装饰器 ====================
def admin_required(f):
"""管理员权限装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
user_id = session.get('user_id')
if not user_id:
return redirect(url_for('login', next=request.url))
user = User.query.get(user_id)
if not user or not user.is_admin:
flash('需要管理员权限', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
# ==================== 后台首页 ====================
@admin_bp.route('/')
@admin_required
def dashboard():
"""后台首页 - 数据概览"""
# 用户统计
total_users = User.query.count()
new_users_today = User.query.filter(
func.date(User.created_at) == date.today()
).count()
vip_users = User.query.filter(User.user_type.startswith('vip')).count()
# 翻译统计
total_translations = Translation.query.count()
today_translations = Translation.query.filter(
func.date(Translation.created_at) == date.today()
).count()
# 缓存统计
total_cache = TranslationCache.query.count()
total_cache_hits = db.session.query(func.sum(TranslationCache.hit_count)).scalar() or 0
total_cache_size = db.session.query(func.sum(TranslationCache.file_size)).scalar() or 0
# 最近翻译
recent_translations = Translation.query.order_by(desc(Translation.created_at)).limit(10).all()
# 最近用户
recent_users = User.query.order_by(desc(User.created_at)).limit(10).all()
# 每日翻译趋势最近7天
daily_stats = []
for i in range(6, -1, -1):
d = date.today() - timedelta(days=i)
count = Translation.query.filter(func.date(Translation.created_at) == d).count()
daily_stats.append({'date': d.strftime('%m-%d'), 'count': count})
return render_template('admin/dashboard.html',
total_users=total_users,
new_users_today=new_users_today,
vip_users=vip_users,
total_translations=total_translations,
today_translations=today_translations,
total_cache=total_cache,
total_cache_hits=total_cache_hits,
total_cache_size=total_cache_size,
recent_translations=recent_translations,
recent_users=recent_users,
daily_stats=daily_stats,
)
# ==================== 用户管理 ====================
@admin_bp.route('/users')
@admin_required
def users():
"""用户列表"""
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
user_type = request.args.get('type', '')
query = User.query
if search:
query = query.filter(
(User.username.contains(search)) | (User.email.contains(search))
)
if user_type:
query = query.filter_by(user_type=user_type)
users = query.order_by(desc(User.created_at)).paginate(page=page, per_page=20)
return render_template('admin/users.html',
users=users,
search=search,
user_type=user_type,
user_types=['free', 'vip_basic', 'vip_pro', 'vip_enterprise', 'admin']
)
@admin_bp.route('/user/<int:user_id>', methods=['GET', 'POST'])
@admin_required
def user_detail(user_id):
"""用户详情/编辑"""
user = User.query.get_or_404(user_id)
if request.method == 'POST':
# 更新用户信息
data = request.json if request.is_json else request.form
user.username = data.get('username', user.username)
user.email = data.get('email', user.email)
user.user_type = data.get('user_type', user.user_type)
user.is_active = data.get('is_active', user.is_active) == 'true' or data.get('is_active') == True
user.is_admin = data.get('is_admin', user.is_admin) == 'true' or data.get('is_admin') == True
# 会员到期时间
expire_str = data.get('membership_expire')
if expire_str:
try:
user.membership_expire = datetime.fromisoformat(expire_str)
except:
pass
# 密码
new_password = data.get('new_password')
if new_password:
user.set_password(new_password)
db.session.commit()
# 记录日志
log = OperationLog(
user_id=session.get('user_id'),
username='admin',
action='edit_user',
target=user.username,
detail=str(data)
)
db.session.add(log)
db.session.commit()
if request.is_json:
return jsonify({'success': True, 'user': user.to_dict()})
flash('用户信息已更新', 'success')
return redirect(url_for('admin.user_detail', user_id=user_id))
# 获取用户翻译记录
translations = Translation.query.filter_by(user_id=user_id).order_by(desc(Translation.created_at)).limit(20).all()
return render_template('admin/user_detail.html', user=user, translations=translations)
@admin_bp.route('/user/<int:user_id>/delete', methods=['POST'])
@admin_required
def delete_user(user_id):
"""删除用户"""
user = User.query.get_or_404(user_id)
if user.is_admin and User.query.filter_by(is_admin=True).count() <= 1:
return jsonify({'error': '不能删除最后一个管理员'}), 400
username = user.username
db.session.delete(user)
db.session.commit()
# 记录日志
log = OperationLog(
user_id=session.get('user_id'),
username='admin',
action='delete_user',
target=username
)
db.session.add(log)
db.session.commit()
return jsonify({'success': True})
# ==================== 翻译记录管理 ====================
@admin_bp.route('/translations')
@admin_required
def translations():
"""翻译记录列表"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', '')
search = request.args.get('search', '')
query = Translation.query
if status:
query = query.filter_by(status=status)
if search:
query = query.filter(Translation.original_filename.contains(search))
translations = query.order_by(desc(Translation.created_at)).paginate(page=page, per_page=20)
return render_template('admin/translations.html',
translations=translations,
status=status,
search=search
)
@admin_bp.route('/translation/<int:trans_id>')
@admin_required
def translation_detail(trans_id):
"""翻译详情"""
translation = Translation.query.get_or_404(trans_id)
# 获取关联用户
user = User.query.get(translation.user_id) if translation.user_id else None
return render_template('admin/translation_detail.html',
translation=translation,
user=user
)
@admin_bp.route('/translation/<int:trans_id>/delete', methods=['POST'])
@admin_required
def delete_translation(trans_id):
"""删除翻译记录"""
translation = Translation.query.get_or_404(trans_id)
# 删除文件
if translation.output_path:
import os
if os.path.exists(translation.output_path):
os.remove(translation.output_path)
db.session.delete(translation)
db.session.commit()
return jsonify({'success': True})
# ==================== 缓存管理 ====================
@admin_bp.route('/cache')
@admin_required
def cache_list():
"""缓存列表"""
page = request.args.get('page', 1, type=int)
caches = TranslationCache.query.order_by(desc(TranslationCache.hit_count)).paginate(page=page, per_page=20)
# 统计
total_size = db.session.query(func.sum(TranslationCache.file_size)).scalar() or 0
total_hits = db.session.query(func.sum(TranslationCache.hit_count)).scalar() or 0
return render_template('admin/cache.html',
caches=caches,
total_size=total_size,
total_hits=total_hits
)
@admin_bp.route('/cache/<int:cache_id>/delete', methods=['POST'])
@admin_required
def delete_cache(cache_id):
"""删除缓存"""
cache = TranslationCache.query.get_or_404(cache_id)
# 删除文件
if cache.cache_path:
import os
if os.path.exists(cache.cache_path):
os.remove(cache.cache_path)
db.session.delete(cache)
db.session.commit()
return jsonify({'success': True})
@admin_bp.route('/cache/clear', methods=['POST'])
@admin_required
def clear_cache():
"""清空缓存"""
import os
caches = TranslationCache.query.all()
for cache in caches:
if cache.cache_path and os.path.exists(cache.cache_path):
os.remove(cache.cache_path)
TranslationCache.query.delete()
db.session.commit()
return jsonify({'success': True, 'deleted': len(caches)})
# ==================== 系统配置 ====================
@admin_bp.route('/settings', methods=['GET', 'POST'])
@admin_required
def settings():
"""系统配置"""
if request.method == 'POST':
data = request.json if request.is_json else request.form
for key, value in data.items():
SystemConfig.set(key, value)
if request.is_json:
return jsonify({'success': True})
flash('配置已保存', 'success')
# 获取所有配置
configs = SystemConfig.query.all()
config_dict = {c.key: c.value for c in configs}
return render_template('admin/settings.html',
configs=config_dict,
user_limits=USER_LIMITS,
membership_plans=MEMBERSHIP_PLANS
)
# ==================== 操作日志 ====================
@admin_bp.route('/logs')
@admin_required
def logs():
"""操作日志"""
page = request.args.get('page', 1, type=int)
action = request.args.get('action', '')
query = OperationLog.query
if action:
query = query.filter_by(action=action)
logs = query.order_by(desc(OperationLog.created_at)).paginate(page=page, per_page=50)
return render_template('admin/logs.html', logs=logs, action=action)
# ==================== 统计报表 ====================
@admin_bp.route('/stats')
@admin_required
def stats():
"""统计报表"""
# 用户增长趋势最近30天
user_growth = []
for i in range(29, -1, -1):
d = date.today() - timedelta(days=i)
count = User.query.filter(func.date(User.created_at) == d).count()
user_growth.append({'date': d.strftime('%Y-%m-%d'), 'count': count})
# 翻译量趋势最近30天
translation_growth = []
for i in range(29, -1, -1):
d = date.today() - timedelta(days=i)
count = Translation.query.filter(func.date(Translation.created_at) == d).count()
translation_growth.append({'date': d.strftime('%Y-%m-%d'), 'count': count})
# 用户类型分布
user_distribution = db.session.query(
User.user_type, func.count(User.id)
).group_by(User.user_type).all()
# 翻译状态分布
status_distribution = db.session.query(
Translation.status, func.count(Translation.id)
).group_by(Translation.status).all()
# Top用户
top_users = db.session.query(
User.username, func.count(Translation.id).label('count')
).join(Translation).group_by(User.id).order_by(desc('count')).limit(10).all()
return render_template('admin/stats.html',
user_growth=user_growth,
translation_growth=translation_growth,
user_distribution=user_distribution,
status_distribution=status_distribution,
top_users=top_users
)
# ==================== API接口 ====================
@admin_bp.route('/api/stats/summary')
@admin_required
def api_stats_summary():
"""API: 统计摘要"""
return jsonify({
'total_users': User.query.count(),
'today_users': User.query.filter(func.date(User.created_at) == date.today()).count(),
'total_translations': Translation.query.count(),
'today_translations': Translation.query.filter(func.date(Translation.created_at) == date.today()).count(),
'total_cache': TranslationCache.query.count(),
'cache_hits': db.session.query(func.sum(TranslationCache.hit_count)).scalar() or 0,
})
@admin_bp.route('/api/user/<int:user_id>/upgrade', methods=['POST'])
@admin_required
def api_user_upgrade(user_id):
"""API: 升级用户会员"""
user = User.query.get_or_404(user_id)
data = request.json
user_type = data.get('user_type')
months = data.get('months', 1)
if user_type not in USER_LIMITS:
return jsonify({'error': '无效的用户类型'}), 400
user.user_type = user_type
if user_type.startswith('vip'):
user.membership_expire = datetime.utcnow() + timedelta(days=30 * months)
db.session.commit()
return jsonify({'success': True, 'user': user.to_dict()})