From fa884ae782017b559f680d83f6fceb4758cb2639 Mon Sep 17 00:00:00 2001 From: haoslm <6083297@qq.com> Date: Thu, 23 Jul 2026 14:57:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B1=82=E5=92=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=9F=E8=AE=A1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- check_data.py | 35 +++++++ check_history.py | 29 ++++++ lib/api/config_api.py | 28 +++++- lib/api/sum_data_api.py | 54 +++++++++- lib/db.py | 95 ++++++++++++------ lib/db_init.py | 21 ++++ lib/field_mapping.py | 4 + lib/report_generator.py | 184 ++++++++++++++++++++--------------- lib/sum_data_collector.py | 113 ++++++++++++++++++--- public/static/js/sum_data.js | 134 +++++++++++++++++-------- templates/index.html | 37 ++++--- 11 files changed, 550 insertions(+), 184 deletions(-) create mode 100644 check_data.py create mode 100644 check_history.py diff --git a/check_data.py b/check_data.py new file mode 100644 index 0000000..4a395aa --- /dev/null +++ b/check_data.py @@ -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']}") diff --git a/check_history.py b/check_history.py new file mode 100644 index 0000000..8b14a56 --- /dev/null +++ b/check_history.py @@ -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}") diff --git a/lib/api/config_api.py b/lib/api/config_api.py index 6fcd592..448f99c 100644 --- a/lib/api/config_api.py +++ b/lib/api/config_api.py @@ -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 ) diff --git a/lib/api/sum_data_api.py b/lib/api/sum_data_api.py index 714b52a..8509cb3 100644 --- a/lib/api/sum_data_api.py +++ b/lib/api/sum_data_api.py @@ -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表查询,对账后的数据) diff --git a/lib/db.py b/lib/db.py index 1da197d..9b91e11 100644 --- a/lib/db.py +++ b/lib/db.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/lib/db_init.py b/lib/db_init.py index f62fcbd..23034cc 100644 --- a/lib/db_init.py +++ b/lib/db_init.py @@ -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: diff --git a/lib/field_mapping.py b/lib/field_mapping.py index 406086d..e02b746 100644 --- a/lib/field_mapping.py +++ b/lib/field_mapping.py @@ -169,6 +169,10 @@ FIELD_MAPPING = { # 数据来源 'data_source': '数据来源', + + # 自定义计算字段(虚拟字段) + 'custom_service_fee': '自定义服务费(元)', + 'total_amount': '实收金额(元)', } # 获取字段中文名称 diff --git a/lib/report_generator.py b/lib/report_generator.py index 083992f..65e539e 100644 --- a/lib/report_generator.py +++ b/lib/report_generator.py @@ -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' diff --git a/lib/sum_data_collector.py b/lib/sum_data_collector.py index 44dc285..93754bd 100644 --- a/lib/sum_data_collector.py +++ b/lib/sum_data_collector.py @@ -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) # 按拆分值过滤(以配置当前拆分为准) diff --git a/public/static/js/sum_data.js b/public/static/js/sum_data.js index 0ed2de7..bad563f 100644 --- a/public/static/js/sum_data.js +++ b/public/static/js/sum_data.js @@ -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 = '
'; @@ -363,11 +368,6 @@ function renderEditGroupFields(items) { fieldsHtml += '
'; - // 替换掉原来的单字段输入 - 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(); + } } // 删除单条(分组) diff --git a/templates/index.html b/templates/index.html index 59930ea..1dab430 100644 --- a/templates/index.html +++ b/templates/index.html @@ -297,6 +297,8 @@ + +
@@ -364,18 +366,29 @@
- - -
- -
- - -
- -
- - + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+