251 lines
8.0 KiB
Python
251 lines
8.0 KiB
Python
|
|
# =============================================================================
|
||
|
|
# 企微IT智能服务台 — 业务路由推荐 API
|
||
|
|
# =============================================================================
|
||
|
|
# 说明:提供业务联系人查询和坐席手动发名片功能
|
||
|
|
# - GET /h5/routing/contact — H5查询联系人(按业务类别)
|
||
|
|
# - GET /routing/contacts — 坐席端联系人列表(P1)
|
||
|
|
# - POST /conversations/{id}/send-contact-card — 坐席手动发名片(P1)
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, Query
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.config import settings
|
||
|
|
from app.models.business_contact import BusinessContact
|
||
|
|
from app.models.conversation import Conversation
|
||
|
|
from app.models.message import Message
|
||
|
|
from app.services.routing_service import send_contact_card
|
||
|
|
from app.services.ws_manager import manager as ws_manager
|
||
|
|
from app.utils.response import success_response
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
router = APIRouter()
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# 数据库依赖
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
async def get_db() -> AsyncSession:
|
||
|
|
"""获取异步 DB session。
|
||
|
|
|
||
|
|
使用 app.database 的 session_factory 创建独立 session。
|
||
|
|
"""
|
||
|
|
from app.database import _get_session_factory
|
||
|
|
factory = _get_session_factory()
|
||
|
|
async with factory() as session:
|
||
|
|
yield session
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Schema 定义
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
class ContactResponse(BaseModel):
|
||
|
|
"""联系人信息响应。"""
|
||
|
|
id: int
|
||
|
|
name: str
|
||
|
|
gender: str
|
||
|
|
department: str
|
||
|
|
position: str
|
||
|
|
responsibility: str
|
||
|
|
extension: str = ""
|
||
|
|
service_area: str = ""
|
||
|
|
wecom_userid: str
|
||
|
|
avatar_url: str = ""
|
||
|
|
business_category: str
|
||
|
|
|
||
|
|
|
||
|
|
class ContactListResponse(BaseModel):
|
||
|
|
"""联系人列表响应。"""
|
||
|
|
items: list[ContactResponse]
|
||
|
|
|
||
|
|
|
||
|
|
class SendContactCardRequest(BaseModel):
|
||
|
|
"""坐席手动发名片请求。"""
|
||
|
|
contact_id: int
|
||
|
|
reason: Optional[str] = None
|
||
|
|
|
||
|
|
|
||
|
|
class SendContactCardResponse(BaseModel):
|
||
|
|
"""坐席手动发名片响应。"""
|
||
|
|
message_id: str
|
||
|
|
contact_name: str
|
||
|
|
business_category: str
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# API 端点
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
@router.get("/h5/routing/contact")
|
||
|
|
async def get_h5_routing_contact(
|
||
|
|
business_category: str = Query(..., description="业务类别(行政/人力资源/财务/法务/行政-物业)"),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
):
|
||
|
|
"""H5 端查询业务联系人。
|
||
|
|
|
||
|
|
按 business_category + is_active=true 查询,取第一条有效联系人。
|
||
|
|
H5 前端收到 WS 推送的 contact_card 消息后,也可通过此接口补充查询。
|
||
|
|
|
||
|
|
Args:
|
||
|
|
business_category: 业务类别
|
||
|
|
db: 异步 DB session
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
统一响应格式,data 为联系人信息
|
||
|
|
"""
|
||
|
|
result = await db.execute(
|
||
|
|
select(BusinessContact)
|
||
|
|
.where(
|
||
|
|
BusinessContact.business_category == business_category,
|
||
|
|
BusinessContact.is_active == True, # noqa: E712
|
||
|
|
)
|
||
|
|
.order_by(BusinessContact.id)
|
||
|
|
.limit(1)
|
||
|
|
)
|
||
|
|
contact = result.scalar_one_or_none()
|
||
|
|
|
||
|
|
if not contact:
|
||
|
|
return success_response(data=None, message=f"未找到{business_category}类别的联系人")
|
||
|
|
|
||
|
|
return success_response(data=ContactResponse(
|
||
|
|
id=contact.id,
|
||
|
|
name=contact.name,
|
||
|
|
gender=contact.gender,
|
||
|
|
department=contact.department,
|
||
|
|
position=contact.position,
|
||
|
|
responsibility=contact.responsibility,
|
||
|
|
extension=contact.extension or "",
|
||
|
|
service_area=contact.service_area or "",
|
||
|
|
wecom_userid=contact.wecom_userid,
|
||
|
|
avatar_url=contact.avatar_url or "",
|
||
|
|
business_category=contact.business_category,
|
||
|
|
))
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/routing/contacts")
|
||
|
|
async def get_routing_contacts(
|
||
|
|
business_category: Optional[str] = Query(None, description="业务类别筛选"),
|
||
|
|
keyword: Optional[str] = Query(None, description="姓名/部门关键词搜索"),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
):
|
||
|
|
"""坐席端获取联系人列表(P1)。
|
||
|
|
|
||
|
|
支持按业务类别筛选和关键词搜索,用于坐席手动发名片时的联系人选择面板。
|
||
|
|
|
||
|
|
Args:
|
||
|
|
business_category: 业务类别筛选(可选)
|
||
|
|
keyword: 姓名/部门关键词搜索(可选)
|
||
|
|
db: 异步 DB session
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
统一响应格式,data 为 { items: [ContactResponse, ...] }
|
||
|
|
"""
|
||
|
|
query = select(BusinessContact).where(
|
||
|
|
BusinessContact.is_active == True # noqa: E712
|
||
|
|
)
|
||
|
|
|
||
|
|
if business_category:
|
||
|
|
query = query.where(BusinessContact.business_category == business_category)
|
||
|
|
|
||
|
|
if keyword:
|
||
|
|
# 在姓名或部门中搜索
|
||
|
|
search_pattern = f"%{keyword}%"
|
||
|
|
query = query.where(
|
||
|
|
BusinessContact.name.ilike(search_pattern)
|
||
|
|
| BusinessContact.department.ilike(search_pattern)
|
||
|
|
)
|
||
|
|
|
||
|
|
query = query.order_by(BusinessContact.business_category, BusinessContact.id)
|
||
|
|
|
||
|
|
result = await db.execute(query)
|
||
|
|
contacts = result.scalars().all()
|
||
|
|
|
||
|
|
items = [
|
||
|
|
ContactResponse(
|
||
|
|
id=c.id,
|
||
|
|
name=c.name,
|
||
|
|
gender=c.gender,
|
||
|
|
department=c.department,
|
||
|
|
position=c.position,
|
||
|
|
responsibility=c.responsibility,
|
||
|
|
extension=c.extension or "",
|
||
|
|
service_area=c.service_area or "",
|
||
|
|
wecom_userid=c.wecom_userid,
|
||
|
|
avatar_url=c.avatar_url or "",
|
||
|
|
business_category=c.business_category,
|
||
|
|
)
|
||
|
|
for c in contacts
|
||
|
|
]
|
||
|
|
|
||
|
|
return success_response(data={"items": [item.model_dump() for item in items]})
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/conversations/{conversation_id}/send-contact-card")
|
||
|
|
async def send_contact_card_endpoint(
|
||
|
|
conversation_id: str,
|
||
|
|
request: SendContactCardRequest,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
):
|
||
|
|
"""坐席手动发送名片卡片(P1)。
|
||
|
|
|
||
|
|
坐席在会话中手动选择联系人,后端创建 contact_card 消息并 WS 推送双通道。
|
||
|
|
|
||
|
|
Args:
|
||
|
|
conversation_id: 会话ID
|
||
|
|
request: 包含 contact_id 和可选的 reason
|
||
|
|
db: 异步 DB session
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
统一响应格式,data 为发送结果
|
||
|
|
"""
|
||
|
|
# 1. 查询会话
|
||
|
|
conversation = await db.get(Conversation, conversation_id)
|
||
|
|
if not conversation:
|
||
|
|
return success_response(
|
||
|
|
data=None,
|
||
|
|
message=f"会话不存在: {conversation_id}",
|
||
|
|
)
|
||
|
|
|
||
|
|
# 2. 查询联系人
|
||
|
|
contact = await db.get(BusinessContact, request.contact_id)
|
||
|
|
if not contact:
|
||
|
|
return success_response(
|
||
|
|
data=None,
|
||
|
|
message=f"联系人不存在: {request.contact_id}",
|
||
|
|
)
|
||
|
|
|
||
|
|
if not contact.is_active:
|
||
|
|
return success_response(
|
||
|
|
data=None,
|
||
|
|
message=f"联系人已停用: {contact.name}",
|
||
|
|
)
|
||
|
|
|
||
|
|
# 3. 构建路由说明文本
|
||
|
|
reason = request.reason or f"为您推荐{contact.business_category}服务联系人:{contact.name}"
|
||
|
|
|
||
|
|
employee_id = conversation.employee_id or ""
|
||
|
|
|
||
|
|
# 4. 发送名片三段式消息(复用 routing_service 的发送逻辑)
|
||
|
|
await send_contact_card(
|
||
|
|
db=db,
|
||
|
|
conversation=conversation,
|
||
|
|
employee_id=employee_id,
|
||
|
|
contact=contact,
|
||
|
|
reason=reason,
|
||
|
|
business_category=contact.business_category,
|
||
|
|
routing_confidence=1.0, # 坐席手动发送,置信度设为 1.0
|
||
|
|
)
|
||
|
|
|
||
|
|
return success_response(data=SendContactCardResponse(
|
||
|
|
message_id="sent",
|
||
|
|
contact_name=contact.name,
|
||
|
|
business_category=contact.business_category,
|
||
|
|
))
|