修复求和数据统计的问题

This commit is contained in:
2026-07-23 14:57:29 +08:00
parent 03098a553e
commit fa884ae782
11 changed files with 550 additions and 184 deletions

35
check_data.py Normal file
View File

@@ -0,0 +1,35 @@
import sys
sys.path.insert(0, '.')
from lib.db import execute_query
print('=== 检查最近的340路历史记录 ===')
history = execute_query('SELECT id, report_date, start_time, end_time, total_orders, sum_results, status FROM t_daily_report_history WHERE config_id = %s ORDER BY report_date DESC LIMIT 5', (1784534746013,))
for h in history:
print(f"ID: {h['id']}")
print(f" 报表日期: {h['report_date']}")
print(f" 开始时间: {h['start_time']}")
print(f" 结束时间: {h['end_time']}")
print(f" 订单数: {h['total_orders']}")
print(f" 状态: {h['status']}")
print(f" 求和结果: {h['sum_results'][:100]}...")
print()
print('=== 检查7月23日是否有报表生成 ===')
history_23 = execute_query('SELECT id, report_date, start_time, end_time, total_orders, status FROM t_daily_report_history WHERE config_id = %s AND report_date = %s', (1784534746013, '2026-07-23'))
for h in history_23:
print(f"ID: {h['id']}, 报表日期: {h['report_date']}, 状态: {h['status']}, 订单数: {h['total_orders']}")
print(f" 开始时间: {h['start_time']}, 结束时间: {h['end_time']}")
print('\n=== 检查7月14日是否有自动采集的数据 ===')
auto_14 = execute_query('SELECT * FROM t_daily_report_sum_data WHERE report_date = %s AND split_value = %s AND data_source = %s', ('2026-07-14', '163425', 'auto'))
print(f"7月14日自动采集数据: {len(auto_14)}")
print('\n=== 检查7月15日是否有自动采集的数据 ===')
auto_15 = execute_query('SELECT * FROM t_daily_report_sum_data WHERE report_date = %s AND split_value = %s AND data_source = %s', ('2026-07-15', '163425', 'auto'))
print(f"7月15日自动采集数据: {len(auto_15)}")
print('\n=== 查询7月23日的求和数据 ===')
sum_23 = execute_query('SELECT * FROM t_daily_report_sum_data WHERE report_date = %s AND split_value = %s', ('2026-07-23', '163425'))
print(f"7月23日求和数据: {len(sum_23)}")
for s in sum_23:
print(f" ID: {s['id']}, 字段: {s['sum_field_key']}, 值: {s['sum_value']}, 来源: {s['data_source']}")

29
check_history.py Normal file
View File

@@ -0,0 +1,29 @@
from lib.db import execute_query
# 查询开启特来电合并的配置的历史记录
h = execute_query("""
SELECT id, config_name, report_date, start_time, end_time, total_orders, status, create_time
FROM t_daily_report_history
WHERE config_name IN ('华辉121路', '华辉188路', '248路', '华辉251路')
AND report_date >= '2026-07-22'
ORDER BY create_time DESC
""")
print("===== 特来电合并配置的历史记录 =====")
for h_ in h:
print(f"{h_['config_name']} | {h_['report_date']} | {h_['start_time']} ~ {h_['end_time']} | {h_['total_orders']}条 | status={h_['status']} | {h_['create_time']}")
print("\n===== 检查配置的selected_fields是否包含车牌号和VIN =====")
configs = execute_query("""
SELECT id, config_name, selected_fields
FROM t_daily_report_config
WHERE config_name IN ('华辉121路', '华辉188路', '248路', '华辉251路')
""")
import json
for c in configs:
fields = json.loads(c['selected_fields'])
has_plate = 'charge_plate_no' in fields
has_vin = 'charge_vin' in fields
print(f"{c['config_name']}: 车牌号={has_plate}, VIN码={has_vin}")
print(f" 字段列表: {fields}")

View File

