django + channels + layim 實現用戶一對一,一對多,群組聊天實時通訊


 

Django Channels介紹

首先要理解Django現有的請求響應策略是這樣的:瀏覽器發出請求,Django服務器接受請求后通過路由匹配該請求到某個視圖,視圖將會返回一個響應並由服務器發送回瀏覽器。類似的請求響應在Flask實現也是如此。對於一般性的網頁瀏覽(比如新聞閱讀),這樣的響應機制是沒有問題的,但對於需要一個保持不斷會話的請求來說,這是行不通的,因為Django的聲明周期只能存在一個請求中,它不能讓服務器在沒有請求的情況下不斷地發送數據島瀏覽器客服端。這樣的場景目前正在不斷地涌現,例如在線聊天室,會話機器人,以及最近很流行的微服務應用。
Channels改變了Django的工作方式,讓它實現了一種包括通道、消費者和worker的worker監聽的模式,所有消費者都會分配有單獨的通道,worker監聽通道的消息,確保消息到來時能進行處理。為了確保上述機制運行,Channels需要有三個工作層:

  1. 接口服務器,Django和用戶(瀏覽器)之間通信的橋梁,包括一個實現WSGI協議的適配器和一個獨立的websocket服務器。
  2. 通道后端, 在接口服務器和worker之間傳遞消息,由插拔式的python代碼和存儲組成,存儲可以是內存、數據庫或者redis,推薦使用redis,兼具其余兩者的優點。
  3. worker,監聽所有channel,當有新消息到來時候喚醒功能函數。

Channels可以讓Django的框架變得更為可靠和可拓展,整個通信的服務器數可以按需拓展,至少保證一台協議服務器和一台工作服務器即可。使用Channels后,你不再需要組織code去為異步調用,Channls已經將一切都已經幫你准備好。

 參考實例:https://www.cnblogs.com/kendrick/p/7218107.html

實驗教程

  • 開發 Windows10 / 生產Centos7
  • Python3.7
  • pyCharm2020
  • 前端框架: https://www.layui.com/layim/
  • redis 3.0.53 Windows x64 
    本實驗的目的是搭建一個用於聊天機器人的WEB交互框架,可以直接拉到最下方看實現效果。
    下面的代碼運行需要redis服務開啟了6379端口正常運行。

演示實例:

用戶名:user001

密碼:p@ssw0rdwcx

客戶端使用谷歌瀏覽器打開: https://www.szyfd.xyz/itkf/app/index/

 

用戶名:kefu001

密碼:p@ssw0rdwcx

服務端使用IE瀏覽器打開:https://www.szyfd.xyz/itkf/app/index/

 

 

前端使用: https://www.layui.com/doc/modules/layim.html

后端版本:python3 + django

運行效果圖:

 

項目目錄:

 

1.pycharm 新建django 項目

2.安裝  pip install -r requirements.txt

aioredis==1.2.0
asgiref==3.2.7
asn1crypto==0.24.0
async-timeout==3.0.1
attrs==19.1.0
autobahn==19.9.2
Automat==0.7.0
backports.csv==1.0.7
certifi==2019.6.16
cffi==1.12.3
channels==2.2.0
channels-redis==2.3.3
chardet==3.0.4
constantly==15.1.0
cryptography==2.7
daphne==2.3.0
defusedxml==0.6.0
diff-match-patch==20181111
Django==2.1.11
django-import-export==1.2.0
django-redis==4.10.0
django-simpleui==4.0
django-utils==0.0.2
et-xmlfile==1.0.1
hiredis==1.0.0
hyperlink==19.0.0
idna==2.8
incremental==17.5.0
jdcal==1.4.1
lark-parser==0.7.4
msgpack==0.6.1
numpy==1.17.1
odfpy==1.4.0
openpyxl==2.6.3
optionaldict==0.1.1
Pillow==7.1.2
pycparser==2.19
PyHamcrest==1.9.0
PyMySQL==0.9.3
python-dateutil==2.8.0
pytz==2019.2
PyYAML==5.1.2
redis==3.3.8
requests==2.22.0
required==0.4.0
six==1.12.0
sqlparse==0.3.0
tablib==0.13.0
Twisted==19.7.0
txaio==18.8.1
urllib3==1.25.3
wechatpy==1.8.3
xlrd==1.2.0
xlwt==1.3.0
xmltodict==0.12.0
zope.interface==4.6.0

3.新建 routing.py

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import app.routing

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter(
            app.routing.websocket_urlpatterns
        )
    ),
})

4.配置 settings.py

