"""系统管理接口(契约1.23),对齐 Java 版 SystemController。 资源监控(实时/趋势/进程)、存储占用、转换器状态、一键体检、 系统信息、运行参数、运行日志、维护操作(预览缓存清理/强制下线)。 查询类只读;维护操作记审计。全部要求系统管理权限(manage_system), 运行日志并入日志审计权限(manage_audit_log,单开窗口查看原始日志)。 """ from __future__ import annotations from flask import request, send_file from dms.api.v1.blueprint import api_v1 from dms.common.enums import AuditAction, AuditTarget from dms.common.errors import ( InvalidArgumentError, ResourceNotFoundError, UserSelfProtectionError, ) from dms.common.response import serialize_id, success_response from dms.extensions import db from dms.models import User from dms.security.auth_context import get_auth_context from dms.security.decorators import ( audit_log_required, bearer_auth_required, system_management_required, ) from dms.services.audit_service import business_audit from dms.services.system_management_service import ( clear_preview_cache, converter_status, current_metrics, health_check, log_file_path, metric_history, process_metrics, read_log_tail, runtime_config, storage_usage, system_info, ) def _positive_int(value: str | None, default: int, maximum: int) -> int: if value is None or not value.strip(): return default try: number = int(value.strip()) except ValueError as exc: raise InvalidArgumentError("参数必须是整数") from exc if number < 1 or number > maximum: raise InvalidArgumentError(f"参数取值范围为 1~{maximum}") return number @api_v1.get("/system/metrics/current") @bearer_auth_required @system_management_required def system_metrics_current(): return success_response(current_metrics()) @api_v1.get("/system/metrics/history") @bearer_auth_required @system_management_required def system_metrics_history(): hours = _positive_int(request.args.get("hours"), default=24, maximum=720) return success_response(metric_history(hours)) @api_v1.get("/system/process") @bearer_auth_required @system_management_required def system_process(): return success_response(process_metrics()) @api_v1.get("/system/storage-usage") @bearer_auth_required @system_management_required def system_storage_usage(): return success_response({"entries": storage_usage()}) @api_v1.get("/system/converter") @bearer_auth_required @system_management_required def system_converter(): return success_response(converter_status()) @api_v1.get("/system/health") @bearer_auth_required @system_management_required def system_health(): return success_response({"items": health_check()}) @api_v1.get("/system/info") @bearer_auth_required @system_management_required def system_information(): return success_response(system_info()) @api_v1.get("/system/runtime-config") @bearer_auth_required @system_management_required def system_runtime_config(): return success_response({"entries": runtime_config()}) @api_v1.get("/system/logs") @bearer_auth_required @system_management_required def system_logs(): lines = _positive_int(request.args.get("lines"), default=500, maximum=5000) level = (request.args.get("level") or "").strip() or None return success_response(read_log_tail(lines=lines, level=level)) @api_v1.get("/system/logs/download") @bearer_auth_required @audit_log_required def system_logs_download(): """原始运行日志文件下载(日志审计页签单开窗口查看)。""" path = log_file_path() if path is None or not path.is_file(): raise ResourceNotFoundError("运行日志文件未启用或不存在") return send_file( path, mimetype="text/plain", as_attachment=True, download_name=path.name, ) @api_v1.post("/system/maintenance/preview-cache") @bearer_auth_required @system_management_required def system_clear_preview_cache(): """清空预览缓存(可重建),记 PREVIEW_CACHE_CLEAR 审计。""" result = clear_preview_cache() db.session.add( business_audit( action=AuditAction.PREVIEW_CACHE_CLEAR, target=AuditTarget.SYSTEM, target_id=0, target_name="preview", detail=result, ) ) db.session.commit() return success_response(result) @api_v1.post("/system/users//force-offline") @bearer_auth_required @system_management_required def system_force_offline(user_id: str): """强制下线:auth_version + 1 使目标用户全部访问令牌立即失效。""" from dms.services.user_service import parse_string_id numeric_id = parse_string_id(user_id, name="userId") if numeric_id is None: raise InvalidArgumentError("userId不能为空") if get_auth_context().user_id == numeric_id: raise UserSelfProtectionError("不能强制自己下线") user = db.session.get(User, numeric_id) if user is None or user.is_deleted: raise ResourceNotFoundError("用户不存在") user.auth_version += 1 db.session.add( business_audit( action=AuditAction.FORCE_OFFLINE, target=AuditTarget.USER, target_id=user.id, target_name=user.username, detail={"authVersion": user.auth_version}, ) ) db.session.commit() return success_response( { "userId": serialize_id(user.id), "username": user.username, "authVersion": user.auth_version, } )