1st_fix_login_issue
This commit is contained in:
494
app/api/admin.py
Normal file
494
app/api/admin.py
Normal file
@@ -0,0 +1,494 @@
|
||||
#!/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:
|
||||
# 取得時間範圍參數
|
||||
period = request.args.get('period', 'month') # day, week, month
|
||||
|
||||
# 計算時間範圍
|
||||
end_date = datetime.utcnow()
|
||||
if period == 'day':
|
||||
start_date = end_date - timedelta(days=1)
|
||||
elif period == 'week':
|
||||
start_date = end_date - timedelta(days=7)
|
||||
else: # month
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
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': User.query.filter(
|
||||
User.last_login >= datetime.utcnow() - timedelta(days=1)
|
||||
).count(),
|
||||
'total_cost': db.session.query(func.sum(APIUsageStats.cost)).scalar() or 0.0
|
||||
}
|
||||
|
||||
# 每日統計
|
||||
daily_stats = db.session.query(
|
||||
func.date(TranslationJob.created_at).label('date'),
|
||||
func.count(TranslationJob.id).label('jobs'),
|
||||
func.sum(func.case(
|
||||
(TranslationJob.status == 'COMPLETED', 1),
|
||||
else_=0
|
||||
)).label('completed'),
|
||||
func.sum(func.case(
|
||||
(TranslationJob.status == 'FAILED', 1),
|
||||
else_=0
|
||||
)).label('failed')
|
||||
).filter(
|
||||
TranslationJob.created_at >= start_date
|
||||
).group_by(func.date(TranslationJob.created_at)).order_by(
|
||||
func.date(TranslationJob.created_at)
|
||||
).all()
|
||||
|
||||
# 每日成本統計
|
||||
daily_costs = db.session.query(
|
||||
func.date(APIUsageStats.created_at).label('date'),
|
||||
func.sum(APIUsageStats.cost).label('cost')
|
||||
).filter(
|
||||
APIUsageStats.created_at >= start_date
|
||||
).group_by(func.date(APIUsageStats.created_at)).order_by(
|
||||
func.date(APIUsageStats.created_at)
|
||||
).all()
|
||||
|
||||
# 組合每日統計資料
|
||||
daily_stats_dict = {stat.date: stat for stat in daily_stats}
|
||||
daily_costs_dict = {cost.date: cost for cost in daily_costs}
|
||||
|
||||
combined_daily_stats = []
|
||||
current_date = start_date.date()
|
||||
while current_date <= end_date.date():
|
||||
stat = daily_stats_dict.get(current_date)
|
||||
cost = daily_costs_dict.get(current_date)
|
||||
|
||||
combined_daily_stats.append({
|
||||
'date': current_date.isoformat(),
|
||||
'jobs': stat.jobs if stat else 0,
|
||||
'completed': stat.completed if stat else 0,
|
||||
'failed': stat.failed if stat else 0,
|
||||
'cost': float(cost.cost) if cost and cost.cost else 0.0
|
||||
})
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
# 使用者排行榜
|
||||
user_rankings = db.session.query(
|
||||
User.id,
|
||||
User.display_name,
|
||||
func.count(TranslationJob.id).label('job_count'),
|
||||
func.sum(APIUsageStats.cost).label('total_cost')
|
||||
).outerjoin(TranslationJob).outerjoin(APIUsageStats).filter(
|
||||
TranslationJob.created_at >= start_date
|
||||
).group_by(User.id, User.display_name).order_by(
|
||||
func.sum(APIUsageStats.cost).desc().nullslast()
|
||||
).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': float(ranking.total_cost) if ranking.total_cost else 0.0
|
||||
})
|
||||
|
||||
return jsonify(create_response(
|
||||
success=True,
|
||||
data={
|
||||
'overview': overview,
|
||||
'daily_stats': combined_daily_stats,
|
||||
'user_rankings': user_rankings_data,
|
||||
'period': period,
|
||||
'start_date': start_date.isoformat(),
|
||||
'end_date': end_date.isoformat()
|
||||
}
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get system stats error: {str(e)}")
|
||||
|
||||
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:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# 驗證分頁參數
|
||||
page, per_page = validate_pagination(page, per_page)
|
||||
|
||||
# 分頁查詢
|
||||
pagination = User.query.order_by(
|
||||
User.last_login.desc().nullslast(),
|
||||
User.created_at.desc()
|
||||
).paginate(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
error_out=False
|
||||
)
|
||||
|
||||
users = pagination.items
|
||||
|
||||
# 組合使用者資料(包含統計)
|
||||
users_data = []
|
||||
for user in users:
|
||||
user_data = user.to_dict(include_stats=True)
|
||||
users_data.append(user_data)
|
||||
|
||||
return jsonify(create_response(
|
||||
success=True,
|
||||
data={
|
||||
'users': users_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 users error: {str(e)}")
|
||||
|
||||
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:
|
||||
# 取得時間範圍
|
||||
days = request.args.get('days', 30, type=int)
|
||||
days = min(days, 90) # 最多90天
|
||||
|
||||
# 取得每日統計
|
||||
daily_stats = APIUsageStats.get_daily_statistics(days=days)
|
||||
|
||||
# 取得使用量排行
|
||||
top_users = APIUsageStats.get_top_users(limit=10)
|
||||
|
||||
# 取得端點統計
|
||||
endpoint_stats = APIUsageStats.get_endpoint_statistics()
|
||||
|
||||
# 取得成本趨勢
|
||||
cost_trend = APIUsageStats.get_cost_trend(days=days)
|
||||
|
||||
return jsonify(create_response(
|
||||
success=True,
|
||||
data={
|
||||
'daily_stats': daily_stats,
|
||||
'top_users': top_users,
|
||||
'endpoint_stats': endpoint_stats,
|
||||
'cost_trend': cost_trend,
|
||||
'period_days': days
|
||||
}
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get API usage error: {str(e)}")
|
||||
|
||||
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('/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
|
Reference in New Issue
Block a user