增加序号,配置导出,导入,求和采集数据导出

This commit is contained in:
2026-07-14 16:05:43 +08:00
parent fdd232989b
commit cc2214ccf6
9 changed files with 587 additions and 42 deletions

View File

@@ -272,3 +272,150 @@ def move_config(config_id):
traceback.print_exc() traceback.print_exc()
print(f"[move_config] 异常: {e}") print(f"[move_config] 异常: {e}")
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@config_bp.route('/config/export', methods=['GET'])
def export_configs():
"""导出所有配置为JSON文件"""
try:
from flask import Response
import os
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)
export_data = {
'version': '1.0',
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'total': len(export_list),
'configs': export_list
}
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
filename = f'日报配置导出_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
response = Response(json_str, mimetype='application/json')
response.headers['Content-Disposition'] = f'attachment; filename="{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文件导入配置"""
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('.json'):
return jsonify({'success': False, 'message': '只支持 JSON 格式的配置文件'}), 400
file_content = file.read().decode('utf-8')
import_data = json.loads(file_content)
if 'configs' not in import_data:
return jsonify({'success': False, 'message': '文件格式不正确,缺少 configs 字段'}), 400
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
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', '')
split_type = config_data.get('split_type', '')
split_value = config_data.get('split_value', '')
if not config_name or not split_type or not split_value:
fail_count += 1
fail_messages.append(f'{idx+1}条:缺少必填字段')
continue
new_id = int(datetime.now().timestamp() * 1000) + idx
new_name = f"{config_name}_导入"
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 []
sum_fields = config_data.get('sum_fields', '[]')
if isinstance(sum_fields, str):
sum_fields = json.loads(sum_fields) if sum_fields else []
time_periods = config_data.get('time_periods', '{}')
if isinstance(time_periods, str):
time_periods = json.loads(time_periods) if time_periods else {}
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
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())
"""
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),
config_data.get('merge_telecom', 0),
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),
sort_order
)
execute_insert(sql, params)
success_count += 1
except Exception as item_error:
fail_count += 1
fail_messages.append(f'{idx+1}条:{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

View File

@@ -9,7 +9,8 @@ from lib.sum_data_collector import (
update_sum_data, update_sum_data,
delete_sum_data, delete_sum_data,
batch_delete_sum_data, batch_delete_sum_data,
get_monthly_charge_degree_total get_monthly_charge_degree_total,
export_sum_data_to_excel
) )
from . import sum_data_bp from . import sum_data_bp
@@ -38,7 +39,8 @@ def list_sum_data():
try: try:
page = int(request.args.get('page', 1)) page = int(request.args.get('page', 1))
page_size = int(request.args.get('page_size', 20)) page_size = int(request.args.get('page_size', 20))
report_date = request.args.get('report_date') start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
config_id = request.args.get('config_id') config_id = request.args.get('config_id')
config_name = request.args.get('config_name') config_name = request.args.get('config_name')
sum_field_key = request.args.get('sum_field_key') sum_field_key = request.args.get('sum_field_key')
@@ -47,7 +49,8 @@ def list_sum_data():
result = get_sum_data_list( result = get_sum_data_list(
page=page, page=page,
page_size=page_size, page_size=page_size,
report_date=report_date, start_date=start_date,
end_date=end_date,
config_id=config_id, config_id=config_id,
config_name=config_name, config_name=config_name,
sum_field_key=sum_field_key, sum_field_key=sum_field_key,
@@ -152,3 +155,39 @@ def monthly_charge_degree_total():
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@sum_data_bp.route('/sum-data/export', methods=['GET'])
def export_sum_data():
"""导出求和数据为Excel
参数:
start_date: 开始日期(可选)
end_date: 结束日期(可选)
config_id: 配置ID可选
data_source: 数据来源(可选)
keyword: 配置名称关键词(可选)
"""
try:
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
config_id = request.args.get('config_id')
data_source = request.args.get('data_source')
keyword = request.args.get('keyword')
result = export_sum_data_to_excel(
start_date=start_date,
end_date=end_date,
config_id=config_id,
data_source=data_source,
keyword=keyword
)
if result['success']:
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500

View File

@@ -82,6 +82,7 @@ def init_database_tables():
sum_field_name VARCHAR(100) NULL COMMENT '求和字段中文名称', sum_field_name VARCHAR(100) NULL COMMENT '求和字段中文名称',
sum_value DECIMAL(18,3) NULL COMMENT '求和值', sum_value DECIMAL(18,3) NULL COMMENT '求和值',
total_orders INT NULL COMMENT '订单数', total_orders INT NULL COMMENT '订单数',
sort_order INT NULL COMMENT '配置排序号',
data_source VARCHAR(20) NULL COMMENT '数据来源 auto=自动采集 manual=手动编辑', data_source VARCHAR(20) NULL COMMENT '数据来源 auto=自动采集 manual=手动编辑',
remark VARCHAR(500) NULL COMMENT '备注', remark VARCHAR(500) NULL COMMENT '备注',
create_time DATETIME NULL COMMENT '创建时间', create_time DATETIME NULL COMMENT '创建时间',
@@ -107,6 +108,18 @@ def init_database_tables():
else: else:
print(f"[数据库] 新增 show_monthly_total 字段时出错(可能已存在): {e}") print(f"[数据库] 新增 show_monthly_total 字段时出错(可能已存在): {e}")
# 给求和数据表增加 sort_order 字段
try:
execute_update(
"ALTER TABLE t_daily_report_sum_data ADD COLUMN sort_order INT NULL COMMENT '配置排序号' "
)
print("[数据库] 求和数据表新增 sort_order 字段成功")
except Exception as e:
if "Duplicate" in str(e) or "duplicate" in str(e) or "Exists" in str(e) or "exists" in str(e):
pass
else:
print(f"[数据库] 求和数据表新增 sort_order 字段时出错(可能已存在): {e}")
# 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值 # 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值
try: try:
null_count = execute_query( null_count = execute_query(

View File

@@ -474,7 +474,8 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
has_supplement=has_supplement, has_supplement=has_supplement,
show_monthly_total=config.get('show_monthly_total', 0) == 1, show_monthly_total=config.get('show_monthly_total', 0) == 1,
config_id=config['id'], config_id=config['id'],
report_date=start_time.strftime('%Y-%m-%d') report_date=start_time.strftime('%Y-%m-%d'),
sort_order=config.get('sort_order')
) )
print(f"[日报生成] Excel文件已生成: {file_path}") print(f"[日报生成] Excel文件已生成: {file_path}")
@@ -564,7 +565,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
def generate_excel(orders, selected_fields, sum_fields, sum_results, def generate_excel(orders, selected_fields, sum_fields, sum_results,
config_name, split_name, start_time, end_time, config_name, split_name, start_time, end_time,
id_name_maps=None, field_custom_names=None, has_supplement=False, id_name_maps=None, field_custom_names=None, has_supplement=False,
show_monthly_total=False, config_id=None, report_date=None): show_monthly_total=False, config_id=None, report_date=None, sort_order=None):
""" """
生成Excel文件 生成Excel文件
@@ -585,7 +586,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports') reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports')
os.makedirs(reports_dir, exist_ok=True) os.makedirs(reports_dir, exist_ok=True)
# 生成文件名 - 格式配置名称_几月几日消费记录.xlsx # 生成文件名 - 格式:序号_配置名称_几月几日消费记录.xlsx
# 从 start_time 提取日期(报表数据对应的是开始时间那天的数据) # 从 start_time 提取日期(报表数据对应的是开始时间那天的数据)
try: try:
start_dt = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') start_dt = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S')
@@ -595,7 +596,13 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
# 如果有补单,添加标记 # 如果有补单,添加标记
supplement_suffix = '_有补单' if has_supplement else '' supplement_suffix = '_有补单' if has_supplement else ''
filename = f'{config_name}_{date_str}消费记录{supplement_suffix}.xlsx'
# 顺序号前缀(两位数字,不足补零)
sort_prefix = ''
if sort_order:
sort_prefix = f'{sort_order:02d}_'
filename = f'{sort_prefix}{config_name}_{date_str}消费记录{supplement_suffix}.xlsx'
file_path = os.path.join(reports_dir, filename) file_path = os.path.join(reports_dir, filename)
# 创建工作簿 # 创建工作簿
@@ -668,7 +675,15 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
decimal3_fields = numeric_fields decimal3_fields = numeric_fields
# 写入表头 # 写入表头
for col_idx, field in enumerate(selected_fields, 1): # 第1列序号
cell = ws.cell(row=header_row, column=1)
cell.value = '序号'
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
for col_idx, field in enumerate(selected_fields, 2):
cell = ws.cell(row=header_row, column=col_idx) cell = ws.cell(row=header_row, column=col_idx)
if field in field_custom_names and field_custom_names[field]: if field in field_custom_names and field_custom_names[field]:
display_name = field_custom_names[field] display_name = field_custom_names[field]
@@ -688,7 +703,17 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
data_start_row = header_row + 1 data_start_row = header_row + 1
for row_idx, order in enumerate(orders, data_start_row): for row_idx, order in enumerate(orders, data_start_row):
is_even = (row_idx - data_start_row) % 2 == 1 is_even = (row_idx - data_start_row) % 2 == 1
for col_idx, field in enumerate(selected_fields, 1):
# 第1列序号
seq_num = row_idx - data_start_row + 1
seq_cell = ws.cell(row=row_idx, column=1)
seq_cell.value = seq_num
seq_cell.border = thin_border
seq_cell.alignment = Alignment(horizontal='center', vertical='center')
if is_even:
seq_cell.fill = even_row_fill
for col_idx, field in enumerate(selected_fields, 2):
cell = ws.cell(row=row_idx, column=col_idx) cell = ws.cell(row=row_idx, column=col_idx)
value = order.get(field, '') value = order.get(field, '')
@@ -734,7 +759,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
ws.cell(row=sum_row, column=1).alignment = Alignment(horizontal='center', vertical='center') ws.cell(row=sum_row, column=1).alignment = Alignment(horizontal='center', vertical='center')
ws.cell(row=sum_row, column=1).border = thin_border ws.cell(row=sum_row, column=1).border = thin_border
for col_idx, field in enumerate(selected_fields, 1): for col_idx, field in enumerate(selected_fields, 2):
cell = ws.cell(row=sum_row, column=col_idx) cell = ws.cell(row=sum_row, column=col_idx)
cell.fill = sum_fill cell.fill = sum_fill
cell.border = thin_border cell.border = thin_border
@@ -767,9 +792,9 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
# 月度总计行(在合计行下面空一行) # 月度总计行(在合计行下面空一行)
monthly_row = sum_row + 2 if sum_fields else sum_row + 1 monthly_row = sum_row + 2 if sum_fields else sum_row + 1
# 找 charge_degree 字段所在的列 # 找 charge_degree 字段所在的列+1因为序号占了第1列
charge_degree_col = None charge_degree_col = None
for col_idx, field in enumerate(selected_fields, 1): for col_idx, field in enumerate(selected_fields, 2):
if field == 'charge_degree': if field == 'charge_degree':
charge_degree_col = col_idx charge_degree_col = col_idx
break break
@@ -782,7 +807,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
ws.cell(row=monthly_row, column=1).border = thin_border ws.cell(row=monthly_row, column=1).border = thin_border
# 其他列也填充背景色 # 其他列也填充背景色
for col_idx in range(2, len(selected_fields) + 1): for col_idx in range(2, len(selected_fields) + 2):
cell = ws.cell(row=monthly_row, column=col_idx) cell = ws.cell(row=monthly_row, column=col_idx)
cell.fill = monthly_fill cell.fill = monthly_fill
cell.border = thin_border cell.border = thin_border
@@ -799,7 +824,11 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
print(f"[日报生成] 写入月度总计时出错: {e}") print(f"[日报生成] 写入月度总计时出错: {e}")
# 调整列宽中文按2个字符宽度计算 # 调整列宽中文按2个字符宽度计算
for col_idx, field in enumerate(selected_fields, 1): # 序号列固定宽度
from openpyxl.utils import get_column_letter
ws.column_dimensions['A'].width = 8
for col_idx, field in enumerate(selected_fields, 2):
header_name = get_field_display_name(field) header_name = get_field_display_name(field)
if field in field_custom_names and field_custom_names[field]: if field in field_custom_names and field_custom_names[field]:
header_name = field_custom_names[field] header_name = field_custom_names[field]

View File

@@ -2,6 +2,7 @@
日报求和数据采集与管理 日报求和数据采集与管理
""" """
import json import json
import os
from datetime import datetime from datetime import datetime
from lib.db import execute_query, execute_update, execute_insert from lib.db import execute_query, execute_update, execute_insert
from lib.field_mapping import get_field_display_name from lib.field_mapping import get_field_display_name
@@ -35,6 +36,7 @@ def collect_sum_data_from_history(history_id=None):
collected_count = 0 collected_count = 0
skipped_count = 0 skipped_count = 0
config_sort_cache = {} # 缓存配置的排序号
for history in history_list: for history in history_list:
sum_results = history.get('sum_results') sum_results = history.get('sum_results')
@@ -45,6 +47,24 @@ def collect_sum_data_from_history(history_id=None):
elif sum_results is None: elif sum_results is None:
sum_results = {} sum_results = {}
# 获取配置的排序号(缓存)
config_id = history.get('config_id')
sort_order = None
if config_id:
if config_id in config_sort_cache:
sort_order = config_sort_cache[config_id]
else:
try:
cfg_result = execute_query(
'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1',
(config_id,)
)
if cfg_result and cfg_result[0]['sort_order'] is not None:
sort_order = cfg_result[0]['sort_order']
config_sort_cache[config_id] = sort_order
except Exception:
config_sort_cache[config_id] = None
# 强制确保有充电电量charge_degree # 强制确保有充电电量charge_degree
if 'charge_degree' not in sum_results or sum_results['charge_degree'] is None: if 'charge_degree' not in sum_results or sum_results['charge_degree'] is None:
# 从订单表重新计算充电电量 # 从订单表重新计算充电电量
@@ -75,9 +95,9 @@ def collect_sum_data_from_history(history_id=None):
sql = ''' sql = '''
INSERT INTO t_daily_report_sum_data INSERT INTO t_daily_report_sum_data
(id, report_date, config_id, config_name, split_type, split_value, split_name, (id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, data_source, remark, sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time) create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
''' '''
params = ( params = (
sum_data_id, sum_data_id,
@@ -91,6 +111,7 @@ def collect_sum_data_from_history(history_id=None):
get_field_display_name(field_key), get_field_display_name(field_key),
float(field_value) if field_value is not None else 0, float(field_value) if field_value is not None else 0,
history.get('total_orders', 0), history.get('total_orders', 0),
sort_order,
'auto', 'auto',
f'从历史记录自动采集历史ID: {history["id"]}' f'从历史记录自动采集历史ID: {history["id"]}'
) )
@@ -161,7 +182,7 @@ def _calculate_charge_degree(history):
return None return None
def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None, def get_sum_data_list(page=1, page_size=20, start_date=None, end_date=None, config_id=None,
config_name=None, sum_field_key=None, data_source=None): config_name=None, sum_field_key=None, data_source=None):
""" """
查询求和数据列表 查询求和数据列表
@@ -169,7 +190,8 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None,
Args: Args:
page: 页码 page: 页码
page_size: 每页大小 page_size: 每页大小
report_date: 报表日期(可选) start_date: 开始日期(可选)
end_date: 结束日期(可选)
config_id: 配置ID可选 config_id: 配置ID可选
config_name: 配置名称(可选,模糊搜索) config_name: 配置名称(可选,模糊搜索)
sum_field_key: 求和字段键名(可选) sum_field_key: 求和字段键名(可选)
@@ -184,9 +206,13 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None,
where_clauses = ['1=1'] where_clauses = ['1=1']
params = [] params = []
if report_date: if start_date:
where_clauses.append('report_date = %s') where_clauses.append('report_date >= %s')
params.append(report_date) params.append(start_date)
if end_date:
where_clauses.append('report_date <= %s')
params.append(end_date)
if config_id: if config_id:
where_clauses.append('config_id = %s') where_clauses.append('config_id = %s')
@@ -217,7 +243,7 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None,
list_result = execute_query( list_result = execute_query(
f'''SELECT * FROM t_daily_report_sum_data f'''SELECT * FROM t_daily_report_sum_data
WHERE {where_sql} WHERE {where_sql}
ORDER BY report_date DESC, config_id, id DESC ORDER BY report_date DESC, sort_order ASC, config_id ASC, id DESC
LIMIT %s OFFSET %s''', LIMIT %s OFFSET %s''',
tuple(params) + (page_size, offset) tuple(params) + (page_size, offset)
) )
@@ -249,12 +275,27 @@ def add_sum_data(data):
""" """
try: try:
sum_data_id = int(datetime.now().timestamp() * 1000) sum_data_id = int(datetime.now().timestamp() * 1000)
# 获取配置的排序号
sort_order = None
config_id = data.get('config_id')
if config_id:
try:
cfg_result = execute_query(
'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1',
(config_id,)
)
if cfg_result and cfg_result[0]['sort_order'] is not None:
sort_order = cfg_result[0]['sort_order']
except Exception:
pass
sql = ''' sql = '''
INSERT INTO t_daily_report_sum_data INSERT INTO t_daily_report_sum_data
(id, report_date, config_id, config_name, split_type, split_value, split_name, (id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, data_source, remark, sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time) create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
''' '''
params = ( params = (
sum_data_id, sum_data_id,
@@ -268,6 +309,7 @@ def add_sum_data(data):
data.get('sum_field_name', ''), data.get('sum_field_name', ''),
float(data.get('sum_value', 0)), float(data.get('sum_value', 0)),
data.get('total_orders', 0), data.get('total_orders', 0),
sort_order,
'manual', 'manual',
data.get('remark', '') data.get('remark', '')
) )
@@ -409,13 +451,176 @@ def batch_delete_sum_data(ids):
return {'success': False, 'message': str(e)} return {'success': False, 'message': str(e)}
def export_sum_data_to_excel(start_date=None, end_date=None, config_id=None, data_source=None, keyword=None):
"""
导出求和数据为Excel
Args:
start_date: 开始日期(可选)
end_date: 结束日期(可选)
config_id: 配置ID可选
data_source: 数据来源(可选)
keyword: 配置名称关键词(可选)
Returns:
dict: {success, message, file_path}
"""
try:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
# 构建查询条件
where_clauses = []
params = []
if start_date:
where_clauses.append('report_date >= %s')
params.append(start_date)
if end_date:
where_clauses.append('report_date <= %s')
params.append(end_date)
if config_id:
where_clauses.append('config_id = %s')
params.append(config_id)
if data_source:
where_clauses.append('data_source = %s')
params.append(data_source)
if keyword:
where_clauses.append('config_name LIKE %s')
params.append(f'%{keyword}%')
where_sql = ''
if where_clauses:
where_sql = 'WHERE ' + ' AND '.join(where_clauses)
# 查询数据(不分页,全量导出)
sql = f'''
SELECT id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time
FROM t_daily_report_sum_data
{where_sql}
ORDER BY report_date DESC, sort_order ASC, config_id ASC, sum_field_key ASC
'''
rows = execute_query(sql, tuple(params) if params else ())
# 创建Excel
wb = Workbook()
ws = wb.active
ws.title = '求和数据'
# 表头
headers = ['序号', '报表日期', '配置名称', '拆分方式', '拆分值', '求和字段', '求和值', '订单数', '数据来源', '修改原因/备注', '创建时间', '更新时间']
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(bold=True, color='FFFFFF', size=11)
header_alignment = Alignment(horizontal='center', vertical='center')
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, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
ws.row_dimensions[1].height = 25
# 拆分方式映射
split_type_map = {
'company_id': '按企业ID',
'user_id': '按用户ID',
'station_id': '按场站ID'
}
# 数据来源映射
source_map = {
'auto': '自动采集',
'manual': '手动编辑'
}
# 写入数据
for row_idx, row in enumerate(rows, 2):
is_even = (row_idx - 2) % 2 == 1
values = [
row.get('sort_order') or '',
row.get('report_date', ''),
row.get('config_name', ''),
split_type_map.get(row.get('split_type', ''), row.get('split_type', '')),
row.get('split_name', '') or row.get('split_value', ''),
row.get('sum_field_name', ''),
float(row.get('sum_value', 0) or 0),
int(row.get('total_orders', 0) or 0),
source_map.get(row.get('data_source', ''), row.get('data_source', '')),
row.get('remark', ''),
str(row.get('create_time', '')) if row.get('create_time') else '',
str(row.get('update_time', '')) if row.get('update_time') else ''
]
for col_idx, val in enumerate(values, 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.value = val
cell.border = thin_border
# 对齐方式:数字靠右,其他居中或靠左
if col_idx in [7, 8]: # 求和值、订单数
cell.alignment = Alignment(horizontal='right', vertical='center')
if col_idx == 7:
cell.number_format = '0.000'
elif col_idx in [1, 2, 4, 9]: # 序号、日期、拆分方式、数据来源
cell.alignment = Alignment(horizontal='center', vertical='center')
else:
cell.alignment = Alignment(horizontal='left', vertical='center')
if is_even:
cell.fill = even_fill
ws.row_dimensions[row_idx].height = 22
# 调整列宽
col_widths = [8, 14, 20, 12, 20, 18, 14, 10, 12, 30, 20, 20]
for col_idx, width in enumerate(col_widths, 1):
from openpyxl.utils import get_column_letter
col_letter = get_column_letter(col_idx)
ws.column_dimensions[col_letter].width = width
# 冻结首行
ws.freeze_panes = 'A2'
# 保存文件
reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports')
os.makedirs(reports_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'求和数据导出_{timestamp}.xlsx'
file_path = os.path.join(reports_dir, filename)
wb.save(file_path)
return {'success': True, 'message': '导出成功', 'file_path': f'/public/reports/{filename}', 'filename': filename, 'total': len(rows)}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def get_monthly_charge_degree_total(config_id=None, month=None): def get_monthly_charge_degree_total(config_id=None, month=None):
""" """
获取当月充电电量累计(从求和数据表查询,对账后的准确数据) 获取当月充电电量累计(从求和数据表查询,对账后的准确数据)
注意:
- 数据来源于 t_daily_report_sum_data 表(线下对账后的数据,可能被手动修改过)
- 只统计 sum_field_key = 'charge_degree' 的记录
- 如果当天已经生成了日报并采集,自动包含在内,不会重复计算 - 如果当天已经生成了日报并采集,自动包含在内,不会重复计算
Args: Args:

View File

@@ -208,6 +208,15 @@ color: white;
background: #d35400; background: #d35400;
} }
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
}
.btn-secondary { .btn-secondary {
background: #eef2f7; background: #eef2f7;
color: #555; color: #555;

View File

@@ -23,15 +23,17 @@ function renderConfigList(configs) {
const tbody = document.getElementById('config-list'); const tbody = document.getElementById('config-list');
if (configs.length === 0) { if (configs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty">暂无配置,请点击"创建配置"按钮</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="empty">暂无配置,请点击"创建配置"按钮</td></tr>';
return; return;
} }
tbody.innerHTML = configs.map((config, index) => { tbody.innerHTML = configs.map((config, index) => {
const isFirst = index === 0; const isFirst = index === 0;
const isLast = index === configs.length - 1; const isLast = index === configs.length - 1;
const sortOrder = index + 1;
return ` return `
<tr> <tr>
<td style="text-align: center; font-weight: bold; color: #4472C4;">${sortOrder}</td>
<td>${config.config_name}</td> <td>${config.config_name}</td>
<td>${config.split_type === 'company_id' ? '按企业 ID' : config.split_type === 'user_id' ? '按用户 ID' : '按场站 ID'}</td> <td>${config.split_type === 'company_id' ? '按企业 ID' : config.split_type === 'user_id' ? '按用户 ID' : '按场站 ID'}</td>
<td>${config.split_name || config.split_value}</td> <td>${config.split_name || config.split_value}</td>
@@ -879,3 +881,56 @@ function onSplitTypeChange() {
document.getElementById('entity-list').innerHTML = '<div class="empty">请先选择拆分方式</div>'; document.getElementById('entity-list').innerHTML = '<div class="empty">请先选择拆分方式</div>';
} }
} }
// 导出配置
function exportConfigs() {
try {
window.open('/api/config/export', '_blank');
} catch (error) {
alert('导出失败: ' + error.message);
}
}
// 导入配置
async function importConfigs(event) {
const fileInput = event.target;
const file = fileInput.files[0];
if (!file) return;
if (!file.name.endsWith('.json')) {
alert('只支持 JSON 格式的配置文件');
fileInput.value = '';
return;
}
if (!confirm('确定要导入配置吗?\n导入的配置会自动命名为"原名称_导入",并默认禁用。')) {
fileInput.value = '';
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/config/import', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
let message = result.message;
if (result.data && result.data.fail_messages && result.data.fail_messages.length > 0) {
message += '\n\n失败详情\n' + result.data.fail_messages.join('\n');
}
alert(message);
loadConfigs();
} else {
alert('导入失败: ' + (result.message || '未知错误'));
}
} catch (error) {
alert('导入失败: ' + error.message);
}
fileInput.value = '';
}

View File

@@ -77,6 +77,7 @@ function renderSumDataList(rawList) {
split_type: item.split_type, split_type: item.split_type,
split_value: item.split_value, split_value: item.split_value,
split_name: item.split_name, split_name: item.split_name,
sort_order: item.sort_order || 9999,
total_orders: item.total_orders || 0, total_orders: item.total_orders || 0,
data_source: item.data_source, data_source: item.data_source,
remark: item.remark || '', remark: item.remark || '',
@@ -97,7 +98,7 @@ function renderSumDataList(rawList) {
sumDataGroupedList = Object.values(groupMap).sort((a, b) => { sumDataGroupedList = Object.values(groupMap).sort((a, b) => {
if (a.report_date !== b.report_date) return b.report_date.localeCompare(a.report_date); if (a.report_date !== b.report_date) return b.report_date.localeCompare(a.report_date);
return (a.config_name || '').localeCompare(b.config_name || ''); return (a.sort_order || 9999) - (b.sort_order || 9999);
}); });
// 分页处理 // 分页处理
@@ -176,12 +177,21 @@ function renderSumDataPagination(total) {
// 加载求和数据 // 加载求和数据
async function loadSumData(page = 1) { async function loadSumData(page = 1) {
sumDataCurrentPage = page; sumDataCurrentPage = page;
const reportDate = document.getElementById('sum-data-report-date').value; const startDate = document.getElementById('sum-data-start-date').value;
const endDate = document.getElementById('sum-data-end-date').value;
const configName = document.getElementById('sum-data-config-name').value; const configName = document.getElementById('sum-data-config-name').value;
const dataSource = document.getElementById('sum-data-source').value; const dataSource = document.getElementById('sum-data-source').value;
const params = new URLSearchParams();
params.append('page', '1');
params.append('page_size', '5000');
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (configName) params.append('config_name', configName);
if (dataSource) params.append('data_source', dataSource);
try { try {
const response = await fetch(`/api/sum-data?page=1&page_size=5000&report_date=${encodeURIComponent(reportDate)}&config_name=${encodeURIComponent(configName)}&data_source=${encodeURIComponent(dataSource)}`); const response = await fetch(`/api/sum-data?${params.toString()}`);
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
renderSumDataList(result.data.list || []); renderSumDataList(result.data.list || []);
@@ -195,7 +205,8 @@ async function loadSumData(page = 1) {
// 清除查询 // 清除查询
function clearSumDataQuery() { function clearSumDataQuery() {
document.getElementById('sum-data-report-date').value = ''; document.getElementById('sum-data-start-date').value = '';
document.getElementById('sum-data-end-date').value = '';
document.getElementById('sum-data-config-name').value = ''; document.getElementById('sum-data-config-name').value = '';
document.getElementById('sum-data-source').value = ''; document.getElementById('sum-data-source').value = '';
loadSumData(1); loadSumData(1);
@@ -525,3 +536,33 @@ async function batchDeleteSumData() {
alert('删除失败: ' + error.message); alert('删除失败: ' + error.message);
} }
} }
async function exportSumData() {
const startDate = document.getElementById('sum-data-start-date').value;
const endDate = document.getElementById('sum-data-end-date').value;
const configName = document.getElementById('sum-data-config-name').value;
const dataSource = document.getElementById('sum-data-source').value;
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (configName) params.append('keyword', configName);
if (dataSource) params.append('data_source', dataSource);
try {
const response = await fetch(`/api/sum-data/export?${params.toString()}`);
const result = await response.json();
if (result.success) {
if (result.total === 0) {
alert('没有可导出的数据');
return;
}
window.open(result.file_path, '_blank');
} else {
alert('导出失败: ' + (result.message || '未知错误'));
}
} catch (error) {
alert('导出失败: ' + error.message);
}
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单日报系统</title> <title>订单日报系统</title>
<link rel="stylesheet" href="/public/static/css/style.css?v=2026071402"> <link rel="stylesheet" href="/public/static/css/style.css?v=2026071404">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@@ -21,12 +21,16 @@
<div id="config-tab" class="tab-content active"> <div id="config-tab" class="tab-content active">
<div class="card"> <div class="card">
<button class="btn btn-primary" onclick="showCreateModal()">创建配置</button> <button class="btn btn-primary" onclick="showCreateModal()">创建配置</button>
<button class="btn btn-info" onclick="exportConfigs()">导出配置</button>
<button class="btn btn-success" onclick="document.getElementById('import-config-file').click()">导入配置</button>
<input type="file" id="import-config-file" accept=".json" style="display: none;" onchange="importConfigs(event)">
</div> </div>
<div class="card"> <div class="card">
<table> <table>
<thead> <thead>
<tr> <tr>
<th style="min-width: 60px; width: 60px;">序号</th>
<th>配置名称</th> <th>配置名称</th>
<th>拆分方式</th> <th>拆分方式</th>
<th>拆分值</th> <th>拆分值</th>
@@ -36,7 +40,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="config-list"> <tbody id="config-list">
<tr><td colspan="6" class="loading">加载中...</td></tr> <tr><td colspan="7" class="loading">加载中...</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -122,7 +126,9 @@
<div id="sum-data-tab" class="tab-content"> <div id="sum-data-tab" class="tab-content">
<div class="card"> <div class="card">
<div style="display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center;"> <div style="display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center;">
<input type="date" id="sum-data-report-date" class="form-control" style="width: auto;"> <input type="date" id="sum-data-start-date" class="form-control" style="width: auto;">
<span style="color: #999;"></span>
<input type="date" id="sum-data-end-date" class="form-control" style="width: auto;">
<input type="text" id="sum-data-config-name" class="form-control" style="width: 180px;" placeholder="配置名称搜索"> <input type="text" id="sum-data-config-name" class="form-control" style="width: 180px;" placeholder="配置名称搜索">
<select id="sum-data-source" class="form-control" style="width: auto;" onchange="loadSumData()"> <select id="sum-data-source" class="form-control" style="width: auto;" onchange="loadSumData()">
<option value="">全部来源</option> <option value="">全部来源</option>
@@ -133,6 +139,7 @@
<button class="btn" onclick="clearSumDataQuery()">清除</button> <button class="btn" onclick="clearSumDataQuery()">清除</button>
<button class="btn btn-success" onclick="showAddSumDataModal()">手动添加</button> <button class="btn btn-success" onclick="showAddSumDataModal()">手动添加</button>
<button class="btn btn-warning" onclick="collectSumDataFromHistory()">从历史采集</button> <button class="btn btn-warning" onclick="collectSumDataFromHistory()">从历史采集</button>
<button class="btn btn-info" onclick="exportSumData()">导出</button>
<button class="btn btn-danger" onclick="batchDeleteSumData()" style="margin-left: auto;">批量删除</button> <button class="btn btn-danger" onclick="batchDeleteSumData()" style="margin-left: auto;">批量删除</button>
</div> </div>
@@ -332,11 +339,11 @@
</div> </div>
<!-- JavaScript 文件 --> <!-- JavaScript 文件 -->
<script src="/public/static/js/common.js?v=2026071402"></script> <script src="/public/static/js/common.js?v=2026071404"></script>
<script src="/public/static/js/config.js?v=2026071402"></script> <script src="/public/static/js/config.js?v=2026071404"></script>
<script src="/public/static/js/entity.js?v=2026071402"></script> <script src="/public/static/js/entity.js?v=2026071404"></script>
<script src="/public/static/js/report.js?v=2026071402"></script> <script src="/public/static/js/report.js?v=2026071404"></script>
<script src="/public/static/js/history.js?v=2026071402"></script> <script src="/public/static/js/history.js?v=2026071404"></script>
<script src="/public/static/js/sum_data.js?v=2026071402"></script> <script src="/public/static/js/sum_data.js?v=2026071404"></script>
</body> </body>
</html> </html>