""" 后台管理模块 """ 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/', 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//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/') @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//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//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//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()})