cuiHe vor 2 Wochen
Ursprung
Commit
2fd15e4999

+ 53 - 33
backend/app/api/v1/module_business/vardict/controller.py

@@ -43,38 +43,6 @@ async def get_vardict_list_controller(
     )
     log.info("查询变量信息列表成功")
     return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
-@BizVarDictRouter.get("/list_alarms", summary="查询变量信息列表", description="查询变量信息列表")
-async def get_vardict_list_alarms_controller(
-    page: PaginationQueryParam = Depends(),
-    search: BizVarDictQueryParam = Depends(),
-    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
-) -> JSONResponse:
-    """查询变量信息列表接口(数据库分页)"""
-    result_dict = await BizVarDictService.page_vardict_service(
-        auth=auth,
-        page_no=page.page_no if page.page_no is not None else 1,
-        page_size=page.page_size if page.page_size is not None else 10,
-        search=search,
-        order_by=[{'mec_type':'asc'}]
-    )
-    #请求采集接口获取状态信息
-    async with httpx.AsyncClient() as client:
-        response = await client.get(
-            url=settings.COLLECT_DATA_FULL,
-            params={},
-            timeout=2
-        )
-        if response.status_code == 200:
-            json_data = response.json()
-            if json_data['code'] == 200 and json_data['data']:
-                for item in result_dict['items']:
-                    item['value'] = False
-                    crane_no = item['crane_no']
-                    alarm = json_data.get('data').get(crane_no).get('data').get('alarm').get(item['var_code'])
-                    if alarm:
-                        item['value'] = alarm.get('value')
-    log.info("查询变量信息列表成功")
-    return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
 
 @BizVarDictRouter.post("/create", summary="创建变量信息", description="创建变量信息")
 async def create_vardict_controller(
@@ -161,6 +129,58 @@ async def export_vardict_template_controller() -> StreamingResponse:
         headers={'Content-Disposition': 'attachment; filename=biz_var_dict_template.xlsx'}
     )
 
+@BizVarDictRouter.get("/list_alarms", summary="查询变量信息列表", description="查询变量信息列表")
+async def get_vardict_list_alarms_controller(
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+    """查询变量信息列表接口(数据库分页)"""
+    result_dict = await BizVarDictService.vardict_alarms_list(auth=auth,crane_no=search.crane_no)
+    #请求采集接口获取状态信息
+    async with httpx.AsyncClient() as client:
+        response = await client.get(
+            url=settings.COLLECT_DATA_FULL,
+            params={},
+            timeout=2
+        )
+        if response.status_code == 200:
+            json_data = response.json()
+            if json_data['code'] == 200 and json_data['data']:
+                for item in result_dict:
+                    item['value'] = False
+                    crane_no = item['crane_no']
+                    alarm = json_data.get('data').get(crane_no).get('data').get('alarm').get(item['var_code'])
+                    if alarm:
+                        item['value'] = alarm.get('value')
+    log.info("查询变量信息列表成功")
+    return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
+
+@BizVarDictRouter.get("/list_analog", summary="查询变量信息列表", description="查询变量信息列表")
+async def get_vardict_list_analog_controller(
+    search: BizVarDictQueryParam = Depends(),
+    auth: AuthSchema = Depends(AuthPermission(["module_business:vardict:query"]))
+) -> JSONResponse:
+    """查询变量信息列表接口(数据库分页)"""
+    result_dict = await BizVarDictService.vardict_analog_list(auth=auth,crane_no=search.crane_no)
+    #请求采集接口获取状态信息
+    async with httpx.AsyncClient() as client:
+        response = await client.get(
+            url=settings.COLLECT_DATA_FULL,
+            params={},
+            timeout=2
+        )
+        if response.status_code == 200:
+            json_data = response.json()
+            if json_data['code'] == 200 and json_data['data']:
+                for item in result_dict:
+                    item['value'] = False
+                    crane_no = item['crane_no']
+                    analog = json_data.get('data').get(crane_no).get('data').get('analog').get(item['var_code'])
+                    if analog:
+                        item['value'] = analog.get('value')
+    log.info("查询变量信息列表成功")
+    return SuccessResponse(data=result_dict, msg="查询变量信息列表成功")
+
 @BizVarDictRouter.get("/varDictMecGroup/{crane_no}", summary="获取变量信息分组数据", description="获取变量信息分组数据")
 async def get_vardict_mec_group_controller(
     crane_no: str = Path(..., description="crane_no"),
@@ -188,7 +208,7 @@ async def get_vardict_mec_group_controller(
                 json_digital = json_data.get('data').get(crane_no).get('data').get('digital')
                 for var_dict in result_dict:
                     for key,inner_dict in var_dict.items():
-                        if key != 'mec_type' and key != 'alarm_varList' and key != 'varList_simple':
+                        if key != 'mec_type' and key != 'alarm_varList' and key != 'mecVarList_simple':
                             for item in inner_dict:
                                 if key == 'digital_varList':
                                     item['value'] = json_digital.get(item.get('var_code')).get('value')

+ 12 - 7
backend/app/api/v1/module_business/vardict/schema.py

@@ -34,6 +34,7 @@ class BizVarDictCreateSchema(BaseModel):
     diagnosis_id: str | None = Field(default=None, description='关联诊断专家')
     status: str = Field(default="1", description='是否启用')
     description: str | None = Field(default=None, max_length=255, description='备注/描述')
+    crane_name: str | None = Field(default=None, max_length=255, description='行车名')
 
 
 class BizVarDictUpdateSchema(BizVarDictCreateSchema):
@@ -82,8 +83,6 @@ class BizVarDictQueryParam:
         updated_id: int | None = Query(None, description="更新人ID"),
         created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
         updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
-        # 新增:场景区分参数,默认 False(代表后台内部请求),True 代表前端接口请求
-        is_api_request: bool | None = Query(False, description="是否为前端接口请求(用于切换 switch_type 查询方式)")
 
     ) -> None:
         
@@ -97,10 +96,7 @@ class BizVarDictQueryParam:
         self.mec_type = mec_type
         # 精确查询字段
         self.data_type = data_type
-        if is_api_request:
-            self.switch_type = (">=", switch_type)
-        else:
-            self.switch_type = switch_type
+        self.switch_type = switch_type
         # 模糊查询字段
         self.addr = ("like", addr)
         # 精确查询字段
@@ -147,10 +143,19 @@ class BizVarDictQueryParam:
 
 class VarDictMecGroupSchema(BaseModel):
     """
-    行车信息页面数据模型
+    行车信息机构分组数据模型
     """
     mec_type: str = Field(default=..., description='所属机构')
     alarm_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='报警变量数据')
     digital_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='开关量变量数据')
     analog_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='模拟量变量数据')
