修复历史记录批量下载的bug

This commit is contained in:
2026-08-02 09:38:48 +08:00
parent 9516078b1d
commit 17b7cd27b9
3 changed files with 279 additions and 147 deletions

View File

@@ -6,7 +6,7 @@ import zipfile
from datetime import datetime
from flask import request, jsonify, send_from_directory
from lib.db import execute_query
from lib.logger import log_info, log_error
from lib.logger import log_info, log_error, log_warning
from . import download_bp
@@ -120,31 +120,32 @@ def batch_download():
# 清理旧的临时文件
cleanup_old_temp_zips()
# 查询所有成功的历史记录
# 查询所有成功的历史记录(附带筛选条件以增强安全性)
placeholders = ', '.join(['%s'] * len(ids))
result = execute_query(
f'SELECT id, file_path, config_name, report_date, status FROM t_daily_report_history WHERE id IN ({placeholders})',
f'SELECT id, file_path, config_name, report_date, status, split_name FROM t_daily_report_history WHERE id IN ({placeholders})',
tuple(ids)
)
if not result:
return jsonify({'success': False, 'message': '未找到对应的历史记录'}), 404
total_matched = len(result)
success_records = [r for r in result if r.get('status') == 1]
skipped_failed = total_matched - len(success_records)
reports_dir = get_reports_dir()
temp_dir = get_temp_dir()
# 收集要打包的文件
files_to_zip = []
failed_names = []
used_names = set() # 检测 zip 内文件名冲突
for item in result:
if item.get('status') != 1:
failed_names.append(f"{item.get('config_name', '未知')} (生成失败)")
continue
for item in success_records:
file_path = item.get('file_path')
if not file_path:
failed_names.append(f"{item.get('config_name', '未知')} (无文件)")
failed_names.append(f"{item.get('config_name', '未知')} (无文件路径)")
continue
# 提取文件名
@@ -157,17 +158,27 @@ def batch_download():
full_path = os.path.join(reports_dir, filename)
if not os.path.exists(full_path):
failed_names.append(f"{item.get('config_name', '未知')} (文件不存在)")
failed_names.append(f"{item.get('config_name', '未知')}_{item.get('report_date', '')} (文件不存在)")
continue
# 使用配置名作为zip内的文件名避免重名
# 使用配置名+报表日期+拆分值作为zip内的文件名避免重名
config_name = item.get('config_name', '未知')
report_date = item.get('report_date', '')
ext = os.path.splitext(filename)[1]
split_name = item.get('split_name', '')
ext = os.path.splitext(filename)[1] or '.xlsx'
base_parts = [config_name]
if report_date:
zip_inner_name = f"{config_name}_{report_date}{ext}"
else:
zip_inner_name = f"{config_name}{ext}"
base_parts.append(str(report_date))
if split_name:
base_parts.append(str(split_name))
base_name = '_'.join(base_parts)
zip_inner_name = f"{base_name}{ext}"
# 处理文件名冲突如果同名追加记录ID
if zip_inner_name in used_names:
zip_inner_name = f"{base_name}_{item.get('id', '')}{ext}"
used_names.add(zip_inner_name)
files_to_zip.append({
'full_path': full_path,
@@ -187,16 +198,23 @@ def batch_download():
for file_info in files_to_zip:
zf.write(file_info['full_path'], file_info['inner_name'])
log_info(f'[下载API] 批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)}', 'download')
log_info(f'[下载API] 批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)},跳过 {skipped_failed} 个失败记录', 'download')
if failed_names:
log_warning(f'[下载API] 缺失/无效文件列表: {failed_names[:20]}', 'download')
# 返回zip文件
# 返回zip文件,通过响应头返回统计信息
from flask import send_file
return send_file(
response = send_file(
zip_path,
as_attachment=True,
download_name=zip_filename,
mimetype='application/zip'
)
response.headers['X-Total-Matched'] = str(total_matched)
response.headers['X-Files-Zipped'] = str(len(files_to_zip))
response.headers['X-Files-Missing'] = str(len(failed_names))
response.headers['X-Skipped-Failed'] = str(skipped_failed)
return response
except Exception as e:
log_error(f'[下载API] 批量下载错误: {e}', 'download')
import traceback
@@ -235,9 +253,8 @@ def batch_download_by_filter():
where_clauses.append('DATE(end_time) <= %s')
params.append(end_date)
if status is not None and status != '':
where_clauses.append('status = %s')
params.append(int(status))
# 注意批量导出只导出成功记录status=1忽略用户传入的 status 筛选
# 因为失败的记录没有文件可下载
if config_name:
where_clauses.append('config_name LIKE %s')
@@ -245,9 +262,17 @@ def batch_download_by_filter():
where_sql = ' AND '.join(where_clauses)
# 查询所有匹配的成功记录
# 查询总数,用于日志记录
count_result = execute_query(
f'SELECT COUNT(*) as total FROM t_daily_report_history WHERE {where_sql}',
tuple(params)
)
total_matched = count_result[0]['total'] if count_result else 0
log_info(f'[下载API] 按筛选匹配到 {total_matched} 条成功记录', 'download')
# 查询所有匹配的成功记录(按报表日期正序排列,方便用户查看)
result = execute_query(
f'SELECT id, file_path, config_name, report_date, status FROM t_daily_report_history WHERE {where_sql} ORDER BY create_time DESC',
f'SELECT id, file_path, config_name, report_date, status, split_name FROM t_daily_report_history WHERE {where_sql} ORDER BY report_date ASC, config_name ASC, id ASC',
tuple(params)
)
@@ -260,11 +285,12 @@ def batch_download_by_filter():
# 收集要打包的文件
files_to_zip = []
failed_names = []
used_names = set() # 用于检测 zip 内文件名冲突
for item in result:
file_path = item.get('file_path')
if not file_path:
failed_names.append(f"{item.get('config_name', '未知')} (无文件)")
failed_names.append(f"{item.get('config_name', '未知')}_{item.get('report_date', '')} (无文件路径)")
continue
# 提取文件名
@@ -277,25 +303,40 @@ def batch_download_by_filter():
full_path = os.path.join(reports_dir, filename)
if not os.path.exists(full_path):
failed_names.append(f"{item.get('config_name', '未知')} (文件不存在)")
failed_names.append(f"{item.get('config_name', '未知')}_{item.get('report_date', '')} (文件不存在)")
continue
# 使用配置名作为zip内文件名避免重名
# 构建 zip 内文件名配置名_报表日期避免重名
config_name_val = item.get('config_name', '未知')
report_date = item.get('report_date', '')
ext = os.path.splitext(filename)[1]
split_name = item.get('split_name', '')
ext = os.path.splitext(filename)[1] or '.xlsx'
# 基础名称配置名_报表日期
base_parts = [config_name_val]
if report_date:
zip_inner_name = f"{config_name_val}_{report_date}{ext}"
else:
zip_inner_name = f"{config_name_val}{ext}"
base_parts.append(str(report_date))
if split_name:
base_parts.append(str(split_name))
base_name = '_'.join(base_parts)
zip_inner_name = f"{base_name}{ext}"
# 处理文件名冲突如果同名追加记录ID
if zip_inner_name in used_names:
zip_inner_name = f"{base_name}_{item.get('id', '')}{ext}"
used_names.add(zip_inner_name)
files_to_zip.append({
'full_path': full_path,
'inner_name': zip_inner_name
})
log_info(f'[下载API] 匹配 {total_matched} 条记录,成功打包 {len(files_to_zip)} 个文件,缺失 {len(failed_names)} 个文件', 'download')
if failed_names:
log_warning(f'[下载API] 缺失的文件列表: {failed_names[:20]}', 'download')
if len(files_to_zip) == 0:
return jsonify({'success': False, 'message': '没有可下载的文件'}), 400
return jsonify({'success': False, 'message': '没有可下载的文件(匹配的记录文件均不存在)'}), 400
# 生成zip文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
@@ -309,16 +350,78 @@ def batch_download_by_filter():
log_info(f'[下载API] 按筛选批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)}', 'download')
# 返回zip文件
# 返回zip文件,通过自定义头部返回统计信息
from flask import send_file
return send_file(
response = send_file(
zip_path,
as_attachment=True,
download_name=zip_filename,
mimetype='application/zip'
)
# 通过响应头返回统计信息,便于前端诊断
response.headers['X-Total-Matched'] = str(total_matched)
response.headers['X-Files-Zipped'] = str(len(files_to_zip))
response.headers['X-Files-Missing'] = str(len(failed_names))
return response
except Exception as e:
log_error(f'[下载API] 按筛选批量下载错误: {e}', 'download')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500
@download_bp.route('/download/batch-filter-count', methods=['POST'])
def batch_download_filter_count():
"""按筛选条件统计可导出的记录数
在批量导出前调用,让用户确认导出范围。
"""
try:
data = request.get_json(silent=True) or {}
config_name = data.get('config_name', '').strip()
start_date = data.get('start_date', '').strip()
end_date = data.get('end_date', '').strip()
# 构建查询条件(与批量导出完全一致,只统计成功记录)
where_clauses = ['status = 1']
params = []
if start_date:
where_clauses.append('DATE(start_time) >= %s')
params.append(start_date)
if end_date:
where_clauses.append('DATE(end_time) <= %s')
params.append(end_date)
if config_name:
where_clauses.append('config_name LIKE %s')
params.append(f'%{config_name}%')
where_sql = ' AND '.join(where_clauses)
# 统计总记录数
count_result = execute_query(
f'SELECT COUNT(*) as total FROM t_daily_report_history WHERE {where_sql}',
tuple(params)
)
total = count_result[0]['total'] if count_result else 0
# 统计日期范围
date_range = execute_query(
f'SELECT MIN(report_date) as min_date, MAX(report_date) as max_date FROM t_daily_report_history WHERE {where_sql}',
tuple(params)
)
min_date = date_range[0]['min_date'] if date_range else None
max_date = date_range[0]['max_date'] if date_range else None
return jsonify({
'success': True,
'data': {
'total': total,
'min_date': str(min_date) if min_date else '',
'max_date': str(max_date) if max_date else ''
}
})
except Exception as e:
log_error(f'[下载API] 统计可导出记录数错误: {e}', 'download')
return jsonify({'success': False, 'message': str(e)}), 500