django channels打造客服聊天系统

    最近公司业务需求, 打算开发一款在线客服的功能提供给app使用, 本人一开始打算是使用第三方平台, 比如腾讯云的客服系统来打造的, 不过后来查找资料, 了解了下websocket之后, 发现实现起来并不困难. 在此之前, 我研究了微信网页版的聊天方式, 发现微信网页版聊天并不是基于websocket的, 还是使用了http请求发送post请求, 进行页面长轮询方式进行信息的交互的. 

    由于公司项目使用了Django框架进行开发的, django框架有一个channels库是支持websocket服务的, 所以在进行开发之前需要进行安装channels

    打开终端, 输入:

    pip install -U channels

    在django配置文件中安装channels应用.

    使用命令django-admin startapp chat创建一个子应用chat, 并在项目的应用的上级目录新建一个routing.py文件(与wsgi.py同级), 作为websocket服务的路由规则. 


    在routing.py中输入以下代码:

    其实routing.py文件和django的总urls.py文件的原理是一样的,  django服务在运行的时候, 如果接收到的是websocket请求, 就会跳到routing.py文件中websocket应用, 接收到http请求时会跳到urls中找http应用. 

     当然, 再此之前需要在配置文件中配置websocket所支持的asgi协议, 配置如下:   

    本次使用了redis数据库作为websocket的管道,  单点通信的信息以及组间的通信数据都缓存在了redis数据库中, 配置redis管道如下:

配置完这些之后运行manage.py, 如截图所示说明成功了

之后正式开始编写业务代码了, 首先需要在刚刚创建的应用chat中创建一个consumers.py文件, 作为channels的消费者, websocket的消息通信都在这个文件中执行. 

复制channels官方文档中(https://channels.readthedocs.io/en/latest/tutorial/part_3.html)的组间消息通信代码到这个文件中,  然后根据这个基础代码进行业务代码的调整.

    其里面使用了python的async库来支持异步操作, 其实现了connect, disconnect,receive,chat_message等方法, connect用来连接用户组, 也可以理解为开始连接进行websocket通信; disconnect正好时断开连接, 客户端通过手动的断开websocket通信会经过此方法; chat_message就是用来发送消息的, receive方法就是进行消息的接收. 

    在chat应用中创建未读消息模型类, 用来记录聊天记录, 在models.py文件中添加如下代码:

from django.db import models

from users.models import User

# Create your models here.

class ChatRecords(models.Model):

    '''在线客服聊天记录模型类'''

    sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='send_records', verbose_name='发送者', null=False)

    receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='receive_records', verbose_name='接收者', null=False)

    message = models.TextField(verbose_name='聊天信息')

    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:

        db_table = "tb_chat_records"

        verbose_name = '客服聊天记录'

        verbose_name_plural = verbose_name

    def __str__(self):

        return self.id

    接下来执行数据库迁移命令

    python manage.py makemigrations 

    数据库中生成表命令

    python manage.py migrate

    在consumers.py文件中添加业务代码, 添加结果如下:

# chat/consumers.py

import re

from django.conf import settings

from fdfs_client.client import Fdfs_client

from channels.exceptions import *

from calendar import timegm

from django_redis import get_redis_connection

from base64 import b64decode

from rest_framework_jwt.authentication import jwt_decode_handler

from channels.db import database_sync_to_async

from channels.generic.websocket import AsyncWebsocketConsumer

import json

