16th_fix
This commit is contained in:
188
app/api/admin.py
188
app/api/admin.py
@@ -94,6 +94,12 @@ def get_system_stats():
|
||||
TranslationJob.status == 'COMPLETED'
|
||||
).count()
|
||||
|
||||
# 當日失敗任務統計
|
||||
daily_failed = TranslationJob.query.filter(
|
||||
func.date(TranslationJob.created_at) == target_date,
|
||||
TranslationJob.status == 'FAILED'
|
||||
).count()
|
||||
|
||||
# 當日成本統計
|
||||
daily_cost = db.session.query(
|
||||
func.sum(TranslationJob.total_cost)
|
||||
@@ -105,6 +111,7 @@ def get_system_stats():
|
||||
'date': target_date.strftime('%Y-%m-%d'),
|
||||
'jobs': daily_jobs,
|
||||
'completed': daily_completed,
|
||||
'failed': daily_failed,
|
||||
'cost': float(daily_cost)
|
||||
})
|
||||
|
||||
@@ -145,6 +152,8 @@ def get_all_jobs():
|
||||
per_page = request.args.get('per_page', 50, type=int)
|
||||
user_id = request.args.get('user_id', type=int)
|
||||
status = request.args.get('status')
|
||||
search = request.args.get('search', '').strip()
|
||||
include_deleted = request.args.get('include_deleted', 'false').lower() == 'true'
|
||||
|
||||
# 驗證分頁參數
|
||||
page, per_page = validate_pagination(page, min(per_page, 100))
|
||||
@@ -152,6 +161,10 @@ def get_all_jobs():
|
||||
# 建立查詢
|
||||
query = TranslationJob.query
|
||||
|
||||
# 預設排除軟刪除的記錄,除非明確要求包含
|
||||
if not include_deleted:
|
||||
query = query.filter(TranslationJob.deleted_at.is_(None))
|
||||
|
||||
# 使用者篩選
|
||||
if user_id:
|
||||
query = query.filter_by(user_id=user_id)
|
||||
@@ -161,6 +174,10 @@ def get_all_jobs():
|
||||
valid_statuses = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'RETRY']
|
||||
if status.upper() in valid_statuses:
|
||||
query = query.filter_by(status=status.upper())
|
||||
|
||||
# 檔案名搜尋
|
||||
if search:
|
||||
query = query.filter(TranslationJob.original_filename.like(f'%{search}%'))
|
||||
|
||||
# 排序
|
||||
query = query.order_by(TranslationJob.created_at.desc())
|
||||
@@ -438,39 +455,61 @@ def get_system_health():
|
||||
}
|
||||
status['status'] = 'unhealthy'
|
||||
|
||||
# Celery 工作者檢查
|
||||
# Celery 工作者檢查 - 使用替代方案檢測
|
||||
try:
|
||||
from celery_app import celery
|
||||
from celery.app.control import Control
|
||||
import redis
|
||||
import os
|
||||
from flask import current_app
|
||||
|
||||
# 檢查 Celery 工作者狀態
|
||||
control = Control(celery)
|
||||
inspect_obj = control.inspect(timeout=2.0) # 設置較短超時
|
||||
# 方法1: 檢查Redis中是否有Celery相關的key
|
||||
redis_client = redis.from_url(current_app.config['REDIS_URL'])
|
||||
|
||||
# 獲取活躍工作者
|
||||
active_workers = inspect_obj.active()
|
||||
# 檢查Celery binding keys(worker存在時會有這些keys)
|
||||
celery_keys = redis_client.keys('_kombu.binding.celery*')
|
||||
|
||||
if active_workers and len(active_workers) > 0:
|
||||
worker_count = len(active_workers)
|
||||
# 方法2: 檢查進程(Docker環境中)
|
||||
worker_detected = False
|
||||
worker_count = 0
|
||||
|
||||
try:
|
||||
# 檢查是否有Celery相關的keys
|
||||
if celery_keys:
|
||||
worker_detected = True
|
||||
worker_count = 1 # Docker環境中通常只有一個worker
|
||||
|
||||
# 額外檢查:如果有最近的任務處理記錄,說明worker在工作
|
||||
recent_tasks = TranslationJob.query.filter(
|
||||
TranslationJob.updated_at >= datetime.utcnow() - timedelta(minutes=10),
|
||||
TranslationJob.status.in_(['PROCESSING', 'COMPLETED'])
|
||||
).count()
|
||||
|
||||
if recent_tasks > 0:
|
||||
worker_detected = True
|
||||
worker_count = max(worker_count, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if worker_detected:
|
||||
status['services']['celery'] = {
|
||||
'status': 'healthy',
|
||||
'active_workers': worker_count,
|
||||
'workers': list(active_workers.keys())
|
||||
'message': 'Worker detected via Redis/Task activity'
|
||||
}
|
||||
else:
|
||||
# Celery 工作者沒有運行,但不一定表示系統異常
|
||||
# Celery 工作者沒有檢測到
|
||||
status['services']['celery'] = {
|
||||
'status': 'warning',
|
||||
'message': 'No active Celery workers found',
|
||||
'message': 'No Celery worker activity detected',
|
||||
'active_workers': 0
|
||||
}
|
||||
# 不設置整體系統為異常,只是警告
|
||||
|
||||
except Exception as e:
|
||||
# Celery 連接失敗,但不一定表示系統異常
|
||||
# Redis連接失敗或其他錯誤
|
||||
status['services']['celery'] = {
|
||||
'status': 'warning',
|
||||
'message': f'Cannot connect to Celery workers: {str(e)[:100]}'
|
||||
'message': f'Cannot check Celery status: {str(e)[:100]}',
|
||||
'active_workers': 0
|
||||
}
|
||||
# 不設置整體系統為異常,只是警告
|
||||
|
||||
@@ -533,21 +572,24 @@ def get_system_metrics():
|
||||
from datetime import datetime, timedelta
|
||||
from app import db
|
||||
|
||||
# 統計任務狀態
|
||||
# 統計任務狀態(排除軟刪除的記錄,反映當前實際狀態)
|
||||
job_stats = db.session.query(
|
||||
TranslationJob.status,
|
||||
func.count(TranslationJob.id)
|
||||
).filter(
|
||||
TranslationJob.deleted_at.is_(None)
|
||||
).group_by(TranslationJob.status).all()
|
||||
|
||||
job_counts = {status: count for status, count in job_stats}
|
||||
|
||||
# 最近24小時的統計
|
||||
# 最近24小時的統計(排除軟刪除的記錄)
|
||||
yesterday = datetime.utcnow() - timedelta(days=1)
|
||||
recent_jobs = db.session.query(
|
||||
TranslationJob.status,
|
||||
func.count(TranslationJob.id)
|
||||
).filter(
|
||||
TranslationJob.created_at >= yesterday
|
||||
TranslationJob.created_at >= yesterday,
|
||||
TranslationJob.deleted_at.is_(None)
|
||||
).group_by(TranslationJob.status).all()
|
||||
|
||||
recent_counts = {status: count for status, count in recent_jobs}
|
||||
@@ -918,4 +960,112 @@ def generate_jobs_report(start_date, end_date):
|
||||
|
||||
return {
|
||||
'任務清單': jobs_df
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@admin_bp.route('/jobs/<job_uuid>/cancel', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_cancel_job(job_uuid):
|
||||
"""管理員取消任務"""
|
||||
try:
|
||||
from app import db
|
||||
|
||||
job = TranslationJob.query.filter_by(job_uuid=job_uuid).first()
|
||||
|
||||
if not job:
|
||||
return jsonify(create_response(
|
||||
success=False,
|
||||
error='NOT_FOUND',
|
||||
message='任務不存在'
|
||||
)), 404
|
||||
|
||||
# 只能取消等待中或處理中的任務
|
||||
if job.status not in ['PENDING', 'PROCESSING']:
|
||||
return jsonify(create_response(
|
||||
success=False,
|
||||
error='CANNOT_CANCEL',
|
||||
message='只能取消等待中或處理中的任務'
|
||||
)), 400
|
||||
|
||||
# 如果任務正在處理中,嘗試撤銷 Celery 任務
|
||||
if job.status == 'PROCESSING':
|
||||
try:
|
||||
from app.services.celery_service import revoke_task
|
||||
revoke_task(job_uuid)
|
||||
logger.info(f"Admin {g.current_user.username} revoked Celery task for job {job_uuid}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to revoke Celery task {job_uuid}: {e}")
|
||||
# 即使撤銷失敗,也繼續標記任務為失敗
|
||||
|
||||
# 更新任務狀態
|
||||
job.status = 'FAILED'
|
||||
job.error_message = f'管理員 {g.current_user.username} 取消了任務'
|
||||
job.updated_at = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Admin {g.current_user.username} cancelled job {job_uuid}")
|
||||
|
||||
return jsonify(create_response(
|
||||
success=True,
|
||||
data={
|
||||
'job_uuid': job_uuid,
|
||||
'status': job.status,
|
||||
'message': '任務已取消'
|
||||
}
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cancelling job {job_uuid}: {e}", exc_info=True)
|
||||
return jsonify(create_response(
|
||||
success=False,
|
||||
error='INTERNAL_ERROR',
|
||||
message=str(e)
|
||||
)), 500
|
||||
|
||||
|
||||
@admin_bp.route('/jobs/<job_uuid>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def admin_delete_job(job_uuid):
|
||||
"""管理員刪除任務(軟刪除)"""
|
||||
try:
|
||||
from app import db
|
||||
|
||||
job = TranslationJob.query.filter_by(job_uuid=job_uuid).first()
|
||||
|
||||
if not job:
|
||||
return jsonify(create_response(
|
||||
success=False,
|
||||
error='NOT_FOUND',
|
||||
message='任務不存在'
|
||||
)), 404
|
||||
|
||||
# 如果任務正在處理中,先嘗試撤銷 Celery 任務
|
||||
if job.status == 'PROCESSING':
|
||||
try:
|
||||
from app.services.celery_service import revoke_task
|
||||
revoke_task(job_uuid)
|
||||
logger.info(f"Admin {g.current_user.username} revoked Celery task before deletion for job {job_uuid}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to revoke Celery task {job_uuid} before deletion: {e}")
|
||||
|
||||
# 軟刪除資料庫記錄(保留數據供報表使用)
|
||||
job.soft_delete()
|
||||
|
||||
logger.info(f"Admin {g.current_user.username} soft deleted job {job_uuid}")
|
||||
|
||||
return jsonify(create_response(
|
||||
success=True,
|
||||
data={
|
||||
'job_uuid': job_uuid,
|
||||
'message': '任務已刪除'
|
||||
}
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting job {job_uuid}: {e}", exc_info=True)
|
||||
return jsonify(create_response(
|
||||
success=False,
|
||||
error='INTERNAL_ERROR',
|
||||
message=str(e)
|
||||
)), 500
|
Reference in New Issue
Block a user