Files
ylt_diy/lib/sum_data_collector.py
2026-07-30 10:59:44 +08:00

961 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
日报求和数据采集与管理
"""
import json
import os
from datetime import datetime
from lib.db import execute_query, execute_update, execute_insert
from lib.field_mapping import get_field_display_name
from lib.logger import log_info, log_error, log_warning
def collect_sum_data_from_history(history_id=None, start_date=None, end_date=None):
"""
从历史记录中采集求和数据并写入求和数据表
充电电量charge_degree是必须采集的其他求和字段有就采集没有就不采集
Args:
history_id: 历史记录ID可选不填则采集所有成功的历史记录
start_date: 开始日期(可选,格式 YYYY-MM-DD
end_date: 结束日期(可选,格式 YYYY-MM-DD
Returns:
dict: 采集结果
"""
try:
# 查询历史记录
if history_id:
history_list = execute_query(
'SELECT * FROM t_daily_report_history WHERE id = %s AND status = 1',
(history_id,)
)
else:
where_clauses = ['status = 1']
params = []
if start_date:
where_clauses.append('report_date >= %s')
params.append(start_date)
if end_date:
where_clauses.append('report_date <= %s')
params.append(end_date)
where_sql = ' AND '.join(where_clauses)
history_list = execute_query(
f'SELECT * FROM t_daily_report_history WHERE {where_sql} ORDER BY create_time DESC',
tuple(params)
)
if not history_list:
return {'success': False, 'message': '没有找到符合条件的历史记录'}
collected_count = 0
skipped_count = 0
config_cache = {} # 缓存配置的排序号和自定义服务费单价
# 获取全局配置的求和数据采集字段
sum_data_fields = None
try:
global_config_result = execute_query(
'SELECT sum_data_fields FROM t_daily_report_global_config WHERE id = 1 LIMIT 1'
)
if global_config_result and global_config_result[0].get('sum_data_fields'):
sum_data_fields = json.loads(global_config_result[0]['sum_data_fields'])
except Exception:
pass
for history in history_list:
sum_results = history.get('sum_results')
# 解析 sum_results JSON
if isinstance(sum_results, str):
sum_results = json.loads(sum_results) if sum_results else {}
elif sum_results is None:
sum_results = {}
# 获取配置信息(排序号、自定义服务费单价)(缓存)
config_id = history.get('config_id')
sort_order = None
custom_service_fee_price = None
if config_id:
if config_id in config_cache:
sort_order = config_cache[config_id].get('sort_order')
custom_service_fee_price = config_cache[config_id].get('custom_service_fee_price')
custom_service_fee_name = config_cache[config_id].get('custom_service_fee_name')
else:
try:
cfg_result = execute_query(
'SELECT sort_order, custom_service_fee_price, custom_service_fee_name FROM t_daily_report_config WHERE id = %s LIMIT 1',
(config_id,)
)
if cfg_result:
sort_order = cfg_result[0].get('sort_order')
custom_service_fee_price = cfg_result[0].get('custom_service_fee_price')
custom_service_fee_name = cfg_result[0].get('custom_service_fee_name')
config_cache[config_id] = {
'sort_order': sort_order,
'custom_service_fee_price': custom_service_fee_price,
'custom_service_fee_name': custom_service_fee_name
}
except Exception:
config_cache[config_id] = {
'sort_order': None,
'custom_service_fee_price': None,
'custom_service_fee_name': None
}
# 强制确保有充电电量charge_degree
if 'charge_degree' not in sum_results or sum_results['charge_degree'] is None:
# 从订单表重新计算充电电量
try:
charge_degree = _calculate_charge_degree(history)
if charge_degree is not None:
sum_results['charge_degree'] = charge_degree
log_info(f"[采集] 历史记录 {history['id']} 补充计算充电电量: {charge_degree} kWh", 'sum_data')
except Exception as calc_error:
log_error(f"[采集] 历史记录 {history['id']} 计算充电电量失败: {calc_error}", 'sum_data')
# 如果还是没有任何求和数据,跳过
if not sum_results:
skipped_count += 1
continue
# 先删除该历史记录对应的旧数据(避免重复)
execute_update(
'''DELETE FROM t_daily_report_sum_data
WHERE report_date = %s AND config_id = %s AND split_value = %s AND data_source = 'auto'
''',
(history['report_date'], history['config_id'], history['split_value'])
)
# 根据配置过滤采集字段
# 如果配置了 sum_data_fields则只采集这些字段charge_degree 始终采集)
if sum_data_fields and isinstance(sum_data_fields, list):
filtered_fields = set(sum_data_fields)
filtered_fields.add('charge_degree') # 充电电量始终采集
sum_results = {k: v for k, v in sum_results.items() if k in filtered_fields}
# 收集所有待插入的数据
batch_params = []
# 收集求和字段数据
for field_key, field_value in sum_results.items():
sum_data_id = int(datetime.now().timestamp() * 1000000) + collected_count + len(batch_params)
batch_params.append((
sum_data_id,
history['report_date'],
history['config_id'],
history.get('config_name', ''),
history.get('split_type', ''),
history.get('split_value', ''),
history.get('split_name', ''),
field_key,
get_field_display_name(field_key),
float(field_value) if field_value is not None else 0,
history.get('total_orders', 0),
sort_order,
'auto',
f'从历史记录自动采集历史ID: {history["id"]}'
))
# 如果配置了自定义服务费单价,自动计算并采集自定义服务费
custom_service_fee = None
if custom_service_fee_price is not None and custom_service_fee_price != '' and sum_results.get('charge_degree') is not None:
try:
charge_degree = float(sum_results['charge_degree'])
custom_service_fee = round(charge_degree * float(custom_service_fee_price), 2)
if custom_service_fee_name and str(custom_service_fee_name).strip():
fee_display_name = str(custom_service_fee_name).strip()
else:
fee_display_name = f'自定义服务费({custom_service_fee_price}元/kWh)'
sum_data_id = int(datetime.now().timestamp() * 1000000) + collected_count + len(batch_params)
batch_params.append((
sum_data_id,
history['report_date'],
history['config_id'],
history.get('config_name', ''),
history.get('split_type', ''),
history.get('split_value', ''),
history.get('split_name', ''),
'custom_service_fee',
fee_display_name,
custom_service_fee,
history.get('total_orders', 0),
sort_order,
'auto',
f'自动计算:充电电量{charge_degree}kWh × 单价{custom_service_fee_price}元/kWh'
))
except Exception as fee_error:
log_warning(f"[采集] 历史记录 {history['id']} 自定义服务费计算失败: {fee_error}", 'sum_data')
# 计算自定义实收总金额(实收电费 + 自定义服务费)
if custom_service_fee is not None:
try:
charge_elecfee_amount = float(sum_results.get('charge_elecfee_amount', 0) or 0)
custom_total_amount = round(charge_elecfee_amount + custom_service_fee, 2)
sum_data_id = int(datetime.now().timestamp() * 1000000) + collected_count + len(batch_params)
batch_params.append((
sum_data_id,
history['report_date'],
history['config_id'],
history.get('config_name', ''),
history.get('split_type', ''),
history.get('split_value', ''),
history.get('split_name', ''),
'custom_total_amount',
'自定义实收总金额',
custom_total_amount,
history.get('total_orders', 0),
sort_order,
'auto',
f'自动计算:实收电费{charge_elecfee_amount}元 + 自定义服务费{custom_service_fee}'
))
except Exception as total_error:
log_warning(f"[采集] 历史记录 {history['id']} 自定义实收总金额计算失败: {total_error}", 'sum_data')
# 批量插入所有数据Doris 优化)
if batch_params:
sql = '''
INSERT INTO t_daily_report_sum_data
(id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
'''
from lib.db import execute_batch_insert
inserted = execute_batch_insert(sql, batch_params, batch_size=50)
collected_count += inserted
log_info(f'[采集] 采集完成,共采集 {collected_count} 条求和数据,跳过 {skipped_count} 条无求和数据的记录', 'sum_data')
return {
'success': True,
'message': f'采集完成,共采集 {collected_count} 条求和数据,跳过 {skipped_count} 条无求和数据的记录'
}
except Exception as e:
log_error(f'[采集] 采集失败: {e}', 'sum_data')
return {'success': False, 'message': f'采集失败: {str(e)}'}
def _calculate_charge_degree(history):
"""
从订单表中计算指定历史记录对应的充电电量总和
Args:
history: 历史记录字典
Returns:
float: 充电电量总和(保留三位小数),失败返回 None
"""
try:
split_type = history.get('split_type', '')
split_value = history.get('split_value', '')
start_time = history.get('start_time', '')
end_time = history.get('end_time', '')
if not split_type or not split_value or not start_time or not end_time:
return None
# 检查是否为多选值(逗号分隔)
split_values = [v.strip() for v in str(split_value).split(',') if v.strip()]
if len(split_values) > 1:
placeholders = ', '.join(['%s'] * len(split_values))
sql = f"""
SELECT SUM(charge_degree) as total_degree
FROM t_equipment_charge_order
WHERE state = 3
AND report_time >= %s
AND report_time < %s
AND {split_type} IN ({placeholders})
"""
params = [start_time, end_time] + split_values
else:
sql = f"""
SELECT SUM(charge_degree) as total_degree
FROM t_equipment_charge_order
WHERE state = 3
AND report_time >= %s
AND report_time < %s
AND {split_type} = %s
"""
params = [start_time, end_time, split_value]
result = execute_query(sql, tuple(params))
if result and result[0].get('total_degree') is not None:
return round(float(result[0]['total_degree']), 3)
return 0.0
except Exception as e:
log_error(f"[计算充电电量] 失败: {e}", 'sum_data')
return None
def get_sum_data_list(page=None, page_size=20, start_date=None, end_date=None, config_id=None,
config_name=None, sum_field_key=None, data_source=None, skip_count=False):
"""
查询求和数据列表
Args:
page: 页码None表示不分页返回所有数据
page_size: 每页大小
start_date: 开始日期(可选)
end_date: 结束日期(可选)
config_id: 配置ID可选
config_name: 配置名称(可选,模糊搜索)
sum_field_key: 求和字段键名(可选)
data_source: 数据来源(可选)
skip_count: 是否跳过 COUNT 查询(优化性能)
Returns:
dict: 查询结果
"""
try:
where_clauses = ['1=1']
params = []
if start_date:
where_clauses.append('report_date >= %s')
params.append(start_date)
if end_date:
where_clauses.append('report_date <= %s')
params.append(end_date)
if config_id:
where_clauses.append('config_id = %s')
params.append(config_id)
if config_name:
where_clauses.append('config_name LIKE %s')
params.append(f'%{config_name}%')
if sum_field_key:
where_clauses.append('sum_field_key = %s')
params.append(sum_field_key)
if data_source:
where_clauses.append('data_source = %s')
params.append(data_source)
where_sql = ' AND '.join(where_clauses)
# 查询总数(如果不跳过)
total = 0
if not skip_count:
total_result = execute_query(
f'SELECT COUNT(*) as total FROM t_daily_report_sum_data WHERE {where_sql}',
tuple(params)
)
total = total_result[0]['total']
# 查询数据
if page is not None:
offset = (page - 1) * page_size
list_result = execute_query(
f'''SELECT * FROM t_daily_report_sum_data
WHERE {where_sql}
ORDER BY report_date DESC, sort_order ASC, config_id ASC, id DESC
LIMIT %s OFFSET %s''',
tuple(params) + (page_size, offset)
)
else:
list_result = execute_query(
f'''SELECT * FROM t_daily_report_sum_data
WHERE {where_sql}
ORDER BY report_date DESC, sort_order ASC, config_id ASC, id DESC'''
,
tuple(params)
)
return {
'success': True,
'data': {
'list': list_result,
'total': total,
'page': page,
'page_size': page_size
}
}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def add_sum_data(data):
"""
手动添加求和数据
Args:
data: 求和数据字典
Returns:
dict: 操作结果
"""
try:
sum_data_id = int(datetime.now().timestamp() * 1000)
# 获取配置的排序号
sort_order = None
config_id = data.get('config_id')
if config_id:
try:
cfg_result = execute_query(
'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1',
(config_id,)
)
if cfg_result and cfg_result[0]['sort_order'] is not None:
sort_order = cfg_result[0]['sort_order']
except Exception:
pass
sql = '''
INSERT INTO t_daily_report_sum_data
(id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
'''
params = (
sum_data_id,
data.get('report_date', ''),
data.get('config_id'),
data.get('config_name', ''),
data.get('split_type', ''),
data.get('split_value', ''),
data.get('split_name', ''),
data.get('sum_field_key', ''),
data.get('sum_field_name', ''),
float(data.get('sum_value', 0)),
data.get('total_orders', 0),
sort_order,
'manual',
data.get('remark', '')
)
execute_insert(sql, params)
return {'success': True, 'message': '添加成功', 'id': sum_data_id}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def batch_add_sum_data(data):
"""
批量添加求和数据
Args:
data: 求和数据字典,包含 fields 列表
Returns:
dict: 操作结果
"""
try:
fields = data.get('fields', [])
if not fields:
return {'success': False, 'message': '请至少添加一个字段'}
# 获取配置的排序号
sort_order = None
config_id = data.get('config_id')
if config_id:
try:
cfg_result = execute_query(
'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1',
(config_id,)
)
if cfg_result and cfg_result[0]['sort_order'] is not None:
sort_order = cfg_result[0]['sort_order']
except Exception:
pass
sql = '''
INSERT INTO t_daily_report_sum_data
(id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
'''
count = 0
for field in fields:
sum_data_id = int(datetime.now().timestamp() * 1000) + count
params = (
sum_data_id,
data.get('report_date', ''),
data.get('config_id'),
data.get('config_name', ''),
data.get('split_type', ''),
data.get('split_value', ''),
data.get('split_name', ''),
field.get('sum_field_key', ''),
field.get('sum_field_name', ''),
float(field.get('sum_value', 0)),
data.get('total_orders', 0),
sort_order,
'manual',
data.get('remark', '')
)
execute_insert(sql, params)
count += 1
return {'success': True, 'message': f'添加成功,共{count}条记录', 'count': count}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def update_sum_data(sum_data_id, data):
"""
更新求和数据
Args:
sum_data_id: 求和数据ID
data: 更新的数据字典
Returns:
dict: 操作结果
"""
try:
update_fields = []
params = []
if 'report_date' in data:
update_fields.append('report_date = %s')
params.append(data['report_date'])
if 'config_id' in data:
update_fields.append('config_id = %s')
params.append(data['config_id'])
if 'config_name' in data:
update_fields.append('config_name = %s')
params.append(data['config_name'])
if 'split_type' in data:
update_fields.append('split_type = %s')
params.append(data['split_type'])
if 'split_value' in data:
update_fields.append('split_value = %s')
params.append(data['split_value'])
if 'split_name' in data:
update_fields.append('split_name = %s')
params.append(data['split_name'])
if 'sum_field_key' in data:
update_fields.append('sum_field_key = %s')
params.append(data['sum_field_key'])
if 'sum_field_name' in data:
update_fields.append('sum_field_name = %s')
params.append(data['sum_field_name'])
if 'sum_value' in data:
update_fields.append('sum_value = %s')
params.append(float(data['sum_value']))
if 'total_orders' in data:
update_fields.append('total_orders = %s')
params.append(data['total_orders'])
if 'remark' in data:
update_fields.append('remark = %s')
params.append(data['remark'])
if 'data_source' in data:
update_fields.append('data_source = %s')
params.append(data['data_source'])
if not update_fields:
return {'success': False, 'message': '没有需要更新的字段'}
update_fields.append('update_time = NOW()')
params.append(sum_data_id)
sql = f'''
UPDATE t_daily_report_sum_data
SET {', '.join(update_fields)}
WHERE id = %s
'''
rowcount = execute_update(sql, tuple(params))
if rowcount > 0:
return {'success': True, 'message': '更新成功'}
else:
return {'success': False, 'message': '记录不存在'}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def delete_sum_data(sum_data_id):
"""
删除求和数据
Args:
sum_data_id: 求和数据ID
Returns:
dict: 操作结果
"""
try:
rowcount = execute_update(
'DELETE FROM t_daily_report_sum_data WHERE id = %s',
(sum_data_id,)
)
if rowcount > 0:
return {'success': True, 'message': '删除成功'}
else:
return {'success': False, 'message': '记录不存在'}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def batch_delete_sum_data(ids):
"""
批量删除求和数据
Args:
ids: ID列表
Returns:
dict: 操作结果
"""
try:
placeholders = ', '.join(['%s'] * len(ids))
rowcount = execute_update(
f'DELETE FROM t_daily_report_sum_data WHERE id IN ({placeholders})',
tuple(ids)
)
return {'success': True, 'message': f'成功删除 {rowcount} 条记录'}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def export_sum_data_to_excel(start_date=None, end_date=None, config_id=None, data_source=None, keyword=None):
"""
导出求和数据为Excel
Args:
start_date: 开始日期(可选)
end_date: 结束日期(可选)
config_id: 配置ID可选
data_source: 数据来源(可选)
keyword: 配置名称关键词(可选)
Returns:
dict: {success, message, file_path}
"""
try:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
# 构建查询条件
where_clauses = []
params = []
if start_date:
where_clauses.append('report_date >= %s')
params.append(start_date)
if end_date:
where_clauses.append('report_date <= %s')
params.append(end_date)
if config_id:
where_clauses.append('config_id = %s')
params.append(config_id)
if data_source:
where_clauses.append('data_source = %s')
params.append(data_source)
if keyword:
where_clauses.append('config_name LIKE %s')
params.append(f'%{keyword}%')
where_sql = ''
if where_clauses:
where_sql = 'WHERE ' + ' AND '.join(where_clauses)
# 使用分页查询避免大数据量超时
rows = []
page_size = 500
page = 1
while True:
offset = (page - 1) * page_size
sql = f'''
SELECT id, report_date, config_id, config_name, split_type, split_value, split_name,
sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark,
create_time, update_time
FROM t_daily_report_sum_data
{where_sql}
ORDER BY report_date DESC, sort_order ASC, config_id ASC, sum_field_key ASC
LIMIT %s OFFSET %s
'''
query_params = tuple(params) + (page_size, offset)
page_rows = execute_query(sql, query_params)
if not page_rows:
break
rows.extend(page_rows)
if len(page_rows) < page_size:
break
page += 1
# 创建Excel
wb = Workbook()
ws = wb.active
ws.title = '求和数据'
# 表头
headers = ['序号', '报表日期', '配置名称', '拆分方式', '拆分值', '求和字段', '求和值', '订单数', '数据来源', '修改原因/备注', '创建时间', '更新时间']
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(bold=True, color='FFFFFF', size=11)
header_alignment = Alignment(horizontal='center', vertical='center')
thin_border = Border(
left=Side(style='thin', color='000000'),
right=Side(style='thin', color='000000'),
top=Side(style='thin', color='000000'),
bottom=Side(style='thin', color='000000')
)
even_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
# 写入表头
for col_idx, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
ws.row_dimensions[1].height = 25
# 拆分方式映射
split_type_map = {
'company_id': '按企业ID',
'user_id': '按用户ID',
'station_id': '按场站ID'
}
# 数据来源映射
source_map = {
'auto': '自动采集',
'manual': '手动编辑'
}
# 写入数据
for row_idx, row in enumerate(rows, 2):
is_even = (row_idx - 2) % 2 == 1
values = [
row.get('sort_order') or '',
row.get('report_date', ''),
row.get('config_name', ''),
split_type_map.get(row.get('split_type', ''), row.get('split_type', '')),
row.get('split_name', '') or row.get('split_value', ''),
row.get('sum_field_name', ''),
float(row.get('sum_value', 0) or 0),
int(row.get('total_orders', 0) or 0),
source_map.get(row.get('data_source', ''), row.get('data_source', '')),
row.get('remark', ''),
str(row.get('create_time', '')) if row.get('create_time') else '',
str(row.get('update_time', '')) if row.get('update_time') else ''
]
for col_idx, val in enumerate(values, 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.value = val
cell.border = thin_border
# 对齐方式:数字靠右,其他居中或靠左
if col_idx in [7, 8]: # 求和值、订单数
cell.alignment = Alignment(horizontal='right', vertical='center')
if col_idx == 7:
cell.number_format = '0.000'
elif col_idx in [1, 2, 4, 9]: # 序号、日期、拆分方式、数据来源
cell.alignment = Alignment(horizontal='center', vertical='center')
else:
cell.alignment = Alignment(horizontal='left', vertical='center')
if is_even:
cell.fill = even_fill
ws.row_dimensions[row_idx].height = 22
# 调整列宽
col_widths = [8, 14, 20, 12, 20, 18, 14, 10, 12, 30, 20, 20]
for col_idx, width in enumerate(col_widths, 1):
from openpyxl.utils import get_column_letter
col_letter = get_column_letter(col_idx)
ws.column_dimensions[col_letter].width = width
# 冻结首行
ws.freeze_panes = 'A2'
# 保存文件
reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports')
os.makedirs(reports_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'求和数据导出_{timestamp}.xlsx'
file_path = os.path.join(reports_dir, filename)
wb.save(file_path)
return {'success': True, 'message': '导出成功', 'file_path': f'/public/reports/{filename}', 'filename': filename, 'total': len(rows)}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def get_monthly_charge_degree_total(config_id=None, month=None, split_type=None, split_value=None, end_date=None):
"""
获取当月充电电量累计(从求和数据表查询,对账后的准确数据)
- 如果当天已经生成了日报并采集,自动包含在内,不会重复计算
- 以配置当前拆分为准,只统计匹配的拆分值数据
- 截止到 end_date报表日期不包含 end_date 之后的数据
- 返回缺失的日期列表(从月初到 end_date 之间缺少哪些天的数据)
Args:
config_id: 配置ID可选不传则统计所有配置
month: 月份,格式 YYYY-MM可选默认当月
split_type: 拆分类型可选company_id/user_id/station_id
split_value: 拆分值,逗号分隔的字符串(可选,用于过滤)
end_date: 截止日期,格式 YYYY-MM-DD可选默认当月最后一天
Returns:
dict: {
success: bool,
data: {
total_degree: float, // 累计充电电量(kWh)
total_days: int, // 有数据的天数
total_orders: int, // 累计订单数
today_degree: float, // 报表日期当天的电量(kWh)
month: str, // 统计月份
detail: list, // 按天明细
end_date: str, // 截止日期
missing_dates: list, // 缺失的日期列表
expected_days: int // 应该有的天数从月初到end_date
},
message: str
}
"""
try:
from datetime import datetime, timedelta
# 默认当月
if not month:
month = datetime.now().strftime('%Y-%m')
# 拼接日期模糊匹配前缀
date_prefix = month + '-'
# 计算截止日期(默认报表日期或当月最后一天)
if end_date:
end_date_str = end_date
else:
# 取当月最后一天
if month.endswith('-12'):
next_month = str(int(month[:4]) + 1) + '-01'
else:
next_month = month[:5] + str(int(month[5:7]) + 1).zfill(2)
end_date_str = (datetime.strptime(next_month + '-01', '%Y-%m-%d') - timedelta(days=1)).strftime('%Y-%m-%d')
# 构建查询SQL - 查询所有字段,按日期统计(包括手动添加的数据)
# 使用 MAX(total_orders) 避免同一天多个字段重复计算订单数
sql = """
SELECT
report_date,
SUM(CASE WHEN sum_field_key = 'charge_degree' THEN sum_value ELSE 0 END) as total_degree,
MAX(total_orders) as total_orders
FROM t_daily_report_sum_data
WHERE report_date LIKE %s
AND report_date <= %s
"""
params = [date_prefix + '%', end_date_str]
# 按配置过滤包含手动添加的数据手动添加的config_id为NULL
if config_id:
sql += ' AND (config_id = %s OR config_id IS NULL)'
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))
# 计算从月初到end_date应该有多少天
start_of_month = month + '-01'
start_date_obj = datetime.strptime(start_of_month, '%Y-%m-%d')
end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d')
expected_days = (end_date_obj - start_date_obj).days + 1
# 收集已有数据的日期
existing_dates = set()
total_degree = 0.0
total_orders = 0
total_days = len(rows)
today_degree = 0.0
detail = []
for row in rows:
degree = float(row['total_degree'] or 0)
orders = int(row['total_orders'] or 0)
total_degree += degree
total_orders += orders
existing_dates.add(row['report_date'])
detail.append({
'report_date': row['report_date'],
'degree': round(degree, 3),
'orders': orders
})
if row['report_date'] == end_date_str:
today_degree = degree
# 计算缺失的日期
missing_dates = []
for i in range(expected_days):
date_obj = start_date_obj + timedelta(days=i)
date_str = date_obj.strftime('%Y-%m-%d')
if date_str not in existing_dates:
missing_dates.append(date_str)
return {
'success': True,
'data': {
'total_degree': round(total_degree, 3),
'total_days': total_days,
'total_orders': total_orders,
'today_degree': round(today_degree, 3),
'month': month,
'detail': detail,
'end_date': end_date_str,
'missing_dates': missing_dates,
'expected_days': expected_days
},
'message': '查询成功'
}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}