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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-06-28 21:00:19 +08:00
1 parent 2905dc3d5f
commit 66d5a8714b
8 files changed
+284 -92

No files matched your search

+84 -70
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)