@@ -57,6 +57,10 @@ def create_config():
custom_service_fee_name = data.get('custom_service_fee_name')
show_custom_service_fee = 1 if data.get('show_custom_service_fee') else 0
show_total_amount = 1 if data.get('show_total_amount') else 0
total_amount_name = data.get('total_amount_name')
if not config_name or not split_type or not split_value:
log_warning('[配置API] 创建配置失败:缺少必填字段', 'api')
return jsonify({'success': False, 'message': '缺少必填字段'}), 400
@@ -76,8 +80,10 @@ def create_config():
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, custom_service_fee_price, custom_service_fee_name, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW())
show_monthly_total, custom_service_fee_price, custom_service_fee_name,
show_custom_service_fee, show_total_amount, total_amount_name,
is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW())
"""
params = (
@@ -95,6 +101,9 @@ def create_config():
show_monthly_total,
custom_service_fee_price,
custom_service_fee_name if custom_service_fee_name else None,
show_custom_service_fee,
show_total_amount,
total_amount_name if total_amount_name else None,
sort_order
)
@@ -136,6 +145,10 @@ def update_config(config_id):
custom_service_fee_name = data.get('custom_service_fee_name')
show_custom_service_fee = 1 if data.get('show_custom_service_fee') else 0
show_total_amount = 1 if data.get('show_total_amount') else 0
total_amount_name = data.get('total_amount_name')
# 查询原配置,检查拆分是否变化
old_configs = execute_query('SELECT * FROM t_daily_report_config WHERE id = %s', (config_id,))
if not old_configs:
@@ -148,6 +161,7 @@ def update_config(config_id):
selected_fields = %s, sum_fields = %s, time_periods = %s,
merge_telecom = %s, telecom_vehicle_no = %s, field_custom_names = %s,
show_monthly_total = %s, custom_service_fee_price = %s, custom_service_fee_name = %s,
show_custom_service_fee = %s, show_total_amount = %s, total_amount_name = %s,
update_time = NOW()
WHERE id = %s
"""
@@ -166,6 +180,9 @@ def update_config(config_id):
show_monthly_total,
custom_service_fee_price,
custom_service_fee_name if custom_service_fee_name else None,
show_custom_service_fee,
show_total_amount,
total_amount_name if total_amount_name else None,
config_id
)
@@ -240,8 +257,8 @@ def copy_config(config_id):
# 插入新配置
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, custom_service_fee_price, custom_service_fee_name, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())''',
(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, custom_service_fee_price, custom_service_fee_name, show_custom_service_fee, show_total_amount, total_amount_name, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())''',
(
new_id,
new_name,
@@ -257,6 +274,9 @@ def copy_config(config_id):
original.get('show_monthly_total', 0),
original.get('custom_service_fee_price'),
original.get('custom_service_fee_name'),
original.get('show_custom_service_fee', 0),
original.get('show_total_amount', 0),
original.get('total_amount_name'),
original['is_active'],
sort_order
)

View File

