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

257 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微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="删除成功")