SpringSunYY vor 4 Monaten
Ursprung
Commit
ae28efd18e

+ 15 - 2
ruoyi_common/base/model.py

@@ -539,10 +539,15 @@ class OrderModel(VoModel):
     
     order_by_column: Annotated[Optional[List[str]],Field(default=None)]
     
-    is_asc: Annotated[Literal["asc", "desc"],Field(default="asc")]
+    is_asc: Annotated[
+        Literal["asc", "desc", "ascending", "descending"],
+        Field(default="asc")
+    ]
     
     @field_validator("order_by_column",mode="before")
-    def order_by_column_before_validation(cls, value:str, info:ValidationInfo) -> List[str]:
+    def order_by_column_before_validation(cls, value:str | None, info:ValidationInfo) -> Optional[List[str]]:
+        if value is None:
+            return None
         value = value.split(",")
         if info.context and isinstance(info.context,VoValidatorContext):
             for val in value:
@@ -550,6 +555,14 @@ class OrderModel(VoModel):
                     raise ValueError(f"排序字段{val},在禁止的模型字段范围内")
         return value
     
+    @field_validator("is_asc", mode="after")
+    def normalize_is_asc(cls, value:str) -> str:
+        if value == "ascending":
+            return "asc"
+        if value == "descending":
+            return "desc"
+        return value
+    
 
 class ExtraModel(VoModel):
     

+ 21 - 9
ruoyi_common/utils/base.py

@@ -470,13 +470,18 @@ class IpUtil:
         Returns:
             str: ip
         '''
+        forwarded_for = request.headers.get('X-Forwarded-For')
         ip = None
-        if 'HTTP_X_FORWARDED_FOR' in request.headers:
-            ip = request.headers['HTTP_X_FORWARDED_FOR']
-        elif 'REMOTE_ADDR' in request.headers:
-            ip = request.headers['REMOTE_ADDR']
-        ip, _ = request.host.rsplit(':', 1)
-        ip = "127.0.0.1" if ip == "localhost" else ip
+        if forwarded_for:
+            ip = forwarded_for.split(',')[0].strip()
+        if not ip:
+            ip = request.headers.get('X-Real-IP')
+        if not ip:
+            ip = request.remote_addr
+        if not ip or ip == '::1':
+            ip = '127.0.0.1'
+        if ip == "localhost":
+            ip = "127.0.0.1"
         return ip
 
     @classmethod
@@ -515,9 +520,16 @@ class AddressUtil:
         Returns:
             str: 地址
         '''
-        # todo
-        address = None
-        return address
+        if not ip:
+            return "未知"
+        try:
+            parsed_ip = ipaddress.ip_address(ip)
+            if parsed_ip.is_loopback or parsed_ip.is_private:
+                return "内网IP"
+        except ValueError:
+            return "未知"
+        # TODO: 可在此集成第三方IP定位服务
+        return "未知"
 
 
 class UserAgentUtil:

+ 43 - 24
ruoyi_framework/descriptor/log.py

@@ -10,7 +10,7 @@ from pydantic import BaseModel
 
 from ruoyi_common.domain.entity import LoginUser
 from ruoyi_common.domain.enum import BusinessStatus, BusinessType,OperatorType
-from ruoyi_common.utils import IpUtil
+from ruoyi_common.utils import AddressUtil, IpUtil
 from ruoyi_common.utils import security_util as SecurityUtil
 from ruoyi_common.base.signal import log_signal
 from ruoyi_common.utils.base import DescriptUtil
@@ -24,7 +24,7 @@ class Log:
     def __init__(self, 
         title:str, 
         business_type:BusinessType=BusinessType.OTHER,
-        operator_type:BusinessType=OperatorType.MANAGE, 
+        operator_type:OperatorType=OperatorType.MANAGE, 
         is_save_request_data:bool=True, 
         is_save_response_data:bool=True
     ):
@@ -33,11 +33,7 @@ class Log:
         self.operator_type = operator_type
         self.is_save_request_data = is_save_request_data
         self.is_save_response_data = is_save_response_data
-
-        self._oper_log = SysOperLog()
-        self._oper_log.title = self.title
-        self._oper_log.business_type = self.business_type.value
-        self._oper_log.operator_type = self.operator_type.value
+        self._oper_log: SysOperLog | None = None
         
     def __call__(self, func) -> Callable:
         
