diff --git a/backend/app/api/db/dashboard.py b/backend/app/api/db/dashboard.py index 29d102f5d..5168bbfc9 100644 --- a/backend/app/api/db/dashboard.py +++ b/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 ] diff --git a/backend/app/api/db/inventory.py b/backend/app/api/db/inventory.py index 77421c97a..ddc926a70 100644 --- a/backend/app/api/db/inventory.py +++ b/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() diff --git a/backend/app/api/db/plans.py b/backend/app/api/db/plans.py index dfaed1706..fdfad446e 100644 --- a/backend/app/api/db/plans.py +++ b/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) diff --git a/backend/app/api/db/weighing.py b/backend/app/api/db/weighing.py index 29d57031f..7c0b1dcfc 100644 --- a/backend/app/api/db/weighing.py +++ b/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: diff --git a/backend/app/api/monitor.py b/backend/app/api/monitor.py index 5b3590f3a..a63bec730 100644 --- a/backend/app/api/monitor.py +++ b/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) diff --git a/backend/app/api/workflow.py b/backend/app/api/workflow.py index dbcc29fe5..3fb5d89d6 100644 --- a/backend/app/api/workflow.py +++ b/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) diff --git a/backend/app/crud/monitor/oper_log.py b/backend/app/crud/monitor/oper_log.py index 236a14e23..0c9d131b8 100644 --- a/backend/app/crud/monitor/oper_log.py +++ b/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) diff --git a/backend/app/services/db_data_scope.py b/backend/app/services/db_data_scope.py new file mode 100644 index 000000000..618a4c21b --- /dev/null +++ b/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)