Files
ylt_diy/app.py
user9994793890 cdab7b905a fix: 修复编辑配置时已选内容未回显的问题
Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 9894087
2026-07-10 14:18:19 +08:00

727 lines
27 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.

"""
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>/copy', methods=['POST'])
def copy_config(config_id):
"""复制配置"""
try:
# 获取原配置
configs = execute_query('SELECT * FROM t_daily_report_config WHERE id = %s', (config_id,))
if not configs:
return jsonify({'success': False, 'message': '原配置不存在'}), 404
original = configs[0]
# 生成新ID和新名称
new_id = int(datetime.now().timestamp() * 1000)
new_name = f"{original['config_name']}_副本"
# 获取最大排序值
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
# 插入新配置
execute_insert(
'''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, %s, %s, NOW(), NOW())''',
(
new_id,
new_name,
original['split_type'],
original['split_value'],
original['split_name'],
original['selected_fields'],
original.get('sum_fields', ''),
original['is_active'],
sort_order
)
)
return jsonify({'success': True, 'message': '配置复制成功', 'id': new_id})
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')
page = int(request.args.get('page', 1))
page_size = int(request.args.get('page_size', 100))
search = request.args.get('search', '')
offset = (page - 1) * page_size
if entity_type == 'company':
# 获取企业列表
if search:
companies = execute_query(
'SELECT id AS company_id, company_name FROM t_company WHERE company_state = "1" AND company_name LIKE %s ORDER BY company_name LIMIT %s OFFSET %s',
(f'%{search}%', page_size, offset)
)
total_result = execute_query(
'SELECT COUNT(*) as total FROM t_company WHERE company_state = "1" AND company_name LIKE %s',
(f'%{search}%',)
)
else:
companies = execute_query(
'SELECT id AS company_id, company_name FROM t_company WHERE company_state = "1" ORDER BY company_name LIMIT %s OFFSET %s',
(page_size, offset)
)
total_result = execute_query(
'SELECT COUNT(*) as total FROM t_company WHERE company_state = "1"'
)
total = total_result[0]['total'] if total_result else 0
return jsonify({'success': True, 'data': companies, 'total': total, 'page': page, 'page_size': page_size})
elif entity_type == 'station':
# 获取场站列表
if search:
stations = execute_query(
'SELECT id AS station_id, station_name FROM t_station WHERE station_name LIKE %s ORDER BY station_name LIMIT %s OFFSET %s',
(f'%{search}%', page_size, offset)
)
total_result = execute_query(
'SELECT COUNT(*) as total FROM t_station WHERE station_name LIKE %s',
(f'%{search}%',)
)
else:
stations = execute_query(
'SELECT id AS station_id, station_name FROM t_station ORDER BY station_name LIMIT %s OFFSET %s',
(page_size, offset)
)
total_result = execute_query(
'SELECT COUNT(*) as total FROM t_station'
)
total = total_result[0]['total'] if total_result else 0
return jsonify({'success': True, 'data': stations, 'total': total, 'page': page, 'page_size': page_size})
else:
# 获取用户列表(支持分页和搜索)
if search:
users = execute_query(
'SELECT id AS user_id, user_name FROM t_user WHERE user_name LIKE %s ORDER BY user_name LIMIT %s OFFSET %s',
(f'%{search}%', page_size, offset)
)
total_result = execute_query(
'SELECT COUNT(*) as total FROM t_user WHERE user_name LIKE %s',
(f'%{search}%',)
)
else:
users = execute_query(
'SELECT id AS user_id, user_name FROM t_user ORDER BY user_name LIMIT %s OFFSET %s',
(page_size, offset)
)
total_result = execute_query(
'SELECT COUNT(*) as total FROM t_user'
)
total = total_result[0]['total'] if total_result else 0
return jsonify({'success': True, 'data': users, 'total': total, 'page': page, 'page_size': page_size})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/entities/by_ids', methods=['POST'])
def get_entities_by_ids():
"""根据ID列表查询实体信息用于编辑回显"""
try:
data = request.json
entity_type = data.get('type', 'company')
ids = data.get('ids', [])
if not ids:
return jsonify({'success': True, 'data': []})
placeholders = ', '.join(['%s'] * len(ids))
if entity_type == 'company':
entities = execute_query(
f'SELECT id AS company_id, company_name FROM t_company WHERE id IN ({placeholders})',
tuple(ids)
)
elif entity_type == 'station':
entities = execute_query(
f'SELECT id AS station_id, station_name FROM t_station WHERE id IN ({placeholders})',
tuple(ids)
)
else:
entities = execute_query(
f'SELECT id AS user_id, user_name FROM t_user WHERE id IN ({placeholders})',
tuple(ids)
)
return jsonify({'success': True, 'data': entities})
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))
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
offset = (page - 1) * page_size
# 构建查询条件
where_clauses = []
params = []
if start_date:
where_clauses.append('DATE(start_time) >= %s')
params.append(start_date)
if end_date:
where_clauses.append('DATE(end_time) <= %s')
params.append(end_date)
where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
# 查询总数
total_result = execute_query(
f'SELECT COUNT(*) as total FROM t_daily_report_history WHERE {where_sql}',
tuple(params)
)
total = total_result[0]['total']
# 查询数据
history = execute_query(
f'SELECT * FROM t_daily_report_history WHERE {where_sql} ORDER BY create_time DESC LIMIT %s OFFSET %s',
tuple(params) + (page_size, offset)
)
# 解析JSON字段并确保file_path正确
for item in history:
item['sum_results'] = json.loads(item['sum_results']) if item['sum_results'] else {}
# 确保file_path是字符串
if item['file_path'] is None:
item['file_path'] = ''
# 调试日志
print(f"[历史记录] ID: {item['id']}, status: {item['status']}, file_path: '{item['file_path']}'")
return jsonify({
'success': True,
'data': {
'list': history,
'total': total,
'page': page,
'page_size': page_size
}
})
except Exception as e:
print(f"[历史记录API] 错误: {e}")
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500
# ==================== 历史记录管理API ====================
@app.route('/api/report/history/<int:history_id>', methods=['DELETE'])
def delete_report_history(history_id):
"""删除单个历史记录"""
try:
# 先查询文件路径
result = execute_query(
'SELECT file_path FROM t_daily_report_history WHERE id = %s',
(history_id,)
)
if result:
file_path = result[0].get('file_path', '')
# 删除文件
if file_path and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', file_path.lstrip('/'))):
os.remove(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', file_path.lstrip('/')))
# 删除数据库记录
execute_update('DELETE FROM t_daily_report_history WHERE id = %s', (history_id,))
return jsonify({'success': True, 'message': '删除成功'})
except Exception as e:
print(f"[删除历史记录] 错误: {e}")
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/report/history/batch-delete', methods=['POST'])
def batch_delete_report_history():
"""批量删除历史记录"""
try:
data = request.get_json()
ids = data.get('ids', [])
if not ids:
return jsonify({'success': False, 'message': '请选择要删除的记录'}), 400
# 查询文件路径
results = execute_query(
f'SELECT file_path FROM t_daily_report_history WHERE id IN ({",".join(["%s"] * len(ids))})',
tuple(ids)
)
# 删除文件
for result in results:
file_path = result.get('file_path', '')
if file_path and os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', file_path.lstrip('/'))):
os.remove(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', file_path.lstrip('/')))
# 删除数据库记录
execute_update(
f'DELETE FROM t_daily_report_history WHERE id IN ({",".join(["%s"] * len(ids))})',
tuple(ids)
)
return jsonify({'success': True, 'message': f'成功删除 {len(ids)} 条记录'})
except Exception as e:
print(f"[批量删除历史记录] 错误: {e}")
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/report/history/<int:history_id>/download', methods=['GET'])
def download_report_by_id(history_id):
"""根据ID下载历史记录"""
try:
# 查询历史记录
result = execute_query(
'SELECT file_path, split_name, report_date FROM t_daily_report_history WHERE id = %s',
(history_id,)
)
if not result:
return jsonify({'success': False, 'message': '记录不存在'}), 404
file_path = result[0]['file_path']
if not file_path:
return jsonify({'success': False, 'message': '文件不存在'}), 404
# 构建完整路径
full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', file_path.lstrip('/'))
if not os.path.exists(full_path):
return jsonify({'success': False, 'message': '文件不存在'}), 404
# 返回文件
directory = os.path.dirname(full_path)
filename = os.path.basename(full_path)
return send_from_directory(directory, filename, as_attachment=True)
except Exception as e:
print(f"[下载历史记录] 错误: {e}")
import traceback
traceback.print_exc()
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')
print(f"[下载] 请求路径: {file_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(os.path.dirname(os.path.abspath(__file__)), 'public', 'reports')
print(f"[下载] 文件目录: {reports_dir}")
print(f"[下载] 文件名: {filename}")
print(f"[下载] 文件是否存在: {os.path.exists(os.path.join(reports_dir, filename))}")
# 列出目录中的所有文件
if os.path.exists(reports_dir):
print(f"[下载] 目录中的文件: {os.listdir(reports_dir)}")
return send_from_directory(reports_dir, filename, as_attachment=True)
except Exception as e:
print(f"[下载] 错误: {str(e)}")
import traceback
traceback.print_exc()
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('DEPLOY_RUN_PORT', os.environ.get('PORT', 5000)))
print(f'启动服务器: http://localhost:{port}')
print(f'数据库: {os.environ.get("DB_HOST", "haoslm2.xicp.net")}:{os.environ.get("DB_PORT", "10216")}')
app.run(host='0.0.0.0', port=port, debug=True)