diff --git a/apps/auth1/serializers.py b/apps/auth1/serializers.py index 0efcbeb4..2732bf2a 100755 --- a/apps/auth1/serializers.py +++ b/apps/auth1/serializers.py @@ -29,3 +29,7 @@ class PwResetSerializer(serializers.Serializer): class SecretLoginSerializer(serializers.Serializer): username = serializers.CharField(label="用户名") secret = serializers.CharField(label="密钥") + + +class FaceLoginSerializer(serializers.Serializer): + base64 = serializers.CharField() \ No newline at end of file diff --git a/apps/auth1/services.py b/apps/auth1/services.py index e7107e74..d7441e31 100644 --- a/apps/auth1/services.py +++ b/apps/auth1/services.py @@ -21,4 +21,4 @@ def validate_password(password): if re.match(pattern, password): return True else: - return False \ No newline at end of file + return False diff --git a/apps/auth1/urls.py b/apps/auth1/urls.py index 41925f5c..e2244f90 100755 --- a/apps/auth1/urls.py +++ b/apps/auth1/urls.py @@ -3,7 +3,7 @@ from django.urls import path from rest_framework_simplejwt.views import TokenRefreshView from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView, - SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView) + SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView, FaceLoginView) API_BASE_URL = 'api/auth/' urlpatterns = [ @@ -18,4 +18,5 @@ urlpatterns = [ path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'), path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'), path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'), + path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login') ] diff --git a/apps/auth1/views.py b/apps/auth1/views.py index f1207948..cb679e9e 100755 --- a/apps/auth1/views.py +++ b/apps/auth1/views.py @@ -17,6 +17,9 @@ from apps.utils.wx import wxClient from django.contrib.auth.hashers import make_password from django.db.models import Q from apps.auth1.services import validate_password +import base64 +from apps.utils.tools import tran64 +from apps.auth1.serializers import FaceLoginSerializer from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer, @@ -244,3 +247,49 @@ class PwResetView(CreateAPIView): user.save() return Response() raise ParseError('账户不存在或已禁用') + + +class FaceLoginView(CreateAPIView): + """人脸识别登录 + + 人脸识别登录 + """ + authentication_classes = [] + permission_classes = [] + serializer_class = FaceLoginSerializer + + + def create(self, request, *args, **kwargs): + """ + 人脸识别登录 + """ + from apps.hrm.services import HrmService + base64_data = base64.urlsafe_b64decode(tran64(request.data.get('base64').replace(' ', '+'))) + ep, msg = HrmService.face_find_from_base64(base64_data) + user = ep.user + if user: + refresh = RefreshToken.for_user(ep.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=400) diff --git a/apps/hrm/serializers.py b/apps/hrm/serializers.py index b2ed9d9b..77684dbc 100755 --- a/apps/hrm/serializers.py +++ b/apps/hrm/serializers.py @@ -68,6 +68,9 @@ class EmployeeCreateUpdateSerializer(CustomModelSerializer): old_job_state = instance.job_state old_name = instance.name instance = super().update(instance, validated_data) + if old_photo != instance.photo: # 如果照片有变动,需要更新人脸库 + from apps.hrm.tasks import delete_face_pkl + delete_face_pkl.delay(instance.id) if instance.user and instance != old_name: instance.user.name = instance.name instance.user.save() diff --git a/apps/hrm/services.py b/apps/hrm/services.py index 6dc4c9cf..bd7ba40d 100755 --- a/apps/hrm/services.py +++ b/apps/hrm/services.py @@ -1,17 +1,21 @@ -from rest_framework.exceptions import ParseError, APIException import logging +import os import time +import uuid +from datetime import datetime, timedelta + +from dateutil import tz +from django.conf import settings +from django.utils import timezone +from django.core.cache import cache + +from apps.system.models import User from apps.hrm.models import ClockRecord, Employee -from apps.third.errors import DH_REQUEST_ERROR +from apps.third.dahua import dhClient from apps.third.models import TDevice from apps.third.tapis import dhapis -from apps.third.dahua import dhClient from apps.utils.tools import rannum, ranstr -from datetime import datetime, timedelta -from django.utils import timezone -from dateutil import tz -from threading import Thread -from django.conf import settings + myLogger = logging.getLogger('log') @@ -254,8 +258,8 @@ class HrmService: @classmethod def swipe_next(cls, nodeCode: str, id_number: str, swip_time: str, e_type:int, detail:dict): - from apps.vm.models import Visit from apps.rpm.models import Rpj + from apps.vm.models import Visit # 进行相关方/访客项目更新 Visit.objects.filter(state=Visit.V_ENTER, visitors__employee__id_number=id_number).update( @@ -353,4 +357,79 @@ class HrmService: - # 此处可触发安全帽事件逻辑 \ No newline at end of file + # 此处可触发安全帽事件逻辑 + + + @classmethod + def get_facedata_from_img(cls, img_path): + import face_recognition + 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, '人脸数据获取失败请重新上传图片' + + @classmethod + def face_compare_from_base64(cls, base64_data): + import face_recognition + filename = str(uuid.uuid4()) + filepath = settings.BASE_DIR +'/temp/face_' + 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', None) + if face_datas is None: + from apps.hrm.tasks import update_all_facedata_cache + update_all_facedata_cache() + face_datas = cache.get('face_datas') + results = face_recognition.compare_faces(face_datas['datas'], + unknown_face_encoding, tolerance=0.45) + for index, value in enumerate(results): + if value: + # 识别成功 + user = User.objects.get(id=face_datas['users'][index]) + return user, '' + return None, '人脸未匹配,请调整位置' + + @classmethod + def face_find_from_base64(cls, base64_data): + from deepface import DeepFace + img_name = str(uuid.uuid4()) + img_path = settings.BASE_DIR +'/temp/face_' + img_name +'.png' + with open(img_path, 'wb') as f: + f.write(base64_data) + db_path = os.path.join(settings.BASE_DIR, 'media/face') + df = DeepFace.find(img_path=img_path, db_path=db_path, model_name='Facenet512') + if df.shape[0] > 0: + matched = df.iloc[0].identity + epId = matched.split('/')[-1].split('.')[0] + return Employee.objects.get(id=epId), '' + else: + return None, '人脸未匹配,请调整位置' + + + +# def VGGFaceloadModel(weight_path): +# from deepface.basemodels import VGGFace +# from keras.src.engine.training import Model +# model = VGGFace.baseModel() +# model.load_weights(weight_path) +# vgg_face_descriptor = Model(inputs=model.layers[0].input, outputs=model.layers[-2].output) +# return vgg_face_descriptor + +# def Facenet512loadModel(weight_path): +# from deepface.basemodels import Facenet +# model = Facenet.InceptionResNetV2(dimension=512) +# model.load_weights(weight_path) +# return model diff --git a/apps/hrm/tasks.py b/apps/hrm/tasks.py index e238e9f1..e950e9c3 100755 --- a/apps/hrm/tasks.py +++ b/apps/hrm/tasks.py @@ -1,16 +1,20 @@ from __future__ import absolute_import, unicode_literals -from celery import shared_task -from apps.hrm.models import Employee, ClockRecord -from dateutil import tz +import time from datetime import datetime, timedelta + +from celery import shared_task +from dateutil import tz +from django.core.cache import cache + +from apps.hrm.models import Employee +from apps.hrm.services import HrmService from apps.third.dahua import dhClient from apps.third.tapis import dhapis -from apps.hrm.services import HrmService -from django.utils import timezone from apps.utils.tasks import CustomTask -import time - +import os +from django.conf import settings +import shutil @shared_task def update_all_employee_not_atwork(): @@ -85,4 +89,39 @@ def correct_card_time(): _, res = dhClient.request(**dhapis['card_update'], json=json_data) HrmService.save(ep, {'dh_face_card_start': start_time_str, 'dh_face_card_end': end_time_new_str}) print('已更新-' + ep.name + '-' + dh_face_card + '-' + end_time_new_str) - time.sleep(1) \ No newline at end of file + time.sleep(1) + + +@shared_task +def update_all_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']) + face_data_dict = {"users": face_users, "datas": face_datas} + cache.set('face_datas', face_data_dict) + + +@shared_task(base=CustomTask) +def delete_face_pkl(epId): + """ + 尝试删除人脸库缓存文件 + """ + if epId: + ep = Employee.objects.get(id=epId) + photo_path = settings.BASE_DIR + ep.photo + face_path = os.path.join(settings.FACE_PATH, f'{epId}.jpg') + shutil.copy(photo_path, face_path) + file_path = os.path.join(settings.BASE_DIR, 'media/face/representations_facenet512.pkl') + if os.path.exists(file_path): + try: + os.remove(file_path) + except Exception as e: + delete_face_pkl.apply_async(countdown=5) + \ No newline at end of file diff --git a/apps/utils/tools.py b/apps/utils/tools.py index 9727c246..18dc6661 100755 --- a/apps/utils/tools.py +++ b/apps/utils/tools.py @@ -9,6 +9,12 @@ import requests from io import BytesIO from rest_framework.serializers import ValidationError +def tran64(s): + missing_padding = len(s) % 4 + if missing_padding != 0: + s = s+'='* (4 - missing_padding) + return s + def singleton(cls): _instance = {}