增加web_api

This commit is contained in:
2026-02-05 15:13:54 +08:00
parent 443ec09c5c
commit d5edbc0723
43 changed files with 7036 additions and 2640 deletions

View File

@ -0,0 +1,192 @@
"""
Reports Blueprint
Provides report listing and management endpoints.
"""
import time
from pathlib import Path
from flask import Blueprint, request, current_app
from ..shared import _get_file_type, _format_response, log_performance, logger, task_status
from ..app import Config
# Create blueprint
reports_bp = Blueprint('reports', __name__, url_prefix='/reports')
@reports_bp.route('', methods=['GET'])
@log_performance
def list_reports():
"""List all generated reports with pagination and filtering."""
logger.debug("Reports list requested")
try:
# Parse query parameters
try:
page = int(request.args.get('page', 1))
if page < 1:
return _format_response(400, "Invalid parameter: page must be >= 1")
except (ValueError, TypeError):
return _format_response(400, "Invalid parameter: page must be a valid integer")
try:
per_page = int(request.args.get('per_page', 20))
if per_page < 1 or per_page > 100:
return _format_response(400, "Invalid parameter: per_page must be between 1 and 100")
except (ValueError, TypeError):
return _format_response(400, "Invalid parameter: per_page must be a valid integer")
sort_by = request.args.get('sort_by', 'created_at')
sort_order = request.args.get('sort_order', 'desc')
status_filter = request.args.get('status', None) # 'completed', 'failed', or None for all
# Validate sort parameters
valid_sort_fields = ['created_at', 'task_id', 'file_size', 'processing_time']
if sort_by not in valid_sort_fields:
return _format_response(400, f"Invalid parameter: sort_by must be one of {valid_sort_fields}")
if sort_order not in ['asc', 'desc']:
return _format_response(400, "Invalid parameter: sort_order must be 'asc' or 'desc'")
# Validate status filter
valid_statuses = ['completed', 'failed', None]
if status_filter is not None and status_filter not in ['completed', 'failed']:
return _format_response(400, "Invalid parameter: status must be 'completed', 'failed', or not specified")
# 兼容缺省:优先 app.config,其次 Config.OUTPUT_FOLDER
output_root = current_app.config.get('OUTPUT_FOLDER') or getattr(Config, 'OUTPUT_FOLDER', None)
if not output_root:
return _format_response(200, "报告列表获取成功", {
'reports': [],
'pagination': {'page': page, 'per_page': per_page, 'total_reports': 0, 'total_pages': 0, 'has_next': False, 'has_prev': False},
'filters': {'sort_by': sort_by, 'sort_order': sort_order, 'status': status_filter}
})
output_folder = Path(output_root)
reports = []
# Scan all task directories
if output_folder.exists():
for task_dir in output_folder.iterdir():
if not task_dir.is_dir():
continue
task_id = task_dir.name
# Get task information from global task_status
task_info = task_status.get(task_id, {})
task_status_value = task_info.get('status')
# Log task status for debugging
logger.debug(f"Task {task_id}: status from memory={task_status_value}, info={task_info}")
# 直接扫描平铺文件
files = [p for p in task_dir.iterdir() if p.is_file()]
if not files:
# 按需应用状态过滤
if status_filter:
continue
reports.append({
'task_id': task_id,
'report_name': "N/A",
'status': 'failed',
'created_at': task_dir.stat().st_mtime,
'file_count': 0,
'total_size': 0,
'processing_time_seconds': None,
'main_report': None,
'all_files': [],
'run_directory': f'{task_id}'
})
continue
# 识别主报告与统计
total_size = sum(f.stat().st_size for f in files)
created_at = max(f.stat().st_mtime for f in files) if files else task_dir.stat().st_mtime
def file_entry(p):
return {
'name': p.name,
'size': p.stat().st_size,
'type': _get_file_type(p.name),
# 使用相对路径下载,清晰且安全
'download_url': f"/download/{task_id}/{p.name}"
}
all_files = [file_entry(f) for f in files]
# 优先 CO2_report,其次任意 *_report_*.html
report_html = None
for f in files:
if f.name.endswith('_report_') and f.suffix == '.html':
report_html = file_entry(f)
break
if not report_html:
for f in files:
if f.name.endswith('.html'):
report_html = file_entry(f)
break
# 任务状态:若有报告或关键产物则视为 completed
has_outputs = any(f.name.startswith(('config_', 'output_vars_', 'processed_data_', 'CO2_report_')) for f in files)
task_status_value = 'completed' if has_outputs else 'unknown'
if status_filter and task_status_value != status_filter:
continue
# Create report entry
report_entry = {
'task_id': task_id,
'report_name': task_id,
'status': task_status_value,
'created_at': created_at,
'file_count': len(files),
'total_size': total_size,
'processing_time_seconds': None,
'main_report': report_html,
'all_files': all_files,
'run_directory': f'{task_id}'
}
reports.append(report_entry)
# Sort reports
reverse_order = sort_order == 'desc'
if sort_by == 'created_at':
reports.sort(key=lambda x: x['created_at'], reverse=reverse_order)
elif sort_by == 'task_id':
reports.sort(key=lambda x: x['task_id'], reverse=reverse_order)
elif sort_by == 'file_size':
reports.sort(key=lambda x: x['total_size'], reverse=reverse_order)
elif sort_by == 'processing_time':
reports.sort(key=lambda x: x['processing_time_seconds'] or 0, reverse=reverse_order)
# Paginate results
total_reports = len(reports)
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
paginated_reports = reports[start_idx:end_idx]
# Calculate pagination metadata
total_pages = (total_reports + per_page - 1) // per_page
response_data = {
'reports': paginated_reports,
'pagination': {
'page': page,
'per_page': per_page,
'total_reports': total_reports,
'total_pages': total_pages,
'has_next': page < total_pages,
'has_prev': page > 1
},
'filters': {
'sort_by': sort_by,
'sort_order': sort_order,
'status': status_filter
}
}
logger.info(f"Returning {len(paginated_reports)} reports (page {page}/{total_pages})")
return _format_response(200, "报告列表获取成功", response_data)
except Exception as e:
logger.error(f"Error listing reports: {str(e)}", exc_info=True)
return _format_response(500, "内部服务器错误")