增加日志

This commit is contained in:
2026-07-15 16:01:06 +08:00
parent d34dd82bd5
commit 727f5434b2
11 changed files with 461 additions and 192 deletions

View File

@@ -5,6 +5,7 @@ import json
from datetime import datetime
from flask import request, jsonify
from lib.db import execute_query, execute_update, execute_insert
from lib.logger import log_info, log_error, log_warning
from . import config_bp
@@ -12,6 +13,7 @@ from . import config_bp
def get_configs():
"""获取所有配置"""
try:
log_info('[配置API] 获取配置列表', 'api')
configs = execute_query(
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id DESC'
)
@@ -22,8 +24,10 @@ def get_configs():
config['sum_fields'] = json.loads(config['sum_fields']) if config['sum_fields'] else []
config['time_periods'] = json.loads(config['time_periods']) if config.get('time_periods') else {}
log_info(f'[配置API] 获取到 {len(configs)} 条配置', 'api')
return jsonify({'success': True, 'data': configs})
except Exception as e:
log_error(f'[配置API] 获取配置列表失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -31,6 +35,7 @@ def get_configs():
def create_config():
"""创建配置"""
try:
log_info('[配置API] 创建配置', 'api')
data = request.json
config_name = data.get('config_name')
split_type = data.get('split_type')
@@ -52,9 +57,11 @@ def create_config():
custom_service_fee_name = data.get('custom_service_fee_name')
if not config_name or not split_type or not split_value:
log_warning('[配置API] 创建配置失败:缺少必填字段', 'api')
return jsonify({'success': False, 'message': '缺少必填字段'}), 400
if not selected_fields:
log_warning('[配置API] 创建配置失败:未选择字段', 'api')
return jsonify({'success': False, 'message': '请至少选择一个字段'}), 400
# 生成 ID
@@ -92,12 +99,14 @@ def create_config():
execute_insert(sql, params)
log_info(f'[配置API] 配置创建成功,ID: {config_id},名称: {config_name}', 'api')
return jsonify({
'success': True,
'message': '配置创建成功',
'data': {'id': config_id}
})
except Exception as e:
log_error(f'[配置API] 创建配置失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -105,6 +114,7 @@ def create_config():
def update_config(config_id):
"""更新配置"""
try:
log_info(f'[配置API] 更新配置,ID: {config_id}', 'api')
data = request.json
config_name = data.get('config_name')
split_type = data.get('split_type')
@@ -125,6 +135,12 @@ def update_config(config_id):
custom_service_fee_name = data.get('custom_service_fee_name')
# 查询原配置,检查拆分是否变化
old_configs = execute_query('SELECT * FROM t_daily_report_config WHERE id = %s', (config_id,))
if not old_configs:
return jsonify({'success': False, 'message': '配置不存在'}), 404
old_config = old_configs[0]
sql = """
UPDATE t_daily_report_config
SET config_name = %s, split_type = %s, split_value = %s, split_name = %s,
@@ -154,8 +170,32 @@ def update_config(config_id):
execute_update(sql, params)
# 如果拆分类型或拆分值变化了,清理旧的求和数据(以当前拆分为准)
old_split_type = old_config.get('split_type')
old_split_value = old_config.get('split_value', '')
if old_split_type != split_type or old_split_value != split_value:
new_values = set(v.strip() for v in split_value.split(',') if v.strip())
old_values = set(v.strip() for v in str(old_split_value).split(',') if v.strip())
removed_values = old_values - new_values
if removed_values:
placeholders = ','.join(['%s'] * len(removed_values))
delete_sql = f'''
DELETE FROM t_daily_report_sum_data
WHERE config_id = %s AND split_type = %s
AND split_value IN ({placeholders})
'''
delete_params = [config_id, old_split_type] + list(removed_values)
try:
from lib.db import execute_update
execute_update(delete_sql, tuple(delete_params))
log_info(f'[配置API] 拆分值变化,已清理{len(removed_values)}个旧拆分的求和数据', 'api')
except Exception as e:
log_error(f'[配置API] 清理旧求和数据失败: {e}', 'api')
log_info(f'[配置API] 配置更新成功,ID: {config_id}', 'api')
return jsonify({'success': True, 'message': '配置更新成功'})
except Exception as e:
log_error(f'[配置API] 更新配置失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -163,9 +203,18 @@ def update_config(config_id):
def delete_config(config_id):
"""删除配置"""
try:
log_info(f'[配置API] 删除配置,ID: {config_id}', 'api')
execute_update('DELETE FROM t_daily_report_config WHERE id = %s', (config_id,))
# 同时删除对应的求和数据
try:
execute_update('DELETE FROM t_daily_report_sum_data WHERE config_id = %s', (config_id,))
log_info(f'[配置API] 已清理配置对应的求和数据', 'api')
except Exception as e:
log_error(f'[配置API] 清理求和数据失败: {e}', 'api')
log_info(f'[配置API] 配置删除成功,ID: {config_id}', 'api')
return jsonify({'success': True, 'message': '配置删除成功'})
except Exception as e:
log_error(f'[配置API] 删除配置失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -236,15 +285,13 @@ def move_config(config_id):
"""移动配置排序"""
try:
direction = request.json.get('direction') # up 或 down
print(f"[move_config] 开始移动 config_id={config_id}, direction={direction}")
log_info(f"[配置API] 移动配置,ID: {config_id}, 方向: {direction}", 'api')
# 查出所有配置,按 sort_order, id 排序
all_configs = execute_query(
'SELECT id, sort_order FROM t_daily_report_config ORDER BY sort_order ASC, id ASC'
)
print(f"[move_config] 所有配置数量: {len(all_configs)}")
for idx, c in enumerate(all_configs):
print(f" [{idx}] id={c['id']}, sort_order={c['sort_order']}")
log_info(f"[配置API] 所有配置数量: {len(all_configs)}", 'api')
# 找到当前配置的索引
current_index = None
@@ -254,10 +301,10 @@ def move_config(config_id):
break
if current_index is None:
print(f"[move_config] 未找到配置 id={config_id}")
log_warning(f"[配置API] 未找到配置,ID: {config_id}", 'api')
return jsonify({'success': False, 'message': '配置不存在'}), 404
print(f"[move_config] 当前配置索引: {current_index}")
log_info(f"[配置API] 当前配置索引: {current_index}", 'api')
# 计算目标索引
if direction == 'up':
@@ -269,29 +316,24 @@ def move_config(config_id):
# 边界检查
if target_index < 0 or target_index >= len(all_configs):
print(f"[move_config] 已到边界,无法移动 target_index={target_index}")
log_info(f"[配置API] 已到边界,无法移动", 'api')
return jsonify({'success': True, 'message': '已到边界'})
# 在内存数组中交换位置
all_configs[current_index], all_configs[target_index] = all_configs[target_index], all_configs[current_index]
print(f"[move_config] 交换后顺序: {[c['id'] for c in all_configs]}")
# 全量重排:按新顺序给每个配置重新赋值 sort_order
for idx, cfg in enumerate(all_configs):
new_sort = idx + 1
print(f" 更新 id={cfg['id']}: sort_order {cfg['sort_order']} -> {new_sort}")
execute_update(
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
(new_sort, cfg['id'])
)
print("[move_config] 重排完成")
log_info(f"[配置API] 配置移动成功,ID: {config_id}", 'api')
return jsonify({'success': True, 'message': '移动成功'})
except Exception as e:
import traceback
traceback.print_exc()
print(f"[move_config] 异常: {e}")
log_error(f"[配置API] 移动配置失败: {e}", 'api')
return jsonify({'success': False, 'message': str(e)}), 500

View File

@@ -7,6 +7,7 @@ from datetime import datetime
from flask import request, jsonify, send_from_directory
from lib.db import execute_query, execute_update
from lib.report_generator import generate_daily_report
from lib.logger import log_info, log_error, log_warning
from . import report_bp
@@ -16,8 +17,10 @@ def generate_report():
try:
data = request.json
config_id = data.get('config_id')
log_info(f'[报表API] 生成日报,配置ID: {config_id}', 'api')
if not config_id:
log_warning('[报表API] 生成日报失败:未选择配置', 'api')
return jsonify({'success': False, 'message': '请选择配置'}), 400
# 解析时间参数
@@ -32,10 +35,13 @@ def generate_report():
result = generate_daily_report(config_id, start_time, end_time)
if result['success']:
log_info(f'[报表API] 日报生成成功,订单数: {result.get("total_orders", 0)}', 'api')
return jsonify(result)
else:
log_warning(f'[报表API] 日报生成失败: {result.get("message", "")}', 'api')
return jsonify(result), 400
except Exception as e:
log_error(f'[报表API] 生成日报异常: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -47,6 +53,8 @@ def get_report_history():
page_size = int(request.args.get('page_size', 20))
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
log_info(f'[报表API] 获取历史记录,第{page}页,日期范围: {start_date or "开始"} ~ {end_date or "结束"}', 'api')
offset = (page - 1) * page_size
# 构建查询条件
@@ -82,10 +90,8 @@ def get_report_history():
# 确保 file_path 是字符串
if item['file_path'] is None:
item['file_path'] = ''
# 调试日志
print(f"[历史记录] ID: {item['id']}, status: {item['status']}, file_path: '{item['file_path']}'")
log_info(f'[报表API] 获取到 {len(history)} 条历史记录,共 {total} 条', 'api')
return jsonify({
'success': True,
'data': {
@@ -96,9 +102,7 @@ def get_report_history():
}
})
except Exception as e:
print(f"[历史记录 API] 错误:{e}")
import traceback
traceback.print_exc()
log_error(f'[报表API] 获取历史记录失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -106,6 +110,7 @@ def get_report_history():
def delete_report_history(history_id):
"""删除单个历史记录"""
try:
log_info(f'[报表API] 删除历史记录,ID: {history_id}', 'api')
# 先查询文件路径
result = execute_query(
'SELECT file_path FROM t_daily_report_history WHERE id = %s',
@@ -117,15 +122,15 @@ def delete_report_history(history_id):
# 删除文件
if file_path and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/'))):
os.remove(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/')))
log_info(f'[报表API] 已删除报表文件: {file_path}', 'api')
# 删除数据库记录
execute_update('DELETE FROM t_daily_report_history WHERE id = %s', (history_id,))
log_info(f'[报表API] 历史记录删除成功,ID: {history_id}', 'api')
return jsonify({'success': True, 'message': '删除成功'})
except Exception as e:
print(f"[删除历史记录] 错误:{e}")
import traceback
traceback.print_exc()
log_error(f'[报表API] 删除历史记录失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -135,8 +140,10 @@ def batch_delete_report_history():
try:
data = request.get_json()
ids = data.get('ids', [])
log_info(f'[报表API] 批量删除历史记录,数量: {len(ids)}', 'api')
if not ids:
log_warning('[报表API] 批量删除失败:未选择记录', 'api')
return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400
# 查询文件路径
@@ -146,10 +153,14 @@ def batch_delete_report_history():
)
# 删除文件
deleted_files = 0
for result in results:
file_path = result.get('file_path', '')
if file_path and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/'))):
os.remove(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/')))
deleted_files += 1
log_info(f'[报表API] 已删除 {deleted_files} 个报表文件', 'api')
# 删除数据库记录
execute_update(
@@ -157,11 +168,10 @@ def batch_delete_report_history():
tuple(ids)
)
log_info(f'[报表API] 批量删除成功,共 {len(ids)} 条记录', 'api')
return jsonify({'success': True, 'message': f'成功删除 {len(ids)} 条记录'})
except Exception as e:
print(f"[批量删除历史记录] 错误:{e}")
import traceback
traceback.print_exc()
log_error(f'[报表API] 批量删除历史记录失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -169,6 +179,7 @@ def batch_delete_report_history():
def download_report_by_id(history_id):
"""根据 ID 下载历史记录"""
try:
log_info(f'[报表API] 下载报表,历史记录ID: {history_id}', 'api')
# 查询历史记录
result = execute_query(
'SELECT file_path, split_name, report_date FROM t_daily_report_history WHERE id = %s',
@@ -176,24 +187,26 @@ def download_report_by_id(history_id):
)
if not result:
log_warning(f'[报表API] 下载报表失败:记录不存在,ID: {history_id}', 'api')
return jsonify({'success': False, 'message': '记录不存在'}), 404
file_path = result[0]['file_path']
if not file_path:
log_warning(f'[报表API] 下载报表失败:文件路径为空,ID: {history_id}', 'api')
return jsonify({'success': False, 'message': '文件不存在'}), 404
# 构建完整路径
full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/'))
if not os.path.exists(full_path):
log_warning(f'[报表API] 下载报表失败:文件不存在,路径: {full_path}', 'api')
return jsonify({'success': False, 'message': '文件不存在'}), 404
# 返回文件
directory = os.path.dirname(full_path)
filename = os.path.basename(full_path)
log_info(f'[报表API] 报表下载成功: {filename}', 'api')
return send_from_directory(directory, filename, as_attachment=True)
except Exception as e:
print(f"[下载历史记录] 错误:{e}")
import traceback
traceback.print_exc()
log_error(f'[报表API] 下载报表失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500

View File

@@ -12,6 +12,7 @@ from lib.sum_data_collector import (
get_monthly_charge_degree_total,
export_sum_data_to_excel
)
from lib.logger import log_info, log_error, log_warning
from . import sum_data_bp
@@ -21,15 +22,17 @@ def collect_sum_data():
try:
data = request.json or {}
history_id = data.get('history_id')
log_info(f'[求和数据API] 采集求和数据,历史记录ID: {history_id or "全部"}', 'api')
result = collect_sum_data_from_history(history_id)
if result['success']:
log_info(f'[求和数据API] 采集成功: {result["message"]}', 'api')
return jsonify(result)
else:
log_warning(f'[求和数据API] 采集失败: {result["message"]}', 'api')
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 采集异常: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -45,6 +48,7 @@ def list_sum_data():
config_name = request.args.get('config_name')
sum_field_key = request.args.get('sum_field_key')
data_source = request.args.get('data_source')
log_info(f'[求和数据API] 查询求和数据列表,第{page}页', 'api')
result = get_sum_data_list(
page=page,
@@ -58,12 +62,12 @@ def list_sum_data():
)
if result['success']:
log_info(f'[求和数据API] 查询成功,共{result["data"]["total"]}条记录', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 查询求和数据列表失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -72,14 +76,15 @@ def create_sum_data():
"""手动添加求和数据"""
try:
data = request.json
log_info('[求和数据API] 手动添加求和数据', 'api')
result = add_sum_data(data)
if result['success']:
log_info(f'[求和数据API] 添加成功,ID: {result.get("id", "")}', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 添加求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -88,14 +93,15 @@ def update_sum_data_api(sum_data_id):
"""更新求和数据"""
try:
data = request.json
log_info(f'[求和数据API] 更新求和数据,ID: {sum_data_id}', '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:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 更新求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -103,14 +109,15 @@ def update_sum_data_api(sum_data_id):
def delete_sum_data_api(sum_data_id):
"""删除求和数据"""
try:
log_info(f'[求和数据API] 删除求和数据,ID: {sum_data_id}', 'api')
result = delete_sum_data(sum_data_id)
if result['success']:
log_info(f'[求和数据API] 删除成功,ID: {sum_data_id}', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 删除求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -120,17 +127,19 @@ def batch_delete_sum_data_api():
try:
data = request.json
ids = data.get('ids', [])
log_info(f'[求和数据API] 批量删除求和数据,数量: {len(ids)}', 'api')
if not ids:
log_warning('[求和数据API] 批量删除失败:未选择记录', 'api')
return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400
result = batch_delete_sum_data(ids)
if result['success']:
log_info(f'[求和数据API] 批量删除成功,{result["message"]}', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 批量删除求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -145,15 +154,16 @@ def monthly_charge_degree_total():
try:
config_id = request.args.get('config_id')
month = request.args.get('month')
log_info(f'[求和数据API] 查询月度累计,配置ID: {config_id or "全部"},月份: {month or "当月"}', 'api')
result = get_monthly_charge_degree_total(config_id, month)
if result['success']:
log_info(f'[求和数据API] 查询成功,累计电量: {result["data"]["total_degree"]} kWh', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 查询月度累计失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500
@@ -174,6 +184,7 @@ def export_sum_data():
config_id = request.args.get('config_id')
data_source = request.args.get('data_source')
keyword = request.args.get('keyword')
log_info(f'[求和数据API] 导出求和数据,日期范围: {start_date or "开始"} ~ {end_date or "结束"}', 'api')
result = export_sum_data_to_excel(
start_date=start_date,
@@ -184,10 +195,10 @@ def export_sum_data():
)
if result['success']:
log_info(f'[求和数据API] 导出成功,共{result.get("total", 0)}条记录', 'api')
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[求和数据API] 导出求和数据失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500

View File

@@ -3,6 +3,7 @@
"""
import pymysql
from contextlib import contextmanager
from lib.logger import log_info, log_error, log_warning
# 数据库配置
DB_CONFIG = {
@@ -23,32 +24,52 @@ DB_CONFIG = {
@contextmanager
def get_connection():
"""获取数据库连接(上下文管理器)"""
conn = pymysql.connect(**DB_CONFIG)
try:
conn = pymysql.connect(**DB_CONFIG)
yield conn
except pymysql.Error as e:
log_error(f'数据库连接失败: {e}', 'db')
raise
finally:
conn.close()
try:
conn.close()
except:
pass
def execute_query(sql, params=None):
"""执行查询并返回结果"""
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
return cursor.fetchall()
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')
raise
def execute_update(sql, params=None):
"""执行更新操作"""
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
return cursor.rowcount
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')
raise
def execute_insert(sql, params=None):
"""执行插入操作并返回插入ID"""
with get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(sql, params)
return cursor.lastrowid
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')
raise

View File

@@ -2,6 +2,36 @@
数据库初始化
"""
from lib.db import execute_update, execute_query
from lib.logger import log_info, log_error, log_warning
def _column_exists(table_name, column_name):
"""检查字段是否存在(通过 information_schema 查询)"""
try:
result = execute_query("""
SELECT COUNT(*) as cnt
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = %s
AND column_name = %s
""", (table_name, column_name))
return result and result[0]['cnt'] > 0
except Exception:
return False
def _add_column_if_not_exists(table_name, column_def_sql, column_name):
"""安全地添加字段:先检查是否存在,不存在再添加"""
if _column_exists(table_name, column_name):
log_info(f"[数据库] 字段 {table_name}.{column_name} 已存在,跳过", 'db_init')
return True
try:
execute_update(f"ALTER TABLE {table_name} ADD COLUMN {column_def_sql}")
log_info(f"[数据库] 新增字段 {table_name}.{column_name} 成功", 'db_init')
return True
except Exception as e:
log_warning(f"[数据库] 新增字段 {table_name}.{column_name} 失败: {e}", 'db_init')
return False
def init_database_tables():
@@ -34,7 +64,7 @@ def init_database_tables():
PROPERTIES("replication_num" = "1")
"""
execute_update(create_config_table)
print("[数据库] t_daily_report_config 表已就绪")
log_info("[数据库] t_daily_report_config 表已就绪", 'db_init')
# 创建历史表
create_history_table = """
@@ -60,16 +90,14 @@ def init_database_tables():
PROPERTIES("replication_num" = "1")
"""
execute_update(create_history_table)
print("[数据库] t_daily_report_history 表已就绪")
log_info("[数据库] t_daily_report_history 表已就绪", 'db_init')
# 为已存在的表添加 config_name 字段(如果不存在)
try:
execute_update("""
ALTER TABLE t_daily_report_history
ADD COLUMN IF NOT EXISTS config_name VARCHAR(200) NULL COMMENT '配置名称'
""")
except Exception as e:
print(f"[数据库] 添加 config_name 字段时出错(可能已存在): {e}")
_add_column_if_not_exists(
't_daily_report_history',
"config_name VARCHAR(200) NULL COMMENT '配置名称'",
'config_name'
)
# 创建求和数据表
create_sum_data_table = """
@@ -96,56 +124,35 @@ def init_database_tables():
PROPERTIES("replication_num" = "1")
"""
execute_update(create_sum_data_table)
print("[数据库] t_daily_report_sum_data 表已就绪")
log_info("[数据库] t_daily_report_sum_data 表已就绪", 'db_init')
# 给配置表增加 show_monthly_total 字段
try:
execute_update(
"ALTER TABLE t_daily_report_config ADD COLUMN show_monthly_total TINYINT NULL COMMENT '是否显示当月充电量总计' "
)
print("[数据库] 配置表新增 show_monthly_total 字段成功")
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"[数据库] 新增 show_monthly_total 字段时出错(可能已存在): {e}")
_add_column_if_not_exists(
't_daily_report_config',
"show_monthly_total TINYINT NULL COMMENT '是否显示当月充电量总计'",
'show_monthly_total'
)
# 给求和数据表增加 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}")
_add_column_if_not_exists(
't_daily_report_sum_data',
"sort_order INT NULL COMMENT '配置排序号'",
'sort_order'
)
# 给配置表增加 custom_service_fee_price 字段(自定义服务费单价)
try:
execute_update(
"ALTER TABLE t_daily_report_config ADD COLUMN custom_service_fee_price DECIMAL(10,4) NULL COMMENT '自定义服务费单价(元/kWh)' "
)
print("[数据库] 配置表新增 custom_service_fee_price 字段成功")
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"[数据库] 新增 custom_service_fee_price 字段时出错(可能已存在): {e}")
_add_column_if_not_exists(
't_daily_report_config',
"custom_service_fee_price DECIMAL(10,4) NULL COMMENT '自定义服务费单价(元/kWh)'",
'custom_service_fee_price'
)
# 给配置表增加 custom_service_fee_name 字段(自定义服务费表头名称)
try:
execute_update(
"ALTER TABLE t_daily_report_config ADD COLUMN custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称' "
)
print("[数据库] 配置表新增 custom_service_fee_name 字段成功")
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"[数据库] 新增 custom_service_fee_name 字段时出错(可能已存在): {e}")
_add_column_if_not_exists(
't_daily_report_config',
"custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称'",
'custom_service_fee_name'
)
# 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值
try:
@@ -153,7 +160,7 @@ def init_database_tables():
"SELECT COUNT(*) as cnt FROM t_daily_report_config WHERE sort_order IS NULL"
)
if null_count and null_count[0]['cnt'] > 0:
print(f"[数据库] 检测到 {null_count[0]['cnt']} 条配置 sort_order 为空,开始初始化排序...")
log_info(f"[数据库] 检测到 {null_count[0]['cnt']} 条配置 sort_order 为空,开始初始化排序...", 'db_init')
all_configs = execute_query(
"SELECT id FROM t_daily_report_config ORDER BY id ASC"
)
@@ -162,11 +169,12 @@ def init_database_tables():
"UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s",
(idx + 1, cfg['id'])
)
print("[数据库] 配置 sort_order 初始化完成")
log_info("[数据库] 配置 sort_order 初始化完成", 'db_init')
except Exception as e:
print(f"[数据库] 初始化 sort_order 时出错: {e}")
log_warning(f"[数据库] 初始化 sort_order 时出错: {e}", 'db_init')
log_info("[数据库] 初始化完成", 'db_init')
return True
except Exception as e:
print(f"[数据库] 初始化失败: {e}")
log_error(f"[数据库] 初始化失败: {e}", 'db_init')
return False

