A组认证加固: P0兜底+Token刷新+环境检测+OTP+RBRAC落地+P1日志审计+Token撤销 - 全局一致性审查通过
This commit is contained in:
@@ -30,6 +30,7 @@ from app.schemas.conversation import (
|
||||
ConversationStatusUpdate,
|
||||
InviteParticipantRequest,
|
||||
JoinConversationRequest,
|
||||
UpdateTagsRequest,
|
||||
)
|
||||
from app.services.session_service import SessionService
|
||||
from app.services.wecom_service import WecomService
|
||||
@@ -38,6 +39,9 @@ from app.utils.response import AppException, success_response
|
||||
# 坐席认证依赖(从 agents.py 导入)
|
||||
from app.api.agents import get_current_agent
|
||||
|
||||
# RBAC 权限装饰器
|
||||
from app.dependencies import require_role, require_permission
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建路由器
|
||||
@@ -48,6 +52,7 @@ router = APIRouter()
|
||||
# GET /api/conversations — 获取坐席会话列表(全局可见)
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/conversations")
|
||||
@require_permission("conversation", "read", "all")
|
||||
async def list_conversations(
|
||||
status: Optional[str] = Query(None, description="按状态过滤: ai_handling/queued/serving/resolved"),
|
||||
agent_id: Optional[str] = Query(None, description="按坐席ID过滤"),
|
||||
@@ -142,6 +147,7 @@ async def list_conversations(
|
||||
# GET /api/conversations/{id} — 获取会话详情
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/conversations/{conversation_id}")
|
||||
@require_permission("conversation", "read", "all")
|
||||
async def get_conversation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -166,6 +172,7 @@ async def get_conversation(
|
||||
# POST /api/conversations/{id}/assign — 坐席接单
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/assign")
|
||||
@require_permission("conversation", "update", "all")
|
||||
async def assign_conversation(
|
||||
conversation_id: str,
|
||||
body: ConversationAssign,
|
||||
@@ -216,6 +223,7 @@ async def assign_conversation(
|
||||
# POST /api/conversations/{id}/resolve — 结单
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/resolve")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def resolve_conversation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -259,6 +267,7 @@ async def resolve_conversation(
|
||||
# POST /api/conversations/{id}/pin — 置顶/取消置顶
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/pin")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def toggle_pin(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -285,6 +294,7 @@ async def toggle_pin(
|
||||
# POST /api/conversations/{id}/todo — 代办/取消代办
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/todo")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def toggle_todo(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -311,6 +321,7 @@ async def toggle_todo(
|
||||
# POST /api/conversations/{id}/transfer — 转接
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/transfer")
|
||||
@require_permission("conversation", "update", "all")
|
||||
async def transfer_conversation(
|
||||
conversation_id: str,
|
||||
body: ConversationAssign,
|
||||
@@ -342,6 +353,7 @@ async def transfer_conversation(
|
||||
# POST /api/conversations/{id}/grab — 接手会话(抢单)
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/grab")
|
||||
@require_permission("conversation", "update", "all")
|
||||
async def grab_conversation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -439,6 +451,7 @@ async def grab_conversation(
|
||||
# POST /api/conversations/{id}/invite — 摇人(邀请坐席协作)
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/invite")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def invite_collaborator(
|
||||
conversation_id: str,
|
||||
body: ConversationInvite,
|
||||
@@ -484,6 +497,7 @@ async def invite_collaborator(
|
||||
# POST /api/conversations/{id}/leave — 退出协作
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/leave")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def leave_collaboration(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -529,6 +543,7 @@ async def leave_collaboration(
|
||||
# POST /api/conversations/{id}/invite-participant — 邀请员工/部门加入会话
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/invite-participant")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def invite_participant(
|
||||
conversation_id: str,
|
||||
body: InviteParticipantRequest,
|
||||
@@ -587,6 +602,7 @@ async def invite_participant(
|
||||
# POST /api/conversations/{id}/join — 被邀请人加入会话
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/join")
|
||||
@require_permission("conversation", "update", "all")
|
||||
async def join_conversation(
|
||||
conversation_id: str,
|
||||
body: JoinConversationRequest,
|
||||
@@ -622,6 +638,7 @@ async def join_conversation(
|
||||
# DELETE /api/conversations/{id}/participants/{user_id} — 移除参与者
|
||||
# --------------------------------------------------------------------------
|
||||
@router.delete("/conversations/{conversation_id}/participants/{user_id}")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def remove_participant(
|
||||
conversation_id: str,
|
||||
user_id: str,
|
||||
@@ -659,6 +676,7 @@ async def remove_participant(
|
||||
# POST /api/conversations/{id}/leave-participant — 参与者主动退出
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/leave-participant")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def leave_as_participant(
|
||||
conversation_id: str,
|
||||
body: JoinConversationRequest,
|
||||
@@ -686,3 +704,60 @@ async def leave_as_participant(
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{conversation_id}/tags — 保存会话标签
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/tags")
|
||||
@require_permission("conversation", "update", "own")
|
||||
async def update_conversation_tags(
|
||||
conversation_id: str,
|
||||
body: UpdateTagsRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""保存会话标签。
|
||||
|
||||
坐席可以为会话添加/更新标签,如问题分类、优先级、情绪状态等。
|
||||
标签以 JSON 形式存储在会话的 tags 字段中。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 标签更新请求,包含 tags 字典
|
||||
current_agent: 当前坐席(通过认证依赖注入)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
更新后的会话详情
|
||||
"""
|
||||
# 1. 验证会话存在性
|
||||
stmt = select(Conversation).where(Conversation.id == conversation_id)
|
||||
result = await db.execute(stmt)
|
||||
conversation = result.scalars().first()
|
||||
|
||||
if not conversation:
|
||||
raise AppException("会话不存在", code=404)
|
||||
|
||||
# 2. 合并现有标签(如果有)
|
||||
existing_tags = {}
|
||||
if conversation.tags:
|
||||
existing_tags = (
|
||||
dict(conversation.tags) if isinstance(conversation.tags, dict) else {}
|
||||
)
|
||||
|
||||
# 3. 合并新旧标签(body.tags 覆盖同名 key)
|
||||
merged_tags = {**existing_tags, **body.tags}
|
||||
|
||||
# 4. 保存到数据库
|
||||
conversation.tags = merged_tags
|
||||
await db.commit()
|
||||
await db.refresh(conversation)
|
||||
|
||||
logger.info(
|
||||
f"坐席 {current_agent.id} 更新会话 {conversation_id} 标签: {merged_tags}"
|
||||
)
|
||||
|
||||
# 5. 返回更新后的会话
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
Reference in New Issue
Block a user