This commit is contained in:
beabigegg
2025-09-04 18:34:05 +08:00
parent f093f4bbc2
commit 6eabdb2f07
35 changed files with 1097 additions and 212 deletions

View File

@@ -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 keysworker存在時會有這些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