@@ -60,6 +56,7 @@ class Log:
             sender: 信号发送者
             **kwargs: 信号消息
         '''
+        self._oper_log = self._create_oper_log()
         message = kwargs.get('message')
         self.handle_request(sender)
         self.handle_login_user()
@@ -69,6 +66,13 @@ class Log:
             self.handle_response(message)
         TaskManager.execute(record_operlog,self._oper_log)
     
+    def _create_oper_log(self) -> SysOperLog:
+        oper_log = SysOperLog()
+        oper_log.title = self.title
+        oper_log.business_type = self.business_type.value
+        oper_log.operator_type = self.operator_type.value
+        return oper_log
+    
     def handle_exception(self, e:Exception):
         '''
         处理异常
@@ -76,6 +80,8 @@ class Log:
         Args:
             e(Exception): 异常
         '''
+        if not self._oper_log:
+            return
         self._oper_log.status = BusinessStatus.FAIL.value
         self._oper_log.error_msg = str(e)[:2000]
     
@@ -86,10 +92,20 @@ class Log:
         Args:
             login_user(LoginUser): 登录用户
         '''
+        if not self._oper_log:
+            return
         self._oper_log.status = BusinessStatus.SUCCESS.value
-        login_user:LoginUser = SecurityUtil.get_login_user()
+        try:
+            login_user:LoginUser = SecurityUtil.get_login_user()
+        except Exception:
+            login_user = None
         if login_user:
             self._oper_log.oper_name = login_user.user_name
+            self._oper_log.dept_name = login_user.dept_name
+            if not self._oper_log.oper_ip:
+                self._oper_log.oper_ip = login_user.ip_addr
+            if not self._oper_log.oper_location:
+                self._oper_log.oper_location = login_user.login_location
         
     def handle_request(self, func):
         '''
@@ -98,14 +114,17 @@ class Log:
         Args:
             func: 被装饰的函数
         '''
-        if self.is_save_request_data:
-            json_param = request.get_data(as_text=True)
-            self._oper_log.oper_ip = IpUtil.get_ip()
-            self._oper_log.oper_param = json_param[:2000] if json_param else ""
-            self._oper_log.request_method = request.method
-            self._oper_log.oper_url = request.path
-            self._oper_log.method = "{}.{}".format(func.__module__, func.__name__)
-            self._oper_log.oper_time = datetime.now()
+        if not self._oper_log or not self.is_save_request_data:
+            return
+        json_param = request.get_data(as_text=True)
+        ip_addr = IpUtil.get_ip()
+        self._oper_log.oper_ip = ip_addr
+        self._oper_log.oper_location = AddressUtil.get_address(ip_addr)
+        self._oper_log.oper_param = json_param[:2000] if json_param else ""
+        self._oper_log.request_method = request.method
+        self._oper_log.oper_url = request.path
+        self._oper_log.method = "{}.{}".format(func.__module__, func.__name__)
+        self._oper_log.oper_time = datetime.now()
                         
     def handle_response(self, response:Response|BaseModel):
         '''
@@ -114,12 +133,12 @@ class Log:
         Args:
             response(Response|BaseModel): 响应参数
         '''
-        if self.is_save_response_data:
-            if isinstance(response, BaseModel):
-                self._oper_log.json_result = response.model_dump_json(exclude_none=True)[:2000]
-            elif isinstance(response, Response):
-                print("handle_response response: {}".format(response))
-                json_result = response.get_data(as_text=False)
-                if json_result:
-                    self._oper_log.json_result = json_result[:2000]
+        if not self._oper_log or not self.is_save_response_data:
+            return
+        if isinstance(response, BaseModel):
+            self._oper_log.json_result = response.model_dump_json(exclude_none=True)[:2000]
+        elif isinstance(response, Response):
+            json_result = response.get_data(as_text=False)
+            if json_result:
+                self._oper_log.json_result = json_result[:2000]
                     

+ 5 - 5
ruoyi_system/domain/entity.py

@@ -161,13 +161,13 @@ class SysOperLog(BaseEntity):
     # 操作人员 
     oper_name: Annotated[
         Optional[str],
-        Field(default=None,exclude=True,vo=VoAccess(query=True))
+        Field(default=None,vo=VoAccess(query=True,sort=True))
     ]
     
     # 部门名称 
     dept_name: Annotated[
         Optional[str],
-        Field(default=None,exclude=True,vo=VoAccess(body=False))
+        Field(default=None,vo=VoAccess(body=False))
     ]
     
     # 请求url 
