249 lines
11 KiB
Python
249 lines
11 KiB
Python
#!/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": "系统配置已更新"})
|