+    mecVarList_simple: list[BizVarDictOutSchema] | None = Field(default=None, description='变量数据')
+
+class VarDictSchema(BaseModel):
+    """
+    行车信息数据模型
+    """
+    alarm_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='报警变量数据')
+    digital_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='开关量变量数据')
+    analog_varList: list[BizVarDictOutSchema] | None = Field(default=None, description='模拟量变量数据')
     varList_simple: list[BizVarDictOutSchema] | None = Field(default=None, description='变量数据')

+ 60 - 4
backend/app/api/v1/module_business/vardict/service.py

@@ -2,6 +2,8 @@
 
 import io
 import json
+from typing import Any
+
 from fastapi import UploadFile
 import pandas as pd
 from redis.asyncio.client import Redis
@@ -67,6 +69,60 @@ class BizVarDictService:
             item['crane_name'] = crane_model.crane_name
             item['gateway_name'] = gateway_model.gateway_name if gateway_model else ""
         return result
+
+    @classmethod
+    async def vardict_alarms_list(cls, auth: AuthSchema, crane_no: str = None) -> list[dict]:
+
+        sql_parts = [
+            """SELECT a.*,b.crane_name 
+            FROM biz_var_dict as a 
+            LEFT JOIN biz_crane as b ON a.crane_no = b.crane_no 
+            WHERE a.`status` = :status AND b.`status` = :status AND a.switch_type >= 2"""
+        ]
+
+        business_params: dict[str, Any] = {"status": 1}
+
+        if crane_no and isinstance(crane_no, str) and crane_no.strip():
+            valid_crane_no = crane_no.strip()
+            sql_parts.append(f"AND a.crane_no = :crane_no")
+            business_params["crane_no"] = valid_crane_no
+
+        sql_parts.append("ORDER BY b.`order` asc,a.mec_type asc,a.var_sort asc")
+        final_sql = " ".join(sql_parts)
+
+        try:
+            obj_list = await BizVarDictCRUD(auth).list_sql(final_sql, business_params)
+
+            return [BizVarDictOutSchema.model_validate(obj).model_dump() for obj in obj_list]
+        except Exception as e:
+            raise CustomException(msg=f"查询变量字典报警列表失败:{str(e)}")
+
+    @classmethod
+    async def vardict_analog_list(cls, auth: AuthSchema, crane_no: str = None) -> list[dict]:
+
+        sql_parts = [
+            """SELECT a.*,b.crane_name 
+            FROM biz_var_dict as a 
+            LEFT JOIN biz_crane as b ON a.crane_no = b.crane_no 
+            WHERE a.`status` = :status AND b.`status` = :status AND a.data_type >= 2"""
+        ]
+
+        business_params: dict[str, Any] = {"status": 1}
+
+        if crane_no and isinstance(crane_no, str) and crane_no.strip():
+            valid_crane_no = crane_no.strip()
+            sql_parts.append(f"AND a.crane_no = :crane_no")
+            business_params["crane_no"] = valid_crane_no
+
+        sql_parts.append("ORDER BY b.`order` asc,a.mec_type asc,a.var_sort asc")
+        final_sql = " ".join(sql_parts)
+
+        try:
+            obj_list = await BizVarDictCRUD(auth).list_sql(final_sql, business_params)
+
+            return [BizVarDictOutSchema.model_validate(obj).model_dump() for obj in obj_list]
+        except Exception as e:
+            raise CustomException(msg=f"查询变量字典报警列表失败:{str(e)}")
     
     @classmethod
     async def create_vardict_service(cls, auth: AuthSchema, data: BizVarDictCreateSchema,redis: Redis) -> dict:
@@ -325,7 +381,7 @@ class BizVarDictService:
             option_list=option_list
         )
     @classmethod
