WIP-CHECKPOINT[auth-refactor]: 固化工程师崩溃前部分成果 + 同树其他未提交WIP(仅源码,不含密钥/二进制)-- 待重激活工程师续作
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 数据看板 API
|
||||
# =============================================================================
|
||||
# 说明:数据统计接口,为管理后台数据看板提供数据支持
|
||||
# 1. GET /api/admin/stats/overview — 获取整体统计概览
|
||||
# 2. GET /api/admin/stats/conversations — 会话趋势统计
|
||||
# 3. GET /api/admin/stats/agents — 坐席绩效统计
|
||||
# 4. GET /api/admin/stats/satisfaction — 满意度统计
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, select, and_, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.agent import Agent
|
||||
from app.models.conversation import Conversation
|
||||
from app.models.conversation_evaluation import ConversationEvaluation
|
||||
from app.models.conversation_annotation import ConversationAnnotation
|
||||
from app.models.message import Message
|
||||
from app.utils.response import success_response
|
||||
|
||||
from app.api.agents import get_current_agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 辅助函数
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
async def get_date_range(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
) -> tuple[datetime, datetime]:
|
||||
"""解析日期范围参数。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
|
||||
Returns:
|
||||
tuple: (开始时间, 结束时间)
|
||||
"""
|
||||
if end_date:
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
else:
|
||||
end_dt = datetime.now() + timedelta(days=1)
|
||||
|
||||
if start_date:
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
else:
|
||||
start_dt = end_dt - timedelta(days=30) # 默认30天
|
||||
|
||||
return start_dt, end_dt
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/admin/stats/overview — 整体统计概览
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/admin/stats/overview")
|
||||
async def get_overview_stats(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""获取整体统计概览。
|
||||
|
||||
包含:总会话数、待处理会话数、已解决会话数、平均响应时间、满意度等。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
db: 数据库会话
|
||||
admin: 当前管理员
|
||||
|
||||
Returns:
|
||||
Dict: 整体统计数据
|
||||
"""
|
||||
start_dt, end_dt = await get_date_range(start_date, end_date)
|
||||
|
||||
# 总会话数
|
||||
stmt_total = select(func.count(Conversation.id)).where(
|
||||
and_(
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt_total)
|
||||
total_conversations = result.scalar() or 0
|
||||
|
||||
# 待处理会话数(状态为 queued 或 serving)
|
||||
stmt_pending = select(func.count(Conversation.id)).where(
|
||||
and_(
|
||||
Conversation.status.in_(["queued", "serving"]),
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt_pending)
|
||||
pending_conversations = result.scalar() or 0
|
||||
|
||||
# 已解决会话数(状态为 resolved)
|
||||
stmt_resolved = select(func.count(Conversation.id)).where(
|
||||
and_(
|
||||
Conversation.status == "resolved",
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt_resolved)
|
||||
resolved_conversations = result.scalar() or 0
|
||||
|
||||
# 计算满意度(已评价会话的平均评分)
|
||||
stmt_satisfaction = select(
|
||||
func.avg(ConversationEvaluation.score),
|
||||
func.count(ConversationEvaluation.id),
|
||||
).join(
|
||||
Conversation,
|
||||
ConversationEvaluation.conversation_id == Conversation.id,
|
||||
).where(
|
||||
and_(
|
||||
ConversationEvaluation.created_at >= start_dt,
|
||||
ConversationEvaluation.created_at < end_dt,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt_satisfaction)
|
||||
satisfaction_row = result.first()
|
||||
avg_satisfaction = float(satisfaction_row[0]) if satisfaction_row[0] else 0.0
|
||||
evaluated_count = satisfaction_row[1] or 0
|
||||
|
||||
# 计算平均响应时间(第一条坐席消息与第一条消息的时间差)
|
||||
# 简化计算:resolved会话的平均解决时长
|
||||
stmt_duration = select(func.avg(
|
||||
func.extract('epoch', Conversation.updated_at) - func.extract('epoch', Conversation.created_at)
|
||||
)).where(
|
||||
and_(
|
||||
Conversation.status == "resolved",
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt_duration)
|
||||
avg_duration_seconds = result.scalar() or 0
|
||||
avg_duration_minutes = avg_duration_seconds / 60 if avg_duration_seconds else 0
|
||||
|
||||
data = {
|
||||
"total_conversations": total_conversations,
|
||||
"pending_conversations": pending_conversations,
|
||||
"resolved_conversations": resolved_conversations,
|
||||
"resolution_rate": round(resolved_conversations / total_conversations * 100, 1) if total_conversations > 0 else 0,
|
||||
"avg_satisfaction": round(avg_satisfaction, 2),
|
||||
"evaluated_count": evaluated_count,
|
||||
"avg_duration_minutes": round(avg_duration_minutes, 1),
|
||||
}
|
||||
|
||||
return success_response(data=data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/admin/stats/conversations — 会话趋势统计
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/admin/stats/conversations")
|
||||
async def get_conversation_stats(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""获取会话趋势统计。
|
||||
|
||||
按天统计每日会话数、解决数。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
db: 数据库会话
|
||||
admin: 当前管理员
|
||||
|
||||
Returns:
|
||||
Dict: 趋势数据列表
|
||||
"""
|
||||
start_dt, end_dt = await get_date_range(start_date, end_date)
|
||||
|
||||
# 按天统计会话数
|
||||
stmt = select(
|
||||
func.date(Conversation.created_at).label("date"),
|
||||
func.count(Conversation.id).label("total"),
|
||||
).where(
|
||||
and_(
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt,
|
||||
)
|
||||
).group_by(
|
||||
func.date(Conversation.created_at)
|
||||
).order_by(
|
||||
func.date(Conversation.created_at)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
# 转换为日期+统计的格式
|
||||
trend_data = []
|
||||
for row in rows:
|
||||
date_val = row.date
|
||||
if isinstance(date_val, datetime):
|
||||
date_str = date_val.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_str = str(date_val)
|
||||
|
||||
trend_data.append({
|
||||
"date": date_str,
|
||||
"total": row.total,
|
||||
})
|
||||
|
||||
return success_response(data={"items": trend_data})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/admin/stats/agents — 坐席绩效统计
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/admin/stats/agents")
|
||||
async def get_agent_stats(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""获取坐席绩效统计。
|
||||
|
||||
统计各坐席的处理会话数、解决数、平均响应时间。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
db: 数据库会话
|
||||
admin: 当前管理员
|
||||
|
||||
Returns:
|
||||
Dict: 坐席绩效列表
|
||||
"""
|
||||
start_dt, end_dt = await get_date_range(start_date, end_date)
|
||||
|
||||
# 统计各坐席的会话数
|
||||
stmt = select(
|
||||
Conversation.assigned_agent_id,
|
||||
func.count(Conversation.id).label("total"),
|
||||
func.sum(
|
||||
func.case((Conversation.status == "resolved", 1), else_=0)
|
||||
).label("resolved"),
|
||||
).where(
|
||||
and_(
|
||||
Conversation.assigned_agent_id.isnot(None),
|
||||
Conversation.created_at >= start_dt,
|
||||
Conversation.created_at < end_dt,
|
||||
)
|
||||
).group_by(
|
||||
Conversation.assigned_agent_id
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
# 获取坐席信息
|
||||
agent_ids = [row[0] for row in rows if row[0]]
|
||||
agent_stmt = select(Agent.id, Agent.name).where(Agent.id.in_(agent_ids))
|
||||
agent_result = await db.execute(agent_stmt)
|
||||
agent_map = {a.id: a.name for a in agent_result.scalars().all()}
|
||||
|
||||
# 转换为坐席绩效数据
|
||||
agent_data = []
|
||||
for row in rows:
|
||||
if not row[0]:
|
||||
continue
|
||||
agent_id = row[0]
|
||||
agent_data.append({
|
||||
"agent_id": agent_id,
|
||||
"agent_name": agent_map.get(agent_id, "未知"),
|
||||
"total_conversations": row[1],
|
||||
"resolved_conversations": row[2] or 0,
|
||||
"resolution_rate": round((row[2] or 0) / row[1] * 100, 1) if row[1] > 0 else 0,
|
||||
})
|
||||
|
||||
# 按处理数排序
|
||||
agent_data.sort(key=lambda x: x["total_conversations"], reverse=True)
|
||||
|
||||
return success_response(data={"items": agent_data})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/admin/stats/satisfaction — 满意度统计
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/admin/stats/satisfaction")
|
||||
async def get_satisfaction_stats(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""获取满意度统计。
|
||||
|
||||
统计评分分布、各表情占比。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
db: 数据库会话
|
||||
admin: 当前管理员
|
||||
|
||||
Returns:
|
||||
Dict: 满意度统计数据
|
||||
"""
|
||||
start_dt, end_dt = await get_date_range(start_date, end_date)
|
||||
|
||||
# 评分分布统计
|
||||
stmt = select(
|
||||
ConversationEvaluation.score,
|
||||
func.count(ConversationEvaluation.id).label("count"),
|
||||
).join(
|
||||
Conversation,
|
||||
ConversationEvaluation.conversation_id == Conversation.id,
|
||||
).where(
|
||||
and_(
|
||||
ConversationEvaluation.created_at >= start_dt,
|
||||
ConversationEvaluation.created_at < end_dt,
|
||||
)
|
||||
).group_by(
|
||||
ConversationEvaluation.score
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
# 评分分布
|
||||
score_distribution = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
|
||||
for row in rows:
|
||||
if row[0] in score_distribution:
|
||||
score_distribution[row[0]] = row[1]
|
||||
|
||||
# 表情分布
|
||||
stmt_emoji = select(
|
||||
ConversationEvaluation.emoji,
|
||||
func.count(ConversationEvaluation.id).label("count"),
|
||||
).join(
|
||||
Conversation,
|
||||
ConversationEvaluation.conversation_id == Conversation.id,
|
||||
).where(
|
||||
and_(
|
||||
ConversationEvaluation.created_at >= start_dt,
|
||||
ConversationEvaluation.created_at < end_dt,
|
||||
ConversationEvaluation.emoji.isnot(None),
|
||||
)
|
||||
).group_by(
|
||||
ConversationEvaluation.emoji
|
||||
)
|
||||
|
||||
result = await db.execute(stmt_emoji)
|
||||
emoji_rows = result.all()
|
||||
|
||||
emoji_distribution = {}
|
||||
for row in emoji_rows:
|
||||
if row[0]:
|
||||
emoji_distribution[row[0]] = row[1]
|
||||
|
||||
# 计算平均分
|
||||
stmt_avg = select(func.avg(ConversationEvaluation.score)).join(
|
||||
Conversation,
|
||||
ConversationEvaluation.conversation_id == Conversation.id,
|
||||
).where(
|
||||
and_(
|
||||
ConversationEvaluation.created_at >= start_dt,
|
||||
ConversationEvaluation.created_at < end_dt,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt_avg)
|
||||
avg_score = result.scalar() or 0
|
||||
|
||||
data = {
|
||||
"avg_score": round(float(avg_score), 2),
|
||||
"total_evaluated": sum(score_distribution.values()),
|
||||
"score_distribution": [
|
||||
{"score": k, "count": v} for k, v in sorted(score_distribution.items())
|
||||
],
|
||||
"emoji_distribution": [
|
||||
{"emoji": k, "count": v} for k, v in emoji_distribution.items()
|
||||
],
|
||||
}
|
||||
|
||||
return success_response(data=data)
|
||||
Reference in New Issue
Block a user