增加配置的导入和导出功能

This commit is contained in:
2026-07-14 16:30:26 +08:00
parent cc2214ccf6
commit 9c3c1e26cc
3 changed files with 296 additions and 63 deletions

View File

@@ -274,38 +274,92 @@ def move_config(config_id):
return jsonify({'success': False, 'message': str(e)}), 500
EXCEL_COLUMNS = [
{'key': 'config_name', 'name': '配置名称', 'required': True},
{'key': 'split_type', 'name': '拆分方式', 'required': True, 'comment': 'company_id=按企业, user_id=按用户, station_id=按场站'},
{'key': 'split_value', 'name': '拆分值', 'required': True, 'comment': '多个值用英文逗号分隔'},
{'key': 'split_name', 'name': '拆分显示名称', 'required': False},
{'key': 'selected_fields', 'name': '选择字段(JSON数组)', 'required': True, 'comment': '如: ["charge_degree","total_energy","charge_money"]'},
{'key': 'sum_fields', 'name': '求和字段(JSON数组)', 'required': False, 'comment': '如: ["peak_charge","flat_charge","valley_charge","charge_money"]'},
{'key': 'time_periods', 'name': '分时段配置(JSON对象)', 'required': False, 'comment': '如: {"":{"start":"10:00","end":"12:00"}}'},
{'key': 'merge_telecom', 'name': '是否合并特来电', 'required': False, 'comment': '0=否, 1=是'},
{'key': 'telecom_vehicle_no', 'name': '特来电自编号', 'required': False},
{'key': 'field_custom_names', 'name': '字段自定义表头(JSON对象)', 'required': False, 'comment': '如: {"charge_degree":"充电量(kWh)"}'},
{'key': 'show_monthly_total', 'name': '是否显示月度总计', 'required': False, 'comment': '0=否, 1=是'},
{'key': 'is_active', 'name': '是否启用', 'required': False, 'comment': '0=禁用, 1=启用'},
{'key': 'sort_order', 'name': '排序号', 'required': False, 'comment': '数字越小越靠前'},
]
@config_bp.route('/config/export', methods=['GET'])
def export_configs():
"""导出所有配置为JSON文件"""
"""导出所有配置为Excel文件"""
try:
from flask import Response
import os
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from io import BytesIO
from urllib.parse import quote
configs = execute_query(
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id ASC'
)
export_list = []
for config in configs:
export_item = {}
for key, value in config.items():
if key == 'id':
continue
export_item[key] = value
export_list.append(export_item)
wb = Workbook()
ws = wb.active
ws.title = '日报配置'
export_data = {
'version': '1.0',
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'total': len(export_list),
'configs': export_list
}
header_font = Font(bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(
left=Side(style='thin', color='000000'),
right=Side(style='thin', color='000000'),
top=Side(style='thin', color='000000'),
bottom=Side(style='thin', color='000000')
)
even_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
filename = f'日报配置导出_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
for col_idx, col in enumerate(EXCEL_COLUMNS, 1):
cell = ws.cell(row=1, column=col_idx)
cell.value = col['name']
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
ws.column_dimensions[cell.column_letter].width = max(len(col['name']) * 2 + 4, 18)
response = Response(json_str, mimetype='application/json')
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
ws.row_dimensions[1].height = 30
for row_idx, config in enumerate(configs, 2):
is_even = (row_idx - 2) % 2 == 1
for col_idx, col in enumerate(EXCEL_COLUMNS, 1):
cell = ws.cell(row=row_idx, column=col_idx)
value = config.get(col['key'], '')
if value is None:
value = ''
if isinstance(value, datetime):
value = value.strftime('%Y-%m-%d %H:%M:%S')
if isinstance(value, list) or isinstance(value, dict):
value = json.dumps(value, ensure_ascii=False)
cell.value = str(value) if value is not None else ''
cell.border = thin_border
cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
if is_even:
cell.fill = even_fill
ws.freeze_panes = 'A2'
output = BytesIO()
wb.save(output)
output.seek(0)
filename = f'日报配置导出_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx'
response = Response(
output.read(),
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response.headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(filename)}"
return response
except Exception as e:
import traceback
@@ -313,9 +367,122 @@ def export_configs():
return jsonify({'success': False, 'message': f'导出失败: {str(e)}'}), 500
@config_bp.route('/config/template', methods=['GET'])
def download_template():
"""下载导入模板"""
try:
from flask import Response
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from io import BytesIO
from urllib.parse import quote
wb = Workbook()
ws = wb.active
ws.title = '日报配置'
header_font = Font(bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
comment_font = Font(color='FF0000', size=9)
thin_border = Border(
left=Side(style='thin', color='000000'),
right=Side(style='thin', color='000000'),
top=Side(style='thin', color='000000'),
bottom=Side(style='thin', color='000000')
)
for col_idx, col in enumerate(EXCEL_COLUMNS, 1):
cell = ws.cell(row=1, column=col_idx)
title = col['name']
if col.get('required'):
title = '*' + title
cell.value = title
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
ws.column_dimensions[cell.column_letter].width = max(len(col['name']) * 2 + 4, 20)
ws.row_dimensions[1].height = 30
for col_idx, col in enumerate(EXCEL_COLUMNS, 1):
cell = ws.cell(row=2, column=col_idx)
if col.get('comment'):
cell.value = col['comment']
cell.font = comment_font
cell.fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
else:
cell.value = ''
cell.border = thin_border
cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
ws.row_dimensions[2].height = 40
example_row = 3
example_data = {
'config_name': '示例-企业日报',
'split_type': 'company_id',
'split_value': '1001,1002',
'split_name': '',
'selected_fields': '["charge_degree","total_energy","charge_money"]',
'sum_fields': '["peak_charge","flat_charge","valley_charge","charge_money"]',
'time_periods': '{"":{"start":"10:00","end":"12:00"},"":{"start":"08:00","end":"10:00"}}',
'merge_telecom': '0',
'telecom_vehicle_no': '',
'field_custom_names': '{"charge_degree":"充电量(kWh)"}',
'show_monthly_total': '1',
'is_active': '0',
'sort_order': '1',
}
for col_idx, col in enumerate(EXCEL_COLUMNS, 1):
cell = ws.cell(row=example_row, column=col_idx)
cell.value = example_data.get(col['key'], '')
cell.border = thin_border
cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
cell.font = Font(color='808080', size=10)
ws.freeze_panes = 'A3'
ws2 = wb.create_sheet('填写说明')
instructions = [
['日报配置导入模板填写说明', ''],
['', ''],
['1. 带 * 号的列为必填项', ''],
['2. 拆分方式可选值', 'company_id=按企业, user_id=按用户, station_id=按场站'],
['3. 拆分值', '多个值用英文逗号分隔,如: 1001,1002,1003'],
['4. JSON格式字段', '选择字段、求和字段、分时段配置、自定义表头都需要填写JSON格式字符串'],
['5. 是否类字段', '填 0 表示否,填 1 表示是'],
['6. 导入规则', '导入后配置名称自动添加"_导入"后缀,默认禁用,排序号追加到末尾'],
['7. 注意事项', '请勿修改第一行表头,第二行为说明行,从第三行开始填写数据(第三行为示例)'],
]
for row_idx, (key, val) in enumerate(instructions, 1):
ws2.cell(row=row_idx, column=1, value=key)
ws2.cell(row=row_idx, column=2, value=val)
ws2.cell(row=row_idx, column=1).font = Font(bold=True if row_idx == 1 else False)
ws2.column_dimensions['A'].width = 25
ws2.column_dimensions['B'].width = 60
output = BytesIO()
wb.save(output)
output.seek(0)
filename = '日报配置导入模板.xlsx'
response = Response(
output.read(),
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response.headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(filename)}"
return response
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': f'模板下载失败: {str(e)}'}), 500
@config_bp.route('/config/import', methods=['POST'])
def import_configs():
"""JSON文件导入配置"""
"""Excel文件导入配置"""
try:
if 'file' not in request.files:
return jsonify({'success': False, 'message': '请选择要导入的文件'}), 400
@@ -324,18 +491,40 @@ def import_configs():
if file.filename == '':
return jsonify({'success': False, 'message': '请选择要导入的文件'}), 400
if not file.filename.endswith('.json'):
return jsonify({'success': False, 'message': '只支持 JSON 格式的配置文件'}), 400
if not (file.filename.endswith('.xlsx') or file.filename.endswith('.xls')):
return jsonify({'success': False, 'message': '只支持 Excel 格式(.xlsx/.xls的配置文件'}), 400
file_content = file.read().decode('utf-8')
import_data = json.loads(file_content)
from openpyxl import load_workbook
from io import BytesIO
if 'configs' not in import_data:
return jsonify({'success': False, 'message': '文件格式不正确,缺少 configs 字段'}), 400
wb = load_workbook(BytesIO(file.read()), data_only=True)
ws = wb.active
configs_to_import = import_data['configs']
if not isinstance(configs_to_import, list) or len(configs_to_import) == 0:
return jsonify({'success': False, 'message': '没有可导入的配置'}), 400
headers = []
for cell in ws[1]:
headers.append(str(cell.value).strip() if cell.value else '')
col_key_map = {}
for col in EXCEL_COLUMNS:
if col['name'] in headers:
col_idx = headers.index(col['name'])
col_key_map[col['key']] = col_idx
configs_to_import = []
for row in ws.iter_rows(min_row=3, values_only=True):
row_data = {}
has_data = False
for key, col_idx in col_key_map.items():
value = row[col_idx] if col_idx < len(row) else None
if value is not None and str(value).strip():
has_data = True
row_data[key] = str(value).strip() if value is not None else ''
if has_data and row_data.get('config_name'):
configs_to_import.append(row_data)
if not configs_to_import:
return jsonify({'success': False, 'message': '没有可导入的配置请从第3行开始填写数据'}), 400
max_sort_result = execute_query('SELECT MAX(sort_order) as max_sort FROM t_daily_report_config')
current_max_sort = max_sort_result[0]['max_sort'] or 0
@@ -346,42 +535,75 @@ def import_configs():
for idx, config_data in enumerate(configs_to_import):
try:
config_name = config_data.get('config_name', '')
split_type = config_data.get('split_type', '')
split_value = config_data.get('split_value', '')
config_name = config_data.get('config_name', '').strip()
split_type = config_data.get('split_type', '').strip()
split_value = config_data.get('split_value', '').strip()
if not config_name or not split_type or not split_value:
if not config_name:
fail_count += 1
fail_messages.append(f'{idx+1}:缺少必填字段')
fail_messages.append(f'{idx+3}:缺少配置名称')
continue
if not split_type:
fail_count += 1
fail_messages.append(f'{idx+3}行:缺少拆分方式')
continue
if not split_value:
fail_count += 1
fail_messages.append(f'{idx+3}行:缺少拆分值')
continue
new_id = int(datetime.now().timestamp() * 1000) + idx
new_name = f"{config_name}_导入"
sort_order = current_max_sort + 1
import_sort_order = config_data.get('sort_order', '').strip()
if import_sort_order and import_sort_order.isdigit():
sort_order = current_max_sort + 1
else:
sort_order = current_max_sort + 1
current_max_sort += 1
selected_fields = config_data.get('selected_fields', '[]')
if isinstance(selected_fields, str):
selected_fields = json.loads(selected_fields) if selected_fields else []
selected_fields_str = config_data.get('selected_fields', '[]').strip()
try:
selected_fields = json.loads(selected_fields_str) if selected_fields_str else []
except Exception:
selected_fields = []
sum_fields = config_data.get('sum_fields', '[]')
if isinstance(sum_fields, str):
sum_fields = json.loads(sum_fields) if sum_fields else []
sum_fields_str = config_data.get('sum_fields', '[]').strip()
try:
sum_fields = json.loads(sum_fields_str) if sum_fields_str else []
except Exception:
sum_fields = []
time_periods = config_data.get('time_periods', '{}')
if isinstance(time_periods, str):
time_periods = json.loads(time_periods) if time_periods else {}
time_periods_str = config_data.get('time_periods', '{}').strip()
try:
time_periods = json.loads(time_periods_str) if time_periods_str else {}
except Exception:
time_periods = {}
field_custom_names = config_data.get('field_custom_names')
if isinstance(field_custom_names, str):
field_custom_names = json.loads(field_custom_names) if field_custom_names else None
field_custom_names_str = config_data.get('field_custom_names', '').strip()
if field_custom_names_str:
try:
field_custom_names = json.loads(field_custom_names_str)
except Exception:
field_custom_names = None
else:
field_custom_names = None
merge_telecom = config_data.get('merge_telecom', '0').strip()
merge_telecom = 1 if merge_telecom in ('1', '', 'true', 'True') else 0
show_monthly_total = config_data.get('show_monthly_total', '0').strip()
show_monthly_total = 1 if show_monthly_total in ('1', '', 'true', 'True') else 0
is_active = config_data.get('is_active', '0').strip()
is_active = 1 if is_active in ('1', '', 'true', 'True') else 0
sql = """
INSERT INTO t_daily_report_config
(id, config_name, split_type, split_value, split_name, selected_fields,
sum_fields, time_periods, merge_telecom, telecom_vehicle_no, field_custom_names,
show_monthly_total, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0, %s, NOW(), NOW())
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
"""
params = (
@@ -393,10 +615,11 @@ def import_configs():
json.dumps(selected_fields, ensure_ascii=False),
json.dumps(sum_fields, ensure_ascii=False),
json.dumps(time_periods, ensure_ascii=False),
config_data.get('merge_telecom', 0),
merge_telecom,
config_data.get('telecom_vehicle_no', ''),
json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None,
config_data.get('show_monthly_total', 0),
show_monthly_total,
is_active,
sort_order
)
@@ -404,7 +627,7 @@ def import_configs():
success_count += 1
except Exception as item_error:
fail_count += 1
fail_messages.append(f'{idx+1}{str(item_error)}')
fail_messages.append(f'{idx+3}{str(item_error)}')
return jsonify({
'success': True,