85
lib/logger.py Normal file
View File

@@ -0,0 +1,85 @@
import os
import logging
from logging.handlers import RotatingFileHandler
from datetime import datetime
_loggers = {}
def get_logger(name='app'):
"""
获取日志记录器
Args:
name: 日志名称(对应日志文件名)
Returns:
logging.Logger: 日志记录器
"""
if name in _loggers:
return _loggers[name]
# 日志目录
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
os.makedirs(log_dir, exist_ok=True)
# 日志文件路径
log_file = os.path.join(log_dir, f'{name}.log')
# 创建 logger
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
logger.propagate = False
# 避免重复添加 handler
if logger.handlers:
_loggers[name] = logger
return logger
# 日志格式
formatter = logging.Formatter(
'%(asctime)s | %(levelname)-7s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 文件 handler(按大小轮转,最多 5 个备份,每个 10MB)
file_handler = RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024,
backupCount=5,
encoding='utf-8'
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# 控制台 handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
_loggers[name] = logger
return logger
def log_info(message, module='app'):
"""记录信息日志"""
get_logger(module).info(message)
def log_error(message, module='app'):
"""记录错误日志"""
get_logger(module).error(message)
import traceback
get_logger(module).error(traceback.format_exc())
def log_warning(message, module='app'):
"""记录警告日志"""
get_logger(module).warning(message)
def log_debug(message, module='app'):
"""记录调试日志"""
get_logger(module).debug(message)

View File

@@ -10,6 +10,7 @@ import pytz
from lib.db import execute_query, execute_insert
from lib.field_mapping import get_field_display_name
from lib.logger import log_info, log_error, log_warning
# ID字段到名称的映射配置
@@ -102,9 +103,9 @@ def query_telecom_orders(vehicle_self_nos, start_time_str, end_time_str):
try:
results = execute_query(sql, params)
print(f"[特来电] 查询到 {len(results)} 条订单")
log_info(f"[特来电] 查询到 {len(results)} 条订单", 'report')
except Exception as e:
print(f"[特来电] 查询失败: {e}")
log_error(f"[特来电] 查询失败: {e}", 'report')
return []
# 转换字段名
@@ -165,7 +166,7 @@ def build_id_to_name_map(orders, selected_fields):
# 合并特来电的名称映射
if telecom_name_values:
id_name_maps[field] = telecom_name_values
print(f"[ID转换] {field}: 特来电数据直接使用名称 {len(telecom_name_values)} 个")
log_info(f"[ID转换] {field}: 特来电数据直接使用名称 {len(telecom_name_values)} 个", 'report')
# 查询驿来特数据的ID映射
if not id_values:
@@ -180,9 +181,9 @@ def build_id_to_name_map(orders, selected_fields):
if field not in id_name_maps:
id_name_maps[field] = {}
id_name_maps[field].update({str(row['id_val']): row['name_val'] for row in results})
print(f"[ID转换] {field}: 查询到 {len(results)} 个名称映射")
log_info(f"[ID转换] {field}: 查询到 {len(results)} 个名称映射", 'report')
except Exception as e:
print(f"[ID转换] {field} 查询失败: {e}")
log_error(f"[ID转换] {field} 查询失败: {e}", 'report')
if field not in id_name_maps:
id_name_maps[field] = {}
@@ -231,9 +232,9 @@ def calculate_time_period_electricity(orders, time_periods_config):
try:
details = execute_query(sql, tuple(order_nos))
print(f"[时段电量] 查询到 {len(details)} 条分时记录")
log_info(f"[时段电量] 查询到 {len(details)} 条分时记录", 'report')
except Exception as e:
print(f"[时段电量] 查询失败: {e}")
log_error(f"[时段电量] 查询失败: {e}", 'report')
return {}
# 按订单号分组计算各时段电量
@@ -320,12 +321,12 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
start_time_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
end_time_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
print(f"[日报生成] 开始生成日报")
print(f" 配置ID: {config_id}")
print(f" 配置名称: {config['config_name']}")
print(f" 拆分方式: {config['split_type']} = {config['split_value']}")
print(f" 时间范围: {start_time_str} 至 {end_time_str}")
print(f" 选择字段: {len(selected_fields)} 个")
log_info(f"[日报生成] 开始生成日报", 'report')
log_info(f" 配置ID: {config_id}", 'report')
log_info(f" 配置名称: {config['config_name']}", 'report')
log_info(f" 拆分方式: {config['split_type']} = {config['split_value']}", 'report')
log_info(f" 时间范围: {start_time_str} 至 {end_time_str}", 'report')
log_info(f" 选择字段: {len(selected_fields)} 个", 'report')
# 构建查询SQL - 过滤掉虚拟字段(时段电量等)
# 虚拟字段不是数据库表中的实际字段,需要动态计算
@@ -370,7 +371,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
# 执行查询
orders = execute_query(sql, tuple(params))
print(f"[日报生成] 驿来特查询到 {len(orders)} 条订单")
log_info(f"[日报生成] 驿来特查询到 {len(orders)} 条订单", 'report')
# 添加数据来源标记
for order in orders:
@@ -379,7 +380,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
# 检查是否有补单记录(finish_type = 2 表示补单结束)
has_supplement = any(order.get('finish_type') == 2 for order in orders)
if has_supplement:
print(f"[日报生成] 检测到补单记录,将在文件名中添加标记")
log_info(f"[日报生成] 检测到补单记录,将在文件名中添加标记", 'report')
# 检查是否需要合并特来电数据
merge_telecom = config.get('merge_telecom', 0)
@@ -389,12 +390,12 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
# 解析车量自编号(支持逗号分隔的多个)
vehicle_nos = [v.strip() for v in telecom_vehicle_no.split(',') if v.strip()]
if vehicle_nos:
print(f"[日报生成] 开始合并特来电数据,车量自编号: {vehicle_nos}")
log_info(f"[日报生成] 开始合并特来电数据,车量自编号: {vehicle_nos}", 'report')
telecom_orders = query_telecom_orders(vehicle_nos, start_time_str, end_time_str)
if telecom_orders:
# 将特来电数据添加到订单列表
orders.extend(telecom_orders)
print(f"[日报生成] 合并后总订单数: {len(orders)}")
log_info(f"[日报生成] 合并后总订单数: {len(orders)}", 'report')
# 检查特来电数据中是否有补单记录
if not has_supplement:
has_supplement = any(order.get('finish_type') == 2 for order in telecom_orders)
@@ -424,7 +425,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
# 解析时段配置并计算分时段电量
time_periods_config = json.loads(config['time_periods']) if config.get('time_periods') else {}
if time_periods_config and any(time_periods_config.values()):
print(f"[日报生成] 时段配置: {time_periods_config}")
log_info(f"[日报生成] 时段配置: {time_periods_config}", 'report')
time_period_electricity = calculate_time_period_electricity(orders, time_periods_config)
# 将分时段电量添加到订单数据中(仅对驿来特订单,特来电订单已有数据)
@@ -441,7 +442,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
order['flat_electricity'] = ep['flat_electricity']
order['valley_electricity'] = ep['valley_electricity']
print(f"[日报生成] 已计算 {len(time_period_electricity)} 条订单的分时段电量")
log_info(f"[日报生成] 已计算 {len(time_period_electricity)} 条订单的分时段电量", 'report')
# 计算求和(保留三位小数)
sum_results = {}
@@ -454,7 +455,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
if 'charge_degree' not in sum_results and orders and 'charge_degree' in orders[0]:
total_charge_degree = sum(float(order.get('charge_degree', 0) or 0) for order in orders)
sum_results['charge_degree'] = round(total_charge_degree, 3)
print(f"[日报生成] 强制采集充电电量: {sum_results['charge_degree']} kWh")
log_info(f"[日报生成] 强制采集充电电量: {sum_results['charge_degree']} kWh", 'report')
# 构建ID到名称的映射
id_name_maps = build_id_to_name_map(orders, selected_fields)
@@ -476,17 +477,19 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
config_id=config['id'],
report_date=start_time.strftime('%Y-%m-%d'),
sort_order=config.get('sort_order'),
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')
)
print(f"[日报生成] Excel文件已生成: {file_path}")
log_info(f"[日报生成] Excel文件已生成: {file_path}", 'report')
# 计算总金额
total_amount = sum(float(order.get('total_money', 0) or 0) for order in orders)
# 保存历史记录
print(f"[日报生成] 准备保存历史记录,file_path={file_path}")
log_info(f"[日报生成] 准备保存历史记录,file_path={file_path}", 'report')
history_id = save_report_history(
report_date=start_time.strftime('%Y-%m-%d'),
start_time=start_time_str,
@@ -502,7 +505,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
file_path=file_path,
status=1
)
print(f"[日报生成] 历史记录已保存,ID={history_id}")
log_info(f"[日报生成] 历史记录已保存,ID={history_id}", 'report')
# 自动采集求和数据到求和数据表
try:
@@ -510,17 +513,17 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
from lib.sum_data_collector import collect_sum_data_from_history
collect_result = collect_sum_data_from_history(history_id)
if collect_result['success']:
print(f"[日报生成] 求和数据采集成功")
log_info(f"[日报生成] 求和数据采集成功", 'report')
else:
print(f"[日报生成] 求和数据采集失败: {collect_result['message']}")
log_warning(f"[日报生成] 求和数据采集失败: {collect_result['message']}", 'report')
except Exception as collect_error:
print(f"[日报生成] 求和数据采集异常: {str(collect_error)}")
log_error(f"[日报生成] 求和数据采集异常: {str(collect_error)}", 'report')
print(f"[日报生成] ✓ 生成成功")
print(f" 订单数量: {len(orders)}")
print(f" 总金额: {total_amount}")
print(f" 文件路径: {file_path}")
print(f" 历史记录ID: {history_id}")
log_info(f"[日报生成] ✓ 生成成功", 'report')
log_info(f" 订单数量: {len(orders)}", 'report')
log_info(f" 总金额: {total_amount}", 'report')
log_info(f" 文件路径: {file_path}", 'report')
log_info(f" 历史记录ID: {history_id}", 'report')
return {
'success': True,
@@ -533,9 +536,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
}
except Exception as e:
print(f"[日报生成] ✗ 生成失败: {str(e)}")
import traceback
traceback.print_exc()
log_error(f"[日报生成] ✗ 生成失败: {str(e)}", 'report')
# 保存失败记录
try:
@@ -554,9 +555,9 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
file_path='',
status=0
)
print(f"[日报生成] 已保存失败记录到历史表")
log_info(f"[日报生成] 已保存失败记录到历史表", 'report')
except Exception as save_error:
print(f"[日报生成] 保存失败记录时出错: {str(save_error)}")
log_error(f"[日报生成] 保存失败记录时出错: {str(save_error)}", 'report')
return {
'success': False,
@@ -568,6 +569,7 @@ 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, sort_order=None,
split_type=None, split_value=None,
custom_service_fee_price=None, custom_service_fee_name=None):
"""
生成Excel文件
@@ -615,7 +617,14 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
# 表头从第1行开始
header_row = 1
field_custom_names = field_custom_names or {}
if field_custom_names:
if isinstance(field_custom_names, str):
try:
field_custom_names = json.loads(field_custom_names)
except (json.JSONDecodeError, TypeError):
field_custom_names = {}
else:
field_custom_names = {}
# 导入边框样式
from openpyxl.styles import Border, Side
@@ -820,7 +829,12 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
try:
from lib.sum_data_collector import get_monthly_charge_degree_total
month = report_date[:7]
monthly_result = get_monthly_charge_degree_total(config_id, month)
monthly_result = get_monthly_charge_degree_total(
config_id=config_id,
month=month,
split_type=split_type,
split_value=split_value
)
if monthly_result['success'] and monthly_result['data']:
total_degree = monthly_result['data']['total_degree']
@@ -868,7 +882,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
ws.row_dimensions[monthly_row].height = 25
except Exception as e:
print(f"[日报生成] 写入月度总计时出错: {e}")
log_error(f"[日报生成] 写入月度总计时出错: {e}", 'report')
# 调整列宽(中文按2个字符宽度计算)
# 序号列固定宽度
@@ -929,10 +943,10 @@ def save_report_history(report_date, start_time, end_time, split_type, split_val
# 生成ID
history_id = int(datetime.now().timestamp() * 1000)
print(f"[保存历史] 准备保存历史记录")
print(f" ID: {history_id}")
print(f" file_path: '{file_path}'")
print(f" status: {status}")
log_info(f"[保存历史] 准备保存历史记录", 'report')
log_info(f" ID: {history_id}", 'report')
log_info(f" file_path: '{file_path}'", 'report')
log_info(f" status: {status}", 'report')
sql = """
INSERT INTO t_daily_report_history
@@ -958,8 +972,8 @@ def save_report_history(report_date, start_time, end_time, split_type, split_val
status
)
print(f"[保存历史] 执行SQL插入...")
log_info(f"[保存历史] 执行SQL插入...", 'report')
result = execute_insert(sql, params)
print(f"[保存历史] 插入完成,返回ID: {result}")
log_info(f"[保存历史] 插入完成,返回ID: {result}", 'report')
return history_id

