完全重构为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
449 lines
16 KiB
Python
449 lines
16 KiB
Python
"""
|
||
Flask主应用
|
||
订单日报系统
|
||
"""
|
||
import os
|
||
import json
|
||
from datetime import datetime
|
||
from flask import Flask, request, jsonify, send_from_directory, render_template
|
||
from flask_cors import CORS
|
||
from apscheduler.schedulers.background import BackgroundScheduler
|
||
import pytz
|
||
|
||
from lib.db import execute_query, execute_update, execute_insert
|
||
from lib.field_mapping import get_all_fields_with_display
|
||
from lib.report_generator import generate_daily_report
|
||
|
||
# 创建Flask应用
|
||
app = Flask(__name__,
|
||
template_folder='templates',
|
||
static_folder='public')
|
||
CORS(app)
|
||
|
||
# 确保public/reports目录存在
|
||
os.makedirs(os.path.join('public', 'reports'), exist_ok=True)
|
||
|
||
|
||
# ==================== 页面路由 ====================
|
||
|
||
@app.route('/')
|
||
def index():
|
||
"""主页"""
|
||
return render_template('index.html')
|
||
|
||
|
||
# ==================== 配置管理API ====================
|
||
|
||
@app.route('/api/config', methods=['GET'])
|
||
def get_configs():
|
||
"""获取所有配置"""
|
||
try:
|
||
configs = execute_query(
|
||
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id DESC'
|
||
)
|
||
|
||
# 解析JSON字段
|
||
for config in configs:
|
||
config['selected_fields'] = json.loads(config['selected_fields'])
|
||
config['sum_fields'] = json.loads(config['sum_fields']) if config['sum_fields'] else []
|
||
|
||
return jsonify({'success': True, 'data': configs})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config', methods=['POST'])
|
||
def create_config():
|
||
"""创建配置"""
|
||
try:
|
||
data = request.json
|
||
config_name = data.get('config_name')
|
||
split_type = data.get('split_type')
|
||
split_value = data.get('split_value')
|
||
split_name = data.get('split_name', '')
|
||
selected_fields = data.get('selected_fields', [])
|
||
sum_fields = data.get('sum_fields', [])
|
||
|
||
if not config_name or not split_type or not split_value:
|
||
return jsonify({'success': False, 'message': '缺少必填字段'}), 400
|
||
|
||
if not selected_fields:
|
||
return jsonify({'success': False, 'message': '请至少选择一个字段'}), 400
|
||
|
||
# 生成ID
|
||
config_id = int(datetime.now().timestamp() * 1000)
|
||
|
||
# 获取最大排序值
|
||
max_sort = execute_query('SELECT MAX(sort_order) as max_sort FROM t_daily_report_config')
|
||
sort_order = (max_sort[0]['max_sort'] or 0) + 1
|
||
|
||
sql = """
|
||
INSERT INTO t_daily_report_config
|
||
(id, config_name, split_type, split_value, split_name, selected_fields,
|
||
sum_fields, is_active, sort_order, create_time, update_time)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW())
|
||
"""
|
||
|
||
params = (
|
||
config_id,
|
||
config_name,
|
||
split_type,
|
||
split_value,
|
||
split_name,
|
||
json.dumps(selected_fields, ensure_ascii=False),
|
||
json.dumps(sum_fields, ensure_ascii=False),
|
||
sort_order
|
||
)
|
||
|
||
execute_insert(sql, params)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '配置创建成功',
|
||
'data': {'id': config_id}
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/<int:config_id>', methods=['PUT'])
|
||
def update_config(config_id):
|
||
"""更新配置"""
|
||
try:
|
||
data = request.json
|
||
config_name = data.get('config_name')
|
||
split_type = data.get('split_type')
|
||
split_value = data.get('split_value')
|
||
split_name = data.get('split_name', '')
|
||
selected_fields = data.get('selected_fields', [])
|
||
sum_fields = data.get('sum_fields', [])
|
||
|
||
sql = """
|
||
UPDATE t_daily_report_config
|
||
SET config_name = %s, split_type = %s, split_value = %s, split_name = %s,
|
||
selected_fields = %s, sum_fields = %s, update_time = NOW()
|
||
WHERE id = %s
|
||
"""
|
||
|
||
params = (
|
||
config_name,
|
||
split_type,
|
||
split_value,
|
||
split_name,
|
||
json.dumps(selected_fields, ensure_ascii=False),
|
||
json.dumps(sum_fields, ensure_ascii=False),
|
||
config_id
|
||
)
|
||
|
||
execute_update(sql, params)
|
||
|
||
return jsonify({'success': True, 'message': '配置更新成功'})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/<int:config_id>', methods=['DELETE'])
|
||
def delete_config(config_id):
|
||
"""删除配置"""
|
||
try:
|
||
execute_update('DELETE FROM t_daily_report_config WHERE id = %s', (config_id,))
|
||
return jsonify({'success': True, 'message': '配置删除成功'})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/<int:config_id>/toggle', methods=['POST'])
|
||
def toggle_config(config_id):
|
||
"""启用/禁用配置"""
|
||
try:
|
||
execute_update(
|
||
'UPDATE t_daily_report_config SET is_active = NOT is_active, update_time = NOW() WHERE id = %s',
|
||
(config_id,)
|
||
)
|
||
return jsonify({'success': True, 'message': '操作成功'})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/config/<int:config_id>/move', methods=['POST'])
|
||
def move_config(config_id):
|
||
"""移动配置排序"""
|
||
try:
|
||
direction = request.json.get('direction') # up 或 down
|
||
|
||
# 获取当前配置
|
||
current = execute_query(
|
||
'SELECT sort_order FROM t_daily_report_config WHERE id = %s',
|
||
(config_id,)
|
||
)
|
||
if not current:
|
||
return jsonify({'success': False, 'message': '配置不存在'}), 404
|
||
|
||
current_order = current[0]['sort_order']
|
||
|
||
if direction == 'up':
|
||
# 找到上一个配置
|
||
prev = execute_query(
|
||
'SELECT id, sort_order FROM t_daily_report_config WHERE sort_order < %s ORDER BY sort_order DESC LIMIT 1',
|
||
(current_order,)
|
||
)
|
||
if prev:
|
||
# 交换排序
|
||
execute_update(
|
||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||
(prev[0]['sort_order'], config_id)
|
||
)
|
||
execute_update(
|
||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||
(current_order, prev[0]['id'])
|
||
)
|
||
elif direction == 'down':
|
||
# 找到下一个配置
|
||
next_config = execute_query(
|
||
'SELECT id, sort_order FROM t_daily_report_config WHERE sort_order > %s ORDER BY sort_order ASC LIMIT 1',
|
||
(current_order,)
|
||
)
|
||
if next_config:
|
||
# 交换排序
|
||
execute_update(
|
||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||
(next_config[0]['sort_order'], config_id)
|
||
)
|
||
execute_update(
|
||
'UPDATE t_daily_report_config SET sort_order = %s WHERE id = %s',
|
||
(current_order, next_config[0]['id'])
|
||
)
|
||
|
||
return jsonify({'success': True, 'message': '移动成功'})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 企业和用户列表API ====================
|
||
|
||
@app.route('/api/entities', methods=['GET'])
|
||
def get_entities():
|
||
"""获取企业或用户列表"""
|
||
try:
|
||
entity_type = request.args.get('type', 'company')
|
||
|
||
if entity_type == 'company':
|
||
# 获取企业列表
|
||
companies = execute_query(
|
||
'SELECT DISTINCT company_id, company_name FROM t_company ORDER BY company_name'
|
||
)
|
||
return jsonify({'success': True, 'data': companies})
|
||
else:
|
||
# 获取用户列表
|
||
users = execute_query(
|
||
'SELECT DISTINCT user_id, user_name, order_type FROM t_equipment_charge_order WHERE user_id IS NOT NULL ORDER BY user_name'
|
||
)
|
||
return jsonify({'success': True, 'data': users})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 字段列表API ====================
|
||
|
||
@app.route('/api/fields', methods=['GET'])
|
||
def get_fields():
|
||
"""获取所有可用字段"""
|
||
try:
|
||
fields = get_all_fields_with_display()
|
||
return jsonify({'success': True, 'data': fields})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 日报生成API ====================
|
||
|
||
@app.route('/api/report/generate', methods=['POST'])
|
||
def generate_report():
|
||
"""生成日报"""
|
||
try:
|
||
data = request.json
|
||
config_id = data.get('config_id')
|
||
|
||
if not config_id:
|
||
return jsonify({'success': False, 'message': '请选择配置'}), 400
|
||
|
||
# 解析时间参数
|
||
start_time = None
|
||
end_time = None
|
||
|
||
if data.get('start_time'):
|
||
start_time = datetime.strptime(data['start_time'], '%Y-%m-%d %H:%M:%S')
|
||
if data.get('end_time'):
|
||
end_time = datetime.strptime(data['end_time'], '%Y-%m-%d %H:%M:%S')
|
||
|
||
result = generate_daily_report(config_id, start_time, end_time)
|
||
|
||
if result['success']:
|
||
return jsonify(result)
|
||
else:
|
||
return jsonify(result), 400
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 历史记录API ====================
|
||
|
||
@app.route('/api/report/history', methods=['GET'])
|
||
def get_report_history():
|
||
"""获取日报生成历史"""
|
||
try:
|
||
page = int(request.args.get('page', 1))
|
||
page_size = int(request.args.get('page_size', 20))
|
||
offset = (page - 1) * page_size
|
||
|
||
# 查询总数
|
||
total_result = execute_query('SELECT COUNT(*) as total FROM t_daily_report_history')
|
||
total = total_result[0]['total']
|
||
|
||
# 查询数据
|
||
history = execute_query(
|
||
'SELECT * FROM t_daily_report_history ORDER BY create_time DESC LIMIT %s OFFSET %s',
|
||
(page_size, offset)
|
||
)
|
||
|
||
# 解析JSON字段
|
||
for item in history:
|
||
item['sum_results'] = json.loads(item['sum_results']) if item['sum_results'] else {}
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'list': history,
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size
|
||
}
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 数据库初始化API ====================
|
||
|
||
@app.route('/api/init-tables', methods=['POST'])
|
||
def init_tables():
|
||
"""初始化数据库表"""
|
||
try:
|
||
# 创建配置表
|
||
create_config_table = """
|
||
CREATE TABLE IF NOT EXISTS t_daily_report_config (
|
||
id BIGINT NOT NULL COMMENT '配置ID',
|
||
config_name VARCHAR(100) NOT NULL COMMENT '配置名称',
|
||
split_type VARCHAR(20) NOT NULL COMMENT '拆分方式',
|
||
split_value VARCHAR(100) NOT NULL COMMENT '拆分值',
|
||
split_name VARCHAR(200) NULL COMMENT '拆分显示名称',
|
||
selected_fields VARCHAR(3000) NOT NULL COMMENT '选择的字段列表',
|
||
sum_fields VARCHAR(3000) NULL COMMENT '求和字段列表',
|
||
is_active TINYINT NULL COMMENT '是否启用',
|
||
sort_order INT NULL COMMENT '排序顺序',
|
||
create_time DATETIME NULL COMMENT '创建时间',
|
||
update_time DATETIME NULL COMMENT '更新时间'
|
||
)
|
||
UNIQUE KEY(id)
|
||
DISTRIBUTED BY HASH(id) BUCKETS 1
|
||
PROPERTIES("replication_num" = "1")
|
||
"""
|
||
execute_update(create_config_table)
|
||
|
||
# 创建历史表
|
||
create_history_table = """
|
||
CREATE TABLE IF NOT EXISTS t_daily_report_history (
|
||
id BIGINT NOT NULL COMMENT '历史ID',
|
||
report_date VARCHAR(20) NULL COMMENT '报表日期',
|
||
start_time VARCHAR(50) NULL COMMENT '开始时间',
|
||
end_time VARCHAR(50) NULL COMMENT '结束时间',
|
||
split_type VARCHAR(20) NULL COMMENT '拆分方式',
|
||
split_value VARCHAR(100) NULL COMMENT '拆分值',
|
||
split_name VARCHAR(200) NULL COMMENT '拆分显示名称',
|
||
config_id BIGINT NULL COMMENT '配置ID',
|
||
total_orders INT NULL COMMENT '订单总数',
|
||
total_amount DECIMAL(10,2) NULL COMMENT '总金额',
|
||
sum_results VARCHAR(2000) NULL COMMENT '求和结果',
|
||
file_path VARCHAR(500) NULL COMMENT '文件路径',
|
||
status TINYINT NULL COMMENT '状态',
|
||
create_time DATETIME NULL COMMENT '创建时间'
|
||
)
|
||
UNIQUE KEY(id)
|
||
DISTRIBUTED BY HASH(id) BUCKETS 1
|
||
PROPERTIES("replication_num" = "1")
|
||
"""
|
||
execute_update(create_history_table)
|
||
|
||
return jsonify({'success': True, 'message': '数据库表初始化成功'})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 文件下载API ====================
|
||
|
||
@app.route('/api/download', methods=['GET'])
|
||
def download_file():
|
||
"""下载文件"""
|
||
try:
|
||
file_path = request.args.get('path')
|
||
if not file_path:
|
||
return jsonify({'success': False, 'message': '文件路径不能为空'}), 400
|
||
|
||
# 安全检查:确保路径在public目录下
|
||
if not file_path.startswith('/reports/'):
|
||
return jsonify({'success': False, 'message': '无效的文件路径'}), 400
|
||
|
||
filename = file_path.replace('/reports/', '')
|
||
reports_dir = os.path.join('public', 'reports')
|
||
|
||
return send_from_directory(reports_dir, filename, as_attachment=True)
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'message': str(e)}), 500
|
||
|
||
|
||
# ==================== 定时任务 ====================
|
||
|
||
def scheduled_job():
|
||
"""定时任务:每天8:01自动生成日报"""
|
||
print(f'[{datetime.now()}] 开始执行定时任务...')
|
||
|
||
try:
|
||
# 获取所有启用的配置
|
||
configs = execute_query(
|
||
'SELECT id, config_name FROM t_daily_report_config WHERE is_active = 1'
|
||
)
|
||
|
||
if not configs:
|
||
print('没有启用的配置,跳过生成')
|
||
return
|
||
|
||
print(f'找到 {len(configs)} 个启用的配置')
|
||
|
||
# 为每个配置生成日报
|
||
for config in configs:
|
||
print(f'正在生成: {config["config_name"]}')
|
||
result = generate_daily_report(config['id'])
|
||
|
||
if result['success']:
|
||
print(f' ✓ 成功: {result["total_orders"]} 条订单, 金额: {result["total_amount"]}')
|
||
else:
|
||
print(f' ✗ 失败: {result["message"]}')
|
||
|
||
print('定时任务执行完成')
|
||
except Exception as e:
|
||
print(f'定时任务执行失败: {e}')
|
||
|
||
|
||
# 启动定时任务
|
||
scheduler = BackgroundScheduler(timezone='Asia/Shanghai')
|
||
scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1)
|
||
scheduler.start()
|
||
print('定时任务已启动:每天 08:01 执行')
|
||
|
||
|
||
# ==================== 启动应用 ====================
|
||
|
||
if __name__ == '__main__':
|
||
port = int(os.environ.get('PORT', 5000))
|
||
print(f'启动服务器: http://localhost:{port}')
|
||
app.run(host='0.0.0.0', port=port, debug=True) |