from __future__ import annotations from fastapi import APIRouter, Body, 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 router = APIRouter(prefix="/system", tags=["system"]) @router.post("/audit/visit") def record_visit(payload: dict = Body(...), request: Request = None) -> 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-"):] 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 ""), ) return {"code": 0, "message": "ok", "data": {"recorded": True}} @router.get("/permissions/codes") def permission_codes(current_user: dict = Depends(get_current_user)) -> dict: """返回平台权限码清单(权限码接口)。""" return {"code": 0, "message": "ok", "data": {"codes": ALL_PERMISSIONS}} @router.get("/permissions") def permissions_overview(current_user: dict = Depends(get_current_user)) -> dict: """返回权限码清单与角色定义。""" store = get_platform_store() return { "code": 0, "message": "ok", "data": {"codes": ALL_PERMISSIONS, "roles": store.roles()}, } @router.get("/audit-logs") def audit_logs( tenant_id: str | None = Query(default=None, description="租户 ID"), project_id: str | None = Query(default=None, description="项目 ID"), actor_id: str | None = Query(default=None, description="操作人 ID"), action: str | None = Query(default=None, description="动作类型"), target_type: str | None = Query(default=None, description="目标类型"), start_time: str | None = Query(default=None, description="ISO8601 起始时间"), end_time: str | None = Query(default=None, description="ISO8601 结束时间"), limit: int = Query(default=50, ge=1, le=200), 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() result = store.audit_logs( tenant_id=tenant_id, project_id=project_id, actor_id=actor_id, action=action, target_type=target_type, start_time=start_time, end_time=end_time, limit=limit, offset=offset, ) return {"code": 0, "message": "ok", "data": result} @router.get("/audit-logs/export") def audit_logs_export( tenant_id: str | None = Query(default=None, description="租户 ID"), project_id: str | None = Query(default=None, description="项目 ID"), actor_id: str | None = Query(default=None, description="操作人 ID"), action: str | None = Query(default=None, description="动作类型"), target_type: str | None = Query(default=None, description="目标类型"), start_time: str | None = Query(default=None, description="ISO8601 起始时间"), end_time: str | None = Query(default=None, description="ISO8601 结束时间"), current_user: dict = Depends(get_current_user), ) -> StreamingResponse: """审计日志导出:返回 CSV 流,与应用查询相同的过滤条件。""" if not is_admin(current_user): from app.api.v1.endpoints.platform import fail raise fail(403, "admin permission required") store = get_platform_store() result = store.audit_logs( tenant_id=tenant_id, project_id=project_id, actor_id=actor_id, action=action, target_type=target_type, start_time=start_time, end_time=end_time, limit=10000, 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" def iter_rows(): yield header for row in items: yield ",".join(f'"{str(row.get(c, "") or "")}"' for c in columns) + "\n" return StreamingResponse( iter_rows(), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=audit_logs.csv"}, ) # ===================== 操作日志 ===================== @router.get("/operation-logs") def operation_logs( user_id: str | None = Query(default=None, description="按用户 ID 筛选"), module: str | None = Query(default=None, description="按模块筛选: fine-tune/model-eval/model-inference/dataset/data-convert/model-manage"), action: str | None = Query(default=None, description="按动作筛选: create/start/stop/delete/upload/convert/merge"), status: str | None = Query(default=None, description="按状态筛选: success/failure(不传则查全部)"), keyword: str | None = Query(default=None, description="关键字搜索报错信息(error_message)"), start_time: str | None = Query(default=None, description="ISO8601 起始时间"), end_time: str | None = Query(default=None, description="ISO8601 结束时间"), limit: int = Query(default=50, ge=1, le=200), 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: conditions.append("user_id = %s") params.append(user_id) if module: conditions.append("module = %s") params.append(module) if action: conditions.append("action = %s") params.append(action) if status: conditions.append("status = %s") params.append(status) if keyword: conditions.append("(error_message ILIKE %s OR error_type ILIKE %s)") params.append(f"%{keyword}%") params.append(f"%{keyword}%") if start_time: conditions.append("create_time >= %s") params.append(start_time) if end_time: conditions.append("create_time <= %s") params.append(end_time) where = " WHERE " + " AND ".join(conditions) if conditions else "" with store.connect() as conn: rows = conn.execute( f"SELECT * FROM operation_logs{where} ORDER BY create_time DESC LIMIT %s OFFSET %s", tuple(params + [limit, offset]), ).fetchall() total = conn.execute(f"SELECT COUNT(*) FROM operation_logs{where}", tuple(params)).fetchone()[0] return {"code": 0, "message": "ok", "data": {"items": [dict(r) for r in rows], "total": total}} @router.get("/operation-logs/stats") def operation_logs_stats( start_time: str | None = Query(default=None, description="ISO8601 起始时间"), 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 = [] if start_time: conditions.append("create_time >= %s") params.append(start_time) if end_time: conditions.append("create_time <= %s") params.append(end_time) where = " WHERE " + " AND ".join(conditions) if conditions else "" failure_where = where + " AND status = 'failure'" if where else " WHERE status = 'failure'" with store.connect() as conn: # 总计 row = conn.execute( f"SELECT status, COUNT(*) as cnt FROM operation_logs{where} GROUP BY status", tuple(params) ).fetchall() total_count = 0 success_count = 0 failure_count = 0 for r in row: total_count += r["cnt"] if r["status"] == "success": success_count = r["cnt"] elif r["status"] == "failure": failure_count = r["cnt"] failure_rate = round(failure_count / total_count * 100, 2) if total_count > 0 else 0 # 各模块失败数 module_stats = conn.execute( f"SELECT module, COUNT(*) as cnt FROM operation_logs{failure_where} GROUP BY module ORDER BY cnt DESC", tuple(params), ).fetchall() # 各异常类型分布 error_type_stats = conn.execute( f"SELECT error_type, COUNT(*) as cnt FROM operation_logs{failure_where} AND error_type IS NOT NULL GROUP BY error_type ORDER BY cnt DESC LIMIT 10", tuple(params), ).fetchall() # 最近的 10 条错误 recent_errors = conn.execute( f"SELECT * FROM operation_logs{failure_where} ORDER BY create_time DESC LIMIT 10", tuple(params), ).fetchall() return { "code": 0, "message": "ok", "data": { "total": total_count, "success": success_count, "failure": failure_count, "failure_rate": failure_rate, "module_failures": [{"module": r["module"], "count": r["cnt"]} for r in module_stats], "error_types": [{"type": r["error_type"], "count": r["cnt"]} for r in error_type_stats], "recent_errors": [dict(r) for r in recent_errors], }, } @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() with store.connect() as conn: rows = conn.execute( "SELECT DISTINCT module FROM operation_logs WHERE module IS NOT NULL ORDER BY module" ).fetchall() modules = [{"value": r["module"], "label": r["module"]} for r in rows] return {"code": 0, "message": "ok", "data": modules}