增加日志

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

33
app.py
View File

@@ -13,6 +13,7 @@ from lib.db import execute_query
from lib.report_generator import generate_daily_report from lib.report_generator import generate_daily_report
from lib.api import register_blueprints from lib.api import register_blueprints
from lib.db_init import init_database_tables from lib.db_init import init_database_tables
from lib.logger import log_info, log_error, log_warning
# 创建 Flask 应用 # 创建 Flask 应用
app = Flask(__name__, app = Flask(__name__,
@@ -27,11 +28,11 @@ os.makedirs(os.path.join('public', 'reports'), exist_ok=True)
register_blueprints(app) register_blueprints(app)
# 应用启动时自动初始化数据库表 # 应用启动时自动初始化数据库表
print('正在初始化数据库表...') log_info('正在初始化数据库表...', 'app')
if init_database_tables(): if init_database_tables():
print('数据库表初始化成功') log_info('数据库表初始化成功', 'app')
else: else:
print('数据库表初始化失败,请检查数据库连接') log_error('数据库表初始化失败,请检查数据库连接', 'app')
# ==================== 页面路由 ==================== # ==================== 页面路由 ====================
@@ -46,7 +47,7 @@ def index():
def scheduled_job(): def scheduled_job():
"""定时任务:每天 8:01 自动生成日报""" """定时任务:每天 8:01 自动生成日报"""
print(f'[{datetime.now()}] 开始执行定时任务...') log_info(f'开始执行定时任务...', 'scheduler')
try: try:
# 获取所有启用的配置 # 获取所有启用的配置
@@ -55,31 +56,35 @@ def scheduled_job():
) )
if not configs: if not configs:
print('没有启用的配置,跳过生成') log_warning('没有启用的配置,跳过生成', 'scheduler')
return return
print(f'找到 {len(configs)} 个启用的配置') log_info(f'找到 {len(configs)} 个启用的配置', 'scheduler')
# 为每个配置生成日报 # 为每个配置生成日报
success_count = 0
fail_count = 0
for config in configs: for config in configs:
print(f'正在生成:{config["config_name"]}') log_info(f'正在生成:{config["config_name"]} (ID: {config["id"]})', 'scheduler')
result = generate_daily_report(config['id']) result = generate_daily_report(config['id'])
if result['success']: if result['success']:
print(f' ✓ 成功:{result["total_orders"]} 条订单,金额:{result["total_amount"]}') log_info(f' ✓ 成功:{result["total_orders"]} 条订单,金额:{result["total_amount"]}', 'scheduler')
success_count += 1
else: else:
print(f' ✗ 失败:{result["message"]}') log_error(f' ✗ 失败:{result["message"]}', 'scheduler')
fail_count += 1
print('定时任务执行完成') log_info(f'定时任务执行完成,成功 {success_count} 个,失败 {fail_count} 个', 'scheduler')
except Exception as e: except Exception as e:
print(f'定时任务执行失败:{e}') log_error(f'定时任务执行失败:{e}', 'scheduler')
# 启动定时任务 # 启动定时任务
scheduler = BackgroundScheduler(timezone='Asia/Shanghai') scheduler = BackgroundScheduler(timezone='Asia/Shanghai')
scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1) scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1)
scheduler.start() scheduler.start()
print('定时任务已启动:每天 08:01 执行') log_info('定时任务已启动:每天 08:01 执行', 'app')
# ==================== 启动应用 ==================== # ==================== 启动应用 ====================
@@ -87,6 +92,6 @@ print('定时任务已启动:每天 08:01 执行')
if __name__ == '__main__': if __name__ == '__main__':
# 支持沙箱环境和本地环境 # 支持沙箱环境和本地环境
port = int(os.environ.get('DEPLOY_RUN_PORT', os.environ.get('PORT', 5000))) port = int(os.environ.get('DEPLOY_RUN_PORT', os.environ.get('PORT', 5000)))
print(f'启动服务器:http://localhost:{port}') log_info(f'启动服务器:http://localhost:{port}', 'app')
print(f'数据库:{os.environ.get("DB_HOST", "haoslm2.xicp.net")}:{os.environ.get("DB_PORT", "10216")}') log_info(f'数据库:{os.environ.get("DB_HOST", "haoslm2.xicp.net")}:{os.environ.get("DB_PORT", "10216")}', 'app')
app.run(host='0.0.0.0', port=port, debug=True) app.run(host='0.0.0.0', port=port, debug=True)

View File

