diff --git a/server/apps/exam/__init__.py b/server/apps/exam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/exam/admin.py b/server/apps/exam/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/apps/exam/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/apps/exam/apps.py b/server/apps/exam/apps.py new file mode 100644 index 0000000..9649087 --- /dev/null +++ b/server/apps/exam/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ExamConfig(AppConfig): + name = 'exam' diff --git a/server/apps/exam/migrations/__init__.py b/server/apps/exam/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/exam/models.py b/server/apps/exam/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/server/apps/exam/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/server/apps/exam/tests.py b/server/apps/exam/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/apps/exam/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/apps/exam/urls.py b/server/apps/exam/urls.py new file mode 100644 index 0000000..e625e34 --- /dev/null +++ b/server/apps/exam/urls.py @@ -0,0 +1,6 @@ +from django.urls import path + +API_BASE_URL = 'api/exam/' +HTML_BASE_URL = 'exam/' +urlpatterns = [ +] \ No newline at end of file diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/server/apps/exam/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/server/apps/ops/__init__.py b/server/apps/ops/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/ops/admin.py b/server/apps/ops/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/apps/ops/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/apps/ops/apps.py b/server/apps/ops/apps.py new file mode 100644 index 0000000..23b85df --- /dev/null +++ b/server/apps/ops/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OpsConfig(AppConfig): + name = 'ops' + verbose_name = '系统监控' diff --git a/server/apps/ops/errors.py b/server/apps/ops/errors.py new file mode 100644 index 0000000..3bd94a2 --- /dev/null +++ b/server/apps/ops/errors.py @@ -0,0 +1 @@ +LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"} diff --git a/server/apps/ops/filters.py b/server/apps/ops/filters.py new file mode 100644 index 0000000..1f1756d --- /dev/null +++ b/server/apps/ops/filters.py @@ -0,0 +1,7 @@ +from django_filters import rest_framework as filters + + +class DrfLogFilterSet(filters.FilterSet): + start_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='gte') + end_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='lte') + id = filters.CharFilter() diff --git a/server/apps/ops/migrations/0001_initial.py b/server/apps/ops/migrations/0001_initial.py new file mode 100644 index 0000000..aceeb38 --- /dev/null +++ b/server/apps/ops/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 3.0.5 on 2022-10-12 06:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DrfRequestLog', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('requested_at', models.DateTimeField(db_index=True)), + ('response_ms', models.PositiveIntegerField(default=0)), + ('path', models.CharField(db_index=True, help_text='请求地址', max_length=400)), + ('view', models.CharField(blank=True, db_index=True, help_text='执行视图', max_length=400, null=True)), + ('view_method', models.CharField(blank=True, db_index=True, max_length=20, null=True)), + ('remote_addr', models.GenericIPAddressField()), + ('host', models.URLField()), + ('method', models.CharField(max_length=10)), + ('query_params', models.TextField(blank=True, null=True)), + ('data', models.TextField(blank=True, null=True)), + ('response', models.TextField(blank=True, null=True)), + ('errors', models.TextField(blank=True, null=True)), + ('agent', models.TextField(blank=True, null=True)), + ('status_code', models.PositiveIntegerField(blank=True, db_index=True, null=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'DRF请求日志', + }, + ), + ] diff --git a/server/apps/ops/migrations/0002_auto_20221012_1455.py b/server/apps/ops/migrations/0002_auto_20221012_1455.py new file mode 100644 index 0000000..e7c6210 --- /dev/null +++ b/server/apps/ops/migrations/0002_auto_20221012_1455.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.5 on 2022-10-12 06:55 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='drfrequestlog', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='drfrequestlog', + name='query_params', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='drfrequestlog', + name='response', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + ] diff --git a/server/apps/ops/migrations/__init__.py b/server/apps/ops/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/ops/mixins.py b/server/apps/ops/mixins.py new file mode 100644 index 0000000..e09b3f3 --- /dev/null +++ b/server/apps/ops/mixins.py @@ -0,0 +1,235 @@ +import ast +import json +import logging +import uuid +from django.utils.timezone import now +import traceback +from django.db import connection +from apps.ops.models import DrfRequestLog +import ipaddress +from user_agents import parse + +# 实例化myLogger +myLogger = logging.getLogger('log') + +class MyLoggingMixin(object): + """Mixin to log requests""" + + CLEANED_SUBSTITUTE = "********************" + + # logging_methods = "__all__" + logging_methods = '__all__' + sensitive_fields = {} + + def __init__(self, *args, **kwargs): + assert isinstance( + self.CLEANED_SUBSTITUTE, str + ), "CLEANED_SUBSTITUTE must be a string." + super().__init__(*args, **kwargs) + + def initial(self, request, *args, **kwargs): + request_id = uuid.uuid4() + self.log = {"requested_at": now(), "id": request_id} + setattr(request, 'request_id', request_id) + if not getattr(self, "decode_request_body", False): + self.log["data"] = "" + else: + self.log["data"] = self._clean_data(request.body) + + super().initial(request, *args, **kwargs) + + try: + # Accessing request.data *for the first time* parses the request body, which may raise + # ParseError and UnsupportedMediaType exceptions. It's important not to swallow these, + # as (depending on implementation details) they may only get raised this once, and + # DRF logic needs them to be raised by the view for error handling to work correctly. + data = self.request.data.dict() + except AttributeError: + data = self.request.data + self.log["data"] = self._clean_data(data) + + def handle_exception(self, exc): + response = super().handle_exception(exc) + self.log["errors"] = traceback.format_exc() + return response + + def finalize_response(self, request, response, *args, **kwargs): + response = super().finalize_response( + request, response, *args, **kwargs + ) + # Ensure backward compatibility for those using _should_log hook + should_log = ( + self._should_log if hasattr(self, "_should_log") else self.should_log + ) + if should_log(request, response): + if (connection.settings_dict.get("ATOMIC_REQUESTS") and + getattr(response, "exception", None) and connection.in_atomic_block): + # response with exception (HTTP status like: 401, 404, etc) + # pointwise disable atomic block for handle log (TransactionManagementError) + connection.set_rollback(True) + connection.set_rollback(False) + + if response.streaming: + rendered_content = None + elif hasattr(response, "rendered_content"): + rendered_content = response.rendered_content + else: + rendered_content = response.getvalue() + + self.log.update( + { + "remote_addr": self._get_ip_address(request), + "view": self._get_view_name(request), + "view_method": self._get_view_method(request), + "path": self._get_path(request), + "host": request.get_host(), + "method": request.method, + "query_params": self._clean_data(request.query_params.dict()), + "user": self._get_user(request), + "response_ms": self._get_response_ms(), + "response": self._clean_data(rendered_content), + "status_code": response.status_code, + "agent": self._get_agent(request), + } + ) + try: + self.handle_log() + except Exception: + # ensure that all exceptions raised by handle_log + # doesn't prevent API call to continue as expected + myLogger.exception("Logging API call raise exception!") + return response + + def handle_log(self): + """ + Hook to define what happens with the log. + + Defaults on saving the data on the db. + """ + DrfRequestLog(**self.log).save() + + def _get_path(self, request): + """Get the request path and truncate it""" + return request.path + + def _get_ip_address(self, request): + """Get the remote ip address the request was generated from.""" + ipaddr = request.META.get("HTTP_X_FORWARDED_FOR", None) + if ipaddr: + ipaddr = ipaddr.split(",")[0] + else: + ipaddr = request.META.get("REMOTE_ADDR", "") + + # Account for IPv4 and IPv6 addresses, each possibly with port appended. Possibilities are: + # + # + # :port + # []:port + # Note that ipv6 addresses are colon separated hex numbers + possibles = (ipaddr.lstrip("[").split("]")[0], ipaddr.split(":")[0]) + + for addr in possibles: + try: + return str(ipaddress.ip_address(addr)) + except ValueError: + pass + + return ipaddr + + def _get_view_name(self, request): + """Get view name.""" + method = request.method.lower() + try: + attributes = getattr(self, method) + return ( + type(attributes.__self__).__module__ + "." + type(attributes.__self__).__name__ + ) + + except AttributeError: + return None + + def _get_view_method(self, request): + """Get view method.""" + if hasattr(self, "action"): + return self.action or None + return request.method.lower() + + def _get_user(self, request): + """Get user.""" + user = request.user + if user.is_anonymous: + return None + return user + + def _get_agent(self, request): + """Get os string""" + return str(parse(request.META['HTTP_USER_AGENT'])) + + def _get_response_ms(self): + """ + Get the duration of the request response cycle is milliseconds. + In case of negative duration 0 is returned. + """ + response_timedelta = now() - self.log["requested_at"] + response_ms = int(response_timedelta.total_seconds() * 1000) + return max(response_ms, 0) + + def should_log(self, request, response): + """ + Method that should return a value that evaluated to True if the request should be logged. + By default, check if the request method is in logging_methods. + """ + return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \ + or (request.method in self.logging_methods and response.status_code not in [401, 403, 404]) + + def _clean_data(self, data): + """ + Clean a dictionary of data of potentially sensitive info before + sending to the database. + Function based on the "_clean_credentials" function of django + (https://github.com/django/django/blob/stable/1.11.x/django/contrib/auth/__init__.py#L50) + + Fields defined by django are by default cleaned with this function + + You can define your own sensitive fields in your view by defining a set + eg: sensitive_fields = {'field1', 'field2'} + """ + + if isinstance(data, bytes): + data = data.decode(errors="replace") + try: + data = json.loads(data) + except: + pass + + if isinstance(data, list): + return [self._clean_data(d) for d in data] + + if isinstance(data, dict): + SENSITIVE_FIELDS = { + "api", + "token", + "key", + "secret", + "password", + "signature", + "access", + "refresh" + } + + data = dict(data) + if self.sensitive_fields: + SENSITIVE_FIELDS = SENSITIVE_FIELDS | { + field.lower() for field in self.sensitive_fields + } + + for key, value in data.items(): + try: + value = ast.literal_eval(value) + except (ValueError, SyntaxError): + pass + if isinstance(value, (list, dict)): + data[key] = self._clean_data(value) + if key.lower() in SENSITIVE_FIELDS: + data[key] = self.CLEANED_SUBSTITUTE + return data \ No newline at end of file diff --git a/server/apps/ops/models.py b/server/apps/ops/models.py new file mode 100644 index 0000000..a4efddd --- /dev/null +++ b/server/apps/ops/models.py @@ -0,0 +1,49 @@ +import uuid +from django.db import models +from django.contrib.postgres.fields import JSONField + +class DrfRequestLog(models.Model): + """Logs Django rest framework API requests""" + + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + user = models.ForeignKey( + 'system.user', + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + requested_at = models.DateTimeField(db_index=True) + response_ms = models.PositiveIntegerField(default=0) + path = models.CharField( + max_length=400, + db_index=True, + help_text="请求地址", + ) + view = models.CharField( + max_length=400, + null=True, + blank=True, + db_index=True, + help_text="执行视图", + ) + view_method = models.CharField( + max_length=20, + null=True, + blank=True, + db_index=True, + ) + remote_addr = models.GenericIPAddressField() + host = models.URLField() + method = models.CharField(max_length=10) + query_params = JSONField(null=True, blank=True) + data = JSONField(null=True, blank=True) + response = JSONField(null=True, blank=True) + errors = models.TextField(null=True, blank=True) + agent = models.TextField(null=True, blank=True) + status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True) + + class Meta: + verbose_name = "DRF请求日志" + + def __str__(self): + return "{} {}".format(self.method, self.path) diff --git a/server/apps/ops/tasks.py b/server/apps/ops/tasks.py new file mode 100644 index 0000000..43300a8 --- /dev/null +++ b/server/apps/ops/tasks.py @@ -0,0 +1,17 @@ +# Create your tasks here +from __future__ import absolute_import, unicode_literals +from datetime import timedelta +from apps.ops.models import DrfRequestLog +# from celery import shared_task +from django.utils import timezone + + +# @shared_task() +# def clear_drf_log(): +# """清除7天前的日志记录 + +# 清除7天前的日志记录 +# """ +# now = timezone.now() +# days7_ago = now - timedelta(days=7) +# DrfRequestLog.objects.filter(create_time__lte=days7_ago).delete() diff --git a/server/apps/ops/tests.py b/server/apps/ops/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/apps/ops/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/apps/ops/urls.py b/server/apps/ops/urls.py new file mode 100644 index 0000000..d00faeb --- /dev/null +++ b/server/apps/ops/urls.py @@ -0,0 +1,16 @@ +from django.urls import path, include +from rest_framework import routers +from .views import CpuView, DiskView, DrfRequestLogViewSet, LogView, LogDetailView, MemoryView + +API_BASE_URL = 'api/ops/' +HTML_BASE_URL = 'ops/' +router = routers.DefaultRouter() +router.register('request_log', DrfRequestLogViewSet, basename="request_log") +urlpatterns = [ + path(API_BASE_URL, include(router.urls)), + path(API_BASE_URL + 'log/', LogView.as_view()), + path(API_BASE_URL + 'log//', LogDetailView.as_view()), + path(API_BASE_URL + 'server/cpu/', CpuView.as_view()), + path(API_BASE_URL + 'server/memory/', MemoryView.as_view()), + path(API_BASE_URL + 'server/disk/', DiskView.as_view()) +] diff --git a/server/apps/ops/views.py b/server/apps/ops/views.py new file mode 100644 index 0000000..9a735cd --- /dev/null +++ b/server/apps/ops/views.py @@ -0,0 +1,159 @@ + +from django.shortcuts import render +import psutil +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from django.conf import settings +import os +from rest_framework import serializers +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework.exceptions import NotFound +from rest_framework.mixins import ListModelMixin +from apps.ops.filters import DrfLogFilterSet +from apps.ops.models import DrfRequestLog +from rest_framework.viewsets import GenericViewSet +from apps.ops.errors import LOG_NOT_FONED +from rest_framework.decorators import action + +# Create your views here. + +class CpuView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, *args, **kwargs): + """ + 获取服务器cpu当前状态 + + 获取服务器cpu当前状态 + """ + ret = {'cpu': {}} + ret['cpu']['count'] = psutil.cpu_count() + ret['cpu']['lcount'] = psutil.cpu_count(logical=False) + ret['cpu']['percent'] = psutil.cpu_percent(interval=1) + return Response(ret) + +class MemoryView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, *args, **kwargs): + """ + 获取服务器内存当前状态 + + 获取服务器内存当前状态 + """ + ret = {'memory': {}} + memory = psutil.virtual_memory() + ret['memory']['total'] = round(memory.total/1024/1024/1024, 2) + ret['memory']['used'] = round(memory.used/1024/1024/1024, 2) + ret['memory']['percent'] = memory.percent + return Response(ret) + +class DiskView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, *args, **kwargs): + """ + 获取服务器硬盘当前状态 + + 获取服务器硬盘当前状态 + """ + ret = {'disk': {}} + disk = psutil.disk_usage('/') + ret['disk']['total'] = round(disk.total/1024/1024/1024, 2) + ret['disk']['used'] = round(disk.used/1024/1024/1024, 2) + ret['disk']['percent'] = disk.percent + return Response(ret) + +def get_file_list(file_path): + dir_list = os.listdir(file_path) + if not dir_list: + return + else: + # 注意,这里使用lambda表达式,将文件按照最后修改时间顺序升序排列 + # os.path.getmtime() 函数是获取文件最后修改时间 + # os.path.getctime() 函数是获取文件最后创建时间 + dir_list = sorted(dir_list, key=lambda x: os.path.getmtime( + os.path.join(file_path, x)), reverse=True) + # print(dir_list) + return dir_list + + +class LogView(APIView): + + @swagger_auto_schema(manual_parameters=[ + openapi.Parameter('name', openapi.IN_QUERY, + description='日志文件名', type=openapi.TYPE_STRING) + ]) + def get(self, request, *args, **kwargs): + """ + 查看最近的日志列表 + + 查看最近的日志列表 + """ + logs = [] + name = request.GET.get('name', None) + # for root, dirs, files in os.walk(settings.LOG_PATH): + # files.reverse() + for file in get_file_list(settings.LOG_PATH): + if len(logs) > 50: + break + filepath = os.path.join(settings.LOG_PATH, file) + if name: + if name in filepath: + fsize = os.path.getsize(filepath) + if fsize: + logs.append({ + "name": file, + "filepath": filepath, + "size": round(fsize/1024, 1) + }) + else: + fsize = os.path.getsize(filepath) + if fsize: + logs.append({ + "name": file, + "filepath": filepath, + "size": round(fsize/1024, 1) + }) + return Response(logs) + + +class LogDetailView(APIView): + + def get(self, request, name): + """ + 查看日志详情 + + 查看日志详情 + """ + try: + with open(os.path.join(settings.LOG_PATH, name)) as f: + data = f.read() + return Response(data) + except Exception: + raise NotFound(**LOG_NOT_FONED) + + +class DrfRequestLogSerializer(serializers.ModelSerializer): + class Meta: + model = DrfRequestLog + fields = '__all__' + + +class DrfRequestLogViewSet(ListModelMixin, GenericViewSet): + """请求日志 + + 请求日志 + """ + perms_map = {'get': '*'} + queryset = DrfRequestLog.objects.all() + serializer_class = DrfRequestLogSerializer + ordering = ['-requested_at'] + filterset_class = DrfLogFilterSet + + @action(methods=['delete'], detail=False, perms_map = {'delete':'log_delete'}) + def clear(self, request, *args, **kwargs): + """ + 清空日志 + """ + DrfRequestLog.objects.all().delete() + return Response() diff --git a/server/apps/system/admin.py b/server/apps/system/admin.py index e47b0d7..166eb4a 100644 --- a/server/apps/system/admin.py +++ b/server/apps/system/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from simple_history.admin import SimpleHistoryAdmin from .models import User, Organization, Role, Permission, DictType, Dict, File # Register your models here. admin.site.register(User) @@ -7,5 +6,5 @@ admin.site.register(Organization) admin.site.register(Role) admin.site.register(Permission) admin.site.register(DictType) -admin.site.register(Dict, SimpleHistoryAdmin) +admin.site.register(Dict) admin.site.register(File) \ No newline at end of file diff --git a/server/apps/system/migrations/0003_auto_20200528_1716.py b/server/apps/system/migrations/0003_auto_20200528_1716.py index 9779308..0125b22 100644 --- a/server/apps/system/migrations/0003_auto_20200528_1716.py +++ b/server/apps/system/migrations/0003_auto_20200528_1716.py @@ -4,7 +4,6 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone -import simple_history.models class Migration(migrations.Migration): @@ -54,7 +53,6 @@ class Migration(migrations.Migration): 'ordering': ('-history_date', '-history_id'), 'get_latest_by': 'history_date', }, - bases=(simple_history.models.HistoricalChanges, models.Model), ), migrations.CreateModel( name='File', diff --git a/server/apps/system/migrations/0022_delete_historicaldict.py b/server/apps/system/migrations/0022_delete_historicaldict.py new file mode 100644 index 0000000..c2520b3 --- /dev/null +++ b/server/apps/system/migrations/0022_delete_historicaldict.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.5 on 2022-10-12 06:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0021_auto_20220530_1520'), + ] + + operations = [ + migrations.DeleteModel( + name='HistoricalDict', + ), + ] diff --git a/server/apps/system/models.py b/server/apps/system/models.py index fa5eed7..8b6781f 100644 --- a/server/apps/system/models.py +++ b/server/apps/system/models.py @@ -4,7 +4,6 @@ import django.utils.timezone as timezone from django.db.models.query import QuerySet from utils.model import SoftModel, BaseModel -from simple_history.models import HistoricalRecords class Province(models.Model): id = models.CharField('id', primary_key=True, max_length=20) @@ -160,7 +159,6 @@ class Dict(SoftModel): pid = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父') is_used = models.BooleanField('是否有效', default=True) - history = HistoricalRecords() class Meta: verbose_name = '字典' diff --git a/server/apps/system/views.py b/server/apps/system/views.py index c2a09e9..c716e5d 100644 --- a/server/apps/system/views.py +++ b/server/apps/system/views.py @@ -1,4 +1,5 @@ import logging +from re import L from django.conf import settings from django.contrib.auth.hashers import check_password, make_password @@ -23,6 +24,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework_simplejwt.tokens import RefreshToken +from apps.ops.mixins import MyLoggingMixin from utils.pagination import PageOrNot from utils.queryset import get_child_queryset2 @@ -30,6 +32,7 @@ from .filters import UserFilter from .models import (City, Dict, DictType, File, Message, Organization, Permission, Position, Province, Role, User, UserThird) from .permission import RbacPermission, get_permission_list +from rest_framework_simplejwt.views import TokenObtainPairView from .permission_data import RbacFilterSet from .serializers import (CitySerializer, DictSerializer, DictTypeSerializer, FileSerializer, OrganizationSerializer, PermissionSerializer, @@ -71,8 +74,11 @@ def get_tokens_for_user(user): } import datetime +class MyTokenView(MyLoggingMixin, TokenObtainPairView): + def should_log(self, request, response): + return response.status_code == 200 -class Login2View(APIView): +class Login2View(MyLoggingMixin, APIView): """ 邮箱验证码登录 """ @@ -88,6 +94,9 @@ class Login2View(APIView): user = User.objects.get(username=mail) return Response(get_tokens_for_user(user), status=status.HTTP_200_OK) return Response('验证码错误', status=status.HTTP_400_BAD_REQUEST) + + def should_log(self, request, response): + return response.status_code == 200 class sendMsg(APIView): authentication_classes = [] @@ -407,7 +416,7 @@ class UserViewSet(PageOrNot, ModelViewSet): -class WXMPlogin(APIView): +class WXMPlogin(MyLoggingMixin, APIView): authentication_classes=[] permission_classes=[] @@ -428,6 +437,9 @@ class WXMPlogin(APIView): return Response(get_tokens_for_user(user), status=status.HTTP_200_OK) except: raise AuthenticationFailed + + def should_log(self, request, response): + return response.status_code == 200 diff --git a/server/apps/vod/migrations/0006_viewrecord_total_seconds.py b/server/apps/vod/migrations/0006_viewrecord_total_seconds.py new file mode 100644 index 0000000..9183cf7 --- /dev/null +++ b/server/apps/vod/migrations/0006_viewrecord_total_seconds.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2022-10-12 08:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vod', '0005_video_sort_str'), + ] + + operations = [ + migrations.AddField( + model_name='viewrecord', + name='total_seconds', + field=models.PositiveIntegerField(default=0, verbose_name='总观看秒数'), + ), + ] diff --git a/server/apps/vod/models.py b/server/apps/vod/models.py index ba65284..694055f 100644 --- a/server/apps/vod/models.py +++ b/server/apps/vod/models.py @@ -1,3 +1,4 @@ +from tabnanny import verbose from django.db import models from utils.model import BaseModel from apps.system.models import User, CommonAModel, Dict @@ -30,6 +31,7 @@ class ViewRecord(BaseModel): views = models.IntegerField(verbose_name='观看次数', default=0) current = models.IntegerField(verbose_name='当前观看进度(秒)', default=0) video = models.ForeignKey(Video, verbose_name='点播视频', on_delete=models.CASCADE, related_name='record_video') + total_seconds = models.PositiveIntegerField(verbose_name='总观看秒数', default=0) class Meta: diff --git a/server/apps/vod/views.py b/server/apps/vod/views.py index f210001..7b1b255 100644 --- a/server/apps/vod/views.py +++ b/server/apps/vod/views.py @@ -133,6 +133,9 @@ class MyViewRecordAPIView(APIView): record.views = record.views + 1 video.views = video.views + 1 video.save() + else: + record.total_seconds = record.total_seconds + 10 + record.save() record.save() return Response() diff --git a/server/requirements.txt b/server/requirements.txt index 5a59d6b..77fa7d5 100644 Binary files a/server/requirements.txt and b/server/requirements.txt differ diff --git a/server/server/settings.py b/server/server/settings.py index 36c0ac0..b7396ec 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -48,7 +48,9 @@ INSTALLED_APPS = [ 'apps.supervision', 'apps.quality', 'apps.vod', - 'apps.consulting' + 'apps.consulting', + 'apps.exam', + 'apps.ops' ] @@ -167,8 +169,8 @@ REST_FRAMEWORK = { 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 'DATE_FORMAT': '%Y-%m-%d', 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - 'UNAUTHENTICATED_USER': None, - 'UNAUTHENTICATED_TOKEN': None, + # 'UNAUTHENTICATED_USER': None, + # 'UNAUTHENTICATED_TOKEN': None, } # simplejwt配置 SIMPLE_JWT = { diff --git a/server/server/urls.py b/server/server/urls.py index 7a4d726..b4b8c41 100644 --- a/server/server/urls.py +++ b/server/server/urls.py @@ -19,17 +19,13 @@ from django.contrib import admin from django.urls import include, path from rest_framework import routers from rest_framework.documentation import include_docs_urls -from rest_framework_simplejwt.views import (TokenObtainPairView, - TokenRefreshView) +from rest_framework_simplejwt.views import TokenRefreshView -from apps.system.views import FileViewSet, LogoutView, Login2View +from apps.system.views import FileViewSet, LogoutView, Login2View, MyTokenView from django.views.generic.base import TemplateView router = routers.DefaultRouter() router.register('file', FileViewSet, basename="file") from django.conf.urls import url - -from rest_framework_simplejwt.serializers import TokenObtainPairSerializer -from rest_framework_simplejwt.views import TokenViewBase from apps.system.views import WXMPlogin,mediaauth from drf_yasg import openapi from drf_yasg.views import get_schema_view @@ -54,7 +50,7 @@ urlpatterns = [ path('api/admin/', admin.site.urls), path('api/mediaauth/',mediaauth), path('api/wxmplogin/',WXMPlogin.as_view()), - path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/', MyTokenView.as_view(), name='token_obtain_pair'), path('api/token2/', Login2View.as_view(), name='token_obtain_2'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/token/black/', LogoutView.as_view(), name='token_black'), @@ -64,6 +60,8 @@ urlpatterns = [ path('api/quality/', include('apps.quality.urls')), path('api/vod/', include('apps.vod.urls')), path('api/consulting/', include('apps.consulting.urls')), + path('', include('apps.ops.urls')), + path('', include('apps.exam.urls')), path('api/docs/', include_docs_urls(title="接口文档",authentication_classes=[], permission_classes=[])), url(r'^api/swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), diff --git a/server/temp/570eef2524c1a8e683521a9e16a59039.djcache b/server/temp/570eef2524c1a8e683521a9e16a59039.djcache new file mode 100644 index 0000000..2106644 Binary files /dev/null and b/server/temp/570eef2524c1a8e683521a9e16a59039.djcache differ diff --git a/server/utils/model.py b/server/utils/model.py index f8ded06..c6ba63b 100644 --- a/server/utils/model.py +++ b/server/utils/model.py @@ -49,6 +49,9 @@ class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager): class BaseModel(models.Model): + """ + 自增ID,带有create_time, update_time, is_deleted + """ create_time = models.DateTimeField( default=timezone.now, verbose_name='创建时间', help_text='创建时间') update_time = models.DateTimeField( diff --git a/server/utils/pagination.py b/server/utils/pagination.py index e0fe3ea..a89a67a 100644 --- a/server/utils/pagination.py +++ b/server/utils/pagination.py @@ -7,11 +7,9 @@ class MyPagination(PageNumberPagination): page_size_query_param = 'page_size' class PageOrNot: - def paginate_queryset(self, queryset): - if (self.paginator is None): - return None - elif (self.request.query_params.get('pageoff', None)) and queryset.count()<500: - return None - elif (self.request.query_params.get('pageoff', None)) and queryset.count()>=500: - raise ParseError('单次请求数据量大,请求中止') - return self.paginator.paginate_queryset(queryset, self.request, view=self) \ No newline at end of file + def paginate_queryset(self, queryset, request, view=None): + if request.query_params.get('pageoff', None) or request.query_params.get('page', None) == '0': + if queryset.count() < 500: + return None + raise ParseError('单次请求数据量大,请分页获取') + return super().paginate_queryset(queryset, request, view=view) \ No newline at end of file