Просмотр исходного кода

feat(db): 地磅、操作日志与工作流补全数据权限过滤

DB 列表/仪表盘 scoped 聚合,操作日志按 user_id 过滤,工作流实例详情与审批前校验 scope。

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 4 месяцев назад
Родитель
Сommit
66d5a8714b

+ 84 - 70
backend/app/api/db/dashboard.py

@@ -1,9 +1,10 @@
 from __future__ import annotations
 
 from datetime import date
+from typing import Dict
 
 from fastapi import APIRouter, Depends
-from sqlalchemy import func, text
+from sqlalchemy import func
 from sqlalchemy.orm import Session
 
 from app.core.database import get_db
@@ -15,10 +16,28 @@ from app.schemas.db.dashboard import (
     DashboardPartnerCargoStat,
     DashboardStats,
 )
+from app.services.db_data_scope import apply_db_scope
 
 router = APIRouter(prefix="/dashboard", tags=["dashboard"])
 
 
+def _scoped_inventory_query(db: Session, tenant_no: str, user: DbUserContext):
+    query = db.query(DbInboundOutbound).filter(
+        DbInboundOutbound.del_flag == 0,
+        DbInboundOutbound.tenant_no == tenant_no,
+    )
+    return apply_db_scope(query, DbInboundOutbound, user, db, tenant_no)
+
+
+def _scoped_weighing_query(db: Session, tenant_no: str, user: DbUserContext):
+    query = db.query(DbWeighingSlip).filter(
+        DbWeighingSlip.is_voided == 0,
+        DbWeighingSlip.del_flag == 0,
+        DbWeighingSlip.tenant_no == tenant_no,
+    )
+    return apply_db_scope(query, DbWeighingSlip, user, db, tenant_no)
+
+
 @router.get("/stats", response_model=DashboardStats)
 def get_dashboard_stats(
     db: Session = Depends(get_db),
@@ -28,36 +47,30 @@ def get_dashboard_stats(
     today = date.today()
 
     today_inbound = float(
-        db.query(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
+        _scoped_inventory_query(db, tenant_no, user)
         .filter(
             DbInboundOutbound.date == today,
             DbInboundOutbound.business_type == "入库",
-            DbInboundOutbound.del_flag == 0,
-            DbInboundOutbound.tenant_no == tenant_no,
         )
+        .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
         .scalar()
         or 0
     )
 
     today_outbound = float(
-        db.query(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
+        _scoped_inventory_query(db, tenant_no, user)
         .filter(
             DbInboundOutbound.date == today,
             DbInboundOutbound.business_type == "出库",
-            DbInboundOutbound.del_flag == 0,
-            DbInboundOutbound.tenant_no == tenant_no,
         )
+        .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
         .scalar()
         or 0
     )
 
     abnormal_orders = int(
-        db.query(func.count(DbWeighingSlip.id))
-        .filter(
-            DbWeighingSlip.is_voided == 0,
-            DbWeighingSlip.del_flag == 0,
-            DbWeighingSlip.tenant_no == tenant_no,
-        )
+        _scoped_weighing_query(db, tenant_no, user)
+        .with_entities(func.count(DbWeighingSlip.id))
         .scalar()
         or 0
     )
@@ -101,36 +114,49 @@ def get_outbound_by_partner_cargo(
     return _partner_cargo_stats(db, user, business_type="出库")
 
 
+def _weighing_actual_map(
+    db: Session, tenant_no: str, user: DbUserContext, inventory_ids: list[str]
+) -> Dict[str, float]:
+    if not inventory_ids:
+        return {}
+    rows = (
+        _scoped_weighing_query(db, tenant_no, user)
+        .filter(DbWeighingSlip.inbound_outbound_id.in_(inventory_ids))
+        .all()
+    )
+    actual_map: Dict[str, float] = {}
+    for row in rows:
+        if not row.inbound_outbound_id:
+            continue
+        actual_map[row.inbound_outbound_id] = actual_map.get(row.inbound_outbound_id, 0.0) + (
+            float(row.net_weight or 0) / 1000.0
+        )
+    return actual_map
+
+
 def _cargo_stats(db: Session, user: DbUserContext, *, business_type: str) -> list[DashboardCargoStat]:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
     today = date.today()
-    query = text(
-        """
-        SELECT
-            COALESCE(io.product, ws.cargo_name, '未填写货物') AS label,
-            COALESCE(SUM(io.quantity), 0) AS planned,
-            COALESCE(SUM(ws.net_weight) / 1000.0, 0) AS actual
-        FROM db_inbound_outbound io
-        LEFT JOIN db_weighing_slip ws
-            ON ws.inbound_outbound_id = io.id
-            AND ws.is_voided = 0
-            AND ws.del_flag = 0
-            AND ws.tenant_no = io.tenant_no
-        WHERE io.date = :today
-            AND io.business_type = :business_type
-            AND io.del_flag = 0
-            AND io.tenant_no = :tenant_no
-        GROUP BY COALESCE(io.product, ws.cargo_name, '未填写货物')
-        ORDER BY planned DESC
-        """
+    inventories = (
+        _scoped_inventory_query(db, tenant_no, user)
+        .filter(
+            DbInboundOutbound.date == today,
+            DbInboundOutbound.business_type == business_type,
+        )
+        .all()
     )
-    rows = db.execute(
-        query,
-        {"today": today, "business_type": business_type, "tenant_no": tenant_no},
-    ).all()
+    actual_map = _weighing_actual_map(db, tenant_no, user, [inv.id for inv in inventories])
+
+    agg: Dict[str, Dict[str, float]] = {}
+    for inv in inventories:
+        label = (inv.product or "").strip() or "未填写货物"
+        bucket = agg.setdefault(label, {"planned": 0.0, "actual": 0.0})
+        bucket["planned"] += float(inv.quantity or 0)
+        bucket["actual"] += actual_map.get(inv.id, 0.0)
+
     return [
-        DashboardCargoStat(label=row.label, planned=float(row.planned), actual=float(row.actual))
-        for row in rows
+        DashboardCargoStat(label=label, planned=values["planned"], actual=values["actual"])
+        for label, values in sorted(agg.items(), key=lambda item: item[1]["planned"], reverse=True)
     ]
 
 
@@ -142,39 +168,27 @@ def _partner_cargo_stats(
 ) -> list[DashboardPartnerCargoStat]:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
     today = date.today()
-    query = text(
-        """
-        SELECT
-            CONCAT(
-                COALESCE(io.partner_company, ''),
-                ' - ',
-                COALESCE(io.product, ws.cargo_name, '未填写货物')
-            ) AS label,
-            COALESCE(SUM(io.quantity), 0) AS planned,
-            COALESCE(SUM(ws.net_weight) / 1000.0, 0) AS actual
-        FROM db_inbound_outbound io
-        LEFT JOIN db_weighing_slip ws
-            ON ws.inbound_outbound_id = io.id
-            AND ws.is_voided = 0
-            AND ws.del_flag = 0
-            AND ws.tenant_no = io.tenant_no
-        WHERE io.date = :today
-            AND io.business_type = :business_type
-            AND io.del_flag = 0
-            AND io.tenant_no = :tenant_no
-        GROUP BY io.partner_company, io.product, ws.cargo_name
-        ORDER BY io.partner_company, planned DESC
-        """
+    inventories = (
+        _scoped_inventory_query(db, tenant_no, user)
+        .filter(
+            DbInboundOutbound.date == today,
+            DbInboundOutbound.business_type == business_type,
+        )
+        .all()
     )
-    rows = db.execute(
-        query,
-        {"today": today, "business_type": business_type, "tenant_no": tenant_no},
-    ).all()
+    actual_map = _weighing_actual_map(db, tenant_no, user, [inv.id for inv in inventories])
+
+    agg: Dict[str, Dict[str, float]] = {}
+    for inv in inventories:
+        product = (inv.product or "").strip() or "未填写货物"
+        label = f"{inv.partner_company or ''} - {product}"
+        bucket = agg.setdefault(label, {"planned": 0.0, "actual": 0.0})
+        bucket["planned"] += float(inv.quantity or 0)
+        bucket["actual"] += actual_map.get(inv.id, 0.0)
+
     return [
-        DashboardPartnerCargoStat(
-            label=row.label,
-            planned=float(row.planned),
-            actual=float(row.actual),
+        DashboardPartnerCargoStat(label=label, planned=values["planned"], actual=values["actual"])
+        for label, values in sorted(
+            agg.items(), key=lambda item: (item[0].split(" - ", 1)[0], -item[1]["planned"])
         )
-        for row in rows
     ]

+ 8 - 3
backend/app/api/db/inventory.py

@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
 from app.core.database import get_db
 from app.core.db_auth import DbUserContext, get_db_current_user, get_db_current_user_optional
 from app.core.tenant_context import get_tenant_no
+from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
 from app.services.db.inventory_id import next_inbound_outbound_id
 from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip
 from app.schemas.db.inventory import (
@@ -101,7 +102,9 @@ def _get_inventory_by_id(db: Session, inventory_id: str) -> DbInboundOutbound:
     return row
 
 
-def _get_inventory(db: Session, tenant_no: str, inventory_id: str) -> DbInboundOutbound:
+def _get_inventory(
+    db: Session, tenant_no: str, inventory_id: str, user: DbUserContext
+) -> DbInboundOutbound:
     row = (
         _base_query(db, tenant_no)
         .filter(DbInboundOutbound.id == inventory_id)
@@ -112,6 +115,7 @@ def _get_inventory(db: Session, tenant_no: str, inventory_id: str) -> DbInboundO
             status_code=status.HTTP_404_NOT_FOUND,
             detail=f"Inventory {inventory_id} not found",
         )
+    assert_db_row_in_scope(db, row, DbInboundOutbound, user, tenant_no)
     return row
 
 
@@ -227,6 +231,7 @@ def list_inventory(
     if business_type:
         query = query.filter(DbInboundOutbound.business_type == business_type)
 
+    query = apply_db_scope(query, DbInboundOutbound, user, db, tenant_no)
     inventories = query.all()
     inventory_ids = [inv.id for inv in inventories]
     weighing_map: dict[str, DbWeighingSlip] = {}
@@ -369,7 +374,7 @@ def get_inventory_site_url(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> InventorySiteUrlRead:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_inventory(db, tenant_no, id)
+    row = _get_inventory(db, tenant_no, id, user)
     token = ensure_site_access_token(row)
     db.commit()
     return InventorySiteUrlRead(site_url=build_site_inventory_url(token))
@@ -420,7 +425,7 @@ def delete_inventory(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> None:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_inventory(db, tenant_no, id)
+    row = _get_inventory(db, tenant_no, id, user)
     row.del_flag = 1
     row.update_by = user.id
     db.commit()

+ 10 - 5
backend/app/api/db/plans.py

@@ -11,6 +11,7 @@ from app.core.database import get_db
 from app.core.db_auth import DbUserContext, get_db_current_user
 from app.core.tenant_context import get_tenant_no
 from app.core.utils import generate_uuid
+from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
 from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip
 from app.schemas.db.inventory import InboundOutboundRead
 from app.schemas.db.plans import (
@@ -117,6 +118,7 @@ def list_plans(
         if start_to:
             query = query.filter(DbPurchasePlan.start_date <= start_to)
 
+    query = apply_db_scope(query, DbPurchasePlan, user, db, tenant_no)
     plans = query.all()
     if not plans:
         return []
@@ -263,7 +265,7 @@ def get_plan(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> PurchasePlanRead:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    plan = _get_plan(db, tenant_no, id)
+    plan = _get_plan(db, tenant_no, id, user)
     return plan_to_read(plan)
 
 
@@ -275,7 +277,7 @@ def update_plan(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> PurchasePlanRead:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    plan = _get_plan(db, tenant_no, id)
+    plan = _get_plan(db, tenant_no, id, user)
     payload_dict = payload.model_dump(exclude_unset=True)
     _apply_plan_product_update(db, tenant_no, plan, payload_dict)
     _apply_plan_update(plan, payload_dict)
@@ -292,13 +294,15 @@ def delete_plan(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> None:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    plan = _get_plan(db, tenant_no, id)
+    plan = _get_plan(db, tenant_no, id, user)
     plan.del_flag = 1
     plan.update_by = user.id
     db.commit()
 
 
-def _get_plan(db: Session, tenant_no: str, plan_id: str) -> DbPurchasePlan:
+def _get_plan(
+    db: Session, tenant_no: str, plan_id: str, user: DbUserContext
+) -> DbPurchasePlan:
     plan = (
         _base_query(db, tenant_no)
         .filter(DbPurchasePlan.id == plan_id)
@@ -309,6 +313,7 @@ def _get_plan(db: Session, tenant_no: str, plan_id: str) -> DbPurchasePlan:
             status_code=status.HTTP_404_NOT_FOUND,
             detail=f"Plan {plan_id} not found",
         )
+    assert_db_row_in_scope(db, plan, DbPurchasePlan, user, tenant_no)
     return plan
 
 
@@ -375,7 +380,7 @@ def get_plan_related(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> PlanRelatedData:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    plan = _get_plan(db, tenant_no, id)
+    plan = _get_plan(db, tenant_no, id, user)
 
     inventories = (
         db.query(DbInboundOutbound)

+ 11 - 6
backend/app/api/db/weighing.py

@@ -11,6 +11,7 @@ from app.core.database import get_db
 from app.core.db_auth import DbUserContext, get_db_current_user
 from app.core.tenant_context import get_tenant_no
 from app.core.utils import generate_uuid
+from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
 from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip
 from app.schemas.db.inventory import InboundOutboundRead
 from app.schemas.db.plans import PurchasePlanRead
@@ -43,7 +44,9 @@ def _base_query(db: Session, tenant_no: str):
     )
 
 
-def _get_weighing(db: Session, tenant_no: str, weighing_id: str) -> DbWeighingSlip:
+def _get_weighing(
+    db: Session, tenant_no: str, weighing_id: str, user: DbUserContext
+) -> DbWeighingSlip:
     row = (
         db.query(DbWeighingSlip)
         .filter(
@@ -58,6 +61,7 @@ def _get_weighing(db: Session, tenant_no: str, weighing_id: str) -> DbWeighingSl
             status_code=status.HTTP_404_NOT_FOUND,
             detail=f"Weighing {weighing_id} not found",
         )
+    assert_db_row_in_scope(db, row, DbWeighingSlip, user, tenant_no)
     return row
 
 
@@ -162,6 +166,7 @@ def list_weighing(
     if business_type:
         query = query.filter(DbWeighingSlip.business_type == business_type)
 
+    query = apply_db_scope(query, DbWeighingSlip, user, db, tenant_no)
     weighings = query.all()
     inventory_ids = [w.inbound_outbound_id for w in weighings if w.inbound_outbound_id]
     inventory_date_map: dict[str, str] = {}
@@ -294,7 +299,7 @@ def get_weighing(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> WeighingSlipRead:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_weighing(db, tenant_no, id)
+    row = _get_weighing(db, tenant_no, id, user)
     return weighing_to_read(row)
 
 
@@ -306,7 +311,7 @@ def update_weighing(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> WeighingSlipRead:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_weighing(db, tenant_no, id)
+    row = _get_weighing(db, tenant_no, id, user)
 
     if row.is_voided == 1:
         raise HTTPException(
@@ -333,7 +338,7 @@ def void_weighing(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> None:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_weighing(db, tenant_no, id)
+    row = _get_weighing(db, tenant_no, id, user)
 
     if row.is_voided == 1:
         raise HTTPException(
@@ -354,7 +359,7 @@ def delete_weighing(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> None:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_weighing(db, tenant_no, id)
+    row = _get_weighing(db, tenant_no, id, user)
     row.is_voided = 1
     row.update_by = user.id
     db.commit()
@@ -373,7 +378,7 @@ def get_weighing_related(
     user: DbUserContext = Depends(get_db_current_user),
 ) -> WeighingRelatedData:
     tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
-    row = _get_weighing(db, tenant_no, id)
+    row = _get_weighing(db, tenant_no, id, user)
 
     plan = None
     if row.purchase_plan_id:

+ 18 - 3
backend/app/api/monitor.py

@@ -13,16 +13,28 @@ from app.schemas.monitor.operlog import OperLogDetailBody, OperLogListBody
 router = APIRouter(prefix="/web/monitor", tags=["系统监控"])
 
 
+def _user_info(user: dict) -> dict:
+    dept_id = (user.get("region_id") or user.get("dept_id") or "").strip()
+    return {
+        "user_id": (user.get("user_id") or user.get("id") or "").strip(),
+        "role": (user.get("role") or "").strip(),
+        "tenant_no": (user.get("tenant_no") or "").strip(),
+        "dept_id": dept_id,
+        "region_id": dept_id,
+    }
+
+
 @router.post("/operlog/list")
 async def operlog_list(
     body: OperLogListBody = None,
-    _user: dict = Depends(require_admin_or_super_admin),
+    user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
     """操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构)"""
     try:
         page = body.page if body else 1
         page_size = body.page_size if body else 10
+        info = _user_info(user)
         rows, total = list_oper_logs(
             db,
             page=page,
@@ -31,6 +43,8 @@ async def operlog_list(
             business_type=body.business_type if body else None,
             oper_name=body.oper_name if body else None,
             status=body.status if body else None,
+            user_info=info,
+            tenant_no=info.get("tenant_no"),
         )
         total_pages = (total + page_size - 1) // page_size if page_size else 0
         return success_response(
@@ -50,12 +64,13 @@ async def operlog_list(
 @router.post("/operlog/detail")
 async def operlog_detail(
     body: OperLogDetailBody,
-    _user: dict = Depends(require_admin_or_super_admin),
+    user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
     """操作日志详情"""
     try:
-        row = get_oper_log(db, body.id)
+        info = _user_info(user)
+        row = get_oper_log(db, body.id, user_info=info, tenant_no=info.get("tenant_no"))
         if not row:
             return error_response(404, "操作日志不存在")
         return success_response(row)

+ 60 - 3
backend/app/api/workflow.py

@@ -5,11 +5,12 @@ from fastapi import APIRouter, Depends, Query
 from pydantic import AliasChoices, BaseModel, ConfigDict, Field
 from sqlalchemy.orm import Session
 
+from app.core.data_scope import assert_row_in_data_scope
 from app.core.database import get_db
 from app.core.menu_guard import require_admin_or_super_admin
 from app.core.response import success_response, error_response
 from app.crud import workflow as wf_crud
-from app.models.models import SysUser, WfNode, WfProcessDef, WfProcessVersion, WfTask
+from app.models.models import SysUser, WfNode, WfProcessDef, WfProcessInstance, WfProcessVersion, WfTask
 from app.services.workflow_engine import WorkflowEngine
 
 router = APIRouter(prefix="/web/admin/workflow", tags=["工作流管理"])
@@ -29,6 +30,30 @@ def _business_type_label(code: Optional[str]) -> str:
     return _BUSINESS_TYPE_LABELS.get(code, code)
 
 
+def _portal_user_info(user: dict) -> dict:
+    dept_id = (user.get("region_id") or user.get("dept_id") or "").strip()
+    return {
+        "user_id": (user.get("user_id") or user.get("id") or "").strip(),
+        "role": (user.get("role") or "").strip(),
+        "tenant_no": (user.get("tenant_no") or "").strip(),
+        "dept_id": dept_id,
+        "region_id": dept_id,
+    }
+
+
+def _assert_wf_instance_in_scope(
+    db: Session,
+    instance: WfProcessInstance,
+    user: dict,
+    *,
+    skip_scope: bool = False,
+) -> None:
+    if skip_scope:
+        return
+    tenant_no = user.get("tenant_no", "shanxia2.0")
+    assert_row_in_data_scope(db, instance, WfProcessInstance, _portal_user_info(user), tenant_no)
+
+
 # ---------------------------------------------------------------------------
 # 业务表单字段提供者注册表
 # 新增业务类型时,只需添加一个 provider 函数并注册即可,前端无需改动
@@ -357,12 +382,13 @@ async def list_my_instances(
 @router.get("/instances/{instance_id}")
 async def get_instance_detail(
     instance_id: str,
-    _user: dict = Depends(require_admin_or_super_admin),
+    user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
     instance = wf_crud.get_instance(db, instance_id)
     if not instance:
         return error_response(404, "流程实例不存在")
+    _assert_wf_instance_in_scope(db, instance, user)
     tasks = db.query(WfTask).filter(
         WfTask.instance_id == instance_id,
         WfTask.del_flag == 0,
@@ -466,6 +492,15 @@ async def approve_task(
     db: Session = Depends(get_db),
 ):
     try:
+        task = wf_crud.get_task(db, task_id)
+        if not task:
+            return error_response(404, "任务不存在")
+        instance = wf_crud.get_instance(db, task.instance_id)
+        if not instance:
+            return error_response(404, "流程实例不存在")
+        user_id = (user.get("user_id") or user.get("id") or "").strip()
+        skip_scope = task.assignee_user_id == user_id
+        _assert_wf_instance_in_scope(db, instance, user, skip_scope=skip_scope)
         tenant_no = user.get("tenant_no", "shanxia2.0")
         engine = WorkflowEngine(db)
         instance = engine.approve_task(task_id, body.comment or "", tenant_no)
@@ -482,6 +517,15 @@ async def reject_task(
     db: Session = Depends(get_db),
 ):
     try:
+        task = wf_crud.get_task(db, task_id)
+        if not task:
+            return error_response(404, "任务不存在")
+        instance = wf_crud.get_instance(db, task.instance_id)
+        if not instance:
+            return error_response(404, "流程实例不存在")
+        user_id = (user.get("user_id") or user.get("id") or "").strip()
+        skip_scope = task.assignee_user_id == user_id
+        _assert_wf_instance_in_scope(db, instance, user, skip_scope=skip_scope)
         tenant_no = user.get("tenant_no", "shanxia2.0")
         engine = WorkflowEngine(db)
         instance = engine.reject_task(task_id, body.comment or "", tenant_no)
@@ -499,9 +543,22 @@ async def reject_task(
 async def get_business_all_tasks(
     business_type: str,
     business_id: str,
-    _user: dict = Depends(require_admin_or_super_admin),
+    user: dict = Depends(require_admin_or_super_admin),
     db: Session = Depends(get_db),
 ):
+    instances = (
+        db.query(WfProcessInstance)
+        .filter(
+            WfProcessInstance.business_type == business_type,
+            WfProcessInstance.business_id == business_id,
+            WfProcessInstance.del_flag == 0,
+        )
+        .order_by(WfProcessInstance.create_time.asc())
+        .all()
+    )
+    if instances:
+        _assert_wf_instance_in_scope(db, instances[0], user)
+
     tasks = wf_crud.get_all_tasks_for_business(db, business_type, business_id)
 
     business_form_fields = _get_business_form_fields(db, business_type, business_id)

+ 66 - 2
backend/app/crud/monitor/oper_log.py

@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple
 from sqlalchemy import desc, func, or_
 from sqlalchemy.orm import Session
 
-from app.models.models import SysApiRequestLog
+from app.models.models import SysApiRequestLog, SysUser
 
 _FMT = "%Y-%m-%d %H:%M:%S"
 
@@ -99,6 +99,62 @@ def _row_to_oper_log(row: Any) -> Dict[str, Any]:
     }
 
 
+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,
     *,
@@ -108,8 +164,12 @@ def list_oper_logs(
     business_type: Optional[int] = None,
     oper_name: Optional[str] = None,
     status: 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)
+    if user_info:
+        q = _apply_oper_log_scope(q, db, user_info, tenant_no or user_info.get("tenant_no"))
 
     if oper_name:
         kw = f"%{oper_name.strip()}%"
@@ -148,7 +208,9 @@ def list_oper_logs(
     return items[start:end], total
 
 
-def get_oper_log(db: Session, log_id: str) -> Optional[Dict[str, Any]]:
+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)
@@ -156,4 +218,6 @@ def get_oper_log(db: Session, log_id: str) -> Optional[Dict[str, Any]]:
     )
     if not row:
         return None
+    if user_info and not _oper_log_row_in_scope(db, row, user_info, tenant_no):
+        return None
     return _row_to_oper_log(row)

+ 27 - 0
backend/app/services/db_data_scope.py

@@ -0,0 +1,27 @@
+"""DB 地磅模块数据权限辅助(create_by / create_dept_id)。"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from sqlalchemy.orm import Session
+
+from app.core.data_scope import apply_data_scope, assert_row_in_data_scope
+
+if TYPE_CHECKING:
+    from app.core.db_auth import DbUserContext
+
+
+def apply_db_scope(query, model, user: "DbUserContext", db: Session, tenant_no: str):
+    """列表/统计查询应用数据权限。"""
+    return apply_data_scope(query, model, user.to_user_info(), db, tenant_no)
+
+
+def assert_db_row_in_scope(
+    db: Session,
+    row,
+    model,
+    user: "DbUserContext",
+    tenant_no: str,
+) -> None:
+    """详情/改/删前校验行级数据权限。"""
+    assert_row_in_data_scope(db, row, model, user.to_user_info(), tenant_no)