You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django Channels WebSocket消费者阻止同步API更新数据库问题

Django同步API与WebSocket消费者的数据库更新冲突问题

问题现象

  • WebSocket连接ChatConsumer时,调用SessionTakeOverAPIView无法更新数据库;断开连接后API功能恢复正常。
  • 通过Django Admin更新Session数据,即使消费者处于连接状态也能正常生效。
  • 移除消费者中self.org = self.session.org的赋值后,API可正常更新数据库;移除API中发送消息到channel_layer的代码后,API也能正常工作。
  • 尝试使用后台任务解决该问题,但未生效。

相关代码

SessionTakeOverAPIView(同步API)

class SessionTakeOverAPIView(generics.GenericAPIView):
    """
    该API视图支持人工或AI接管聊天会话,处理会话接管验证、更新会话状态,并向聊天组广播相关事件。
    使用POST请求触发人工或AI接管会话,访问需要身份验证。
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.room_group_name = None

    permission_classes = [BotUserHasRequiredPermissionForMethod]
    post_permission_required = ['session.reply_session']
    queryset = Session.objects.select_related('bot').all()
    serializer_class = SessionTakeOverSerializer

    def get_object(self):
        """
        根据请求数据中的session_id获取会话对象,会话不存在时返回404错误。
        """
        try:
            return super().get_queryset().get(session_id=self.request.data.get('session_id'))
        except Session.DoesNotExist:
            raise Http404  # 会话不存在时返回404错误


    def handle_human_take_over(self):
        """
        处理人工接管聊天会话的逻辑:
        - 验证会话是否已被其他人接管
        - 更新会话状态,标记当前用户为接管人
        - 向聊天组发送接管通知消息
        - 异步创建接管日志记录
        """
        request = self.request
        session: Session = self.get_object()

        # 检查会话是否已被其他人工接管
        if session.is_session_currently_taken_over:
            raise ValidationError({
                "detail" :f"会话当前已被{session.currently_taken_over_by.get_full_name}接管"})

        # 准备接管通知消息
        take_over_msg = f"{session.bot.human_take_over_msg} {request.user.get_full_name}"

        # 准备发送到组的响应消息
        response = {
            "event_type": EventTypeChoicesChat.HUMAN_TAKEOVER,
            "user_type": "client",
            "sender_icon": request.user.profile_icon_url,

            "chat_data": {
                "msg": take_over_msg,
                "recorded_audio_msg_link": None,
                "images_link": [],
                "files_link": []
            }
        }

        # 更新会话状态以反映人工接管
        session.currently_taken_over_by = request.user
        session.is_session_currently_taken_over = True
        session.is_ai_active = False  # AI不再控制会话

        session.save(
            update_fields=[
                'currently_taken_over_by',
                'is_session_currently_taken_over',
                'is_ai_active'
            ]
        )

        transaction.on_commit(
            lambda: send_data_to_channel_layer(
                self.room_group_name, 
                'send_chat_message', 
                response
            )
        )

        # 异步创建会话接管操作日志
        create_session_take_over_info.delay(session.id, request.user.id)

    def handle_ai_take_over(self):
        """
        处理AI接管聊天会话的逻辑:
        - 验证AI是否已在会话中激活
        - 更新会话状态以反映AI控制,并向聊天组发送通知消息
        """
        session: Session = self.get_object()

        # 检查AI是否已在会话中激活
        if session.is_ai_active:
            raise ValidationError({"detail": "AI已在运行中"})

        # 准备AI接管通知消息
        take_over_msg = session.bot.ai_take_over_msg

        # 准备发送到聊天室组的响应消息
        response = {
            "event_type": EventTypeChoicesChat.AI_TAKE_OVER,
            "user_type": "ai",
            "sender_icon": session.bot.bot_icon,

            "chat_data": {
                "msg": take_over_msg,
                "recorded_audio_msg_link": None,
                "images_link": [],
                "files_link": []
            }
        }

        # 更新会话状态以反映AI接管
        session.currently_taken_over_by = None  # 无人工控制
        session.is_session_currently_taken_over = False
        session.is_ai_active = True  # AI现在控制会话
        
        session.save(
            update_fields=[
                'currently_taken_over_by',
                'is_session_currently_taken_over',
                'is_ai_active'
            ]
        )

        transaction.on_commit(
            lambda: send_data_to_channel_layer(
                self.room_group_name, 
                'send_chat_message', 
                response
            )
        )


    def post(self, request, *args, **kwargs):
        """
        处理POST请求以接管会话(人工或AI):
        验证会话ID,并检查会话是否处于激活状态,然后允许人工或AI接管。
        """

        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)

        # 根据请求数据确定接管类型(人工或AI)
        take_over_type = serializer.validated_data.get('take_over_type')
        self.room_group_name = constants.CHAT_SESSION_ROOM_GROUP_NAME.format(
            session_id=request.data.get('session_id')
        )  

        session: Session = self.get_object()
        if session.session_has_ended:
            raise ValidationError({"detail" :"会话已失效。"})

        with transaction.atomic():
            if take_over_type == EventTypeChoicesChat.HUMAN_TAKEOVER:
                self.handle_human_take_over()  # 处理人工接管
            elif take_over_type == EventTypeChoicesChat.AI_TAKE_OVER:
                self.handle_ai_take_over()  # 处理AI接管
            else:
                raise ValidationError({"detail": "无效的接管类型。"})

        return Response({"message": "接管成功"})

ChatConsumer(异步WebSocket消费者)

class ChatConsumer(AsyncWebsocketConsumer):
    """ 处理特定会话中聊天消息的WebSocket消费者 """

    async def connect(self):
        """处理WebSocket连接初始化"""
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        self.room_group_name = constants.CHAT_SESSION_ROOM_GROUP_NAME.format(session_id=self.room_name)

        session = await self.__get_session({"session_id": self.room_name, "session_has_ended": False})
        if not session:
            logger.info(f"会话 [{self.room_name}] 已过期或不存在")
            # 关闭连接
            await self.close()
            return

        self.session = session

        await self.__set_attributes()
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
        await self.accept()

        # 验证连接条件
        if not await self.__validate_connection():
            await self.__send_validation_failed_message()
            return

        # 如果没有消息则发送欢迎消息
        if not await database_sync_to_async(lambda: session.has_messages)():
            await self.__send_intro_message()


    async def __set_attributes(self):
        self.org = self.session.org
        self.bot = self.session.bot
        self.platform = self.bot.platform
        self.language = self.bot.language
        self.ai_response_url = settings.AI_BASE_URL + f'/api/v1.0/ai/conversation/{self.org.id}/{self.bot.id}'
        self.goal = '你是一名智能客服代理'
        self.customer_name = self.session.user_name if self.session.user_name else ""
        self.customer_email = self.session.user_email if self.session.user_email else ""
        self.user_type = None
        self.user = self.scope["user"] if not self.scope["user"].is_anonymous else None


    @database_sync_to_async
    def __get_session(self, filter_kwargs: dict):
        """获取包含关联数据的会话"""
        try:
            session = Session.objects.select_related(
                "bot",
                "org",
                "bot__language", 
                "bot__platform"
            ).get(**filter_kwargs)
            return session
        except Exception as err:
            logger.error(f"获取会话时出错: {err}")
            return None

内容的提问来源于stack exchange,提问作者Zahid Hassan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:00:54