#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import annotations import database from app_logger import get_logger from app_security import is_safe_outbound_url, validate_email from flask import jsonify, request from routes.admin_api import admin_api_bp from routes.decorators import admin_required from services.browse_types import BROWSE_TYPE_SHOULD_READ, validate_browse_type from services.tasks import get_task_scheduler logger = get_logger("app") @admin_api_bp.route("/system/config", methods=["GET"]) @admin_required def get_system_config_api(): """获取系统配置""" return jsonify(database.get_system_config()) @admin_api_bp.route("/system/config", methods=["POST"]) @admin_required def update_system_config_api(): """更新系统配置""" data = request.json or {} max_concurrent = data.get("max_concurrent_global") schedule_enabled = data.get("schedule_enabled") schedule_time = data.get("schedule_time") schedule_browse_type = data.get("schedule_browse_type") schedule_weekdays = data.get("schedule_weekdays") new_max_concurrent_per_account = data.get("max_concurrent_per_account") new_max_screenshot_concurrent = data.get("max_screenshot_concurrent") db_slow_query_ms = data.get("db_slow_query_ms") enable_screenshot = data.get("enable_screenshot") auto_approve_enabled = data.get("auto_approve_enabled") auto_approve_hourly_limit = data.get("auto_approve_hourly_limit") auto_approve_vip_days = data.get("auto_approve_vip_days") kdocs_enabled = data.get("kdocs_enabled") kdocs_doc_url = data.get("kdocs_doc_url") kdocs_default_unit = data.get("kdocs_default_unit") kdocs_sheet_name = data.get("kdocs_sheet_name") kdocs_sheet_index = data.get("kdocs_sheet_index") kdocs_unit_column = data.get("kdocs_unit_column") kdocs_image_column = data.get("kdocs_image_column") kdocs_admin_notify_enabled = data.get("kdocs_admin_notify_enabled") kdocs_admin_notify_email = data.get("kdocs_admin_notify_email") kdocs_row_start = data.get("kdocs_row_start") kdocs_row_end = data.get("kdocs_row_end") if max_concurrent is not None: if not isinstance(max_concurrent, int) or max_concurrent < 1: return jsonify({"error": "全局并发数必须大于0(建议:小型服务器2-5,中型5-10,大型10-20)"}), 400 if new_max_concurrent_per_account is not None: if not isinstance(new_max_concurrent_per_account, int) or new_max_concurrent_per_account < 1: return jsonify({"error": "单账号并发数必须大于0(建议设为1,避免同一用户任务相互影响)"}), 400 if new_max_screenshot_concurrent is not None: if not isinstance(new_max_screenshot_concurrent, int) or new_max_screenshot_concurrent < 1: return jsonify({"error": "截图并发数必须大于0(建议根据服务器配置设置,wkhtmltoimage 资源占用较低)"}), 400 if db_slow_query_ms is not None: try: db_slow_query_ms = int(db_slow_query_ms) except (ValueError, TypeError): return jsonify({"error": "慢 SQL 阈值必须是数字(毫秒)"}), 400 if db_slow_query_ms < 0 or db_slow_query_ms > 60000: return jsonify({"error": "慢 SQL 阈值范围应在 0-60000 毫秒之间"}), 400 if enable_screenshot is not None: if isinstance(enable_screenshot, bool): enable_screenshot = 1 if enable_screenshot else 0 if enable_screenshot not in (0, 1): return jsonify({"error": "截图开关必须是0或1"}), 400 if schedule_time is not None: import re if not re.match(r"^([01]\d|2[0-3]):([0-5]\d)$", schedule_time): return jsonify({"error": "时间格式错误,应为 HH:MM"}), 400 if schedule_browse_type is not None: normalized = validate_browse_type(schedule_browse_type, default=BROWSE_TYPE_SHOULD_READ) if not normalized: return jsonify({"error": "浏览类型无效"}), 400 schedule_browse_type = normalized if schedule_weekdays is not None: try: days = [int(d.strip()) for d in schedule_weekdays.split(",") if d.strip()] if not all(1 <= d <= 7 for d in days): return jsonify({"error": "星期数字必须在1-7之间"}), 400 except (ValueError, AttributeError): return jsonify({"error": "星期格式错误"}), 400 if auto_approve_hourly_limit is not None: if not isinstance(auto_approve_hourly_limit, int) or auto_approve_hourly_limit < 1: return jsonify({"error": "每小时注册限制必须大于0"}), 400 if auto_approve_vip_days is not None: if not isinstance(auto_approve_vip_days, int) or auto_approve_vip_days < 0: return jsonify({"error": "注册赠送VIP天数不能为负数"}), 400 if kdocs_enabled is not None: if isinstance(kdocs_enabled, bool): kdocs_enabled = 1 if kdocs_enabled else 0 if kdocs_enabled not in (0, 1): return jsonify({"error": "表格上传开关必须是0或1"}), 400 if kdocs_doc_url is not None: kdocs_doc_url = str(kdocs_doc_url or "").strip() if kdocs_doc_url and not is_safe_outbound_url(kdocs_doc_url): return jsonify({"error": "文档链接格式不正确"}), 400 if kdocs_default_unit is not None: kdocs_default_unit = str(kdocs_default_unit or "").strip() if len(kdocs_default_unit) > 50: return jsonify({"error": "默认县区长度不能超过50"}), 400 if kdocs_sheet_name is not None: kdocs_sheet_name = str(kdocs_sheet_name or "").strip() if len(kdocs_sheet_name) > 50: return jsonify({"error": "Sheet名称长度不能超过50"}), 400 if kdocs_sheet_index is not None: try: kdocs_sheet_index = int(kdocs_sheet_index) except Exception: return jsonify({"error": "Sheet序号必须是数字"}), 400 if kdocs_sheet_index < 0: return jsonify({"error": "Sheet序号不能为负数"}), 400 if kdocs_unit_column is not None: kdocs_unit_column = str(kdocs_unit_column or "").strip().upper() if not kdocs_unit_column: return jsonify({"error": "县区列不能为空"}), 400 import re if not re.match(r"^[A-Z]{1,3}$", kdocs_unit_column): return jsonify({"error": "县区列格式错误"}), 400 if kdocs_image_column is not None: kdocs_image_column = str(kdocs_image_column or "").strip().upper() if not kdocs_image_column: return jsonify({"error": "图片列不能为空"}), 400 import re if not re.match(r"^[A-Z]{1,3}$", kdocs_image_column): return jsonify({"error": "图片列格式错误"}), 400 if kdocs_admin_notify_enabled is not None: if isinstance(kdocs_admin_notify_enabled, bool): kdocs_admin_notify_enabled = 1 if kdocs_admin_notify_enabled else 0 if kdocs_admin_notify_enabled not in (0, 1): return jsonify({"error": "管理员通知开关必须是0或1"}), 400 if kdocs_admin_notify_email is not None: kdocs_admin_notify_email = str(kdocs_admin_notify_email or "").strip() if kdocs_admin_notify_email: is_valid, error_msg = validate_email(kdocs_admin_notify_email) if not is_valid: return jsonify({"error": error_msg}), 400 if kdocs_row_start is not None: try: kdocs_row_start = int(kdocs_row_start) except (ValueError, TypeError): return jsonify({"error": "起始行必须是数字"}), 400 if kdocs_row_start < 0: return jsonify({"error": "起始行不能为负数"}), 400 if kdocs_row_end is not None: try: kdocs_row_end = int(kdocs_row_end) except (ValueError, TypeError): return jsonify({"error": "结束行必须是数字"}), 400 if kdocs_row_end < 0: return jsonify({"error": "结束行不能为负数"}), 400 old_config = database.get_system_config() or {} if not database.update_system_config( max_concurrent=max_concurrent, schedule_enabled=schedule_enabled, schedule_time=schedule_time, schedule_browse_type=schedule_browse_type, schedule_weekdays=schedule_weekdays, max_concurrent_per_account=new_max_concurrent_per_account, max_screenshot_concurrent=new_max_screenshot_concurrent, enable_screenshot=enable_screenshot, auto_approve_enabled=auto_approve_enabled, auto_approve_hourly_limit=auto_approve_hourly_limit, auto_approve_vip_days=auto_approve_vip_days, kdocs_enabled=kdocs_enabled, kdocs_doc_url=kdocs_doc_url, kdocs_default_unit=kdocs_default_unit, kdocs_sheet_name=kdocs_sheet_name, kdocs_sheet_index=kdocs_sheet_index, kdocs_unit_column=kdocs_unit_column, kdocs_image_column=kdocs_image_column, kdocs_admin_notify_enabled=kdocs_admin_notify_enabled, kdocs_admin_notify_email=kdocs_admin_notify_email, kdocs_row_start=kdocs_row_start, kdocs_row_end=kdocs_row_end, db_slow_query_ms=db_slow_query_ms, ): return jsonify({"error": "更新失败"}), 400 try: new_config = database.get_system_config() or {} scheduler = get_task_scheduler() scheduler.update_limits( max_global=int(new_config.get("max_concurrent_global", old_config.get("max_concurrent_global", 2))), max_per_user=int(new_config.get("max_concurrent_per_account", old_config.get("max_concurrent_per_account", 1))), ) try: import db_pool db_pool.configure_slow_query_runtime(threshold_ms=new_config.get("db_slow_query_ms")) except Exception as slow_sql_error: logger.warning(f"慢 SQL 运行时阈值更新失败: {slow_sql_error}") if new_max_screenshot_concurrent is not None: try: from browser_pool_worker import resize_browser_worker_pool if resize_browser_worker_pool(int(new_config.get("max_screenshot_concurrent", new_max_screenshot_concurrent))): logger.info(f"截图线程池并发已更新为: {new_config.get('max_screenshot_concurrent')}") except Exception as pool_error: logger.warning(f"截图线程池并发更新失败: {pool_error}") except Exception: pass if max_concurrent is not None and max_concurrent != old_config.get("max_concurrent_global"): logger.info(f"全局并发数已更新为: {max_concurrent}") if new_max_concurrent_per_account is not None and new_max_concurrent_per_account != old_config.get("max_concurrent_per_account"): logger.info(f"单用户并发数已更新为: {new_max_concurrent_per_account}") if new_max_screenshot_concurrent is not None: logger.info(f"截图并发数已更新为: {new_max_screenshot_concurrent}") if db_slow_query_ms is not None: logger.info(f"慢 SQL 阈值已更新为: {db_slow_query_ms}ms") return jsonify({"message": "系统配置已更新"})