"""
Django settings for itkf project.

Generated by 'django-admin startproject' using Django 3.0.5.

For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '&avamfpy-nj-9q#91nn89^(zjl0s-&iu3*+g+strp&qjxqwerh'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["*"]

# Application definition

INSTALLED_APPS = [
    'simpleui',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app.apps.AppConfig',
    'import_export',
    'channels',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'itkf.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')]
        ,
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
ASGI_APPLICATION = 'itkf.routing.application'
WSGI_APPLICATION = 'itkf.wsgi.application'




# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases

# redis配置
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
            "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
            # "PASSWORD": "密碼",
        }
    }
}
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'NAME': 'itkf',
        'USER': 'root',
        'PASSWORD': '123456'
    }
}
# 開發redis 路徑 C:\Program Files\Redis redis-server redis.windows.conf
'''
windows下安裝Redis第一次啟動報錯:

[2368] 21 Apr 02:57:05.611 # Creating Server TCP listening socket 127.0.0.1:6379: bind: No error
解決方法:在命令行中運行
redis-cli.exe
127.0.0.1:6379>shutdown
not connected>exit
然后重新運行redis-server.exe redis.windows.conf
'''

# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/

domain = "http://127.0.0.1:8000"

# 圖片上傳路徑
MEDIA_URL = '/'
MEDIA_ROOT = r'D:/itkf/itkfstatic/uploadImage/'

STATIC_URL = '/itkfstatic/'
SIMPLEUI_HOME_INFO = False
# SIMPLEUI 配置
SIMPLEUI_STATIC_OFFLINE = True

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'itkfstatic'),
)
# 登錄頁面
LOGIN_URL = '/itkf/admin/login/'

# 權限緩存配置
SESSION_ENGINE = 'django.contrib.sessions.backends.db'  # 引擎(默認)
SESSION_COOKIE_NAME = "sessionid"  # Session的cookie保存在瀏覽器上時的key,即:sessionid=隨機字符串(默認)
SESSION_COOKIE_PATH = "/"  # Session的cookie保存的路徑(默認)
SESSION_COOKIE_DOMAIN = None  # Session的cookie保存的域名(默認)
SESSION_COOKIE_SECURE = False  # 是否Https傳輸cookie(默認)
SESSION_COOKIE_HTTPONLY = True  # 是否Session的cookie只支持http傳輸(默認)
SESSION_COOKIE_AGE = 1209600  # Session的cookie失效日期(2周)(默認)
SESSION_EXPIRE_AT_BROWSER_CLOSE = True  # 是否關閉瀏覽器使得Session過期(默認)
SESSION_SAVE_EVERY_REQUEST = False  # 是否每次請求都保存Session,默認修改之后才保存(默認)

weChatWork = {
    'corpid': "",
    'secret': "",
    'sourceFile': "static/source",
    'serviceUser_': 'serviceUser_',
    'customeUser_': 'customeUser_',
    "media_image_url": "/itkfstatic/uploadImage/",
    "avatar_image_url": "/itkfstatic/avatar/"
}

5.urls.py 配置

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('itkf/admin/', admin.site.urls),

    path('itkf/app/', include("app.urls")),
]

6. 項目名稱下  >> __init__.py  文件配置

import pymysql
#pymysql.version_info = (1, 3, 13, "final", 0)
pymysql.install_as_MySQLdb()

7.應用名稱(app) >> models.py

from django.contrib.auth.models import User
from django.db import models
# Create your models here.
from django.utils.html import format_html
from django.db import models
import datetime
import uuid

from django.db import models
from django.contrib.auth.models import User
# Create your models here.
from django.utils.html import format_html
from django.db.models import IntegerField, Model
from django.core.validators import MaxValueValidator, MinValueValidator
import datetime
import random, os
# Create your models here.
from django.contrib.auth.models import AbstractUser
from django.db import models

ENV_PROFILE = os.getenv("ENV")
if ENV_PROFILE == "test":
    import itkf.test_settings as config
elif ENV_PROFILE == "production":
    import itkf.prd_settings as config
else:
    import itkf.settings as config

corpid = config.weChatWork["corpid"]
sourceFile = config.weChatWork["sourceFile"]
media_image_url = config.weChatWork["media_image_url"]


def rename(newname):
    def decorator(fn):
        fn.__name__ = newname
        return fn

    return decorator


def newImageName(instance, filename):
    filename = '{}.{}'.format(uuid.uuid4().hex, "png")
    return filename


# 生成預約訂單號

# 用時間生成一個唯一隨機數

def random_with_N_digits(n):
    range_start = 10 ** (n - 1)
    range_end = (10 ** n) - 1
    return random.randint(range_start, range_end)


def get_ran_dom():
    nowTime = datetime.datetime.now().strftime("%Y%m%d%H%M%S")  # 生成當前時間
    randomNum = random_with_N_digits(3)  # 生成的隨機整數n,其中0<=n<=100
    if randomNum <= 10:
        randomNum = str(0) + str(randomNum)
    uniqueNum = str(nowTime) + str(randomNum)
    return uniqueNum



# 應用管理
class agent(models.Model):
    name = models.CharField(max_length=225, verbose_name="部門名稱", blank=True, default="")
    agentid = models.CharField(max_length=225, verbose_name="應用ID", blank=True, default="")
    secret = models.CharField(max_length=225, verbose_name="應用密鑰", blank=True, default="")
    avatar = models.ImageField(max_length=225, verbose_name="部門Logo", blank=True, default="")
    conversationTime = models.IntegerField(verbose_name="會話時長(分鍾)", default=20)
    webhook_url = models.URLField(verbose_name="群機器人地址", default="", blank=True, null=True)

    createTime = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
    lastTime = models.DateTimeField(auto_now=True, verbose_name="修改時間")
    author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="創建者",
                               related_name="agent_author")
    editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者",
                               related_name="agent_creator")

    @rename("部門Logo")
    def showAvatar(self):
        return format_html("<img src='{}{}' style='width: 60px;height: 60px;' class='showAvatar' />", media_image_url,
                           self.avatar)

    @rename("詳情")
    def checkMessage(self):
        return format_html("<a href='/app/index/{}.html' target='blank'>回復</a>", self.id)

    class Meta:
        verbose_name = verbose_name_plural = '部門管理'
        ordering = ['id']

    def __str__(self):
        return self.name

# 客服人員
class KF(models.Model):
    agent = models.ForeignKey(agent, null=True, on_delete=models.CASCADE, verbose_name="應用名稱")
    username = models.CharField(max_length=225, verbose_name="姓名", blank=True, default="")
    userid = models.CharField(max_length=225, verbose_name="UM", blank=True, default="")
    status = models.BooleanField(verbose_name="是否在線", default=False)
    avatar = models.ImageField(max_length=225, verbose_name="頭像", blank=True, default="")
    createTime = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
    lastTime = models.DateTimeField(auto_now=True, verbose_name="修改時間")
    author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="創建者",
                               related_name="kf_author")
    editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者",
                               related_name="kf_creator")

    class Meta:
        verbose_name = verbose_name_plural = '在線客服'
        ordering = ['id']

    @rename("頭像")
    def showAvatar(self):
        return format_html("<img src='{}{}' style='width: 60px;height: 60px;' class='showAvatar' />", media_image_url,
                           self.avatar)

    def __str__(self):
        return self.username


# 行內員工

def randomSign():
    switch = {
        0: "只要還有明天,今天就永遠是起跑線。",
        1: "只要還有明天,今天就永遠是起跑線。",
        2: "只要還有明天,今天就永遠是起跑線。"
    }
    return switch[0]


class userList(models.Model):
    agent = models.ForeignKey(agent, null=True, on_delete=models.CASCADE, verbose_name="應用名稱")
    username = models.CharField(max_length=225, verbose_name="姓名", blank=True, default="")
    userid = models.CharField(max_length=225, verbose_name="UM", blank=True, default="")
    avatar = models.ImageField(max_length=225, verbose_name="頭像", blank=True, default="")
    sign = models.CharField(max_length=225, verbose_name="個性簽名", blank=True, default=randomSign)

    ISLEAD_CHOICES = ((0, ''), (1, ''),)
    islead = models.IntegerField(choices=ISLEAD_CHOICES, verbose_name="等級", default=1)
    createTime = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
    lastTime = models.DateTimeField(auto_now=True, verbose_name="修改時間")
    author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="創建者",
                               related_name="userlist_author")
    editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者",
                               related_name="userlist_creator")

    @rename("頭像")
    def showAvatar(self):
        return format_html("<img src='{}{}' style='width: 60px;height: 60px;' class='showAvatar' />", media_image_url,
                           self.avatar)

    class Meta:
        verbose_name = verbose_name_plural = '用戶列表'
        ordering = ['id']

    def __str__(self):
        return self.username


# 接受的消息

class Message(models.Model):
    ToUserName = models.CharField(max_length=225, verbose_name="接受者", blank=True, default="")
    FromUserName = models.CharField(max_length=225, verbose_name="發送者", blank=True, default="")
    CreateTime = models.DateTimeField(verbose_name="發送時間", blank=True, default=None)
    MsgId = models.CharField(max_length=225, verbose_name="消息ID", blank=True, default="")
    AgentID = models.CharField(max_length=225, verbose_name="部門名稱", blank=True, default="")
    MsgType = models.CharField(max_length=225, verbose_name="消息類型", blank=True, default="")
    content = models.TextField(max_length=2000, verbose_name="消息內容", blank=True, default="")
    userList = models.ForeignKey('userList', null=True, to_field="id", on_delete=models.CASCADE)
    createDateTime = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
    lastTime = models.DateTimeField(auto_now=True, verbose_name="修改時間")
    author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="創建者",
                               related_name="message_author")
    editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者",
                               related_name="message_creator")

    class Meta:
        verbose_name = verbose_name_plural = '所有消息'
        ordering = ['id']

    def __str__(self):
        return self.FromUserName





# 員工服務
class staffService(models.Model):
    agent = models.ForeignKey('agent', null=True, on_delete=models.CASCADE, verbose_name="應用名稱")
    title = models.CharField(max_length=225, verbose_name="標題", blank=True, default="")
    avatar = models.ImageField(max_length=225, verbose_name="頭像", blank=True, default="")
    desc = models.TextField(max_length=500, verbose_name="描述", default="", blank=True, null=True)

    welcomeText = models.TextField(max_length=2000, verbose_name="歡迎語", blank=True, default="")
    firstText = models.TextField(max_length=2000, verbose_name="會話提示語", blank=True, default="您好,很高興為您服務!")

    notuserText = models.TextField(max_length=2000, verbose_name="客服不在線提示語", blank=True,
                                   default="非常抱歉,客服處於離線狀態,您的消息我們已發送IT服務台,馬上會有IT同事跟進處理!")

    createTime = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
    lastTime = models.DateTimeField(auto_now=True, verbose_name="修改時間")
    author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="創建者",
                               related_name="staffService_author")
    editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者",
                               related_name="staffService_creator")

    class Meta:
        verbose_name = verbose_name_plural = '員工服務'
        ordering = ['createTime']

    @rename("頭像")
    def showAvatar(self):
        return format_html("<img src='{}{}' style='width: 60px;height: 60px;' class='showAvatar' />", media_image_url,
                           self.avatar)

    def __str__(self):
        return self.title


class knowledgeBase(models.Model):
    questionType = models.CharField(max_length=225, verbose_name="問題類型", blank=True, default="")
    key = models.CharField(max_length=225, verbose_name="關鍵字", blank=True, default="")
    rule = models.IntegerField(choices=((0, '包含'), (1, '完全匹配')), default=0, verbose_name='規則')
    answerType = models.IntegerField(choices=((0, '文字'), (1, '圖文'), (2, '圖片'), (3, '語音'), (4, '視頻')), default=0,
                                     verbose_name='發送類型')
    content = models.TextField(max_length=2000, verbose_name="消息內容", blank=True, default="")
    createTime = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
    lastTime = models.DateTimeField(auto_now=True, verbose_name="修改時間")
    author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="創建者",
                               related_name="knowledgeBase_author")
    editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者",
                               related_name="knowledgeBase_creator")

    class Meta:
        verbose_name = verbose_name_plural = '知識庫'
        ordering = ['id']

    def __str__(self):
        return self.content

8.應用名稱(app) >> admin.py

from django.contrib import admin

# Register your models here.

from import_export import resources
from import_export.admin import ImportExportModelAdmin
from wechatpy.enterprise import WeChatClient
from wechatpy.enterprise.crypto import WeChatCrypto
from wechatpy.exceptions import InvalidSignatureException
import itkf as config
import os
from app import models

admin.site.site_title = "企業號后台管理"
admin.site.site_header = "企業號后台管理"

# 企業號ID
ENV_PROFILE = os.getenv("ENV")
if ENV_PROFILE == "test":
    import itkf.test_settings as config
elif ENV_PROFILE == "production":
    import itkf.prd_settings as config
else:
    import itkf.settings as config


class agentResource(resources.ModelResource):
    def get_export_headers(self):
        # 是你想要的導出頭部標題headers
        return ['應用名稱', '歡迎語', '會話提示語', '部門Logo', '創建時間', '修改時間', '創建者', '修改者']

    class Meta:
        field = ('name', 'welcomeText', 'firstText', 'avatar', 'createTime', 'lastTime', 'author', 'editor')
        model = models.agent
        fields = field
        export_order = field


@admin.register(models.agent)
class agentAdmin(ImportExportModelAdmin):
    fields = (
        'name', 'avatar', 'agentid', 'secret', 'webhook_url',
        'conversationTime')
    # 需要顯示的字段信息
    list_display = ('showAvatar', 'name', 'webhook_url', 'conversationTime',
                    'createTime',
                    'lastTime', 'author',
                    'editor',
                    'checkMessage')
    exclude = ('author', 'editor')
    # 設置哪些字段可以點擊進入編輯界面,默認是第一個字段
    list_display_links = ('showAvatar', 'name',)
    model_icon = "fa fa-tag"
    list_per_page = 10
    resource_class = agentResource

    def save_model(self, request, obj, form, change):
        if form.is_valid():
            if not change:
                obj.author = request.user
            obj.editor = request.user
            obj.save()
        super().save_model(request, obj, form, change)


class KFResource(resources.ModelResource):
    def get_export_headers(self):
        # 是你想要的導出頭部標題headers
        return ['姓名', 'UM', '頭像']

    class Meta:
        field = ('username', 'userid', 'sign', 'avatar',)
        model = models.KF
        fields = field
        export_order = field


@admin.register(models.KF)
class KFAdmin(ImportExportModelAdmin):
    fields = ("agent", "avatar", "username", 'userid')
    # 需要顯示的字段信息
    list_display = ("agent", "showAvatar", 'username', 'userid', 'status', 'createTime', 'lastTime', 'author', 'editor')
    exclude = ('status',)
    # 設置哪些字段可以點擊進入編輯界面,默認是第一個字段
    list_display_links = ('username',)
    model_icon = "fa fa-tag"
    list_per_page = 10
    resource_class = KFResource

    def save_model(self, request, obj, form, change):
        if form.is_valid():
            if not change:
                obj.author = request.user
            obj.editor = request.user
            obj.save()
        super().save_model(request, obj, form, change)


class userListResource(resources.ModelResource):
    def get_export_headers(self):
        # 是你想要的導出頭部標題headers
        return ['姓名', 'UM', '頭像']

    class Meta:
        field = ('username', 'userid', 'avatar', 'createTime', 'lastTime', 'author', 'editor')
        model = models.userList
        fields = field
        export_order = field


@admin.register(models.userList)
class userListAdmin(ImportExportModelAdmin):
    fields = ('avatar', 'username', 'userid', 'islead',)
    # 需要顯示的字段信息
    list_display = ('showAvatar', 'username', 'userid', 'islead', 'createTime', 'lastTime', 'author', 'editor')
    # 設置哪些字段可以點擊進入編輯界面,默認是第一個字段
    list_display_links = ('username',)
    search_fields = ('username', 'userid')
    model_icon = "fa fa-tag"
    list_per_page = 50
    resource_class = userListResource

    def save_model(self, request, obj, form, change):
        if form.is_valid():
            if not change:
                obj.author = request.user
            obj.editor = request.user
            obj.save()
        super().save_model(request, obj, form, change)


class MessageResource(resources.ModelResource):
    def get_export_headers(self):
        # 是你想要的導出頭部標題headers
        return ['企業號ID', '發送者', '發送時間', '消息ID', '應用ID', '消息類型', '消息內容']

    class Meta:
        field = ('FromUserName', 'CreateTime', 'MsgId', 'AgentID', 'MsgType', 'content')
        model = models.Message
        fields = field
        export_order = field


# Register your models here.
@admin.register(models.Message)
class MessageAdmin(ImportExportModelAdmin):
    fields = ('FromUserName', 'CreateTime', 'MsgId', 'AgentID', 'MsgType', 'content')
    # 需要顯示的字段信息
    list_display = (
    'id', 'ToUserName', 'FromUserName', 'CreateTime', 'MsgId', 'AgentID', 'MsgType', 'content', 'author')
    # 設置哪些字段可以點擊進入編輯界面,默認是第一個字段
    list_display_links = ('id', 'FromUserName')
    model_icon = "fa fa-tag"
    list_per_page = 10
    resource_class = MessageResource


class knowledgeBaseResource(resources.ModelResource):
    def get_export_headers(self):
        # 是你想要的導出頭部標題headers
        return ['問題類型', '關鍵字', '規則', '發送類型', '消息內容', '創建時間', '修改時間', '創建者', '修改者']

    class Meta:
        field = ('questionType', 'key', 'rule', 'answerType', 'content', 'createTime', 'lastTime', 'author', 'editor')
        model = models.knowledgeBase
        fields = field
        export_order = field


# Register your models here.
@admin.register(models.knowledgeBase)
class knowledgeBaseAdmin(ImportExportModelAdmin):
    fields = ('questionType', 'key', 'rule', 'answerType', 'content')
    # 需要顯示的字段信息
    list_display = ('id', 'questionType', 'key', 'rule', 'answerType', 'content')
    # 設置哪些字段可以點擊進入編輯界面,默認是第一個字段
    list_display_links = ('id', 'questionType')
    model_icon = "fa fa-tag"
    list_per_page = 10
    resource_class = knowledgeBaseResource

    def save_model(self, request, obj, form, change):
        if form.is_valid():
            if not change:
                obj.author = request.user
            obj.editor = request.user
            obj.save()
        super().save_model(request, obj, form, change)


class staffServiceResource(resources.ModelResource):
    class Meta:
        field = (
            'agent', 'title', 'avatar', 'welcomeText', 'firstText', 'notuserText', "desc", 'createTime', 'lastTime',
            'author', 'editor')
        model = models.staffService
        fields = field
        export_order = field


# Register your models here.
@admin.register(models.staffService)
class staffServiceBaseAdmin(ImportExportModelAdmin):
    fields = ('agent', 'title', 'avatar', "desc", 'welcomeText', 'firstText', 'notuserText',)
    # 需要顯示的字段信息
    list_display = (
        'showAvatar', 'agent', 'title', "desc", 'welcomeText', 'firstText', 'notuserText', 'createTime', 'lastTime',
        'author', 'editor')
    # 設置哪些字段可以點擊進入編輯界面,默認是第一個字段
    list_display_links = ('agent', 'title',)
    model_icon = "fa fa-tag"
    list_per_page = 10
    resource_class = staffServiceResource

    def save_model(self, request, obj, form, change):
        if form.is_valid():
            if not change:
                obj.author = request.user
            obj.editor = request.user
            obj.save()
        super().save_model(request, obj, form, change)

9.應用名稱(app) >> consumers.py

import time

from channels.generic.websocket import WebsocketConsumer
from channels.generic.websocket import AsyncWebsocketConsumer
import json
import numpy as np
from app import models
from django.core.cache import cache
from asgiref.sync import async_to_sync
import itkf as config
from django.utils.safestring import mark_safe
import os

ENV_PROFILE = os.getenv("ENV")
if ENV_PROFILE == "test":
    import itkf.test_settings as config
elif ENV_PROFILE == "production":
    import itkf.prd_settings as config
else:
    import itkf.settings as config

corpid = config.weChatWork["corpid"]
sourceFile = config.weChatWork["sourceFile"]
serviceUser = config.weChatWork["serviceUser_"]
customeUser = config.weChatWork["customeUser_"]
media_image_url = config.weChatWork["media_image_url"]


# 根據類型選擇發送對應的格式
def sendContent(item):
    MsgType = item["MsgType"]
    content = item["content"]
    if MsgType == "image":
        content = mark_safe("img[{0}]".format(content))
    if MsgType == "video":
        content = mark_safe("video[{0}]".format(content))
    if MsgType == "voice":
        content = mark_safe("audio[{0}]".format(content))
    return {'username': item['username'], 'avatar': item['avatar'], 'id': item['id'], 'type': 'friend',
            'content': content}


class ChatConsumer(AsyncWebsocketConsumer):

    async def connect(self):
        print("connect")
        groupName = ""
        self.user = self.scope["user"]
        # 客服上線
        username = self.scope["user"].username
        print("username===========", username)

        agentid = 1

        staff = models.staffService.objects.filter(agent__id=agentid)

        kfUser = models.KF.objects.filter(userid=username).first()
        if kfUser:
            groupName = serviceUser + str(kfUser.agent.id) + username
            kfUser.status = True
            kfUser.save()
        else:
            groupName = customeUser + username

        self.room_group_name = groupName
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()

        await self.receive("once")

    async def disconnect(self, close_code):
        print("disconnect")
        print(close_code)
        print(self.scope["user"])
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )
        # 客服下線
        kfUser = models.KF.objects.filter(userid=self.scope["user"].username).update(status=False)
        print(kfUser)

    async def receive(self, text_data):

        self.user = self.scope["user"]
        # Send message to room group
        # loginKF = models.KF.objects.filter(userid=self.user).first()
        AgentID = "1"
        redisMessage = cache.iter_keys(AgentID + "$*")
        for msgg in redisMessage:
            currentMsg = cache.get(msgg)
            for item in currentMsg:
                message = sendContent(item)
                await self.channel_layer.group_send(
                    self.room_group_name,
                    {
                        'type': 'chat_message',
                        'message': message
                    }
                )
                newRedisMessage = cache.get(msgg)
                if currentMsg == newRedisMessage:
                    cache.delete(msgg)

    async def chat_message(self, message):
        print(message)
        # Send message to WebSocket
        await self.send(text_data=json.dumps(message))

10.應用名稱(app) >> routing.py

from django.urls import path
from app.consumers import ChatConsumer

websocket_urlpatterns = [
    path('ws/chat/', ChatConsumer),
]

11.應用名稱(app) >> urls.py

from django.contrib import admin
from django.urls import path
from app import views

urlpatterns = [
    # path('login/', views.login),  # 授權登錄
    path('requestInfo/', views.requestInfo),  # 請求信息
    path('index/', views.index),  # PC客服主頁
    path('custome/', views.custome),  # PC客戶主頁
    path('GetUserList/', views.GetUserList),  # 用戶信息
    path('departmentServices/', views.departmentServices),  # 用戶信息
    path('uploadImage/', views.uploadImage),  # 上傳圖片

    path('getWelcomeText/', views.getWelcomeText),  # 上傳圖片
    # 更新客服狀態在線/下線
    path('updateUserStatus/', views.updateUserStatus),  # 上傳圖片
]

12.應用名稱(app) >> views.py

import wechatpy
from django.contrib import auth
from wechatpy.enterprise.client.api import WeChatOAuth
from wechatpy.enterprise.exceptions import InvalidCorpIdException
from wechatpy import enterprise, parse_message
from django.shortcuts import render, redirect
from django.http import JsonResponse, HttpResponse, HttpResponseRedirect
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
import uuid, datetime, json, time
import itkf as config
from django.utils.safestring import mark_safe
from wechatpy.enterprise.crypto import WeChatCrypto
from wechatpy.exceptions import InvalidSignatureException
import os
import urllib
import itkf
from app import models
from django.core.cache import cache
from wechatpy.enterprise import WeChatClient
from wechatpy.session.redisstorage import RedisStorage
from redis import Redis
from urllib.parse import quote
from django.utils.safestring import mark_safe
import json
import urllib3

ENV_PROFILE = os.getenv("ENV")
if ENV_PROFILE == "test":
    import itkf.test_settings as config
elif ENV_PROFILE == "production":
    import itkf.prd_settings as config
else:
    import itkf.settings as config

corpid = config.weChatWork["corpid"]
sourceFile = config.weChatWork["sourceFile"]
serviceUser = config.weChatWork["serviceUser_"]
customeUser = config.weChatWork["customeUser_"]
media_image_url = config.weChatWork["media_image_url"]
avatar_image_url = config.weChatWork["avatar_image_url"]
domain = config.domain


# 群消息提問內容
def template_string(**kwargs):
    return """<font color="warning">新消息</font>
