#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 翻譯任務管理 API Author: PANJIT IT Team Created: 2024-01-28 Modified: 2024-01-28 """ from flask import Blueprint, request, jsonify, g from app.utils.decorators import jwt_login_required, admin_required from app.utils.validators import ( validate_job_uuid, validate_pagination, validate_date_range ) from app.utils.helpers import create_response, calculate_processing_time from app.utils.exceptions import ValidationError from app.utils.logger import get_logger from app.models.job import TranslationJob from app.models.stats import APIUsageStats from app.models.log import SystemLog from sqlalchemy import and_, or_ jobs_bp = Blueprint('jobs', __name__, url_prefix='/jobs') logger = get_logger(__name__) @jobs_bp.route('', methods=['GET']) @jwt_login_required def get_user_jobs(): """取得使用者任務列表""" try: # 取得查詢參數 page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) status = request.args.get('status', 'all') # 驗證分頁參數 page, per_page = validate_pagination(page, per_page) # 建立查詢 query = TranslationJob.query.filter_by(user_id=g.current_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(include_files=False) # 計算處理時間 if job.processing_started_at and job.completed_at: job_data['processing_time'] = calculate_processing_time( job.processing_started_at, job.completed_at ) # 取得佇列位置(只對 PENDING 狀態) if job.status == 'PENDING': job_data['queue_position'] = TranslationJob.get_queue_position(job.job_uuid) 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 user jobs error: {str(e)}") return jsonify(create_response( success=False, error='SYSTEM_ERROR', message='取得任務列表失敗' )), 500 @jobs_bp.route('/', methods=['GET']) @jwt_login_required def get_job_detail(job_uuid): """取得任務詳細資訊""" try: # 驗證 UUID 格式 validate_job_uuid(job_uuid) # 取得任務 job = TranslationJob.query.filter_by(job_uuid=job_uuid).first() if not job: return jsonify(create_response( success=False, error='JOB_NOT_FOUND', message='任務不存在' )), 404 # 檢查權限 if job.user_id != g.current_user_id and not g.is_admin: return jsonify(create_response( success=False, error='PERMISSION_DENIED', message='無權限存取此任務' )), 403 # 取得任務詳細資料 job_data = job.to_dict(include_files=True) # 計算處理時間 if job.processing_started_at and job.completed_at: job_data['processing_time'] = calculate_processing_time( job.processing_started_at, job.completed_at ) elif job.processing_started_at: job_data['processing_time'] = calculate_processing_time( job.processing_started_at ) # 取得佇列位置(只對 PENDING 狀態) if job.status == 'PENDING': job_data['queue_position'] = TranslationJob.get_queue_position(job.job_uuid) # 取得 API 使用統計(如果已完成) if job.status == 'COMPLETED': api_stats = APIUsageStats.get_user_statistics( user_id=job.user_id, start_date=job.created_at, end_date=job.completed_at ) job_data['api_usage'] = api_stats return jsonify(create_response( success=True, data={ 'job': job_data } )) 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 job detail error: {str(e)}") return jsonify(create_response( success=False, error='SYSTEM_ERROR', message='取得任務詳情失敗' )), 500 @jobs_bp.route('//retry', methods=['POST']) @jwt_login_required def retry_job(job_uuid): """重試失敗任務""" try: # 驗證 UUID 格式 validate_job_uuid(job_uuid) # 取得任務 job = TranslationJob.query.filter_by(job_uuid=job_uuid).first() if not job: return jsonify(create_response( success=False, error='JOB_NOT_FOUND', message='任務不存在' )), 404 # 檢查權限 if job.user_id != g.current_user_id and not g.is_admin: return jsonify(create_response( success=False, error='PERMISSION_DENIED', message='無權限操作此任務' )), 403 # 檢查是否可以重試 if not job.can_retry(): return jsonify(create_response( success=False, error='CANNOT_RETRY', message='任務無法重試(狀態不正確或重試次數已達上限)' )), 400 # 重置任務狀態 job.update_status('PENDING', error_message=None) job.increment_retry() # 計算新的佇列位置 queue_position = TranslationJob.get_queue_position(job.job_uuid) # 記錄重試日誌 SystemLog.info( 'jobs.retry', f'Job retry requested: {job_uuid}', user_id=g.current_user_id, job_id=job.id, extra_data={ 'retry_count': job.retry_count, 'previous_error': job.error_message } ) logger.info(f"Job retry requested: {job_uuid} (retry count: {job.retry_count})") # 重新觸發翻譯任務(這裡會在實作 Celery 時加入) # from app.tasks.translation import process_translation_job # process_translation_job.delay(job.id) return jsonify(create_response( success=True, data={ 'job_uuid': job.job_uuid, 'status': job.status, 'retry_count': job.retry_count, 'queue_position': queue_position }, message='任務已重新加入佇列' )) 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"Job retry error: {str(e)}") return jsonify(create_response( success=False, error='SYSTEM_ERROR', message='重試任務失敗' )), 500 @jobs_bp.route('/statistics', methods=['GET']) @jwt_login_required def get_user_statistics(): """取得使用者統計資料""" try: # 取得日期範圍參數 start_date = request.args.get('start_date') end_date = request.args.get('end_date') # 驗證日期範圍 if start_date or end_date: start_date, end_date = validate_date_range(start_date, end_date) # 取得任務統計 job_stats = TranslationJob.get_statistics( user_id=g.current_user_id, start_date=start_date, end_date=end_date ) # 取得 API 使用統計 api_stats = APIUsageStats.get_user_statistics( user_id=g.current_user_id, start_date=start_date, end_date=end_date ) return jsonify(create_response( success=True, data={ 'job_statistics': job_stats, 'api_statistics': api_stats } )) 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 user statistics error: {str(e)}") return jsonify(create_response( success=False, error='SYSTEM_ERROR', message='取得統計資料失敗' )), 500 @jobs_bp.route('/queue/status', methods=['GET']) def get_queue_status(): """取得佇列狀態(不需登入)""" try: # 取得各狀態任務數量 pending_count = TranslationJob.query.filter_by(status='PENDING').count() processing_count = TranslationJob.query.filter_by(status='PROCESSING').count() # 取得當前處理中的任務(最多5個) processing_jobs = TranslationJob.query.filter_by( status='PROCESSING' ).order_by(TranslationJob.processing_started_at).limit(5).all() processing_jobs_data = [] for job in processing_jobs: processing_jobs_data.append({ 'job_uuid': job.job_uuid, 'original_filename': job.original_filename, 'progress': float(job.progress) if job.progress else 0.0, 'processing_started_at': job.processing_started_at.isoformat() if job.processing_started_at else None, 'processing_time': calculate_processing_time(job.processing_started_at) if job.processing_started_at else None }) return jsonify(create_response( success=True, data={ 'queue_status': { 'pending': pending_count, 'processing': processing_count, 'total_in_queue': pending_count + processing_count }, 'processing_jobs': processing_jobs_data } )) except Exception as e: logger.error(f"Get queue status error: {str(e)}") return jsonify(create_response( success=False, error='SYSTEM_ERROR', message='取得佇列狀態失敗' )), 500 @jobs_bp.route('//cancel', methods=['POST']) @jwt_login_required def cancel_job(job_uuid): """取消任務(僅限 PENDING 狀態)""" try: # 驗證 UUID 格式 validate_job_uuid(job_uuid) # 取得任務 job = TranslationJob.query.filter_by(job_uuid=job_uuid).first() if not job: return jsonify(create_response( success=False, error='JOB_NOT_FOUND', message='任務不存在' )), 404 # 檢查權限 if job.user_id != g.current_user_id and not g.is_admin: return jsonify(create_response( success=False, error='PERMISSION_DENIED', message='無權限操作此任務' )), 403 # 只能取消等待中的任務 if job.status != 'PENDING': return jsonify(create_response( success=False, error='CANNOT_CANCEL', message='只能取消等待中的任務' )), 400 # 更新任務狀態為失敗(取消) job.update_status('FAILED', error_message='使用者取消任務') # 記錄取消日誌 SystemLog.info( 'jobs.cancel', f'Job cancelled by user: {job_uuid}', user_id=g.current_user_id, job_id=job.id ) logger.info(f"Job cancelled by user: {job_uuid}") return jsonify(create_response( success=True, data={ 'job_uuid': job.job_uuid, 'status': job.status }, message='任務已取消' )) 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"Cancel job error: {str(e)}") return jsonify(create_response( success=False, error='SYSTEM_ERROR', message='取消任務失敗' )), 500