WIP-CHECKPOINT[auth-refactor]: 固化工程师崩溃前部分成果 + 同树其他未提交WIP(仅源码,不含密钥/二进制)-- 待重激活工程师续作
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 满意度评价 API
|
||||
# =============================================================================
|
||||
# 说明:满意度评价相关接口
|
||||
# 1. POST /api/conversation/{conversation_id}/evaluate - 提交评价
|
||||
# 2. GET /api/conversation/{conversation_id}/evaluation - 获取会话评价
|
||||
# 3. GET /api/evaluations/stats - 获取评价统计(管理后台)
|
||||
# 4. POST /api/conversations/{id}/send-evaluation-invite - 发送评价邀请(坐席端触发)
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
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.schemas.evaluation import (
|
||||
EvaluationInviteRequest,
|
||||
EvaluationStatsItem,
|
||||
EvaluationStatsResponse,
|
||||
EvaluationSubmitRequest,
|
||||
EvaluationResponse,
|
||||
)
|
||||
from app.services.wecom_service import WecomService
|
||||
from app.utils.response import AppException, success_response
|
||||
|
||||
# H5认证依赖(从 h5.py 导入)
|
||||
from app.api.h5 import _get_current_employee
|
||||
from app.models.employee import Employee
|
||||
|
||||
# 坐席认证依赖(从 agents.py 导入)
|
||||
from app.api.agents import get_current_agent
|
||||
|
||||
# RBAC 权限装饰器
|
||||
from app.dependencies import UserInfo, get_current_user, get_redis, require_permission
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 表情标签映射
|
||||
# --------------------------------------------------------------------------
|
||||
EMOJI_LABELS = {
|
||||
"satisfied": "满意",
|
||||
"neutral": "一般",
|
||||
"dissatisfied": "不满意",
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversation/{conversation_id}/evaluate - 提交评价
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversation/{conversation_id}/evaluate")
|
||||
async def submit_evaluation(
|
||||
conversation_id: str,
|
||||
body: EvaluationSubmitRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
employee_id: str = Depends(_get_current_employee),
|
||||
):
|
||||
"""提交满意度评价。
|
||||
|
||||
员工对已结束的会话进行满意度评价。
|
||||
评价要素:星级(1-5)、表情(satisfied/neutral/dissatisfied)、文字反馈(可选)。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 评价请求体
|
||||
db: 数据库会话
|
||||
employee_id: 当前员工ID(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含评价记录
|
||||
"""
|
||||
# 1. 获取员工姓名
|
||||
emp_stmt = select(Employee).where(Employee.employee_id == employee_id)
|
||||
emp_result = await db.execute(emp_stmt)
|
||||
employee = emp_result.scalars().first()
|
||||
employee_name = employee.name if employee else ""
|
||||
|
||||
# 2. 验证会话存在且已结单
|
||||
stmt = select(Conversation).where(Conversation.id == conversation_id)
|
||||
result = await db.execute(stmt)
|
||||
conversation = result.scalars().first()
|
||||
|
||||
if not conversation:
|
||||
raise AppException(3003, "会话不存在")
|
||||
if conversation.status != "resolved":
|
||||
raise AppException(3040, "只能评价已结单的会话")
|
||||
|
||||
# 3. 检查是否已评价(防止重复评价)
|
||||
existing_stmt = select(ConversationEvaluation).where(
|
||||
ConversationEvaluation.conversation_id == conversation_id,
|
||||
ConversationEvaluation.employee_id == employee_id,
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
existing = existing_result.scalars().first()
|
||||
|
||||
if existing:
|
||||
raise AppException(3041, "您已对该会话提交过评价")
|
||||
|
||||
# 4. 创建评价记录
|
||||
evaluation = ConversationEvaluation(
|
||||
id=None, # UUID自动生成
|
||||
conversation_id=conversation_id,
|
||||
employee_id=employee_id,
|
||||
employee_name=employee_name,
|
||||
star_rating=body.star_rating,
|
||||
emoji=body.emoji,
|
||||
feedback_text=body.feedback_text,
|
||||
)
|
||||
db.add(evaluation)
|
||||
await db.commit()
|
||||
await db.refresh(evaluation)
|
||||
|
||||
logger.info(
|
||||
f"员工 {employee_name} 提交评价: "
|
||||
f"会话={conversation_id}, 星级={body.star_rating}, 表情={body.emoji}"
|
||||
)
|
||||
|
||||
response_data = EvaluationResponse.model_validate(evaluation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/conversation/{conversation_id}/evaluation - 获取会话评价
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/conversation/{conversation_id}/evaluation")
|
||||
async def get_evaluation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取会话的评价记录。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含评价记录(如果已评价)
|
||||
"""
|
||||
stmt = select(ConversationEvaluation).where(
|
||||
ConversationEvaluation.conversation_id == conversation_id
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
evaluation = result.scalars().first()
|
||||
|
||||
if not evaluation:
|
||||
return success_response(data=None)
|
||||
|
||||
response_data = EvaluationResponse.model_validate(evaluation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/evaluations/stats - 获取评价统计
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/evaluations/stats")
|
||||
@require_permission("evaluation", "read", "all")
|
||||
async def get_evaluation_stats(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: UserInfo = Depends(get_current_user),
|
||||
):
|
||||
"""获取满意度评价统计数据。
|
||||
|
||||
供管理后台查看评价统计信息,包括:
|
||||
- 总评价数
|
||||
- 平均星级
|
||||
- 星级分布
|
||||
- 表情分布
|
||||
- 最近评价记录
|
||||
|
||||
Args:
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含统计数据
|
||||
"""
|
||||
# 1. 获取总评价数
|
||||
total_stmt = select(func.count(ConversationEvaluation.id))
|
||||
total_result = await db.execute(total_stmt)
|
||||
total_count = total_result.scalar() or 0
|
||||
|
||||
# 2. 获取平均星级
|
||||
avg_stmt = select(func.avg(ConversationEvaluation.star_rating))
|
||||
avg_result = await db.execute(avg_stmt)
|
||||
avg_star_rating = float(avg_result.scalar() or 0)
|
||||
|
||||
# 3. 星级分布统计
|
||||
star_dist_stmt = select(
|
||||
ConversationEvaluation.star_rating,
|
||||
func.count(ConversationEvaluation.id).label("count"),
|
||||
).group_by(ConversationEvaluation.star_rating)
|
||||
star_dist_result = await db.execute(star_dist_stmt)
|
||||
star_rows = star_dist_result.all()
|
||||
|
||||
star_distribution = []
|
||||
for star in range(1, 6):
|
||||
count = next((row.count for row in star_rows if row.star_rating == star), 0)
|
||||
percentage = (count / total_count * 100) if total_count > 0 else 0
|
||||
star_distribution.append(
|
||||
EvaluationStatsItem(
|
||||
label=f"{star}星",
|
||||
count=count,
|
||||
percentage=round(percentage, 1),
|
||||
)
|
||||
)
|
||||
|
||||
# 4. 表情分布统计
|
||||
emoji_dist_stmt = select(
|
||||
ConversationEvaluation.emoji,
|
||||
func.count(ConversationEvaluation.id).label("count"),
|
||||
).group_by(ConversationEvaluation.emoji)
|
||||
emoji_dist_result = await db.execute(emoji_dist_stmt)
|
||||
emoji_rows = emoji_dist_result.all()
|
||||
|
||||
emoji_distribution = []
|
||||
for emoji_key in ["satisfied", "neutral", "dissatisfied"]:
|
||||
count = next((row.count for row in emoji_rows if row.emoji == emoji_key), 0)
|
||||
percentage = (count / total_count * 100) if total_count > 0 else 0
|
||||
emoji_distribution.append(
|
||||
EvaluationStatsItem(
|
||||
label=EMOJI_LABELS.get(emoji_key, emoji_key),
|
||||
count=count,
|
||||
percentage=round(percentage, 1),
|
||||
)
|
||||
)
|
||||
|
||||
# 5. 最近评价记录
|
||||
recent_stmt = (
|
||||
select(ConversationEvaluation)
|
||||
.order_by(ConversationEvaluation.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
recent_result = await db.execute(recent_stmt)
|
||||
recent_evaluations = recent_result.scalars().all()
|
||||
|
||||
recent_list = [
|
||||
EvaluationResponse.model_validate(e).model_dump()
|
||||
for e in recent_evaluations
|
||||
]
|
||||
|
||||
response_data = EvaluationStatsResponse(
|
||||
total_count=total_count,
|
||||
avg_star_rating=round(avg_star_rating, 2),
|
||||
star_distribution=star_distribution,
|
||||
emoji_distribution=emoji_distribution,
|
||||
recent_evaluations=recent_list,
|
||||
).model_dump()
|
||||
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/send-evaluation-invite - 发送评价邀请
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/send-evaluation-invite")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def send_evaluation_invite(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
redis: aioredis.Redis = Depends(get_redis),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""发送评价邀请(坐席结单后触发)。
|
||||
|
||||
坐席点击"结单"后,系统自动向员工推送评价邀请消息。
|
||||
员工点击消息中的链接可进入H5页面提交评价。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
redis: Redis连接
|
||||
current_agent: 当前坐席
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式
|
||||
"""
|
||||
# 1. 验证会话存在
|
||||
stmt = select(Conversation).where(Conversation.id == conversation_id)
|
||||
result = await db.execute(stmt)
|
||||
conversation = result.scalars().first()
|
||||
|
||||
if not conversation:
|
||||
raise AppException(3003, "会话不存在")
|
||||
|
||||
# 2. 验证会话已结单
|
||||
if conversation.status != "resolved":
|
||||
raise AppException(3042, "只能对已结单的会话发送评价邀请")
|
||||
|
||||
# 3. 检查是否已评价
|
||||
eval_stmt = select(ConversationEvaluation).where(
|
||||
ConversationEvaluation.conversation_id == conversation_id
|
||||
)
|
||||
eval_result = await db.execute(eval_stmt)
|
||||
existing_eval = eval_result.scalars().first()
|
||||
|
||||
if existing_eval:
|
||||
raise AppException(3043, "该会话已收到评价,无需再次邀请")
|
||||
|
||||
# 4. 通过企微发送评价邀请消息
|
||||
try:
|
||||
wecom_service = WecomService(redis)
|
||||
|
||||
# 构建评价邀请消息内容
|
||||
agent_name = current_agent.name if current_agent else "IT服务台"
|
||||
content = (
|
||||
f"您好!您与 {agent_name} 的会话已结束。\n\n"
|
||||
f"请对本次服务进行评价,帮助我们改进服务质量。\n\n"
|
||||
f"点击下方链接进行评价 >>"
|
||||
)
|
||||
|
||||
# TODO: 后续接入企微应用消息推送
|
||||
# message_data = {
|
||||
# "touser": conversation.employee_id,
|
||||
# "msgtype": "text",
|
||||
# "agentid": settings.WECOM_AGENT_ID,
|
||||
# "text": {"content": content},
|
||||
# }
|
||||
# await wecom_service.send_message(message_data)
|
||||
|
||||
logger.info(
|
||||
f"发送评价邀请: 会话={conversation_id}, "
|
||||
f"员工={conversation.employee_id}, 坐席={agent_name}"
|
||||
)
|
||||
|
||||
# 关闭企微服务连接
|
||||
await wecom_service.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"发送评价邀请失败: {e}")
|
||||
# 失败不影响结单流程,只记录日志
|
||||
|
||||
return success_response(data={"message": "评价邀请已发送"})
|
||||
Reference in New Issue
Block a user