> 姓名:{username}
> UM:{um}
> 發送內容:{content}
> 點擊查看:{url}
""".format(**kwargs)


# 查看請求信息

def requestInfo(request):
    result = request.environ.items()
    return render(request, 'requestInfo.html', {'rinfo': result})


# 登錄功能


# 注銷
def logout(request):
    kfUser = models.KF.objects.filter(userid=request.user.username).first()
    kfUser.status = False
    kfUser.save()
    auth.logout(request)
    return redirect('/admin/login/')


# 注冊
def registered(request, userid):
    user = User.objects.filter(username=userid).first()
    if not user:
        user = User.objects.create_user(username=userid, email=str(userid) + "@pingan.com.cn", password=uuid.uuid1(),
                                        is_staff=True, is_active=True)
    auth.login(request, user)
    return user


# Create your tests here.
@login_required
def index(request):
    method = request.method.upper()
    agentid = request.GET.get("nid", 1)
    if method == "GET":
        return render(request, "index.html")
    elif method == "POST":
        UserId = request.user.username
        kfUser = models.KF.objects.filter(userid=UserId).first()
        kfUser.status = True
        kfUser.save()
        print(request.POST)
        Content = request.POST.get('mine[content]')
        userid = request.POST.get('mine[id]')
        FromUserName = request.POST.get('to[id]')
        cur_time = datetime.datetime.now()
        models.Message.objects.create(ToUserName=userid, AgentID=kfUser.agent, FromUserName=FromUserName,
                                      content=Content,
                                      CreateTime=cur_time, author=request.user, editor=request.user)

        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        from asgiref.sync import async_to_sync
        print("FromUserName============", FromUserName)

        obj = {
            "username": request.user.first_name
            , "avatar": media_image_url + str(kfUser.avatar)
            , "id": FromUserName
            , "type": "chat_message"
            , "content": Content
        }
        print("rindex===========", FromUserName[str(FromUserName).rindex("_") + 1:])
        print("customeUser + FromUserName=============", customeUser + FromUserName)
        async_to_sync(channel_layer.group_send)(customeUser + FromUserName[str(FromUserName).rindex("_") + 1:], obj)

        obj = {
            "username": request.user.first_name
            , "avatar": media_image_url + str(kfUser.avatar)
            , "id": FromUserName
            , "type": "chat_message"
            , "content": Content
        }
        currentOnlineKF = models.KF.objects.filter(agent__id=agentid, status=True).exclude(userid=UserId)
        print(currentOnlineKF.query)
        for item in currentOnlineKF:
            async_to_sync(channel_layer.group_send)(serviceUser + str(agentid) + item.userid, obj)

        result = {"code": 200, "msg": "ok"}
        return JsonResponse(result)




# @login_required
@login_required
def custome(request):
    method = request.method.upper()
    agentid = request.GET.get("nid", 1)
    if method == "GET":
        return render(request, "custome.html",
                      {"agentid": agentid,
                       "currentUser": request.user})
    else:
        UserId = request.user.username
        print(request.POST)
        Content = request.POST.get('mine[content]')
        FromUserName = request.POST.get('to[userid]')
        cur_time = datetime.datetime.now()
        models.Message.objects.create(ToUserName=UserId, AgentID=agentid, FromUserName=FromUserName,
                                      content=Content,
                                      CreateTime=cur_time, author=request.user, editor=request.user)

        currentKF = models.KF.objects.filter(agent__id=agentid, status=True).count()
        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        from asgiref.sync import async_to_sync
        if currentKF > 0:
            obj = {
                "username": request.user.first_name
                , "avatar": avatar_image_url + UserId + ".png"
                , "id": FromUserName
                , "type": "chat_message"
                , "content": Content
            }

            # 首次會話查詢知識庫直接返回信息 未完成

            currentOnlineKF = models.KF.objects.filter(agent__id=agentid, status=True).exclude(userid=UserId)
            print(currentOnlineKF)
            for item in currentOnlineKF:
                async_to_sync(channel_layer.group_send)(serviceUser + agentid + item.userid, obj)
        else:
            sid = str(FromUserName).split('_')[1]
            print("staffService===========", sid)

            staffService = models.staffService.objects.filter(id=sid).first()
            obj = {
                "username": staffService.title
                , "avatar": media_image_url + str(staffService.avatar)
                , "id": FromUserName
                , "type": "chat_message"
                , "content": staffService.notuserText
            }
            async_to_sync(channel_layer.group_send)(customeUser + FromUserName[str(FromUserName).rindex("_") + 1:], obj)

        result = {"code": 200, "msg": "ok"}
        return JsonResponse(result)


@login_required
def GetUserList(request):
    loginUser = request.user.username
    print("loginUser=================", loginUser)
    user = models.KF.objects.filter(userid=loginUser).first()
    mine = {"username": user.username, "id": user.userid, "status": "online", "sign": "客服001",
            "avatar": media_image_url + str(user.avatar)}

    ulist = models.userList.objects.filter(agent__agentid=user.agent.agentid).values("id", "username", "userid",
                                                                                     "avatar", "sign").order_by(
        "-createTime")[0:200]

    for item in ulist:
        item["avatar"] = media_image_url + item["avatar"]
        print(item["avatar"])

    friend = [{"groupname": "今天", "id": 1, "online": len(ulist), "list": list(ulist)},
              {"groupname": "前天", "id": 2, "online": 0, "list": []},
              {"groupname": "三天前", "id": 4, "online": 0, "list": []},
              {"groupname": "已回復", "id": 5, "online": 0, "list": []},
              {"groupname": "未回復", "id": 6, "online": 0, "list": []}]
    return JsonResponse({'code': 0, 'msg': "", "data": {"mine": mine, "friend": friend, "group": []}})


@login_required
def departmentServices(request):
    loginUser = request.user.username
    mine = {"username": request.user.first_name, "id": request.user.id, "status": "online", "sign": "127.0.0.1",
            "avatar": avatar_image_url + loginUser + ".png"}
    # 好友組
    friend = []
    agent = models.agent.objects.order_by("agentid")
    for item in agent:
        # 好友列表
        ulist = []
        staff = models.staffService.objects.filter(agent__agentid=item.agentid)
        for s in staff:
            ulist.append(
                {"id": serviceUser + str(s.id) + "_" + loginUser, "username": s.title,
                 "userid": serviceUser + str(s.id) + "_" + loginUser,
                 "avatar": media_image_url + str(s.avatar), "sign": s.desc})

        friend.append({"groupname": item.name, "id": item.id, "online": len(ulist), "list": list(ulist)})

    print("friend=============", friend)
    return JsonResponse({'code': 0, 'msg': "", "data": {"mine": mine, "friend": friend, "group": []}})


@login_required
def uploadImage(request):
    file = request.FILES.get("file")

    # 2.創建一個文件(用於保存圖片)
    fileName = str(uuid.uuid4()) + ".png"
    save_path = config.MEDIA_ROOT + "/" + fileName  # pic.name 上傳文件的源文件名
    with open(save_path, 'wb') as f:
        # 3.獲取上傳文件的內容並寫到創建的文件中
        for content in file.chunks():  # pic.chunks() 上傳文件的內容。
            f.write(content)

    return JsonResponse(
        {'code': 0, 'msg': "", "data": {"src": "{}/itkfstatic/uploadImage/{}".format(domain, fileName)}})


@login_required
def getWelcomeText(request):
    data = request.POST.get("data", None)
    name = request.POST["data[name]"]
    type = request.POST["data[type]"]
    avatar = request.POST["data[avatar]"]
    id = request.POST["data[id]"]

    # 當前服務id
    nid = id[str(id).find("_") + 1]

    import datetime
    # 獲取當前時間
    now = datetime.datetime.now()
    # 獲取今天零點
    zeroToday = now - datetime.timedelta(hours=now.hour, minutes=now.minute, seconds=now.second,
                                         microseconds=now.microsecond)
    # 獲取23:59:59
    lastToday = zeroToday + datetime.timedelta(hours=23, minutes=59, seconds=59)

    seconds = int((lastToday - now).total_seconds())
    redisKey = request.user.username + "$" + nid
    if not cache.get(redisKey):
        cache.set(redisKey, nid, timeout=seconds)

        staff = models.staffService.objects.filter(id=id[str(id).find("_") + 1]).first()

        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        from asgiref.sync import async_to_sync

        obj = {
            "username": name
            , "avatar": avatar
            , "id": id
            , "type": "chat_message"
            , "content": staff.welcomeText
        }

        print("rindex===========", id[str(id).rindex("_") + 1:])

        async_to_sync(channel_layer.group_send)(customeUser + id[str(id).rindex("_") + 1:], obj)

    return JsonResponse({'code': 0, 'msg': ""})


@login_required
def updateUserStatus(request):
    print(request.POST["state"])
    flag = False
    if request.POST["state"] == "online":
        flag = True

    models.KF.objects.filter(userid=request.user.username).update(status=flag)
    return JsonResponse({'code': 0, 'msg': ""})


# 微信企業號 接收消息服務器配置
from django.views.decorators.csrf import csrf_exempt


def downloadFile(data, fileType):
    '''
        result = client.media.get_url(data["MediaId"])
    file = str(uuid.uuid1()) + "." + fileType
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sourceUrl = os.path.join(BASE_DIR, sourceFile)
    LocalPath = os.path.join(sourceUrl, file)
    # os.path.join將多個路徑組合后返回
    urllib.request.urlretrieve(result, LocalPath)
    return "/{0}/{1}".format(sourceFile, file)

    '''
    return "OK"


class switch_wechat_messages(object):

    def case_to_function(self, case):
        fun_name = str(case) + "Message"
        method = getattr(self, fun_name, self.unknownMessage)
        return method

    def textMessage(self, data):
        Content = data["Content"]

    def imageMessage(self, data):
        PicUrl = data["PicUrl"]

        print(data)

    def shortVideoMessage(self, data):
        print(data)

    def videoMessage(self, data):
        fileType = "avi"
        print(data)

    def voiceMessage(self, data):
        fileType = data["Format"]

        print(data)

    def locationMessage(self, data):
        print(data)

    def linkMessage(self, data):
        print(data)

    def eventMessage(self, data):
        AgentID = data["AgentID"]
        FromUserName = data["FromUserName"]
        createUser(FromUserName, AgentID)

    def unknownMessage(self, data):
        print(data)


def createUser(userid, AgentID):
    return "OK"

13.nginx.conf 配置

upstream itkf {                                                         
        server 106.54.5.14:8004;                                                
}
location /ws/chat/ {
    proxy_pass http://itkf;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $server_name;
}

location /itkf {
    proxy_pass http://itkf;
    proxy_set_header Host $host:$server_port;
}

14.supervisor_test_itkf_http.conf 配置

[program:itkf]
environment=PUBTYPE="app",ENV="test";
command=python3 manage.py runserver 172.17.0.17:8004 --settings=itkf.test_settings ;被監控的進程路徑
directory=/itkf/       ; 執行前要不要先cd到目錄去,一般不用
priority=7                    ;數字越高,優先級越高
numprocs=1                    ; 啟動幾個進程
autostart=true                ; 隨着supervisord的啟動而啟動
autorestart=true              ; 自動重啟。。當然要選上了
startretries=10               ; 啟動失敗時的最多重試次數
exitcodes=0                   ; 正常退出代碼(是說退出代碼是這個時就不再重啟了嗎?待確定)
stopsignal=KILL               ; 用來殺死進程的信號
stopwaitsecs=10               ; 發送SIGKILL前的等待時間
redirect_stderr=true          ; 重定向stderr到stdout

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM