A组认证加固: P0兜底+Token刷新+环境检测+OTP+RBRAC落地+P1日志审计+Token撤销 - 全局一致性审查通过

This commit is contained in:
Simon
2026-07-02 19:06:12 +08:00
parent 78f60c6857
commit fc22de7f4d
12 changed files with 1813 additions and 106 deletions
+75
View File
@@ -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)