feat(db): 地磅、操作日志与工作流补全数据权限过滤
DB 列表/仪表盘 scoped 聚合,操作日志按 user_id 过滤,工作流实例详情与审批前校验 scope。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2905dc3d5f
commit
66d5a8714b
8 files changed
+284
-92
No files matched your search
@@ -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
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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 +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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user