Django Channels介紹
首先要理解Django現有的請求響應策略是這樣的:瀏覽器發出請求,Django服務器接受請求后通過路由匹配該請求到某個視圖,視圖將會返回一個響應並由服務器發送回瀏覽器。類似的請求響應在Flask實現也是如此。對於一般性的網頁瀏覽(比如新聞閱讀),這樣的響應機制是沒有問題的,但對於需要一個保持不斷會話的請求來說,這是行不通的,因為Django的聲明周期只能存在一個請求中,它不能讓服務器在沒有請求的情況下不斷地發送數據島瀏覽器客服端。這樣的場景目前正在不斷地涌現,例如在線聊天室,會話機器人,以及最近很流行的微服務應用。
Channels改變了Django的工作方式,讓它實現了一種包括通道、消費者和worker的worker監聽的模式,所有消費者都會分配有單獨的通道,worker監聽通道的消息,確保消息到來時能進行處理。為了確保上述機制運行,Channels需要有三個工作層:
- 接口服務器,Django和用戶(瀏覽器)之間通信的橋梁,包括一個實現WSGI協議的適配器和一個獨立的websocket服務器。
- 通道后端, 在接口服務器和worker之間傳遞消息,由插拔式的python代碼和存儲組成,存儲可以是內存、數據庫或者redis,推薦使用redis,兼具其余兩者的優點。
- 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