Files
Document_Translator/app/api/admin.py
2025-09-03 09:05:51 +08:00

540 lines
17 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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
管理員 API
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify, g
from app.utils.decorators import admin_required
from app.utils.validators import validate_pagination, validate_date_range
from app.utils.helpers import create_response
from app.utils.exceptions import ValidationError
from app.utils.logger import get_logger
from app.models.user import User
from app.models.job import TranslationJob
from app.models.stats import APIUsageStats
from app.utils.timezone import format_taiwan_time
from app.models.log import SystemLog
from app.models.cache import TranslationCache
from sqlalchemy import func, desc
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
logger = get_logger(__name__)
@admin_bp.route('/stats', methods=['GET'])
@admin_required
def get_system_stats():
"""取得系統統計資料(簡化版本)"""
try:
from app import db
# 基本統計
overview = {
'total_jobs': TranslationJob.query.count(),
'completed_jobs': TranslationJob.query.filter_by(status='COMPLETED').count(),
'failed_jobs': TranslationJob.query.filter_by(status='FAILED').count(),
'pending_jobs': TranslationJob.query.filter_by(status='PENDING').count(),
'processing_jobs': TranslationJob.query.filter_by(status='PROCESSING').count(),
'total_users': User.query.count(),
'active_users_today': 0, # 簡化版本先設為0
'total_cost': 0.0 # 簡化版本先設為0
}
# 簡化的用戶排行榜 - 按任務數排序
user_rankings = db.session.query(
User.id,
User.display_name,
func.count(TranslationJob.id).label('job_count')
).outerjoin(TranslationJob).group_by(
User.id, User.display_name
).order_by(
func.count(TranslationJob.id).desc()
).limit(10).all()
user_rankings_data = []
for ranking in user_rankings:
user_rankings_data.append({
'user_id': ranking.id,
'display_name': ranking.display_name,
'job_count': ranking.job_count or 0,
'total_cost': 0.0 # 簡化版本
})
# 簡化的每日統計 - 只返回空數組
daily_stats = []
return jsonify(create_response(
success=True,
data={
'overview': overview,
'daily_stats': daily_stats,
'user_rankings': user_rankings_data,
'period': 'month',
'start_date': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S"),
'end_date': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S")
}
))
except Exception as e:
logger.error(f"Get system stats error: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得系統統計失敗'
)), 500
@admin_bp.route('/jobs', methods=['GET'])
@admin_required
def get_all_jobs():
"""取得所有使用者任務"""
try:
# 取得查詢參數
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
user_id = request.args.get('user_id', type=int)
status = request.args.get('status')
# 驗證分頁參數
page, per_page = validate_pagination(page, min(per_page, 100))
# 建立查詢
query = TranslationJob.query
# 使用者篩選
if user_id:
query = query.filter_by(user_id=user_id)
# 狀態篩選
if status and status != 'all':
valid_statuses = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'RETRY']
if status.upper() in valid_statuses:
query = query.filter_by(status=status.upper())
# 排序
query = query.order_by(TranslationJob.created_at.desc())
# 分頁
pagination = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
jobs = pagination.items
# 組合回應資料(包含使用者資訊)
jobs_data = []
for job in jobs:
job_data = job.to_dict()
job_data['user'] = {
'id': job.user.id,
'username': job.user.username,
'display_name': job.user.display_name,
'email': job.user.email
}
jobs_data.append(job_data)
return jsonify(create_response(
success=True,
data={
'jobs': jobs_data,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages,
'has_prev': pagination.has_prev,
'has_next': pagination.has_next
}
}
))
except ValidationError as e:
return jsonify(create_response(
success=False,
error=e.error_code,
message=str(e)
)), 400
except Exception as e:
logger.error(f"Get all jobs error: {str(e)}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得任務列表失敗'
)), 500
@admin_bp.route('/users', methods=['GET'])
@admin_required
def get_all_users():
"""取得所有使用者(簡化版本)"""
try:
# 簡化版本 - 不使用分頁,直接返回所有用戶
users = User.query.order_by(User.created_at.desc()).limit(50).all()
users_data = []
for user in users:
# 直接構建基本用戶資料不使用to_dict方法
users_data.append({
'id': user.id,
'username': user.username,
'display_name': user.display_name,
'email': user.email,
'department': user.department or '',
'is_admin': user.is_admin,
'last_login': user.last_login.isoformat() if user.last_login else None,
'created_at': user.created_at.isoformat() if user.created_at else None,
'updated_at': user.updated_at.isoformat() if user.updated_at else None
})
return jsonify(create_response(
success=True,
data={
'users': users_data,
'pagination': {
'page': 1,
'per_page': 50,
'total': len(users_data),
'pages': 1,
'has_prev': False,
'has_next': False
}
}
))
except Exception as e:
logger.error(f"Get all users error: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得使用者列表失敗'
)), 500
@admin_bp.route('/logs', methods=['GET'])
@admin_required
def get_system_logs():
"""取得系統日誌"""
try:
# 取得查詢參數
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 100, type=int)
level = request.args.get('level')
module = request.args.get('module')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
# 驗證參數
page, per_page = validate_pagination(page, min(per_page, 500))
if start_date or end_date:
start_date, end_date = validate_date_range(start_date, end_date)
# 取得日誌
logs = SystemLog.get_logs(
level=level,
module=module,
start_date=start_date,
end_date=end_date,
limit=per_page,
offset=(page - 1) * per_page
)
# 取得總數(簡化版本,不完全精確)
total = len(logs) if len(logs) < per_page else (page * per_page) + 1
logs_data = [log.to_dict() for log in logs]
return jsonify(create_response(
success=True,
data={
'logs': logs_data,
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'has_more': len(logs) == per_page
}
}
))
except ValidationError as e:
return jsonify(create_response(
success=False,
error=e.error_code,
message=str(e)
)), 400
except Exception as e:
logger.error(f"Get system logs error: {str(e)}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得系統日誌失敗'
)), 500
@admin_bp.route('/api-usage', methods=['GET'])
@admin_required
def get_api_usage():
"""取得 API 使用統計(簡化版本)"""
try:
from app import db
# 基本統計
total_calls = db.session.query(APIUsageStats).count()
total_cost = db.session.query(func.sum(APIUsageStats.cost)).scalar() or 0.0
total_tokens = db.session.query(func.sum(APIUsageStats.total_tokens)).scalar() or 0
# 簡化版本返回基本數據
return jsonify(create_response(
success=True,
data={
'daily_stats': [], # 簡化版本
'top_users': [], # 簡化版本
'endpoint_stats': [], # 簡化版本
'cost_trend': [], # 簡化版本
'period_days': 30,
'summary': {
'total_calls': total_calls,
'total_cost': float(total_cost),
'total_tokens': total_tokens
}
}
))
except Exception as e:
logger.error(f"Get API usage error: {str(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得API使用統計失敗'
)), 500
@admin_bp.route('/cache/stats', methods=['GET'])
@admin_required
def get_cache_stats():
"""取得翻譯快取統計"""
try:
cache_stats = TranslationCache.get_cache_statistics()
return jsonify(create_response(
success=True,
data=cache_stats
))
except Exception as e:
logger.error(f"Get cache stats error: {str(e)}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得快取統計失敗'
)), 500
@admin_bp.route('/health', methods=['GET'])
@admin_required
def get_system_health():
"""取得系統健康狀態(管理員專用)"""
try:
from datetime import datetime
status = {
'timestamp': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S"),
'status': 'healthy',
'services': {}
}
# 資料庫檢查
try:
from app import db
db.session.execute('SELECT 1')
status['services']['database'] = {'status': 'healthy'}
except Exception as e:
status['services']['database'] = {
'status': 'unhealthy',
'error': str(e)
}
status['status'] = 'unhealthy'
# 基本統計
try:
total_jobs = TranslationJob.query.count()
pending_jobs = TranslationJob.query.filter_by(status='PENDING').count()
status['services']['translation_service'] = {
'status': 'healthy',
'total_jobs': total_jobs,
'pending_jobs': pending_jobs
}
except Exception as e:
status['services']['translation_service'] = {
'status': 'unhealthy',
'error': str(e)
}
status['status'] = 'unhealthy'
return jsonify(create_response(
success=True,
data=status
))
except Exception as e:
logger.error(f"Get system health error: {str(e)}")
return jsonify({
'timestamp': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S"),
'status': 'error',
'error': str(e)
}), 500
@admin_bp.route('/metrics', methods=['GET'])
@admin_required
def get_system_metrics():
"""取得系統指標(管理員專用)"""
try:
from datetime import datetime, timedelta
from app import db
# 統計任務狀態
job_stats = db.session.query(
TranslationJob.status,
func.count(TranslationJob.id)
).group_by(TranslationJob.status).all()
job_counts = {status: count for status, count in job_stats}
# 最近24小時的統計
yesterday = datetime.utcnow() - timedelta(days=1)
recent_jobs = db.session.query(
TranslationJob.status,
func.count(TranslationJob.id)
).filter(
TranslationJob.created_at >= yesterday
).group_by(TranslationJob.status).all()
recent_counts = {status: count for status, count in recent_jobs}
metrics_data = {
'timestamp': format_taiwan_time(datetime.utcnow(), "%Y-%m-%d %H:%M:%S"),
'jobs': {
'pending': job_counts.get('PENDING', 0),
'processing': job_counts.get('PROCESSING', 0),
'completed': job_counts.get('COMPLETED', 0),
'failed': job_counts.get('FAILED', 0),
'retry': job_counts.get('RETRY', 0),
'total': sum(job_counts.values())
},
'recent_24h': {
'pending': recent_counts.get('PENDING', 0),
'processing': recent_counts.get('PROCESSING', 0),
'completed': recent_counts.get('COMPLETED', 0),
'failed': recent_counts.get('FAILED', 0),
'retry': recent_counts.get('RETRY', 0),
'total': sum(recent_counts.values())
}
}
return jsonify(create_response(
success=True,
data=metrics_data
))
except Exception as e:
logger.error(f"Get system metrics error: {str(e)}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='取得系統指標失敗'
)), 500
@admin_bp.route('/maintenance/cleanup', methods=['POST'])
@admin_required
def cleanup_system():
"""系統清理維護"""
try:
data = request.get_json() or {}
# 清理選項
cleanup_logs = data.get('cleanup_logs', False)
cleanup_cache = data.get('cleanup_cache', False)
cleanup_files = data.get('cleanup_files', False)
logs_days = data.get('logs_days', 30)
cache_days = data.get('cache_days', 90)
files_days = data.get('files_days', 7)
cleanup_results = {}
# 清理舊日誌
if cleanup_logs:
deleted_logs = SystemLog.cleanup_old_logs(days_to_keep=logs_days)
cleanup_results['logs'] = {
'deleted_count': deleted_logs,
'days_kept': logs_days
}
# 清理舊快取
if cleanup_cache:
deleted_cache = TranslationCache.clear_old_cache(days_to_keep=cache_days)
cleanup_results['cache'] = {
'deleted_count': deleted_cache,
'days_kept': cache_days
}
# 清理舊檔案(這裡會在檔案服務中實作)
if cleanup_files:
# from app.services.file_service import cleanup_old_files
# deleted_files = cleanup_old_files(days_to_keep=files_days)
cleanup_results['files'] = {
'message': 'File cleanup not implemented yet',
'days_kept': files_days
}
# 記錄維護日誌
SystemLog.info(
'admin.maintenance',
f'System cleanup performed by {g.current_user.username}',
user_id=g.current_user.id,
extra_data={
'cleanup_options': data,
'results': cleanup_results
}
)
logger.info(f"System cleanup performed by {g.current_user.username}")
return jsonify(create_response(
success=True,
data=cleanup_results,
message='系統清理完成'
))
except Exception as e:
logger.error(f"System cleanup error: {str(e)}")
return jsonify(create_response(
success=False,
error='SYSTEM_ERROR',
message='系統清理失敗'
)), 500