-    async def get_vardict_group_service(cls,auth: AuthSchema, redis: Redis,crane_no: str):
+    async def get_vardict_group_service(cls, auth: AuthSchema,redis: Redis,crane_no: str):
         """
         从缓存获取变量分组数据列表信息service
 
@@ -399,17 +455,17 @@ class BizVarDictService:
                             mec_list = await BizMecCRUD(auth).list(search={'crane_no':crane_no,'status':'1'},order_by=[{'sort':'asc'}])
                             for mec in mec_list:
                                 # 获取分组数据
-                                varDicts = await BizVarDictCRUD(auth).list(
+                                mecVarDicts = await BizVarDictCRUD(auth).list(
                                     search={'crane_no': crane_no, 'mec_type': mec.mec_type, 'status': '1'},
                                     order_by=[{'var_sort': 'asc'}])
-                                if not varDicts:
+                                if not mecVarDicts:
                                     continue
                                 alarmVarList = await BizVarDictCRUD(auth).list(search={'crane_no': crane_no,'mec_type':mec.mec_type, 'switch_type': ('>=','2'), 'status': '1'},order_by=[{'var_sort': 'asc'}])
                                 digitalVarList = await BizVarDictCRUD(auth).list(search={'crane_no':crane_no,'mec_type':mec.mec_type,'data_type':'1','status':'1'},order_by=[{'var_sort':'asc'}])
                                 analogVarList = await BizVarDictCRUD(auth).list(search={'crane_no': crane_no,'mec_type':mec.mec_type, 'data_type': ('!=', '1'), 'status': '1'},order_by=[{'var_sort': 'asc'}])
                                 varDictMecGroupSchema.append(
                                     VarDictMecGroupSchema(mec_type=mec.mec_type,
-                                                          varList_simple=varDicts,
+                                                          mecVarList_simple=mecVarDicts,
                                                           digital_varList=digitalVarList,
                                                           analog_varList=analogVarList,
                                                           alarm_varList=alarmVarList))

+ 103 - 1
backend/app/core/base_crud.py

@@ -5,7 +5,7 @@ from typing import TypeVar, Sequence, Generic, Dict, Any, List, Optional, Type,
 from sqlalchemy.sql.elements import ColumnElement
 from sqlalchemy.orm import selectinload
 from sqlalchemy.engine import Result
-from sqlalchemy import asc, func, select, delete, Select, desc, update
+from sqlalchemy import asc, func, select, delete, Select, desc, update, text
 from sqlalchemy import inspect as sa_inspect
 
 from app.core.base_model import MappedBase
@@ -93,6 +93,40 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
         except Exception as e:
             raise CustomException(msg=f"列表查询失败: {str(e)}")
 
+    async def list_sql(self, sql:str,params: Optional[Dict[str, Any]] = None) -> List[Dict]:
+        """
+        根据条件获取对象列表
+
+        参数:
+        - search (Optional[Dict]): 查询条件,格式为 {'id': value, 'name': value}
+        - order_by (Optional[List[Dict[str, str]]]): 排序字段,格式为 [{'id': 'asc'}, {'name': 'desc'}]
+        - preload (Optional[List[Union[str, Any]]]): 预加载关系,支持关系名字符串或SQLAlchemy loader option
+
+        返回:
+        - Sequence[ModelType]: 对象列表
+
+        异常:
+        - CustomException: 查询失败时抛出异常
+        """
+        try:
+            business_params = params.copy() if params else {}
+
+            filtered_sql, permission_params = await self.__filter_permissions_sql(sql)
+
+            final_params = business_params
+            if permission_params:
+                final_params.update(permission_params)
+
+            result = await self.execute_raw_sql(
+                sql=filtered_sql,
+                params=final_params,  # 传入权限参数,绑定current_user_id
+                fetch_one=False,
+                scalar=False
+            )
+            return result
+        except Exception as e:
+            raise CustomException(msg=f"列表查询失败: {str(e)}")
+
     async def tree_list(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, children_attr: str = 'children', preload: Optional[List[Union[str, Any]]] = None) -> Sequence[ModelType]:
         """
         获取树形结构数据列表
@@ -315,6 +349,63 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
         except Exception as e:
             raise CustomException(msg=f"清空失败: {str(e)}")
 
