Files
wecom_it_smart_desk/backend/app/api/quick_replies.py
T

257 lines
8.1 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 快速回复模板 API
# =============================================================================
# 说明:坐席端的快速回复模板管理接口,包括:
# 1. GET /api/quick-replies — 获取模板列表(按分类)
# 2. POST /api/quick-replies — 创建模板
# 3. PUT /api/quick-replies/{id} — 更新模板
# 4. DELETE /api/quick-replies/{id} — 删除模板
# =============================================================================
import logging
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, Header, Query
from sqlalchemy import or_, and_
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.agent import Agent
from app.models.quick_reply_template import QuickReplyTemplate
from app.schemas.quick_reply import (
QuickReplyCreate,
QuickReplyResponse,
QuickReplyUpdate,
)
from app.utils.response import AppException, ERR_NOT_FOUND, ERR_UNAUTHORIZED, success_response
logger = logging.getLogger(__name__)
# 创建路由器
router = APIRouter()
# --------------------------------------------------------------------------
# 可选坐席认证(有 token 则认证,无 token 则跳过)
# --------------------------------------------------------------------------
async def get_optional_agent(
authorization: Optional[str] = Header(None, alias="Authorization"),
db: AsyncSession = Depends(get_db),
) -> Optional[Agent]:
"""可选坐席认证依赖。
有 Authorization 头时尝试认证,无或认证失败时返回 None。
Args:
authorization: 请求头中的 Authorization 字段
db: 数据库会话
Returns:
Optional[Agent]: 认证成功返回坐席对象,否则返回 None
"""
if not authorization:
return None
token = authorization.replace("Bearer ", "") if authorization.startswith("Bearer ") else authorization
if not token:
return None
try:
import redis.asyncio as aioredis
from app.config import settings
redis_client = settings.create_redis_client()
try:
agent_user_id = await redis_client.get(f"agent:token:{token}")
if not agent_user_id:
return None
uid = agent_user_id.decode("utf-8") if isinstance(agent_user_id, bytes) else agent_user_id
stmt = select(Agent).where(Agent.user_id == uid)
result = await db.execute(stmt)
agent = result.scalars().first()
return agent
finally:
try:
await redis_client.close()
except Exception:
pass
except Exception as e:
logger.warning(f"可选坐席认证失败: {e}")
return None
# --------------------------------------------------------------------------
# GET /api/quick-replies — 获取模板列表
# --------------------------------------------------------------------------
@router.get("/quick-replies")
async def list_quick_replies(
category: Optional[str] = Query(None, description="按分类过滤: 账号/网络/软件/硬件/通用"),
db: AsyncSession = Depends(get_db),
agent: Optional[Agent] = Depends(get_optional_agent),
):
"""获取快速回复模板列表。
支持按分类过滤,按 sort_order 排序。
坐席端可见性规则:
- 有认证:返回 approved + 自己的 pending_review
- 无认证:只返回 approved
Args:
category: 按分类过滤(可选)
db: 数据库会话
agent: 当前坐席(可选认证)
Returns:
Dict: 统一响应格式,包含模板列表
"""
stmt = select(QuickReplyTemplate).order_by(
QuickReplyTemplate.category, QuickReplyTemplate.sort_order
)
if category:
stmt = stmt.where(QuickReplyTemplate.category == category)
# 状态筛选:坐席端可见性规则
if agent:
# 有认证:approved + 自己的 pending_review
stmt = stmt.where(
or_(
QuickReplyTemplate.status == "approved",
and_(
QuickReplyTemplate.status == "pending_review",
QuickReplyTemplate.submitted_by == agent.id,
),
)
)
else:
# 无认证:只返回 approved
stmt = stmt.where(QuickReplyTemplate.status == "approved")
result = await db.execute(stmt)
templates = list(result.scalars().all())
items = [QuickReplyResponse.model_validate(t).model_dump() for t in templates]
return success_response(data={"items": items})
# --------------------------------------------------------------------------
# POST /api/quick-replies — 创建模板
# --------------------------------------------------------------------------
@router.post("/quick-replies")
async def create_quick_reply(
body: QuickReplyCreate,
db: AsyncSession = Depends(get_db),
):
"""创建快速回复模板。
Args:
body: 创建请求体(包含 category、title、content、variables、sort_order
db: 数据库会话
Returns:
Dict: 统一响应格式,包含创建的模板
"""
template = QuickReplyTemplate(
category=body.category,
title=body.title,
content=body.content,
variables=body.variables,
sort_order=body.sort_order,
)
db.add(template)
await db.flush()
logger.info(f"创建快速回复模板: category={body.category}, title={body.title}")
template_data = QuickReplyResponse.model_validate(template).model_dump()
return success_response(data=template_data)
# --------------------------------------------------------------------------
# PUT /api/quick-replies/{id} — 更新模板
# --------------------------------------------------------------------------
@router.put("/quick-replies/{template_id}")
async def update_quick_reply(
template_id: UUID,
body: QuickReplyUpdate,
db: AsyncSession = Depends(get_db),
):
"""更新快速回复模板。
只更新传入的字段(部分更新)。
Args:
template_id: 模板ID
body: 更新请求体(所有字段可选)
db: 数据库会话
Returns:
Dict: 统一响应格式,包含更新后的模板
"""
# 查找模板
stmt = select(QuickReplyTemplate).where(QuickReplyTemplate.id == template_id)
result = await db.execute(stmt)
template = result.scalars().first()
if not template:
raise ERR_NOT_FOUND
# 只更新传入的字段
if body.category is not None:
template.category = body.category
if body.title is not None:
template.title = body.title
if body.content is not None:
template.content = body.content
if body.variables is not None:
template.variables = body.variables
if body.sort_order is not None:
template.sort_order = body.sort_order
db.add(template)
await db.flush()
logger.info(f"更新快速回复模板: id={template_id}")
template_data = QuickReplyResponse.model_validate(template).model_dump()
return success_response(data=template_data)
# --------------------------------------------------------------------------
# DELETE /api/quick-replies/{id} — 删除模板
# --------------------------------------------------------------------------
@router.delete("/quick-replies/{template_id}")
async def delete_quick_reply(
template_id: UUID,
db: AsyncSession = Depends(get_db),
):
"""删除快速回复模板。
第一步使用物理删除。
Args:
template_id: 模板ID
db: 数据库会话
Returns:
Dict: 统一响应格式
"""
# 查找模板
stmt = select(QuickReplyTemplate).where(QuickReplyTemplate.id == template_id)
result = await db.execute(stmt)
template = result.scalars().first()
if not template:
raise ERR_NOT_FOUND
# 物理删除
await db.delete(template)
await db.flush()
logger.info(f"删除快速回复模板: id={template_id}")
return success_response(data=None, message="删除成功")