539 lines
17 KiB
Python
539 lines
17 KiB
Python
#!/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.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': datetime.utcnow().isoformat(),
|
||
'end_date': datetime.utcnow().isoformat()
|
||
}
|
||
))
|
||
|
||
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': datetime.utcnow().isoformat(),
|
||
'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': datetime.utcnow().isoformat(),
|
||
'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': datetime.utcnow().isoformat(),
|
||
'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 |