+    async def execute_raw_sql(
+            self,
+            sql: str,
+            params: Optional[Dict[str, Any]] = None,
+            fetch_one: bool = False,
+            scalar: bool = False
+    ) -> Optional[Union[Dict, List[Dict], Any, Sequence[Any]]]:
+        try:
+            # ---------------------- 1. 严格校验输入:避免空SQL、非字符串等错误 ----------------------
+            # 校验SQL是否为非空字符串
+            if not isinstance(sql, str) or len(sql.strip()) == 0:
+                raise CustomException(msg="传入的原始SQL不能为空且必须为字符串类型")
+
+            # 初始化最终SQL(仅对字符串操作,规避TextClause错误)
+            final_sql = sql.strip()
+            # 初始化最终参数:兜底空字典,避免None报错
+            final_params = params.copy() if (params and isinstance(params, Dict)) else {}
+
+            # ---------------------- 3. 核心:执行SQL(仅包装一次TextClause,不进行后续字符串操作) ----------------------
+            # 包装为TextClause(SQLAlchemy执行原始SQL必需,仅执行此步骤,无额外操作)
+            raw_sql_clause = text(final_sql)
+
+            # 执行SQL:传入上层已合并(业务+权限)的参数,确保所有占位符绑定完成
+            db_result: Result = await self.auth.db.execute(raw_sql_clause, final_params)
+
+            # ---------------------- 4. 处理结果:格式统一,兼容上层list_sql的返回需求 ----------------------
+            # 处理SELECT查询(返回字典/字典列表,兼容Sequence[ModelType])
+            if final_sql.upper().startswith("SELECT"):
+                # 标量结果(单个值,如COUNT(*))
+                if scalar:
+                    return db_result.scalar() if fetch_one else db_result.scalars().all()
+
+                # 完整行结果:转换为字典列表,避免字段名丢失,兜底空列表
+                row_mappings = db_result.mappings().all()
+                result_list = [dict(row) for row in row_mappings] if row_mappings else []
+
+                # 单条/多条结果返回,确保返回序列类型
+                if fetch_one:
+                    return result_list[0] if len(result_list) > 0 else None
+                else:
+                    return result_list
+
+            # 处理DML操作(INSERT/UPDATE/DELETE):刷新会话,返回None
+            await self.auth.db.flush()
+            return None
+
+        # ---------------------- 5. 异常捕获:补充上下文,方便排查上层拼接后的问题 ----------------------
+        except CustomException as ce:
+            raise ce
+        except Exception as e:
+            error_msg = (
+                f"执行原始SQL失败:{str(e)}"
+                f"\n  执行的SQL(上层已拼接权限):{final_sql[:500]}..."
+                f"\n  传入的参数(业务+权限):{final_params}"
+            )
+            raise CustomException(msg=error_msg)
+
     async def set(self, ids: List[int], **kwargs) -> None:
         """
         批量更新对象
@@ -365,6 +456,17 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
         )
         return await filter.filter_query(sql)
 
+    async def __filter_permissions_sql(self, sql: str) -> Select:
+        """
+        过滤数据权限(仅用于Select)。
+        """
+        filter = Permission(
+            model=self.model,
+            auth=self.auth
+        )
+        return await filter.filter_query_sql(sql)
+
+
     async def __build_conditions(self, **kwargs) -> List[ColumnElement]:
         """
         构建查询条件

+ 150 - 2
backend/app/core/permission.py

@@ -1,6 +1,8 @@
 # -*- coding: utf-8 -*-
 
-from typing import Any
+from typing import Any, Optional, Dict, Set
+
+from pluggy import Result
 from sqlalchemy.sql.elements import ColumnElement
 from sqlalchemy import select
 from app.api.v1.module_system.user.model import UserModel
@@ -156,4 +158,150 @@ class Permission:
         created_id_attr = getattr(self.model, "created_id", None)
         if created_id_attr is not None:
             return created_id_attr == self.auth.user.id