@@ -5,6 +5,7 @@ import json
from datetime import datetime from datetime import datetime
from flask import request, jsonify from flask import request, jsonify
from lib.db import execute_query, execute_update, execute_insert from lib.db import execute_query, execute_update, execute_insert
from lib.logger import log_info, log_error, log_warning
from . import config_bp from . import config_bp
@@ -12,6 +13,7 @@ from . import config_bp
def get_configs(): def get_configs():
"""获取所有配置""" """获取所有配置"""
try: try:
log_info('[配置API] 获取配置列表', 'api')
configs = execute_query( configs = execute_query(
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id DESC' '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['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 {} 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}) return jsonify({'success': True, 'data': configs})
except Exception as e: except Exception as e:
log_error(f'[配置API] 获取配置列表失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -31,6 +35,7 @@ def get_configs():
def create_config(): def create_config():
"""创建配置""" """创建配置"""
try: try:
log_info('[配置API] 创建配置', 'api')
data = request.json data = request.json
config_name = data.get('config_name') config_name = data.get('config_name')
split_type = data.get('split_type') split_type = data.get('split_type')
@@ -52,9 +57,11 @@ def create_config():
custom_service_fee_name = data.get('custom_service_fee_name') custom_service_fee_name = data.get('custom_service_fee_name')
if not config_name or not split_type or not split_value: if not config_name or not split_type or not split_value:
log_warning('[配置API] 创建配置失败:缺少必填字段', 'api')
return jsonify({'success': False, 'message': '缺少必填字段'}), 400 return jsonify({'success': False, 'message': '缺少必填字段'}), 400
if not selected_fields: if not selected_fields:
log_warning('[配置API] 创建配置失败:未选择字段', 'api')
return jsonify({'success': False, 'message': '请至少选择一个字段'}), 400 return jsonify({'success': False, 'message': '请至少选择一个字段'}), 400
# 生成 ID # 生成 ID
@@ -92,12 +99,14 @@ def create_config():
execute_insert(sql, params) execute_insert(sql, params)
log_info(f'[配置API] 配置创建成功,ID: {config_id},名称: {config_name}', 'api')
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '配置创建成功', 'message': '配置创建成功',
'data': {'id': config_id} 'data': {'id': config_id}
}) })
except Exception as e: except Exception as e:
log_error(f'[配置API] 创建配置失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -105,6 +114,7 @@ def create_config():
def update_config(config_id): def update_config(config_id):
"""更新配置""" """更新配置"""
try: try:
log_info(f'[配置API] 更新配置,ID: {config_id}', 'api')
data = request.json data = request.json
config_name = data.get('config_name') config_name = data.get('config_name')
split_type = data.get('split_type') 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') 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 = """ sql = """
UPDATE t_daily_report_config UPDATE t_daily_report_config
SET config_name = %s, split_type = %s, split_value = %s, split_name = %s, 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) 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': '配置更新成功'}) return jsonify({'success': True, 'message': '配置更新成功'})
except Exception as e: except Exception as e:
log_error(f'[配置API] 更新配置失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -163,9 +203,18 @@ def update_config(config_id):
def delete_config(config_id): def delete_config(config_id):
"""删除配置""" """删除配置"""
try: try:
log_info(f'[配置API] 删除配置,ID: {config_id}', 'api')
execute_update('DELETE FROM t_daily_report_config WHERE id = %s', (config_id,)) 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': '配置删除成功'}) return jsonify({'success': True, 'message': '配置删除成功'})
except Exception as e: except Exception as e:
log_error(f'[配置API] 删除配置失败: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -236,15 +285,13 @@ def move_config(config_id):
"""移动配置排序""" """移动配置排序"""
try: try:
direction = request.json.get('direction') # up 或 down 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 排序 # 查出所有配置,按 sort_order, id 排序
all_configs = execute_query( all_configs = execute_query(
'SELECT id, sort_order FROM t_daily_report_config ORDER BY sort_order ASC, id ASC' 'SELECT id, sort_order FROM t_daily_report_config ORDER BY sort_order ASC, id ASC'
) )
print(f"[move_config] 所有配置数量: {len(all_configs)}") log_info(f"[配置API] 所有配置数量: {len(all_configs)}", 'api')
for idx, c in enumerate(all_configs):
print(f" [{idx}] id={c['id']}, sort_order={c['sort_order']}")
# 找到当前配置的索引 # 找到当前配置的索引
current_index = None current_index = None
@@ -254,10 +301,10 @@ def move_config(config_id):
break break
if current_index is None: 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 return jsonify({'success': False, 'message': '配置不存在'}), 404
print(f"[move_config] 当前配置索引: {current_index}") log_info(f"[配置API] 当前配置索引: {current_index}", 'api')
# 计算目标索引 # 计算目标索引
if direction == 'up': if direction == 'up':
@@ -269,29 +316,24 @@ def move_config(config_id):
# 边界检查 # 边界检查
if target_index < 0 or target_index >= len(all_configs): 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': '已到边界'}) return jsonify({'success': True, 'message': '已到边界'})
# 在内存数组中交换位置 # 在内存数组中交换位置
all_configs[current_index], all_configs[target_index] = all_configs[target_index], all_configs[current_index] 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 # 全量重排:按新顺序给每个配置重新赋值 sort_order
for idx, cfg in enumerate(all_configs): for idx, cfg in enumerate(all_configs):
new_sort = idx + 1 new_sort = idx + 1
print(f" 更新 id={cfg['id']}: sort_order {cfg['sort_order']} -> {new_sort}")
execute_update( execute_update(
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s', 'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
(new_sort, cfg['id']) (new_sort, cfg['id'])
) )
print("[move_config] 重排完成") log_info(f"[配置API] 配置移动成功,ID: {config_id}", 'api')
return jsonify({'success': True, 'message': '移动成功'}) return jsonify({'success': True, 'message': '移动成功'})
except Exception as e: except Exception as e:
import traceback log_error(f"[配置API] 移动配置失败: {e}", 'api')
traceback.print_exc()
print(f"[move_config] 异常: {e}")
return jsonify({'success': False, 'message': str(e)}), 500 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 flask import request, jsonify, send_from_directory
from lib.db import execute_query, execute_update from lib.db import execute_query, execute_update
from lib.report_generator import generate_daily_report from lib.report_generator import generate_daily_report
from lib.logger import log_info, log_error, log_warning
from . import report_bp from . import report_bp
@@ -16,8 +17,10 @@ def generate_report():
try: try:
data = request.json data = request.json
config_id = data.get('config_id') config_id = data.get('config_id')
log_info(f'[报表API] 生成日报,配置ID: {config_id}', 'api')
if not config_id: if not config_id:
log_warning('[报表API] 生成日报失败:未选择配置', 'api')
return jsonify({'success': False, 'message': '请选择配置'}), 400 return jsonify({'success': False, 'message': '请选择配置'}), 400
# 解析时间参数 # 解析时间参数
@@ -32,10 +35,13 @@ def generate_report():
result = generate_daily_report(config_id, start_time, end_time) result = generate_daily_report(config_id, start_time, end_time)
if result['success']: if result['success']:
log_info(f'[报表API] 日报生成成功,订单数: {result.get("total_orders", 0)}', 'api')
return jsonify(result) return jsonify(result)
else: else:
log_warning(f'[报表API] 日报生成失败: {result.get("message", "")}', 'api')
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
log_error(f'[报表API] 生成日报异常: {e}', 'api')
return jsonify({'success': False, 'message': str(e)}), 500 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)) page_size = int(request.args.get('page_size', 20))
start_date = request.args.get('start_date') start_date = request.args.get('start_date')
end_date = request.args.get('end_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 offset = (page - 1) * page_size
# 构建查询条件 # 构建查询条件
@@ -83,9 +91,7 @@ def get_report_history():
if item['file_path'] is None: if item['file_path'] is None:
item['file_path'] = '' item['file_path'] = ''
# 调试日志 log_info(f'[报表API] 获取到 {len(history)} 条历史记录,共 {total} 条', 'api')
print(f"[历史记录] ID: {item['id']}, status: {item['status']}, file_path: '{item['file_path']}'")
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': { 'data': {
@@ -96,9 +102,7 @@ def get_report_history():
} }
}) })
except Exception as e: except Exception as e:
print(f"[历史记录 API] 错误:{e}") log_error(f'[报表API] 获取历史记录失败: {e}', 'api')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -106,6 +110,7 @@ def get_report_history():
def delete_report_history(history_id): def delete_report_history(history_id):
"""删除单个历史记录""" """删除单个历史记录"""
try: try:
log_info(f'[报表API] 删除历史记录,ID: {history_id}', 'api')
# 先查询文件路径 # 先查询文件路径
result = execute_query( result = execute_query(
'SELECT file_path FROM t_daily_report_history WHERE id = %s', '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('/'))): 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('/'))) 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,)) 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': '删除成功'}) return jsonify({'success': True, 'message': '删除成功'})
except Exception as e: except Exception as e:
print(f"[删除历史记录] 错误:{e}") log_error(f'[报表API] 删除历史记录失败: {e}', 'api')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -135,8 +140,10 @@ def batch_delete_report_history():
try: try:
data = request.get_json() data = request.get_json()
ids = data.get('ids', []) ids = data.get('ids', [])
log_info(f'[报表API] 批量删除历史记录,数量: {len(ids)}', 'api')
if not ids: if not ids:
log_warning('[报表API] 批量删除失败:未选择记录', 'api')
return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400 return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400
# 查询文件路径 # 查询文件路径
@@ -146,10 +153,14 @@ def batch_delete_report_history():
) )
# 删除文件 # 删除文件
deleted_files = 0
for result in results: for result in results:
file_path = result.get('file_path', '') 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('/'))): 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('/'))) 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( execute_update(
@@ -157,11 +168,10 @@ def batch_delete_report_history():
tuple(ids) tuple(ids)
) )
log_info(f'[报表API] 批量删除成功,共 {len(ids)} 条记录', 'api')
return jsonify({'success': True, 'message': f'成功删除 {len(ids)} 条记录'}) return jsonify({'success': True, 'message': f'成功删除 {len(ids)} 条记录'})
except Exception as e: except Exception as e:
print(f"[批量删除历史记录] 错误:{e}") log_error(f'[报表API] 批量删除历史记录失败: {e}', 'api')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -169,6 +179,7 @@ def batch_delete_report_history():
def download_report_by_id(history_id): def download_report_by_id(history_id):
"""根据 ID 下载历史记录""" """根据 ID 下载历史记录"""
try: try:
log_info(f'[报表API] 下载报表,历史记录ID: {history_id}', 'api')
# 查询历史记录 # 查询历史记录
result = execute_query( result = execute_query(
'SELECT file_path, split_name, report_date FROM t_daily_report_history WHERE id = %s', '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: if not result:
log_warning(f'[报表API] 下载报表失败:记录不存在,ID: {history_id}', 'api')
return jsonify({'success': False, 'message': '记录不存在'}), 404 return jsonify({'success': False, 'message': '记录不存在'}), 404
file_path = result[0]['file_path'] file_path = result[0]['file_path']
if not file_path: if not file_path:
log_warning(f'[报表API] 下载报表失败:文件路径为空,ID: {history_id}', 'api')
return jsonify({'success': False, 'message': '文件不存在'}), 404 return jsonify({'success': False, 'message': '文件不存在'}), 404
# 构建完整路径 # 构建完整路径
full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/')) full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'public', file_path.lstrip('/'))
if not os.path.exists(full_path): if not os.path.exists(full_path):
log_warning(f'[报表API] 下载报表失败:文件不存在,路径: {full_path}', 'api')
return jsonify({'success': False, 'message': '文件不存在'}), 404 return jsonify({'success': False, 'message': '文件不存在'}), 404
# 返回文件 # 返回文件
directory = os.path.dirname(full_path) directory = os.path.dirname(full_path)
filename = os.path.basename(full_path) filename = os.path.basename(full_path)
log_info(f'[报表API] 报表下载成功: {filename}', 'api')
return send_from_directory(directory, filename, as_attachment=True) return send_from_directory(directory, filename, as_attachment=True)
except Exception as e: except Exception as e:
print(f"[下载历史记录] 错误:{e}") log_error(f'[报表API] 下载报表失败: {e}', 'api')
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 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, get_monthly_charge_degree_total,
export_sum_data_to_excel export_sum_data_to_excel
) )
from lib.logger import log_info, log_error, log_warning
from . import sum_data_bp from . import sum_data_bp
@@ -21,15 +22,17 @@ def collect_sum_data():
try: try:
data = request.json or {} data = request.json or {}
history_id = data.get('history_id') history_id = data.get('history_id')
log_info(f'[求和数据API] 采集求和数据,历史记录ID: {history_id or "全部"}', 'api')
result = collect_sum_data_from_history(history_id) result = collect_sum_data_from_history(history_id)
if result['success']: if result['success']:
log_info(f'[求和数据API] 采集成功: {result["message"]}', 'api')
return jsonify(result) return jsonify(result)
else: else:
log_warning(f'[求和数据API] 采集失败: {result["message"]}', 'api')
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 采集异常: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -45,6 +48,7 @@ def list_sum_data():
config_name = request.args.get('config_name') config_name = request.args.get('config_name')
sum_field_key = request.args.get('sum_field_key') sum_field_key = request.args.get('sum_field_key')
data_source = request.args.get('data_source') data_source = request.args.get('data_source')
log_info(f'[求和数据API] 查询求和数据列表,第{page}页', 'api')
result = get_sum_data_list( result = get_sum_data_list(
page=page, page=page,
@@ -58,12 +62,12 @@ def list_sum_data():
) )
if result['success']: if result['success']:
log_info(f'[求和数据API] 查询成功,共{result["data"]["total"]}条记录', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 查询求和数据列表失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -72,14 +76,15 @@ def create_sum_data():
"""手动添加求和数据""" """手动添加求和数据"""
try: try:
data = request.json data = request.json
log_info('[求和数据API] 手动添加求和数据', 'api')
result = add_sum_data(data) result = add_sum_data(data)
if result['success']: if result['success']:
log_info(f'[求和数据API] 添加成功,ID: {result.get("id", "")}', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 添加求和数据失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -88,14 +93,15 @@ def update_sum_data_api(sum_data_id):
"""更新求和数据""" """更新求和数据"""
try: try:
data = request.json data = request.json
log_info(f'[求和数据API] 更新求和数据,ID: {sum_data_id}', 'api')
result = update_sum_data(sum_data_id, data) result = update_sum_data(sum_data_id, data)
if result['success']: if result['success']:
log_info(f'[求和数据API] 更新成功,ID: {sum_data_id}', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 更新求和数据失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 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): def delete_sum_data_api(sum_data_id):
"""删除求和数据""" """删除求和数据"""
try: try:
log_info(f'[求和数据API] 删除求和数据,ID: {sum_data_id}', 'api')
result = delete_sum_data(sum_data_id) result = delete_sum_data(sum_data_id)
if result['success']: if result['success']:
log_info(f'[求和数据API] 删除成功,ID: {sum_data_id}', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 删除求和数据失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -120,17 +127,19 @@ def batch_delete_sum_data_api():
try: try:
data = request.json data = request.json
ids = data.get('ids', []) ids = data.get('ids', [])
log_info(f'[求和数据API] 批量删除求和数据,数量: {len(ids)}', 'api')
if not ids: if not ids:
log_warning('[求和数据API] 批量删除失败:未选择记录', 'api')
return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400 return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400
result = batch_delete_sum_data(ids) result = batch_delete_sum_data(ids)
if result['success']: if result['success']:
log_info(f'[求和数据API] 批量删除成功,{result["message"]}', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 批量删除求和数据失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -145,15 +154,16 @@ def monthly_charge_degree_total():
try: try:
config_id = request.args.get('config_id') config_id = request.args.get('config_id')
month = request.args.get('month') 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) result = get_monthly_charge_degree_total(config_id, month)
if result['success']: if result['success']:
log_info(f'[求和数据API] 查询成功,累计电量: {result["data"]["total_degree"]} kWh', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 查询月度累计失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500
@@ -174,6 +184,7 @@ def export_sum_data():
config_id = request.args.get('config_id') config_id = request.args.get('config_id')
data_source = request.args.get('data_source') data_source = request.args.get('data_source')
keyword = request.args.get('keyword') keyword = request.args.get('keyword')
log_info(f'[求和数据API] 导出求和数据,日期范围: {start_date or "开始"} ~ {end_date or "结束"}', 'api')
result = export_sum_data_to_excel( result = export_sum_data_to_excel(
start_date=start_date, start_date=start_date,
@@ -184,10 +195,10 @@ def export_sum_data():
) )
if result['success']: if result['success']:
log_info(f'[求和数据API] 导出成功,共{result.get("total", 0)}条记录', 'api')
return jsonify(result) return jsonify(result)
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
import traceback log_error(f'[求和数据API] 导出求和数据失败: {e}', 'api')
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500 return jsonify({'success': False, 'message': str(e)}), 500

View File

@@ -3,6 +3,7 @@
""" """
import pymysql import pymysql
from contextlib import contextmanager from contextlib import contextmanager
from lib.logger import log_info, log_error, log_warning
# 数据库配置 # 数据库配置
DB_CONFIG = { DB_CONFIG = {
@@ -23,32 +24,52 @@ DB_CONFIG = {
@contextmanager @contextmanager
def get_connection(): def get_connection():
"""获取数据库连接(上下文管理器)""" """获取数据库连接(上下文管理器)"""
conn = pymysql.connect(**DB_CONFIG)
try: try:
conn = pymysql.connect(**DB_CONFIG)
yield conn yield conn
except pymysql.Error as e:
log_error(f'数据库连接失败: {e}', 'db')
raise
finally: finally:
conn.close() try:
conn.close()
except:
pass
def execute_query(sql, params=None): def execute_query(sql, params=None):
"""执行查询并返回结果""" """执行查询并返回结果"""
with get_connection() as conn: try:
with conn.cursor() as cursor: with get_connection() as conn:
cursor.execute(sql, params) with conn.cursor() as cursor:
return cursor.fetchall() 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): def execute_update(sql, params=None):
"""执行更新操作""" """执行更新操作"""
with get_connection() as conn: try:
with conn.cursor() as cursor: with get_connection() as conn:
cursor.execute(sql, params) with conn.cursor() as cursor:
return cursor.rowcount 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): def execute_insert(sql, params=None):
"""执行插入操作并返回插入ID""" """执行插入操作并返回插入ID"""
with get_connection() as conn: try:
with conn.cursor() as cursor: with get_connection() as conn:
cursor.execute(sql, params) with conn.cursor() as cursor:
return cursor.lastrowid 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.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(): def init_database_tables():
@@ -34,7 +64,7 @@ def init_database_tables():
PROPERTIES("replication_num" = "1") PROPERTIES("replication_num" = "1")
""" """
execute_update(create_config_table) execute_update(create_config_table)
print("[数据库] t_daily_report_config 表已就绪") log_info("[数据库] t_daily_report_config 表已就绪", 'db_init')
# 创建历史表 # 创建历史表
create_history_table = """ create_history_table = """
@@ -60,16 +90,14 @@ def init_database_tables():
PROPERTIES("replication_num" = "1") PROPERTIES("replication_num" = "1")
""" """
execute_update(create_history_table) execute_update(create_history_table)
print("[数据库] t_daily_report_history 表已就绪") log_info("[数据库] t_daily_report_history 表已就绪", 'db_init')
# 为已存在的表添加 config_name 字段(如果不存在) # 为已存在的表添加 config_name 字段(如果不存在)
try: _add_column_if_not_exists(
execute_update(""" 't_daily_report_history',
ALTER TABLE t_daily_report_history "config_name VARCHAR(200) NULL COMMENT '配置名称'",
ADD COLUMN IF NOT EXISTS config_name VARCHAR(200) NULL COMMENT '配置名称' 'config_name'
""") )
except Exception as e:
print(f"[数据库] 添加 config_name 字段时出错(可能已存在): {e}")
# 创建求和数据表 # 创建求和数据表
create_sum_data_table = """ create_sum_data_table = """
@@ -96,56 +124,35 @@ def init_database_tables():
PROPERTIES("replication_num" = "1") PROPERTIES("replication_num" = "1")
""" """
execute_update(create_sum_data_table) execute_update(create_sum_data_table)
print("[数据库] t_daily_report_sum_data 表已就绪") log_info("[数据库] t_daily_report_sum_data 表已就绪", 'db_init')
# 给配置表增加 show_monthly_total 字段 # 给配置表增加 show_monthly_total 字段
try: _add_column_if_not_exists(
execute_update( 't_daily_report_config',
"ALTER TABLE t_daily_report_config ADD COLUMN show_monthly_total TINYINT NULL COMMENT '是否显示当月充电量总计' " "show_monthly_total TINYINT NULL COMMENT '是否显示当月充电量总计'",
) 'show_monthly_total'
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}")
# 给求和数据表增加 sort_order 字段 # 给求和数据表增加 sort_order 字段
try: _add_column_if_not_exists(
execute_update( 't_daily_report_sum_data',
"ALTER TABLE t_daily_report_sum_data ADD COLUMN sort_order INT NULL COMMENT '配置排序号' " "sort_order INT NULL COMMENT '配置排序号'",
) 'sort_order'
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}")
# 给配置表增加 custom_service_fee_price 字段(自定义服务费单价) # 给配置表增加 custom_service_fee_price 字段(自定义服务费单价)
try: _add_column_if_not_exists(
execute_update( 't_daily_report_config',
"ALTER TABLE t_daily_report_config ADD COLUMN custom_service_fee_price DECIMAL(10,4) NULL COMMENT '自定义服务费单价(元/kWh)' " "custom_service_fee_price DECIMAL(10,4) NULL COMMENT '自定义服务费单价(元/kWh)'",
) 'custom_service_fee_price'
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}")
# 给配置表增加 custom_service_fee_name 字段(自定义服务费表头名称) # 给配置表增加 custom_service_fee_name 字段(自定义服务费表头名称)
try: _add_column_if_not_exists(
execute_update( 't_daily_report_config',
"ALTER TABLE t_daily_report_config ADD COLUMN custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称' " "custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称'",
) 'custom_service_fee_name'
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}")
# 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值 # 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值
try: try:
@@ -153,7 +160,7 @@ def init_database_tables():
"SELECT COUNT(*) as cnt FROM t_daily_report_config WHERE sort_order IS NULL" "SELECT COUNT(*) as cnt FROM t_daily_report_config WHERE sort_order IS NULL"
) )
if null_count and null_count[0]['cnt'] > 0: 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( all_configs = execute_query(
"SELECT id FROM t_daily_report_config ORDER BY id ASC" "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", "UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s",
(idx + 1, cfg['id']) (idx + 1, cfg['id'])
) )
print("[数据库] 配置 sort_order 初始化完成") log_info("[数据库] 配置 sort_order 初始化完成", 'db_init')
except Exception as e: except Exception as e:
print(f"[数据库] 初始化 sort_order 时出错: {e}") log_warning(f"[数据库] 初始化 sort_order 时出错: {e}", 'db_init')
log_info("[数据库] 初始化完成", 'db_init')
return True return True
except Exception as e: except Exception as e:
print(f"[数据库] 初始化失败: {e}") log_error(f"[数据库] 初始化失败: {e}", 'db_init')
return False 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.db import execute_query, execute_insert
from lib.field_mapping import get_field_display_name from lib.field_mapping import get_field_display_name
from lib.logger import log_info, log_error, log_warning
# ID字段到名称的映射配置 # ID字段到名称的映射配置
@@ -102,9 +103,9 @@ def query_telecom_orders(vehicle_self_nos, start_time_str, end_time_str):
try: try:
results = execute_query(sql, params) results = execute_query(sql, params)
print(f"[特来电] 查询到 {len(results)} 条订单") log_info(f"[特来电] 查询到 {len(results)} 条订单", 'report')
except Exception as e: except Exception as e:
print(f"[特来电] 查询失败: {e}") log_error(f"[特来电] 查询失败: {e}", 'report')
return [] return []
# 转换字段名 # 转换字段名
@@ -165,7 +166,7 @@ def build_id_to_name_map(orders, selected_fields):
# 合并特来电的名称映射 # 合并特来电的名称映射
if telecom_name_values: if telecom_name_values:
id_name_maps[field] = 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映射 # 查询驿来特数据的ID映射
if not id_values: if not id_values:
@@ -180,9 +181,9 @@ def build_id_to_name_map(orders, selected_fields):
if field not in id_name_maps: if field not in id_name_maps:
id_name_maps[field] = {} id_name_maps[field] = {}
id_name_maps[field].update({str(row['id_val']): row['name_val'] for row in results}) 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: except Exception as e:
print(f"[ID转换] {field} 查询失败: {e}") log_error(f"[ID转换] {field} 查询失败: {e}", 'report')
if field not in id_name_maps: if field not in id_name_maps:
id_name_maps[field] = {} id_name_maps[field] = {}
@@ -231,9 +232,9 @@ def calculate_time_period_electricity(orders, time_periods_config):
try: try:
details = execute_query(sql, tuple(order_nos)) details = execute_query(sql, tuple(order_nos))
print(f"[时段电量] 查询到 {len(details)} 条分时记录") log_info(f"[时段电量] 查询到 {len(details)} 条分时记录", 'report')
except Exception as e: except Exception as e:
print(f"[时段电量] 查询失败: {e}") log_error(f"[时段电量] 查询失败: {e}", 'report')
return {} 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') start_time_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
end_time_str = end_time.strftime('%Y-%m-%d %H:%M:%S') end_time_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
print(f"[日报生成] 开始生成日报") log_info(f"[日报生成] 开始生成日报", 'report')
print(f" 配置ID: {config_id}") log_info(f" 配置ID: {config_id}", 'report')
print(f" 配置名称: {config['config_name']}") log_info(f" 配置名称: {config['config_name']}", 'report')
print(f" 拆分方式: {config['split_type']} = {config['split_value']}") log_info(f" 拆分方式: {config['split_type']} = {config['split_value']}", 'report')
print(f" 时间范围: {start_time_str} 至 {end_time_str}") log_info(f" 时间范围: {start_time_str} 至 {end_time_str}", 'report')
print(f" 选择字段: {len(selected_fields)} 个") log_info(f" 选择字段: {len(selected_fields)} 个", 'report')
# 构建查询SQL - 过滤掉虚拟字段(时段电量等) # 构建查询SQL - 过滤掉虚拟字段(时段电量等)
# 虚拟字段不是数据库表中的实际字段,需要动态计算 # 虚拟字段不是数据库表中的实际字段,需要动态计算
@@ -370,7 +371,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
# 执行查询 # 执行查询
orders = execute_query(sql, tuple(params)) orders = execute_query(sql, tuple(params))
print(f"[日报生成] 驿来特查询到 {len(orders)} 条订单") log_info(f"[日报生成] 驿来特查询到 {len(orders)} 条订单", 'report')
# 添加数据来源标记 # 添加数据来源标记
for order in orders: for order in orders:
@@ -379,7 +380,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
# 检查是否有补单记录(finish_type = 2 表示补单结束) # 检查是否有补单记录(finish_type = 2 表示补单结束)
has_supplement = any(order.get('finish_type') == 2 for order in orders) has_supplement = any(order.get('finish_type') == 2 for order in orders)
if has_supplement: if has_supplement:
print(f"[日报生成] 检测到补单记录,将在文件名中添加标记") log_info(f"[日报生成] 检测到补单记录,将在文件名中添加标记", 'report')
# 检查是否需要合并特来电数据 # 检查是否需要合并特来电数据
merge_telecom = config.get('merge_telecom', 0) 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()] vehicle_nos = [v.strip() for v in telecom_vehicle_no.split(',') if v.strip()]
if vehicle_nos: 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) telecom_orders = query_telecom_orders(vehicle_nos, start_time_str, end_time_str)
if telecom_orders: if telecom_orders:
# 将特来电数据添加到订单列表 # 将特来电数据添加到订单列表
orders.extend(telecom_orders) orders.extend(telecom_orders)
print(f"[日报生成] 合并后总订单数: {len(orders)}") log_info(f"[日报生成] 合并后总订单数: {len(orders)}", 'report')
# 检查特来电数据中是否有补单记录 # 检查特来电数据中是否有补单记录
if not has_supplement: if not has_supplement:
has_supplement = any(order.get('finish_type') == 2 for order in telecom_orders) 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 {} time_periods_config = json.loads(config['time_periods']) if config.get('time_periods') else {}
if time_periods_config and any(time_periods_config.values()): 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) 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['flat_electricity'] = ep['flat_electricity']
order['valley_electricity'] = ep['valley_electricity'] order['valley_electricity'] = ep['valley_electricity']
print(f"[日报生成] 已计算 {len(time_period_electricity)} 条订单的分时段电量") log_info(f"[日报生成] 已计算 {len(time_period_electricity)} 条订单的分时段电量", 'report')
# 计算求和(保留三位小数) # 计算求和(保留三位小数)
sum_results = {} 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]: 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) 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) 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到名称的映射
id_name_maps = build_id_to_name_map(orders, selected_fields) 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'], config_id=config['id'],
report_date=start_time.strftime('%Y-%m-%d'), report_date=start_time.strftime('%Y-%m-%d'),
sort_order=config.get('sort_order'), 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_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')
) )
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) 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( history_id = save_report_history(
report_date=start_time.strftime('%Y-%m-%d'), report_date=start_time.strftime('%Y-%m-%d'),
start_time=start_time_str, 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, file_path=file_path,
status=1 status=1
) )
print(f"[日报生成] 历史记录已保存,ID={history_id}") log_info(f"[日报生成] 历史记录已保存,ID={history_id}", 'report')
# 自动采集求和数据到求和数据表 # 自动采集求和数据到求和数据表
try: 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 from lib.sum_data_collector import collect_sum_data_from_history
collect_result = collect_sum_data_from_history(history_id) collect_result = collect_sum_data_from_history(history_id)
if collect_result['success']: if collect_result['success']:
print(f"[日报生成] 求和数据采集成功") log_info(f"[日报生成] 求和数据采集成功", 'report')
else: else:
print(f"[日报生成] 求和数据采集失败: {collect_result['message']}") log_warning(f"[日报生成] 求和数据采集失败: {collect_result['message']}", 'report')
except Exception as collect_error: except Exception as collect_error:
print(f"[日报生成] 求和数据采集异常: {str(collect_error)}") log_error(f"[日报生成] 求和数据采集异常: {str(collect_error)}", 'report')
print(f"[日报生成] ✓ 生成成功") log_info(f"[日报生成] ✓ 生成成功", 'report')
print(f" 订单数量: {len(orders)}") log_info(f" 订单数量: {len(orders)}", 'report')
print(f" 总金额: {total_amount}") log_info(f" 总金额: {total_amount}", 'report')
print(f" 文件路径: {file_path}") log_info(f" 文件路径: {file_path}", 'report')
print(f" 历史记录ID: {history_id}") log_info(f" 历史记录ID: {history_id}", 'report')
return { return {
'success': True, 'success': True,
@@ -533,9 +536,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
} }
except Exception as e: except Exception as e:
print(f"[日报生成] ✗ 生成失败: {str(e)}") log_error(f"[日报生成] ✗ 生成失败: {str(e)}", 'report')
import traceback
traceback.print_exc()
# 保存失败记录 # 保存失败记录
try: try:
@@ -554,9 +555,9 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
file_path='', file_path='',
status=0 status=0
) )
print(f"[日报生成] 已保存失败记录到历史表") log_info(f"[日报生成] 已保存失败记录到历史表", 'report')
except Exception as save_error: except Exception as save_error:
print(f"[日报生成] 保存失败记录时出错: {str(save_error)}") log_error(f"[日报生成] 保存失败记录时出错: {str(save_error)}", 'report')
return { return {
'success': False, 'success': False,
@@ -568,6 +569,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
config_name, split_name, start_time, end_time, config_name, split_name, start_time, end_time,
id_name_maps=None, field_custom_names=None, has_supplement=False, id_name_maps=None, field_custom_names=None, has_supplement=False,
show_monthly_total=False, config_id=None, report_date=None, sort_order=None, 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):
""" """
生成Excel文件 生成Excel文件
@@ -615,7 +617,14 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
# 表头从第1行开始 # 表头从第1行开始
header_row = 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 from openpyxl.styles import Border, Side
@@ -820,7 +829,12 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
try: try:
from lib.sum_data_collector import get_monthly_charge_degree_total from lib.sum_data_collector import get_monthly_charge_degree_total
month = report_date[:7] 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']: if monthly_result['success'] and monthly_result['data']:
total_degree = monthly_result['data']['total_degree'] 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 ws.row_dimensions[monthly_row].height = 25
except Exception as e: except Exception as e:
print(f"[日报生成] 写入月度总计时出错: {e}") log_error(f"[日报生成] 写入月度总计时出错: {e}", 'report')
# 调整列宽(中文按2个字符宽度计算) # 调整列宽(中文按2个字符宽度计算)
# 序号列固定宽度 # 序号列固定宽度
@@ -929,10 +943,10 @@ def save_report_history(report_date, start_time, end_time, split_type, split_val
# 生成ID # 生成ID
history_id = int(datetime.now().timestamp() * 1000) history_id = int(datetime.now().timestamp() * 1000)
print(f"[保存历史] 准备保存历史记录") log_info(f"[保存历史] 准备保存历史记录", 'report')
print(f" ID: {history_id}") log_info(f" ID: {history_id}", 'report')
print(f" file_path: '{file_path}'") log_info(f" file_path: '{file_path}'", 'report')
print(f" status: {status}") log_info(f" status: {status}", 'report')
sql = """ sql = """
INSERT INTO t_daily_report_history 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 status
) )
print(f"[保存历史] 执行SQL插入...") log_info(f"[保存历史] 执行SQL插入...", 'report')
result = execute_insert(sql, params) result = execute_insert(sql, params)
print(f"[保存历史] 插入完成,返回ID: {result}") log_info(f"[保存历史] 插入完成,返回ID: {result}", 'report')
return history_id return history_id

