增加序号,配置导出,导入,求和采集数据导出
This commit is contained in:
@@ -272,3 +272,150 @@ def move_config(config_id):
|
||||
traceback.print_exc()
|
||||
print(f"[move_config] 异常: {e}")
|
||||
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
|
||||
|
||||
@@ -9,7 +9,8 @@ from lib.sum_data_collector import (
|
||||
update_sum_data,
|
||||
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
|
||||
|
||||
@@ -38,7 +39,8 @@ def list_sum_data():
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
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_name = request.args.get('config_name')
|
||||
sum_field_key = request.args.get('sum_field_key')
|
||||
@@ -47,7 +49,8 @@ def list_sum_data():
|
||||
result = get_sum_data_list(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
report_date=report_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
config_id=config_id,
|
||||
config_name=config_name,
|
||||
sum_field_key=sum_field_key,
|
||||
@@ -152,3 +155,39 @@ def monthly_charge_degree_total():
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
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
|
||||
|
||||
@@ -82,6 +82,7 @@ def init_database_tables():
|
||||
sum_field_name VARCHAR(100) NULL COMMENT '求和字段中文名称',
|
||||
sum_value DECIMAL(18,3) NULL COMMENT '求和值',
|
||||
total_orders INT NULL COMMENT '订单数',
|
||||
sort_order INT NULL COMMENT '配置排序号',
|
||||
data_source VARCHAR(20) NULL COMMENT '数据来源 auto=自动采集 manual=手动编辑',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
create_time DATETIME NULL COMMENT '创建时间',
|
||||
@@ -107,6 +108,18 @@ def init_database_tables():
|
||||
else:
|
||||
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 顺序赋值
|
||||
try:
|
||||
null_count = execute_query(
|
||||
|
||||
@@ -474,7 +474,8 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
|
||||
has_supplement=has_supplement,
|
||||
show_monthly_total=config.get('show_monthly_total', 0) == 1,
|
||||
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}")
|
||||
@@ -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,
|
||||
config_name, split_name, start_time, end_time,
|
||||
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文件
|
||||
|
||||
@@ -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')
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
|
||||
# 生成文件名 - 格式:配置名称_几月几日消费记录.xlsx
|
||||
# 生成文件名 - 格式:序号_配置名称_几月几日消费记录.xlsx
|
||||
# 从 start_time 提取日期(报表数据对应的是开始时间那天的数据)
|
||||
try:
|
||||
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 ''
|
||||
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)
|
||||
|
||||
# 创建工作簿
|
||||
@@ -668,7 +675,15 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
|
||||
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)
|
||||
if field in field_custom_names and 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
|
||||
for row_idx, order in enumerate(orders, data_start_row):
|
||||
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)
|
||||
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).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.fill = sum_fill
|
||||
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
|
||||
|
||||
# 找 charge_degree 字段所在的列
|
||||
# 找 charge_degree 字段所在的列(+1因为序号占了第1列)
|
||||
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':
|
||||
charge_degree_col = col_idx
|
||||
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
|
||||
|
||||
# 其他列也填充背景色
|
||||
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.fill = monthly_fill
|
||||
cell.border = thin_border
|
||||
@@ -799,7 +824,11 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
|
||||
print(f"[日报生成] 写入月度总计时出错: {e}")
|
||||
|
||||
# 调整列宽(中文按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)
|
||||
if field in field_custom_names and field_custom_names[field]:
|
||||
header_name = field_custom_names[field]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
日报求和数据采集与管理
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from lib.db import execute_query, execute_update, execute_insert
|
||||
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
|
||||
skipped_count = 0
|
||||
config_sort_cache = {} # 缓存配置的排序号
|
||||
|
||||
for history in history_list:
|
||||
sum_results = history.get('sum_results')
|
||||
@@ -45,6 +47,24 @@ def collect_sum_data_from_history(history_id=None):
|
||||
elif sum_results is None:
|
||||
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)
|
||||
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 = '''
|
||||
INSERT INTO t_daily_report_sum_data
|
||||
(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)
|
||||
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 = (
|
||||
sum_data_id,
|
||||
@@ -91,6 +111,7 @@ def collect_sum_data_from_history(history_id=None):
|
||||
get_field_display_name(field_key),
|
||||
float(field_value) if field_value is not None else 0,
|
||||
history.get('total_orders', 0),
|
||||
sort_order,
|
||||
'auto',
|
||||
f'从历史记录自动采集,历史ID: {history["id"]}'
|
||||
)
|
||||
@@ -161,7 +182,7 @@ def _calculate_charge_degree(history):
|
||||
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):
|
||||
"""
|
||||
查询求和数据列表
|
||||
@@ -169,7 +190,8 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None,
|
||||
Args:
|
||||
page: 页码
|
||||
page_size: 每页大小
|
||||
report_date: 报表日期(可选)
|
||||
start_date: 开始日期(可选)
|
||||
end_date: 结束日期(可选)
|
||||
config_id: 配置ID(可选)
|
||||
config_name: 配置名称(可选,模糊搜索)
|
||||
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']
|
||||
params = []
|
||||
|
||||
if report_date:
|
||||
where_clauses.append('report_date = %s')
|
||||
params.append(report_date)
|
||||
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')
|
||||
@@ -217,7 +243,7 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None,
|
||||
list_result = execute_query(
|
||||
f'''SELECT * FROM t_daily_report_sum_data
|
||||
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''',
|
||||
tuple(params) + (page_size, offset)
|
||||
)
|
||||
@@ -249,12 +275,27 @@ def add_sum_data(data):
|
||||
"""
|
||||
try:
|
||||
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 = '''
|
||||
INSERT INTO t_daily_report_sum_data
|
||||
(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)
|
||||
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 = (
|
||||
sum_data_id,
|
||||
@@ -268,6 +309,7 @@ def add_sum_data(data):
|
||||
data.get('sum_field_name', ''),
|
||||
float(data.get('sum_value', 0)),
|
||||
data.get('total_orders', 0),
|
||||
sort_order,
|
||||
'manual',
|
||||
data.get('remark', '')
|
||||
)
|
||||
@@ -409,13 +451,176 @@ def batch_delete_sum_data(ids):
|
||||
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):
|
||||
"""
|
||||
获取当月充电电量累计(从求和数据表查询,对账后的准确数据)
|
||||
|
||||
注意:
|
||||
- 数据来源于 t_daily_report_sum_data 表(线下对账后的数据,可能被手动修改过)
|
||||
- 只统计 sum_field_key = 'charge_degree' 的记录
|
||||
- 如果当天已经生成了日报并采集,自动包含在内,不会重复计算
|
||||
|
||||
Args:
|
||||
|
||||
@@ -208,6 +208,15 @@ color: white;
|
||||
background: #d35400;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #138496;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #eef2f7;
|
||||
color: #555;
|
||||
|
||||
@@ -23,15 +23,17 @@ function renderConfigList(configs) {
|
||||
const tbody = document.getElementById('config-list');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
tbody.innerHTML = configs.map((config, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === configs.length - 1;
|
||||
const sortOrder = index + 1;
|
||||
return `
|
||||
<tr>
|
||||
<td style="text-align: center; font-weight: bold; color: #4472C4;">${sortOrder}</td>
|
||||
<td>${config.config_name}</td>
|
||||
<td>${config.split_type === 'company_id' ? '按企业 ID' : config.split_type === 'user_id' ? '按用户 ID' : '按场站 ID'}</td>
|
||||
<td>${config.split_name || config.split_value}</td>
|
||||
@@ -879,3 +881,56 @@ function onSplitTypeChange() {
|
||||
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 = '';
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ function renderSumDataList(rawList) {
|
||||
split_type: item.split_type,
|
||||
split_value: item.split_value,
|
||||
split_name: item.split_name,
|
||||
sort_order: item.sort_order || 9999,
|
||||
total_orders: item.total_orders || 0,
|
||||
data_source: item.data_source,
|
||||
remark: item.remark || '',
|
||||
@@ -97,7 +98,7 @@ function renderSumDataList(rawList) {
|
||||
|
||||
sumDataGroupedList = Object.values(groupMap).sort((a, b) => {
|
||||
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) {
|
||||
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 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 {
|
||||
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();
|
||||
if (result.success) {
|
||||
renderSumDataList(result.data.list || []);
|
||||
@@ -195,7 +205,8 @@ async function loadSumData(page = 1) {
|
||||
|
||||
// 清除查询
|
||||
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-source').value = '';
|
||||
loadSumData(1);
|
||||
@@ -525,3 +536,33 @@ async function batchDeleteSumData() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -21,12 +21,16 @@
|
||||
<div id="config-tab" class="tab-content active">
|
||||
<div class="card">
|
||||
<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 class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width: 60px; width: 60px;">序号</th>
|
||||
<th>配置名称</th>
|
||||
<th>拆分方式</th>
|
||||
<th>拆分值</th>
|
||||
@@ -36,7 +40,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-list">
|
||||
<tr><td colspan="6" class="loading">加载中...</td></tr>
|
||||
<tr><td colspan="7" class="loading">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -122,7 +126,9 @@
|
||||
<div id="sum-data-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<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="配置名称搜索">
|
||||
<select id="sum-data-source" class="form-control" style="width: auto;" onchange="loadSumData()">
|
||||
<option value="">全部来源</option>
|
||||
@@ -133,6 +139,7 @@
|
||||
<button class="btn" onclick="clearSumDataQuery()">清除</button>
|
||||
<button class="btn btn-success" onclick="showAddSumDataModal()">手动添加</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>
|
||||
</div>
|
||||
|
||||
@@ -332,11 +339,11 @@
|
||||
</div>
|
||||
|
||||
<!-- JavaScript 文件 -->
|
||||
<script src="/public/static/js/common.js?v=2026071402"></script>
|
||||
<script src="/public/static/js/config.js?v=2026071402"></script>
|
||||
<script src="/public/static/js/entity.js?v=2026071402"></script>
|
||||
<script src="/public/static/js/report.js?v=2026071402"></script>
|
||||
<script src="/public/static/js/history.js?v=2026071402"></script>
|
||||
<script src="/public/static/js/sum_data.js?v=2026071402"></script>
|
||||
<script src="/public/static/js/common.js?v=2026071404"></script>
|
||||
<script src="/public/static/js/config.js?v=2026071404"></script>
|
||||
<script src="/public/static/js/entity.js?v=2026071404"></script>
|
||||
<script src="/public/static/js/report.js?v=2026071404"></script>
|
||||
<script src="/public/static/js/history.js?v=2026071404"></script>
|
||||
<script src="/public/static/js/sum_data.js?v=2026071404"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user