Files
ylt_diy/lib/report_generator.py
user9994793890 9383612712 feat: 将项目从Next.js重构为Python Flask版本
完全重构为Python + Flask架构,包含:

1. 核心模块:
   - lib/db.py:数据库连接(PyMySQL)
   - lib/field_mapping.py:字段映射(100+字段英文→中文)
   - lib/report_generator.py:日报生成核心逻辑

2. Flask应用:
   - app.py:主应用,包含所有API路由
   - 配置管理API(CRUD、启用/禁用、排序)
   - 日报生成API(支持预设/自定义时间)
   - 历史记录API(分页查询)
   - 文件下载API
   - 定时任务(APScheduler,每天8:01)

3. 前端页面:
   - templates/index.html:完整的单页面应用
   - 三个标签页:配置管理、日报生成、历史记录
   - 原生HTML+CSS+JavaScript,无需构建

4. 依赖管理:
   - requirements.txt:Python依赖清单
   - Flask 3.0.0 + Flask-CORS 4.0.0
   - PyMySQL 1.1.0(数据库驱动)
   - openpyxl 3.1.2(Excel处理)
   - APScheduler 3.10.4(定时任务)

5. 启动脚本:
   - scripts/dev.bat:Windows开发环境启动
   - scripts/start.bat:Windows生产环境启动

6. 文档:
   - README.md:完整的安装、运行、使用指南
   - 包含API接口说明、数据库表结构、常见问题

Windows运行方式:
1. 安装Python 3.8+
2. pip install -r requirements.txt
3. scripts\dev.bat 或 python app.py
4. 访问 http://localhost:5000
5. 初始化数据库:POST /api/init-tables

所有功能完整保留:
✅ 定时任务(每天8:01)
✅ 配置管理(CRUD+启用/禁用+排序)
✅ 日报生成(按配置+时间范围)
✅ 历史记录(查看+下载)
✅ 自定义字段(中文显示)
✅ 求和功能(多字段求和)
✅ 按企业/用户拆分
✅ Excel导出

Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 9894087
2026-07-09 11:26:23 +08:00