@@ -176,7 +176,7 @@ class SysOperLog(BaseEntity):
     # 操作地址 
     oper_ip: Annotated[
         Optional[str],
-        Field(default=None,exclude=True,vo=VoAccess(query=True))
+        Field(default=None,vo=VoAccess(query=True))
     ]
     
     # 操作地点 
@@ -194,7 +194,7 @@ class SysOperLog(BaseEntity):
     # 操作状态(0正常 1异常) 
     status: Annotated[
         Optional[int],
-        Field(default=None,exclude=True,vo=VoAccess(query=True))
+        Field(default=None,vo=VoAccess(query=True))
     ]
     
     # 错误消息 
@@ -204,7 +204,7 @@ class SysOperLog(BaseEntity):
     oper_time: Annotated[
         Optional[datetime],
         BeforeValidator(to_datetime()),
-        Field(default=None,vo=VoAccess())
+        Field(default=None,vo=VoAccess(query=True,sort=True))
     ]
     
     

+ 43 - 13
ruoyi_system/mapper/sys_oper_log.py

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # @Author  : YY
 
+import re
 from typing import List, Optional
 from flask import g
 from sqlalchemy import delete, insert, select
@@ -39,24 +40,20 @@ class SysOperLogMapper:
         Returns:
             List[SysOperLog]: 操作日志集合
         '''
+        criterions = []
         if oper:
-            criterions = []
             if oper.oper_ip:
-                criterions.append(SysOperLogPo.ipaddr.like(f'%{oper.oper_ip}%'))
-            if oper.status:
+                criterions.append(SysOperLogPo.oper_ip.like(f'%{oper.oper_ip}%'))
+            if oper.status is not None:
                 criterions.append(SysOperLogPo.status == oper.status)
             if oper.oper_name:
                 criterions.append(SysOperLogPo.oper_name.like(f'%{oper.oper_name}%'))
-            if "criterian_meta" in g and g.criterian_meta.extra:
-                extra:ExtraModel = g.criterian_meta.extra
-                if extra.start_time and extra.end_time:
-                    criterions.append(SysOperLogPo.oper_time >= extra.start_time)
-                    criterions.append(SysOperLogPo.oper_time <= extra.end_time)
-            stmt = select(*cls.default_columns) \
-                .where(*criterions)
-        else:
-            stmt = select(*cls.default_columns)
-            
+            cls._append_extra_criterions(criterions)
+        stmt = select(*cls.default_columns)
+        if criterions:
+            stmt = stmt.where(*criterions)
+        stmt = cls._apply_sorting(stmt)
+        
         if "criterian_meta" in g and g.criterian_meta.page:
             g.criterian_meta.page.stmt = stmt
         
@@ -86,6 +83,39 @@ class SysOperLogMapper:
         stmt = insert(SysOperLogPo).values(data)
         out = db.session.execute(stmt).inserted_primary_key
         return out[0] if out else 0
+
+    @classmethod
+    def _append_extra_criterions(cls, criterions:list):
+        if "criterian_meta" not in g or not g.criterian_meta.extra:
+            return
+        extra:ExtraModel = g.criterian_meta.extra
+        if getattr(extra, "begin_time", None):
+            criterions.append(SysOperLogPo.oper_time >= extra.begin_time)
+        if getattr(extra, "end_time", None):
+            criterions.append(SysOperLogPo.oper_time <= extra.end_time)
+
+    @classmethod
+    def _apply_sorting(cls, stmt):
+        sort = getattr(getattr(g, "criterian_meta", None), "sort", None)
+        order_columns = []
+        if sort and sort.order_by_column:
+            for alias in sort.order_by_column:
+                field_name = cls._camel_to_snake(alias)
+                column = getattr(SysOperLogPo, field_name, None)
+                if not column:
+                    continue
+                if sort.is_asc == "asc":
+                    order_columns.append(column.asc())
+                else:
+                    order_columns.append(column.desc())
+        if not order_columns:
+            order_columns.append(SysOperLogPo.oper_time.desc())
+        return stmt.order_by(*order_columns)
+
+    @staticmethod
+    def _camel_to_snake(value:str) -> str:
+        s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', value)
+        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
     
     @classmethod
     @Transactional(db.session)