Files

245 lines
9.1 KiB
Python

"""客户维度KPI看板 API — P0-2"""
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func, desc
from typing import Optional
from datetime import datetime, timedelta
from app.database import get_db
from app.auth_middleware import require_auth, require_role
from app.models import KPIDefinition, KPIValue, KPIAlert, User
import logging
logger = logging.getLogger("cma.customer")
router = APIRouter(prefix="/api/cma/customer-dashboard", tags=["客户看板"],
dependencies=[Depends(require_role("ceo", "finance", "business", "it"))],
)
def parse_period(period_type: str, start_date: str = None, end_date: str = None):
"""解析时间区间"""
today = datetime.now()
if period_type == "month":
start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = today
elif period_type == "quarter":
q = (today.month - 1) // 3
start = today.replace(month=q*3+1, day=1, hour=0, minute=0, second=0, microsecond=0)
end = today
elif period_type == "year":
start = today.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
end = today
elif period_type == "custom" and start_date and end_date:
start = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
else:
start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = today
return start, end
@router.get("")
def list_customer_kpis(
period: str = Query("month"),
start_date: str = Query(None),
end_date: str = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(require_auth),
):
"""获取客户维度KPI列表(含最新值、预警、趋势)"""
start, end = parse_period(period, start_date, end_date)
period_str = start.strftime("%Y-%m")
# 只查 customer 维度的 KPI
kpis = db.query(KPIDefinition).filter(
KPIDefinition.status == "active",
KPIDefinition.dimension == "customer",
).order_by(KPIDefinition.kpi_code).all()
result = []
for k in kpis:
base_query = db.query(KPIValue).filter(KPIValue.kpi_id == k.id)
if period == "month":
latest = base_query.filter(KPIValue.period == period_str).order_by(KPIValue.id.desc()).first()
elif period == "quarter":
q_month = (datetime.now().month - 1) // 3
months = [f"{datetime.now().year}-{m:02d}" for m in range(q_month*3+1, q_month*3+4)]
values = base_query.filter(KPIValue.period.in_(months)).all()
latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None
latest = type('obj', (object,), {"actual_value": latest_val, "period": f"{months[0]}~{months[-1]}"})() if latest_val else None
elif period == "year":
values = base_query.filter(KPIValue.period.like(f"{period_str[:4]}%")).all()
latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None
latest = type('obj', (object,), {"actual_value": latest_val, "period": period_str[:4]})() if latest_val else None
elif period == "custom" and start_date and end_date:
periods = []
d = start
while d <= end:
periods.append(d.strftime("%Y-%m"))
d += timedelta(days=32)
d = d.replace(day=1)
values = base_query.filter(KPIValue.period.in_(set(periods))).all()
latest_val = sum(v.actual_value for v in values if v.actual_value) if values else None
latest = type('obj', (object,), {"actual_value": latest_val, "period": f"{start_date}~{end_date}"})() if latest_val else None
else:
latest = base_query.order_by(KPIValue.period.desc()).first()
# 最新预警
alert = db.query(KPIAlert).filter(
KPIAlert.kpi_id == k.id,
KPIAlert.status == "pending",
).order_by(KPIAlert.id.desc()).first()
# 趋势(环比变化率)
trend = None
achievement_rate = None
period_values = []
if latest and latest.actual_value:
prev_period_str = None
if period == "month":
year_s, month_s = period_str.split("-")
y_s, m_s = int(year_s), int(month_s)
m_s -= 1
if m_s <= 0:
m_s += 12
y_s -= 1
prev_period_str = f"{y_s}-{m_s:02d}"
if prev_period_str:
prev_val = db.query(KPIValue).filter(
KPIValue.kpi_id == k.id,
KPIValue.period == prev_period_str,
).order_by(KPIValue.id.desc()).first()
if prev_val and prev_val.actual_value and prev_val.actual_value > 0:
trend = round((latest.actual_value - prev_val.actual_value) / prev_val.actual_value * 100, 2)
elif prev_val and prev_val.actual_value and prev_val.actual_value == 0:
trend = 100.0 if latest.actual_value > 0 else 0
# 达成率
if latest and latest.actual_value and k.target_value and k.target_value > 0:
achievement_rate = round(latest.actual_value / k.target_value * 100, 1)
# 最近6期趋势数据
period_q = db.query(KPIValue).filter(
KPIValue.kpi_id == k.id,
).order_by(KPIValue.period.desc()).limit(6).all()
period_values = [
{"period": v.period, "value": v.actual_value}
for v in reversed(period_q) if v.actual_value is not None
]
result.append({
"id": k.id,
"kpi_code": k.kpi_code,
"kpi_name": k.kpi_name,
"dimension": k.dimension,
"category": k.category,
"unit": k.unit,
"target_value": k.target_value,
"actual_value": latest.actual_value if latest else None,
"period": latest.period if latest else None,
"alert_level": alert.alert_level if alert else "none",
"alert_message": alert.alert_message if alert else None,
"frequency": k.frequency,
"responsible_dept": k.responsible_dept,
"responsible_user": k.responsible_user,
"trend": trend,
"achievement_rate": achievement_rate,
"period_values": period_values,
"kpi_name": k.kpi_name,
})
return {"data": result, "period": period, "total": len(result)}
@router.get("/trend/{kpi_id}")
def get_kpi_trend(
kpi_id: int,
months: int = Query(12, ge=3, le=24),
db: Session = Depends(get_db),
):
"""获取单个KPI的历史趋势数据"""
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
if not kpi:
raise HTTPException(404, "KPI不存在")
# 获取最近N期数据
values = db.query(KPIValue).filter(
KPIValue.kpi_id == kpi_id,
).order_by(KPIValue.period.desc()).limit(months).all()
trend_data = [
{"period": v.period, "value": v.actual_value}
for v in reversed(values) if v.actual_value is not None
]
# 计算预警水平和触发时间
alerts = db.query(KPIAlert).filter(
KPIAlert.kpi_id == kpi_id,
).order_by(KPIAlert.created_at.desc()).limit(10).all()
alert_logs = [
{
"level": a.alert_level,
"message": a.alert_message,
"time": a.created_at.isoformat() if a.created_at else None,
"status": a.status,
}
for a in alerts
]
return {
"kpi": {
"id": kpi.id,
"kpi_code": kpi.kpi_code,
"kpi_name": kpi.kpi_name,
"target_value": kpi.target_value,
"unit": kpi.unit,
"threshold_green": kpi.threshold_green,
"threshold_yellow": kpi.threshold_yellow,
"threshold_red": kpi.threshold_red,
},
"trend_data": trend_data,
"alerts": alert_logs,
}
@router.get("/summary")
def get_customer_summary(
period: str = Query("month"),
db: Session = Depends(get_db),
):
"""客户维度概要统计"""
total = db.query(func.count(KPIDefinition.id)).filter(
KPIDefinition.status == "active",
KPIDefinition.dimension == "customer",
).scalar() or 0
# 预警统计
pending_alerts = db.query(func.count(KPIAlert.id)).filter(
KPIAlert.status == "pending",
KPIAlert.kpi_id.in_(
db.query(KPIDefinition.id).filter(
KPIDefinition.status == "active",
KPIDefinition.dimension == "customer",
)
),
).scalar() or 0
# 二级类别分布
cat_stats = db.query(
KPIDefinition.category,
func.count(KPIDefinition.id),
).filter(
KPIDefinition.status == "active",
KPIDefinition.dimension == "customer",
).group_by(KPIDefinition.category).all()
return {
"total": total,
"pending_alerts": pending_alerts,
"category_stats": [{"category": c[0], "count": c[1]} for c in cat_stats],
}