修改普通用户的数据类型转换在数据集管理看不见的问题

This commit is contained in:
wangjiming
2026-08-18 14:49:12 +08:00
parent 4e27b98a84
commit 04c3c1412c
12 changed files with 1221 additions and 37 deletions

View File

@@ -180,9 +180,22 @@ def filter_accessible_resource_ids(
accessible = {r["resource_id"] for r in rows}
if resource_type in OWNER_TABLES:
table, column = OWNER_TABLES[resource_type]
with store.connect() as conn:
owned = conn.execute(f"SELECT id FROM {table} WHERE {column}=?", (user_id,)).fetchall()
accessible.update(row["id"] for row in owned)
if column == "payload":
# payload 是 JSON 字符串,需要查出后解析 created_by
with store.connect() as conn:
owned = conn.execute(f"SELECT id, {column} FROM {table}").fetchall()
for row in owned:
try:
import json
payload = json.loads(row[column] or "{}")
if payload.get("created_by") == user_id:
accessible.add(row["id"])
except (TypeError, ValueError):
pass
else:
with store.connect() as conn:
owned = conn.execute(f"SELECT id FROM {table} WHERE {column}=?", (user_id,)).fetchall()
accessible.update(row["id"] for row in owned)
return [rid for rid in all_ids if rid in accessible]
@@ -210,8 +223,18 @@ def filter_accessible_resource_ids_batch(
table, column = table_info
with store.connect() as conn:
owned = conn.execute(
f"SELECT id FROM {table} WHERE id IN ({placeholders}) AND {column}=?",
(*resource_ids, user["id"]),
f"SELECT id, {column} FROM {table} WHERE id IN ({placeholders})",
(*resource_ids,),
).fetchall()
accessible.update(row["id"] for row in owned)
for row in owned:
owner = row[column]
# 如果列是 payloadJSON需要解析后提取 created_by
if column == "payload":
try:
import json
owner = json.loads(owner or "{}").get("created_by")
except (TypeError, ValueError):
owner = None
if owner == user["id"]:
accessible.add(row["id"])
return accessible

View File

@@ -4,6 +4,7 @@ from contextvars import ContextVar
from datetime import date, datetime, timedelta
import json
import logging
import sys
from logging import Handler, LogRecord
from pathlib import Path
import re
@@ -338,7 +339,7 @@ def setup_request_logging(app: FastAPI) -> None:
try:
response = await call_next(request)
elapsed_ms = (time.perf_counter() - started_at) * 1000
noisy_paths = ("/health", "/system-info", "/compute/jobs/", "/model-eval/", "/model-compare/")
log_method = logger.debug if request.method == "GET" and response.status_code < 400 else logger.info
if any(request.url.path.endswith(path) or path in request.url.path for path in noisy_paths) and response.status_code < 400:
@@ -353,6 +354,27 @@ def setup_request_logging(app: FastAPI) -> None:
elapsed_ms,
request.client.host if request.client else "-",
)
# 5xx 系统错误自动写入操作日志(未被 @op_log 覆盖的系统级异常)
if response.status_code >= 500:
try:
from app.core.op_log import log_operation, OpModule, OpStatus
log_operation(
module=OpModule.SYSTEM,
action="request",
target_type="api",
target_name=request.url.path,
status=OpStatus.FAILURE,
error_message=f"HTTP {response.status_code} - 系统内部错误",
error_type="HTTPError",
detail=f'{{"method":"{request.method}","path":"{request.url.path}","status":{response.status_code}}}',
func_name="request_logging_middleware",
request=request,
duration_ms=elapsed_ms,
)
except Exception:
pass # 日志写入失败不影响主流程
response.headers["X-Request-ID"] = request_id
return response
except Exception:
@@ -364,6 +386,28 @@ def setup_request_logging(app: FastAPI) -> None:
elapsed_ms,
request.client.host if request.client else "-",
)
# 未被捕获的异常,写入操作日志
try:
import traceback as _tb
from app.core.op_log import log_operation, OpModule, OpStatus
log_operation(
module=OpModule.SYSTEM,
action="request",
target_type="api",
target_name=request.url.path,
status=OpStatus.FAILURE,
error_message=str(sys.exc_info()[1])[:1000] if sys.exc_info()[1] else "未知异常",
error_type=type(sys.exc_info()[1]).__name__ if sys.exc_info()[1] else "UnknownError",
error_traceback="".join(_tb.format_exception(*sys.exc_info()))[:5000],
func_name="request_logging_middleware",
detail=f'{{"method":"{request.method}","path":"{request.url.path}"}}',
request=request,
duration_ms=elapsed_ms,
)
except Exception:
pass # 日志写入失败不影响主流程
raise
finally:
request_id_var.reset(token)

382
backend/app/core/op_log.py Normal file
View File

