From b6b970f7a213b2451264efed459a0ecba08454b7 Mon Sep 17 00:00:00 2001 From: ahhui Date: Tue, 8 Aug 2023 17:40:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=89=A9=E8=81=94=E7=BD=91=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/application/routing.py | 5 +- backend/application/settings.py | 6 +-- backend/application/urls.py | 5 ++ backend/application/websocketConfig.py | 18 ++++--- backend/device/__init__.py | 0 backend/device/admin.py | 3 ++ backend/device/apps.py | 6 +++ backend/device/migrations/__init__.py | 0 backend/device/models.py | 73 ++++++++++++++++++++++++++ backend/device/routing.py | 6 +++ backend/device/tests.py | 3 ++ backend/device/urls.py | 16 ++++++ backend/device/views/gateway.py | 18 +++++++ backend/device/views/template.py | 33 ++++++++++++ backend/device/views/terminal.py | 18 +++++++ backend/device/websocket.py | 41 +++++++++++++++ 16 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 backend/device/__init__.py create mode 100644 backend/device/admin.py create mode 100644 backend/device/apps.py create mode 100644 backend/device/migrations/__init__.py create mode 100644 backend/device/models.py create mode 100644 backend/device/routing.py create mode 100644 backend/device/tests.py create mode 100644 backend/device/urls.py create mode 100644 backend/device/views/gateway.py create mode 100644 backend/device/views/template.py create mode 100644 backend/device/views/terminal.py create mode 100644 backend/device/websocket.py diff --git a/backend/application/routing.py b/backend/application/routing.py index 237e5d1..6f8bf02 100644 --- a/backend/application/routing.py +++ b/backend/application/routing.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from django.urls import path from application.websocketConfig import MegCenter +from device.routing import websocket_urlpatterns as device_ws_url websocket_urlpatterns = [ - path('ws//', MegCenter.as_asgi()), #consumers.DvadminWebSocket 是该路由的消费者 + path('ws//', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者 + *device_ws_url, ] - diff --git a/backend/application/settings.py b/backend/application/settings.py index f2cca95..b311bae 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -166,9 +166,9 @@ CORS_ORIGIN_ALLOW_ALL = True # 允许cookie CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持对cookie的操作 -# ================================================= # +# ===================================================== # # ********************* channels配置 ******************* # -# ================================================= # +# ===================================================== # ASGI_APPLICATION = 'application.asgi.application' CHANNEL_LAYERS = { "default": { @@ -306,7 +306,7 @@ AUTHENTICATION_BACKENDS = ["dvadmin.utils.backends.CustomBackend"] # ================================================= # SIMPLE_JWT = { # token有效时长 - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=120), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1440), # token刷新后的有效时间 "REFRESH_TOKEN_LIFETIME": timedelta(days=1), # 设置前缀 diff --git a/backend/application/urls.py b/backend/application/urls.py index f7ba7cb..04297e8 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -30,6 +30,7 @@ from dvadmin.system.views.login import ( CaptchaView, ApiLogin, LogoutView, + LoginTokenView ) from dvadmin.system.views.system_config import InitSettingsViewSet from dvadmin.utils.swagger import CustomOpenAPISchemaGenerator @@ -81,6 +82,10 @@ urlpatterns = ( path("api/init/dictionary/", InitDictionaryViewSet.as_view()), path("api/init/settings/", InitSettingsViewSet.as_view()), path("apiLogin/", ApiLogin.as_view()), + path("api/device/", include("device.urls")), + + # 仅用于开发,上线需关闭 + path("api/token/", LoginTokenView.as_view()), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_URL) diff --git a/backend/application/websocketConfig.py b/backend/application/websocketConfig.py index 80515f4..c36d97a 100644 --- a/backend/application/websocketConfig.py +++ b/backend/application/websocketConfig.py @@ -122,7 +122,8 @@ class MessageCreateSerializer(CustomModelSerializer): fields = "__all__" read_only_fields = ["id"] -def websocket_push(user_id,message): + +def websocket_push(user_id, message): username = "user_" + str(user_id) channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( @@ -133,8 +134,9 @@ def websocket_push(user_id,message): } ) -def create_message_push(title: str, content: str, target_type: int=0, target_user: list=[], target_dept=None, target_role=None, - message: dict = {'contentType': 'INFO', 'content': '测试~'}, request= Request): + +def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None, + target_role=None, message: dict = None, request=Request): if message is None: message = {"contentType": "INFO", "content": None} if target_role is None: @@ -145,11 +147,11 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use "title": title, "content": content, "target_type": target_type, - "target_user":target_user, - "target_dept":target_dept, - "target_role":target_role + "target_user": target_user, + "target_dept": target_dept, + "target_role": target_role } - message_center_instance = MessageCreateSerializer(data=data,request=request) + message_center_instance = MessageCreateSerializer(data=data, request=request) message_center_instance.is_valid(raise_exception=True) message_center_instance.save() users = target_user or [] @@ -176,6 +178,6 @@ def create_message_push(title: str, content: str, target_type: int=0, target_use username, { "type": "push.message", - "json": {**message,'unread':unread_count} + "json": {**message, 'unread': unread_count} } ) diff --git a/backend/device/__init__.py b/backend/device/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/device/admin.py b/backend/device/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/device/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/device/apps.py b/backend/device/apps.py new file mode 100644 index 0000000..af56959 --- /dev/null +++ b/backend/device/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DeviceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'device' diff --git a/backend/device/migrations/__init__.py b/backend/device/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/device/models.py b/backend/device/models.py new file mode 100644 index 0000000..434ed6c --- /dev/null +++ b/backend/device/models.py @@ -0,0 +1,73 @@ +from django.db import models + +from dvadmin.utils.models import CoreModel, table_prefix + + +class Template(CoreModel): + CLASSIFY_CHOICE = ( + (0, "网关"), + (1, "终端设备"), + ) + classify = models.SmallIntegerField(choices=CLASSIFY_CHOICE, verbose_name="模板类别") + name = models.CharField(max_length=128, verbose_name="模板名称", unique=True) + + class Meta: + db_table = table_prefix + 'device_template' + verbose_name = "模板表" + verbose_name_plural = verbose_name + ordering = ("-id",) + + +class TemplateDetail(CoreModel): + template = models.ForeignKey(to="Template", null=False, on_delete=models.CASCADE, db_constraint=False, + verbose_name="所属模板") + key_title = models.CharField(max_length=64, verbose_name="键标题", unique=True) + key_name = models.CharField(max_length=64, verbose_name="键名") + key_type = models.CharField(max_length=32, verbose_name="键值类型") + parent_key = models.ForeignKey(to="TemplateDetail", null=True, on_delete=models.CASCADE, db_constraint=False, + verbose_name="父级键") + + class Meta: + db_table = table_prefix + 'device_template_detail' + verbose_name = "模板详情表" + verbose_name_plural = verbose_name + ordering = ("-id",) + unique_together = (('key_name', 'parent_key'),) + + +class Gateway(CoreModel): + name = models.CharField(max_length=128, verbose_name="设备名称", unique=True) + specification = models.CharField(max_length=32, verbose_name="设备型号") + mac_address = models.CharField(max_length=32, verbose_name="设备MAC地址") + version = models.CharField(max_length=64, verbose_name="设备固件版本号") + ip_address = models.CharField(max_length=32, verbose_name="设备IP地址") + physics_address = models.CharField(max_length=255, default="暂无位置信息", verbose_name="设备实际地址") + account = models.CharField(max_length=32, verbose_name="设备账号") + password = models.CharField(max_length=32, verbose_name="设备密码") + template = models.ForeignKey(to="Template", null=False, on_delete=models.CASCADE, db_constraint=False, + verbose_name="所用模板") + + class Meta: + db_table = table_prefix + 'device_gateways' + verbose_name = "网关表" + verbose_name_plural = verbose_name + ordering = ("-id",) + + +class Terminal(CoreModel): + name = models.CharField(max_length=128, verbose_name="设备名称", unique=True) + specification = models.CharField(max_length=32, verbose_name="设备型号") + identify = models.CharField(max_length=128, verbose_name="设备标识", unique=True) + physics_address = models.CharField(max_length=255, default="暂无位置信息", verbose_name="设备实际地址") + project = models.CharField(max_length=128, verbose_name="所属项目") + remark = models.CharField(max_length=255, null=True, verbose_name="设备备注") + gateway = models.ForeignKey(to=Gateway, null=True, on_delete=models.CASCADE, db_constraint=False, + verbose_name="关联网关") + template = models.ForeignKey(to="Template", null=False, on_delete=models.CASCADE, db_constraint=False, + verbose_name="所用模板") + + class Meta: + db_table = table_prefix + 'device_terminal' + verbose_name = "终端设备表" + verbose_name_plural = verbose_name + ordering = ("-id",) diff --git a/backend/device/routing.py b/backend/device/routing.py new file mode 100644 index 0000000..b7a0300 --- /dev/null +++ b/backend/device/routing.py @@ -0,0 +1,6 @@ +from django.urls import path +from device.websocket import DeviceStatusWebSocket + +websocket_urlpatterns = [ + path('ws/gateway_status//', DeviceStatusWebSocket.as_asgi()), +] diff --git a/backend/device/tests.py b/backend/device/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/device/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/device/urls.py b/backend/device/urls.py new file mode 100644 index 0000000..db1e72b --- /dev/null +++ b/backend/device/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from rest_framework import routers + +from device.views.gateway import GatewayViewSet +from device.views.template import TemplateViewSet, TemplateDetailViewSet +from device.views.terminal import TerminalViewSet + + +device_url = routers.SimpleRouter() +device_url.register(r'gateway', GatewayViewSet) +device_url.register(r'template', TemplateViewSet) +device_url.register(r'template_detail', TemplateDetailViewSet) +device_url.register(r'terminal', TerminalViewSet) + +urlpatterns = [] +urlpatterns += device_url.urls diff --git a/backend/device/views/gateway.py b/backend/device/views/gateway.py new file mode 100644 index 0000000..3396af2 --- /dev/null +++ b/backend/device/views/gateway.py @@ -0,0 +1,18 @@ +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.utils.viewset import CustomModelViewSet +from device.models import Gateway + + +class GatewaySerializer(CustomModelSerializer): + """网关管理序列化器""" + + class Meta: + model = Gateway + fields = '__all__' + read_only_fields = ['id'] + + +class GatewayViewSet(CustomModelViewSet): + """网关管理视图集""" + queryset = Gateway.objects.all() + serializer_class = GatewaySerializer diff --git a/backend/device/views/template.py b/backend/device/views/template.py new file mode 100644 index 0000000..42d2093 --- /dev/null +++ b/backend/device/views/template.py @@ -0,0 +1,33 @@ +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.utils.viewset import CustomModelViewSet +from device.models import Template, TemplateDetail + + +class TemplateSerializer(CustomModelSerializer): + """模板管理序列化器""" + + class Meta: + model = Template + fields = '__all__' + read_only_fields = ['id'] + + +class TemplateDetailSerializer(CustomModelSerializer): + """模板详情序列化器""" + + class Meta: + model = TemplateDetail + fields = '__all__' + read_only_fields = ['id'] + + +class TemplateViewSet(CustomModelViewSet): + """模板管理视图集""" + queryset = Template.objects.all() + serializer_class = TemplateSerializer + + +class TemplateDetailViewSet(CustomModelViewSet): + """模板详情视图集""" + queryset = TemplateDetail.objects.all() + serializer_class = TemplateDetailSerializer diff --git a/backend/device/views/terminal.py b/backend/device/views/terminal.py new file mode 100644 index 0000000..8d6fb33 --- /dev/null +++ b/backend/device/views/terminal.py @@ -0,0 +1,18 @@ +from dvadmin.utils.serializers import CustomModelSerializer +from dvadmin.utils.viewset import CustomModelViewSet +from device.models import Terminal + + +class TerminalSerializer(CustomModelSerializer): + """终端设备管理序列化器""" + + class Meta: + model = Terminal + fields = '__all__' + read_only_fields = ['id'] + + +class TerminalViewSet(CustomModelViewSet): + """终端设备管理视图集""" + queryset = Terminal.objects.all() + serializer_class = TerminalSerializer diff --git a/backend/device/websocket.py b/backend/device/websocket.py new file mode 100644 index 0000000..4570787 --- /dev/null +++ b/backend/device/websocket.py @@ -0,0 +1,41 @@ +import json +from hashlib import md5 +from datetime import datetime + +import jwt +from channels.generic.websocket import AsyncJsonWebsocketConsumer + +from application.websocketConfig import set_message + + +class DeviceStatusWebSocket(AsyncJsonWebsocketConsumer): + """设备状态ws""" + chat_group = 'device_status' + + async def connect(self): + try: + print('设备ws创建连接', self.scope) + self.token = self.scope['url_route']['kwargs']['token'] + decoded_result = jwt.decode(self.token, settings.SECRET_KEY, algorithms=['HS256']) + if decoded_result: + self.uid = decoded_result.get('user_id') + self.hash = md5(str(self.token).encode('utf-8')) + self.chat_name = f'{self.chat_group}:user_{self.uid}' + await self.channel_layer.group_add(self.chat_name, self.channel_name) + await self.accept() + else: + raise jwt.InvalidSignatureError() + except jwt.InvalidSignatureError: + await self.send_json(set_message('system', 'SYSTEM', {'message': 'Token无效'}), True) + await self.disconnect(None) + except jwt.ExpiredSignatureError: + await self.send_json(set_message('system', 'SYSTEM', {'message': 'Token过期'}), True) + await self.disconnect(None) + + async def disconnect(self, code): + print('设备ws连接关闭') + await self.channel_layer.group_discard(self.chat_name, self.channel_name) + try: + await self.close(code) + except: + pass