chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# =============================================================================
|
||||
# 企微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="删除成功")
|
||||
Reference in New Issue
Block a user