feat: 多租户实时切换 — 前端拦截器统一entity_id + 后端get_entity_id依赖 + dashboard 6端点支持租户过滤
- 前端: api/index.ts拦截器自动附加X-Entity-Id header + entity_id query参数 - 后端: 新增app/deps.py的get_entity_id公共依赖(query→header→默认1) - dashboard.py: summary/kpis/finance-analysis/predict/my-kpis/my-dashboard全部支持entity_id - 解决: 42个页面仅13个传entity_id导致切换企业后数据混乱
This commit is contained in:
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.auth_middleware import require_auth, require_role
|
from app.auth_middleware import require_auth, require_role
|
||||||
|
from app.deps import get_entity_id
|
||||||
from app.models import KPIDefinition, KPIValue, KPIAlert, User
|
from app.models import KPIDefinition, KPIValue, KPIAlert, User
|
||||||
from app.utils.cache import get as cache_get, set as cache_set
|
from app.utils.cache import get as cache_get, set as cache_set
|
||||||
import json
|
import json
|
||||||
@@ -52,15 +53,18 @@ def period_prefix(period_type: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@router.get("/summary")
|
@router.get("/summary")
|
||||||
def get_dashboard_summary(role: str = Query("ceo"), period: str = Query("month"), db: Session = Depends(get_db)):
|
def get_dashboard_summary(role: str = Query("ceo"), period: str = Query("month"),
|
||||||
cache_key = f"summary:{role}:{period}"
|
db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||||
|
cache_key = f"summary:{role}:{period}:{entity_id}"
|
||||||
cached = cache_get("dashboard", cache_key)
|
cached = cache_get("dashboard", cache_key)
|
||||||
if cached:
|
if cached:
|
||||||
return cached
|
return cached
|
||||||
kpi_total = db.query(func.count(KPIDefinition.id)).filter(KPIDefinition.status == "active").scalar()
|
kpi_total = db.query(func.count(KPIDefinition.id)).filter(
|
||||||
|
KPIDefinition.status == "active", KPIDefinition.entity_id == entity_id).scalar()
|
||||||
alert_count = db.query(func.count(KPIAlert.id)).filter(KPIAlert.status == "pending").scalar()
|
alert_count = db.query(func.count(KPIAlert.id)).filter(KPIAlert.status == "pending").scalar()
|
||||||
dims = db.query(KPIDefinition.dimension, func.count(KPIDefinition.id)).filter(
|
dims = db.query(KPIDefinition.dimension, func.count(KPIDefinition.id)).filter(
|
||||||
KPIDefinition.status == "active").group_by(KPIDefinition.dimension).all()
|
KPIDefinition.status == "active", KPIDefinition.entity_id == entity_id
|
||||||
|
).group_by(KPIDefinition.dimension).all()
|
||||||
|
|
||||||
# 读取最近一次同步状态(从日志文件最后一行)
|
# 读取最近一次同步状态(从日志文件最后一行)
|
||||||
sync_status = {"last_sync": None, "status": "unknown", "detail": ""}
|
sync_status = {"last_sync": None, "status": "unknown", "detail": ""}
|
||||||
@@ -94,11 +98,14 @@ def get_dashboard_summary(role: str = Query("ceo"), period: str = Query("month")
|
|||||||
@router.get("/kpis")
|
@router.get("/kpis")
|
||||||
def get_dashboard_kpis(role: str = Query("ceo"), period: str = Query("month"),
|
def get_dashboard_kpis(role: str = Query("ceo"), period: str = Query("month"),
|
||||||
start_date: str = Query(None), end_date: str = Query(None),
|
start_date: str = Query(None), end_date: str = Query(None),
|
||||||
db: Session = Depends(get_db)):
|
db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||||
start, end = parse_period(period, start_date, end_date)
|
start, end = parse_period(period, start_date, end_date)
|
||||||
period_str = start.strftime("%Y-%m")
|
period_str = start.strftime("%Y-%m")
|
||||||
|
|
||||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
kpis = db.query(KPIDefinition).filter(
|
||||||
|
KPIDefinition.status == "active",
|
||||||
|
KPIDefinition.entity_id == entity_id
|
||||||
|
).all()
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for k in kpis:
|
for k in kpis:
|
||||||
@@ -213,6 +220,7 @@ def get_finance_analysis(
|
|||||||
current_user: User = Depends(require_auth),
|
current_user: User = Depends(require_auth),
|
||||||
period: str = Query("month"),
|
period: str = Query("month"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
entity_id: int = Depends(get_entity_id),
|
||||||
):
|
):
|
||||||
"""财务工作台分析数据"""
|
"""财务工作台分析数据"""
|
||||||
period_str = datetime.now().strftime("%Y-%m")
|
period_str = datetime.now().strftime("%Y-%m")
|
||||||
@@ -220,6 +228,7 @@ def get_finance_analysis(
|
|||||||
finance_kpis = db.query(KPIDefinition).filter(
|
finance_kpis = db.query(KPIDefinition).filter(
|
||||||
KPIDefinition.status == "active",
|
KPIDefinition.status == "active",
|
||||||
KPIDefinition.dimension == "finance",
|
KPIDefinition.dimension == "finance",
|
||||||
|
KPIDefinition.entity_id == entity_id,
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
kpi_data = []
|
kpi_data = []
|
||||||
@@ -269,7 +278,7 @@ def get_finance_analysis(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/predict")
|
@router.get("/predict")
|
||||||
def predict_kpis(db: Session = Depends(get_db)):
|
def predict_kpis(db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||||
"""基于历史趋势预测下月KPI值(简单线性回归)"""
|
"""基于历史趋势预测下月KPI值(简单线性回归)"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -281,7 +290,10 @@ def predict_kpis(db: Session = Depends(get_db)):
|
|||||||
next_year += 1
|
next_year += 1
|
||||||
next_period = f"{next_year}-{next_month:02d}"
|
next_period = f"{next_year}-{next_month:02d}"
|
||||||
|
|
||||||
kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all()
|
kpis = db.query(KPIDefinition).filter(
|
||||||
|
KPIDefinition.status == "active",
|
||||||
|
KPIDefinition.entity_id == entity_id
|
||||||
|
).all()
|
||||||
predictions = []
|
predictions = []
|
||||||
|
|
||||||
for k in kpis:
|
for k in kpis:
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""多租户公共依赖 — 从请求头/参数读取当前企业ID"""
|
||||||
|
from fastapi import Request, Header, Query, Depends
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_id(
|
||||||
|
request: Request,
|
||||||
|
x_entity_id: Optional[str] = Header(None, alias="X-Entity-Id"),
|
||||||
|
entity_id: Optional[int] = Query(None, ge=1),
|
||||||
|
) -> int:
|
||||||
|
"""解析当前企业ID:优先query参数 → header → 默认1(酣客)
|
||||||
|
前端拦截器已统一附加 X-Entity-Id header 和 entity_id query参数
|
||||||
|
"""
|
||||||
|
if entity_id is not None:
|
||||||
|
return entity_id
|
||||||
|
if x_entity_id and x_entity_id.isdigit():
|
||||||
|
return int(x_entity_id)
|
||||||
|
# 兼容 body 中的 entity_id(POST场景)
|
||||||
|
if request.method in ("POST", "PUT", "PATCH"):
|
||||||
|
try:
|
||||||
|
body = request.state.body_json or {}
|
||||||
|
if body.get("entity_id"):
|
||||||
|
return int(body["entity_id"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 1 # 默认酣客
|
||||||
@@ -8,6 +8,12 @@ const api = axios.create({
|
|||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('cma_token')
|
const token = localStorage.getItem('cma_token')
|
||||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||||
|
// 多租户实时切换:统一附加当前企业ID(解决29个页面未传entity_id的问题)
|
||||||
|
const entityId = Number(localStorage.getItem('cma_entity_id') || 1)
|
||||||
|
config.headers['X-Entity-Id'] = String(entityId)
|
||||||
|
if (config.method === 'get' || config.method === 'GET') {
|
||||||
|
config.params = { ...(config.params || {}), entity_id: entityId }
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user