View File

@@ -6,6 +6,7 @@ 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
from lib.logger import log_info, log_error, log_warning
def collect_sum_data_from_history(history_id=None):
@@ -85,9 +86,9 @@ def collect_sum_data_from_history(history_id=None):
charge_degree = _calculate_charge_degree(history)
if charge_degree is not None:
sum_results['charge_degree'] = charge_degree
print(f"[采集] 历史记录 {history['id']} 补充计算充电电量: {charge_degree} kWh")
log_info(f"[采集] 历史记录 {history['id']} 补充计算充电电量: {charge_degree} kWh", 'sum_data')
except Exception as calc_error:
print(f"[采集] 历史记录 {history['id']} 计算充电电量失败: {calc_error}")
log_error(f"[采集] 历史记录 {history['id']} 计算充电电量失败: {calc_error}", 'sum_data')
# 如果还是没有任何求和数据,跳过
if not sum_results:
@@ -167,15 +168,15 @@ def collect_sum_data_from_history(history_id=None):
execute_insert(sql, params)
collected_count += 1
except Exception as fee_error:
print(f"[采集] 计算自定义服务费失败: {fee_error}")
log_error(f"[采集] 计算自定义服务费失败: {fee_error}", 'sum_data')
log_info(f'[采集] 采集完成,共采集 {collected_count} 条求和数据,跳过 {skipped_count} 条无求和数据的记录', 'sum_data')
return {
'success': True,
'message': f'采集完成,共采集 {collected_count} 条求和数据,跳过 {skipped_count} 条无求和数据的记录'
}
except Exception as e:
import traceback
traceback.print_exc()
log_error(f'[采集] 采集失败: {e}', 'sum_data')
return {'success': False, 'message': f'采集失败: {str(e)}'}
@@ -229,7 +230,7 @@ def _calculate_charge_degree(history):
return 0.0
except Exception as e:
print(f"[计算充电电量] 失败: {e}")
log_error(f"[计算充电电量] 失败: {e}", 'sum_data')
return None
@@ -669,14 +670,17 @@ def export_sum_data_to_excel(start_date=None, end_date=None, config_id=None, dat
return {'success': False, 'message': str(e)}
def get_monthly_charge_degree_total(config_id=None, month=None):
def get_monthly_charge_degree_total(config_id=None, month=None, split_type=None, split_value=None):
"""
获取当月充电电量累计(从求和数据表查询,对账后的准确数据)
- 如果当天已经生成了日报并采集,自动包含在内,不会重复计算
- 以配置当前拆分为准,只统计匹配的拆分值数据
Args:
config_id: 配置ID(可选,不传则统计所有配置)
month: 月份,格式 YYYY-MM(可选,默认当月)
split_type: 拆分类型(可选,company_id/user_id/station_id)
split_value: 拆分值,逗号分隔的字符串(可选,用于过滤)
Returns:
dict: {
@@ -716,6 +720,15 @@ def get_monthly_charge_degree_total(config_id=None, month=None):
sql += ' AND config_id = %s'
params.append(config_id)
# 按拆分值过滤(以配置当前拆分为准)
if split_type and split_value:
values = [v.strip() for v in split_value.split(',') if v.strip()]
if values:
placeholders = ','.join(['%s'] * len(values))
sql += f' AND split_type = %s AND split_value IN ({placeholders})'
params.append(split_type)
params.extend(values)
sql += ' GROUP BY report_date ORDER BY report_date ASC'
rows = execute_query(sql, tuple(params))