class ChatConsumer(AsyncWebsocketConsumer):

    async def connect(self):

        # 获取url中的参数

        query_string = self.scope['query_string'].decode()

        params = query_string.split('&')

        item = {}

        for param in params:

            item[param.split('=')[0]] = param.split('=')[1]

        token = item.get('token')

        self.user_group = 'chat_'

        if token:

            try:

                payload = jwt_decode_handler(token)

            except:

                raise DenyConnection("签证错误")

            user_id = payload['user_id']

            user = await self.get_user(id=user_id)

            last_login = payload.get('last_login')

            if last_login != timegm(user.last_login.utctimetuple()):

                raise DenyConnection("签证已过期")

        else:

            user = self.scope['user']

        if not user:

            raise DenyConnection("用户不存在")

        receiver_name = item.get('receiver')

        if not receiver_name:

            raise DenyConnection("接收者名称错误")

        receiver = await self.get_user(username=receiver_name)

        if not receiver:

            raise DenyConnection("接收者不存在")

        self.receiver = receiver

        self.user = user

        # 远程组

        self.receiver_group = 'chat_%s_%s' % (self.receiver.username, self.user.username)

        # 用户组

        self.user_group = 'chat_%s_%s' % (self.user.username, self.receiver.username)

        # Join room group

        await self.channel_layer.group_add(

            self.user_group,

            self.channel_name

        )

        await self.accept()

    async def disconnect(self, close_code):

        # Leave room group

        await self.channel_layer.group_discard(

            self.user_group,

            self.channel_name

        )

    # Receive message from WebSocket

    async def receive(self, text_data):

        text_data_json = json.loads(text_data)

        message = text_data_json['message']

        if message:

            # data:image/png;base64,i

            ret = re.findall('data:image/.*;base64,(.*)', message)

            if ret:

                user_pic_str = ret[0]

                image_src = await self.save_image_to_fdfs(user_pic_str)

                # 构造message

                message = ''

            # Send message to room group

            chat_record = await self.save_model(self.user, self.receiver, message)

            if self.receiver.username == 'admin':

                '''为管理员添加消息提示'''

                await self.save_unread_records(chat_record, self.user)

            await self.channel_layer.group_send(

                # websocket发送消息

                self.receiver_group,

                {

                    'type': 'chat_message',

                    'message': message

                }

            )

    # Receive message from room group

    async def chat_message(self, event):

        message = event['message']

        # Send message to WebSocket

        await self.send(text_data=json.dumps({

            'message': message

        }))

    @database_sync_to_async

    def save_model(self, sender, receiver, message):

        # 保存消息记录到数据库中

        from .models import ChatRecords

        return ChatRecords.objects.create(sender=sender, receiver=receiver, message=message)

    @database_sync_to_async

    def get_user(self, id=None, username=None):

        # 异步获取用户

        from users.models import User

        user = None

        if id:

            try:

                user = User.objects.get(id=id)

            except:

                return None

        if username:

            try:

                user = User.objects.get(username=username)

            except:

                return None

        return user

    async def save_unread_records(self, chat_record, sender):

        # 保存未读消息

        redis_conn = get_redis_connection('chatRecord')

        p = redis_conn.pipeline()

        p.rpush(sender.id, chat_record.id)

        p.set('new_records', 1)  # 在redis中添加未读标记

        p.execute()

    async def save_image_to_fdfs(self, pic_str):

        # 把图片存储到fastdfs文件系统中

        client = Fdfs_client(settings.FDFS_CLIENT_CONF)

        ret = client.upload_appender_by_buffer(b64decode(pic_str))

        if ret.get("Status") != "Upload successed.":

            raise Exception("upload file failed")

        file_name = ret.get("Remote file_id")

        return settings.FDFS_URL + file_name

    接下来还需要在chat应用中创建一个routing.py文件, 将子路由会分发到这个文件中, 输入以下代码:

通过上面的步骤, django channels搭建websocket的任务已经完成, 这个时候需要为后台管理页面提供一个聊天界面,  在项目的模板目录中创建一个chat文件夹, 在里面创建一个index.html文件

复制以下代码进去

   

    koalas 客服

   

   

   

       

               

  •                

                       

                       

                            [[item.count]]

                       

                   

                   

                      [[ item.username ]]

                       

                       

                   

               

  •        

   

   

       

            [[ name ]]

           

                更多消息

           

       

       

           

               

           

           

                未选择聊天!

           

       

       

           

                   

                        [[item.create_time]]

                   

               

                   

                       

                   

                   

                   

               

               

                   

                   

                   

                       

                   

               

           

       

       

           

           

发送

       

   

之后为django Xadmin后台添加一个模块, 把后台聊天页面添加到xadmin中.

首先在xadmin全局配置中添加get_site_menu方法, 为聊天页面添加一个导航框, 代码如下:

之后进行注册全局配置类

from xadmin import views

xadmin.site.register(views.CommAdminView, GlobalSetting)

运行manage.py, 打开谷歌浏览器输入127.0.0.1:8000/xadmin, 打开截图如下:


多了一个chat模块, 点开进去, 进入聊天界面(小编的这个界面是模仿了微信界面), 怎么进入到了微信网页界面呢? 哈哈哈.

由于这个是app端和后台客服管理端的聊天应用, 所以要把本地代码部署到服务器上, 然后借助前端混合开发小哥的移动端才能进行消息通讯, 不过各位朋友也可以相应的制作一款网页版的聊天系统来玩玩.

下一期内容就是部署这个channels到服务器上, 敬请期待...

你可能感兴趣的:(django channels打造客服聊天系统)