-        return None
+        return None
+
+    async def filter_query_sql(self, sql: str) -> (str, Optional[Dict[str, Any]]):
+        """
+        异步过滤**原始SQL字符串**,拼接完整权限条件(无逻辑省略)
+        Args:
+            sql: 原始SQL字符串(如"SELECT * FROM biz_var_dict")
+        Returns:
+            拼接权限条件后的完整SQL字符串、权限参数字典(防SQL注入,可直接传入execute_raw_sql)
+        """
+        # 1. 获取完整的权限SQL条件和参数(无省略,包含所有角色/部门逻辑)
+        permission_condition, permission_params = await self.__permission_condition_sql()
+
+        # 2. 无权限条件,直接返回原始SQL和空参数(保持原有逻辑)
+        if not permission_condition:
+            return sql.strip(), None
+
+        # 3. 清理原始SQL,保证拼接语法正确(移除末尾分号、多余空格)
+        cleaned_sql = sql.strip().rstrip("; \n\t")  # 处理多种空白字符和分号
+
+        # 4. 智能判断原始SQL是否包含WHERE子句,进行拼接(兼容所有场景)
+        if "WHERE" in cleaned_sql.upper():
+            # 已有WHERE,用AND拼接权限条件(保留原有查询条件)
+            final_sql = f"{cleaned_sql} AND {permission_condition}"
+        else:
+            # 无WHERE,直接添加WHERE和权限条件
+            final_sql = f"{cleaned_sql} WHERE {permission_condition}"
+
+        # 5. 返回拼接后的完整SQL和权限参数(后续执行可直接使用)
+        return final_sql, permission_params
+    async def __permission_condition_sql(self) -> (Optional[str], Optional[Dict[str, Any]]):
+        """
+        应用数据范围权限隔离(完整逻辑,无任何省略)
+        基于角色的五种数据权限范围过滤,返回原始SQL条件字符串和参数(防注入)
+        Returns:
+            权限条件字符串(如"created_id = :current_user_id AND dept_id IN (:accessible_dept_ids)")
+            权限参数字典(如{"current_user_id": 100, "accessible_dept_ids": [1,2,3]})
+        """
+        # 初始化返回结果(权限SQL条件、权限参数)
+        permission_sql: Optional[str] = None
+        permission_params: Dict[str, Any] = {}
+
+        # --------------- 原有逻辑:前置判断(无任何省略)---------------
+        # 1. 未登录用户,不限制数据权限
+        if not self.auth.user:
+            return permission_sql, permission_params
+
+        # 2. 关闭数据权限检查,不限制数据权限
+        if not self.auth.check_data_scope:
+            return permission_sql, permission_params
+
+        # 3. 模型无created_id字段,无法进行权限过滤,不限制
+        if not hasattr(self.model, "created_id"):
+            return permission_sql, permission_params
+
+        # 4. 超级管理员,拥有全部数据权限,不限制
+        if self.auth.user.is_superuser:
+            return permission_sql, permission_params
+
+        # --------------- 原有逻辑:获取用户角色与数据范围(无任何省略)---------------
+        current_user_id = self.auth.user.id
+        roles = getattr(self.auth.user, "roles", []) or []
+
+        # 5. 无角色用户,仅能查看自己创建的数据(基础权限)
+        if not roles:
+            permission_sql = "created_id = :current_user_id"
+            permission_params["current_user_id"] = current_user_id
+            return permission_sql, permission_params
+
+        # 6. 收集所有角色的权限范围和自定义部门ID
+        data_scopes: Set[int] = set()
+        custom_dept_ids: Set[int] = set()
+
+        for role in roles:
+            data_scopes.add(role.data_scope)
+            # 收集自定义权限(DATA_SCOPE_CUSTOM=5)关联的部门ID
+            if role.data_scope == self.DATA_SCOPE_CUSTOM and hasattr(role, 'depts') and role.depts:
+                for dept in role.depts:
+                    custom_dept_ids.add(dept.id)
+
+        # 7. 全部数据权限(最高优先级),不限制数据权限
+        if self.DATA_SCOPE_ALL in data_scopes:
+            return None, None
+
+        # --------------- 收集可访问的部门ID---------------
+        accessible_dept_ids: Set[int] = set()
+        user_dept_id = getattr(self.auth.user, "dept_id", None)
+
+        # 8. 处理自定义数据权限(DATA_SCOPE_CUSTOM=5)
+        if self.DATA_SCOPE_CUSTOM in data_scopes:
+            accessible_dept_ids.update(custom_dept_ids)
+
+        # 9. 处理本部门数据权限(DATA_SCOPE_DEPT=3)
+        if self.DATA_SCOPE_DEPT in data_scopes:
+            if user_dept_id is not None:
+                accessible_dept_ids.add(user_dept_id)
+
+        # 10. 处理本部门及以下数据权限(DATA_SCOPE_DEPT_AND_CHILD=4)
+        if self.DATA_SCOPE_DEPT_AND_CHILD in data_scopes:
+            if user_dept_id is not None:
+                try:
+                    # 完整查询所有部门,递归获取子部门ID(无省略)
+                    dept_sql = select(DeptModel)
+                    dept_result = await self.auth.db.execute(dept_sql)
+                    dept_objs = dept_result.scalars().all()
+                    id_map = get_child_id_map(dept_objs)
+                    # 递归获取当前部门及所有子部门ID(包含自身)
+                    dept_with_children_ids = get_child_recursion(id=user_dept_id, id_map=id_map)
+
+                    # 合并到可访问部门ID集合
+                    accessible_dept_ids.update(dept_with_children_ids)
+                except Exception:
+                    # 查询失败降级:仅保留本部门ID(与原有逻辑一致)
+                    accessible_dept_ids.add(user_dept_id)
+
+        # --------------- 原有逻辑:构建部门权限过滤条件(无任何省略)---------------
+        if accessible_dept_ids:
+            # 转换部门ID集合为列表(方便SQL IN条件使用)
+            accessible_dept_list = list(accessible_dept_ids)
+
+            # 优先使用模型的created_by关联(关联UserModel.dept_id,性能更优)
+            creator_rel = getattr(self.model, "created_by", None)
+            if creator_rel is not None and hasattr(UserModel, 'dept_id'):
+                # 构建部门权限SQL条件(参数化,防注入)
+                permission_sql = "created_by_dept_id IN (:accessible_dept_ids)"
+                permission_params["accessible_dept_ids"] = accessible_dept_list
+
+                # 补充:同时保留创建人ID条件(可选,根据你的业务需求调整)
+                permission_sql = f"created_id = :current_user_id AND {permission_sql}"
+                permission_params["current_user_id"] = current_user_id
+            else:
+                # 降级方案:无created_by关联,仅能查看自己创建的数据
+                permission_sql = "created_id = :current_user_id"
+                permission_params["current_user_id"] = current_user_id
+
+            return permission_sql, permission_params
+
+        # --------------- 原有逻辑:处理仅本人数据权限(DATA_SCOPE_SELF=2)---------------
+        if self.DATA_SCOPE_SELF in data_scopes:
+            permission_sql = "created_id = :current_user_id"
+            permission_params["current_user_id"] = current_user_id
+            return permission_sql, permission_params
+
+        # --------------- 原有逻辑:默认情况(无有效权限范围,仅能查看自己数据)---------------
+        permission_sql = "created_id = :current_user_id"
+        permission_params["current_user_id"] = current_user_id
+        return permission_sql, permission_params

+ 1 - 0
frontend/src/api/module_business/crane.ts

@@ -119,6 +119,7 @@ export interface BizCranePageQuery extends PageQuery {
   updated_id?: number;
   created_time?: string[];
   updated_time?: string[];
+  order_by?: string;
 }
 
 // 列表展示项

+ 10 - 2
frontend/src/api/module_business/vardict.ts

