2026-07-09 11:26:23 +08:00
|
|
|
|
"""
|
2026-07-13 15:43:03 +08:00
|
|
|
|
Flask 主应用
|
2026-07-09 11:26:23 +08:00
|
|
|
|
订单日报系统
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
from datetime import datetime
|
2026-07-21 16:00:46 +08:00
|
|
|
|
from flask import Flask, render_template, Response, request, jsonify, g
|
2026-07-09 11:26:23 +08:00
|
|
|
|
from flask_cors import CORS
|
|
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
|
|
|
|
import pytz
|
|
|
|
|
|
|
2026-07-13 15:43:03 +08:00
|
|
|
|
from lib.db import execute_query
|
2026-07-09 11:26:23 +08:00
|
|
|
|
from lib.report_generator import generate_daily_report
|
2026-07-13 15:43:03 +08:00
|
|
|
|
from lib.api import register_blueprints
|
2026-07-14 14:24:21 +08:00
|
|
|
|
from lib.db_init import init_database_tables
|
2026-07-15 16:01:06 +08:00
|
|
|
|
from lib.logger import log_info, log_error, log_warning
|
2026-07-21 15:21:29 +08:00
|
|
|
|
from lib.api.download_api import cleanup_old_temp_zips
|
2026-07-21 16:00:46 +08:00
|
|
|
|
from lib.api.auth_api import get_current_user
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
2026-07-13 15:43:03 +08:00
|
|
|
|
# 创建 Flask 应用
|
2026-07-09 11:26:23 +08:00
|
|
|
|
app = Flask(__name__,
|
|
|
|
|
|
template_folder='templates',
|
|
|
|
|
|
static_folder='public')
|
|
|
|
|
|
CORS(app)
|
|
|
|
|
|
|
2026-07-13 15:43:03 +08:00
|
|
|
|
# 确保 public/reports 目录存在
|
2026-07-09 11:26:23 +08:00
|
|
|
|
os.makedirs(os.path.join('public', 'reports'), exist_ok=True)
|
|
|
|
|
|
|
2026-07-13 15:43:03 +08:00
|
|
|
|
# 注册 API 蓝图
|
|
|
|
|
|
register_blueprints(app)
|
|
|
|
|
|
|
2026-07-14 14:24:21 +08:00
|
|
|
|
# 应用启动时自动初始化数据库表
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info('正在初始化数据库表...', 'app')
|
2026-07-14 14:24:21 +08:00
|
|
|
|
if init_database_tables():
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info('数据库表初始化成功', 'app')
|
2026-07-14 14:24:21 +08:00
|
|
|
|
else:
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_error('数据库表初始化失败,请检查数据库连接', 'app')
|
2026-07-14 14:24:21 +08:00
|
|
|
|
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
2026-07-21 16:00:46 +08:00
|
|
|
|
# ==================== 登录验证中间件 ====================
|
|
|
|
|
|
|
|
|
|
|
|
# 不需要登录的路径白名单
|
|
|
|
|
|
WHITELIST_PATHS = [
|
|
|
|
|
|
'/api/auth/login',
|
|
|
|
|
|
'/api/auth/info',
|
|
|
|
|
|
'/public/',
|
|
|
|
|
|
'/static/',
|
|
|
|
|
|
'/favicon.ico',
|
|
|
|
|
|
'/'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.before_request
|
|
|
|
|
|
def check_login():
|
|
|
|
|
|
"""全局登录验证"""
|
|
|
|
|
|
path = request.path
|
|
|
|
|
|
|
|
|
|
|
|
# 白名单路径直接放行
|
|
|
|
|
|
for wp in WHITELIST_PATHS:
|
|
|
|
|
|
if path.startswith(wp):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 页面请求(HTML)直接放行,由前端控制登录
|
|
|
|
|
|
if path == '/' or path.endswith('.html'):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 静态文件直接放行
|
|
|
|
|
|
if path.startswith('/public/') or path.startswith('/static/'):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# API 请求需要验证登录
|
|
|
|
|
|
if path.startswith('/api/'):
|
|
|
|
|
|
# 先从 header 取 token,再从 query 参数取(用于下载链接)
|
|
|
|
|
|
token = request.headers.get('Authorization', '')
|
|
|
|
|
|
if token.startswith('Bearer '):
|
|
|
|
|
|
token = token[7:]
|
|
|
|
|
|
|
|
|
|
|
|
if not token:
|
|
|
|
|
|
token = request.args.get('token', '')
|
|
|
|
|
|
|
|
|
|
|
|
user = None
|
|
|
|
|
|
if token:
|
|
|
|
|
|
from lib.api.auth_api import get_user_by_token
|
|
|
|
|
|
user = get_user_by_token(token)
|
|
|
|
|
|
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'message': '未登录或登录已过期',
|
|
|
|
|
|
'code': 401
|
|
|
|
|
|
}), 401
|
|
|
|
|
|
g.current_user = user
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 11:26:23 +08:00
|
|
|
|
# ==================== 页面路由 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
|
|
def index():
|
|
|
|
|
|
"""主页"""
|
|
|
|
|
|
return render_template('index.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-20 16:42:56 +08:00
|
|
|
|
@app.route('/favicon.ico')
|
|
|
|
|
|
def favicon():
|
|
|
|
|
|
"""网站图标(返回空响应避免404)"""
|
|
|
|
|
|
return Response(status=204)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 11:26:23 +08:00
|
|
|
|
# ==================== 定时任务 ====================
|
|
|
|
|
|
|
|
|
|
|
|
def scheduled_job():
|
2026-07-13 15:43:03 +08:00
|
|
|
|
"""定时任务:每天 8:01 自动生成日报"""
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info(f'开始执行定时任务...', 'scheduler')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取所有启用的配置
|
|
|
|
|
|
configs = execute_query(
|
|
|
|
|
|
'SELECT id, config_name FROM t_daily_report_config WHERE is_active = 1'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not configs:
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_warning('没有启用的配置,跳过生成', 'scheduler')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info(f'找到 {len(configs)} 个启用的配置', 'scheduler')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
|
|
|
|
|
# 为每个配置生成日报
|
2026-07-15 16:01:06 +08:00
|
|
|
|
success_count = 0
|
|
|
|
|
|
fail_count = 0
|
2026-07-09 11:26:23 +08:00
|
|
|
|
for config in configs:
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info(f'正在生成:{config["config_name"]} (ID: {config["id"]})', 'scheduler')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
result = generate_daily_report(config['id'])
|
|
|
|
|
|
|
|
|
|
|
|
if result['success']:
|
2026-07-27 14:13:50 +08:00
|
|
|
|
log_info(f' ✓ 成功:{result["total_orders"]} 条订单,总电量:{result.get("total_degree", 0)} kWh', 'scheduler')
|
2026-07-15 16:01:06 +08:00
|
|
|
|
success_count += 1
|
2026-07-09 11:26:23 +08:00
|
|
|
|
else:
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_error(f' ✗ 失败:{result["message"]}', 'scheduler')
|
|
|
|
|
|
fail_count += 1
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info(f'定时任务执行完成,成功 {success_count} 个,失败 {fail_count} 个', 'scheduler')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
except Exception as e:
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_error(f'定时任务执行失败:{e}', 'scheduler')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 启动定时任务
|
|
|
|
|
|
scheduler = BackgroundScheduler(timezone='Asia/Shanghai')
|
|
|
|
|
|
scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1)
|
2026-07-21 15:21:29 +08:00
|
|
|
|
scheduler.add_job(cleanup_old_temp_zips, 'interval', hours=1)
|
2026-07-09 11:26:23 +08:00
|
|
|
|
scheduler.start()
|
2026-07-21 15:21:29 +08:00
|
|
|
|
log_info('定时任务已启动:每天 08:01 执行日报生成,每小时清理临时文件', 'app')
|
2026-07-09 11:26:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 启动应用 ====================
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2026-07-09 15:00:25 +08:00
|
|
|
|
# 支持沙箱环境和本地环境
|
|
|
|
|
|
port = int(os.environ.get('DEPLOY_RUN_PORT', os.environ.get('PORT', 5000)))
|
2026-07-15 16:01:06 +08:00
|
|
|
|
log_info(f'启动服务器:http://localhost:{port}', 'app')
|
|
|
|
|
|
log_info(f'数据库:{os.environ.get("DB_HOST", "haoslm2.xicp.net")}:{os.environ.get("DB_PORT", "10216")}', 'app')
|
2026-07-28 09:49:01 +08:00
|
|
|
|
app.run(host='0.0.0.0', port=port, debug=True)
|