""" Flask 主应用 订单日报系统 """ import os from datetime import datetime from flask import Flask, render_template, Response, request, jsonify, g from flask_cors import CORS from apscheduler.schedulers.background import BackgroundScheduler import pytz from lib.db import execute_query from lib.report_generator import generate_daily_report from lib.api import register_blueprints from lib.db_init import init_database_tables from lib.logger import log_info, log_error, log_warning from lib.api.download_api import cleanup_old_temp_zips from lib.api.auth_api import get_current_user # 创建 Flask 应用 app = Flask(__name__, template_folder='templates', static_folder='public') CORS(app) # 确保 public/reports 目录存在 os.makedirs(os.path.join('public', 'reports'), exist_ok=True) # 注册 API 蓝图 register_blueprints(app) # 应用启动时自动初始化数据库表 log_info('正在初始化数据库表...', 'app') if init_database_tables(): log_info('数据库表初始化成功', 'app') else: log_error('数据库表初始化失败,请检查数据库连接', 'app') # ==================== 登录验证中间件 ==================== # 不需要登录的路径白名单 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 # ==================== 页面路由 ==================== @app.route('/') def index(): """主页""" return render_template('index.html') @app.route('/favicon.ico') def favicon(): """网站图标(返回空响应避免404)""" return Response(status=204) # ==================== 定时任务 ==================== def scheduled_job(): """定时任务:每天 8:01 自动生成日报""" log_info(f'开始执行定时任务...', 'scheduler') try: # 获取所有启用的配置 configs = execute_query( 'SELECT id, config_name FROM t_daily_report_config WHERE is_active = 1' ) if not configs: log_warning('没有启用的配置,跳过生成', 'scheduler') return log_info(f'找到 {len(configs)} 个启用的配置', 'scheduler') # 为每个配置生成日报 success_count = 0 fail_count = 0 for config in configs: log_info(f'正在生成:{config["config_name"]} (ID: {config["id"]})', 'scheduler') result = generate_daily_report(config['id']) if result['success']: log_info(f' ✓ 成功:{result["total_orders"]} 条订单,金额:{result["total_amount"]}', 'scheduler') success_count += 1 else: log_error(f' ✗ 失败:{result["message"]}', 'scheduler') fail_count += 1 log_info(f'定时任务执行完成,成功 {success_count} 个,失败 {fail_count} 个', 'scheduler') except Exception as e: log_error(f'定时任务执行失败:{e}', 'scheduler') # 启动定时任务 scheduler = BackgroundScheduler(timezone='Asia/Shanghai') scheduler.add_job(scheduled_job, 'cron', hour=8, minute=1) scheduler.add_job(cleanup_old_temp_zips, 'interval', hours=1) scheduler.start() log_info('定时任务已启动:每天 08:01 执行日报生成,每小时清理临时文件', 'app') # ==================== 启动应用 ==================== if __name__ == '__main__': # 支持沙箱环境和本地环境 port = int(os.environ.get('DEPLOY_RUN_PORT', os.environ.get('PORT', 5000))) 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') app.run(host='0.0.0.0', port=port, debug=True)