feat: 平台治理与权限体系完善,存储进度/GPU预留/审批中心与日志整合

- 平台治理: 租户用户权限层次、资源ACL、审批中心与审批模板、访问申请
- 存储: MinIO 存储进度迁移、对象存储安全加固与测试
- 计算: GPU 资源预留、compute 轮询与同步增强
- 权限: permission v2 迁移、权限安全验收测试
- 日志: 后端运行日志中文说明、操作日志整合
- 数据处理/评测: 数据转换与模型评测优化

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wuyongtao
2026-08-21 09:49:48 +08:00
parent 080ef6ab00
commit 6f0e82f351
94 changed files with 9547 additions and 1045 deletions

View File

@@ -1,33 +1,56 @@
from __future__ import annotations
from fastapi import APIRouter, Body, Query, Request, Depends
import csv
import io
from fastapi import APIRouter, Body, HTTPException, Query, Request, Depends
from fastapi.responses import StreamingResponse
from app.db.platform_store import ALL_PERMISSIONS, get_platform_store
from app.core.auth import get_current_user, is_admin
from app.core.logging import get_client_ip
router = APIRouter(prefix="/system", tags=["system"])
_VISIT_MODULES = {
"dashboard",
"fine-tune",
"model-eval",
"model-inference",
"model-manage",
"dataset",
"data-process",
"data-convert",
}
@router.post("/audit/visit")
def record_visit(payload: dict = Body(...), request: Request = None) -> dict:
def record_visit(
payload: dict = Body(...),
request: Request = None,
current_user: dict = Depends(get_current_user),
) -> dict:
"""记录用户访问业务模块的行为,用于看板用户操作分布统计。"""
action = str(payload.get("action") or payload.get("module") or "").strip()
if not action:
return {"code": 0, "message": "ok", "data": {"recorded": False}}
actor_id = ""
if request is not None:
auth = request.headers.get("Authorization", "")
token = auth.replace("Bearer ", "").strip()
if token.startswith("platform-token-"):
actor_id = token[len("platform-token-"):]
# Visit statistics are intentionally limited to known module names. This
# endpoint must not become a free-form audit-log injection point.
if action not in _VISIT_MODULES:
return {"code": 0, "message": "ok", "data": {"recorded": False}}
actor_id = current_user.get("id")
get_platform_store().record_audit(
action=action,
actor_id=actor_id or None,
target_type="module",
target_id=action,
detail=str(payload.get("detail") or ""),
tenant_id=str(current_user.get("tenant_id") or "") or None,
session_id=str(current_user.get("session_id") or "") or None,
request_id=(request.headers.get("X-Request-ID") if request else None),
detail="module visit",
metadata={"source": "frontend", "detail_length": len(str(payload.get("detail") or ""))},
ip=get_client_ip(request) or None,
)
return {"code": 0, "message": "ok", "data": {"recorded": True}}
@@ -117,13 +140,22 @@ def audit_logs_export(
offset=0,
)
items = result["items"]
columns = ["time", "tenant_id", "project_id", "actor_id", "action", "target_type", "target_id", "detail", "client_ip"]
header = ",".join(columns) + "\n"
columns = [
"time", "tenant_id", "project_id", "actor_id", "action", "target_type",
"target_id", "detail", "client_ip", "result", "reason", "request_id",
"session_id", "metadata",
]
def iter_rows():
yield header
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerow(columns)
yield buffer.getvalue()
for row in items:
yield ",".join(f'"{str(row.get(c, "") or "")}"' for c in columns) + "\n"
buffer.seek(0)
buffer.truncate(0)
writer.writerow([row.get(c, "") or "" for c in columns])
yield buffer.getvalue()
return StreamingResponse(
iter_rows(),
@@ -134,6 +166,16 @@ def audit_logs_export(
# ===================== 操作日志 =====================
def _operation_log_scope(current_user: dict, conditions: list[str], params: list) -> None:
"""校验操作日志权限,并为普通用户追加本人范围。"""
if is_admin(current_user):
return
if "logs" not in (current_user.get("permissions") or []):
raise HTTPException(status_code=403, detail="missing permission: logs")
conditions.append("user_id = %s")
params.append(str(current_user.get("id") or ""))
@router.get("/operation-logs")
def operation_logs(
user_id: str | None = Query(default=None, description="按用户 ID 筛选"),
@@ -147,14 +189,12 @@ def operation_logs(
offset: int = Query(default=0, ge=0),
current_user: dict = Depends(get_current_user),
) -> dict:
"""操作日志查询:按用户/模块/动作/状态/关键字/时间范围分页过滤"""
if not is_admin(current_user):
from app.api.v1.endpoints.platform import fail
raise fail(403, "admin permission required")
"""操作日志查询:管理员查全量,普通用户只能查本人记录"""
store = get_platform_store()
conditions = []
params: list = []
if user_id:
_operation_log_scope(current_user, conditions, params)
if user_id and is_admin(current_user):
conditions.append("user_id = %s")
params.append(user_id)
if module:
@@ -192,13 +232,11 @@ def operation_logs_stats(
end_time: str | None = Query(default=None, description="ISO8601 结束时间"),
current_user: dict = Depends(get_current_user),
) -> dict:
"""操作日志统计:总操作数、成功数、失败数、失败率、各模块失败分布、最近错误列表"""
if not is_admin(current_user):
from app.api.v1.endpoints.platform import fail
raise fail(403, "admin permission required")
"""操作日志统计:管理员统计全量,普通用户统计本人记录"""
store = get_platform_store()
conditions = []
params: list = []
_operation_log_scope(current_user, conditions, params)
if start_time:
conditions.append("create_time >= %s")
params.append(start_time)
@@ -260,13 +298,17 @@ def operation_logs_stats(
@router.get("/operation-logs/modules")
def operation_log_modules(current_user: dict = Depends(get_current_user)) -> dict:
"""返回操作日志中出现的模块列表(用于筛选下拉框)。"""
if not is_admin(current_user):
from app.api.v1.endpoints.platform import fail
raise fail(403, "admin permission required")
store = get_platform_store()
conditions: list[str] = []
params: list = []
_operation_log_scope(current_user, conditions, params)
where = " WHERE " + " AND ".join(conditions) if conditions else ""
with store.connect() as conn:
rows = conn.execute(
"SELECT DISTINCT module FROM operation_logs WHERE module IS NOT NULL ORDER BY module"
f"SELECT DISTINCT module FROM operation_logs{where}"
+ (" AND" if where else " WHERE")
+ " module IS NOT NULL ORDER BY module",
tuple(params),
).fetchall()
modules = [{"value": r["module"], "label": r["module"]} for r in rows]
return {"code": 0, "message": "ok", "data": modules}