@@ -14,13 +14,21 @@ const BizVarDictAPI = {
   },
 
   listBizVarDictAlarms(query: BizVarDictPageQuery) {
-    return request<ApiResponse<PageResult<BizVarDictTable[]>>>({
+    return request<ApiResponse<BizVarDictTable[]>>({
       url: `${API_PATH}/list_alarms`,
       method: "get",
       params: query,
     });
   },
 
+  listBizVarDictAnalog(query: BizVarDictPageQuery) {
+    return request<ApiResponse<BizVarDictTable[]>>({
+      url: `${API_PATH}/list_analog`,
+      method: "get",
+      params: query,
+    });
+  },
+
   // 详情查询
   detailBizVarDict(id: number) {
     return request<ApiResponse<BizVarDictTable>>({
@@ -128,7 +136,7 @@ export interface BizVarDictPageQuery extends PageQuery {
   updated_id?: number;
   created_time?: string[];
   updated_time?: string[];
-  is_api_request?: string;
+  order_by?:string;
 }
 
 // 列表展示项

+ 13 - 1
frontend/src/router/index.ts

@@ -96,7 +96,19 @@ export const constantRoutes: RouteRecordRaw[] = [
             name: 'HistoryAlarm',
             meta: { title: '历史报警' },
             component: import("@/views/web/detail/historyAlarm.vue")
-          }
+          },
+          {
+            path: '/detail/realtimeCurve',
+            name: 'RealtimeCurve',
+            meta: { title: '实时曲线' },
+            component: import("@/views/web/detail/realtimeCurve.vue")
+          },
+          {
+            path: '/detail/historyCurve',
+            name: 'HistoryCurve',
+            meta: { title: '历史曲线' },
+            component: import("@/views/web/detail/HistoryCurve.vue")
+          },
         ]
       }
     ],

+ 12 - 0
frontend/src/views/web/detail/historyCurve.vue

@@ -0,0 +1,12 @@
+<template>
+    这是历史曲线
+
+  </template>
+  
+  <script setup lang="ts">
+
+  </script>
+  
+  <style lang="less" scoped>
+
+  </style>

+ 5 - 6
frontend/src/views/web/detail/realtimeAlarm.vue

@@ -18,7 +18,7 @@
 <script setup lang="ts">
 import { ref } from 'vue';
 import { useDictStore } from "@/store";
-import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery,MecDataItem } from '@/api/module_business/vardict'
+import BizVarDictAPI, { BizVarDictTable,BizVarDictPageQuery } from '@/api/module_business/vardict'
 import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
 
 const tabHeight = ref('calc(100vh - 70px - 5px - 50px - 10px - 44px)')
@@ -46,13 +46,13 @@ const mqttConfig = {
 const mqttUtil = new MqttUtil(mqttConfig);
 
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
-  page_no: 1,
-  page_size: 100,
+  page_no: 0,
+  page_size: 0,
   crane_no: craneInfo.crane_no,
   var_code: undefined,
   var_name: undefined,
   mec_type: undefined,
-  switch_type: '2',
+  switch_type: undefined,
   gateway_id: undefined,
   var_group: undefined,
   var_category: undefined,
@@ -65,7 +65,6 @@ const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   updated_time: undefined,
   created_id: undefined,
   updated_id: undefined,
-  is_api_request: 'True',
 });
 const allData = ref<BizVarDictTable[]>([]);
 
@@ -74,7 +73,7 @@ const filteredData = computed(() => {
 });
 const getData = async () => {
   const response = await BizVarDictAPI.listBizVarDictAlarms(queryFormVarDictData);
-  allData.value = response.data.data.items
+  allData.value = response.data.data
   tab_loading.value = false
 }
 

+ 315 - 0
frontend/src/views/web/detail/realtimeCurve.vue

