From 680a0dceeb1d9f0bccef32b4d95bae9ed7344776 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 8 Apr 2022 09:31:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AF=B7=E6=B1=82=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/auth1/errors.py | 2 +- apps/auth1/views.py | 3 +- apps/hrm/__init__.py | 0 apps/hrm/admin.py | 3 + apps/hrm/apps.py | 9 + apps/hrm/filters.py | 37 +++ apps/hrm/migrations/0001_initial.py | 39 +++ .../hrm/migrations/0002_auto_20210924_1127.py | 35 +++ .../hrm/migrations/0003_employee_face_data.py | 18 ++ apps/hrm/migrations/0004_clockrecord.py | 32 +++ .../hrm/migrations/0005_auto_20220126_1351.py | 37 +++ .../hrm/migrations/0006_auto_20220217_2155.py | 23 ++ .../hrm/migrations/0007_auto_20220218_0843.py | 41 +++ .../hrm/migrations/0008_auto_20220222_1112.py | 32 +++ .../migrations/0009_employee_show_atwork.py | 18 ++ apps/hrm/migrations/__init__.py | 0 apps/hrm/models.py | 63 +++++ apps/hrm/serializers.py | 37 +++ apps/hrm/services.py | 50 ++++ apps/hrm/signals.py | 13 + apps/hrm/tasks.py | 30 +++ apps/hrm/tests.py | 3 + apps/hrm/urls.py | 15 ++ apps/hrm/views.py | 187 ++++++++++++++ apps/monitor/errors.py | 1 + apps/monitor/middleware.py | 1 - apps/monitor/migrations/0001_initial.py | 45 ++++ apps/monitor/models.py | 46 +++- apps/monitor/urls.py | 3 +- apps/monitor/views.py | 23 +- apps/system/errors.py | 8 +- .../migrations/0002_auto_20220406_1458.py | 64 +++++ apps/system/models.py | 1 + apps/system/serializers.py | 99 +++++--- apps/system/views.py | 53 ++-- apps/third/views.py | 42 +++- apps/utils/{vars.py => constants.py} | 0 apps/utils/dahua.py | 25 +- apps/utils/errors.py | 4 + apps/utils/exceptions.py | 19 +- apps/utils/mixins.py | 237 +++++++++++++++++- apps/utils/models.py | 49 ++-- apps/utils/permission.py | 3 + apps/utils/serializers.py | 21 +- apps/utils/views.py | 3 +- apps/utils/viewsets.py | 94 +++++-- apps/utils/xunxi.py | 22 +- apps/wf/views.py | 4 +- requirements.txt | 1 - server/settings.py | 7 +- 50 files changed, 1421 insertions(+), 181 deletions(-) create mode 100644 apps/hrm/__init__.py create mode 100644 apps/hrm/admin.py create mode 100644 apps/hrm/apps.py create mode 100644 apps/hrm/filters.py create mode 100644 apps/hrm/migrations/0001_initial.py create mode 100644 apps/hrm/migrations/0002_auto_20210924_1127.py create mode 100644 apps/hrm/migrations/0003_employee_face_data.py create mode 100644 apps/hrm/migrations/0004_clockrecord.py create mode 100644 apps/hrm/migrations/0005_auto_20220126_1351.py create mode 100644 apps/hrm/migrations/0006_auto_20220217_2155.py create mode 100644 apps/hrm/migrations/0007_auto_20220218_0843.py create mode 100644 apps/hrm/migrations/0008_auto_20220222_1112.py create mode 100644 apps/hrm/migrations/0009_employee_show_atwork.py create mode 100644 apps/hrm/migrations/__init__.py create mode 100644 apps/hrm/models.py create mode 100644 apps/hrm/serializers.py create mode 100644 apps/hrm/services.py create mode 100644 apps/hrm/signals.py create mode 100644 apps/hrm/tasks.py create mode 100644 apps/hrm/tests.py create mode 100644 apps/hrm/urls.py create mode 100644 apps/hrm/views.py create mode 100644 apps/monitor/errors.py delete mode 100644 apps/monitor/middleware.py create mode 100644 apps/monitor/migrations/0001_initial.py create mode 100644 apps/system/migrations/0002_auto_20220406_1458.py rename apps/utils/{vars.py => constants.py} (100%) create mode 100644 apps/utils/errors.py diff --git a/apps/auth1/errors.py b/apps/auth1/errors.py index a982796f..7d2f78f8 100644 --- a/apps/auth1/errors.py +++ b/apps/auth1/errors.py @@ -1,2 +1,2 @@ -NAME_OR_PASSWORD_WRONG = '账户名或密码错误' \ No newline at end of file +USERNAME_OR_PASSWORD_WRONG = {"code":"username_or_password_wrong", "detail":"账户名或密码错误"} \ No newline at end of file diff --git a/apps/auth1/views.py b/apps/auth1/views.py index 6a2c4bb7..0b70f055 100644 --- a/apps/auth1/views.py +++ b/apps/auth1/views.py @@ -7,6 +7,7 @@ from rest_framework import status from django.contrib.auth import authenticate, login, logout from rest_framework.generics import CreateAPIView from rest_framework.permissions import IsAuthenticated +from apps.auth1.errors import USERNAME_OR_PASSWORD_WRONG from apps.auth1.serializers import LoginSerializer @@ -45,7 +46,7 @@ class LoginView(CreateAPIView): if user is not None: login(request, user) return Response() - raise ParseError('账户名或密码错误', 'username_or_password_wrong') + raise ParseError(**USERNAME_OR_PASSWORD_WRONG) class LogoutView(APIView): authentication_classes = [] diff --git a/apps/hrm/__init__.py b/apps/hrm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/hrm/admin.py b/apps/hrm/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/hrm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/hrm/apps.py b/apps/hrm/apps.py new file mode 100644 index 00000000..852f1e7f --- /dev/null +++ b/apps/hrm/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + +class HrmConfig(AppConfig): + name = 'apps.hrm' + verbose_name = '人力资源管理' + + def ready(self): + import apps.hrm.signals + diff --git a/apps/hrm/filters.py b/apps/hrm/filters.py new file mode 100644 index 00000000..c377d119 --- /dev/null +++ b/apps/hrm/filters.py @@ -0,0 +1,37 @@ +from django_filters import rest_framework as filters +from apps.hrm.models import ClockRecord, Employee, NotWorkRemark + +class ClockRecordFilterSet(filters.FilterSet): + create_time_start = filters.DateFilter(field_name="create_time", lookup_expr='gte') + create_time_end = filters.DateFilter(field_name="create_time", lookup_expr='lte') + year = filters.NumberFilter(method='filter_year') + month = filters.NumberFilter(method='filter_month') + class Meta: + model = ClockRecord + fields = ['create_by', 'create_time_start', 'create_time_end', 'year', 'month'] + + def filter_year(self, queryset, name, value): + return queryset.filter(create_time_date__year=value) + + def filter_month(self, queryset, name, value): + return queryset.filter(create_time_date__month=value) + +class EmployeeFilterSet(filters.FilterSet): + + class Meta: + model = Employee + fields = ['job_state', 'show_atwork'] + + +class NotWorkRemarkFilterSet(filters.FilterSet): + year = filters.NumberFilter(method='filter_year') + month = filters.NumberFilter(method='filter_month') + class Meta: + model = NotWorkRemark + fields = ['year', 'month', 'user'] + + def filter_year(self, queryset, name, value): + return queryset.filter(not_work_date__year=value) + + def filter_month(self, queryset, name, value): + return queryset.filter(not_work_date__month=value) \ No newline at end of file diff --git a/apps/hrm/migrations/0001_initial.py b/apps/hrm/migrations/0001_initial.py new file mode 100644 index 00000000..496c0e6c --- /dev/null +++ b/apps/hrm/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.6 on 2021-08-13 09:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Employee', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('number', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='人员编号')), + ('photo', models.CharField(blank=True, max_length=1000, null=True, verbose_name='证件照')), + ('ID_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='身份证号')), + ('gender', models.CharField(default='男', max_length=10, verbose_name='性别')), + ('signature', models.CharField(blank=True, max_length=200, null=True, verbose_name='签名图片')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='employee_user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': '员工补充信息', + 'verbose_name_plural': '员工补充信息', + }, + ), + ] diff --git a/apps/hrm/migrations/0002_auto_20210924_1127.py b/apps/hrm/migrations/0002_auto_20210924_1127.py new file mode 100644 index 00000000..89485c70 --- /dev/null +++ b/apps/hrm/migrations/0002_auto_20210924_1127.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.6 on 2021-09-24 03:27 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0003_auto_20210812_0909'), + ('hrm', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='academic', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='学历'), + ), + migrations.AddField( + model_name='employee', + name='birthdate', + field=models.DateField(blank=True, null=True, verbose_name='出生年月'), + ), + migrations.AddField( + model_name='employee', + name='job', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.position', verbose_name='岗位'), + ), + migrations.AddField( + model_name='employee', + name='jobstate', + field=models.IntegerField(choices=[(1, '在职'), (2, '离职')], default=1, verbose_name='在职状态'), + ), + ] diff --git a/apps/hrm/migrations/0003_employee_face_data.py b/apps/hrm/migrations/0003_employee_face_data.py new file mode 100644 index 00000000..1823ad20 --- /dev/null +++ b/apps/hrm/migrations/0003_employee_face_data.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-10-18 05:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hrm', '0002_auto_20210924_1127'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='face_data', + field=models.JSONField(blank=True, null=True, verbose_name='人脸识别数据'), + ), + ] diff --git a/apps/hrm/migrations/0004_clockrecord.py b/apps/hrm/migrations/0004_clockrecord.py new file mode 100644 index 00000000..97ae8896 --- /dev/null +++ b/apps/hrm/migrations/0004_clockrecord.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.9 on 2022-01-21 06:45 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('hrm', '0003_employee_face_data'), + ] + + operations = [ + migrations.CreateModel( + name='ClockRecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('type', models.PositiveSmallIntegerField(choices=[(10, '上班打卡')], default=10, verbose_name='打卡类型')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clockrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clockrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/hrm/migrations/0005_auto_20220126_1351.py b/apps/hrm/migrations/0005_auto_20220126_1351.py new file mode 100644 index 00000000..d6bd2c30 --- /dev/null +++ b/apps/hrm/migrations/0005_auto_20220126_1351.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.9 on 2022-01-26 05:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hrm', '0004_clockrecord'), + ] + + operations = [ + migrations.RenameField( + model_name='employee', + old_name='birthdate', + new_name='birthday', + ), + migrations.RenameField( + model_name='employee', + old_name='ID_number', + new_name='id_number', + ), + migrations.RenameField( + model_name='employee', + old_name='jobstate', + new_name='job_state', + ), + migrations.RenameField( + model_name='employee', + old_name='academic', + new_name='qualification', + ), + migrations.RemoveField( + model_name='employee', + name='job', + ), + ] diff --git a/apps/hrm/migrations/0006_auto_20220217_2155.py b/apps/hrm/migrations/0006_auto_20220217_2155.py new file mode 100644 index 00000000..15ed4781 --- /dev/null +++ b/apps/hrm/migrations/0006_auto_20220217_2155.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.9 on 2022-02-17 13:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hrm', '0005_auto_20220126_1351'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='is_atwork', + field=models.BooleanField(default=False, verbose_name='当前在岗'), + ), + migrations.AddField( + model_name='employee', + name='last_check_time', + field=models.DateTimeField(blank=True, null=True, verbose_name='打卡时间'), + ), + ] diff --git a/apps/hrm/migrations/0007_auto_20220218_0843.py b/apps/hrm/migrations/0007_auto_20220218_0843.py new file mode 100644 index 00000000..36752f4a --- /dev/null +++ b/apps/hrm/migrations/0007_auto_20220218_0843.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.9 on 2022-02-18 00:43 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('hrm', '0006_auto_20220217_2155'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='not_work_remark', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='当前未打卡说明'), + ), + migrations.CreateModel( + name='NotWorkRemark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('year', models.PositiveSmallIntegerField(default=2022, verbose_name='年')), + ('month', models.PositiveSmallIntegerField(default=2, verbose_name='月')), + ('day', models.PositiveSmallIntegerField(default=1, verbose_name='日')), + ('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='未打卡说明')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notworkremark_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notworkremark_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/hrm/migrations/0008_auto_20220222_1112.py b/apps/hrm/migrations/0008_auto_20220222_1112.py new file mode 100644 index 00000000..406a088c --- /dev/null +++ b/apps/hrm/migrations/0008_auto_20220222_1112.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.9 on 2022-02-22 03:12 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('hrm', '0007_auto_20220218_0843'), + ] + + operations = [ + migrations.RemoveField( + model_name='notworkremark', + name='day', + ), + migrations.RemoveField( + model_name='notworkremark', + name='month', + ), + migrations.RemoveField( + model_name='notworkremark', + name='year', + ), + migrations.AddField( + model_name='notworkremark', + name='not_work_date', + field=models.DateField(default=django.utils.timezone.now, verbose_name='未打卡日期'), + preserve_default=False, + ), + ] diff --git a/apps/hrm/migrations/0009_employee_show_atwork.py b/apps/hrm/migrations/0009_employee_show_atwork.py new file mode 100644 index 00000000..83650ec0 --- /dev/null +++ b/apps/hrm/migrations/0009_employee_show_atwork.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.9 on 2022-03-17 03:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hrm', '0008_auto_20220222_1112'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='show_atwork', + field=models.BooleanField(default=True, verbose_name='是否展示在岗状态'), + ), + ] diff --git a/apps/hrm/migrations/__init__.py b/apps/hrm/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/hrm/models.py b/apps/hrm/models.py new file mode 100644 index 00000000..b9482fe6 --- /dev/null +++ b/apps/hrm/models.py @@ -0,0 +1,63 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.db.models.base import Model +import django.utils.timezone as timezone +from django.db.models.query import QuerySet +from apps.system.models import CommonADModel, CommonAModel, CommonBModel, Organization, User, Dict, File,Position +from utils.model import SoftModel, BaseModel +from simple_history.models import HistoricalRecords + + + + +class Employee(CommonAModel): + """ + 员工信息 + """ + JOB_ON = 1 + JOB_OFF = 2 + jobstate_choices = ( + (JOB_ON, '在职'), + (JOB_OFF, '离职'), + ) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='employee_user') + number = models.CharField('人员编号', max_length=50,null=True, blank=True, unique=True) + photo = models.CharField('证件照', max_length=1000, null=True, blank=True) + id_number = models.CharField('身份证号', max_length=100, null=True, blank=True) + gender = models.CharField('性别', max_length=10, default='男') + signature = models.CharField('签名图片', max_length=200, null=True, blank=True) + birthday = models.DateField('出生年月', null=True, blank=True) + qualification = models.CharField('学历', max_length=50, null=True, blank=True) + job_state = models.IntegerField('在职状态', choices=jobstate_choices, default=1) + face_data = models.JSONField('人脸识别数据', null=True, blank=True) + is_atwork = models.BooleanField('当前在岗', default=False) + show_atwork = models.BooleanField('是否展示在岗状态', default=True) + last_check_time = models.DateTimeField('打卡时间', null=True, blank=True) + not_work_remark = models.CharField('当前未打卡说明', null=True, blank=True, max_length=200) + + class Meta: + verbose_name = '员工补充信息' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + +class NotWorkRemark(CommonAModel): + """ + 离岗说明 + """ + not_work_date = models.DateField('未打卡日期') + user = models.ForeignKey(User, verbose_name='用户', on_delete=models.CASCADE) + remark = models.CharField('未打卡说明', null=True, blank=True, max_length=200) + +class ClockRecord(CommonADModel): + """ + 打卡记录 + """ + ClOCK_WORK1 = 10 + type_choice = ( + (ClOCK_WORK1, '上班打卡'), + ) + type = models.PositiveSmallIntegerField('打卡类型', choices=type_choice, default=ClOCK_WORK1) + + diff --git a/apps/hrm/serializers.py b/apps/hrm/serializers.py new file mode 100644 index 00000000..1ebc9237 --- /dev/null +++ b/apps/hrm/serializers.py @@ -0,0 +1,37 @@ +from apps.system.models import User +from rest_framework.serializers import ModelSerializer +from rest_framework import serializers + +from utils.mixins import DynamicFieldsSerializerMixin +from .models import ClockRecord, Employee, NotWorkRemark +from apps.system.serializers import OrganizationSimpleSerializer, UserSimpleSerializer + +class EmployeeSerializer(DynamicFieldsSerializerMixin, ModelSerializer): + name = serializers.CharField(source='user.name', read_only=True) + dept_ = OrganizationSimpleSerializer(source='user.dept', read_only=True) + class Meta: + model = Employee + exclude = ['face_data'] + +class EmployeeNotWorkRemarkSerializer(ModelSerializer): + class Meta: + model = Employee + fields = ['not_work_remark'] + +class FaceLoginSerializer(serializers.Serializer): + base64 = serializers.CharField() + + +class FaceClockCreateSerializer(serializers.Serializer): + base64 = serializers.CharField() + +class ClockRecordListSerializer(serializers.ModelSerializer): + create_by_ = UserSimpleSerializer(source='create_by', read_only=True) + class Meta: + model = ClockRecord + fields = '__all__' + +class NotWorkRemarkListSerializer(serializers.ModelSerializer): + class Meta: + model = NotWorkRemark + fields = '__all__' \ No newline at end of file diff --git a/apps/hrm/services.py b/apps/hrm/services.py new file mode 100644 index 00000000..eca4639e --- /dev/null +++ b/apps/hrm/services.py @@ -0,0 +1,50 @@ +from django.conf import settings +import uuid +import face_recognition +import os +from apps.hrm.models import Employee +from apps.hrm.tasks import update_all_user_facedata_cache +from apps.system.models import User +from django.core.cache import cache + +class HRMService: + + @classmethod + def face_compare_from_base64(cls, base64_data): + filename = str(uuid.uuid4()) + filepath = settings.BASE_DIR +'/temp/' + filename +'.png' + with open(filepath, 'wb') as f: + f.write(base64_data) + try: + unknown_picture = face_recognition.load_image_file(filepath) + unknown_face_encoding = face_recognition.face_encodings(unknown_picture, num_jitters=2)[0] + os.remove(filepath) + except: + os.remove(filepath) + return None, '识别失败,请调整位置' + + # 匹配人脸库 + face_datas = cache.get('face_datas') + if face_datas is None: + update_all_user_facedata_cache() + face_datas = cache.get('face_datas') + face_users = cache.get('face_users') + results = face_recognition.compare_faces(face_datas, + unknown_face_encoding, tolerance=0.45) + for index, value in enumerate(results): + if value: + # 识别成功 + user = User.objects.get(id=face_users[index]) + return user, '' + return None, '人脸未匹配,请调整位置' + + @classmethod + def get_facedata_from_img(cls, img_path): + try: + photo_path = settings.BASE_DIR + img_path + picture_of_me = face_recognition.load_image_file(photo_path) + my_face_encoding = face_recognition.face_encodings(picture_of_me, num_jitters=2)[0] + face_data_list = my_face_encoding.tolist() + return face_data_list, '' + except: + return None, '人脸数据获取失败请重新上传图片' \ No newline at end of file diff --git a/apps/hrm/signals.py b/apps/hrm/signals.py new file mode 100644 index 00000000..7e8c9227 --- /dev/null +++ b/apps/hrm/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_save +from apps.system.models import User +from django.dispatch import receiver +from apps.hrm.models import Employee +from django.conf import settings +import face_recognition +import logging +logger = logging.getLogger('log') + +@receiver(post_save, sender=User) +def createEmployee(sender, instance, created, **kwargs): + if created: + Employee.objects.get_or_create(user=instance) diff --git a/apps/hrm/tasks.py b/apps/hrm/tasks.py new file mode 100644 index 00000000..b365f58f --- /dev/null +++ b/apps/hrm/tasks.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import, unicode_literals + +from celery import shared_task +from apps.hrm.models import Employee +from django.core.cache import cache + + +@shared_task +def update_all_employee_not_atwork(): + """ + 将所有员工设为非在岗状态 + """ + Employee.objects.all().update(is_atwork=False, last_check_time = None, not_work_remark=None) + +@shared_task +def update_all_user_facedata_cache(): + """ + 更新人脸数据缓存 + """ + facedata_queyset = Employee.objects.filter(face_data__isnull=False, + user__is_active=True).values('user', 'face_data') + face_users = [] + face_datas = [] + for i in facedata_queyset: + face_users.append(i['user']) + face_datas.append(i['face_data']) + cache.set('face_users', face_users, timeout=None) + cache.set('face_datas', face_datas, timeout=None) + + \ No newline at end of file diff --git a/apps/hrm/tests.py b/apps/hrm/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/hrm/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/hrm/urls.py b/apps/hrm/urls.py new file mode 100644 index 00000000..b0ced0c8 --- /dev/null +++ b/apps/hrm/urls.py @@ -0,0 +1,15 @@ + +from rest_framework import urlpatterns +from apps.hrm.views import ClockRecordViewSet, EmployeeViewSet, FaceLogin, NotWorkRemarkViewSet +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() +router.register('employee', EmployeeViewSet, basename='employee') +router.register('clock_record', ClockRecordViewSet, basename='clock_record') +router.register('not_work_remark', NotWorkRemarkViewSet, basename='not_work_reamrk') +urlpatterns = [ + path('facelogin/', FaceLogin.as_view()), + path('', include(router.urls)), +] + diff --git a/apps/hrm/views.py b/apps/hrm/views.py new file mode 100644 index 00000000..bca0b796 --- /dev/null +++ b/apps/hrm/views.py @@ -0,0 +1,187 @@ +from django.shortcuts import render +from django.utils import timezone +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet, GenericViewSet +from rest_framework.mixins import UpdateModelMixin, RetrieveModelMixin, CreateModelMixin, ListModelMixin +from apps.hrm.filters import ClockRecordFilterSet, EmployeeFilterSet, NotWorkRemarkFilterSet +from apps.hrm.services import HRMService +from apps.hrm.tasks import update_all_user_facedata_cache +from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin +from apps.hrm.models import ClockRecord, Employee, NotWorkRemark +from apps.hrm.serializers import ClockRecordListSerializer, EmployeeNotWorkRemarkSerializer, EmployeeSerializer, FaceClockCreateSerializer, FaceLoginSerializer, NotWorkRemarkListSerializer + + + +from rest_framework.generics import CreateAPIView +from rest_framework import status +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework import exceptions +from apps.system.models import User +from apps.system.serializers import UserSimpleSerializer +from rest_framework.permissions import AllowAny +from rest_framework.decorators import action + + +# Create your views here. +class EmployeeViewSet(CreateUpdateModelAMixin, OptimizationMixin, UpdateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): + """ + 员工详细信息 + """ + perms_map = {'get': '*', 'put': 'employee_update'} + queryset = Employee.objects.all() + filterset_class = EmployeeFilterSet + serializer_class = EmployeeSerializer + search_fields = ['user__name', 'number', 'user__username'] + ordering = ['-pk'] + + def update(self, request, *args, **kwargs): + partial = kwargs.pop('partial', False) + instance = self.get_object() + data = request.data + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + photo = data.get('photo', None) + if instance.photo != photo: + f_l, msg = HRMService.get_facedata_from_img(img_path=photo) + if f_l: + serializer.save(update_by=request.user, face_data = f_l) + # 更新人脸缓存 + update_all_user_facedata_cache.delay() + return Response() + return Response(msg, status=status.HTTP_400_BAD_REQUEST) + serializer.save(update_by=request.user) + return Response() + + @action(methods=['post'], detail=True, perms_map={'post': 'employee_notworkremark'} + , serializer_class=EmployeeNotWorkRemarkSerializer) + def not_work_remark(self, request, pk=None): + """ + 填写离岗说明 + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + obj = self.get_object() + if not obj.is_atwork: + remark = request.data.get('not_work_remark', '') + obj.not_work_remark = remark + obj.save() + now = timezone.now() + instance, created = NotWorkRemark.objects.get_or_create( + not_work_date = now.date(), + user = obj.user, + defaults={ + "not_work_date":now.date(), + "user":obj.user, + "remark":remark, + "create_by":request.user, + } + ) + if not created: + instance.remark = remark + instance.update_by = request.user + instance.save() + return Response() + return Response('无需填写离岗说明', status=status.HTTP_400_BAD_REQUEST) + + +class ClockRecordViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): + """ + 打卡记录 + """ + perms_map = {'get':'*', 'post':'*'} + authentication_classes = [] + permission_classes = [AllowAny] + queryset = ClockRecord.objects.select_related('create_by').all() + serializer_class = ClockRecordListSerializer + filterset_class = ClockRecordFilterSet + ordering = ['-pk'] + + def get_serializer_class(self): + if self.action == 'create': + return FaceClockCreateSerializer + return super().get_serializer_class() + + def create(self, request, *args, **kwargs): + now = timezone.now() + now_local = timezone.localtime() + if 8<=now_local.hour<=17: + base64_data = base64.urlsafe_b64decode(tran64( + request.data.get('base64').replace(' ', '+'))) + user, msg = HRMService.face_compare_from_base64(base64_data) + if user: + ins, created = ClockRecord.objects.get_or_create( + create_by = user, create_time__hour__range = [8,18], + create_time__year=now_local.year, create_time__month=now_local.month, + create_time__day=now_local.day, + defaults={ + 'type':ClockRecord.ClOCK_WORK1, + 'create_by':user, + 'create_time':now + }) + if not created: + ins.update_time = now + ins.save() + # 设为在岗 + Employee.objects.filter(user=user).update(is_atwork=True, last_check_time=now) + return Response(UserSimpleSerializer(instance=user).data) + return Response(msg, status=status.HTTP_400_BAD_REQUEST) + return Response('非打卡时间范围', status=status.HTTP_400_BAD_REQUEST) + + +class NotWorkRemarkViewSet(ListModelMixin, GenericViewSet): + """ + 离岗说明 + """ + perms_map = {'get':'*'} + queryset = NotWorkRemark.objects.select_related('user').all() + serializer_class = NotWorkRemarkListSerializer + filterset_class = NotWorkRemarkFilterSet + ordering = ['-pk'] + + +import base64 + +def tran64(s): + missing_padding = len(s) % 4 + if missing_padding != 0: + s = s+'='* (4 - missing_padding) + return s + +class FaceLogin(CreateAPIView): + authentication_classes = [] + permission_classes = [] + serializer_class = FaceLoginSerializer + + + def create(self, request, *args, **kwargs): + """ + 人脸识别登录 + """ + base64_data = base64.urlsafe_b64decode(tran64(request.data.get('base64').replace(' ', '+'))) + user, msg = HRMService.face_compare_from_base64(base64_data) + if user: + refresh = RefreshToken.for_user(user) + # 可设为在岗 + now = timezone.now() + now_local = timezone.localtime() + if 8<=now_local.hour<=17: + ins, created = ClockRecord.objects.get_or_create( + create_by = user, create_time__hour__range = [8,18], + create_time__year=now_local.year, create_time__month=now_local.month, + create_time__day=now_local.day, + defaults={ + 'type':ClockRecord.ClOCK_WORK1, + 'create_by':user, + 'create_time':now + }) + # 设为在岗 + if created: + Employee.objects.filter(user=user).update(is_atwork=True, last_check_time=now) + + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'username':user.username, + 'name':user.name + }) + return Response(msg, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/apps/monitor/errors.py b/apps/monitor/errors.py new file mode 100644 index 00000000..49abfed6 --- /dev/null +++ b/apps/monitor/errors.py @@ -0,0 +1 @@ +LOG_NOT_FONED = {"code":"log_not_found", "detail":"日志不存在"} \ No newline at end of file diff --git a/apps/monitor/middleware.py b/apps/monitor/middleware.py deleted file mode 100644 index 038a432f..00000000 --- a/apps/monitor/middleware.py +++ /dev/null @@ -1 +0,0 @@ -from django.utils.deprecation import MiddlewareMixin diff --git a/apps/monitor/migrations/0001_initial.py b/apps/monitor/migrations/0001_initial.py new file mode 100644 index 00000000..628e5cde --- /dev/null +++ b/apps/monitor/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.12 on 2022-04-08 00:58 + +import apps.utils.snowflake +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DrfRequestLog', + fields=[ + ('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('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=6, 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)), + ('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/apps/monitor/models.py b/apps/monitor/models.py index 71a83623..19f92601 100644 --- a/apps/monitor/models.py +++ b/apps/monitor/models.py @@ -1,3 +1,47 @@ from django.db import models -# Create your models here. +from apps.utils.models import BaseModel + +class DrfRequestLog(BaseModel): + """Logs Django rest framework API requests""" + + 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=6, + null=True, + blank=True, + db_index=True, + ) + remote_addr = models.GenericIPAddressField() + host = models.URLField() + method = models.CharField(max_length=10) + query_params = models.TextField(null=True, blank=True) + data = models.TextField(null=True, blank=True) + response = models.TextField(null=True, blank=True) + errors = 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/apps/monitor/urls.py b/apps/monitor/urls.py index aa934695..fbe38d2f 100644 --- a/apps/monitor/urls.py +++ b/apps/monitor/urls.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework import routers -from .views import ServerInfoView, LogView, LogDetailView, index, room, video +from .views import DrfRequestLogViewSet, ServerInfoView, LogView, LogDetailView, index, room, video API_BASE_URL = 'api/monitor/' HTML_BASE_URL = 'monitor/' @@ -13,4 +13,5 @@ urlpatterns = [ path(API_BASE_URL + 'log/', LogView.as_view()), path(API_BASE_URL + 'log//', LogDetailView.as_view()), path(API_BASE_URL + 'server/', ServerInfoView.as_view()), + path(API_BASE_URL + 'request_log/', DrfRequestLogViewSet.as_view({'get':'list'}), name='requestlog_view') ] diff --git a/apps/monitor/views.py b/apps/monitor/views.py index adaa2ddf..5cfd95ab 100644 --- a/apps/monitor/views.py +++ b/apps/monitor/views.py @@ -11,6 +11,11 @@ from rest_framework import serializers, status 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.monitor.models import DrfRequestLog + +from apps.monitor.errors import LOG_NOT_FONED +from apps.utils.viewsets import CustomGenericViewSet # Create your views here. @@ -117,5 +122,21 @@ class LogDetailView(APIView): data = f.read() return Response(data) except: - raise NotFound('不存在该日志') + raise NotFound(**LOG_NOT_FONED) + +class DrfRequestLogSerializer(serializers.ModelSerializer): + class Meta: + model = DrfRequestLog + fields = '__all__' + +class DrfRequestLogViewSet(ListModelMixin, CustomGenericViewSet): + """请求日志 + + 请求日志 + """ + perms_map = {'get':'requestlog_view'} + queryset = DrfRequestLog.objects.all() + list_serializer_class = DrfRequestLogSerializer + ordering = ['-requested_at'] + diff --git a/apps/system/errors.py b/apps/system/errors.py index cb18dd06..49d4ea3c 100644 --- a/apps/system/errors.py +++ b/apps/system/errors.py @@ -1,2 +1,6 @@ -# 自定义的错误码 -from rest_framework.exceptions import ValidationError \ No newline at end of file +SCHEDULE_WRONG = {"code":"schedule_wrong", "detail":"时间策略有误"} +PASSWORD_NOT_SAME = {"code":"password_not_same", "detail":"新旧密码不一致"} +OLD_PASSWORD_WRONG = {"code":"old_password_wrong", "detail":"旧密码错误"} +PHONE_F_WRONG = {"code":"phone_f_wrong", "detail":"手机号格式错误"} +PHONE_EXIST = {"code":"phone_exist", "detail":"手机号已存在"} +USERNAME_EXIST = {"code":"username_exist", "detail":"账户已存在"} \ No newline at end of file diff --git a/apps/system/migrations/0002_auto_20220406_1458.py b/apps/system/migrations/0002_auto_20220406_1458.py new file mode 100644 index 00000000..dc1125be --- /dev/null +++ b/apps/system/migrations/0002_auto_20220406_1458.py @@ -0,0 +1,64 @@ +# Generated by Django 3.2.12 on 2022-04-06 06:58 + +import apps.utils.snowflake +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='dept', + name='third_info', + field=models.JSONField(default=dict, verbose_name='三方系统信息'), + ), + migrations.AlterField( + model_name='dept', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='dict', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='dicttype', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='file', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='permission', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='post', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='role', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + migrations.AlterField( + model_name='userpost', + name='id', + field=models.CharField(default=apps.utils.snowflake.IdWorker.get_id, editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID'), + ), + ] diff --git a/apps/system/models.py b/apps/system/models.py index ed991d71..12950e37 100644 --- a/apps/system/models.py +++ b/apps/system/models.py @@ -46,6 +46,7 @@ class Dept(CommonAModel): parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父') sort = models.PositiveSmallIntegerField('排序标记', default=1) + third_info = models.JSONField('三方系统信息', default=dict) class Meta: verbose_name = '部门' diff --git a/apps/system/serializers.py b/apps/system/serializers.py index 80d14bc1..0a0b018e 100644 --- a/apps/system/serializers.py +++ b/apps/system/serializers.py @@ -2,11 +2,13 @@ import re from django_celery_beat.models import PeriodicTask, CrontabSchedule, IntervalSchedule from rest_framework import serializers from django_celery_results.models import TaskResult +from apps.system.errors import PHONE_EXIST, PHONE_F_WRONG, USERNAME_EXIST from apps.utils.serializers import CustomModelSerializer -from apps.utils.vars import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE +from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE from .models import (Dict, DictType, File, Dept, Permission, Post, Role, User, UserPost) - +from rest_framework.exceptions import ParseError, APIException +from django.db import transaction class IntervalSerializer(CustomModelSerializer): class Meta: @@ -36,12 +38,6 @@ class PTaskSerializer(CustomModelSerializer): model = PeriodicTask fields = '__all__' - @staticmethod - def setup_eager_loading(queryset): - """ Perform necessary eager loading of data. """ - queryset = queryset.select_related('interval', 'crontab') - return queryset - def get_schedule(self, obj): if obj.interval: return obj.interval.__str__() @@ -188,7 +184,48 @@ class DeptCreateUpdateSerializer(CustomModelSerializer): class Meta: model = Dept - exclude = EXCLUDE_FIELDS + exclude = EXCLUDE_FIELDS + ['third_info'] + + @transaction.atomic + def create(self, validated_data): + from apps.utils.dahua import dhClient + if dhClient: + data = { + "parentId":1, + "name":validated_data['name'], + "service":"ehs" + } + ok, res = dhClient.request('/evo-apigw/evo-brm/1.0.0/department/add', + 'post',json=data) + if ok == 'success': + third_info = {'dh_id':str(res['id'])} + validated_data['third_info'] = third_info + elif ok == 'fail': + raise ParseError(**res) + else: + raise APIException(**res) + return super().create(validated_data) + + + @transaction.atomic + def update(self, instance, validated_data): + if instance.name != validated_data.get('name', ''): + from apps.utils.dahua import dhClient + if dhClient and instance.third_info.get('dh_id', False): + data = { + "id":instance.third_info['dh_id'], + "parentId":1, + "name":validated_data['name'] + } + ok, res = dhClient.request('/evo-apigw/evo-brm/1.0.0/department/update', + 'put',json=data) + if ok == 'success': + pass + elif ok == 'fail': + raise ParseError(**res) + else: + raise APIException(**res) + return super().update(instance, validated_data) class UserSimpleSerializer(CustomModelSerializer): class Meta: @@ -214,50 +251,54 @@ class UserListSerializer(CustomModelSerializer): queryset = queryset.prefetch_related('posts') return queryset +def phone_check(phone): + re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$' + if not re.match(re_phone, phone): + raise serializers.ValidationError(**PHONE_F_WRONG) + return phone + +def phone_exist(phone): + if User.objects.filter(phone=phone): + raise serializers.ValidationError(**PHONE_EXIST) + +def user_exist(username): + if User.objects.filter(username=username): + raise serializers.ValidationError(**USERNAME_EXIST) + return username class UserUpdateSerializer(CustomModelSerializer): """ 用户编辑序列化 """ - phone = serializers.CharField(max_length=11, required=False) + phone = serializers.CharField(max_length=11, + required=False, validators=[phone_check]) class Meta: model = User fields = ['id', 'username', 'name', 'phone', 'email', 'belong_dept', 'avatar', 'is_active', 'is_superuser'] - def validate_phone(self, phone): - re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$' - if not re.match(re_phone, phone): - raise serializers.ValidationError('手机号码不合法') - return phone + class UserCreateSerializer(CustomModelSerializer): """ 创建用户序列化 """ - username = serializers.CharField(required=True) - phone = serializers.CharField(max_length=11, required=False) + username = serializers.CharField(required=True, validators=[user_exist]) + phone = serializers.CharField(max_length=11, + required=False, validators=[phone_check, phone_exist]) class Meta: model = User fields = ['id', 'username', 'name', 'phone', 'email', 'belong_dept', 'avatar', 'is_active'] - def validate_username(self, username): - if User.objects.filter(username=username): - raise serializers.ValidationError(username + ' 账号已存在') - return username - - def validate_phone(self, phone): - re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$' - if not re.match(re_phone, phone): - raise serializers.ValidationError('手机号码不合法') - if User.objects.filter(phone=phone): - raise serializers.ValidationError('手机号已经被注册') - return phone +class PasswordChangeSerializer(serializers.Serializer): + old_password = serializers.CharField(label="原密码") + new_password1 = serializers.CharField(label="新密码1") + new_password2 = serializers.CharField(label="新密码2") class PTaskResultSerializer(CustomModelSerializer): class Meta: diff --git a/apps/system/views.py b/apps/system/views.py index fc55e539..2611dd40 100644 --- a/apps/system/views.py +++ b/apps/system/views.py @@ -12,10 +12,11 @@ from rest_framework.parsers import (JSONParser, from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG from apps.utils.mixins import (CustomCreateModelMixin) from django.conf import settings -from apps.utils.permission import get_user_perms_map +from apps.utils.permission import ALL_PERMS, get_user_perms_map from apps.utils.queryset import get_child_queryset2 from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from server.celery import app as celery_app @@ -23,7 +24,7 @@ from .filters import UserFilter from .models import (Dept, Dict, DictType, File, Permission, Post, Role, User, UserPost) from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer, DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer, - FileSerializer, PermissionCreateUpdateSerializer, PermissionSerializer, PostCreateUpdateSerializer, PostSerializer, + FileSerializer, PasswordChangeSerializer, PermissionCreateUpdateSerializer, PermissionSerializer, PostCreateUpdateSerializer, PostSerializer, PTaskCreateUpdateSerializer, PTaskResultSerializer, PTaskSerializer, RoleCreateUpdateSerializer, RoleSerializer, UserCreateSerializer, UserListSerializer, UserPostCreateSerializer, @@ -62,8 +63,10 @@ class PTaskViewSet(CustomModelViewSet): serializer_class = PTaskSerializer create_serializer_class = PTaskCreateUpdateSerializer update_serializer_class = PTaskCreateUpdateSerializer + partial_update_serializer_class = PTaskCreateUpdateSerializer search_fields = ['name', 'task'] filterset_fields = ['enabled'] + select_related_fields = ['interval', 'crontab'] ordering = ['-create_time'] @action(methods=['put'], detail=True, perms_map={'put': 'ptask_update'}) @@ -94,7 +97,7 @@ class PTaskViewSet(CustomModelViewSet): **interval_, defaults=interval_) data['interval'] = interval.id except: - raise ParseError('时间策略有误', 'schedule_wrong') + raise ParseError(**SCHEDULE_WRONG) if timetype == 'crontab' and crontab_: data['interval'] = None try: @@ -103,7 +106,7 @@ class PTaskViewSet(CustomModelViewSet): **crontab_, defaults=crontab_) data['crontab'] = crontab.id except: - raise ParseError('时间策略有误', 'schedule_wrong') + raise ParseError(**SCHEDULE_WRONG) serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) serializer.save() @@ -128,7 +131,7 @@ class PTaskViewSet(CustomModelViewSet): **interval_, defaults=interval_) data['interval'] = interval.id except: - raise ParseError('时间策略有误', 'schedule_wrong') + raise ParseError(**SCHEDULE_WRONG) if timetype == 'crontab' and crontab_: data['interval'] = None try: @@ -139,7 +142,7 @@ class PTaskViewSet(CustomModelViewSet): **crontab_, defaults=crontab_) data['crontab'] = crontab.id except: - raise ParseError('时间策略有误', 'schedule_wrong') + raise ParseError(**SCHEDULE_WRONG) instance = self.get_object() serializer = self.get_serializer(instance, data=data) serializer.is_valid(raise_exception=True) @@ -173,6 +176,7 @@ class DictTypeViewSet(CustomModelViewSet): serializer_class = DictTypeSerializer create_serializer_class = DictTypeCreateUpdateSerializer update_serializer_class = DictTypeCreateUpdateSerializer + partial_update_serializer_class = DictTypeCreateUpdateSerializer search_fields = ['name'] @@ -187,6 +191,7 @@ class DictViewSet(CustomModelViewSet): serializer_class = DictSerializer create_serializer_class = DictCreateUpdateSerializer update_serializer_class = DictCreateUpdateSerializer + partial_update_serializer_class = DictCreateUpdateSerializer search_fields = ['name'] @@ -199,6 +204,7 @@ class PostViewSet(CustomModelViewSet): serializer_class = PostSerializer create_serializer_class = PostCreateUpdateSerializer update_serializer_class = PostCreateUpdateSerializer + partial_update_serializer_class = PostCreateUpdateSerializer search_fields = ['name', 'description'] @@ -212,8 +218,18 @@ class PermissionViewSet(CustomModelViewSet): serializer_class = PermissionSerializer create_serializer_class = PermissionCreateUpdateSerializer update_serializer_class = PermissionCreateUpdateSerializer + partial_update_serializer_class = PermissionCreateUpdateSerializer search_fields = ['name', 'code'] + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) + def codes(self, request, pk=None): + """获取全部权限标识 + + 需要先请求一次swagger + """ + ALL_PERMS.sort() + return Response(ALL_PERMS) + class DeptViewSet(CustomModelViewSet): """部门-增删改查 @@ -224,6 +240,7 @@ class DeptViewSet(CustomModelViewSet): serializer_class = DeptSerializer create_serializer_class = DeptCreateUpdateSerializer update_serializer_class = DeptCreateUpdateSerializer + partial_update_serializer_class = DeptCreateUpdateSerializer filterset_fields = ['type'] search_fields = ['name'] @@ -262,7 +279,7 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo def perform_destroy(self, instance): user = instance.user - instance.delete() + instance.delete(update_by = self.request.user) fdept = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first() if fdept: user.belong_dept = fdept @@ -279,18 +296,8 @@ class UserViewSet(CustomModelViewSet): update_serializer_class = UserUpdateSerializer filterset_class = UserFilter search_fields = ['username', 'name', 'phone', 'email'] - - def get_queryset(self): - queryset = self.queryset - if hasattr(self.get_serializer_class(), 'setup_eager_loading'): - queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化 - dept = self.request.query_params.get( - 'belong_dept', None) # 该部门及其子部门所有员工 - if dept: - dept_queryset = get_child_queryset2( - Dept.objects.get(pk=dept)) - queryset = queryset.filter(dept__in=dept_queryset) - return queryset + select_related_fields = ['superior', 'belong_dept'] + prefetch_related_fields = ['posts'] def create(self, request, *args, **kwargs): """创建用户 @@ -307,7 +314,9 @@ class UserViewSet(CustomModelViewSet): serializer.save(password=password) return Response(data=serializer.data) - @action(methods=['put'], detail=False, permission_classes=[IsAuthenticated]) + @action(methods=['put'], detail=False, + permission_classes=[IsAuthenticated], + serializer_class = PasswordChangeSerializer) def password(self, request, pk=None): """修改密码 @@ -323,9 +332,9 @@ class UserViewSet(CustomModelViewSet): user.save() return Response() else: - raise ParseError('新密码两次输入不一致!', 'password_not_same') + raise ParseError(**PASSWORD_NOT_SAME) else: - raise ValidationError('旧密码错误!', 'old_password_wrong') + raise ValidationError(**OLD_PASSWORD_WRONG) @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated]) def info(self, request, pk=None): diff --git a/apps/third/views.py b/apps/third/views.py index 901960f8..741f56a6 100644 --- a/apps/third/views.py +++ b/apps/third/views.py @@ -1,4 +1,6 @@ +from rest_framework.exceptions import ParseError, APIException from apps.utils.dahua import dhClient +from apps.utils.errors import XX_REQUEST_ERROR from apps.utils.xunxi import xxClient from rest_framework.response import Response from rest_framework.views import APIView @@ -24,8 +26,10 @@ class DahuaTestView(APIView): "type": "hls" } } - res = dhClient.request( - url='/evo-apigw/admin/API/video/stream/realtime', method='post', json=data) + # ok, res = dhClient.request( + # url='/evo-apigw/admin/API/video/stream/realtime', method='post', json=data) + ok, res = dhClient.request(url='/evo-apigw/evo-brm/1.2.0/department/tree', + method='get') # data = { # "pageNum":1, # "pageSize":100, @@ -41,7 +45,12 @@ class DahuaTestView(APIView): # } # res = dhClient.request( # url='/evo-apigw/evo-accesscontrol/1.0.0/card/accessControl/channelControl/closeDoor', method='post', json=data) - return Response(res) + if ok == 'success': + return Response(res) + elif ok == 'fail': + raise ParseError(**res) + else: + raise APIException(**res) class XxTestView(APIView): @@ -51,9 +60,14 @@ class XxTestView(APIView): permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): - res = xxClient.request( + ok, res = xxClient.request( url='/api/application/build/buildListV2', json={}) - return Response(res) + if ok == 'success': + return Response(res) + elif ok == 'fail': + raise ParseError(**res) + else: + raise APIException(**res) class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet): @@ -67,12 +81,17 @@ class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) vdata = serializer.validated_data - res = xxClient.request( + ok, res = xxClient.request( url=vdata['url'], method=vdata.get('method', 'post'), params=vdata.get('params', {}), json=vdata.get('data', {})) - return Response(res) + if ok == 'success': + return Response(res) + elif ok == 'fail': + raise ParseError(**res) + else: + raise APIException(**res) class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet): @@ -86,9 +105,14 @@ class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) vdata = serializer.validated_data - res = dhClient.request( + ok, res = dhClient.request( url=vdata['url'], method=vdata.get('method', 'post'), params=vdata.get('params', {}), json=vdata.get('data', {})) - return Response(res) + if ok == 'success': + return Response(res) + elif ok == 'fail': + raise ParseError(**res) + else: + raise APIException(**res) diff --git a/apps/utils/vars.py b/apps/utils/constants.py similarity index 100% rename from apps/utils/vars.py rename to apps/utils/constants.py diff --git a/apps/utils/dahua.py b/apps/utils/dahua.py index fb554342..cd3b4e2f 100644 --- a/apps/utils/dahua.py +++ b/apps/utils/dahua.py @@ -1,10 +1,12 @@ from threading import Thread import traceback import requests +from apps.utils.errors import DH_REQUEST_ERROR from apps.utils.tools import print_roundtrip from server import settings import json import time +from rest_framework.exceptions import APIException, ParseError requests.packages.urllib3.disable_warnings() class DhClient: @@ -14,6 +16,8 @@ class DhClient: def __init__(self, client_id= settings.DAHUA_CLIENTID , client_secret = settings.DAHUA_SECRET) -> None: + if not settings.DAHUA_ENABLED: + return None self.client_id = client_id self.client_secret = client_secret self.headers = {} @@ -74,8 +78,8 @@ class DhClient: else: r = getattr(requests, method)('{}{}'.format(settings.DAHUA_BASE_URL, url) , headers = self.headers, params=params, json=json, verify=False) - if settings.DEBUG: - print_roundtrip(r) + # if settings.DEBUG: + # print_roundtrip(r) if r.status_code == 200: """ 请求成功 @@ -85,15 +89,10 @@ class DhClient: self.get_token() # 重新获取token self.request(url, method, params, json, timeout) # 重新请求 else: - - msg = '{}|{}{}'.format(str(ret['code']), ret.get('errMsg',''), ret.get('desc', '')) + if ret['code'] not in ['0', '100', '00000', '1000', 0, 100, 1000]: + detail = '{}|{}{}'.format(str(ret['code']), ret.get('errMsg',''), ret.get('desc', '')) + return 'fail', dict(detail=detail, code='dh_'+str(ret['code'])) + return 'success', ret['data'] + return 'error', DH_REQUEST_ERROR - res = dict(success=True, code=200000, msg= msg, data=ret.get('data', None)) - if ret['code'] not in ['0', '100', '00000']: - res['success'] = False - res['code'] = 400000 - return res - return dict(success=False, code=400901, msg='大华接口访问异常', data=None) - -if settings.DAHUA_ENABLED: - dhClient = DhClient() \ No newline at end of file +dhClient = DhClient() \ No newline at end of file diff --git a/apps/utils/errors.py b/apps/utils/errors.py new file mode 100644 index 00000000..cb160605 --- /dev/null +++ b/apps/utils/errors.py @@ -0,0 +1,4 @@ +XX_REQUEST_ERROR = {"code":"xx_request_error", "detail":"寻息接口访问异常"} +DH_REQUEST_ERROR = {"code":"dh_request_error", "detail":"大华接口访问异常"} +SIGN_MAKE_FAIL = {"code":"sign_make_fail", "detail":"签名照生成失败,请重新上传"} +PKS_ERROR = {"code":"pks_error", "detail":"未获取到主键列表"} \ No newline at end of file diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index e05aabd8..07157600 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -1,21 +1,9 @@ -from typing import Tuple from django.core.exceptions import PermissionDenied from django.http import Http404 -from server.settings import myLogger from rest_framework.response import Response from rest_framework import exceptions from rest_framework.views import set_rollback from django.utils.translation import gettext_lazy as _ -import traceback - -class MyError(exceptions.ParseError): - """自定义业务异常 - """ - - def __init__(self, error:Tuple[str, str]=(), detail=None, code=None): - if error: - code, detail = error - super().__init__(detail, code) def custom_exception_hander(exc, context): """ @@ -26,6 +14,7 @@ def custom_exception_hander(exc, context): elif isinstance(exc, PermissionDenied): exc = exceptions.PermissionDenied() + request_id = getattr(context['request'], 'request_id', None) if isinstance(exc, exceptions.APIException): headers = {} if getattr(exc, 'auth_header', None): @@ -41,7 +30,7 @@ def custom_exception_hander(exc, context): data = {'err_msg': exc.detail, 'err_code':exc.get_codes()} set_rollback() + data['request_id'] = request_id return Response(data, status=exc.status_code, headers=headers) - # 未处理的异常记录日志 - myLogger.error(traceback.format_exc()) - return Response(data={'err_code':'server_error', 'err_msg':'服务器错误'}, status=500) \ No newline at end of file + + return Response(data={'err_code':'server_error', 'err_msg':'服务器错误', 'request_id': request_id}, status=500) \ No newline at end of file diff --git a/apps/utils/mixins.py b/apps/utils/mixins.py index f17391a8..75173c64 100644 --- a/apps/utils/mixins.py +++ b/apps/utils/mixins.py @@ -1,5 +1,15 @@ +import uuid from django.db.models.query import QuerySet from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, ListModelMixin, RetrieveModelMixin, DestroyModelMixin +import ast +import ipaddress +import traceback +from apps.monitor.models import DrfRequestLog +from server.settings import myLogger +from django.db import connection +from django.utils.timezone import now +from apps.utils.snowflake import idWorker + class CreateUpdateModelAMixin: """ 业务用基本表A用 @@ -32,19 +42,6 @@ class CreateUpdateCustomMixin: def perform_update(self, serializer): serializer.save(update_by = self.request.user) -class OptimizationMixin: - """ - 性能优化,需要在序列化器里定义setup_eager_loading,可在必要的View下继承 - """ - def get_queryset(self): - queryset = self.queryset - if isinstance(queryset, QuerySet): - # Ensure queryset is re-evaluated on each request. - queryset = queryset.all() - if hasattr(self.get_serializer_class(), 'setup_eager_loading'): - queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化 - return queryset - class CustomCreateModelMixin(CreateModelMixin): def perform_create(self, serializer): @@ -63,3 +60,217 @@ class CustomDestoryModelMixin(DestroyModelMixin): def perform_destroy(self, instance): instance.delete(update_by = self.request.user) + +class MyLoggingMixin(object): + """Mixin to log requests""" + + CLEANED_SUBSTITUTE = "********************" + + # logging_methods = "__all__" + logging_methods = ['POST', 'PUT', 'DELETE', 'PATCH'] + 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 = idWorker.get_id() + 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) + err_str = traceback.format_exc() + self.log["errors"] = err_str + myLogger.error('{}-{}'.format(self.log['request_id'], err_str)) + 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, + } + ) + 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_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 request.method in self.logging_methods + ) + + 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") + + 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", + } + + 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 + diff --git a/apps/utils/models.py b/apps/utils/models.py index 6e9b9811..bcb0eb09 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -1,7 +1,8 @@ +from copy import copy import django.utils.timezone as timezone from django.db import models from django.db.models.query import QuerySet - +from django.core.exceptions import ObjectDoesNotExist from apps.utils.snowflake import idWorker @@ -56,7 +57,8 @@ class BaseModel(models.Model): """ 基本表 """ - id = models.CharField(max_length=20, primary_key=True, default=idWorker.get_id, editable=False, verbose_name='主键ID', help_text='主键ID') + id = models.CharField(max_length=20, primary_key=True, default=idWorker.get_id, + editable=False, verbose_name='主键ID', help_text='主键ID') create_time = models.DateTimeField( default=timezone.now, verbose_name='创建时间', help_text='创建时间') update_time = models.DateTimeField( @@ -67,25 +69,7 @@ class BaseModel(models.Model): class Meta: abstract = True - def save(self, *args, **kwargs): - if self.pk: - # If self.pk is not None then it's an update. - cls = self.__class__ - old = cls.objects.filter(pk=self.pk).first() - if old: - # This will get the current model state since super().save() isn't called yet. - new = self # This gets the newly instantiated Mode object with the new values. - changed_fields = [] - for field in cls._meta.get_fields(): - field_name = field.name - try: - if getattr(old, field_name) != getattr(new, field_name): - changed_fields.append(field_name) - except Exception as ex: # Catch field does not exist exception - pass - kwargs['update_fields'] = changed_fields - super().save(*args, **kwargs) - + class SoftModel(BaseModel): """ 软删除基本表 @@ -113,51 +97,52 @@ class CommonAModel(SoftModel): 业务用基本表A,包含create_by, update_by字段 """ create_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name= '%(class)s_create_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name='%(class)s_create_by') update_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name= '%(class)s_update_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name='%(class)s_update_by') class Meta: abstract = True - class CommonBModel(SoftModel): """ 业务用基本表B,包含create_by, update_by, belong_dept字段 """ create_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name = '%(class)s_create_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name='%(class)s_create_by') update_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name = '%(class)s_update_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name='%(class)s_update_by') belong_dept = models.ForeignKey( - 'system.dept', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name= '%(class)s_belong_dept') + 'system.dept', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name='%(class)s_belong_dept') class Meta: abstract = True + class CommonADModel(BaseModel): """ 业务用基本表A, 物理删除, 包含create_by, update_by字段 """ create_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name= '%(class)s_create_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name='%(class)s_create_by') update_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name= '%(class)s_update_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name='%(class)s_update_by') class Meta: abstract = True + class CommonBDModel(BaseModel): """ 业务用基本表B, 物理删除, 包含create_by, update_by, belong_dept字段 """ create_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name = '%(class)s_create_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name='%(class)s_create_by') update_by = models.ForeignKey( - 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name = '%(class)s_update_by') + 'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name='%(class)s_update_by') belong_dept = models.ForeignKey( - 'system.organzation', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name= '%(class)s_belong_dept') + 'system.organzation', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name='%(class)s_belong_dept') class Meta: abstract = True diff --git a/apps/utils/permission.py b/apps/utils/permission.py index 6b0c8f90..7ce672be 100644 --- a/apps/utils/permission.py +++ b/apps/utils/permission.py @@ -5,6 +5,9 @@ from apps.system.models import Dept, Permission, Post, Role, UserPost from django.db.models import Q from django.db.models.query import QuerySet +ALL_PERMS = [ + +] def get_user_perms_map(user): """ diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 5b397521..b65afb92 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -1,7 +1,8 @@ from rest_framework import serializers from django_restql.mixins import DynamicFieldsMixin - +from rest_framework.fields import empty +from rest_framework.request import Request class PkSerializer(serializers.Serializer): pks = serializers.ListField(child=serializers.CharField(max_length=20), label="主键ID列表") @@ -9,5 +10,21 @@ class GenSignatureSerializer(serializers.Serializer): path = serializers.CharField(label="图片地址") class CustomModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer): - pass + + def __init__(self, instance=None, data=empty, request=None, **kwargs): + super().__init__(instance, data, **kwargs) + self.request: Request = request or self.context.get('request', None) + def create(self, validated_data): + if self.request: + if getattr(self.request, 'user', None): + validated_data['create_by'] = self.request.user + if getattr(self.request.user, 'belong_dept', None): + validated_data['belong_dept'] = self.request.user.belong_dept + return super().update(validated_data) + + def update(self, instance, validated_data): + if self.request: + if hasattr(instance, 'update_by'): + validated_data['update_by'] = getattr(self.request, 'user', None) + return super().update(instance, validated_data) diff --git a/apps/utils/views.py b/apps/utils/views.py index 945524a5..1739977b 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -1,6 +1,7 @@ from rest_framework.views import APIView import os import cv2 +from apps.utils.errors import SIGN_MAKE_FAIL from server.settings import BASE_DIR import numpy as np from rest_framework.response import Response @@ -45,4 +46,4 @@ class SignatureViewSet(CustomCreateModelMixin, CustomGenericViewSet): cv2.imwrite(new_path, image) return Response({'path': new_path.replace(BASE_DIR, '')}) except: - raise ParseError('签名照处理失败,请重新上传') + raise ParseError(**SIGN_MAKE_FAIL) diff --git a/apps/utils/viewsets.py b/apps/utils/viewsets.py index 1792bf47..73caa134 100644 --- a/apps/utils/viewsets.py +++ b/apps/utils/viewsets.py @@ -1,29 +1,38 @@ -from rest_framework.viewsets import ModelViewSet, GenericViewSet +from rest_framework.viewsets import GenericViewSet from rest_framework.decorators import action -from apps.utils.mixins import CustomCreateModelMixin, CustomDestoryModelMixin, CustomUpdateModelMixin, OptimizationMixin -from apps.utils.permission import RbacDataMixin, RbacPermission +from apps.system.models import Dept, Post +from apps.utils.errors import PKS_ERROR +from apps.utils.mixins import CustomDestoryModelMixin, MyLoggingMixin +from apps.utils.permission import ALL_PERMS, RbacPermission, get_user_perms_map +from apps.utils.queryset import get_child_queryset2 from apps.utils.serializers import PkSerializer from rest_framework.response import Response -from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, ListModelMixin +from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, CreateModelMixin, UpdateModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.exceptions import ValidationError +from django.core.cache import cache -class CustomGenericViewSet(GenericViewSet): +class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): """ 增强的GenericViewSet """ - perms_map = {} + perms_map = {} # 权限标识 + logging_methods = ['POST', 'PUT', 'PATCH', 'DELETE'] ordering_fields = '__all__' filter_fields = '__all__' ordering = '-create_time' filterset_fields = '__all__' create_serializer_class = None update_serializer_class = None + partial_update_serializer_class = None list_serializer_class = None retrieve_serializer_class = None + select_related_fields = [] + prefetch_related_fields = [] permission_classes = [IsAuthenticated & RbacPermission] + data_filter = False # 数据权限过滤是否开启(需要RbacPermission) def get_serializer_class(self): action_serializer_name = f"{self.action}_serializer_class" @@ -32,13 +41,62 @@ class CustomGenericViewSet(GenericViewSet): return action_serializer_class return super().get_serializer_class() -class CustomDataGenericViewSet(RbacDataMixin, CustomGenericViewSet): - """ - 增强的GenericViewSet, 带数据权限过滤 - """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.perms_map: + for k, v in self.perms_map.items(): + if v not in ALL_PERMS and v!='*': + ALL_PERMS.append(v) + + def get_queryset(self): + queryset = super().get_queryset() + if self.select_related_fields: + queryset = queryset.select_related(*self.select_related_fields) + if self.prefetch_related_fields: + queryset = queryset.prefetch_related(*self.prefetch_related_fields) + if self.data_filter: + if self.request.user.is_superuser: + return queryset + if hasattr(queryset.model, 'belong_dept'): + user = self.request.user + user_perms_map = cache.get('perms_' + user.id, None) + if user_perms_map is None: + user_perms_map = get_user_perms_map(self.request.user) + if isinstance(user_perms_map, dict): + if hasattr(self.view, 'perms_map'): + perms_map = self.view.perms_map + action_str = perms_map.get(self.request._request.method.lower(), None) + if '*' in perms_map: + return queryset + elif action_str == '*': + return queryset + elif action_str in user_perms_map: + new_queryset = queryset.none() + for dept_id, data_range in user_perms_map[action_str].items: + dept = Dept.objects.get(id=dept_id) + if data_range == Post.POST_DATA_ALL: + return queryset + elif data_range == Post.POST_DATA_SAMELEVE_AND_BELOW: + if dept.parent: + belong_depts = get_child_queryset2(dept.parent) + else: + belong_depts = get_child_queryset2(dept) + queryset = queryset.filter(belong_dept__in = belong_depts) + elif data_range == Post.POST_DATA_THISLEVEL_AND_BELOW: + belong_depts = get_child_queryset2(dept) + queryset = queryset.filter(belong_dept__in = belong_depts) + elif data_range == Post.POST_DATA_THISLEVEL: + queryset = queryset.filter(belong_dept = dept) + elif data_range == Post.POST_DATA_THISLEVEL: + queryset = queryset.filter(create_by = user) + new_queryset = new_queryset | queryset + return new_queryset + else: + return queryset.none() + return queryset -class CustomModelViewSet(OptimizationMixin, CustomCreateModelMixin - , CustomUpdateModelMixin, ListModelMixin, RetrieveModelMixin +class CustomModelViewSet(CreateModelMixin + , UpdateModelMixin, ListModelMixin, RetrieveModelMixin , CustomDestoryModelMixin, CustomGenericViewSet): """ 增强的ModelViewSet @@ -53,6 +111,9 @@ class CustomModelViewSet(OptimizationMixin, CustomCreateModelMixin ,'patch':'{}_update'.format(basename) ,'delete':'{}_delete'.format(basename) ,'deletes':'{}_delete'.format(basename)} + for k, v in self.perms_map.items(): + if v not in ALL_PERMS and v!='*': + ALL_PERMS.append(v) @action(methods=['post'], detail=False, serializer_class=PkSerializer) def deletes(self,request,*args,**kwargs): @@ -62,11 +123,4 @@ class CustomModelViewSet(OptimizationMixin, CustomCreateModelMixin self.get_queryset().filter(id__in=pks).delete(update_by=request.user) return Response() else: - raise ValidationError("未获取到pks字段") - - - -class CustomDataModelViewSet(RbacDataMixin, CustomModelViewSet): - """ - 增强的ModelViewSet,带数据权限过滤 - """ \ No newline at end of file + raise ValidationError(**PKS_ERROR) \ No newline at end of file diff --git a/apps/utils/xunxi.py b/apps/utils/xunxi.py index 57d447e0..8d185229 100644 --- a/apps/utils/xunxi.py +++ b/apps/utils/xunxi.py @@ -1,9 +1,11 @@ from threading import Thread import requests import json +from apps.utils.errors import XX_REQUEST_ERROR from apps.utils.tools import print_roundtrip from server import settings import time +from rest_framework.exceptions import APIException, ParseError requests.packages.urllib3.disable_warnings() @@ -13,6 +15,8 @@ class XxClient: 寻息 """ def __init__(self, licence=settings.XX_LICENCE, username=settings.XX_USERNAME) -> None: + if not settings.XX_ENABLED: + return None self.licence = licence self.username = username self.isGetingToken = False @@ -57,6 +61,7 @@ class XxClient: def request(self, url:str, method:str='post', params=dict(), json=dict(), timeout=20): params['accessToken'] = self.token json['username'] = self.username + json['buildId'] = settings.XX_BUILDID if self.isGetingToken: req_num = 0 while True: @@ -69,20 +74,17 @@ class XxClient: else: r = getattr(requests, method)('{}{}'.format(settings.XX_BASE_URL, url) , params=params, json=json, verify=False) - if settings.DEBUG: - print_roundtrip(r) + # if settings.DEBUG: + # print_roundtrip(r) ret = r.json() if ret.get('errorCode') == '1060000': self.get_token() # 重新获取token self.request(url, method, params, json, timeout) # 重新请求 else: - msg = '{}|{}'.format(str(ret['errorCode']), '|'.join(ret['errorMsg'])) - res = dict(success=True, code=200000, msg= msg, data=ret['data']) if ret['errorCode'] != 0: - res['success'] = False - res['code'] = 400000 - return res - return dict(success=False, code=400900, msg='寻息接口访问异常', data=None) + return 'fail', dict(detail='|'.join(ret['errorMsg']), + code='xx_' + str(ret['errorCode'])) + return 'success', ret['data'] + return 'error', XX_REQUEST_ERROR -if settings.XX_ENABLED: - xxClient = XxClient() \ No newline at end of file +xxClient = XxClient() \ No newline at end of file diff --git a/apps/wf/views.py b/apps/wf/views.py index ca97334e..9a72b605 100644 --- a/apps/wf/views.py +++ b/apps/wf/views.py @@ -14,7 +14,7 @@ from django.shortcuts import get_object_or_404, render from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.decorators import action, api_view from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow -from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin +from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin from apps.wf.services import WfService from rest_framework.exceptions import APIException, PermissionDenied from rest_framework import status @@ -119,7 +119,7 @@ class CustomFieldViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, return CustomFieldCreateUpdateSerializer return super().get_serializer_class() -class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): +class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): perms_map = {'get':'*', 'post':'ticket_create'} queryset = Ticket.objects.all() serializer_class = TicketSerializer diff --git a/requirements.txt b/requirements.txt index e5571a6d..6eeafdfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ django-celery-beat==2.2.1 django-celery-results==2.3.0 django-cors-headers==3.11.0 django-filter==21.1 -django-simple-history==3.0.0 djangorestframework==3.13.1 djangorestframework-simplejwt==5.1.0 drf-yasg==1.20.0 diff --git a/server/settings.py b/server/settings.py index 4689345f..e15145ac 100644 --- a/server/settings.py +++ b/server/settings.py @@ -46,7 +46,6 @@ INSTALLED_APPS = [ 'drf_yasg', 'rest_framework', "django_filters", - 'simple_history', 'apps.utils', 'apps.third', 'apps.system', @@ -64,7 +63,6 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'simple_history.middleware.HistoryRequestMiddleware', ] ROOT_URLCONF = 'server.urls' @@ -303,7 +301,7 @@ LOGGING = { } } # 实例化myLogger -myLogger = logging.getLogger('log') +myLogger = logging.getLogger(__name__) # 大华ICC平台 DAHUA_ENABLED = conf.DAHUA_ENABLED @@ -317,4 +315,5 @@ DAHUA_SECRET = conf.DAHUA_SECRET XX_ENABLED = conf.XX_ENABLED XX_BASE_URL = conf.XX_BASE_URL XX_LICENCE = conf.XX_LICENCE -XX_USERNAME = conf.XX_USERNAME \ No newline at end of file +XX_USERNAME = conf.XX_USERNAME +XX_BUILDID = conf.XX_BUILDID \ No newline at end of file