feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# =============================================================================
|
||||
# 企微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,
|
||||
))
|
||||
Reference in New Issue
Block a user