@@ -0,0 +1,315 @@
+<template>
+    <div class="realtime-curve-container">
+      <!-- ECharts 容器 -->
+      <div ref="chartRef" class="chart-box"></div>
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  // 补全所有必要导入,并严格指定类型
+  import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
+  import * as echarts from 'echarts';
+  import type { ECharts, EChartsOption, YAxisOption, SeriesOption } from 'echarts';
+  import BizVarDictAPI, { BizVarDictTable, BizVarDictPageQuery } from '@/api/module_business/vardict';
+  import MqttUtil, { MqttMessageCallback } from '@/utils/mqttUtil';
+  
+  // ========== 严格 TS 类型定义 ==========
+  /** MQTT 消息 payload 数据项类型 */
+  interface MqttPayloadItem {
+    var_code: string;
+    value: number | string;
+    [key: string]: unknown; // 兼容其他字段
+  }
+  
+  /** MQTT 消息 payload 类型 */
+  interface MqttPayload {
+    data: MqttPayloadItem[];
+    [key: string]: unknown;
+  }
+  
+  /** 曲线数据结构类型 */
+  interface CurveDataItem {
+    xData: string[]; // X轴:时间戳字符串
+    yData: number[]; // Y轴:数值
+  }
+  
+  /** 曲线数据映射类型 */
+  type CurveDataMap = Record<string, CurveDataItem>;
+  
+  // ========== 业务配置 ==========
+  const craneInfo = JSON.parse(localStorage.getItem('craneInfo') || '{}') as Record<string, string>;
+  const loading = ref<boolean>(true);
+  // 严格指定查询参数类型,避免 undefined 隐式类型
+  const queryFormVarDictData = reactive<BizVarDictPageQuery>({
+    page_no: 0,
+    page_size: 0,
+    crane_no: craneInfo.crane_no || '', // 兜底空字符串,避免 TS 报错
+    var_code: undefined,
+    var_name: undefined,
+    mec_type: undefined,
+    switch_type: undefined,
+    gateway_id: undefined,
+    var_group: undefined,
+    var_category: undefined,
+    is_top_show: undefined,
+    is_save: undefined,
+    is_overview_top_show: undefined,
+    is_home_page_show: undefined,
+    status: undefined,
+    created_time: undefined,
+    updated_time: undefined,
+    created_id: undefined,
+    updated_id: undefined,
+  });
+  
+  // MQTT 配置(TS 类型约束)
+  const mqttConfig = {
+    wsUrl: import.meta.env.VITE_APP_WS_ENDPOINT || 'ws://127.0.0.1:9001',
+    topics: [`cdc/${craneInfo.crane_no || ''}/analog/batch/#`], // 兜底空字符串
+  };
+  const mqttUtil = new MqttUtil(mqttConfig);
+  
+  // ========== 核心数据(严格 TS 类型) ==========
+  const allData = ref<BizVarDictTable[]>([]);
+  const chartRef = ref<HTMLDivElement | null>(null);
+  let chartInstance: ECharts | null = null;
+  const curveData = ref<CurveDataMap>({});
+  const MAX_DATA_LEN = 30; // 最大数据点数量
+  
+  // ========== 解决 ECharts 报错核心:规范 Y 轴/系列配置 ==========
+  const initECharts = (): void => {
+    if (!chartRef.value) return;
+    
+    // 销毁旧实例,避免重复创建
+    if (chartInstance) {
+      chartInstance.dispose();
+    }
+  
+    // 初始化 ECharts 实例(指定 DOM 类型)
+    chartInstance = echarts.init(chartRef.value);
+    
+    // 基础配置:无标题、图例置顶(规避 getAxesOnZeroOf 报错的核心:Y轴先初始化单轴,后续动态更新)
+    const baseOption: EChartsOption = {
+      title: { show: false }, // 隐藏标题
+      legend: {
+        top: 0, // 图例置顶
+        left: 'center',
+        textStyle: { fontSize: 12 },
+        type: 'scroll', // 图例过多时滚动,避免溢出
+      },
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: { type: 'line' }, // 改用 line 类型,避免 shadow 兼容性问题
+        formatter: (params: echarts.EChartOption.TooltipFormatterParams) => {
+          if (!Array.isArray(params) || params.length === 0) return '';
+          const timeStr = new Date(params[0].name).toLocaleTimeString();
+          return params.reduce((res, param) => {
+            const value = param.value as number;
+            return `${res}${param.seriesName}: ${value.toFixed(2)}<br/>`;
+          }, `${timeStr}<br/>`);
+        },
+      },
+      grid: {
+        left: '10%',
+        right: '10%',
+        bottom: '15%',
+        top: '15%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: [],
+        axisLabel: {
+          formatter: (value: string) => new Date(value).toLocaleTimeString(),
+        },
+        boundaryGap: false, // 取消边界间隙,曲线更连续
+      },
+      // 初始单 Y 轴(解决 getAxesOnZeroOf 报错),后续按变量值域分组分配区间
+      yAxis: {
+        type: 'value',
+        axisLine: { show: true },
+        splitLine: { show: true },
+      },
+      series: [], // 动态生成系列
+    };
+  
+    chartInstance.setOption(baseOption);
+    
+    // 监听窗口大小变化
+    window.addEventListener('resize', handleResize);
+  };
+  
+  /** 窗口大小变化处理 */
+  const handleResize = (): void => {
+    chartInstance?.resize();
+  };
+  
+  /** 初始化曲线数据 */
+  const initCurveData = (): void => {
+    if (!allData.value.length) return;
+  
+    // 初始化每个变量的曲线数据
+    curveData.value = allData.value.reduce((map, item) => {
+      map[item.var_code] = { xData: [], yData: [] };
+      return map;
+    }, {} as CurveDataMap);
+  
+    // 初始化系列配置(曲线不交叉核心:按变量值域分配不同区间)
+    updateEChartsSeries();
+  };
+  
+  /** 更新 ECharts 系列配置(规避曲线交叉:值域分段) */
+  const updateEChartsSeries = (): void => {
+    if (!chartInstance || !allData.value.length) return;
+  
+    // 为每个变量分配独立值域区间(避免交叉)
+    const seriesList: SeriesOption[] = allData.value.map((item, index) => {
+      // 按索引分配值域区间(如第1个变量[0-100],第2个[100-200],以此类推)
+      const valueRange = index * 100;
+  
+      return {
+        name: item.var_name || item.var_code,
+        type: 'line',
+        data: curveData.value[item.var_code]?.yData || [],
+        symbol: 'none',
+        smooth: true,
+        lineStyle: { width: 2 },
+        // 值域映射:将原始值映射到独立区间,避免交叉
+        encode: { y: 0 },
+        // 自定义数值转换(核心:曲线不交叉)
+        transform: [
+          {
+            type: 'ecSimpleTransform:custom',
+            config: {
+              callback: (params: { value: number }) => {
+                // 原始值 + 区间偏移量,保证每条曲线值域不重叠
+                return { value: params.value + valueRange };
+              },
+            },
+          },
+        ],
+      };
+    });
+  
+    // 更新系列配置(避免直接修改 Y 轴导致 getAxesOnZeroOf 报错)
+    chartInstance.setOption({
+      series: seriesList,
+    });
+  };
+  
+  /** 更新曲线数据(严格 TS 类型) */
+  const updateCurveData = (varCode: string, value: number): void => {
+    if (!curveData.value[varCode]) return;
+  
+    const now = new Date().toISOString();
+    const curveItem = curveData.value[varCode];
+  
+    // 添加新数据
+    curveItem.xData.push(now);
+    curveItem.yData.push(Number(value));
+  
+    // 限制数据长度
+    if (curveItem.xData.length > MAX_DATA_LEN) {
+      curveItem.xData.shift();
+      curveItem.yData.shift();
+    }
+  
+    // 更新 ECharts(仅更新数据,不修改轴配置,规避报错)
+    if (chartInstance) {
+      const seriesOption = chartInstance.getOption().series as SeriesOption[];
+      const targetSeriesIndex = allData.value.findIndex(item => item.var_code === varCode);
+      
+      if (targetSeriesIndex > -1) {
+        chartInstance.setOption({
+          xAxis: { data: curveItem.xData },
+          series: [
+            {
+              index: targetSeriesIndex,
+              data: curveItem.yData,
+            },
+          ],
+        });
+      }
+    }
+  };
+  
+  // ========== 数据获取 & MQTT 处理(严格 TS 类型) ==========
+  const getData = async (): Promise<void> => {
+    try {
+      const response = await BizVarDictAPI.listBizVarDictAnalog(queryFormVarDictData);
+      allData.value = response.data.data || [];
+      initCurveData();
+    } catch (error) {
+      console.error('获取变量数据失败:', error);
+    } finally {
+      loading.value = false;
+    }
+  };
+  
+  /** MQTT 消息处理(严格 TS 类型,兼容 analog/alarm 后缀) */
+  const handleMqttMessage: MqttMessageCallback = (topic: string, payload: MqttPayload): void => {
+    const topicLevels = topic.split('/');
+    if (topicLevels.length < 4 || !payload.data) return;
+  
+    const suffix = topicLevels[2];
+    // 兼容 analog(实时数据)和 alarm(告警数据)
+    if (['analog', 'alarm'].includes(suffix)) {
+      payload.data.forEach((payloadItem) => {
+        const { var_code, value } = payloadItem;
+        if (!var_code || value === undefined) return;
+  
+        // 更新 allData 中的值
+        const targetItem = allData.value.find(item => item.var_code === var_code);
+        if (targetItem) {
+          targetItem.value = Number(value);
+        }
+  
+        // 更新曲线数据(仅处理数值类型)
+        const numValue = Number(value);
+        if (!isNaN(numValue)) {
+          updateCurveData(var_code, numValue);
+        }
+      });
+    }
+  };
+  
+  // ========== 生命周期(严格 TS 规范) ==========
+  onMounted(async (): Promise<void> => {
+    initECharts();
+    await getData();
+    mqttUtil.initConnect(handleMqttMessage);
+  });
+  
+  onUnmounted((): void => {
+    mqttUtil.releaseResources();
+    window.removeEventListener('resize', handleResize);
+    // 销毁 ECharts 实例,避免内存泄漏
+    if (chartInstance) {
+      chartInstance.dispose();
+      chartInstance = null;
+    }
+  });
+  
+  // 深度监听 allData 变化
+  watch(
+    allData,
+    () => {
+      if (!loading.value) {
+        initCurveData();
+      }
+    },
+    { deep: true }
+  );
+  </script>
+  
+  <style lang="less" scoped>
+  .realtime-curve-container {
+    width: 100%;
+    height: 100vh;
+  
+    .chart-box {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  </style>

+ 6 - 5
frontend/src/views/web/overview/index.vue

@@ -127,16 +127,17 @@ const queryFormCraneData = reactive<BizCranePageQuery>({
   updated_time: undefined,
   created_id: undefined,
   updated_id: undefined,
+  order_by: 'order,asc;',
 });
 
 const queryFormVarDictData = reactive<BizVarDictPageQuery>({
-  page_no: 1,
-  page_size: 100,
+  page_no: 0,
+  page_size: 0,
   crane_no: undefined,
   var_code: undefined,
   var_name: undefined,
   mec_type: undefined,
-  switch_type: '2',
+  switch_type: undefined,
   gateway_id: undefined,
   var_group: undefined,
   var_category: undefined,
@@ -149,7 +150,7 @@ const queryFormVarDictData = reactive<BizVarDictPageQuery>({
   updated_time: undefined,
   created_id: undefined,
   updated_id: undefined,
-  is_api_request: 'True',
+  order_by: undefined,
 });
 
 // 操作按钮点击事件
@@ -176,7 +177,7 @@ const getVarDictData = async () => {
   try {
     alarm_loading.value = true;
     const response = await BizVarDictAPI.listBizVarDictAlarms(queryFormVarDictData);
-    varDictData.value = response.data.data.items;
+    varDictData.value = response.data.data;
   } finally {
     alarm_loading.value = false
   }