View File

@@ -6,6 +6,7 @@ import os
from datetime import datetime from datetime import datetime
from lib.db import execute_query, execute_update, execute_insert from lib.db import execute_query, execute_update, execute_insert
from lib.field_mapping import get_field_display_name from lib.field_mapping import get_field_display_name
from lib.logger import log_info, log_error, log_warning
def collect_sum_data_from_history(history_id=None): 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) charge_degree = _calculate_charge_degree(history)
if charge_degree is not None: if charge_degree is not None:
sum_results['charge_degree'] = charge_degree 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: except Exception as calc_error:
print(f"[采集] 历史记录 {history['id']} 计算充电电量失败: {calc_error}") log_error(f"[采集] 历史记录 {history['id']} 计算充电电量失败: {calc_error}", 'sum_data')
# 如果还是没有任何求和数据,跳过 # 如果还是没有任何求和数据,跳过
if not sum_results: if not sum_results:
@@ -167,15 +168,15 @@ def collect_sum_data_from_history(history_id=None):
execute_insert(sql, params) execute_insert(sql, params)
collected_count += 1 collected_count += 1
except Exception as fee_error: 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 { return {
'success': True, 'success': True,
'message': f'采集完成,共采集 {collected_count} 条求和数据,跳过 {skipped_count} 条无求和数据的记录' 'message': f'采集完成,共采集 {collected_count} 条求和数据,跳过 {skipped_count} 条无求和数据的记录'
} }
except Exception as e: except Exception as e:
import traceback log_error(f'[采集] 采集失败: {e}', 'sum_data')
traceback.print_exc()
return {'success': False, 'message': f'采集失败: {str(e)}'} return {'success': False, 'message': f'采集失败: {str(e)}'}
@@ -229,7 +230,7 @@ def _calculate_charge_degree(history):
return 0.0 return 0.0
except Exception as e: except Exception as e:
print(f"[计算充电电量] 失败: {e}") log_error(f"[计算充电电量] 失败: {e}", 'sum_data')
return None 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)} 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: Args:
config_id: 配置ID(可选,不传则统计所有配置) config_id: 配置ID(可选,不传则统计所有配置)
month: 月份,格式 YYYY-MM(可选,默认当月) month: 月份,格式 YYYY-MM(可选,默认当月)
split_type: 拆分类型(可选,company_id/user_id/station_id)
split_value: 拆分值,逗号分隔的字符串(可选,用于过滤)
Returns: Returns:
dict: { dict: {
@@ -716,6 +720,15 @@ def get_monthly_charge_degree_total(config_id=None, month=None):
sql += ' AND config_id = %s' sql += ' AND config_id = %s'
params.append(config_id) 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' sql += ' GROUP BY report_date ORDER BY report_date ASC'
rows = execute_query(sql, tuple(params)) rows = execute_query(sql, tuple(params))

View File

@@ -16,8 +16,8 @@ async function batchDownloadHistory() {
// 按日期查询 // 按日期查询
function queryHistoryByDate() { function queryHistoryByDate() {
const startDate = document.getElementById('query-start-date').value; const startDate = document.getElementById('history-start-date').value;
const endDate = document.getElementById('query-end-date').value; const endDate = document.getElementById('history-end-date').value;
if (!startDate && !endDate) { if (!startDate && !endDate) {
alert('请至少选择一个日期'); alert('请至少选择一个日期');
return; return;
@@ -28,8 +28,8 @@ function queryHistoryByDate() {
// 清除日期查询 // 清除日期查询
function clearDateQuery() { function clearDateQuery() {
document.getElementById('query-start-date').value = ''; document.getElementById('history-start-date').value = '';
document.getElementById('query-end-date').value = ''; document.getElementById('history-end-date').value = '';
loadHistory(1); loadHistory(1);
} }
@@ -227,3 +227,59 @@ function toggleSelectAllHistory() {
cb.checked = selectAllCheckbox.checked; cb.checked = selectAllCheckbox.checked;
}); });
} }
// 删除单条历史记录
async function deleteHistory(id) {
if (!confirm('确定要删除这条历史记录吗?对应的报表文件也会被删除。')) {
return;
}
try {
const response = await fetch('/api/report/history/batch-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: [id] })
});
const result = await response.json();
if (result.success) {
alert('删除成功');
loadHistory();
} else {
alert('删除失败: ' + result.message);
}
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 批量删除历史记录
async function batchDeleteHistory() {
const checkboxes = document.querySelectorAll('.history-checkbox:checked');
if (checkboxes.length === 0) {
alert('请至少选择一条记录');
return;
}
if (!confirm(`确定要删除选中的 ${checkboxes.length} 条历史记录吗?对应的报表文件也会被删除。`)) {
return;
}
const ids = Array.from(checkboxes).map(cb => parseInt(cb.value));
try {
const response = await fetch('/api/report/history/batch-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: ids })
});
const result = await response.json();
if (result.success) {
alert(result.message);
loadHistory();
} else {
alert('删除失败: ' + result.message);
}
} catch (error) {
alert('删除失败: ' + error.message);
}
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单日报系统</title> <title>订单日报系统</title>
<link rel="stylesheet" href="/public/static/css/style.css?v=2026071506"> <link rel="stylesheet" href="/public/static/css/style.css?v=2026071507">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@@ -97,7 +97,8 @@
<input type="date" id="history-end-date" class="form-control" style="width: auto;"> <input type="date" id="history-end-date" class="form-control" style="width: auto;">
<button class="btn btn-primary" onclick="queryHistoryByDate()">查询</button> <button class="btn btn-primary" onclick="queryHistoryByDate()">查询</button>
<button class="btn" onclick="clearDateQuery()">清除</button> <button class="btn" onclick="clearDateQuery()">清除</button>
<button class="btn btn-danger" onclick="batchDownloadHistory()" style="margin-left: auto;">批量下载</button> <button class="btn btn-danger" onclick="batchDeleteHistory()" style="margin-left: auto;">批量删除</button>
<button class="btn btn-danger" onclick="batchDownloadHistory()">批量下载</button>
</div> </div>
<table> <table>
@@ -350,11 +351,11 @@
</div> </div>
<!-- JavaScript 文件 --> <!-- JavaScript 文件 -->
<script src="/public/static/js/common.js?v=2026071506"></script> <script src="/public/static/js/common.js?v=2026071507"></script>
<script src="/public/static/js/config.js?v=2026071506"></script> <script src="/public/static/js/config.js?v=2026071507"></script>
<script src="/public/static/js/entity.js?v=2026071506"></script> <script src="/public/static/js/entity.js?v=2026071507"></script>
<script src="/public/static/js/report.js?v=2026071506"></script> <script src="/public/static/js/report.js?v=2026071507"></script>
<script src="/public/static/js/history.js?v=2026071506"></script> <script src="/public/static/js/history.js?v=2026071507"></script>
<script src="/public/static/js/sum_data.js?v=2026071506"></script> <script src="/public/static/js/sum_data.js?v=2026071507"></script>
</body> </body>
</html> </html>