1st_fix_login_issue

This commit is contained in:
beabigegg
2025-09-02 10:31:35 +08:00
commit a60d965317
103 changed files with 12402 additions and 0 deletions

443
app/api/jobs.py Normal file
View File

@@ -0,0 +1,443 @@
#!/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('/<job_uuid>', 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('/<job_uuid>/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('/<job_uuid>/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