@@ -0,0 +1,382 @@
"""
操作日志工具模块
提供 @op_log 装饰器和 log_operation 函数,用于记录用户在各业务模块的详细操作。
自动捕获成功/失败状态、完整报错堆栈、操作耗时等。
核心设计:
- 失败操作必须清晰记录完整异常堆栈traceback
- 记录异常类型(如 RuntimeError / ValueError / ConnectionError
- 记录具体出错的函数名和文件位置,方便定位 bug
- 记录 HTTP 状态码,方便区分用户错误(4xx)和系统错误(5xx)
使用示例:
from app.core.op_log import op_log, OpModule, OpAction
@router.post("/inference/start")
@op_log(module=OpModule.INFERENCE, action=OpAction.START, target_type="inference")
async def start_inference(...):
...
"""
from __future__ import annotations
import asyncio
import functools
import json
import time
import traceback
from datetime import datetime, timezone
from typing import Any, Callable, Optional, TypeVar
from fastapi import Request
from app.core.logging import get_logger, request_id_var
from app.db.platform_store import get_platform_store, new_id, utcnow
logger = get_logger("app.op_log")
F = TypeVar("F", bound=Callable[..., Any])
class OpModule:
"""业务模块常量"""
FINE_TUNE = "fine-tune" # 模型训练
MODEL_EVAL = "model-eval" # 模型评测
INFERENCE = "model-inference" # 模型推理
MODEL_MANAGE = "model-manage" # 模型管理
DATASET = "dataset" # 数据集
DATA_PROCESS = "data-process" # 数据处理
DATA_CONVERT = "data-convert" # 数据类型转换
COMPUTE = "compute" # 算力节点
SYSTEM = "system" # 系统
class OpAction:
"""操作动作常量"""
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
START = "start"
STOP = "stop"
UPLOAD = "upload"
DOWNLOAD = "download"
CONVERT = "convert"
MERGE = "merge"
IMPORT = "import"
LOGIN = "login"
LOGOUT = "logout"
PUBLISH = "publish"
RETRY = "retry"
class OpStatus:
"""操作状态常量"""
SUCCESS = "success"
FAILURE = "failure"
def op_log(
module: str,
action: str,
target_type: str = "",
*,
target_name_param: str = "name",
detail_params: Optional[list[str]] = None,
) -> Callable[[F], F]:
"""
操作日志装饰器
自动记录:
- 谁在什么时间操作了什么
- 成功还是失败
- 失败时记录完整异常堆栈(traceback)、异常类型、异常消息
- 出错的函数名和文件位置,方便定位 bug
- 操作耗时(ms)
- 客户端 IP、请求路径
Args:
module: 业务模块OpModule 常量)
action: 操作动作OpAction 常量)
target_type: 资源类型
target_name_param: 从 kwargs 中提取目标名称的参数名
detail_params: 需要记录到 detail 的参数名列表
"""
def decorator(func: F) -> F:
func_name = f"{func.__module__}.{func.__qualname__}"
if asyncio.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
started_at = time.perf_counter()
trace_id = request_id_var.get("-")
user = _extract_user(args, kwargs)
request = _extract_request(args)
target_name = _get_param(kwargs, target_name_param, "")
target_id = _get_param(kwargs, "task_id", "") or _get_param(kwargs, "dataset_id", "") or _get_param(kwargs, "model_id", "")
detail_dict = _build_detail(detail_params, kwargs)
detail_str = json.dumps(detail_dict, ensure_ascii=False) if detail_dict else ""
try:
result = await func(*args, **kwargs)
elapsed_ms = (time.perf_counter() - started_at) * 1000
if not target_id and isinstance(result, dict):
target_id = str(result.get("id", ""))
_write_log(
module=module,
action=action,
target_type=target_type,
target_id=str(target_id) if target_id else None,
target_name=str(target_name) if target_name else None,
status=OpStatus.SUCCESS,
error_message="",
error_type="",
error_traceback="",
func_name=func_name,
detail=detail_str,
user=user,
request=request,
trace_id=trace_id,
duration_ms=elapsed_ms,
)
return result
except Exception as exc:
elapsed_ms = (time.perf_counter() - started_at) * 1000
# 捕获完整异常堆栈
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
full_traceback = "".join(tb_lines)
error_msg = str(exc)[:1000]
error_type = type(exc).__name__
_write_log(
module=module,
action=action,
target_type=target_type,
target_id=str(target_id) if target_id else None,
target_name=str(target_name) if target_name else None,
status=OpStatus.FAILURE,
error_message=error_msg,
error_type=error_type,
error_traceback=full_traceback,
func_name=func_name,
detail=detail_str,
user=user,
request=request,
trace_id=trace_id,
duration_ms=elapsed_ms,
)
raise
return async_wrapper # type: ignore
else:
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
started_at = time.perf_counter()
trace_id = request_id_var.get("-")
user = _extract_user(args, kwargs)
request = _extract_request(args)
target_name = _get_param(kwargs, target_name_param, "")
target_id = _get_param(kwargs, "task_id", "") or _get_param(kwargs, "dataset_id", "") or _get_param(kwargs, "model_id", "")
detail_dict = _build_detail(detail_params, kwargs)
detail_str = json.dumps(detail_dict, ensure_ascii=False) if detail_dict else ""
try:
result = func(*args, **kwargs)
elapsed_ms = (time.perf_counter() - started_at) * 1000
if not target_id and isinstance(result, dict):
target_id = str(result.get("id", ""))
_write_log(
module=module,
action=action,
target_type=target_type,
target_id=str(target_id) if target_id else None,
target_name=str(target_name) if target_name else None,
status=OpStatus.SUCCESS,
error_message="",
error_type="",
error_traceback="",
func_name=func_name,
detail=detail_str,
user=user,
request=request,
trace_id=trace_id,
duration_ms=elapsed_ms,
)
return result
except Exception as exc:
elapsed_ms = (time.perf_counter() - started_at) * 1000
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
full_traceback = "".join(tb_lines)
error_msg = str(exc)[:1000]
error_type = type(exc).__name__
_write_log(
module=module,
action=action,
target_type=target_type,
target_id=str(target_id) if target_id else None,
target_name=str(target_name) if target_name else None,
status=OpStatus.FAILURE,
error_message=error_msg,
error_type=error_type,
error_traceback=full_traceback,
func_name=func_name,
detail=detail_str,
user=user,
request=request,
trace_id=trace_id,
duration_ms=elapsed_ms,
)
raise
return sync_wrapper # type: ignore
return decorator
def log_operation(
*,
module: str,
action: str,
target_type: str = "",
target_id: str = "",
target_name: str = "",
status: str = OpStatus.SUCCESS,
error_message: str = "",
error_type: str = "",
error_traceback: str = "",
detail: str = "",
func_name: str = "",
user: Optional[dict] = None,
request: Optional[Request] = None,
duration_ms: float = 0,
) -> None:
"""手动记录操作日志(不方便用装饰器时使用)"""
trace_id = request_id_var.get("-")
_write_log(
module=module,
action=action,
target_type=target_type,
target_id=target_id or None,
target_name=target_name or None,
status=status,
error_message=error_message,
error_type=error_type,
error_traceback=error_traceback,
func_name=func_name,
detail=detail,
user=user,
request=request,
trace_id=trace_id,
duration_ms=duration_ms,
)
def _build_detail(detail_params: Optional[list[str]], kwargs: dict) -> dict:
"""从 kwargs 中提取需要记录的参数"""
detail_dict = {}
if detail_params:
for p in detail_params:
val = kwargs.get(p)
if val is not None:
detail_dict[p] = str(val)[:200]
return detail_dict
def _extract_user(args: tuple, kwargs: dict) -> Optional[dict]:
"""从函数参数中提取 current_user dict"""
for arg in args:
if isinstance(arg, dict) and "id" in arg and "username" in arg:
return arg
for v in kwargs.values():
if isinstance(v, dict) and "id" in v and "username" in v:
return v
return None
def _extract_request(args: tuple) -> Optional[Request]:
"""从函数参数中提取 Request 对象"""
for arg in args:
if isinstance(arg, Request):
return arg
return None
def _get_param(kwargs: dict, key: str, default: str = "") -> str:
"""安全获取参数值"""
val = kwargs.get(key, default)
if val is None:
return default
return str(val)
def _write_log(
module: str,
action: str,
target_type: str,
target_id: Optional[str],
target_name: Optional[str],
status: str,
error_message: str,
error_type: str,
error_traceback: str,
func_name: str,
detail: str,
user: Optional[dict],
request: Optional[Request],
trace_id: str,
duration_ms: float,
) -> None:
"""写入操作日志到数据库"""
try:
store = get_platform_store()
log_id = new_id("op")
user_id = user.get("id") if user else None
username = user.get("username") if user else None
client_ip = None
req_method = None
req_path = None
if request:
client_ip = request.client.host if request.client else None
req_method = request.method
req_path = request.url.path
with store.connect() as conn:
conn.execute(
"""
INSERT INTO operation_logs
(id, user_id, username, module, action, target_type, target_id,
target_name, status, error_message, error_type, error_traceback,
func_name, detail, client_ip,
request_method, request_path, trace_id, duration_ms, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
log_id, user_id, username, module, action,
target_type or None, target_id, target_name,
status,
error_message[:1000] if error_message else None,
error_type or None,
error_traceback[:5000] if error_traceback else None,
func_name or None,
detail[:2000] if detail else None,
client_ip, req_method, req_path, trace_id,
round(duration_ms, 2), utcnow(),
),
)
except Exception:
logger.error("写入操作日志失败 module=%s action=%s", module, action, exc_info=True)