"""操作日志:基于 sys_api_request_log 适配 RuoYi OperLog 视图""" from __future__ import annotations from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import desc, func, or_ from sqlalchemy.orm import Session from app.core.oper_log_infer import business_type_from_path, title_from_path from app.models.models import SysApiRequestLog, SysUser _FMT = "%Y-%m-%d %H:%M:%S" def _oper_status(status_code: int) -> int: return 0 if 200 <= int(status_code or 0) < 400 else 1 def _row_to_oper_log(row: Any) -> Dict[str, Any]: path = getattr(row, "path", "") or "" http_method = getattr(row, "http_method", "") or "" status_code = int(getattr(row, "status_code", 0) or 0) create_at = getattr(row, "create_at", None) oper_time = create_at.strftime(_FMT) if isinstance(create_at, datetime) else "" oper_name = getattr(row, "create_name", None) or getattr(row, "username", None) or "" request_body = getattr(row, "request_body", None) or "" response_body = getattr(row, "response_body", None) or "" stored_title = getattr(row, "business_title", None) stored_type = getattr(row, "business_type", None) title = stored_title or title_from_path(path) business_type = stored_type if stored_type is not None else business_type_from_path(path, http_method) oper_summary = getattr(row, "oper_summary", None) or "" return { "id": getattr(row, "id", ""), "title": title, "businessType": business_type, "businessId": getattr(row, "business_id", None) or "", "operSummary": oper_summary, "method": http_method, "operName": oper_name, "operUrl": path, "operIp": getattr(row, "ip_address", "") or "", "operLocation": "", "operParam": request_body, "jsonResult": response_body, "status": _oper_status(status_code), "costTime": int(getattr(row, "duration_ms", 0) or 0), "operTime": oper_time, "errorMsg": "" if _oper_status(status_code) == 0 else f"HTTP {status_code}", } def _apply_oper_log_scope(q, db: Session, user_info: dict, tenant_no: str): """操作日志按 oper_user_id(user_id/create_by)应用数据权限。""" from app.core.config import settings from app.core.data_scope import _collect_scope_ids, _resolve_user_role_scopes if not user_info or user_info.get("role") == "super_admin": return q user_id = user_info.get("user_id") if not user_id: return q.filter(False) effective_tenant = tenant_no or user_info.get("tenant_no") or settings.DEFAULT_TENANT_NO role_scopes = _resolve_user_role_scopes(db, user_id, effective_tenant) if not role_scopes: return q.filter(False) if any(rs["data_scope"] == "1" for rs in role_scopes): return q dept_ids, user_ids = _collect_scope_ids( db, user_info, user_id, effective_tenant, role_scopes ) conditions = [] if user_ids: conditions.append( or_( SysApiRequestLog.user_id.in_(user_ids), SysApiRequestLog.create_by.in_(user_ids), ) ) if dept_ids: dept_user_ids = ( db.query(SysUser.id) .filter( SysUser.dept_id.in_(dept_ids), SysUser.del_flag == 0, SysUser.tenant_no == effective_tenant, ) ) conditions.append(SysApiRequestLog.user_id.in_(dept_user_ids)) if not conditions: return q.filter(False) return q.filter(or_(*conditions)) def _oper_log_row_in_scope(db: Session, row, user_info: dict, tenant_no: str) -> bool: from app.core.data_scope import row_in_data_scope class _OperLogScopeRow: create_by = getattr(row, "user_id", None) or getattr(row, "create_by", None) create_dept_id = None return row_in_data_scope(db, _OperLogScopeRow(), user_info, tenant_no) def list_oper_logs( db: Session, *, page: int = 1, page_size: int = 10, title: Optional[str] = None, business_type: Optional[int] = None, oper_name: Optional[str] = None, status: Optional[int] = None, business_id: Optional[str] = None, business_title: Optional[str] = None, ids: Optional[List[str]] = None, max_page_size: Optional[int] = None, user_info: Optional[dict] = None, tenant_no: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], int]: q = db.query(SysApiRequestLog).filter(SysApiRequestLog.is_del == 0) effective_tenant = tenant_no or (user_info or {}).get("tenant_no") if effective_tenant: q = q.filter(SysApiRequestLog.tenant_no == effective_tenant) if user_info: q = _apply_oper_log_scope(q, db, user_info, effective_tenant or "") if business_id: q = q.filter(SysApiRequestLog.business_id == business_id.strip()) if business_title: kw = f"%{business_title.strip()}%" q = q.filter(SysApiRequestLog.business_title.like(kw)) if title: title_kw = f"%{title.strip()}%" q = q.filter(SysApiRequestLog.business_title.like(title_kw)) if business_type is not None: q = q.filter(SysApiRequestLog.business_type == int(business_type)) if oper_name: kw = f"%{oper_name.strip()}%" q = q.filter( or_( SysApiRequestLog.username.like(kw), SysApiRequestLog.create_name.like(kw), ) ) if status is not None: if int(status) == 0: q = q.filter(SysApiRequestLog.status_code >= 200, SysApiRequestLog.status_code < 400) else: q = q.filter( or_( SysApiRequestLog.status_code < 200, SysApiRequestLog.status_code >= 400, ) ) if ids: q = q.filter(SysApiRequestLog.id.in_(ids)) total = int(q.with_entities(func.count(SysApiRequestLog.id)).order_by(None).scalar() or 0) page = max(1, page) cap = int(max_page_size) if max_page_size else 500 page_size = min(max(1, page_size), cap) start = (page - 1) * page_size rows = ( q.order_by(desc(SysApiRequestLog.create_at)) .offset(start) .limit(page_size) .all() ) return [_row_to_oper_log(r) for r in rows], total def get_oper_log( db: Session, log_id: str, *, user_info: Optional[dict] = None, tenant_no: Optional[str] = None ) -> Optional[Dict[str, Any]]: row = ( db.query(SysApiRequestLog) .filter(SysApiRequestLog.id == log_id, SysApiRequestLog.is_del == 0) .first() ) if not row: return None if user_info and not _oper_log_row_in_scope(db, row, user_info, tenant_no): return None if tenant_no and row.tenant_no and row.tenant_no != tenant_no: return None return _row_to_oper_log(row)