Files
ylt_diy/lib/api/config_api.py

645 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
配置管理 API
"""
import json
from datetime import datetime
from flask import request, jsonify
from lib.db import execute_query, execute_update, execute_insert
from . import config_bp
@config_bp.route('/config', methods=['GET'])
def get_configs():
"""获取所有配置"""
try:
configs = execute_query(
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id DESC'
)
# 解析 JSON 字段
for config in configs:
config['selected_fields'] = json.loads(config['selected_fields'])
config['sum_fields'] = json.loads(config['sum_fields']) if config['sum_fields'] else []
config['time_periods'] = json.loads(config['time_periods']) if config.get('time_periods') else {}
return jsonify({'success': True, 'data': configs})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config', methods=['POST'])
def create_config():
"""创建配置"""
try:
data = request.json
config_name = data.get('config_name')
split_type = data.get('split_type')
split_value = data.get('split_value')
split_name = data.get('split_name', '')
selected_fields = data.get('selected_fields', [])
sum_fields = data.get('sum_fields', [])
time_periods = data.get('time_periods', {})
merge_telecom = 1 if data.get('merge_telecom') else 0
telecom_vehicle_no = data.get('telecom_vehicle_no', '')
field_custom_names = data.get('field_custom_names', {})
show_monthly_total = 1 if data.get('show_monthly_total') else 0
if not config_name or not split_type or not split_value:
return jsonify({'success': False, 'message': '缺少必填字段'}), 400
if not selected_fields:
return jsonify({'success': False, 'message': '请至少选择一个字段'}), 400
# 生成 ID
config_id = int(datetime.now().timestamp() * 1000)
# 获取最大排序值
max_sort = execute_query('SELECT MAX(sort_order) as max_sort FROM t_daily_report_config')
sort_order = (max_sort[0]['max_sort'] or 0) + 1
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, 1, %s, NOW(), NOW())
"""
params = (
config_id,
config_name,
split_type,
split_value,
split_name,
json.dumps(selected_fields, ensure_ascii=False),
json.dumps(sum_fields, ensure_ascii=False),
json.dumps(time_periods, ensure_ascii=False),
merge_telecom,
telecom_vehicle_no,
json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None,
show_monthly_total,
sort_order
)
execute_insert(sql, params)
return jsonify({
'success': True,
'message': '配置创建成功',
'data': {'id': config_id}
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config/<int:config_id>', methods=['PUT'])
def update_config(config_id):
"""更新配置"""
try:
data = request.json
config_name = data.get('config_name')
split_type = data.get('split_type')
split_value = data.get('split_value')
split_name = data.get('split_name', '')
selected_fields = data.get('selected_fields', [])
sum_fields = data.get('sum_fields', [])
time_periods = data.get('time_periods', {})
merge_telecom = 1 if data.get('merge_telecom') else 0
telecom_vehicle_no = data.get('telecom_vehicle_no', '')
field_custom_names = data.get('field_custom_names', {})
show_monthly_total = 1 if data.get('show_monthly_total') else 0
sql = """
UPDATE t_daily_report_config
SET config_name = %s, split_type = %s, split_value = %s, split_name = %s,
selected_fields = %s, sum_fields = %s, time_periods = %s,
merge_telecom = %s, telecom_vehicle_no = %s, field_custom_names = %s,
show_monthly_total = %s, update_time = NOW()
WHERE id = %s
"""
params = (
config_name,
split_type,
split_value,
split_name,
json.dumps(selected_fields, ensure_ascii=False),
json.dumps(sum_fields, ensure_ascii=False),
json.dumps(time_periods, ensure_ascii=False),
merge_telecom,
telecom_vehicle_no,
json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None,
show_monthly_total,
config_id
)
execute_update(sql, params)
return jsonify({'success': True, 'message': '配置更新成功'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config/<int:config_id>', methods=['DELETE'])
def delete_config(config_id):
"""删除配置"""
try:
execute_update('DELETE FROM t_daily_report_config WHERE id = %s', (config_id,))
return jsonify({'success': True, 'message': '配置删除成功'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config/<int:config_id>/copy', methods=['POST'])
def copy_config(config_id):
"""复制配置"""
try:
# 获取原配置
configs = execute_query('SELECT * FROM t_daily_report_config WHERE id = %s', (config_id,))
if not configs:
return jsonify({'success': False, 'message': '原配置不存在'}), 404
original = configs[0]
# 生成新 ID 和新名称
new_id = int(datetime.now().timestamp() * 1000)
new_name = f"{original['config_name']}_副本"
# 获取最大排序值
max_sort = execute_query('SELECT MAX(sort_order) as max_sort FROM t_daily_report_config')
sort_order = (max_sort[0]['max_sort'] or 0) + 1
# 插入新配置
execute_insert(
'''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, %s, %s, NOW(), NOW())''',
(
new_id,
new_name,
original['split_type'],
original['split_value'],
original['split_name'],
original['selected_fields'],
original.get('sum_fields', ''),
original.get('time_periods', ''),
original.get('merge_telecom', 0),
original.get('telecom_vehicle_no', ''),
original.get('field_custom_names', None),
original.get('show_monthly_total', 0),
original['is_active'],
sort_order
)
)
return jsonify({'success': True, 'message': '配置复制成功', 'id': new_id})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config/<int:config_id>/toggle', methods=['POST'])
def toggle_config(config_id):
"""启用/禁用配置"""
try:
execute_update(
'UPDATE t_daily_report_config SET is_active = NOT is_active, update_time = NOW() WHERE id = %s',
(config_id,)
)
return jsonify({'success': True, 'message': '操作成功'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config/<int:config_id>/move', methods=['POST'])
def move_config(config_id):
"""移动配置排序"""
try:
direction = request.json.get('direction') # up 或 down
print(f"[move_config] 开始移动 config_id={config_id}, direction={direction}")
# 查出所有配置,按 sort_order, id 排序
all_configs = execute_query(
'SELECT id, sort_order FROM t_daily_report_config ORDER BY sort_order ASC, id ASC'
)
print(f"[move_config] 所有配置数量: {len(all_configs)}")
for idx, c in enumerate(all_configs):
print(f" [{idx}] id={c['id']}, sort_order={c['sort_order']}")
# 找到当前配置的索引
current_index = None
for i, c in enumerate(all_configs):
if c['id'] == config_id:
current_index = i
break
if current_index is None:
print(f"[move_config] 未找到配置 id={config_id}")
return jsonify({'success': False, 'message': '配置不存在'}), 404
print(f"[move_config] 当前配置索引: {current_index}")
# 计算目标索引
if direction == 'up':
target_index = current_index - 1
elif direction == 'down':
target_index = current_index + 1
else:
return jsonify({'success': False, 'message': '无效的移动方向'}), 400
# 边界检查
if target_index < 0 or target_index >= len(all_configs):
print(f"[move_config] 已到边界,无法移动 target_index={target_index}")
return jsonify({'success': True, 'message': '已到边界'})
# 在内存数组中交换位置
all_configs[current_index], all_configs[target_index] = all_configs[target_index], all_configs[current_index]
print(f"[move_config] 交换后顺序: {[c['id'] for c in all_configs]}")
# 全量重排:按新顺序给每个配置重新赋值 sort_order
for idx, cfg in enumerate(all_configs):
new_sort = idx + 1
print(f" 更新 id={cfg['id']}: sort_order {cfg['sort_order']} -> {new_sort}")
execute_update(
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
(new_sort, cfg['id'])
)
print("[move_config] 重排完成")
return jsonify({'success': True, 'message': '移动成功'})
except Exception as e:
import traceback
traceback.print_exc()
print(f"[move_config] 异常: {e}")
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():
"""导出所有配置为Excel文件"""
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
configs = execute_query(
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id ASC'
)
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)
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')
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)
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
traceback.print_exc()
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():
"""从Excel文件导入配置"""
try:
if 'file' not in request.files:
return jsonify({'success': False, 'message': '请选择要导入的文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': '请选择要导入的文件'}), 400
if not (file.filename.endswith('.xlsx') or file.filename.endswith('.xls')):
return jsonify({'success': False, 'message': '只支持 Excel 格式(.xlsx/.xls的配置文件'}), 400
from openpyxl import load_workbook
from io import BytesIO
wb = load_workbook(BytesIO(file.read()), data_only=True)
ws = wb.active
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
success_count = 0
fail_count = 0
fail_messages = []
for idx, config_data in enumerate(configs_to_import):
try:
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:
fail_count += 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}_导入"
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_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_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_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_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, %s, %s, NOW(), NOW())
"""
params = (
new_id,
new_name,
split_type,
split_value,
config_data.get('split_name', ''),
json.dumps(selected_fields, ensure_ascii=False),
json.dumps(sum_fields, ensure_ascii=False),
json.dumps(time_periods, ensure_ascii=False),
merge_telecom,
config_data.get('telecom_vehicle_no', ''),
json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None,
show_monthly_total,
is_active,
sort_order
)
execute_insert(sql, params)
success_count += 1
except Exception as item_error:
fail_count += 1
fail_messages.append(f'{idx+3}行:{str(item_error)}')
return jsonify({
'success': True,
'message': f'导入完成:成功 {success_count} 条,失败 {fail_count}',
'data': {
'success_count': success_count,
'fail_count': fail_count,
'fail_messages': fail_messages
}
})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': f'导入失败: {str(e)}'}), 500