Files
ylt_diy/app.py
user9994793890 a1431e421d feat: 配置管理增加复制功能,修复企业加载问题
## 修复问题

### 1. 企业加载不出来
**问题**:`Unknown column 'company_id' in 'table list'`
**原因**:`t_company` 表中字段名是 `id` 而不是 `company_id`
**修复**:修改 `/api/entities` API 中的SQL查询,使用正确的字段名 `id`

## 新增功能

### 2. 配置复制功能
- 在配置列表中每个配置添加"复制"按钮
- 点击复制按钮,复制整个配置(包括所有字段设置)
- 新配置名称自动添加"_副本"后缀
- 后端API:`POST /api/config/<id>/copy`

## 功能说明

### 复制配置
1. 在配置管理标签页
2. 找到要复制的配置
3. 点击"复制"按钮
4. 系统会创建一个新配置,名称为"原名称_副本"
5. 新配置包含原配置的所有设置(拆分方式、字段、求和等)

### 企业加载
现在企业列表可以正常加载了,显示企业ID和名称。

## 重启应用测试

```bash
# Ctrl+C 停止
python app.py
```

然后刷新页面,测试:
1. 企业列表是否正常显示
2. 配置复制功能是否正常工作

Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 9894087
2026-07-10 12:44:30 +08:00

628 lines
23 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]
# 生成新配置名称
new_name = f"{original['config_name']}_副本"
# 插入新配置
new_id = execute_insert(
'''INSERT INTO t_daily_report_config
(config_name, split_type, split_value, split_name, selected_fields, sum_fields, is_active, sort_order)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)''',
(
new_name,
original['split_type'],
original['split_value'],
original['split_name'],
original['selected_fields'],
original.get('sum_fields', ''),
original['is_active'],
original.get('sort_order', 0)
)
)
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')
if entity_type == 'company':
# 获取企业列表
companies = execute_query(
'SELECT id AS company_id, company_name FROM t_company WHERE company_state = "1" 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))
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)