278 lines
9.3 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 os
import json
from datetime import datetime, timedelta
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill
import pytz
from lib.db import execute_query, execute_insert
from lib.field_mapping import get_field_display_name
def get_beijing_time():
"""获取北京时间"""
beijing_tz = pytz.timezone('Asia/Shanghai')
return datetime.now(beijing_tz)
def generate_daily_report(config_id, start_time=None, end_time=None):
"""
生成日报
Args:
config_id: 配置ID
start_time: 开始时间(可选,默认为昨天8点)
end_time: 结束时间(可选,默认为今天8点)
Returns:
dict: 生成结果,包含 success, message, file_path, total_orders, total_amount, sum_results
"""
try:
# 获取配置信息
configs = execute_query(
'SELECT * FROM t_daily_report_config WHERE id = %s',
(config_id,)
)
if not configs:
return {'success': False, 'message': '配置不存在'}
config = configs[0]
# 解析字段
selected_fields = json.loads(config['selected_fields'])
sum_fields = json.loads(config['sum_fields']) if config['sum_fields'] else []
# 设置时间范围
if not start_time or not end_time:
now = get_beijing_time()
end_time = now.replace(hour=8, minute=0, second=0, microsecond=0)
start_time = end_time - timedelta(days=1)
# 格式化时间
start_time_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
end_time_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
# 构建查询SQL
fields_str = ', '.join(selected_fields)
sql = f"""
SELECT {fields_str}
FROM t_equipment_charge_order
WHERE state = 3
AND report_time >= %s
AND report_time < %s
AND {config['split_type']} = %s
ORDER BY report_time DESC
"""
# 执行查询
orders = execute_query(sql, (start_time_str, end_time_str, config['split_value']))
if not orders:
return {
'success': False,
'message': f'时间范围内没有符合条件的订单数据'
}
# 计算求和
sum_results = {}
for field in sum_fields:
if field in orders[0]:
total = sum(float(order.get(field, 0) or 0) for order in orders)
sum_results[field] = total
# 生成Excel文件
file_path = generate_excel(
orders=orders,
selected_fields=selected_fields,
sum_fields=sum_fields,
sum_results=sum_results,
config_name=config['config_name'],
split_name=config.get('split_name', config['split_value']),
start_time=start_time_str,
end_time=end_time_str
)
# 计算总金额
total_amount = sum(float(order.get('total_money', 0) or 0) for order in orders)
# 保存历史记录
history_id = save_report_history(
report_date=start_time.strftime('%Y-%m-%d'),
start_time=start_time_str,
end_time=end_time_str,
split_type=config['split_type'],
split_value=config['split_value'],
split_name=config.get('split_name', config['split_value']),
config_id=config_id,
total_orders=len(orders),
total_amount=total_amount,
sum_results=sum_results,
file_path=file_path,
status=1
)
return {
'success': True,
'message': '日报生成成功',
'file_path': file_path,
'total_orders': len(orders),
'total_amount': total_amount,
'sum_results': sum_results,
'history_id': history_id
}
except Exception as e:
# 保存失败记录
try:
save_report_history(
report_date=start_time.strftime('%Y-%m-%d') if start_time else datetime.now().strftime('%Y-%m-%d'),
start_time=start_time_str if start_time else '',
end_time=end_time_str if end_time else '',
split_type=config['split_type'] if config else '',
split_value=config['split_value'] if config else '',
split_name=config.get('split_name', '') if config else '',
config_id=config_id,
total_orders=0,
total_amount=0,
sum_results={},
file_path='',
status=0
)
except:
pass
return {
'success': False,
'message': f'生成日报失败: {str(e)}'
}
def generate_excel(orders, selected_fields, sum_fields, sum_results,
config_name, split_name, start_time, end_time):
"""
生成Excel文件
Returns:
str: 文件路径
"""
# 创建reports目录
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'daily_report_{config_name}_{split_name}_{timestamp}.xlsx'
file_path = os.path.join(reports_dir, filename)
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = '日报'
# 写入标题
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(selected_fields))
title_cell = ws.cell(row=1, column=1)
title_cell.value = f'{config_name} - {split_name} 日报'
title_cell.font = Font(size=16, bold=True)
title_cell.alignment = Alignment(horizontal='center')
# 写入时间范围
ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=len(selected_fields))
time_cell = ws.cell(row=2, column=1)
time_cell.value = f'统计时间: {start_time} 至 {end_time}'
time_cell.alignment = Alignment(horizontal='center')
# 写入表头(中文)
header_row = 4
for col_idx, field in enumerate(selected_fields, 1):
cell = ws.cell(row=header_row, column=col_idx)
cell.value = get_field_display_name(field)
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
cell.alignment = Alignment(horizontal='center')
# 写入数据
for row_idx, order in enumerate(orders, header_row + 1):
for col_idx, field in enumerate(selected_fields, 1):
cell = ws.cell(row=row_idx, column=col_idx)
value = order.get(field, '')
# 格式化时间字段
if field in ['report_time', 'start_time', 'end_time', 'create_time', 'update_time', 'pay_time']:
if isinstance(value, datetime):
value = value.strftime('%Y-%m-%d %H:%M:%S')
cell.value = value
# 写入求和行
if sum_fields:
sum_row = header_row + len(orders) + 1
ws.cell(row=sum_row, column=1).value = '合计'
ws.cell(row=sum_row, column=1).font = Font(bold=True)
for col_idx, field in enumerate(selected_fields, 1):
if field in sum_fields and field in sum_results:
cell = ws.cell(row=sum_row, column=col_idx)
cell.value = sum_results[field]
cell.font = Font(bold=True)
cell.number_format = '0.00'
# 调整列宽
for col_idx, field in enumerate(selected_fields, 1):
max_length = len(get_field_display_name(field))
for row in ws.iter_rows(min_row=header_row + 1, max_row=header_row + len(orders),
min_col=col_idx, max_col=col_idx):
for cell in row:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
ws.column_dimensions[ws.cell(row=1, column=col_idx).column_letter].width = min(max_length + 2, 50)
# 保存文件
wb.save(file_path)
# 返回相对路径
return f'/reports/{filename}'
def save_report_history(report_date, start_time, end_time, split_type, split_value,
split_name, config_id, total_orders, total_amount,
sum_results, file_path, status):
"""
保存日报生成历史记录
Returns:
int: 历史记录ID
"""
# 生成ID
history_id = int(datetime.now().timestamp() * 1000)
sql = """
INSERT INTO t_daily_report_history
(id, report_date, start_time, end_time, split_type, split_value, split_name,
config_id, total_orders, total_amount, sum_results, file_path, status, create_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
"""
params = (
history_id,
report_date,
start_time,
end_time,
split_type,
split_value,
split_name,
config_id,
total_orders,
total_amount,
json.dumps(sum_results, ensure_ascii=False),
file_path,
status
)
execute_insert(sql, params)
return history_id