Files
shanxia2.0/backend/app/api/db/dashboard.py
T
zlwuandCursor 66d5a8714b feat(db): 地磅、操作日志与工作流补全数据权限过滤
DB 列表/仪表盘 scoped 聚合,操作日志按 user_id 过滤,工作流实例详情与审批前校验 scope。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 21:00:19 +08:00

195 lines
6.4 KiB
Python

from __future__ import annotations
from datetime import date
from typing import Dict
from fastapi import APIRouter, Depends
from sqlalchemy import func
from sqlalchemy.orm import Session
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.models.db_models import DbInboundOutbound, DbWeighingSlip
from app.schemas.db.dashboard import (
DashboardCargoStat,
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),
user: DbUserContext = Depends(get_db_current_user),
) -> DashboardStats:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
today = date.today()
today_inbound = float(
_scoped_inventory_query(db, tenant_no, user)
.filter(
DbInboundOutbound.date == today,
DbInboundOutbound.business_type == "入库",
)
.with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
.scalar()
or 0
)
today_outbound = float(
_scoped_inventory_query(db, tenant_no, user)
.filter(
DbInboundOutbound.date == today,
DbInboundOutbound.business_type == "出库",
)
.with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
.scalar()
or 0
)
abnormal_orders = int(
_scoped_weighing_query(db, tenant_no, user)
.with_entities(func.count(DbWeighingSlip.id))
.scalar()
or 0
)
return DashboardStats(
today_inbound=today_inbound,
today_outbound=today_outbound,
abnormal_orders=abnormal_orders,
)
@router.get("/inbound/by-cargo", response_model=list[DashboardCargoStat])
def get_inbound_by_cargo(
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[DashboardCargoStat]:
return _cargo_stats(db, user, business_type="入库")
@router.get("/inbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat])
def get_inbound_by_partner_cargo(
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[DashboardPartnerCargoStat]:
return _partner_cargo_stats(db, user, business_type="入库")
@router.get("/outbound/by-cargo", response_model=list[DashboardCargoStat])
def get_outbound_by_cargo(
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[DashboardCargoStat]:
return _cargo_stats(db, user, business_type="出库")
@router.get("/outbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat])
def get_outbound_by_partner_cargo(
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> list[DashboardPartnerCargoStat]:
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()
inventories = (
_scoped_inventory_query(db, tenant_no, user)
.filter(
DbInboundOutbound.date == today,
DbInboundOutbound.business_type == business_type,
)
.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=label, planned=values["planned"], actual=values["actual"])
for label, values in sorted(agg.items(), key=lambda item: item[1]["planned"], reverse=True)
]
def _partner_cargo_stats(
db: Session,
user: DbUserContext,
*,
business_type: str,
) -> list[DashboardPartnerCargoStat]:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
today = date.today()
inventories = (
_scoped_inventory_query(db, tenant_no, user)
.filter(
DbInboundOutbound.date == today,
DbInboundOutbound.business_type == business_type,
)
.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=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"])
)
]