@@ -93,12 +93,13 @@ def update_sum_data_api(sum_data_id):
"""更新求和数据"""
try:
data = request.json
log_info(f'[求和数据API] 更新求和数据ID: {sum_data_id}', 'api')
log_info(f'[求和数据API] 更新求和数据ID: {sum_data_id}, 请求数据: {data}', 'api')
result = update_sum_data(sum_data_id, data)
if result['success']:
log_info(f'[求和数据API] 更新成功ID: {sum_data_id}', 'api')
return jsonify(result)
else:
log_warning(f'[求和数据API] 更新失败ID: {sum_data_id}, 原因: {result["message"]}', 'api')
return jsonify(result), 400
except Exception as e:
log_error(f'[求和数据API] 更新求和数据失败: {e}', 'api')
@@ -143,6 +144,57 @@ def batch_delete_sum_data_api():
return jsonify({'success': False, 'message': str(e)}), 500
@sum_data_bp.route('/sum-data/batch-query', methods=['POST'])
def batch_query_sum_data():
"""根据ID列表批量查询求和数据"""
try:
data = request.json
ids = data.get('ids', [])
log_info(f'[求和数据API] 批量查询求和数据ID数量: {len(ids)}', 'api')
if not ids:
return jsonify({'success': False, 'message': '请提供ID列表'}), 400
from lib.db import execute_query
placeholders = ','.join(['%s'] * len(ids))
sql = f'SELECT * FROM t_daily_report_sum_data WHERE id IN ({placeholders})'
result = execute_query(sql, tuple(ids))
return jsonify({
'success': True,
'data': result
})
except Exception as e:
log_error(f'[求和数据API] 批量查询求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@sum_data_bp.route('/sum-data/batch-add', methods=['POST'])
def batch_add_sum_data():
"""批量添加求和数据"""
try:
data = request.json
fields = data.get('fields', [])
log_info(f'[求和数据API] 批量添加求和数据,字段数量: {len(fields)}', 'api')
if not fields:
return jsonify({'success': False, 'message': '请至少添加一个字段'}), 400
from lib.sum_data_collector import batch_add_sum_data
result = batch_add_sum_data(data)
if result['success']:
log_info(f'[求和数据API] 批量添加成功,添加{result["count"]}条记录', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
log_error(f'[求和数据API] 批量添加求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@sum_data_bp.route('/sum-data/monthly-total', methods=['GET'])
def monthly_charge_degree_total():
"""获取指定配置当月充电电量累计从sum_data表查询对账后的数据

View File

@@ -18,8 +18,8 @@ def get_db_config():
'cursorclass': pymysql.cursors.DictCursor,
'autocommit': True,
'connect_timeout': 30,
'read_timeout': 60,
'write_timeout': 60
'read_timeout': 120,
'write_timeout': 120
}
DB_CONFIG = get_db_config()
@@ -41,39 +41,72 @@ def get_connection():
pass
def execute_query(sql, params=None):
def execute_query(sql, params=None, retry=2):
"""执行查询并返回结果"""
try:
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
result = cursor.fetchall()
return result
except Exception as e:
log_error(f'查询失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
for attempt in range(retry + 1):
try:
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
result = cursor.fetchall()
if attempt > 0:
log_info(f'[数据库] 查询重试成功,第{attempt+1}次尝试', 'db')
return result
except pymysql.err.OperationalError as e:
if attempt < retry and (e.args[0] == 2013 or e.args[0] == 2006):
log_warning(f'[数据库] 查询连接断开,正在重试(第{attempt+1}次): {e}', 'db')
import time
time.sleep(1)
continue
log_error(f'查询失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
except Exception as e:
log_error(f'查询失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
def execute_update(sql, params=None):
def execute_update(sql, params=None, retry=2):
"""执行更新操作"""
try:
with get_connection() as conn:
with conn.cursor() as cursor:
affected = cursor.execute(sql, params)
return cursor.rowcount
except Exception as e:
log_error(f'更新失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
for attempt in range(retry + 1):
try:
with get_connection() as conn:
with conn.cursor() as cursor:
affected = cursor.execute(sql, params)
if attempt > 0:
log_info(f'[数据库] 更新重试成功,第{attempt+1}次尝试', 'db')
return cursor.rowcount
except pymysql.err.OperationalError as e:
if attempt < retry and (e.args[0] == 2013 or e.args[0] == 2006):
log_warning(f'[数据库] 更新连接断开,正在重试(第{attempt+1}次): {e}', 'db')
import time
time.sleep(1)
continue
log_error(f'更新失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
except Exception as e:
log_error(f'更新失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
def execute_insert(sql, params=None):
def execute_insert(sql, params=None, retry=2):
"""执行插入操作并返回插入ID"""
try:
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
last_id = cursor.lastrowid
return last_id
except Exception as e:
log_error(f'插入失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
for attempt in range(retry + 1):
try:
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
last_id = cursor.lastrowid
if attempt > 0:
log_info(f'[数据库] 插入重试成功,第{attempt+1}次尝试', 'db')
return last_id
except pymysql.err.OperationalError as e:
if attempt < retry and (e.args[0] == 2013 or e.args[0] == 2006):
log_warning(f'[数据库] 插入连接断开,正在重试(第{attempt+1}次): {e}', 'db')
import time
time.sleep(1)
continue
log_error(f'插入失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise
except Exception as e:
log_error(f'插入失败: {e}\nSQL: {sql}\nParams: {params}', 'db', exc_info=True)
raise

View File

@@ -161,6 +161,27 @@ def init_database_tables():
"custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称'",
'custom_service_fee_name'
)
# 给配置表增加 show_custom_service_fee 字段(是否显示自定义服务费列)
_add_column_if_not_exists(
't_daily_report_config',
"show_custom_service_fee TINYINT NULL COMMENT '是否显示自定义服务费列(0=不显示,1=显示)'",
'show_custom_service_fee'
)
# 给配置表增加 show_total_amount 字段(是否显示实收金额列)
_add_column_if_not_exists(
't_daily_report_config',
"show_total_amount TINYINT NULL COMMENT '是否显示实收金额列(0=不显示,1=显示)'",
'show_total_amount'
)
# 给配置表增加 total_amount_name 字段(实收金额表头名称)
_add_column_if_not_exists(
't_daily_report_config',
"total_amount_name VARCHAR(100) NULL COMMENT '实收金额报表表头名称'",
'total_amount_name'
)
# 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值
try:

View File

@@ -169,6 +169,10 @@ FIELD_MAPPING = {
# 数据来源
'data_source': '数据来源',
# 自定义计算字段(虚拟字段)
'custom_service_fee': '自定义服务费(元)',
'total_amount': '实收金额(元)',
}
# 获取字段中文名称

View File

@@ -340,10 +340,14 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
report_date = start_time.strftime('%Y-%m-%d')
delete_old_history(config_id, report_date, config['split_type'], config['split_value'])
# 构建查询SQL - 过滤掉虚拟字段(时段电量等)
# 构建查询SQL - 过滤掉虚拟字段(时段电量、自定义服务费、实收金额等)
# 虚拟字段不是数据库表中的实际字段,需要动态计算
TIME_PERIOD_FIELDS = {'sharp_electricity', 'peak_electricity', 'flat_electricity', 'valley_electricity'}
db_fields = [f for f in selected_fields if f not in TIME_PERIOD_FIELDS]
VIRTUAL_FIELDS = {'sharp_electricity', 'peak_electricity', 'flat_electricity', 'valley_electricity', 'custom_service_fee', 'total_amount'}
db_fields = [f for f in selected_fields if f not in VIRTUAL_FIELDS]
# 如果选择了实收金额字段,需要确保查询 charge_elecfee_amount电费金额
if 'total_amount' in selected_fields and 'charge_elecfee_amount' not in db_fields:
db_fields.append('charge_elecfee_amount')
# 确保至少有 order_no 字段用于关联分时数据
if 'order_no' not in db_fields:
@@ -501,7 +505,10 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
split_type=config.get('split_type'),
split_value=config.get('split_value'),
custom_service_fee_price=config.get('custom_service_fee_price'),
custom_service_fee_name=config.get('custom_service_fee_name')
custom_service_fee_name=config.get('custom_service_fee_name'),
show_custom_service_fee=config.get('show_custom_service_fee', 0) == 1,
show_total_amount=config.get('show_total_amount', 0) == 1,
total_amount_name=config.get('total_amount_name')
)
log_info(f"[日报生成] Excel文件已生成: {file_path}", 'report')
@@ -591,7 +598,8 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
id_name_maps=None, field_custom_names=None, has_supplement=False,
show_monthly_total=False, config_id=None, report_date=None, sort_order=None,
split_type=None, split_value=None,
custom_service_fee_price=None, custom_service_fee_name=None):
custom_service_fee_price=None, custom_service_fee_name=None,
show_custom_service_fee=False, show_total_amount=False, total_amount_name=None):
"""
生成Excel文件
@@ -711,35 +719,36 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
cell.alignment = header_alignment
cell.border = thin_border
has_custom_service_fee = custom_service_fee_price is not None and custom_service_fee_price != ''
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]:
if field == 'custom_service_fee':
if custom_service_fee_name and str(custom_service_fee_name).strip():
display_name = str(custom_service_fee_name).strip()
elif has_custom_service_fee:
display_name = f'自定义服务费({custom_service_fee_price}元/kWh)'
else:
display_name = '自定义服务费'
elif field == 'total_amount':
if total_amount_name and str(total_amount_name).strip():
display_name = str(total_amount_name).strip()
else:
display_name = '实收金额'
elif field in field_custom_names and field_custom_names[field]:
display_name = field_custom_names[field]
elif field in id_name_maps and id_name_maps[field]:
display_name = get_field_display_name(field).replace('ID', '')
else:
display_name = get_field_display_name(field)
cell.value = display_name
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
# 自定义服务费列表头(如果设置了单价)
has_custom_service_fee = custom_service_fee_price is not None and custom_service_fee_price != ''
if has_custom_service_fee:
custom_fee_col = len(selected_fields) + 2
cell = ws.cell(row=header_row, column=custom_fee_col)
if custom_service_fee_name and str(custom_service_fee_name).strip():
header_name = str(custom_service_fee_name).strip()
else:
header_name = f'自定义服务费({custom_service_fee_price}元/kWh)'
cell.value = header_name
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
ws.row_dimensions[header_row].height = 25
# 写入数据
@@ -758,29 +767,44 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
for col_idx, field in enumerate(selected_fields, 2):
cell = ws.cell(row=row_idx, column=col_idx)
value = order.get(field, '')
# 格式化时间字段
if field in ['report_time', 'start_time', 'end_time', 'create_time', 'update_time', 'pay_time']:
if isinstance(value, datetime):
value = value.strftime('%Y-%m-%d %H:%M:%S')
# 将ID转换为名称
if field in id_name_maps and value is not None and value != '':
name_map = id_name_maps[field]
name_value = name_map.get(str(value))
if name_value:
value = name_value
if field == 'custom_service_fee':
if has_custom_service_fee:
charge_degree = float(order.get('charge_degree', 0) or 0)
value = round(charge_degree * float(custom_service_fee_price), 2)
else:
value = ''
elif field == 'total_amount':
actual_money = float(order.get('charge_elecfee_amount', 0) or 0)
if has_custom_service_fee:
charge_degree = float(order.get('charge_degree', 0) or 0)
custom_fee = round(charge_degree * float(custom_service_fee_price), 2)
value = round(actual_money + custom_fee, 2)
else:
value = round(actual_money, 2)
else:
value = order.get(field, '')
# 格式化时间字段
if field in ['report_time', 'start_time', 'end_time', 'create_time', 'update_time', 'pay_time']:
if isinstance(value, datetime):
value = value.strftime('%Y-%m-%d %H:%M:%S')
# 将ID转换为名称
if field in id_name_maps and value is not None and value != '':
name_map = id_name_maps[field]
name_value = name_map.get(str(value))
if name_value:
value = name_value
cell.value = value
cell.border = thin_border
# 数字字段靠右对齐,其他居中或靠左
if field in numeric_fields:
if field in numeric_fields or field in ['custom_service_fee', 'total_amount']:
cell.alignment = Alignment(horizontal='right', vertical='center')
if isinstance(value, (int, float)):
if field in decimal3_fields:
cell.number_format = '0.000'
cell.number_format = '0.00'
elif field in ['order_no', 'vehicle_no', 'gun_no', 'connector_no']:
cell.alignment = Alignment(horizontal='center', vertical='center')
else:
@@ -790,19 +814,6 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
if is_even:
cell.fill = even_row_fill
# 自定义服务费列(如果设置了单价)
if has_custom_service_fee:
custom_fee_col = len(selected_fields) + 2
cell = ws.cell(row=row_idx, column=custom_fee_col)
charge_degree = float(order.get('charge_degree', 0) or 0)
custom_fee = round(charge_degree * float(custom_service_fee_price), 2)
cell.value = custom_fee
cell.border = thin_border
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.00'
if is_even:
cell.fill = even_row_fill
ws.row_dimensions[row_idx].height = 22
# 写入求和行
@@ -819,25 +830,33 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
cell = ws.cell(row=sum_row, column=col_idx)
cell.fill = sum_fill
cell.border = thin_border
if field in sum_fields and field in sum_results:
if field == 'custom_service_fee':
if has_custom_service_fee:
total_charge_degree = sum_results.get('charge_degree', 0)
value = round(float(total_charge_degree) * float(custom_service_fee_price), 2)
cell.value = value
cell.font = sum_font
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.00'
elif field == 'total_amount':
total_actual_money = sum_results.get('charge_elecfee_amount', 0)
if has_custom_service_fee:
total_charge_degree = sum_results.get('charge_degree', 0)
total_custom_fee = round(float(total_charge_degree) * float(custom_service_fee_price), 2)
value = round(float(total_actual_money) + total_custom_fee, 2)
else:
value = round(float(total_actual_money), 2)
cell.value = value
cell.font = sum_font
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.00'
elif field in sum_fields and field in sum_results:
cell.value = sum_results[field]
cell.font = sum_font
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.000'
# 自定义服务费合计
if has_custom_service_fee:
custom_fee_col = len(selected_fields) + 2
cell = ws.cell(row=sum_row, column=custom_fee_col)
cell.fill = sum_fill
cell.border = thin_border
total_charge_degree = sum_results.get('charge_degree', 0)
total_custom_fee = round(float(total_charge_degree) * float(custom_service_fee_price), 2)
cell.value = total_custom_fee
cell.font = sum_font
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.00'
ws.row_dimensions[sum_row].height = 25
# 写入当月充电量累计总计行(从求和采集表查询,对账后数据)
@@ -881,8 +900,6 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
# 总列数
total_cols = len(selected_fields) + 2
if has_custom_service_fee:
total_cols += 1
# 标题文字
title_text = f'当月累计充电量(共{total_days}天,截止{report_date}'
@@ -935,10 +952,15 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
log_error(f"[日报生成] 写入月度总计时出错: {e}", 'report')
# 调整列宽中文按2个字符宽度计算
# 序号列固定宽度
from openpyxl.utils import get_column_letter
# 序号列固定宽度
ws.column_dimensions['A'].width = 8
# 计算所有字段的宽度
col_widths = {}
numeric_cols = []
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]:
@@ -955,21 +977,25 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
cell_len = len(cell_str) + cn_count
max_length = max(max_length, cell_len)
from openpyxl.utils import get_column_letter
col_letter = get_column_letter(col_idx)
ws.column_dimensions[col_letter].width = min(max_length + 2, 50)
col_widths[col_idx] = min(max_length + 2, 50)
# 标记数值类型字段(求和类列)
if field in numeric_fields or field in ['custom_service_fee', 'total_amount']:
numeric_cols.append(col_idx)
# 自定义服务费列宽度
if has_custom_service_fee:
custom_fee_col = len(selected_fields) + 2
if custom_service_fee_name and str(custom_service_fee_name).strip():
header_name = str(custom_service_fee_name).strip()
else:
header_name = f'自定义服务费({custom_service_fee_price}元/kWh)'
cn_count = sum(1 for c in str(header_name) if '\u4e00' <= c <= '\u9fff')
max_length = len(str(header_name)) + cn_count
col_letter = get_column_letter(custom_fee_col)
ws.column_dimensions[col_letter].width = min(max_length + 2, 50)
# 计算数值类型列的最大宽度,使所有求和类列宽度一致
if numeric_cols:
max_numeric_width = max(col_widths[col] for col in numeric_cols)
for col_idx in numeric_cols:
col_letter = get_column_letter(col_idx)
ws.column_dimensions[col_letter].width = max_numeric_width
# 设置非数值类型列的宽度
for col_idx, width in col_widths.items():
if col_idx not in numeric_cols:
col_letter = get_column_letter(col_idx)
ws.column_dimensions[col_letter].width = width
# 冻结首行
ws.freeze_panes = 'A2'

View File

@@ -373,6 +373,72 @@ def add_sum_data(data):
return {'success': False, 'message': str(e)}
def batch_add_sum_data(data):
"""
批量添加求和数据
Args:
data: 求和数据字典,包含 fields 列表
Returns:
dict: 操作结果
"""
try:
fields = data.get('fields', [])
if not fields:
return {'success': False, 'message': '请至少添加一个字段'}
# 获取配置的排序号
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, sort_order, data_source, remark,
create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
'''
count = 0
for field in fields:
sum_data_id = int(datetime.now().timestamp() * 1000) + count
params = (
sum_data_id,
data.get('report_date', ''),
data.get('config_id'),
data.get('config_name', ''),
data.get('split_type', ''),
data.get('split_value', ''),
data.get('split_name', ''),
field.get('sum_field_key', ''),
field.get('sum_field_name', ''),
float(field.get('sum_value', 0)),
data.get('total_orders', 0),
sort_order,
'manual',
data.get('remark', '')
)
execute_insert(sql, params)
count += 1
return {'success': True, 'message': f'添加成功,共{count}条记录', 'count': count}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def update_sum_data(sum_data_id, data):
"""
更新求和数据
@@ -549,16 +615,30 @@ def export_sum_data_to_excel(start_date=None, end_date=None, config_id=None, dat
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 ())
# 使用分页查询避免大数据量超时
rows = []
page_size = 500
page = 1
while True:
offset = (page - 1) * page_size
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
LIMIT %s OFFSET %s
'''
query_params = tuple(params) + (page_size, offset)
page_rows = execute_query(sql, query_params)
if not page_rows:
break
rows.extend(page_rows)
if len(page_rows) < page_size:
break
page += 1
# 创建Excel
wb = Workbook()
@@ -723,21 +803,22 @@ def get_monthly_charge_degree_total(config_id=None, month=None, split_type=None,
next_month = month[:5] + str(int(month[5:7]) + 1).zfill(2)
end_date_str = (datetime.strptime(next_month + '-01', '%Y-%m-%d') - timedelta(days=1)).strftime('%Y-%m-%d')
# 构建查询SQL
# 构建查询SQL - 查询所有字段,按日期统计(包括手动添加的数据)
# 使用 MAX(total_orders) 避免同一天多个字段重复计算订单数
sql = """
SELECT
report_date,
SUM(sum_value) as total_degree,
SUM(total_orders) as total_orders
SUM(CASE WHEN sum_field_key = 'charge_degree' THEN sum_value ELSE 0 END) as total_degree,
MAX(total_orders) as total_orders
FROM t_daily_report_sum_data
WHERE sum_field_key = 'charge_degree'
AND report_date LIKE %s
WHERE report_date LIKE %s
AND report_date <= %s
"""
params = [date_prefix + '%', end_date_str]
# 按配置过滤包含手动添加的数据手动添加的config_id为NULL
if config_id:
sql += ' AND config_id = %s'
sql += ' AND (config_id = %s OR config_id IS NULL)'
params.append(config_id)
# 按拆分值过滤(以配置当前拆分为准)

View File

@@ -34,7 +34,8 @@ function renderSumDataList(rawList) {
});
// 排序:峰平谷按顺序
const fieldOrder = ['sharp_electricity', 'peak_electricity', 'flat_electricity', 'valley_electricity'];
const fieldOrder = ['sharp_electricity', 'peak_electricity', 'flat_electricity', 'valley_electricity',
'peak_degree', 'peak_time_degree', 'normal_time_degree', 'valley_time_degree'];
sumDataDynamicFields = Object.keys(fieldMap).sort((a, b) => {
const idxA = fieldOrder.indexOf(a);
const idxB = fieldOrder.indexOf(b);
@@ -188,15 +189,15 @@ async function loadSumData(page = 1) {
}
const params = new URLSearchParams();
params.append('page', '1');
params.append('page_size', '5000');
params.append('page', page);
params.append('page_size', '50');
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?${params.toString()}`);
const response = await fetch(`/api/sum-data?${params.toString()}&_t=${Date.now()}`);
const result = await response.json();
if (result.success) {
renderSumDataList(result.data.list || []);
@@ -266,9 +267,11 @@ function showAddSumDataModal() {
document.getElementById('sum-data-split-type-input').value = 'company_id';
document.getElementById('sum-data-split-value-input').value = '';
document.getElementById('sum-data-split-name-input').value = '';
document.getElementById('sum-data-field-key-input').value = '';
document.getElementById('sum-data-field-name-input').value = '';
document.getElementById('sum-data-value-input').value = '';
document.getElementById('sum-data-charge-degree-input').value = '';
document.getElementById('sum-data-peak-degree-input').value = '';
document.getElementById('sum-data-peak-time-input').value = '';
document.getElementById('sum-data-normal-time-input').value = '';
document.getElementById('sum-data-valley-time-input').value = '';
document.getElementById('sum-data-total-orders-input').value = 0;
document.getElementById('sum-data-remark-input').value = '';
document.getElementById('sum-data-modal').classList.add('active');
@@ -280,8 +283,11 @@ let editingGroupData = null;
async function showEditSumDataGroup(ids) {
try {
// 获取所有选中的记录详情
const response = await fetch(`/api/sum-data?page=1&page_size=5000`);
const response = await fetch('/api/sum-data/batch-query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: ids })
});
const result = await response.json();
if (!result.success) {
@@ -289,8 +295,7 @@ async function showEditSumDataGroup(ids) {
return;
}
const allList = result.data.list || [];
const groupItems = allList.filter(item => ids.includes(item.id));
const groupItems = result.data || [];
if (groupItems.length === 0) {
alert('未找到该记录');
@@ -300,7 +305,6 @@ async function showEditSumDataGroup(ids) {
editingGroupIds = ids;
editingGroupData = groupItems;
// 找到充电电量那条作为主记录
const chargeItem = groupItems.find(i => i.sum_field_key === 'charge_degree') || groupItems[0];
document.getElementById('sum-data-modal-title').textContent = '编辑求和数据';
@@ -311,11 +315,10 @@ async function showEditSumDataGroup(ids) {
document.getElementById('sum-data-split-value-input').value = chargeItem.split_value || '';
document.getElementById('sum-data-split-name-input').value = chargeItem.split_name || '';
// 动态生成字段输入区域
renderEditGroupFields(groupItems);
document.getElementById('sum-data-total-orders-input').value = chargeItem.total_orders || 0;
document.getElementById('sum-data-remark-input').value = '';
document.getElementById('sum-data-remark-input').value = chargeItem.remark || '';
document.getElementById('sum-data-modal').classList.add('active');
} catch (error) {
alert('加载详情失败: ' + error.message);
@@ -324,9 +327,11 @@ async function showEditSumDataGroup(ids) {
// 渲染编辑分组时的动态字段输入
function renderEditGroupFields(items) {
const fieldKeyInput = document.getElementById('sum-data-field-key-input');
const fieldNameInput = document.getElementById('sum-data-field-name-input');
const valueInput = document.getElementById('sum-data-value-input');
// 隐藏静态字段输入区域(整个求和字段值区域)
const chargeDegreeInput = document.getElementById('sum-data-charge-degree-input');
if (chargeDegreeInput) {
chargeDegreeInput.parentElement.parentElement.parentElement.style.display = 'none';
}
// 先构造动态字段区域
let fieldsHtml = '<div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 8px; font-weight: 600;">求和字段值:</label>';
@@ -363,11 +368,6 @@ function renderEditGroupFields(items) {
fieldsHtml += '</div></div>';
// 替换掉原来的单字段输入
fieldKeyInput.parentElement.style.display = 'none';
fieldNameInput.parentElement.style.display = 'none';
valueInput.parentElement.style.display = 'none';
// 插入到订单数前面
const totalOrdersGroup = document.getElementById('sum-data-total-orders-input').parentElement;
@@ -404,6 +404,8 @@ async function saveSumData() {
const fieldId = input.getAttribute('data-field-id');
const fieldValue = input.value;
console.log(`更新字段ID: ${fieldId}, 值: ${fieldValue}`);
const response = await fetch(`/api/sum-data/${fieldId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -414,6 +416,7 @@ async function saveSumData() {
})
});
const result = await response.json();
console.log(`更新结果: ${JSON.stringify(result)}`);
if (result.success) successCount++;
}
@@ -427,34 +430,73 @@ async function saveSumData() {
return;
}
// 单条新增模式
// 单条新增模式(批量添加多个字段)
const fieldsData = [];
const chargeDegree = document.getElementById('sum-data-charge-degree-input').value;
const peakDegree = document.getElementById('sum-data-peak-degree-input').value;
const peakTime = document.getElementById('sum-data-peak-time-input').value;
const normalTime = document.getElementById('sum-data-normal-time-input').value;
const valleyTime = document.getElementById('sum-data-valley-time-input').value;
if (!chargeDegree && !peakDegree && !peakTime && !normalTime && !valleyTime) {
alert('请至少输入一个求和字段值');
return;
}
if (chargeDegree) {
fieldsData.push({
sum_field_key: 'charge_degree',
sum_field_name: '充电电量(kWh)',
sum_value: chargeDegree
});
}
if (peakDegree) {
fieldsData.push({
sum_field_key: 'peak_degree',
sum_field_name: '尖时电量(kWh)',
sum_value: peakDegree
});
}
if (peakTime) {
fieldsData.push({
sum_field_key: 'peak_time_degree',
sum_field_name: '峰时电量(kWh)',
sum_value: peakTime
});
}
if (normalTime) {
fieldsData.push({
sum_field_key: 'normal_time_degree',
sum_field_name: '平时电量(kWh)',
sum_value: normalTime
});
}
if (valleyTime) {
fieldsData.push({
sum_field_key: 'valley_time_degree',
sum_field_name: '谷时电量(kWh)',
sum_value: valleyTime
});
}
const data = {
report_date: document.getElementById('sum-data-report-date-input').value,
config_name: document.getElementById('sum-data-config-name-input').value,
split_type: document.getElementById('sum-data-split-type-input').value,
split_value: document.getElementById('sum-data-split-value-input').value,
split_name: document.getElementById('sum-data-split-name-input').value,
sum_field_key: document.getElementById('sum-data-field-key-input').value,
sum_field_name: document.getElementById('sum-data-field-name-input').value,
sum_value: document.getElementById('sum-data-value-input').value,
total_orders: totalOrders,
remark: remark
remark: remark,
fields: fieldsData
};
if (!data.report_date) {
alert('请选择报表日期');
return;
}
if (!data.sum_field_key) {
alert('请输入求和字段键名');
return;
}
if (data.sum_value === '' || data.sum_value === null) {
alert('请输入求和值');
return;
}
const response = await fetch('/api/sum-data', {
const response = await fetch('/api/sum-data/batch-add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
@@ -478,12 +520,22 @@ function closeSumDataModal() {
document.getElementById('sum-data-modal').classList.remove('active');
editingGroupIds = [];
editingGroupData = null;
// 恢复单字段输入的显示
document.querySelector('#sum-data-field-key-input').parentElement.style.display = '';
document.querySelector('#sum-data-field-name-input').parentElement.style.display = '';
document.querySelector('#sum-data-value-input').parentElement.style.display = '';
// 清空新增字段输入
document.getElementById('sum-data-charge-degree-input').value = '';
document.getElementById('sum-data-peak-degree-input').value = '';
document.getElementById('sum-data-peak-time-input').value = '';
document.getElementById('sum-data-normal-time-input').value = '';
document.getElementById('sum-data-valley-time-input').value = '';
// 恢复静态字段区域显示
const chargeDegreeInput = document.getElementById('sum-data-charge-degree-input');
if (chargeDegreeInput) {
chargeDegreeInput.parentElement.parentElement.parentElement.style.display = '';
}
// 清理动态区域
const dynamicArea = document.getElementById('sum-data-dynamic-fields');
if (dynamicArea) dynamicArea.innerHTML = '';
if (dynamicArea) {
dynamicArea.remove();
}
}
// 删除单条(分组)

View File

@@ -297,6 +297,8 @@
<input type="text" id="custom-service-fee-name" class="form-control" placeholder="留空则使用默认名称(如:自定义服务费(0.8元/kWh)" style="width: 300px;">
</div>
<div class="form-group">
<label>分时段电量统计:</label>
<div id="time-period-editor">
@@ -364,18 +366,29 @@
</div>
<div class="form-group">
<label>求和字段键名</label>
<input type="text" id="sum-data-field-key-input" class="form-control" placeholder="例如charge_degree">
</div>
<div class="form-group">
<label>求和字段名称:</label>
<input type="text" id="sum-data-field-name-input" class="form-control" placeholder="例如:充电电量(kWh)">
</div>
<div class="form-group">
<label>求和值:</label>
<input type="number" step="0.001" id="sum-data-value-input" class="form-control" placeholder="请输入求和值">
<label>求和字段</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group" style="margin-bottom: 0;">
<label>充电电量(kWh) <span style="color: #dc3545;">*</span></label>
<input type="number" step="0.001" id="sum-data-charge-degree-input" class="form-control" placeholder="充电电量">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label>尖时电量(kWh)</label>
<input type="number" step="0.001" id="sum-data-peak-degree-input" class="form-control" placeholder="尖时电量">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label>峰时电量(kWh)</label>
<input type="number" step="0.001" id="sum-data-peak-time-input" class="form-control" placeholder="峰时电量">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label>平时电量(kWh)</label>
<input type="number" step="0.001" id="sum-data-normal-time-input" class="form-control" placeholder="平时电量">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label>谷时电量(kWh)</label>
<input type="number" step="0.001" id="sum-data-valley-time-input" class="form-control" placeholder="谷时电量">
</div>
</div>
</div>
<div class="form-group">