From a7bd6e0f6b4d0f91fe904569536b8161463db58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=94=E4=BC=9F=E6=A0=8B=5F28095?= <1361575048@qq.com> Date: Wed, 19 Feb 2025 16:15:13 +0800 Subject: [PATCH] first commit --- .gitignore | 12 + ChaCeRndTrans/__init__.py | 0 ChaCeRndTrans/asgi.py | 16 + ChaCeRndTrans/basic.py | 45 + ChaCeRndTrans/code.py | 85 ++ ChaCeRndTrans/database_router.py | 44 + ChaCeRndTrans/settings.py | 458 +++++++ ChaCeRndTrans/urls.py | 50 + ChaCeRndTrans/wsgi.py | 16 + apps/__init__.py | 0 apps/common/__init__.py | 0 apps/common/admin.py | 3 + apps/common/apps.py | 6 + apps/common/migrations/0001_initial.py | 123 ++ apps/common/migrations/__init__.py | 0 apps/common/models.py | 215 +++ apps/common/serializers/__init__.py | 0 apps/common/serializers/archive_serializer.py | 13 + apps/common/serializers/dict_serializer.py | 57 + apps/common/tests.py | 3 + apps/common/urls.py | 25 + apps/common/views.py | 3 + apps/common/views/__init__.py | 0 apps/common/views/archive.py | 959 ++++++++++++++ apps/common/views/area.py | 72 + apps/common/views/dict.py | 413 ++++++ apps/common/views/history.py | 88 ++ apps/common/views/index.py | 15 + apps/common/views/upload_file.py | 197 +++ apps/rbac/__init__.py | 0 apps/rbac/admin.py | 3 + apps/rbac/apps.py | 6 + apps/rbac/migrations/0001_initial.py | 121 ++ apps/rbac/migrations/__init__.py | 0 apps/rbac/models.py | 129 ++ apps/rbac/serializers/__init__.py | 0 apps/rbac/serializers/batch_serializer.py | 13 + apps/rbac/serializers/menu_serializer.py | 13 + .../rbac/serializers/permission_serializer.py | 12 + apps/rbac/serializers/role_serializer.py | 31 + apps/rbac/serializers/user_serializer.py | 127 ++ apps/rbac/signals.py | 14 + apps/rbac/tests.py | 3 + apps/rbac/urls.py | 30 + apps/rbac/views.py | 3 + apps/rbac/views/FrontRole.py | 96 ++ apps/rbac/views/Slider_Verification.py | 286 ++++ apps/rbac/views/SubAccount.py | 393 ++++++ apps/rbac/views/__init__.py | 0 apps/rbac/views/company.py | 560 ++++++++ apps/rbac/views/menu.py | 138 ++ apps/rbac/views/message.py | 105 ++ apps/rbac/views/permission.py | 95 ++ apps/rbac/views/role.py | 73 ++ apps/rbac/views/user.py | 1163 +++++++++++++++++ apps/staff/__init__.py | 0 apps/staff/admin.py | 3 + apps/staff/apps.py | 5 + apps/staff/migrations/0001_initial.py | 177 +++ apps/staff/migrations/__init__.py | 0 apps/staff/models.py | 650 +++++++++ apps/staff/serializers/Serializer.py | 259 ++++ apps/staff/serializers/__init__.py | 0 apps/staff/serializers/staffSerializer.py | 28 + apps/staff/tests.py | 3 + apps/staff/urls.py | 9 + apps/staff/utils.py | 301 +++++ apps/staff/views.py | 3 + apps/staff/views/__init__.py | 0 apps/staff/views/dept.py | 566 ++++++++ apps/tasks/__init__.py | 1 + apps/tasks/admin.py | 3 + apps/tasks/apps.py | 5 + apps/tasks/migrations/__init__.py | 0 apps/tasks/models.py | 20 + apps/tasks/tasks.py | 18 + apps/tasks/tests.py | 0 apps/tasks/urls.py | 13 + apps/tasks/views.py | 256 ++++ manage.py | 22 + media/research_descriptions.txt | 121 ++ media/人员工时生成表.xlsx | Bin 0 -> 10239 bytes media/人员工资导入模板.xlsx | Bin 0 -> 10184 bytes media/场景一模版.zip | Bin 0 -> 27187 bytes requirements.txt | Bin 0 -> 3254 bytes requirementstest.txt | Bin 0 -> 11580 bytes utils/__init__.py | 0 utils/custom.py | 1017 ++++++++++++++ utils/funcs.py | 83 ++ utils/http_sms_mt.py | 56 + utils/middleware.py | 128 ++ utils/sms_client.py | 147 +++ utils/thread_pool.py | 123 ++ utils/throttles.py | 109 ++ 94 files changed, 10458 insertions(+) create mode 100644 .gitignore create mode 100644 ChaCeRndTrans/__init__.py create mode 100644 ChaCeRndTrans/asgi.py create mode 100644 ChaCeRndTrans/basic.py create mode 100644 ChaCeRndTrans/code.py create mode 100644 ChaCeRndTrans/database_router.py create mode 100644 ChaCeRndTrans/settings.py create mode 100644 ChaCeRndTrans/urls.py create mode 100644 ChaCeRndTrans/wsgi.py create mode 100644 apps/__init__.py create mode 100644 apps/common/__init__.py create mode 100644 apps/common/admin.py create mode 100644 apps/common/apps.py create mode 100644 apps/common/migrations/0001_initial.py create mode 100644 apps/common/migrations/__init__.py create mode 100644 apps/common/models.py create mode 100644 apps/common/serializers/__init__.py create mode 100644 apps/common/serializers/archive_serializer.py create mode 100644 apps/common/serializers/dict_serializer.py create mode 100644 apps/common/tests.py create mode 100644 apps/common/urls.py create mode 100644 apps/common/views.py create mode 100644 apps/common/views/__init__.py create mode 100644 apps/common/views/archive.py create mode 100644 apps/common/views/area.py create mode 100644 apps/common/views/dict.py create mode 100644 apps/common/views/history.py create mode 100644 apps/common/views/index.py create mode 100644 apps/common/views/upload_file.py create mode 100644 apps/rbac/__init__.py create mode 100644 apps/rbac/admin.py create mode 100644 apps/rbac/apps.py create mode 100644 apps/rbac/migrations/0001_initial.py create mode 100644 apps/rbac/migrations/__init__.py create mode 100644 apps/rbac/models.py create mode 100644 apps/rbac/serializers/__init__.py create mode 100644 apps/rbac/serializers/batch_serializer.py create mode 100644 apps/rbac/serializers/menu_serializer.py create mode 100644 apps/rbac/serializers/permission_serializer.py create mode 100644 apps/rbac/serializers/role_serializer.py create mode 100644 apps/rbac/serializers/user_serializer.py create mode 100644 apps/rbac/signals.py create mode 100644 apps/rbac/tests.py create mode 100644 apps/rbac/urls.py create mode 100644 apps/rbac/views.py create mode 100644 apps/rbac/views/FrontRole.py create mode 100644 apps/rbac/views/Slider_Verification.py create mode 100644 apps/rbac/views/SubAccount.py create mode 100644 apps/rbac/views/__init__.py create mode 100644 apps/rbac/views/company.py create mode 100644 apps/rbac/views/menu.py create mode 100644 apps/rbac/views/message.py create mode 100644 apps/rbac/views/permission.py create mode 100644 apps/rbac/views/role.py create mode 100644 apps/rbac/views/user.py create mode 100644 apps/staff/__init__.py create mode 100644 apps/staff/admin.py create mode 100644 apps/staff/apps.py create mode 100644 apps/staff/migrations/0001_initial.py create mode 100644 apps/staff/migrations/__init__.py create mode 100644 apps/staff/models.py create mode 100644 apps/staff/serializers/Serializer.py create mode 100644 apps/staff/serializers/__init__.py create mode 100644 apps/staff/serializers/staffSerializer.py create mode 100644 apps/staff/tests.py create mode 100644 apps/staff/urls.py create mode 100644 apps/staff/utils.py create mode 100644 apps/staff/views.py create mode 100644 apps/staff/views/__init__.py create mode 100644 apps/staff/views/dept.py create mode 100644 apps/tasks/__init__.py create mode 100644 apps/tasks/admin.py create mode 100644 apps/tasks/apps.py create mode 100644 apps/tasks/migrations/__init__.py create mode 100644 apps/tasks/models.py create mode 100644 apps/tasks/tasks.py create mode 100644 apps/tasks/tests.py create mode 100644 apps/tasks/urls.py create mode 100644 apps/tasks/views.py create mode 100644 manage.py create mode 100644 media/research_descriptions.txt create mode 100644 media/人员工时生成表.xlsx create mode 100644 media/人员工资导入模板.xlsx create mode 100644 media/场景一模版.zip create mode 100644 requirements.txt create mode 100644 requirementstest.txt create mode 100644 utils/__init__.py create mode 100644 utils/custom.py create mode 100644 utils/funcs.py create mode 100644 utils/http_sms_mt.py create mode 100644 utils/middleware.py create mode 100644 utils/sms_client.py create mode 100644 utils/thread_pool.py create mode 100644 utils/throttles.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cd88d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.vscode/ +.vs/ +.idea +.vscode +.DS_Store +logs/ +celecelerybeat.* +celerybeat-schedule.* +*/__pycache__ +__pycache__/ +.pytest_cache/ +.idea/ \ No newline at end of file diff --git a/ChaCeRndTrans/__init__.py b/ChaCeRndTrans/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ChaCeRndTrans/asgi.py b/ChaCeRndTrans/asgi.py new file mode 100644 index 0000000..8bacf65 --- /dev/null +++ b/ChaCeRndTrans/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for ChaCeRndTrans project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ChaCeRndTrans.settings') + +application = get_asgi_application() diff --git a/ChaCeRndTrans/basic.py b/ChaCeRndTrans/basic.py new file mode 100644 index 0000000..ba0807b --- /dev/null +++ b/ChaCeRndTrans/basic.py @@ -0,0 +1,45 @@ +import six +from rest_framework.response import Response +from rest_framework.serializers import Serializer + + +class CCAIResponse(Response): + def __init__(self, data=None, status=200, code=200, msg="成功", + template_name=None, headers=None, + exception=False, content_type=None, pagination=None, count=None): + + super(Response, self).__init__(None, status=status) + + if isinstance(data, Serializer): + msg = ( + "You passed a Serializer instance as data, but " + "probably meant to pass serialized `.data` or " + "`.error`. representation." + ) + raise AssertionError(msg) + if status >= 400 and msg=='成功': + msg = "失败" + if code == 200: + code = status + if pagination or count: + self.data = { + "code": code, + "message": msg, + "data": data, + "count": count, + "pagination": pagination + } + else: + self.data = { + "code": code, + "message": msg, + "data": data, + } + self.template_name = template_name + self.exception = exception + self.content_type = content_type + + if headers: + for name, value in six.iteritems(headers): + self[name] = value + diff --git a/ChaCeRndTrans/code.py b/ChaCeRndTrans/code.py new file mode 100644 index 0000000..42b3bf4 --- /dev/null +++ b/ChaCeRndTrans/code.py @@ -0,0 +1,85 @@ +from rest_framework import status + +# 成功 +OK = status.HTTP_200_OK + +NO_CONTENT = status.HTTP_204_NO_CONTENT + +ACCEPTED = status.HTTP_202_ACCEPTED +# 失败 +BAD = status.HTTP_400_BAD_REQUEST +# 无权限 +FORBIDDEN = status.HTTP_403_FORBIDDEN +# 未认证 +UNAUTHORIZED = status.HTTP_401_UNAUTHORIZED +# 创建 +CREATED = status.HTTP_201_CREATED +# NOT_FOUND +NOT_FOUND = status.HTTP_404_NOT_FOUND +# INTERNAL_SERVER_ERROR +SERVER_ERROR = status.HTTP_500_INTERNAL_SERVER_ERROR +# TOO_MANY_REQUESTS +TOO_MANY_REQUESTS = status.HTTP_429_TOO_MANY_REQUESTS + +# 达到次数限制 +USER_VISITLiMIT = 10001 +# 非账号客户 +NOT_CUSTOMER = 10002 + +# 添加企业成功-用于主账号已存在企业,子账号添加分配成功 +ALOCATE_CUSTOMER = 10003 + +# 订阅设置、超出订阅城市数量限制 +LIMIT_SETTING = 10004 +# 访问次数剩下50 +USER_VISIT50LiMIT = 10005 + +# 添加企业成功-企业未匹配到数据,为方便使用,请补充数据 +ADD_NO_QIXINBAO = 10100 + +# 同步失败 +SYN_ERROR = 10101 + +# 用户登录请求code编码 +# 用户不存在 +NO_FOUND_USER = 20001 +# 密码错误 +BAD_PSW = 20002 +# 用户未激活 +USER_NO_ACTIVE = 20003 +# 用户未绑定 +USER_NO_BIND = 20004 +# 已有手机号未注册 +PHONE_NO_BIND = 20005 + +#手机号已被注册 +PHONE_IS_BIND = 20006 + +# 未注册手机号 +NO_REGISTER_PHONE = 20007 + +# 会员过期 +USER_EXPIRED = 20008 + +# 修改密码,旧密码错误 +OLD_PWD_ERROR = 20009 + +# 验证码验证错误 +VERIFYCODE_SEND_ERROR = 20011 +# 验证码失效 +VERIFYCODE_NO_FOUND = 20012 +# 验证码验证错误 +VERIFYCODE_ERROR = 20013 +# 滑块验证码验证 +NEED_SLIDER_VERIFYCODE = 20014 + +# 邮箱号验证错误,请输入正确的邮箱账号 +ERROR_EMAIL = 20015 + +# 一个账号不能重复登录 +REPEATED_LOGIN = 30001 + + +# 请求参数错误 +PARAMS_ERR = "params error" + diff --git a/ChaCeRndTrans/database_router.py b/ChaCeRndTrans/database_router.py new file mode 100644 index 0000000..d7dd41f --- /dev/null +++ b/ChaCeRndTrans/database_router.py @@ -0,0 +1,44 @@ +# !/usr/bin/env python +# coding:utf8 +from django.conf import settings + +DATABASE_MAPPING = settings.DATABASE_APPS_MAPPING # 在setting中定义的路由表 + + +class DatabaseAppsRouter(object): + def db_for_read(self, model, **hints): + if model._meta.app_label in DATABASE_MAPPING: + return DATABASE_MAPPING[model._meta.app_label] + return None + + def db_for_write(self, model, **hints): + + if model._meta.app_label in DATABASE_MAPPING: + return DATABASE_MAPPING[model._meta.app_label] + return None + + def allow_relation(self, obj1, obj2, **hints): + + db_obj1 = DATABASE_MAPPING.get(obj1._meta.app_label) + db_obj2 = DATABASE_MAPPING.get(obj2._meta.app_label) + if db_obj1 and db_obj2: + if db_obj1 == db_obj2: + return True + else: + return False + return None + + def allow_syncdb(self, db, model): + + if db in DATABASE_MAPPING.values(): + return DATABASE_MAPPING.get(model._meta.app_label) == db + elif model._meta.app_label in DATABASE_MAPPING: + return False + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if db in DATABASE_MAPPING.values(): + return DATABASE_MAPPING.get(app_label) == db + elif app_label in DATABASE_MAPPING: + return False + return None \ No newline at end of file diff --git a/ChaCeRndTrans/settings.py b/ChaCeRndTrans/settings.py new file mode 100644 index 0000000..e029d87 --- /dev/null +++ b/ChaCeRndTrans/settings.py @@ -0,0 +1,458 @@ +""" +Django settings for ChaCeRDE project. +Generated by 'django-admin startproject' using Django 3.1.4. +For more information on this file, see +https://docs.djangoproject.com/en/3.1/topics/settings/ +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.1/ref/settings/ +""" +import datetime +import os +import sys +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, os.path.join(BASE_DIR, "apps")) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +# 测试/开发 +SECRET_KEY = 'mm3oss9fkz(0tirduc^^!-bv%o(u4=_-9geo=1(*s=9s#04flo' +# 生产环境 +# SECRET_KEY = 'QPEVJt.$wnGS8yonA+XWPW7B92X8-=T9pF95iTr9eJEWU7IfuA' + +# 数据ID的hash +ID_KEY = "gcos8pr02#f%dpr0!#u7(l0bl#oo1fcq)ee-@y-v$2_oz+j9ufsmv" + +# 数值编码 +FONT_ID_KEY = "gcos8pr02#f%dpr0!#u7(l0bl#oo1fcq)ee-@y-v$2_oz+j9ufsmv" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False # 本地开发的时候记得设置为TRUE,否则拿不到静态文件, 提交到线上时记得改为FALSE +# DEBUG = True # 本地开发的时候记得设置为TRUE,否则拿不到静态文件, 提交到线上时记得改为FALSE +DEVELOP_DEBUG = False + +TEST_DOMAIN = 'https://test.nw.chace-ai.net' +RELEASE_DOMAIN = 'https://narwhale.chace-ai.net' + +# 短信接口平台url +MSG_URL = 'http://www.chace-ai.cn/api/message/send/' +TEST_MSG_URL = 'http://test.chace-ai.cn/api/message/send/' + +ALLOWED_HOSTS = ['*'] +# 跳转404链接页面 +RED_404_URL = 'https://www.chace-ai.com/404/' + +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +if DEBUG: + # 文件存放路径和文件访问域名 + FILE_PATH = MEDIA_ROOT # 开发本地存储目录 + FILE_HTTP = "http://127.0.0.1:8000" # 本地开发使用 媒体域名拼接使用 +else: + # 文件存放路径和文件访问域名 + FILE_PATH = "/data/chacerndtrans" # 线上本地存储目录 + FILE_HTTP = "https://test.nw.chace-ai.net" # 线上开发使用 媒体域名拼接使用 + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django_apscheduler', + 'simple_history', + "rest_framework", + "corsheaders", # 跨域 + "drf_yasg", # 用于集成 drf与swagger + "django_filters", + "utils", + "common", # 通用模块 + "rbac", # 权限模块 + "staff", # 权限模块 + "tasks", # 定时任务 +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', # 操作记录 + + 'utils.middleware.RequestLogMiddleware', # 将当前的request信息保存到当前线程供日志打印使用 + 'utils.middleware.ExceptionLoggingMiddleware', +] + +#跨域增加忽略 +CORS_ALLOW_CREDENTIALS = True +CORS_ORIGIN_ALLOW_ALL = True +CORS_ORIGIN_WHITELIST = ( + '' +) + +CORS_ALLOW_METHODS = ( + 'DELETE', + 'GET', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + 'VIEW', +) + +CORS_ALLOW_HEADERS = ( + 'XMLHttpRequest', + 'X_FILENAME', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', + 'Pragma', +) + +ROOT_URLCONF = 'ChaCeRndTrans.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'ChaCeRndTrans.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + # "ENGINE": "dj_db_conn_pool.backends.mysql", + "NAME": "chace_rnd_3x", + "USER": "ccwtdm", + "PASSWORD": "fhRZLEu562wi23M4QC4iYq615UZEvgeB", + "HOST": "47.112.242.103", + "PORT": 17601, + # 本地数据库 + # "USER": "root", + # "PASSWORD": "123456", + # "HOST": "localhost", + # "PORT": 3306, + # "CONN_MAX_AGE": 600, + "OPTIONS": { + "charset": "utf8mb4" + }, + 'POOL_OPTIONS': { + 'POOL_SIZE': 20, + 'MAX_OVERFLOW': 1000, + 'RECYCLE': 60 * 60 + } + }, + "chace_rnd": { + "ENGINE": "django.db.backends.mysql", + # "ENGINE": "dj_db_conn_pool.backends.mysql", + "NAME": "chace_rnd", + "USER": "ccwtdm", + "PASSWORD": "fhRZLEu562wi23M4QC4iYq615UZEvgeB", + "HOST": "47.112.242.103", + "PORT": 17601, + # 本地数据库 + # "USER": "root", + # "PASSWORD": "123456", + # "HOST": "localhost", + # "PORT": 3306, + # "CONN_MAX_AGE": 600, + "OPTIONS": { + "charset": "utf8mb4" + }, + 'POOL_OPTIONS': { + 'POOL_SIZE': 20, + 'MAX_OVERFLOW': 1000, + 'RECYCLE': 60 * 60 + } + }, +} + +# use multi-database in django +DATABASE_ROUTERS = ["ChaCeRndTrans.database_router.DatabaseAppsRouter"] +DATABASE_APPS_MAPPING = { + # example: + #"app_name":"database_name", + "chace_rnd": "chace_rnd", +} + + +# Password validation +# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +REST_FRAMEWORK = { + "DEFAULT_VERSION": "v1", # 默认的版本 + "ALLOWED_VERSIONS": ["v1", "v2"], # 允许的版本 + "VERSION_PARAM": "version", # GET方式url中参数的名字 ?version=xxx + "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + ), + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + "rest_framework.filters.SearchFilter", + "rest_framework.filters.OrderingFilter", + ], + # "DEFAULT_PERMISSION_CLASSES": [ + # "rest_framework.permissions.IsAuthenticated", + # # "rest_framework.permissions.AllowAny", + # ], + "DEFAULT_AUTHENTICATION_CLASSES": [ + # 此方法是 djangorestframework-jwt 的方法,用于检查用户token是否合法 + "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + ], + # 自定义异常处理 + "EXCEPTION_HANDLER": "utils.custom.chacerde_exception_handler", + # 限流 + "DEFAULT_THROTTLE_CLASSES": ( + # "rest_framework.throttling.AnonRateThrottle", # 限制所有匿名未认证的用户 + # "rest_framework.throttling.UserRateThrottle" # 限制认证用户,使用User id 来区分 + "rest_framework.throttling.ScopedRateThrottle", # 限制用户对于每个视图的访问频次,使用ip或user id + "utils.throttles.CustomThrottle", + ), + # DEFAULT_THROTTLE_RATES 可以使用 second, minute, hour 或day来指明周期。 + "DEFAULT_THROTTLE_RATES": { + # 'anon':s '10/s', # 匿名用户对应的节流次数 + # 'user': '200/s' # 登录用户对应 的节流次数 + }, + "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", +} + +# jwt setting +JWT_AUTH = { + "JWT_AUTH_COOKIE": "chacewang", + "JWT_EXPIRATION_DELTA": datetime.timedelta(days=3), + "JWT_ISSUER": "https://www.chacewang.com", + "JWT_AUTH_HEADER_PREFIX": "Bearer", + "JWT_ALLOW_REFRESH": True, + "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=1) +} + +# **********************缓存设置*************************** +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://:123456@47.112.242.103:16799", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + # "PASSWORD": "jKCrqEb3XINybCyGp38H&z4eHbHrby" + "CONNECTION_POOL_KWARGS": {"max_connections": 100}, + } + }, +} + +MAX_RETRIES = 3 # 最大重试次数 + +REDIS_TIMEOUT = 7*24*60*60 +DAY_REDIS_TIMEOUT = 24*60*60 +CUBES_REDIS_TIMEOUT = 60*60 +NEVER_REDIS_TIMEOUT = 365*24*60*60 + +# # swagger 配置项 +SWAGGER_SETTINGS = { + # 基础样式 + "SECURITY_DEFINITIONS": { + "basic": { + "type": "basic" + }, + }, + "DEFAULT_INFO": "DjangoDrfTest.urls.swagger_info", # 这里注意,更改DjangoDrfTest + # 如果需要登录才能够查看接口文档, 登录的链接使用restframework自带的. + "LOGIN_URL": "rest_framework:login", + "LOGOUT_URL": "rest_framework:logout", + # "DOC_EXPANSION": None, + # "SHOW_REQUEST_HEADERS":True, + # "USE_SESSION_AUTH": True, + # "DOC_EXPANSION": "list", + # 接口文档中方法列表以首字母升序排列 + "APIS_SORTER": "alpha", + # 如果支持json提交, 则接口文档中包含json输入框 + "JSON_EDITOR": True, + # 方法列表字母排序 + "OPERATIONS_SORTER": "alpha", + "VALIDATOR_URL": None, +} + +# Internationalization +# https://docs.djangoproject.com/en/3.1/topics/i18n/ + +LANGUAGE_CODE = "zh-hans" + +TIME_ZONE = "Asia/Shanghai" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = False + +# 用户验证 +AUTH_USER_MODEL = "rbac.UserProfile" + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.1/howto/static-files/ + +STATIC_URL = '/static/' + +FILE_PATH = "/data/chacewang_file/" # 本地存储目录 +SHOW_UPLOAD_PATH = "/upload/" # 前端显示目录 +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MAX_FILE_SIZE = 104857600 # 限制最大文件100MB +MAX_IMAGE_SIZE = 2097152 # 限制最大图片文件2MB + +MAX_MP3_FILE_SIZE = 20 # 音频文件最大限制20M +MAX_MP4_FILE_SIZE = 100 # 视频文件最大限制100M +MAX_IMAGE_FILE_SIZE = 5 # 图片文件最大限制5M +MAX_PDF_FILE_SIZE = 80 # PDF文件最大限制80M + +DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 单位为字节数, 此处配置的值为10M + +# ********************************************************** +# ********************** 全局常量 ************************** +# ********************************************************** +MY_PAGE_SIZE = 10 # 默认分页,每页显示条数 +MY_ARTICLE_PAGE_SIZE = 5 # 客户端文章列表分页,每页显示条数 +MY_PAGE_SIZE_QUERY_PARAM = "size" # 可以通过传入pager1/?page=2&size=4,改变默认每页显示的个数 +MY_MAX_PAGE_SIZE = 100 # 最大条数不超过100 +MY_PAGE_QUERY_PARAM = "page" # 获取页码数的 +# DEFAULT_PAGINATION_CLASS = 'path.to.PageNumberPaginationWithoutCount' # 去掉分页的count + +# 存放日志的路径 +# window +BASE_LOG_DIR = os.path.join(BASE_DIR, "logs") +# linux +# BASE_LOG_DIR = '/var/log/project/' + +# 如果不存在这个logs文件夹,就自动创建一个 +if not os.path.exists(BASE_LOG_DIR): + os.mkdir(BASE_LOG_DIR) + +LOGGING = { + "version": 1, # 保留字 + "disable_existing_loggers": False, # 禁用已经存在的logger实例 + "formatters": { + "standard": { + "format": "[%(asctime)s][%(levelname)s]""[%(filename)s:%(lineno)d][%(message)s]" + }, + "simple": { + "format": "[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s" + }, + + }, + # 过滤器 + "filters": { + "require_debug_true": { + "()": "django.utils.log.RequireDebugTrue", + }, + # 注册该过滤器 + "request_info": { + "()": "utils.middleware.RequestLogFilter", + } + }, + "handlers": { + "default": { + "level": "INFO", + "class": "logging.handlers.TimedRotatingFileHandler", # 保存到文件,根据时间自动切 + "filename": os.path.join(BASE_LOG_DIR, "chacewang_info.log"), + "backupCount": 3, # 备份数为3 xx.log --> xx.log.2018-08-23_00-00-00 --> xx.log.2018-08-24_00-00-00 --> ... + "when": "D", # 每天一切, 可选值有S/秒 M/分 H/小时 D/天 W0-W6/周(0=周一) midnight/如果没指定时间就默认在午夜 + "formatter": "standard", + "encoding": "utf-8", + }, + "warn": { + "level": "WARNING", + "class": "logging.handlers.TimedRotatingFileHandler", # 保存到文件,根据时间自动切 + "filename": os.path.join(BASE_LOG_DIR, "chacewang_warning.log"), + "backupCount": 3, # 备份数为3 xx.log --> xx.log.2018-08-23_00-00-00 --> xx.log.2018-08-24_00-00-00 --> ... + "when": "D", # 每天一切, 可选值有S/秒 M/分 H/小时 D/天 W0-W6/周(0=周一) midnight/如果没指定时间就默认在午夜 + "formatter": "standard", + "encoding": "utf-8", + }, + # 专门用来记错误日志 + "error": { + "level": "ERROR", + "class": "logging.handlers.TimedRotatingFileHandler", # 保存到文件,根据时间自动切 + "filename": os.path.join(BASE_LOG_DIR, "chacewang_err.log"), # 日志文件 + "backupCount": 3, # 备份数为3 xx.log --> xx.log.2018-08-23_00-00-00 --> xx.log.2018-08-24_00-00-00 --> ... + "when": "D", # 每天一切, 可选值有S/秒 M/分 H/小时 D/天 W0-W6/周(0=周一) midnight/如果没指定时间就默认在午夜 + "formatter": "standard", + "encoding": "utf-8", + }, + # 按文件大小分割 + # "DEMO": { + # "level": "INFO", + # "class": "logging.handlers.RotatingFileHandler", # 保存到文件,根据文件大小自动切 + # "filename": os.path.join(BASE_LOG_DIR, "chacewang_info.log"), # 日志文件 + # "maxBytes": 1024 * 1024 * 50, # 日志大小 50M + # "backupCount": 3, # 备份数为3 xx.log --> xx.log.1 --> xx.log.2 --> xx.log.3 + # "formatter": "standard", + # "encoding": "utf-8", + # }, + }, + "loggers": { + "info": { + "handlers": ["default"], + "level": "INFO", + "propagate": True, # 向不向更高级别的logger传递 + }, + "warn": { + "handlers": ["warn"], + "level": "WARNING", + "propagate": True, + }, + "error": { + "handlers": ["error"], + "level": "ERROR", + "propagate": True, + } + } +} diff --git a/ChaCeRndTrans/urls.py b/ChaCeRndTrans/urls.py new file mode 100644 index 0000000..01c50f3 --- /dev/null +++ b/ChaCeRndTrans/urls.py @@ -0,0 +1,50 @@ +"""ChaCeRndTrans URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf.urls import url +from django.contrib import admin +from django.urls import path, re_path, include +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from django.conf.urls.static import static +from ChaCeRndTrans import settings + +schema_view = get_schema_view( + openapi.Info( + title="独角鲸 API接口文档平台", # 必传 + default_version="v1", # 必传 + # description="这是查策AI管理后台的接口文档", + # terms_of_service="https://www.chacewang.net", + # contact=openapi.Contact(email="1361575048@qq.com"), + # license=openapi.License(name="BSD License"), + ), + public=True, + # permission_classes=(permissions.AllowAny,), # 权限类 + + +) + +urlpatterns = [ + # swagger接口文档路由,三种不同风格的接口文档,自选其一 + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + re_path(r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), name="schema-json"), + path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), + path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + path('admin/', admin.site.urls), + path("", include("rbac.urls")), + path("", include("common.urls")), + # path("", include("tasks.urls")), + # path(r"^media/(?P.*)$", serve, {'document_root': settings.MEDIA_ROOT}), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/ChaCeRndTrans/wsgi.py b/ChaCeRndTrans/wsgi.py new file mode 100644 index 0000000..377ceb3 --- /dev/null +++ b/ChaCeRndTrans/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for ChaCeRndTrans project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ChaCeRndTrans.settings') + +application = get_wsgi_application() diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/common/__init__.py b/apps/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/common/admin.py b/apps/common/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/common/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/common/apps.py b/apps/common/apps.py new file mode 100644 index 0000000..01cca2f --- /dev/null +++ b/apps/common/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommonConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'common' diff --git a/apps/common/migrations/0001_initial.py b/apps/common/migrations/0001_initial.py new file mode 100644 index 0000000..f52e106 --- /dev/null +++ b/apps/common/migrations/0001_initial.py @@ -0,0 +1,123 @@ +# Generated by Django 3.1.4 on 2024-01-24 15:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Area', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('DataDictionaryDetailId', models.CharField(default='', max_length=36, verbose_name='字典详情id')), + ('Code', models.CharField(blank=True, max_length=10, null=True, verbose_name='地区编号')), + ('Abbreviation', models.CharField(blank=True, max_length=50, null=True, verbose_name='地区简称')), + ('TopImg', models.CharField(blank=True, max_length=500, null=True, verbose_name='封面图')), + ('BaiDuCode', models.CharField(blank=True, max_length=30, null=True, verbose_name='百度地图编号')), + ('IsMunicipality', models.BooleanField(blank=True, null=True, verbose_name='是否自辖市')), + ('IsProvince', models.BooleanField(blank=True, null=True, verbose_name='是否省级')), + ('IsCity', models.BooleanField(blank=True, null=True, verbose_name='是否市级')), + ('IsHotCity', models.BooleanField(blank=True, null=True, verbose_name='是否热门城市')), + ('IsTownship', models.BooleanField(blank=True, null=True, verbose_name='是否区/乡镇')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人ID')), + ('CreateByName', models.CharField(blank=True, max_length=30, null=True, verbose_name='创建人名称')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人ID')), + ('UpdateByName', models.CharField(blank=True, max_length=30, null=True, verbose_name='更新人名称')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '地区', + 'verbose_name_plural': '地区', + 'ordering': ['id'], + 'managed': False, + }, + ), + migrations.CreateModel( + name='DataDictionary', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('DataDictionaryId', models.CharField(blank=True, max_length=36, null=True, verbose_name='字典id')), + ('CompanyId', models.CharField(blank=True, max_length=50, null=True, verbose_name='企业id')), + ('ParentId', models.CharField(blank=True, max_length=36, null=True, verbose_name='父类字典')), + ('CategoryType', models.CharField(blank=True, max_length=50, null=True, verbose_name='字典类别')), + ('DictionaryCode', models.CharField(blank=True, max_length=50, null=True, verbose_name='字典类别')), + ('FullName', models.CharField(blank=True, max_length=50, null=True, verbose_name='字典名称')), + ('Remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')), + ('IsTree', models.BooleanField(blank=True, null=True, verbose_name='是否是树结构')), + ('IsEnabled', models.BooleanField(blank=True, null=True, verbose_name='是否可用')), + ('SortNo', models.IntegerField(blank=True, null=True, verbose_name='排序')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('AddByName', models.CharField(blank=True, max_length=30, null=True, verbose_name='创建人名称')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('UpdateByName', models.CharField(blank=True, max_length=30, null=True, verbose_name='更新人名称')), + ('AddDate', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDate', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '字典', + 'verbose_name_plural': '字典', + 'db_table': 'common_datadictionary', + 'managed': False, + }, + ), + migrations.CreateModel( + name='DataDictionaryDetail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('DataDictionaryDetailId', models.CharField(blank=True, max_length=36, null=True, verbose_name='字典详情id')), + ('DataDictionaryId', models.CharField(blank=True, max_length=50, null=True, verbose_name='字典id')), + ('ParentId', models.CharField(blank=True, max_length=50, null=True, verbose_name='父类字典')), + ('ParentCode', models.CharField(blank=True, max_length=50, null=True, verbose_name='字典id')), + ('DictionaryCode', models.CharField(blank=True, max_length=50, null=True, verbose_name='键')), + ('DictionaryValue', models.CharField(blank=True, max_length=100, null=True, verbose_name='值')), + ('FullName', models.CharField(blank=True, max_length=300, null=True, verbose_name='字典名称')), + ('Remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='备注')), + ('IsTree', models.BooleanField(blank=True, null=True, verbose_name='是否是树结构')), + ('IsEnabled', models.BooleanField(blank=True, null=True, verbose_name='是否可用')), + ('SortNo', models.IntegerField(blank=True, null=True, verbose_name='排序')), + ('SortNoTwo', models.IntegerField(blank=True, null=True, verbose_name='查策网排序')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('AddByName', models.CharField(blank=True, max_length=30, null=True, verbose_name='创建人名称')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('UpdateByName', models.CharField(blank=True, max_length=30, null=True, verbose_name='更新人名称')), + ('AddDate', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDate', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '字典详情', + 'verbose_name_plural': '字典详情', + 'db_table': 'common_datadictionarydetail', + 'managed': False, + }, + ), + migrations.CreateModel( + name='OperationHistoryLog', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='操作记录id')), + ('des', models.CharField(blank=True, max_length=50, null=True, verbose_name='操作描述')), + ('detail', models.TextField(blank=True, null=True, verbose_name='操作详情')), + ('ip', models.CharField(blank=True, max_length=20, null=True, verbose_name='操作人IP')), + ('user_id', models.IntegerField(blank=True, null=True, verbose_name='操作人ID')), + ('username', models.CharField(blank=True, max_length=30, null=True, verbose_name='操作人名称')), + ('update_user_id', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('update_username', models.CharField(blank=True, max_length=30, null=True, verbose_name='更新人名称')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '操作日志记录', + 'verbose_name_plural': '操作日志记录', + 'db_table': 'operation_history', + 'managed': False, + }, + ), + ] diff --git a/apps/common/migrations/__init__.py b/apps/common/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/common/models.py b/apps/common/models.py new file mode 100644 index 0000000..b9fd2ca --- /dev/null +++ b/apps/common/models.py @@ -0,0 +1,215 @@ +from django.db import models + + +class DataDictionary(models.Model): + """ + 字典 + """ + DataDictionaryId = models.CharField(max_length=36, null=True, blank=True, verbose_name='字典id') + CompanyId = models.CharField(max_length=50, null=True, blank=True, verbose_name='企业id') + ParentId = models.CharField(max_length=36, null=True, blank=True, verbose_name='父类字典') + CategoryType = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典类别') + DictionaryCode = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典类别') + FullName = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典名称') + Remark = models.CharField(max_length=200, null=True, blank=True, verbose_name='备注') + IsTree = models.BooleanField(null=True, blank=True, verbose_name="是否是树结构") + IsEnabled = models.BooleanField(null=True, blank=True, verbose_name="是否可用") + SortNo = models.IntegerField(null=True, blank=True, verbose_name="排序") + pid = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="父类字典") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + AddByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="创建人名称") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + UpdateByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="更新人名称") + AddDate = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDate = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + def __str__(self): + return self.FullName + + class Meta: + db_table = "common_datadictionary" + managed = False # False是不会为当前模型创建和删除数据表 + verbose_name = '字典' + verbose_name_plural = verbose_name + # app_label = 'chace' # 指定在chace数据库下创建数据表 + indexes = [ + models.Index(fields=['DataDictionaryId']), + models.Index(fields=['DictionaryCode']), + ] + + +class DataDictionaryDetail(models.Model): + """ + 字典详情 + """ + DataDictionaryDetailId = models.CharField(max_length=36, null=True, blank=True, verbose_name='字典详情id') + DataDictionaryId = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典id') + ParentId = models.CharField(max_length=50, null=True, blank=True, verbose_name='父类字典') + ParentCode = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典id') + DictionaryCode = models.CharField(max_length=50, null=True, blank=True, verbose_name='键') + DictionaryValue = models.CharField(max_length=100, null=True, blank=True, verbose_name='值') + FullName = models.CharField(max_length=300, null=True, blank=True, verbose_name='字典名称') + Remark = models.CharField(max_length=200, null=True, blank=True, verbose_name='备注') + IsTree = models.BooleanField(null=True, blank=True, verbose_name="是否是树结构") + IsEnabled = models.BooleanField(null=True, blank=True, verbose_name="是否可用") + SortNo = models.IntegerField(null=True, blank=True, verbose_name="排序") + SortNoTwo = models.IntegerField(null=True, blank=True, verbose_name="查策网排序") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + AddByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="创建人名称") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + UpdateByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="更新人名称") + AddDate = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDate = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + def __str__(self): + return self.FullName + + class Meta: + db_table = "common_datadictionarydetail" + managed = False # False是不会为当前模型创建和删除数据表 + verbose_name = '字典详情' + verbose_name_plural = verbose_name + # app_label = 'chace' # 指定在chace数据库下创建数据表 + indexes = [ + models.Index(fields=['DataDictionaryDetailId']), + models.Index(fields=['DataDictionaryId']), + models.Index(fields=['DictionaryCode']), + models.Index(fields=['DictionaryValue']), + models.Index(fields=['ParentCode']), + models.Index(fields=['IsEnabled']), + models.Index(fields=['ParentCode', 'IsEnabled']), + ] + + +class OperationHistoryLog(models.Model): + """ + 操作日志记录 + """ + id = models.IntegerField(primary_key=True, verbose_name='操作记录id') + des = models.CharField(max_length=50, null=True, blank=True, verbose_name="操作描述") + detail = models.TextField(null=True, blank=True, verbose_name="操作详情") + ip = models.CharField(max_length=20, null=True, blank=True, verbose_name="操作人IP") + user_id = models.IntegerField(null=True, blank=True, verbose_name="操作人ID") + username = models.CharField(max_length=30, null=True, blank=True, verbose_name="操作人名称") + update_user_id = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + update_username = models.CharField(max_length=30, null=True, blank=True, verbose_name="更新人名称") + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + def __str__(self): + return self.des + + class Meta: + db_table = "operation_history" + managed = False # False是不会为当前模型创建和删除数据表 + verbose_name = '操作日志记录' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['username']), + models.Index(fields=['des']), + models.Index(fields=['create_time']), + models.Index(fields=['detail']), + ] + + +class DataDictionaryDetailZY(models.Model): + """ + 字典详情 + """ + DataDictionaryDetailId = models.CharField(max_length=36, null=True, blank=True, verbose_name='字典详情id') + DataDictionaryId = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典id') + ParentId = models.CharField(max_length=50, null=True, blank=True, verbose_name='父类字典') + ParentCode = models.CharField(max_length=50, null=True, blank=True, verbose_name='字典id') + DictionaryCode = models.CharField(max_length=50, null=True, blank=True, verbose_name='键') + DictionaryValue = models.CharField(max_length=100, null=True, blank=True, verbose_name='值') + FullName = models.CharField(max_length=300, null=True, blank=True, verbose_name='字典名称') + Remark = models.CharField(max_length=200, null=True, blank=True, verbose_name='备注') + IsTree = models.BooleanField(null=True, blank=True, verbose_name="是否是树结构") + IsEnabled = models.BooleanField(null=True, blank=True, verbose_name="是否可用") + SortNo = models.IntegerField(null=True, blank=True, verbose_name="排序") + SortNoTwo = models.IntegerField(null=True, blank=True, verbose_name="查策网排序") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + AddByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="创建人名称") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + UpdateByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="更新人名称") + AddDate = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDate = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + def __str__(self): + return self.FullName + + class Meta: + db_table = "common_datadictionarydetail" + managed = False # False是不会为当前模型创建和删除数据表 + verbose_name = '字典详情' + verbose_name_plural = verbose_name + app_label = 'chace' # 指定在chace数据库下创建数据表 + + +class Area(models.Model): + """ + 地区 + """ + DataDictionaryDetailId = models.CharField(max_length=36, default="", verbose_name="字典详情id") + Code = models.CharField(max_length=10, null=True, blank=True, verbose_name="地区编号") + Abbreviation = models.CharField(max_length=50, null=True, blank=True, verbose_name="地区简称") + TopImg = models.CharField(max_length=500, null=True, blank=True, verbose_name="封面图") + BaiDuCode = models.CharField(max_length=30, null=True, blank=True, verbose_name="百度地图编号") + IsMunicipality = models.BooleanField(null=True, blank=True, verbose_name="是否自辖市") + IsProvince = models.BooleanField(null=True, blank=True, verbose_name="是否省级") + IsCity = models.BooleanField(null=True, blank=True, verbose_name="是否市级") + IsHotCity = models.BooleanField(null=True, blank=True, verbose_name="是否热门城市") + IsTownship = models.BooleanField(null=True, blank=True, verbose_name="是否区/乡镇") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人ID") + CreateByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="创建人名称") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人ID") + UpdateByName = models.CharField(max_length=30, null=True, blank=True, verbose_name="更新人名称") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, + verbose_name="更新时间") # 该字段仅在调用 Model.save() 时自动更新。在以其他方式(例如 QuerySet.update())更新其他字段时,不会更新该字段,但可以在此类更新中为字段指定自定义值。 + + def __str__(self): + return self.Abbreviation + + class Meta: + managed = False # False是不会为当前模型创建和删除数据表 + verbose_name = "地区" + verbose_name_plural = verbose_name + ordering = ["id"] + + +class Archive(models.Model): + """ + 用户上传的资料管理 + """ + MainId = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件唯一标识", help_text="文件唯一标识") + Name = models.CharField(max_length=255, null=True, blank=True, verbose_name="用户上传的文件名称", help_text="用户上传的文件名称") + Ext = models.CharField(max_length=10, null=True, blank=True, verbose_name="文件扩展名", help_text="文件扩展名") + ProjectCodes = models.TextField(null=True, blank=True, verbose_name="绑定项目code", help_text="绑定项目code") # 可能绑定多个项目 + Label = models.IntegerField(null=True, blank=True, verbose_name="类型标签", help_text="类型标签 类型:1-文档 2-pdf 3-excel 4-图片 5-ppt 6-mp4") + Url = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件路径", help_text="文件路径") + Remark = models.CharField(max_length=255, null=True, blank=True, verbose_name='备注') + Global = models.IntegerField(null=True, blank=True, verbose_name="是否后台管理员上传全局查阅资料 # 1:是 2:不是") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + issuingDepartment = models.CharField(max_length=200, null=True, blank=True, verbose_name="发文部门") + dateOfPublication = models.DateField(null=True, blank=True, verbose_name="发文时间") + originalLink = models.CharField(null=True, blank=True, max_length=2000, verbose_name="原文链接") + digest = models.TextField(null=True, blank=True, verbose_name="摘要") + title = models.CharField(null=True, blank=True, max_length=200, verbose_name="摘要") + + class Meta: + db_table = "common_archive" + managed = False # False是不会为当前模型创建和删除数据表 + verbose_name = '用户上传的资料管理' + verbose_name_plural = verbose_name + app_label = 'chace_rnd' # 指定在chace数据库下创建数据表 diff --git a/apps/common/serializers/__init__.py b/apps/common/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/common/serializers/archive_serializer.py b/apps/common/serializers/archive_serializer.py new file mode 100644 index 0000000..a3a0826 --- /dev/null +++ b/apps/common/serializers/archive_serializer.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from common.models import Archive + + +class ArchiveSerializer(serializers.ModelSerializer): + """ + 用户上传资料管理序列化 + """ + + class Meta: + model = Archive + fields = "__all__" \ No newline at end of file diff --git a/apps/common/serializers/dict_serializer.py b/apps/common/serializers/dict_serializer.py new file mode 100644 index 0000000..e02089c --- /dev/null +++ b/apps/common/serializers/dict_serializer.py @@ -0,0 +1,57 @@ +from rest_framework import serializers +from ..models import DataDictionary, DataDictionaryDetail + + +class DictSerializer(serializers.ModelSerializer): + ''' + 字典序列化 + ''' + id = serializers.IntegerField(label='id', read_only=True) + dict_id = serializers.CharField(max_length=50, source='DataDictionaryId') + label = serializers.CharField(max_length=50, source='FullName') + parent_id = serializers.CharField(max_length=50, required=False, allow_blank=True, allow_null=True, source='ParentId') + dict_code = serializers.CharField(max_length=50, required=False, allow_blank=True, allow_null=True, source='DictionaryCode') + remark = serializers.CharField(required=False, allow_blank=True, allow_null=True, source='Remark') + is_enabled = serializers.BooleanField(required=False, label='是否可用', source='IsEnabled') + + create_id = serializers.IntegerField(label='创建人id', required=False, source='CreateByUid') + create_user = serializers.CharField(label='创建人', max_length=50, required=False, source="AddByName") + create_date = serializers.DateTimeField(label='创建时间', required=False, source="AddDate") + update_id = serializers.IntegerField(label='修改人id', required=False, source='UpdateByUid') + update_user = serializers.CharField(label='更新人', max_length=50, required=False, source="UpdateByName") + update_date = serializers.DateTimeField(label='更新时间', required=False, source="UpdateDate") + + class Meta: + model = DataDictionary + fields = ['id', 'dict_id', 'label', 'parent_id', 'dict_code', 'is_enabled', 'remark','create_id', 'create_user', + 'create_date', 'update_id', 'update_user', 'update_date'] + + +class DictDetailSerializer(serializers.ModelSerializer): + ''' + 字典详情序列化 + ''' + id = serializers.IntegerField(label='id', read_only=True) + dict_id = serializers.CharField(label='字典id', max_length=50, required=False, allow_blank=True, allow_null=True, source='DataDictionaryId') + dict_detail_id = serializers.CharField(label='字典详情id', max_length=50, required=False, allow_blank=True, allow_null=True, source='DataDictionaryDetailId') + label = serializers.CharField(label='字典名称', max_length=300, allow_null=True, source='FullName') + remark = serializers.CharField(label='备注', max_length=300, required=False, allow_blank=True, allow_null=True, source='Remark') + parent_id = serializers.CharField(label='父类字典', required=False, allow_blank=True, allow_null=True, max_length=50, source='ParentId') + dict_code = serializers.CharField(label='键', max_length=50, source='DictionaryCode') + dict_val = serializers.CharField(label='值', max_length=50, source='DictionaryValue') + parent_code = serializers.CharField(label='字典id', required=False, allow_blank=True, allow_null=True, max_length=50, source='ParentCode') + is_enabled = serializers.BooleanField(label='是否可用', required=False, source='IsEnabled') + sort_no_two = serializers.IntegerField(label='查策网排序', required=False, source='SortNoTwo') + create_id = serializers.IntegerField(label='创建人id', required=False, source='CreateByUid') + create_user = serializers.CharField(label='创建人', max_length=50, required=False, source="AddByName") + create_date = serializers.DateTimeField(label='创建时间', required=False, source="AddDate") + update_id = serializers.IntegerField(label='修改人id', required=False, source='UpdateByUid') + update_user = serializers.CharField(label='更新人', max_length=50, required=False, source="UpdateByName") + update_date = serializers.DateTimeField(label='更新时间', required=False, source="UpdateDate") + + class Meta: + model = DataDictionaryDetail + fields = ['id', 'dict_id', 'dict_detail_id', 'label', 'remark', 'parent_id', + 'dict_code', 'dict_val', 'is_enabled', 'parent_code', 'create_id', 'create_user', + 'create_date', 'update_id', 'update_user', 'update_date', 'sort_no_two' + ] diff --git a/apps/common/tests.py b/apps/common/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/common/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/common/urls.py b/apps/common/urls.py new file mode 100644 index 0000000..cd4c240 --- /dev/null +++ b/apps/common/urls.py @@ -0,0 +1,25 @@ +from django.urls import path, include +from rest_framework import routers + +from common.views import dict, upload_file, archive +from common.views.area import NewRegisterAreaListAPIView +from common.views.history import HistoryLogAPIView + +router = routers.SimpleRouter() +router.register(r"dict/detail/list", dict.DataDictionaryDetailListViewSet, basename="dict_detail_list") +router.register(r"dict/detail", dict.DataDictionaryDetailTreeViewSet, basename="dict_detail") +router.register(r"dict", dict.DataDictionaryViewSet, basename="dict") +router.register(r"archive", archive.ArchiveViewSet, basename="archive") + +urlpatterns = [ + path(r"api/", include(router.urls)), + path(r'api/att/file/upload/', upload_file.UploadFileAPIView.as_view(), name='upload_file'), # 上传附件 + path(r'api/file/download/', upload_file.DownLoadFileAPIView.as_view(), name='download_file'), + path(r'api/file/delete/', upload_file.DeleteFileAPIView.as_view(), name='delete_file'), + path(r'api/history/list/', HistoryLogAPIView.as_view(), name='history_list'), # 操作日志 + path(r'api/dict/zy/detail/', dict.DataDictionaryDetailTreeViewSetZY.as_view({'get': 'list'}), + name='dict_detail_zy'), # 操作日志 + path(r'api/register/area/newlist/', NewRegisterAreaListAPIView.as_view(), name='area_list'), # 去除区县 + path(r"api/break/point/upload/", archive.BreakPointUploadAPIView.as_view(), name="break_point_upload"), + path(r"api/merge/file/", archive.MergeFile.as_view(), name="merge_file"), +] diff --git a/apps/common/views.py b/apps/common/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/common/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/common/views/__init__.py b/apps/common/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/common/views/archive.py b/apps/common/views/archive.py new file mode 100644 index 0000000..e8ba5e4 --- /dev/null +++ b/apps/common/views/archive.py @@ -0,0 +1,959 @@ +import datetime +import json +import logging +import os +import shutil +import time +import traceback +import uuid +from concurrent.futures import ThreadPoolExecutor +from urllib import parse + +from django.db import transaction +from django.http import HttpResponse, FileResponse +from hashids import Hashids +from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import BAD, SERVER_ERROR +from ChaCeRndTrans.settings import ID_KEY, FILE_PATH, FILE_HTTP, MEDIA_ROOT, MAX_MP4_FILE_SIZE +from common.models import Archive, OperationHistoryLog +from common.serializers.archive_serializer import ArchiveSerializer +from utils.custom import CommonPagination, RbacPermission, AESEnDECryptRelated, req_operate_by_user, asyncDeleteFile, \ + generate_random_str_for_fileName, generate_random_str, create_operation_history_log + +from django.core.files.storage import default_storage + +from rest_framework.decorators import action + +from django.db import connection + +from rbac.models import UserProfile + +err_logger = logging.getLogger('error') + +# 创建线程池 +threadPool = ThreadPoolExecutor(max_workers=10, thread_name_prefix="test_") + + +class ArchiveViewSet(ModelViewSet): + """ + 用户上传的资料管理 + """ + perms_map = ({"*": "admin"}, {"*": "comadmin"}, {"*": "archive_all"}, {"get": "archive_list"}, {"post": "archive_create"}, {"put": "archive_update"}, + {"delete": "archive_delete"}) + queryset = Archive.objects.all() + serializer_class = ArchiveSerializer + pagination_class = CommonPagination + filter_backends = (OrderingFilter, SearchFilter) + ordering_fields = ("create_time",) + # search_fields = ("Name", "ProjectCodes", ) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated, RbacPermission) + + def get_object(self): + """ + 重写get_object获取对象函数 + @return: + """ + pk = self.kwargs.get('pk') + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + decode_id = hashids.decode(pk) + if not decode_id: + err_logger.error("decode id failed, id: \n%s" % pk) + return CCAIResponse("invalid ID", BAD) + # 使用id 获取对象 + obj = self.queryset.get(id=decode_id[0]) + except Exception as e: + err_logger.error("get object failed, id: \n%s" % pk) + return CCAIResponse("invalid ID", BAD) + # 检查对象是否存在 + self.check_object_permissions(self.request, obj) + return obj + + def list(self, request, *args, **kwargs): + """ + 获取上传文件列表 + :param request: + :param args: + :param kwargs: + :return: + """ + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + params = request.GET + page_size = params.get('size', None) + page = params.get('page', None) + companyMid = params.get('companyMid', None) + ProjectCodes = params.get('ProjectCodes', None) + title = params.get('title', None) + Label = params.get('Label', None) + pagination = {} + adminUserId = UserProfile.objects.filter(roles__id=1).first() # 查询管理员用户id + + sql = r''' + SELECT a.id, a.MainId, a.Name, a.Ext, a.ProjectCodes, a.Label, a.Url, a.Remark, a.Global, + a.Label, a.CreateBy, a.UpdateBy, a.CreateByUid, a.UpdateByUid, + a.CreateDateTime, a.UpdateDateTime, a.companyMid, a.title + FROM common_archive a + WHERE 1=1 + ''' + count_sql = r'''SELECT a.id FROM common_archive a WHERE 1=1 ''' + where_params = [] + + if companyMid: + temp = " AND a.companyMid = '{}' ".format(companyMid) + sql = sql + temp + count_sql = count_sql + temp + # where_params.append(companyMid) + else: + return CCAIResponse("参数缺失!", BAD) + # 可能有多个项目 + if ProjectCodes: + temp = ' AND ( ' + project_codes_list = ProjectCodes.split(',') + for index, code in enumerate(project_codes_list): + if index == len(project_codes_list) - 1: + temp = temp + " FIND_IN_SET('{}', a.ProjectCodes) ".format(code) + else: + temp = temp + " FIND_IN_SET('{}', a.ProjectCodes) OR ".format(code) + temp = temp + ' ) ' + sql = sql + temp + count_sql = count_sql + temp + # temp = " AND FIND_IN_SET('{}', a.ProjectCodes) ".format(ProjectCodes + # where_params.append(ProjectCodes) + + if title: + temp = r''' AND a.title like %s ''' + sql = sql + temp + count_sql = count_sql + temp + where_params.append('%' + title + '%') + + if Label: + temp = r''' AND a.Label = '{}' '''.format(Label) + sql = sql + temp + count_sql = count_sql + temp + + # 每页最多20条 + if page_size: + if int(page_size) > 20: + err_logger.error("user: %s, page size over failed, size: \n%s" % (request.user.id, page_size)) + page_size = 20 + else: + page_size = 20 + + if page: + if int(page) > 2: + if request.user.id is None: + err_logger.error("user: %s, page over failed, size: \n%s" % ( + request.user.id, page)) + page = 1 + page_size = 20 + else: + page = 1 + start_index = (int(page) - 1) * int(page_size) + # 管理员上传的文件,所有用户可以查看 + # sql += " OR a.Global = {} ".format(1) + # count_sql += " OR a.Global = {} ".format(1) + # sql += " OR a.CreateByUid = {} ".format(adminUserId.id) + # count_sql += " OR a.CreateByUid = {} ".format(adminUserId.id) + count_sql = count_sql + ' ORDER BY {} desc'.format('a.id') + sql += ' ORDER BY {} desc LIMIT {}, {} '.format('a.CreateDateTime', start_index, page_size) + queryset = Archive.objects.raw(sql, where_params) + # 查看总数 + count = 0 + count_result = Archive.objects.raw(count_sql, where_params) + count = len(count_result) + # 返回分页结果 + rows = [] + for item in queryset: + item.__dict__.pop('_state') + item.__dict__['CreateDateTime'] = item.__dict__['CreateDateTime'].strftime('%Y-%m-%d %H:%M:%S') + item.__dict__['UpdateDateTime'] = item.__dict__['UpdateDateTime'].strftime('%Y-%m-%d %H:%M:%S') + item.__dict__['id'] = hashids.encode(item.__dict__['id']) + if item.__dict__['Url']: + media_folder = FILE_PATH.replace("\\", "/").split("/")[-1] + if media_folder == "media": # 开发时 + item.__dict__['Url'] = FILE_HTTP + parse.quote( + item.__dict__['Url'].replace("upload", "media")) + else: # 线上 + item.__dict__['Url'] = FILE_HTTP + parse.quote(item.__dict__['Url']) + rows.append(item.__dict__) + pagination['page'] = page + pagination['page_size'] = page_size + # encrypt_instance = AESEnDECryptRelated() + # new_ciphertext = encrypt_instance.start_encrypt(rows) + # print("e1:", new_ciphertext) + return CCAIResponse(data=rows, count=count, pagination=pagination) + + except Exception as e: + err_logger.error("user: %s, get archive list failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("获取文件列表失败", status=500) + + def retrieve(self, request, *args, **kwargs): + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + archive_id = kwargs.get('pk') + archive_id = hashids.decode(archive_id)[0] + archive = get_object_or_404(self.queryset, pk=int(archive_id)) + data = self.get_serializer(archive, many=False) + return CCAIResponse(data=data) + + except Exception as e: + err_logger.error("user: %s, get archive detail failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("获取文件详情失败", status=500) + + def update(self, request, *args, **kwargs): + """ + 修改已经上传的文件信息 + @param request: + @param args: + @param kwargs: + @return: + """ + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + data = req_operate_by_user(request=request) + if not data['title']: + return CCAIResponse("Missing title", BAD) + # if not data['ProjectCodes']: + # return CCAIResponse("Missing ProjectCodes", BAD) + if data['ProjectCodes'] and len(data['ProjectCodes']) > 0: + data['ProjectCodes'] = ','.join(data['ProjectCodes']) + else: + data['ProjectCodes'] = None + if not data['Label']: + return CCAIResponse("Missing Label", BAD) + if not request.user.is_superuser and request.user.id != data['CreateByUid']: + return CCAIResponse("不能修改他人文件", BAD) + data['UpdateByUid'] = request.user.id + data['UpdateDateTime'] = datetime.datetime.now() + data['UpdateBy'] = request.user.name + archive_id = kwargs.get('pk') + archive_id = hashids.decode(archive_id)[0] + data['id'] = archive_id + partial = kwargs.pop('partial', False) # True:所有字段全部更新, False:仅更新提供的字段 + # instance = self.get_object(archive_id) + instance = self.get_object() + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + # 操作记录 + info = { + "des": "更新文档", + "detail": "文档名称: " + data["Name"] + } + create_operation_history_log(request, info, OperationHistoryLog) + + return CCAIResponse(data="success") + + except Exception as e: + err_logger.error("user: %s, update article failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("更新文章失败", status=500) + + def create(self, request, *args, **kwargs): + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + data = req_operate_by_user(request=request) + data = req_operate_by_user(request) + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + # 操作记录 + info = { + "des": "非法创建文章", + "detail": "文章名称: " + params["title"] + } + create_operation_history_log(request, info, OperationHistoryLog) + return CCAIResponse(data="success") + except Exception as e: + err_logger.error("user: %s, create archive failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("创建文章失败", status=500) + + def destroy(self, request, *args, **kwargs): + """ + 删除文件 + @param request: + @param args: + @param kwargs: + @return: + """ + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + userid = request.user.id + is_superuser = request.user.is_superuser + archive_id = kwargs.get('pk') + archive_id = hashids.decode(archive_id)[0] + url_path = None + with transaction.atomic(): + instance = self.get_object() + url_path = instance.Url + self.perform_destroy(instance) + + # 通常在文件数据不是非常多的情况下,可以认为编码与文件是一对一,即编码唯一 + # 文件存放路径 + D_FILE_PATH = FILE_PATH.replace("\\", "/") + upload_folder = D_FILE_PATH.split("/")[-1] + if upload_folder == "media": # 开发时 + DEL_FILE_PATH = "/".join(D_FILE_PATH.split("/")[0:-1]) # 去掉"media", 因为url里面存在"upload" + url_path = url_path.replace("upload", "media") + else: # 线上 + DEL_FILE_PATH = D_FILE_PATH + url_path = url_path + + # 删除物理文件 DEL_FILE_PATH + url_path:文件完整的路径 + if os.path.exists(DEL_FILE_PATH + url_path): + # os.remove(url_path) # 直接删除文件,考虑到大文件删除比较慢,所以使用异步删除 + try: + threadPool.submit(asyncDeleteFile, request, DEL_FILE_PATH + url_path) + except Exception as e: + err_logger.error("user: %s, local delete archive failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("删除文件资源失败", status=200) + else: + err_logger.error("user: %s, want to delete archive dosen't exist, archive's name: \n%s" % ( + request.user.id, instance.Name)) + + info = { + "des": "删除文章", + "detail": "文件名称: " + instance.Name + } + create_operation_history_log(request, info, OperationHistoryLog) + return CCAIResponse(msg="删除成功!", status=200) + except Exception as e: + err_logger.error("user: %s, delete article failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("删除用户上传文件资源失败", status=500) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getGlobal", url_name="getGlobal") + def getGlobal(self, request, *args, **kwargs): + """ + 获取管理员上传的全局文件列表 + :param request: + :param args: + :param kwargs: + :return: + """ + try: + hashids = Hashids(salt=ID_KEY, min_length=32) + params = request.GET + page_size = params.get('size', None) + page = params.get('page', None) + companyMid = params.get('companyMid', None) + ProjectCodes = params.get('ProjectCodes', None) + title = params.get('title', None) + Label = params.get('Label', None) + pagination = {} + adminUserId = UserProfile.objects.filter(roles__id=1).first() # 查询管理员用户id + + sql = r''' + SELECT a.id, a.MainId, a.Name, a.Ext, a.ProjectCodes, a.Label, a.Url, a.Remark, a.Global, + a.Label, a.CreateBy, a.UpdateBy, a.CreateByUid, a.UpdateByUid, + a.CreateDateTime, a.UpdateDateTime, a.companyMid, + a.issuingDepartment, a.dateOfPublication, a.originalLink, a.digest, a.title + FROM common_archive a + WHERE 1=1 + ''' + count_sql = r'''SELECT a.id FROM common_archive a WHERE 1=1 ''' + where_params = [] + + if title: + temp = r''' AND a.title like %s ''' + sql = sql + temp + count_sql = count_sql + temp + where_params.append('%' + title + '%') + + # 每页最多20条 + if page_size: + if int(page_size) > 20: + err_logger.error("user: %s, page size over failed, size: \n%s" % (request.user.id, page_size)) + page_size = 20 + else: + page_size = 20 + + if page: + if int(page) > 2: + if request.user.id is None: + err_logger.error("user: %s, page over failed, size: \n%s" % ( + request.user.id, page)) + page = 1 + page_size = 20 + else: + page = 1 + start_index = (int(page) - 1) * int(page_size) + # 管理员上传的文件,所有用户可以查看 + sql += " AND a.Global = {} ".format(1) + count_sql += " AND a.Global = {} ".format(1) + # sql += " OR a.CreateByUid = {} ".format(adminUserId.id) + # count_sql += " OR a.CreateByUid = {} ".format(adminUserId.id) + count_sql = count_sql + ' ORDER BY {} desc'.format('a.id') + sql += ' ORDER BY {} desc LIMIT {}, {} '.format('a.CreateDateTime', start_index, page_size) + queryset = Archive.objects.raw(sql, where_params) + # 查看总数 + count = 0 + count_result = Archive.objects.raw(count_sql, where_params) + count = len(count_result) + # 返回分页结果 + rows = [] + for item in queryset: + item.__dict__.pop('_state') + item.__dict__['CreateDateTime'] = item.__dict__['CreateDateTime'].strftime('%Y-%m-%d %H:%M:%S') + item.__dict__['UpdateDateTime'] = item.__dict__['UpdateDateTime'].strftime('%Y-%m-%d %H:%M:%S') + if item.__dict__['dateOfPublication']: + item.__dict__['dateOfPublication'] = item.__dict__['dateOfPublication'].strftime('%Y-%m-%d') + item.__dict__['id'] = hashids.encode(item.__dict__['id']) + if item.__dict__['Url']: + media_folder = FILE_PATH.replace("\\", "/").split("/")[-1] + if media_folder == "media": # 开发时 + item.__dict__['Url'] = FILE_HTTP + parse.quote( + item.__dict__['Url'].replace("upload", "media")) + else: # 线上 + item.__dict__['Url'] = FILE_HTTP + parse.quote(item.__dict__['Url']) + rows.append(item.__dict__) + pagination['page'] = page + pagination['page_size'] = page_size + # encrypt_instance = AESEnDECryptRelated() + # new_ciphertext = encrypt_instance.start_encrypt(rows) + # print("e1:", new_ciphertext) + return CCAIResponse(data=rows, count=count, pagination=pagination) + + except Exception as e: + err_logger.error("user: %s, get archive list failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("获取文件列表失败", status=500) + + + +class BreakPointUploadAPIView(APIView): + """ + 音频、视屏、PDF上传接口(断点续传,BreakPoint组件接口) + """ + + perms_map = ({"*": "admin"}, {"*": "comadmin"}, {"*", "archive_all"}, {"post": "archive_create"}, {"put": "archive_update"}) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated, RbacPermission) + + def post(self, request): + """ + 备查文件 上传接口 包含: 断点续传,或者直传,两种方式 + @param request: + @return: + """ + try: + params = request.data + hash_name = params.get("hashName", None) + hashids = Hashids(salt=ID_KEY, min_length=32) + type_ = params.get("type", 1) # 类型:1-文档 2-pdf 3-excel 4-图片 5-ppt 6-mp4 + if type_ is None: + return CCAIResponse("Miss hashName", status=BAD) + type_map = {1: 'doc', 2: 'pdf', 3: 'excel', 4: 'pic', 5: 'ppt', 6: 'mp4'} + typeName = type_map.get(int(type_), 'doc') # 文件类型目录名 + file_name = params.get("fileName", None) + ProjectCodes = params.get("ProjectCodes", None) # 绑定的项目ID,可能同时绑定多个项目 + Remark = params.get("Remark", None) # 备注 + companyMid = request.GET.get('companyMid', None) # 公司MainID + title = params.get('title', None) + last_dot_index = title.rfind('.') + if not title: + return CCAIResponse("missing fileName", status=BAD) + if last_dot_index != -1: + title = title[:last_dot_index] + else: + return title # 如果没有找到 '.',则返回原字符串 + + if companyMid is None: + return CCAIResponse("Miss companyMid", status=BAD) + if ProjectCodes and ProjectCodes !='' and len(ProjectCodes.split(',')) > 0: + ProjectCodes = ProjectCodes + else: + ProjectCodes = None + + if file_name is None: + return CCAIResponse("Miss fileName", status=BAD) + # adminUser = UserProfile.objects.filter(roles__id=1).first() # 查询管理员用户 + # Gobal 是否后台管理员上传全局查看资料 管理员为 1 + Global = 2 + if request.user.is_superuser: + Global = 1 + + # 将媒体文件统一放到upload下面 + D_FILE_PATH = FILE_PATH.replace("\\", "/") + media_folder = D_FILE_PATH.split("/")[-1] + if media_folder != "media": # 线上存储路径 + NEW_FILE_PATH = os.path.join(D_FILE_PATH, "upload") + if not os.path.exists(NEW_FILE_PATH): + os.makedirs(NEW_FILE_PATH) + else: # 本地开发时用到的路径 + NEW_FILE_PATH = D_FILE_PATH + + file_dir = os.path.join(NEW_FILE_PATH, "hash") # 存放hash文件的总目录 + + if hash_name: + # 断点续传 + file_folder_hash, current_chunk = hash_name.split( + "_") # 每一个文件块的文件名,例如:313b0fe8c583ab4f6e1ef4747e501f9f_0.mp3 + + file_folder_hash = str(request.user.id) + file_folder_hash # hash文件夹名称,加id主要防止不同用户上传一模一样的文件 + + h_file_name = os.path.splitext(file_name)[0] # file_name.split(".")[0] # 文件名(不包含扩展) + ext = os.path.splitext(file_name)[1].replace(".", "") # file_name.split(".")[-1] # 扩展 + hash_dir = os.path.join(file_dir, ext, h_file_name, file_folder_hash) # 用hash名命名的hash文件夹,存放hash文件块 + if not os.path.exists(hash_dir): + os.makedirs(hash_dir) + + files = os.listdir(hash_dir) + count = len(files) + if int(current_chunk) + 1 < count: + # 判断里面的文件是否连续,因为有些过期文件可能被定时任务删除了 + file_index = [] + for each_file in files: + index_str = each_file.split("_")[-1] + file_index.append(int(index_str.split(".")[0])) # 存放每个文件的下标 + file_index.sort() # 将下标进行排序 + compare_list = [] # 用来记录期望的文件下标 + for each_num in range(len(file_index)): + compare_list.append(each_num) + # 开始判断期望的文件下标数组是否和已经存在的文件下标数组相等 + if file_index == compare_list: + nextIndex = count + else: + shutil.rmtree(hash_dir) # 里面的文件不连续,先删除然后再新建一个一样的文件夹 + os.makedirs(hash_dir) + nextIndex = int(current_chunk) + 1 + file_chunk = request.FILES.get('file', '') # 获取每一个文件块 + save_path = os.path.join(hash_dir, hash_name + "." + ext) # 文件块存储路径 + file = open(save_path, "wb") + file.write(file_chunk.read()) + file.close() + + else: + nextIndex = int(current_chunk) + 1 + file_chunk = request.FILES.get('file', '') # 获取每一个文件块 + save_path = os.path.join(hash_dir, hash_name + "." + ext) # 文件块存储路径 + file = open(save_path, "wb") + file.write(file_chunk.read()) + file.close() + data = { + "code": 200, + "message": "成功", + "nextIndex": nextIndex + } + return HttpResponse(json.dumps(data), content_type="application/json") + + else: + # 直传 + file_obj_list = request.FILES.getlist('file') + if not file_obj_list: + return CCAIResponse("文件为空", BAD) + # tags = params.get("tags", None) + # roles = params['roles'] + # public = params.get("public", 1) + # is_download = params.get("is_download", 1) + type_ = params.get("type", 1) # 类型:1-文档 2-pdf 3-excel 4-图片 5-ppt 6-mp4 + type_map = {1: 'doc', 2: 'pdf', 3: 'excel', 4: 'pic', 5: 'ppt', 6: 'mp4'} + typeName = type_map.get(int(type_), 'doc') # 文件类型目录名 + # importance = params.get("importance", 1) + + err_data = { + "code": 500, + "message": "参数错误", + } + + suc_data = { + "code": 200, + "message": "成功" + } + + is_limit_size = (file_obj_list[0].size / 1024 / 1024) < MAX_MP4_FILE_SIZE # 不超过100M + if not is_limit_size: + data = { + "code": 500, + "message": "不允许上传超多100M的文件!", + } + return HttpResponse(json.dumps(data), content_type="application/json") + + import time + ct = time.time() # 取得系统时间 + local_time = time.localtime(ct) # 将系统时间转成结构化时间 + date_head = time.strftime("%Y%m%d", local_time) # 格式化时间 + date_m_secs = str(datetime.datetime.now().timestamp()).split(".")[-1] # 毫秒级时间戳 + time_stamp = "%s%.3s" % (date_head, date_m_secs) # 拼接时间字符串 + + if not os.path.exists(os.path.join(NEW_FILE_PATH, typeName)): + os.makedirs(os.path.join(NEW_FILE_PATH, typeName)) + save_dir = os.path.join(NEW_FILE_PATH, typeName, date_head) + # 如果不存在则创建目录 + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + if file_name == "" or file_name is None: + return HttpResponse(json.dumps(err_data), content_type="application/json") + ext = os.path.splitext(file_name)[1].replace(".", "") # file_name.split(".")[-1] # 文件扩展名 + title = "无标题" + # 打开文件,没有新增 + for each in file_obj_list: + random_name = time_stamp + generate_random_str_for_fileName() + "." + ext + file_path = os.path.join(save_dir, random_name) # 文件存储路径 + destination = open(file_path, 'wb') + for chunk in each.chunks(): + destination.write(chunk) + destination.close() + title = os.path.splitext(each.name)[0] # "".join(each.name.split(".")[0:-1]) # 使用音频名字作为标题 + url = os.path.join('/upload', typeName, date_head, random_name) + url = url.replace("\\", "/") + # 将数据写入数据库article表 + Archive.objects.create( + MainId=uuid.uuid4().__str__(), + Name=title, + Ext=ext, + ProjectCodes=ProjectCodes, + Label=int(type_), + Url=url, + Remark=Remark, + Global=Global, + CreateBy=request.user.name, + UpdateBy=request.user.name, + CreateByUid=request.user.id, + UpdateByUid=request.user.id, + CreateDateTime=datetime.datetime.now(), + UpdateDateTime=datetime.datetime.now(), + companyMid=companyMid, + title=title + ) + # 操作记录 + if int(type_) == 1: + info = { + "des": "上传文档", + "detail": "文档名称: " + title + } + elif int(type_) == 2: + info = { + "des": "上传PDF文件", + "detail": "PDF文件名称: " + title + } + elif int(type_) == 3: + info = { + "des": "上传Excel文件", + "detail": "Excel文件名称: " + title + } + elif int(type_) == 4: + info = { + "des": "上传图片", + "detail": "图片名称: " + title + } + else: + info = { + "des": "上传PPT文件", + "detail": "PPT文件名称: " + title + } + create_operation_history_log(request, info, OperationHistoryLog) + + return HttpResponse(json.dumps(suc_data), content_type="application/json") + + except Exception as e: + err_logger.error( + "user: %s, break point upload file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("断点续传上传失败", status=500) + + +class MergeFile(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + """ + 合并文件(断点续传, BreakPoint组件接口) + """ + + def get(self, request): # 合并文件 + try: + params = request.GET + err_data = { + "code": 500, + "message": "参数错误", + "state": 0 + } + + hashids = Hashids(salt=ID_KEY, min_length=32) + + hash_name = params.get("hashName", None) + file_name = params.get("fileName", None) + file_total_length = params.get("chunks", 0) + type_ = params.get("type", None) + type_map = {1: 'doc', 2: 'pdf', 3: 'excel', 4: 'pic', 5: 'ppt', 6: 'mp4'} + path_type = type_map.get(int(type_), 'doc') # 文件类型目录名 + ProjectCodes = params.get("ProjectCodes", None) + Remark = params.get("Remark", None) + companyMid = params.get("companyMid", None) + title = params.get('title', None) + last_dot_index = title.rfind('.') + if not title: + return CCAIResponse("missing fileName", status=BAD) + if last_dot_index != -1: + title = title[:last_dot_index] + else: + return title # 如果没有找到 '.',则返回原字符串 + + if not file_name: + return CCAIResponse("missing fileName", status=BAD) + # if ProjectCodes is None: + # return CCAIResponse("Miss ProjectCodes", status=BAD) + if not companyMid: + return CCAIResponse("Miss companyMid", status=BAD) + # Gobal 是否后台管理员上传全局查看资料 管理员为 1 + Global = 2 + if request.user.is_superuser: + Global = 1 + + # 将媒体文件统一放到upload下面 + D_FILE_PATH = FILE_PATH.replace("\\", "/") + media_folder = D_FILE_PATH.split("/")[-1] + if media_folder != "media": # 线上存储路径 + NEW_FILE_PATH = os.path.join(D_FILE_PATH, "upload") + if not os.path.exists(NEW_FILE_PATH): + os.makedirs(NEW_FILE_PATH) + else: # 本地开发时用到的路径 + NEW_FILE_PATH = D_FILE_PATH + + file_dir = os.path.join(NEW_FILE_PATH, "hash") # 存放hash文件的总目录 + + h_file_name, ext = os.path.splitext(file_name) # file_name.split(".") # 文件名, 扩展 + ext = ext.replace(".", "") + ext_name = "." + ext # 扩展名 + file_dir_hash = os.path.join(file_dir, ext, h_file_name) # hash文件下与文件名同名字的文件夹 + hash_name_new = str(request.user.id) + hash_name + hash_dir = os.path.join(file_dir_hash, hash_name_new) # 当前文件hash存储路径 + + import time + ct = time.time() # 取得系统时间 + local_time = time.localtime(ct) # 将系统时间转成结构化时间 + date_head = time.strftime("%Y%m%d", local_time) # 格式化时间 + date_m_secs = str(datetime.datetime.now().timestamp()).split(".")[-1] # 毫秒级时间戳 + time_stamp = "%s%.3s" % (date_head, date_m_secs) # 拼接时间字符串 + + if not os.path.exists(os.path.join(NEW_FILE_PATH, path_type)): + os.makedirs(os.path.join(NEW_FILE_PATH, path_type)) + save_dir = os.path.join(NEW_FILE_PATH, path_type, date_head) + # 如果不存在则创建目录 + if not os.path.exists(save_dir): # 合并后的文件存储目录 + os.makedirs(save_dir) + + random_name = time_stamp + generate_random_str() + ext_name + save_path = os.path.join(save_dir, random_name) # 合并后的文件存储路径 + count = len(os.listdir(hash_dir)) + if count != int(file_total_length): # 长度不相等,还未到达合并要求 + return HttpResponse(json.dumps({"state": 0}), content_type="application/json") + try: + temp = open(save_path, 'wb') # 创建新文件 + for i in range(0, count): + fp = open(hash_dir + "/" + hash_name + "_" + str(i) + ext_name, 'rb') # 以二进制读取分割文件 + temp.write(fp.read()) # 写入读取数据 + fp.close() + temp.close() + except Exception as e: + temp.close() # 在合并文件块失败的时候关闭临时合并的文件 + os.remove(save_path) # 删除临时合并的文件 + raise Exception + shutil.rmtree(hash_dir) # 删除 + self.judge_folder_null(file_dir_hash) + + title = h_file_name # 使用文件名字作为标题 + url = os.path.join('/upload', path_type, date_head, random_name) + url = url.replace("\\", "/") + # 将数据写入数据库Archive表 + Archive.objects.create( + MainId=uuid.uuid4().__str__(), + Name=title, + Ext=ext, + ProjectCodes=ProjectCodes, + Label=int(type_), + Url=url, + Remark=Remark, + Global=Global, + CreateBy=request.user.name, + UpdateBy=request.user.name, + CreateByUid=request.user.id, + UpdateByUid=request.user.id, + CreateDateTime=datetime.datetime.now(), + UpdateDateTime=datetime.datetime.now(), + companyMid=companyMid, + title=title + ) + + data = { + "code": 200, + "message": "成功", + "state": 1 + } + # 操作记录 + if int(type_) == 1: + info = { + "des": "上传文档", + "detail": "文档名称: " + title + } + elif int(type_) == 2: + info = { + "des": "上传PDF文件", + "detail": "PDF文件名称: " + title + } + elif int(type_) == 3: + info = { + "des": "上传Excel文件", + "detail": "Excel文件名称: " + title + } + elif int(type_) == 4: + info = { + "des": "上传图片", + "detail": "图片名称: " + title + } + else: + info = { + "des": "上传ppt文件", + "detail": "ppt文件名称: " + title + } + create_operation_history_log(request, info, OperationHistoryLog) + return HttpResponse(json.dumps(data), content_type="application/json") + + except Exception as e: + err_logger.error( + "user: %s, break point merge file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("断点续传合并文件失败", status=500) + + # 判断文件夹是否为空 + def judge_folder_null(self, path): + files = os.listdir(path) # 查找路径下的所有的文件夹及文件 + if len(files) == 0: + shutil.rmtree(path) + + +class UploadImageAPIView(APIView): + """ + 富文本编辑器上传图片调用的接口 + """ + perms_map = ({"*": "admin"}, {"post": "article_create"}, {"put": "article_update"}) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated, RbacPermission) + + def post(self, request): + try: + # 允许的图片范围 + imgRange = [".png", ".jpg", ".jpeg", ".gif", ".jfif"] + + file_obj_list = request.FILES.getlist("file") + + is_limit_img = (file_obj_list[0].size / 1024 / 1024) < 5 # 不超过5M + if not is_limit_img: + return CCAIResponse("文件不许超过5M", status=500) + + ct = time.time() # 取得系统时间 + local_time = time.localtime(ct) # 将系统时间转成结构化时间 + date_head = time.strftime("%Y%m%d", local_time) # 格式化时间 + date_m_secs = str(datetime.datetime.now().timestamp()).split(".")[-1] # 毫秒级时间戳 + time_stamp = "%s%.3s" % (date_head, date_m_secs) # 拼接时间字符串 + + ext = "." + file_obj_list[0].name.split(".")[-1] + # 判断文件后缀是否符合要求 + img_flag = False + if ext in imgRange: + img_flag = True + + # 文件保存的位置 + if img_flag: + # 将媒体文件统一放到upload下面 + D_FILE_PATH = FILE_PATH.replace("\\", "/") + media_folder = D_FILE_PATH.split("/")[-1] + if media_folder != "media": # 线上存储路径 + NEW_FILE_PATH = os.path.join(D_FILE_PATH, "upload") + if not os.path.exists(NEW_FILE_PATH): + os.makedirs(NEW_FILE_PATH) + else: # 本地开发时用到的路径 + NEW_FILE_PATH = D_FILE_PATH + + if not os.path.exists(os.path.join(NEW_FILE_PATH, "images")): + os.makedirs(os.path.join(NEW_FILE_PATH, "images")) + save_dir = os.path.join(NEW_FILE_PATH, "images", date_head) + # 如果不存在则创建目录 + if not os.path.exists(save_dir): + os.makedirs(save_dir) + # 打开文件,没有新增 + for each in file_obj_list: + random_name = time_stamp + generate_random_str() + ext + file_path = os.path.join(save_dir, random_name) + destination = open(file_path, 'wb') + for chunk in each.chunks(): + destination.write(chunk) + destination.close() + url = os.path.join('/upload', "images", date_head, random_name) + url = url.replace("\\", "/") + # 操作记录 + info = { + "des": "富文本图片上传", + "detail": "图片地址: " + url + } + create_operation_history_log(request, info, OperationHistoryLog) + if media_folder != "media": # 线上存储路径 + url = FILE_HTTP + url + return CCAIResponse(url, status=200) + else: + url = url.replace("upload", "media") + url = FILE_HTTP + url + return CCAIResponse(url, status=200) + else: + return CCAIResponse("文件格式错误", status=500) + + except Exception as e: + err_logger.error("user: %s, vue-quill-editor upload image failed: \n%s" % ( + request.user.id, traceback.format_exc() + )) + return CCAIResponse("上传失败", status=500) + + +class PreviewWordAPIView(APIView): + """ + 预览word文件 + """ + + # perms_map = ({"*": "admin"}, {"get": "get_preview"}) + # authentication_classes = (JSONWebTokenAuthentication,) + # permission_classes = (IsAuthenticated, RbacPermission) + + def get(self, request): + try: + file = open(r"C:\Users\Ykim H\Desktop\渡渡鸟知识库用户操作手册.doc", 'rb') + response = FileResponse(file) + response['Content-Type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + response['Content-Disposition'] = 'attachment;filename="渡渡鸟知识库用户操作手册.doc"' + return response + except Exception as e: + err_logger.error("user: %s, get doc file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("下载渡渡鸟知识库用户操作手册.doc失败", SERVER_ERROR) + + +class CheckResourceAPIView(APIView): + # perms_map = ({"*": "admin"}, {"get": "get_preview"}) + # authentication_classes = (JSONWebTokenAuthentication,) + # permission_classes = (IsAuthenticated, RbacPermission) + + def get(self, request): + """反hook""" + try: + print("来了") + return CCAIResponse("success") + except Exception as e: + err_logger.error("user: %s, check resource failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("资源检测失败", SERVER_ERROR) diff --git a/apps/common/views/area.py b/apps/common/views/area.py new file mode 100644 index 0000000..e471cf4 --- /dev/null +++ b/apps/common/views/area.py @@ -0,0 +1,72 @@ +import logging +import traceback + +from rest_framework.views import APIView + +# 去掉区县级 +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR +from common.models import Area + +logger = logging.getLogger('error') + + +class NewRegisterAreaListAPIView(APIView): + ''' + 注册地区 + ''' + def get(self, request, format=None): + try: + sql = 'SELECT a.id, b.DataDictionaryDetailId as dict_id,' \ + 'b.ParentCode as parent_code, b.ParentId as parent_id, b.DictionaryValue as dict_val, b.DictionaryCode as dict_code,' \ + 'b.FullName as dict_name, b.Remark as remark, b.IsEnabled as is_enabled, b.SortNo as sort_no, a.BaiDuCode as baidu_code,' \ + 'a.IsMunicipality as is_municipality, a.IsProvince as is_province, a.IsCity as is_city, a.IsTownship as is_town_ship, a.IsHotCity as is_hot_city,' \ + 'a.Code as code, a.Abbreviation as abbreviation, b.SortNoTwo as sort_no_two, a.TopImg as top_img ' \ + 'FROM common_area a ' \ + 'LEFT JOIN common_datadictionarydetail b ' \ + 'ON a.DataDictionaryDetailId = b.DataDictionaryDetailId WHERE b.FullName != "" and (a.IsMunicipality = 1 or a.IsProvince = 1 or a.IsCity = 1)' + query_rows = Area.objects.raw(sql) + + municipality_map = {} + municipality_data = [] + province_data = [] + all_data = [] + + dict_map = {} + tree_dict = {} + tree_data = [] + for item in query_rows: + item.__dict__.pop('_state') + + if item.parent_code != 'RegisterArea_' and item.dict_code != 'RegisterArea_' and item.dict_code.count( + '_') < 5 and 'RegisterArea_ZXS_Chongqing_' not in item.dict_code and 'RegisterArea_ZXS_Tianjin_' not in item.dict_code and 'RegisterArea_ZXS_Beijing_' not in item.dict_code and 'RegisterArea_ZXS_Shanghai_' not in item.dict_code: + dict_map[item.dict_code] = item.dict_name + tree_dict[item.dict_id] = item.__dict__ + + for i in tree_dict: + data = tree_dict[i] + parent_id = tree_dict[i]['parent_id'] + parent_code = tree_dict[i]['parent_code'] + if parent_id and parent_code in dict_map: + pid = parent_id + parent = tree_dict[pid] + parent.setdefault('children', []).append(data) + else: + tree_data.append(data) + for item in tree_data: + if item['is_municipality'] == 1: + municipality_data.append(item) + else: + province_data.append(item) + + municipality_map['dict_code'] = 'RegisterArea_ZXS' + municipality_map['dict_name'] = '直辖市' + municipality_map['children'] = municipality_data + + all_data.append(municipality_map) + all_data.extend(province_data) + return CCAIResponse(all_data) + + except Exception as e: + logger.error("user: %s, get area failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取地区失败", SERVER_ERROR) \ No newline at end of file diff --git a/apps/common/views/dict.py b/apps/common/views/dict.py new file mode 100644 index 0000000..f30bc8e --- /dev/null +++ b/apps/common/views/dict.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +import datetime +import distutils +import json +import logging +import traceback +import uuid +from collections import OrderedDict + +from django.db.models import Q +from rest_framework.views import View +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ModelViewSet +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR, PARAMS_ERR +from utils.custom import RbacPermission, CustomViewBase, req_operate_by_user, create_operation_history_log +from ..models import DataDictionary, DataDictionaryDetail, OperationHistoryLog, DataDictionaryDetailZY +from ..serializers.dict_serializer import DictSerializer, DictDetailSerializer + +logger = logging.getLogger('error') + + +class DataDictionaryViewSet(CustomViewBase): + ''' + 字典管理:增删改查 + ''' + perms_map = ({'*': 'admin'}, {'*': 'dictionary_all'}, {'get': 'dictionary_list'}, {'post': 'dictionary_create'}, + {'put': 'dictionary_edit'}, + {'delete': 'dictionary_delete'}) + queryset = DataDictionary.objects.all() + serializer_class = DictSerializer + filter_backends = (SearchFilter, OrderingFilter) + search_fields = ('DictionaryCode', 'DictionaryValue') + ordering_fields = ('id',) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + def list(self, request, *args, **kwargs): + ''' + 重写list方法 + ''' + queryset = self.filter_queryset(self.get_queryset()) + serializer = self.get_serializer(queryset, many=True) + tree_dict = {} + tree_data = [] + try: + for item in serializer.data: + tree_dict[item['dict_id']] = item + for i in tree_dict: + if tree_dict[i]['parent_id'] and tree_dict[i]['parent_id'] != "0": # 顶级的父级是 0,所以跳过 + pid = tree_dict[i]['parent_id'] + parent = tree_dict[pid] + parent.setdefault('children', []).append(tree_dict[i]) + else: + tree_data.append(tree_dict[i]) + results = tree_data + except Exception as e: + logger.error("get dict list failed: \n%s" % traceback.format_exc()) + return CCAIResponse("获取字典列表失败", SERVER_ERROR) + + return CCAIResponse(results) + + def create(self, request, *args, **kwargs): + '''添加数据POST''' + try: + data = req_operate_by_user(request) + data['dict_id'] = uuid.uuid4().__str__() + + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + # 操作记录 + # info = { + # "des": "添加数据字典", + # "detail": "字典id: " + data['dict_id'] + ", " + "字典名称: " + data["label"] + # } + # create_operation_history_log(request, info, OperationHistoryLog) + return CCAIResponse(data="success") + except Exception as e: + logger.error("dict create failed: \n%s" % traceback.format_exc()) + return CCAIResponse("create failed", SERVER_ERROR) + + +class DataDictionaryDetailTreeViewSet(CustomViewBase): + ''' + 字典详情树 + ''' + perms_map = ({'*': 'admin'}, {'*': 'dictionarydetail_all'}, {'get': 'dictionarydetail_list'}, + {'post': 'dictionarydetail_create'}, {'put': 'dictionarydetail_edit'}, + {'delete': 'dictionarydetail_delete'}) + queryset = DataDictionaryDetail.objects.all() + serializer_class = DictDetailSerializer + filter_backends = (SearchFilter, OrderingFilter) + ordering_fields = ('id',) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request, *args, **kwargs): + ''' + 重写list方法 + ''' + parent_code = request.GET.get("parent_code") + is_enabled = request.GET.get("is_enabled") + label = request.GET.get("label") + order = request.GET.get("order") + sort = request.GET.get("ordering") + order_by = 'id' + if order and sort: + if order == '1': + order_by = sort + else: + order_by = '-' + sort + + tree_dict = {} + tree_data = [] + dict_map = {} + results = {} + + if parent_code: + try: + condtions = OrderedDict() + if is_enabled: + condtions['IsEnabled'] = is_enabled + if label: + condtions['FullName__contains'] = label + if 'TechArea' == parent_code: + Tech = DataDictionary.objects.filter(DictionaryCode=parent_code).first() + condtions['DataDictionaryId'] = Tech.DataDictionaryId + else: + condtions['ParentCode__istartswith'] = parent_code + # condtions['ParentCode'] = parent_code + + queryset = DataDictionaryDetail.objects.filter(**condtions).order_by(order_by) + serializer = self.get_serializer(queryset, many=True) + for item in serializer.data: + dict_map[item['dict_code']] = item['label'] + + for item in serializer.data: + tree_dict[item['dict_detail_id']] = item + for i in tree_dict: + data = tree_dict[i] + parent_id = tree_dict[i]['parent_id'] + parent_code = tree_dict[i]['parent_code'] + if parent_id and parent_code in dict_map and parent_code != 'CusMYType_XSSRZZL': + pid = parent_id + parent = tree_dict[pid] + parent.setdefault('children', []).append(data) + else: + tree_data.append(data) + if 'RND_COST' == request.GET.get("parent_code"): + results['list'] = sorted(tree_data, key=lambda x: distutils.version.LooseVersion(x['dict_code']), reverse=False) + else: + results['list'] = tree_data + + results['map'] = dict_map + return CCAIResponse(results) + except Exception as e: + logger.error("get dict detail tree failed: \n%s" % traceback.format_exc()) + return CCAIResponse("获取字典详情树失败", SERVER_ERROR) + else: + return CCAIResponse(PARAMS_ERR, SERVER_ERROR) + + def create(self, request, *args, **kwargs): + '''添加数据POST''' + try: + data = req_operate_by_user(request) + data['dict_detail_id'] = uuid.uuid4().__str__() + + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + # 操作记录 + # info = { + # "des": "添加字典详情", + # "detail": "字典详情id: " + data['dict_detail_id'] + ", " + "字典详情名称: " + data["label"] + # } + # create_operation_history_log(request, info, OperationHistoryLog) + return CCAIResponse(data="success") + except Exception as e: + logger.error("dict detail create failed: \n%s" % traceback.format_exc()) + return CCAIResponse("create failed", SERVER_ERROR) + + # 获取地区字典接口 - 从redis从获取 + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getDictList", url_name="getDictList") + def getDictList(self, request): + parent_code = request.GET.get("parent_code") + if parent_code: + if parent_code == '': + return CCAIResponse("parent_code参数不能为空!", status=status.HTTP_400_BAD_REQUEST) + from django_redis import get_redis_connection + startTime = datetime.datetime.now() + # print(datetime.datetime.now()) + conn = get_redis_connection('default') + # conn.hset('dicList', 'area', 1) + # print(conn.hget('dicList', parent_code)) + str = conn.hget('dicList', parent_code) + endTime = datetime.datetime.now() + # print(datetime.datetime.now()) + # print((endTime - startTime).seconds) + return CCAIResponse(data=str) + else: + return CCAIResponse("parent_code参数不能为空!", status=status.HTTP_400_BAD_REQUEST) + + # 设置地区字典接口 - 存储到redis中 + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="setDictList", url_name="setDictList") + def setDictList(self, request): + + parent_code = request.GET.get("parent_code") + is_enabled = request.GET.get("is_enabled") + label = request.GET.get("label") + order = request.GET.get("order") + sort = request.GET.get("ordering") + order_by = 'id' + if order and sort: + if order == '1': + order_by = sort + else: + order_by = '-' + sort + + tree_dict = {} + tree_data = [] + dict_map = {} + results = {} + + if parent_code: + try: + condtions = OrderedDict() + if is_enabled: + condtions['IsEnabled'] = is_enabled + if label: + condtions['FullName__contains'] = label + condtions['ParentCode__istartswith'] = parent_code + # condtions['ParentCode'] = parent_code + + queryset = DataDictionaryDetail.objects.filter(**condtions).order_by(order_by) + serializer = self.get_serializer(queryset, many=True) + for item in serializer.data: + dict_map[item['dict_code']] = item['label'] + for item in serializer.data: + tree_dict[item['dict_detail_id']] = item + for i in tree_dict: + if tree_dict[i]['parent_id'] and tree_dict[i]['parent_code'] in dict_map: + pid = tree_dict[i]['parent_id'] + parent = tree_dict[pid] + parent.setdefault('children', []).append(tree_dict[i]) + else: + tree_data.append(tree_dict[i]) + results['list'] = tree_data + + results['map'] = dict_map + from django_redis import get_redis_connection + conn = get_redis_connection('default') + str = json.dumps(results).encode('utf-8').decode('unicode_escape') + # print(str) + conn.hset('dicList', parent_code, str) + return CCAIResponse(data="success") + except Exception as e: + logger.error("get dict detail tree failed: \n%s" % traceback.format_exc()) + return CCAIResponse("获取字典详情树失败", SERVER_ERROR) + + +class DataDictionaryDetailListViewSet(ModelViewSet): + ''' + 字典详情列表 + ''' + perms_map = ({'*': 'admin'}, {'*': 'dictionarydetail_list_all'}, {'get': 'dictionarydetail_list_list'}, + {'post': 'dictionarydetail_list_create'}, {'put': 'dictionarydetail_listlist_edit'}, + {'delete': 'dictionarydetail_list_delete'}) + queryset = DataDictionaryDetail.objects.all() + serializer_class = DictDetailSerializer + filter_backends = (SearchFilter, OrderingFilter) + ordering_fields = ('id',) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request, *args, **kwargs): + ''' + 重写list方法 + ''' + parent_code = request.GET.get("parent_code") + is_enabled = request.GET.get("is_enabled") + label = request.GET.get("label") + order = request.GET.get("order") + sort = request.GET.get("ordering") + select_all = request.GET.get("select_all") + + dict_list = [] + dict_map = {} + dict_value_map = {} + results = {} + + order_by = '-id' + if order and sort: + if order == '1': + order_by = sort + else: + order_by = '-' + sort + + if parent_code: + try: + condtions = OrderedDict() + if label: + condtions['FullName__contains'] = label + if is_enabled: + condtions['IsEnabled'] = is_enabled + if select_all: + condtions['ParentCode__istartswith'] = parent_code + else: + condtions['ParentCode'] = parent_code + + queryset = DataDictionaryDetail.objects.filter(**condtions).order_by(order_by) + serializer = self.get_serializer(queryset, many=True) + + for item in serializer.data: + item['children'] = [] + item['hasChildren'] = True + dict_list.append(item) + dict_map[item['dict_code']] = item['label'] + dict_value_map[item['dict_val']] = item['label'] + + results['list'] = dict_list + + results['map'] = dict_map + results['value_map'] = dict_value_map + return CCAIResponse(results) + except Exception as e: + logger.error("get dict detail tree failed: \n%s" % traceback.format_exc()) + return CCAIResponse("获取字典详情列表失败", SERVER_ERROR) + else: + return CCAIResponse(PARAMS_ERR, SERVER_ERROR) + + +# 获取智云那边的区域 +class DataDictionaryDetailTreeViewSetZY(CustomViewBase): + ''' + 字典详情树 + ''' + perms_map = ({'*': 'admin'}, {'*': 'dictionarydetail_all'}, {'get': 'dictionarydetail_list'}, + {'post': 'dictionarydetail_create'}, {'put': 'dictionarydetail_edit'}, + {'delete': 'dictionarydetail_delete'}) + queryset = DataDictionaryDetailZY.objects.all() + serializer_class = DictDetailSerializer + filter_backends = (SearchFilter, OrderingFilter) + ordering_fields = ('id',) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request, *args, **kwargs): + ''' + 重写list方法 + ''' + parent_code = request.GET.get("parent_code") + is_enabled = request.GET.get("is_enabled") + label = request.GET.get("label") + order = request.GET.get("order") + sort = request.GET.get("ordering") + order_by = 'id' + if order and sort: + if order == '1': + order_by = sort + else: + order_by = '-' + sort + + tree_dict = {} + tree_data = [] + dict_map = {} + results = {} + + if parent_code: + try: + condtions = OrderedDict() + if is_enabled: + condtions['IsEnabled'] = is_enabled + if label: + condtions['FullName__contains'] = label + condtions['ParentCode__istartswith'] = parent_code + # condtions['ParentCode'] = parent_code + + queryset = DataDictionaryDetailZY.objects.filter(**condtions).order_by(order_by) + serializer = self.get_serializer(queryset, many=True) + for item in serializer.data: + dict_map[item['dict_code']] = item['label'] + + for item in serializer.data: + tree_dict[item['dict_detail_id']] = item + for i in tree_dict: + data = tree_dict[i] + parent_id = tree_dict[i]['parent_id'] + parent_code = tree_dict[i]['parent_code'] + if parent_id and parent_code in dict_map and parent_code != 'CusMYType_XSSRZZL': + pid = parent_id + parent = tree_dict[pid] + parent.setdefault('children', []).append(data) + else: + tree_data.append(data) + results['list'] = tree_data + + results['map'] = dict_map + return CCAIResponse(results) + except Exception as e: + logger.error("get dict detail tree failed: \n%s" % traceback.format_exc()) + return CCAIResponse("获取字典详情树失败", SERVER_ERROR) + else: + return CCAIResponse(PARAMS_ERR, SERVER_ERROR) diff --git a/apps/common/views/history.py b/apps/common/views/history.py new file mode 100644 index 0000000..4dcfd3e --- /dev/null +++ b/apps/common/views/history.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +import logging +import traceback + +from rest_framework.permissions import IsAuthenticated +from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from rest_framework.views import APIView +from django.core.paginator import Paginator +from hashids import Hashids + +from ChaCeRndTrans.basic import CCAIResponse +from utils.custom import RbacPermission +from ChaCeRndTrans.settings import ID_KEY +from common.models import OperationHistoryLog + +err_logger = logging.getLogger('error') + + +class HistoryLogAPIView(APIView): + """ + 操作日志 + """ + perms_map = ({"*": "admin"},) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated, RbacPermission) + + def get(self, request, *args, **kwargs): + try: + hashids = Hashids(salt=ID_KEY, min_length=32) # 对ID加密再返回 + params = request.GET + page = params.get("page") + page_size = params.get("size") + des = params.get("des") + username = params.get("username") + ip = params.get("ip") + create_time = params.get("create_time") + detail = params.get("detail") + + if page != "" and page is not None: + page = int(page) + else: + page = 1 + if page_size != "" and page_size is not None: + page_size = int(page_size) + if page_size > 100: + page_size = 10 + else: + page_size = 10 + + query_result = OperationHistoryLog.objects.all().order_by("-create_time") + if des != "" and des is not None: + query_result = query_result.filter(des__icontains=des) + if username != "" and username is not None: + query_result = query_result.filter(username__icontains=username) + if detail != "" and detail is not None: + query_result = query_result.filter(detail__icontains=detail) + if ip != "" and ip is not None: + query_result = query_result.filter(ip__icontains=ip) + if create_time != "" and create_time is not None: + end_time = create_time.split(" ")[0] + end_time = end_time + " " + "23:59:59" + query_result = query_result.filter(create_time__range=[create_time, end_time]) + + p = Paginator(query_result, page_size) + try: + query = p.page(page).object_list.values() + except Exception as e: + query = p.page(p.num_pages).object_list.values() + page = p.num_pages + + data = [] + for each in query: + each['create_time'] = each['create_time'].strftime('%Y-%m-%d %H:%M:%S') + each['id'] = hashids.encode(each["id"]) + del each["update_time"], each["user_id"], each["update_user_id"], each["update_username"] + data.append(each) + + pagination = { + page: page, + page_size: page_size + } + count = p.count + return CCAIResponse(data=data, pagination=pagination, count=count, status=200) + + except Exception as e: + err_logger.error("user: %s, get history log failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("获取操作日志失败", status=500) diff --git a/apps/common/views/index.py b/apps/common/views/index.py new file mode 100644 index 0000000..d8ed056 --- /dev/null +++ b/apps/common/views/index.py @@ -0,0 +1,15 @@ +import logging +import traceback +from datetime import datetime + +from rest_framework.views import APIView + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR, BAD +from project.models import Project +from rbac.models import Company +from staff.models import Staff +from utils.custom import RbacPermission + +logger = logging.getLogger('error') + diff --git a/apps/common/views/upload_file.py b/apps/common/views/upload_file.py new file mode 100644 index 0000000..95cbdc1 --- /dev/null +++ b/apps/common/views/upload_file.py @@ -0,0 +1,197 @@ +import logging +import os +import traceback +import uuid +import time +import datetime +from urllib import parse + +from django.http import FileResponse +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans import settings +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR, PARAMS_ERR +from utils.custom import RbacPermission, create_operation_history_log, generate_random_str +from common.models import OperationHistoryLog + +logger = logging.getLogger('error') + +class UploadFileAPIView(APIView): + ''' + 上传附件 + ''' + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + upload_file = request.FILES.get("file", None) + file_type = request.data.get("file_type", None) + try: + if not upload_file or not file_type: + return CCAIResponse(msg="上传失败", status=status.HTTP_400_BAD_REQUEST) + if upload_file.size > settings.MAX_FILE_SIZE: + return CCAIResponse(msg="上传文件需小于或等于10M", status=status.HTTP_400_BAD_REQUEST) + + # 上传的是学员模板 + # if file_type == "template-xlsx" or file_type == "template-xls": + # name = os.path.splitext(upload_file.name)[0] + # ext = os.path.splitext(upload_file.name)[-1] + # file_name = name + generate_random_str(3) + ext + # path_1, path_2 = file_type.split("-") + # # 本地存储目录 + # save_path = os.path.join(settings.FILE_PATH, path_1, path_2) + # save_path = save_path.replace('\\', '/') + # # 如果不存在则创建目录 + # if not os.path.exists(save_path): + # os.makedirs(save_path) + # files = os.listdir(save_path) # 查找路径下的所有的文件夹及文件 + # if path_2 == "xlsx": + # n_path = os.path.join(settings.FILE_PATH, path_1, "xls") + # if os.path.exists(n_path): + # files2 = os.listdir(n_path) # 查找路径下的所有的文件夹及文件 + # else: + # files2 = [] + # else: + # n_path = os.path.join(settings.FILE_PATH, path_1, "xlsx") + # if os.path.exists(n_path): + # files2 = os.listdir(n_path) # 查找路径下的所有的文件夹及文件 + # else: + # files2 = [] + # file_path = open(save_path + "/" + file_name, 'wb') + # for chunk in upload_file.chunks(): + # file_path.write(chunk) + # file_path.close() + # # 删除曾经的旧模板 + # if len(files) > 0: + # for each in files: + # # 删除文件 + # os.remove(save_path + "/" + each) + # if len(files2) > 0: + # for each in files: + # # 删除文件 + # os.remove(save_path + "/" + each) + # # 操作记录 + # # info = { + # # "des": "上传模板", + # # "detail": "后端存储路径: " + save_path + "/" + file_name + # # } + # # create_operation_history_log(request, info, OperationHistoryLog) + # return CCAIResponse("success", status=status.HTTP_200_OK) + + # 文件名进行url编码 + # file_name = parse.quote(upload_file.name) + file_name = upload_file.name + name = os.path.splitext(upload_file.name)[0] + ext = os.path.splitext(upload_file.name)[-1] + ct = time.time() # 取得系统时间 + local_time = time.localtime(ct) + date_head = time.strftime("%Y%m%d%H%M%S", local_time) # 格式化时间 + date_m_secs = str(datetime.datetime.now().timestamp()).split(".")[-1] # 毫秒级时间戳 + time_stamp = "%s%.3s" % (date_head, date_m_secs) # 拼接时间字符串 + # print(time_stamp) + # 本地存储目录 + save_path = os.path.join(settings.FILE_PATH, file_type) + save_path = save_path.replace('\\', '/') + # save_path = os.path.join(settings.FILE_PATH, file_type) + # 前端显示目录 + start = file_name.rindex('.') + # name = file_name[0: start] + # type = file_name[start:] + show_path = os.path.join(settings.SHOW_UPLOAD_PATH, file_type, time_stamp + ext) + show_path = show_path.replace('\\', '/') + # 如果不存在则创建目录 + if not os.path.exists(save_path): + os.makedirs(save_path) + + file_path = open(save_path + '/' + time_stamp + ext, 'wb') + for chunk in upload_file.chunks(): + file_path.write(chunk) + file_path.close() + + # 操作记录 + # info = { + # "des": "上传附件", + # "detail": "后端存储路径: " + save_path + "/" + file_name + ", 前端显示路径: " + show_path + # } + # create_operation_history_log(request, info, OperationHistoryLog) + return CCAIResponse(show_path) + except Exception as e: + logger.error("upload file failed: \n%s" % traceback.format_exc()) + return CCAIResponse("上传失败", SERVER_ERROR) + +class DeleteFileAPIView(APIView): + ''' + 删除附件 + ''' + def post(self, request, *args, **kwargs): + + tempdict = request.data.copy() + if 'upload_url' not in tempdict and tempdict['upload_url'] == '': + return CCAIResponse("upload_url参数不能为空!", status=status.HTTP_400_BAD_REQUEST) + upload_url = tempdict['upload_url'].replace(settings.SHOW_UPLOAD_PATH, settings.FILE_PATH) + start = upload_url.rindex('/') + # name_start = upload_url.rindex('_') + # type_start = upload_url.rindex('.') + #拼接本地文件路劲 + # local_path = upload_url[0: start] + '/' + upload_url[name_start+1: type_start] + # local_url = local_path + upload_url[start: name_start] + upload_url[type_start:] + local_path = upload_url[0: start + 1] + import os + if os.path.exists(upload_url): # 如果文件存在 + # 删除文件,可使用以下两种方法。 + os.remove(upload_url) + # 删除空目录,不是空目录时候rmdir不会删除 + try: + os.rmdir(local_path) + except Exception as e: + logger.error("rmdir failed: \n%s" % traceback.format_exc()) + + # 操作记录 + # info = { + # "des": "删除附件", + # "detail": "后端存储路径: " + local_path + # } + # create_operation_history_log(request, info, OperationHistoryLog) + return CCAIResponse("success") + else: + return CCAIResponse("文件不存在") + +class DownLoadFileAPIView(APIView): + ''' + 下载附件 + ''' + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + download_file = request.data.get("ccw_file_path") + try: + if download_file is None or download_file == "": + return CCAIResponse(PARAMS_ERR, SERVER_ERROR) + + re_file = download_file.replace('/upload/file/', settings.FILE_PATH) + file = open(re_file, 'rb') + # url解码 + file_name = parse.unquote(file.name) + response = FileResponse(file) + response['Content-Type'] = 'application/octet-stream' + response['Content-Disposition'] = 'attachment;filename="' + file_name + '"' + + # 操作记录 + # info = { + # "des": "下载附件", + # "detail": "后端存储路径: " + re_file + ", url解码文件名: " + file_name + # } + # create_operation_history_log(request, info, OperationHistoryLog) + + return response + except Exception as e: + logger.error("download file failed: \n%s" % traceback.format_exc()) + return CCAIResponse("下载失败", SERVER_ERROR) + + + diff --git a/apps/rbac/__init__.py b/apps/rbac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rbac/admin.py b/apps/rbac/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/rbac/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/rbac/apps.py b/apps/rbac/apps.py new file mode 100644 index 0000000..16f05ab --- /dev/null +++ b/apps/rbac/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RbacConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'rbac' diff --git a/apps/rbac/migrations/0001_initial.py b/apps/rbac/migrations/0001_initial.py new file mode 100644 index 0000000..93e07e0 --- /dev/null +++ b/apps/rbac/migrations/0001_initial.py @@ -0,0 +1,121 @@ +# Generated by Django 3.1.4 on 2024-02-02 15:47 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='企业名称', max_length=100, verbose_name='企业名称')), + ('MainId', models.CharField(blank=True, help_text='guid全局id', max_length=36, null=True, verbose_name='guid全局id')), + ('userMid', models.CharField(blank=True, help_text='企业所属人的全局id', max_length=36, null=True, verbose_name='企业所属人的全局id')), + ('EUCC', models.CharField(blank=True, help_text='企业统一信用代码', max_length=50, null=True, verbose_name='企业统一信用代码')), + ('employeesCount', models.IntegerField(blank=True, help_text='企业员工总数', null=True, verbose_name='企业员工总数')), + ('hightechCode', models.CharField(blank=True, help_text='高企编码', max_length=36, verbose_name='高企编码')), + ], + options={ + 'verbose_name': '公司信息', + 'verbose_name_plural': '公司信息', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Menu', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='菜单名')), + ('route_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='组件名称')), + ('icon', models.CharField(blank=True, max_length=50, null=True, verbose_name='图标')), + ('path', models.CharField(blank=True, max_length=158, null=True, verbose_name='链接地址')), + ('is_frame', models.BooleanField(default=False, verbose_name='外部菜单')), + ('is_show', models.BooleanField(default=True, verbose_name='显示标记')), + ('sort', models.IntegerField(blank=True, null=True, verbose_name='排序标记')), + ('component', models.CharField(blank=True, max_length=200, null=True, verbose_name='组件')), + ('pid', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rbac.menu', verbose_name='父菜单')), + ], + options={ + 'verbose_name': '菜单', + 'verbose_name_plural': '菜单', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Permission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30, unique=True, verbose_name='权限名')), + ('method', models.CharField(blank=True, max_length=50, null=True, verbose_name='方法')), + ('pid', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rbac.permission', verbose_name='父权限')), + ], + options={ + 'verbose_name': '权限', + 'verbose_name_plural': '权限', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='角色')), + ('desc', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ('menus', models.ManyToManyField(blank=True, to='rbac.Menu', verbose_name='菜单')), + ('permissions', models.ManyToManyField(blank=True, to='rbac.Permission', verbose_name='权限')), + ], + ), + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='aigc status')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='姓名')), + ('MainId', models.CharField(blank=True, max_length=36, null=True, verbose_name='guid全局id')), + ('mobile', models.CharField(blank=True, max_length=50, verbose_name='手机号码')), + ('gender', models.CharField(blank=True, max_length=10, null=True, verbose_name='性别')), + ('email', models.EmailField(blank=True, max_length=50, null=True, verbose_name='邮箱')), + ('image', models.CharField(default='/upload/logo/default.jpg', max_length=100, verbose_name='头像')), + ('area_code', models.CharField(blank=True, max_length=1000, null=True, verbose_name='地区代码')), + ('area_name', models.CharField(blank=True, max_length=15, null=True, verbose_name='地区名')), + ('is_sub', models.IntegerField(default=2, verbose_name='是否子账号')), + ('wx_phone_info', models.CharField(blank=True, max_length=500, null=True, verbose_name='微信手机信息')), + ('wx_openid', models.CharField(blank=True, max_length=255, null=True, verbose_name='微信小程序唯一标识')), + ('parent_id', models.IntegerField(blank=True, null=True, verbose_name='父账号id')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ('is_active', models.BooleanField(blank=True, default=True, null=True, verbose_name='用户是否可用')), + ('company', models.ManyToManyField(blank=True, to='rbac.Company', verbose_name='公司')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('roles', models.ManyToManyField(blank=True, to='rbac.Role', verbose_name='角色')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': '用户信息', + 'verbose_name_plural': '用户信息', + 'ordering': ['id'], + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/apps/rbac/migrations/__init__.py b/apps/rbac/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rbac/models.py b/apps/rbac/models.py new file mode 100644 index 0000000..847d3d5 --- /dev/null +++ b/apps/rbac/models.py @@ -0,0 +1,129 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + + +class Menu(models.Model): + """ + 菜单 + """ + name = models.CharField(max_length=30, unique=True, verbose_name="菜单名") + route_name = models.CharField(max_length=30, null=True, blank=True, verbose_name="组件名称") + icon = models.CharField(max_length=50, null=True, blank=True, verbose_name="图标") + path = models.CharField(max_length=158, null=True, blank=True, verbose_name="链接地址") + is_frame = models.BooleanField(default=False, verbose_name="外部菜单") + is_show = models.BooleanField(default=True, verbose_name="显示标记") + sort = models.IntegerField(blank=True, null=True, verbose_name="排序标记") + component = models.CharField(max_length=200, null=True, blank=True, verbose_name="组件") + pid = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="父菜单") + label = models.IntegerField(default=1, verbose_name="是否前台菜单 # 1:是 2:不是") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "菜单" + verbose_name_plural = verbose_name + ordering = ["id"] + + +class Permission(models.Model): + """ + 权限 + """ + name = models.CharField(max_length=30, unique=True, verbose_name="权限名") + method = models.CharField(max_length=50, null=True, blank=True, verbose_name="方法") + pid = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="父权限") + label = models.IntegerField(default=1, verbose_name="是否前台权限 # 1:是 2:不是") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "权限" + verbose_name_plural = verbose_name + ordering = ["id"] + + +class Role(models.Model): + """ + 角色 + """ + name = models.CharField(max_length=32, verbose_name="角色") + permissions = models.ManyToManyField("Permission", blank=True, verbose_name="权限") + menus = models.ManyToManyField("Menu", blank=True, verbose_name="菜单") + desc = models.CharField(max_length=50, blank=True, null=True, verbose_name="描述") + label = models.IntegerField(default=1, verbose_name="是否前台角色 # 1:是 2:不是") + + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + # class Meta: + # unique_together = ('name', 'companymid') # name companymid 两个字段共同形成唯一约束 + + +class UserProfile(AbstractUser): + """ + 用户 + """ + name = models.CharField(max_length=20, null=True, blank=True, verbose_name="姓名") + MainId = models.CharField(max_length=36, null=True, blank=True, verbose_name="guid全局id") + mobile = models.CharField(max_length=50, null=False, blank=True, verbose_name="手机号码") + gender = models.CharField(max_length=10, null=True, blank=True, verbose_name="性别") + email = models.EmailField(max_length=50, null=True, blank=True, verbose_name="邮箱") + image = models.CharField(max_length=100, default="/image/default.png", verbose_name="头像") + area_code = models.CharField(max_length=1000, null=True, blank=True, verbose_name="地区代码") # 地区代码 + area_name = models.CharField(max_length=15, null=True, blank=True, verbose_name="地区名") # 地区名 + is_sub = models.IntegerField(default=2, verbose_name="是否子账号") # 1:是;2:否。 + wx_phone_info = models.CharField(null=True, blank=True, max_length=500, verbose_name="微信手机信息") + wx_openid = models.CharField(null=True, blank=True, max_length=255, verbose_name="微信小程序唯一标识") + parent_id = models.IntegerField(null=True, blank=True, verbose_name="父账号id") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + is_active = models.BooleanField(null=True, blank=True, default=True, verbose_name="用户是否可用") # 用户锁定与激活用户 + label = models.IntegerField(default=1, verbose_name="是否前台用户 # 1:是 2:不是") + + roles = models.ManyToManyField("Role", verbose_name="角色", blank=True) + company = models.ManyToManyField("Company", verbose_name="公司", blank=True) + + class Meta: + verbose_name = "用户信息" + verbose_name_plural = verbose_name + ordering = ["id"] + + def __str__(self): + return self.username + + def get_companies(self): + return self.company.all() + + # 重写方法,校验密码时,不区分大小写 + def check_password(self, raw_password): + return super().check_password(raw_password.lower()) + + +class Company(models.Model): + """ + 公司 + """ + name = models.CharField(max_length=100, verbose_name="企业名称", help_text="企业名称") + MainId = models.CharField(max_length=36, null=True, blank=True, verbose_name="guid全局id", help_text="guid全局id") + userMid = models.CharField(max_length=36, null=True, blank=True, verbose_name="企业所属人的全局id", help_text="企业所属人的全局id") + EUCC = models.CharField(max_length=50, null=True, blank=True, verbose_name="企业统一信用代码", help_text="企业统一信用代码") + employeesCount = models.IntegerField(null=True, blank=True, verbose_name="企业员工总数", help_text="企业员工总数") + hightechCode = models.CharField(max_length=36, blank=True, verbose_name="高企编码", help_text="高企编码") + hightechDate = models.DateField(null=True, blank=True, verbose_name="获取高企时间", help_text="获取高企时间") + AmStart = models.CharField(max_length=10, default='8:00', null=True, blank=True, verbose_name="早班起始时间", help_text="早班起始时间") + AmEnd = models.CharField(max_length=10, default='12:00', null=True, blank=True, verbose_name="早班结束时间", help_text="早班结束时间") + PmStart = models.CharField(max_length=10, default='14:00', null=True, blank=True, verbose_name="午班起始时间", help_text="午班起始时间") + PmEnd = models.CharField(max_length=10, default='18:00', null=True, blank=True, verbose_name="午班结束时间", help_text="午班结束时间") + tech = models.CharField(max_length=200, null=True, blank=True, verbose_name='技术领域id', help_text="技术领域id") + tolerance = models.IntegerField(default=900, verbose_name="打卡容错时间,单位s, 默认900s=15min", help_text="打卡容错时间,单位s, 默认900s=15min") + checkIn = models.IntegerField(default=0, verbose_name="打卡方案,0-原方案, 1-新方案(细颗粒度)", help_text="打卡方案,0-原方案, 1-新方案(细颗粒度)") + + class Meta: + verbose_name = "公司信息" + verbose_name_plural = verbose_name + ordering = ["id"] + + def __str__(self): + return self.name + + diff --git a/apps/rbac/serializers/__init__.py b/apps/rbac/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rbac/serializers/batch_serializer.py b/apps/rbac/serializers/batch_serializer.py new file mode 100644 index 0000000..8da0e91 --- /dev/null +++ b/apps/rbac/serializers/batch_serializer.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from ..models import StuBatch + + +class BatchSerializer(serializers.ModelSerializer): + """ + 批次管理序列化 + """ + + class Meta: + model = StuBatch + fields = ("id", "name", "desc", "create_username", "update_username", "create_time", "year", "user_id") + extra_kwargs = {"name": {"required": True, "error_messages": {"required": "必须填写批次名"}}} diff --git a/apps/rbac/serializers/menu_serializer.py b/apps/rbac/serializers/menu_serializer.py new file mode 100644 index 0000000..5129e73 --- /dev/null +++ b/apps/rbac/serializers/menu_serializer.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from ..models import Menu + + +class MenuSerializer(serializers.ModelSerializer): + """ + 菜单序列化 + """ + + class Meta: + model = Menu + fields = ("id", "name", "route_name", "icon", "path", "is_show", "is_frame", "sort", "component", "pid", "label") + extra_kwargs = {"name": {"required": True, "error_messages": {"required": "必须填写菜单名"}}} diff --git a/apps/rbac/serializers/permission_serializer.py b/apps/rbac/serializers/permission_serializer.py new file mode 100644 index 0000000..8413ffe --- /dev/null +++ b/apps/rbac/serializers/permission_serializer.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from ..models import Permission + +class PermissionListSerializer(serializers.ModelSerializer): + """ + 权限列表序列化 + """ + menuname = serializers.ReadOnlyField(source="menus.name") + + class Meta: + model = Permission + fields = ("id","name","method","menuname","pid", "label") \ No newline at end of file diff --git a/apps/rbac/serializers/role_serializer.py b/apps/rbac/serializers/role_serializer.py new file mode 100644 index 0000000..9885978 --- /dev/null +++ b/apps/rbac/serializers/role_serializer.py @@ -0,0 +1,31 @@ +from rest_framework import serializers +from ..models import Role + + +class RoleListSerializer(serializers.ModelSerializer): + """ + 角色序列化 + """ + + class Meta: + model = Role + fields = "__all__" + # depth = 1 + + +class RoleModifySerializer(serializers.ModelSerializer): + class Meta: + model = Role + fields = "__all__" + # extra_kwargs = {"menus": {"required": True, "error_messages": {"required": "必须填写角色名"}}} + + # def validate_menus(self, menus): + # if not menus: + # raise serializers.ValidationError("必须选择菜单") + # return menus + + +class FrontRoleModifySerializer(serializers.ModelSerializer): + class Meta: + model = Role + fields = ['id', 'name', 'permissions', 'menus', 'companyMid', 'desc'] diff --git a/apps/rbac/serializers/user_serializer.py b/apps/rbac/serializers/user_serializer.py new file mode 100644 index 0000000..c9f129e --- /dev/null +++ b/apps/rbac/serializers/user_serializer.py @@ -0,0 +1,127 @@ +import uuid + +from rest_framework import serializers + +from .role_serializer import RoleListSerializer +from ..models import UserProfile, Company +import re + + +class UserListSerializer(serializers.ModelSerializer): + """ + 用户列表的序列化 + """ + roles = serializers.SerializerMethodField() + company = serializers.SerializerMethodField() + parent_user = serializers.SerializerMethodField() + + def get_roles(self, obj): + return obj.roles.values() + + def get_company(self, obj): + return obj.company.values() + + def get_parent_user(self, obj): + if obj.parent_id: + parent = UserProfile.objects.filter(id=obj.parent_id).values() + else: + parent = None + return parent + + class Meta: + model = UserProfile + fields = ["id", "username", "name", "mobile", "email", "image", "area_name", + "is_active", "roles", "is_superuser", 'companyMid', 'company', 'label', 'is_sub', 'parent_user'] + depth = 1 + + +class UserModifySerializer(serializers.ModelSerializer): + """ + 用户编辑的序列化 + """ + mobile = serializers.CharField(max_length=11) + username = serializers.CharField(required=True, allow_blank=False) + + + class Meta: + model = UserProfile + fields = ["id", "username", "name", "mobile", "email", "image", + "is_active", "roles", "is_superuser", 'label', 'company'] + + def validate_username(self, value): + return value + + def validate_mobile(self, mobile): + REGEX_MOBILE = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$" + if not re.match(REGEX_MOBILE, mobile): + raise serializers.ValidationError("手机号码不合法") + user = UserProfile.objects.filter(mobile=mobile).first() + if user: + user_id = self.context['view'].kwargs.get("pk", "0") + # print(user_id, user.id) + if int(user_id) == user.id: + return mobile + else: + raise serializers.ValidationError("手机号 "+mobile+" 已经被注册") + return mobile + + +class UserCreateSerializer(serializers.ModelSerializer): + """ + 创建用户序列化 + """ + username = serializers.CharField(required=True, allow_blank=False) + mobile = serializers.CharField(max_length=11) + + class Meta: + model = UserProfile + fields = ["id", "MainId", "username", "name", "mobile", "email", "is_active", "area_name", "roles", + "password", "is_superuser", "companyMid", "company", 'label'] + + def validate_username(self, username): + if UserProfile.objects.filter(username=username): + raise serializers.ValidationError("用户名 "+username+" 已存在") + return username + + def validate_mobile(self, mobile): + REGEX_MOBILE = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$" + if not re.match(REGEX_MOBILE, mobile): + raise serializers.ValidationError("手机号码不合法") + if UserProfile.objects.filter(mobile=mobile): + raise serializers.ValidationError("手机号 "+mobile+" 已经被注册") + return mobile + + def perform_create(self, serializer): + usermid = uuid.uuid4() + serializer.save(MainId=usermid) + + +class UserInfoListSerializer(serializers.ModelSerializer): + """ + 公共users + """ + + class Meta: + model = UserProfile + fields = ("id", "name", "mobile", "email") + + +class CompanySerializer(serializers.ModelSerializer): + """ + 公司序列化 + """ + + class Meta: + model = Company + fields = "__all__" + + +class CompanyRoleSerializer(serializers.ModelSerializer): + """ + 公司角色序列化 + """ + roles = RoleListSerializer(many=True) + + class Meta: + model = Company + fields = "__all__" diff --git a/apps/rbac/signals.py b/apps/rbac/signals.py new file mode 100644 index 0000000..3b919dc --- /dev/null +++ b/apps/rbac/signals.py @@ -0,0 +1,14 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.contrib.auth import get_user_model + +User = get_user_model() + + +@receiver(post_save, sender=User) +# 注册一个信号,在创建用户时自动加密密码 +def create_user(sender, instance=None, created=False, **kwargs): + if created: + password = instance.password + # instance.set_password(password) + # instance.save() diff --git a/apps/rbac/tests.py b/apps/rbac/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/rbac/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/rbac/urls.py b/apps/rbac/urls.py new file mode 100644 index 0000000..1da8736 --- /dev/null +++ b/apps/rbac/urls.py @@ -0,0 +1,30 @@ +from django.urls import path, include +from rbac.views import user, menu, role, permission, message, SubAccount, FrontRole, company +from rest_framework import routers + +from rbac.views.Slider_Verification import SliderVerification + +router = routers.SimpleRouter() +router.register(r"users", user.UserViewSet, basename="users") +router.register(r"menus", menu.MenuViewSet, basename="menus") +router.register(r"permissions", permission.PermissionViewSet, basename="permissions") +router.register(r"roles", role.RoleViewSet, basename="roles") +router.register(r"subAccount", SubAccount.SubAccountViewSet, basename="subAccount") +router.register(r"frontrole", FrontRole.FrontRoleViewSet, basename="frontrole") +router.register(r"company", company.CompanyCustomViewSet, basename="company") + +urlpatterns = [ + path(r"api/", include(router.urls)), + path(r"auth/login/", user.UserAuthView.as_view()), + path(r"api/login/message/", message.MessageView.as_view()), + path(r"auth/info/", user.UserInfoView.as_view(), name="user_info"), + path(r"auth/build/menus/", user.UserBuildMenuView.as_view(), name="build_menus"), + path(r"api/menu/tree/", menu.MenuTreeView.as_view(), name="menus_tree"), + path(r"api/permission/tree/", permission.PermissionTreeView.as_view(), name="permissions_tree"), + path(r"api/user/list/", user.UserListView.as_view(), name="user_list"), + path(r'api/slider_code/', SliderVerification.as_view(), name='slider_code'), # 获取滑块验证码 + path(r"auth/register/", user.UserRegisterView.as_view()), # 注册接口 + path(r"api/company/name/list/", company.CompanyNameListAPIView.as_view()), # 模糊查询公司名称 + path(r"api/user/refresh/token/", user.RefreshTokenView.as_view(), name="user_list"), + # path(r"auth/bind/wechat/", user.UserBindWeChat.as_view()), # 微信绑定接口 +] diff --git a/apps/rbac/views.py b/apps/rbac/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/rbac/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/rbac/views/FrontRole.py b/apps/rbac/views/FrontRole.py new file mode 100644 index 0000000..7805a8a --- /dev/null +++ b/apps/rbac/views/FrontRole.py @@ -0,0 +1,96 @@ +import logging +import traceback + +from django.db.models import Q +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import * +from utils.custom import CommonPagination, RbacPermission, CustomViewBase, req_operate_by_user +from ..models import Role, Company +from ..serializers.role_serializer import RoleListSerializer, FrontRoleModifySerializer + +logger = logging.getLogger('error') + + +class FrontRoleViewSet(CustomViewBase): + """ + 前台角色管理:增删改查 (提供前台用户管理自己的公司的角色,所有数据需要绑定公司mid) + """ + perms_map = ( + {"*": "admin"}, + {"*": "frontRole_all"}, + {"get": "frontRole_list"}, + {"post": "frontRole_create"}, + {"put": "frontRole_edit"}, + {"delete": "frontRole_delete"} + ) + queryset = Role.objects.all().order_by('id') + serializer_class = RoleListSerializer + pagination_class = CommonPagination + filter_backends = (SearchFilter, OrderingFilter) + search_fields = ("name",) + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + def get_queryset(self): + companyMid = self.request.query_params.get('companyMid', None) + queryset = Role.objects.all() + + if companyMid: + # 查询该公司自定义角色及默认的前台角色 + queryset = queryset.filter(Q(companyMid=companyMid) | (Q(companyMid__isnull=True) & Q(label=1))) + return queryset + + def get_serializer_class(self): + if self.action == "list": + return RoleListSerializer + return FrontRoleModifySerializer + + def create(self, request, *args, **kwargs): + """ + 前台用户自定义角色,绑定公司,label=1(默认为前台角色) + """ + try: + data = req_operate_by_user(request) + companyMid = request.query_params.get('companyMid', None) + if not companyMid: + return CCAIResponse("Missing company information", BAD) + is_com_exist = Company.objects.filter(MainId=companyMid).exists() + if is_com_exist: + data['companyMid'] = companyMid + data['label'] = 1 # 前台角色 + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + return CCAIResponse(data="success") + else: + return CCAIResponse("Company information error", BAD) + except Exception: + logger.error("user: %s, aigc create failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("create failed", SERVER_ERROR) + + @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], + url_path="rolelist", url_name="rolelist") + def rolelist(self, request): + """ + 获取对应公司的角色,及前台默认角色 + """ + try: + companyMid = request.GET.get('companyMid') + if companyMid: + rolelist = Role.objects.filter( + Q(companyMid=companyMid) | (Q(companyMid__isnull=True) & Q(label=1))).values('name', 'id').all() + # serializer = RoleListSerializer(rolelist, many=True) + return CCAIResponse(rolelist) + else: + return CCAIResponse(BAD, msg='公司数据缺失') + except Exception as e: + logger.error( + "user: %s get rolelist failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='获取公司角色列表失败', status=SERVER_ERROR) diff --git a/apps/rbac/views/Slider_Verification.py b/apps/rbac/views/Slider_Verification.py new file mode 100644 index 0000000..6792f82 --- /dev/null +++ b/apps/rbac/views/Slider_Verification.py @@ -0,0 +1,286 @@ +import base64 +import io +import json +import logging +import math +import os +import traceback +from PIL import Image, ImageDraw, ImageShow +import random + +from django_redis import get_redis_connection +from rest_framework.views import APIView + +from rest_framework.response import Response + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import BAD, SERVER_ERROR +from ChaCeRndTrans.settings import MEDIA_ROOT + +logger = logging.getLogger('error') + +# 验证码图片索引0-19,共20张图 +image_start = 0 +image_end = 11 +DEBUG = False + + +# 随机生成滑块验证码 +class SliderVerificationCode: + def __init__(self): + self.bg_color = (255, 255, 255) # 背景色 + self.slider_color = (125, 125, 125) # 滑块颜色 + self.img_size = (260, 120) # 图片尺寸 + + self.block_size = (40, 40) # 滑块尺寸 + self.blockX = 0 # 滑块的横坐标 + self.blockY = 0 # 滑块的纵坐标 + self.block_radius = 5 # 滑块凹凸半径 + self.block_position = self.generate_block_position() # 生成滑块位置 + + def generate_block_position(self): + block_x = random.randint(0, self.img_size[0] - self.block_size[0]) # 随机生成滑块起始横坐标 + block_y = random.randint(0, self.img_size[1] - self.block_size[1]) # 随机生成滑块起始纵坐标 + return block_x, block_y + + +class SliderVerification(APIView): + + def get(self, request): + try: + params = request.GET + username = params.get('username') # 手机号码/账号/邮箱 + if username is None: + return CCAIResponse("缺少username参数", BAD) + slider_code = SliderVerificationCode() + # 获取画布的宽高 + canvasWidth, canvasHeight = slider_code.img_size + # 滑块的宽高,半径 + block_width, block_height = slider_code.block_size + block_radius = slider_code.block_radius + path = MEDIA_ROOT + '/VerificationCode_image' + canvasImage = get_image(path) + # 调整原图到指定大小 + canvasImage = image_resize(canvasImage, canvasWidth, canvasHeight) + if DEBUG: + info = {"Title": "这是 canvasImage "} + ImageShow.show(canvasImage, info) + + # 随机生成滑块坐标 + blockX = random.randint(block_width, canvasWidth - block_width - 10) + blockY = random.randint(10, canvasHeight - block_height + 1) + slider_code.blockX = blockX + slider_code.blockY = blockY + # 新建滑块 + block_image = Image.new("RGBA", (block_width, block_height)) + # 新建的图像根据轮廓图颜色赋值,源图生成遮罩 + cut_by_template(canvasImage, block_image, block_width, block_height, block_radius, blockX, blockY) + if DEBUG: + print(canvasImage) + print(block_image) + print(blockX) + print(blockY) + print(block_width) + print(block_height) + print(block_radius) + info = {"Title": "这是 final block "} + ImageShow.show(block_image, info) + info = {"Title": "这是 final canvasImage "} + ImageShow.show(canvasImage, info) + canvas_str = image_to_base64(canvasImage) + blockc_str = image_to_base64(block_image) + + # 将滑块的X坐标 存到redis中做验证 + slider_key = 'chace_rnd_slider_' + username + conn = get_redis_connection('default') + conn.set(slider_key, blockX) + conn.expire(slider_key, 60) + rows = [] + img_dict = {} + img_dict['canvas_str'] = canvas_str + img_dict['blockc_str'] = blockc_str + img_dict['blockY'] = blockY + rows.append(img_dict) + request.session['blockX'] = blockX # 备选方案,如果redis连不上,拿session中的 + return CCAIResponse(rows) + + except Exception as e: + logger.error( + "user: %s, get slider verification code failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取滑块验证码失败", SERVER_ERROR) + + def post(self, request): + try: + params = request.data + username = params.get('username') # 手机号码 + slide_blockX = params.get('blockX') # 获取用户提交的滑块位置坐标 + if username is None: + return CCAIResponse("缺少username参数", BAD) + + if slide_blockX is None: + return CCAIResponse("缺少 blockX 参数", BAD) + else: + slide_blockX = int(slide_blockX) + + try: + conn = get_redis_connection('default') + cache_slider = conn.get('chace_rnd_slider_' + username) # 获取滑块缓存的X坐标 + if DEBUG: + print(cache_slider) + except Exception as e: + logger.error("user: %s, connect redis failed: \n%s" % (request.user.id, traceback.format_exc())) + if cache_slider: + cache_slider = json.loads(cache_slider) + else: + cache_slider = request.session.get('blockX') # 从session中获取滑块位置坐标 + + rows = {"code": 400} + if cache_slider is None: + rows = {"code": 401} + return CCAIResponse(rows) + + if abs(int(cache_slider) - int(slide_blockX)) < 5: # 判断是否验证成功 + conn.set("rnd_manual_slider_" + username, username, ex=60) + return CCAIResponse("验证成功") + else: + return CCAIResponse(rows) + + except Exception as e: + logger.error( + "user: %s, POST slider verification code failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("提交滑块验证码失败", SERVER_ERROR) + + +# 获取验证码资源图 +def get_image(folder_path): + # pass # 获取指定范围内的随机数 + image_index = random.randint(image_start, image_end) # 获取随机码 0-19的随机数 + image_path = os.path.join(folder_path, str(image_index) + '.jpg') + image = Image.open(image_path) + if DEBUG: + print("image_index") + print(image_index) + print(image_path) + info = {"Title": "这是 srcImage "} + ImageShow.show(image, info) + return image + + +# 调整图片的尺寸大小 +def image_resize(original_image, width, height): + image = original_image.resize((width, height)) # Image.ANTIALIAS 是选择高质量缩放滤镜 + if DEBUG: + info = {"Title": "这是 resizeImage "} + ImageShow.show(image, info) + # result_image = Image.new('RGBA', (width, height)) # 创建一个空白的image + # graphics2D = result_image.convert('RGBA') # 转换为RGBA模式,就可以在上面绘制图像 + # graphics2D.paste(image, (0, 0)) + # return result_image + return image + + +# 抠图,并生成阻塞块 +def cut_by_template(canvas_image, block_image, block_width, block_height, block_radius, block_x, block_y): + water_image = Image.new("RGBA", (block_width, block_height), (0, 0, 0, 0)) + # 阻滑块的轮廓图 + block_data = get_block_data(block_width, block_height, block_radius) + # 防止数组越界,保证边界为0 + block_data[0] = [0] * len(block_data[0]) + block_data[-1] = [0] * len(block_data[-1]) + # 创建滑块具体形状 + for i in range(block_width): + # 将第1列 和 最后1列,设置为0,防止数组越界,保证边界为0 + block_data[i][0] = 0 + block_data[i][-1] = 0 + for j in range(block_height): + # 原图中对应位置变色处理 + if block_data[i][j] == 1: + # 背景设置为黑色 + water_image.putpixel((i, j), (0, 0, 0, 255)) + block_image.putpixel((i, j), canvas_image.getpixel((block_x + i, block_y + j))) + # 轮廓设置为白色,取带像素和无像素的界点,判断该点是不是临界轮廓点 + if block_data[i + 1][j] == 0 or block_data[i][j + 1] == 0 or block_data[i - 1][j] == 0 or \ + block_data[i][j - 1] == 0: + block_image.putpixel((i, j), (255, 255, 255, 255)) + water_image.putpixel((i, j), (255, 255, 255, 255)) + # 把背景设为透明 + else: + block_image.putpixel((i, j), (0, 0, 0, 0)) + water_image.putpixel((i, j), (0, 0, 0, 0)) + # 在画布上添加阻塞块水印 + if DEBUG: + info = {"Title": "这是block_image"} + ImageShow.show(block_image, info) + info = {"Title": "这是 water_image "} + ImageShow.show(water_image, info) + add_block_watermark(canvas_image, water_image, block_x, block_y) + + +# 构建拼图轮廓轨迹 +def get_block_data(block_width, block_height, block_radius): + """ + 先创建一个二维数组data,然后随机生成两个圆的坐标,并在4个方向上随机找到2个方向添加凸/凹 + 它获取凸/凹起位置坐标,并随机选择凸/凹类型。 + 最后,计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色。 + """ + data = [[0 for _ in range(block_width)] for _ in range(block_height)] # 初始化二维数组,元素为0 + po = math.pow(block_radius, 2) + # 随机生成两个圆的坐标,在4个方向上 随机找到2个方向添加凸/凹 + # 凸/凹1 + face1 = random.randint(0, 4) + # 凸/凹2 + face2 = random.randint(0, 4) + # 保证两个凸/凹不在同一位置 + while face1 == face2: + face2 = random.randint(0, 4) + # 获取凸/凹起位置坐标 + circle1 = get_circle_coords(face1, block_width, block_height, block_radius) + circle2 = get_circle_coords(face2, block_width, block_height, block_radius) + # 随机凸/凹类型 + shape = random.randint(0, 1) + # 圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆 + # 计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色 + for i in range(block_width): + for j in range(block_height): + # 创建中间的方形区域 + if ( + i >= block_radius and i <= block_width - block_radius and j >= block_radius and j <= block_height - block_radius): + data[i][j] = 1 + double_d1 = math.pow(i - circle1[0], 2) + math.pow(j - circle1[1], 2) + double_d2 = math.pow(i - circle2[0], 2) + math.pow(j - circle2[1], 2) + # 创建两个凸/凹 + if double_d1 <= po or double_d2 <= po: + data[i][j] = shape + return data + + +# 根据朝向获取圆心坐标 +def get_circle_coords(face, block_width, block_height, block_radius): + """ + 根据传入的face值(0表示上,1表示左,2表示下,3表示右), + 返回一个包含两个整数的数组,分别表示圆心的 横坐标 和 纵坐标。 + """ + if face == 0: # 上 + return [block_width // 2 - 1, block_radius] + elif face == 1: # 左 + return [block_radius, block_height // 2 - 1] + elif face == 2: # 下 + return [block_width // 2 - 1, block_height - block_radius - 1] + # elif face == 3: # 右 + # return [block_width - block_radius - 1, block_height // 2 - 1] + else: + return [block_width - block_radius - 1, block_height // 2 - 1] # face == 4, 按3处理 + + +# 在画布上添加阻塞块水印 +def add_block_watermark(canvas_image, block_image, x, y): + draw = ImageDraw.Draw(canvas_image) + canvas_image.paste(block_image, (x, y), block_image) + + +# 将图片转换为base64 +def image_to_base64(image): + buffered = io.BytesIO() + image.save(buffered, format="png") + img_str = base64.b64encode(buffered.getvalue()) + return img_str.decode('utf-8') diff --git a/apps/rbac/views/SubAccount.py b/apps/rbac/views/SubAccount.py new file mode 100644 index 0000000..d9319f4 --- /dev/null +++ b/apps/rbac/views/SubAccount.py @@ -0,0 +1,393 @@ +import logging +import random +import string +import traceback +import uuid + +from django.db import transaction +from django.db.models import Q +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans import settings +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR, BAD, OK +from ChaCeRndTrans.settings import RELEASE_DOMAIN, TEST_DOMAIN +from rbac.models import UserProfile, Role, Company +from rbac.serializers.role_serializer import RoleListSerializer +from rbac.serializers.user_serializer import UserListSerializer, CompanyRoleSerializer, UserCreateSerializer, \ + UserModifySerializer +from utils.custom import CustomViewBase, sha1_encrypt, CommonPagination, is_valid_username + +logger = logging.getLogger('error') + + +class SubAccountViewSet(CustomViewBase): + ''' + 子账号 + ''' + perms_map = ( + {"*": "admin"}, + {"*": "sub_account_all"}, + {"get": "sub_account_list"}, + {"post": "sub_account_create"}, + {"put": "sub_account_edit"}, + {"delete": "sub_account_delete"} + ) + queryset = UserProfile.objects.all() + serializer_class = UserListSerializer + pagination_class = CommonPagination + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + filter_fields = ("is_active",) + search_fields = ("username", "name", "mobile", "email") + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + + def get_serializer_class(self): + # 根据请求类型动态变更serializer + if self.action == "create": + return UserCreateSerializer + elif self.action == "list": + return UserListSerializer + return UserModifySerializer + + def get_queryset(self): + """ + 我的企业-子账号 + 1.后台人员直接查看当前公司所有子用户 + 2.前台用户只能查看当前公司,当前用户的子用户 + """ + companyMid = self.request.query_params.get('companyMid', None) + parent_id = self.request.query_params.get('parent_id', None) + queryset = UserProfile.objects.all() + query = Q() + if 1 == self.request.user.label: # 前台用户 + if parent_id: # 查询当前用户的子账号 + query &= Q(parent_id=parent_id) + if companyMid: # 且子账号需要有关联本公司 + query &= Q(company__MainId=companyMid) + else: # 后台用户 + if companyMid: # 且子账号需要有关联本公司 + query &= Q(company__MainId=companyMid) + query &= ~Q(parent_id=None) + queryset = queryset.filter(query) + return queryset + + # def list(self, request, *args, **kwargs): + # pagination = {} + # try: + # params = request.GET + # page_size = params.get('size', None) + # page = params.get('page', None) + # name = params.get("name", None) # 子账号真实姓名 + # mobile = params.get("mobile", None) # 手机号码 + # is_active = params.get("is_active", None) # 是否激活 + # if page is None: + # page = 1 + # if page_size is None: + # page_size = 10 + # start_index = (int(page) - 1) * int(page_size) + # + # parent_id = params.get("parent_id", None) + # if parent_id is None: + # return CCAIResponse("缺少参数parent_id", BAD) + # main_account = UserProfile.objects.filter(id=parent_id).first() + # count = 0 + # row = [] + # sql = """ + # select U.id, U.username, U.is_active, U.mobile, U.name + # from rbac_userprofile as U + # where U.is_sub = 1 and U.parent_id = {} + # """.format(parent_id) + # sql_count = """ + # select U.id, count(U.id) as count + # from rbac_userprofile as U + # where U.is_sub = 1 and U.parent_id = {} + # """.format(parent_id) + # if name: + # temp = r""" and U.name = '{}' """.format(name) + # sql = sql + temp + # sql_count = sql_count + temp + # if mobile: + # temp = r""" and U.mobile = '{}' """.format(mobile) + # sql = sql + temp + # sql_count = sql_count + temp + # if is_active: + # temp = r""" and U.is_active = '{}' """.format(is_active) + # sql = sql + temp + # sql_count = sql_count + temp + # sql = sql + """ ORDER BY U.id desc limit {}, {} """.format(start_index, int(page_size)) + # sub_account = UserProfile.objects.raw(sql) + # count_result = UserProfile.objects.raw(sql_count) + # if len(count_result) > 0: + # count = count_result[0].count + # if sub_account: + # for item in sub_account: + # item.__dict__.pop('_state') + # row.append(item.__dict__) + # return CCAIResponse(data=row, count=count) + # except Exception as e: + # logger.error("get sub account list failed: \n%s" % traceback.format_exc()) + # return CCAIResponse("获取子账号列表失败", SERVER_ERROR) + + def list(self, request, *args, **kwargs): + try: + queryset = self.filter_queryset(self.get_queryset().filter().all()) + page = self.paginate_queryset(queryset) + domain = RELEASE_DOMAIN + if settings.DEVELOP_DEBUG: + domain = TEST_DOMAIN + if page is not None: + serializer = self.get_serializer(page, many=True) + return_data = serializer.data + # for item in return_data: + # start = item['image'].rindex('/media/') + # item['image'] = domain + item['image'][start:] + return self.get_paginated_response(return_data) + + serializer = self.get_serializer(queryset, many=True) + return_data = serializer.data + for item in return_data: + start = item['image'].rindex('/media/') + item['image'] = domain + item['image'][start:] + return CCAIResponse(data=return_data, status=200) + except Exception as e: + logger.error("get sub account list failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='获取子用户列表失败', status=SERVER_ERROR) + def create(self, request, *args, **kwargs): + try: + companyMid = request.query_params.get('companyMid', None) + data = request.data.copy() + company = data.get('company') + roles = data.get('roles') + + if request.user.is_sub == 1: + return CCAIResponse("本账号为子账号,不允许开设子账号!", BAD) + + # 验证公司信息是否有效 + # if companyMid: + # company = Company.objects.filter(MainId=companyMid).first() + + # if roles: + # roleid_list = roles.split(',') + # + # if companies: + # companies_list = companies.split(',') + + # if not data['parent_id']: + # return CCAIResponse(data="缺少参数parent_id", status=BAD) + user_count = UserProfile.objects.filter(username=data['username']).count() + if user_count > 0: + return CCAIResponse(data="用户名冲突,请重新输入", msg="用户名冲突,请重新输入", status=BAD) + + if data['mobile']: + user_count = UserProfile.objects.filter(mobile=data['mobile']).count() + if user_count > 0: + return CCAIResponse(data="手机号码已被注册,请校对手机号码", msg="手机号码已被注册,请校对手机号码", + status=BAD) + if data['username']: + if not is_valid_username(data['username']): + return CCAIResponse(data="用户名命名不符合规定,只能使用汉字、英文字母、数字,不能使用特殊字符。", + msg="用户名命名不符合规定,只能使用汉字、英文字母、数字,不能使用特殊字符。", + status=BAD) + with transaction.atomic(): + # 生成用户与公司的全局id + userMid = uuid.uuid4().__str__() + # 创建用户 + user = UserProfile() + user.MainId = userMid + user.companyMid = companyMid # 默认绑定当前公司 + user.username = data['username'] + user.name = data['username'] + user.mobile = data['mobile'] + user.is_sub = 1 # 子账号账号 + user.is_active = 1 # 注册即激活 + user.label = 1 # 前台用户 + user.parent_id = request.user.id + # 创建随机密码,并返回 + random_password = ''.join(random.sample(string.ascii_letters + string.digits, 8)) + password = sha1_encrypt(random_password) + user.set_password(password) + user.save() + + com_list = Company.objects.filter(id__in=company) + for c in com_list: + # 为注册用户添加公司 + user.company.add(c) + + # 添加默认角色测试服29 / 正式服 32 + if settings.DEBUG: + role = Role.objects.get(id=29) + else: + role = Role.objects.get(id=32) + user.roles.add(role) + + if roles and len(roles) > 0: + # 为子账号添加角色 + roles = Role.objects.filter(id__in=roles).all() + for role in roles: + user.roles.add(role) + + return CCAIResponse(data=random_password, status=OK) + except Exception: + logger.error("user:%s, add sub account failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("创建子账号失败", SERVER_ERROR) + + def update(self, request, *args, **kwargs): + try: + data = request.data.copy() + + partial = kwargs.pop('partial', False) + if 'parent_id' not in data and data['parent_id'] is None: + return CCAIResponse("缺少参数parent_id", BAD) + # roles = data.get('roles') + # companies = data.get('companies') + # if roles: + # roles = roles.split(',') + # data['roles'] =roles + # if companies: + # companies = companies.split(',') + # data['companies'] =companies + + instance = self.get_object() + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + user = UserProfile.objects.filter(id=data['id']).first() + if user is None: + return CCAIResponse("账号不存在", SERVER_ERROR) + if data['mobile']: + user_count = UserProfile.objects.filter(mobile=data['mobile']).exclude(id=data['id']).count() + if user_count > 0: + return CCAIResponse(data="手机号码已被注册,请校对手机号码", msg="手机号码已被注册,请校对手机号码", + status=BAD) + if data['username']: + if not is_valid_username(data['username']): + return CCAIResponse(data="用户名格式不对,只能输入汉字,英文字母,数字!", + msg="用户名格式不对,只能输入汉字,英文字母,数字!", status=BAD) + user_count = UserProfile.objects.filter(username=data['username']).exclude(id=data['id']).count() + if user_count > 0: + return CCAIResponse("该用户名已被注册,请校对用户名", status=BAD) + # if data['ratio']: + # ratio = Decimal(data['ratio']) # 子账号的分销比例 = 父账号的分销比例 * 父账号为其分配的分销比例 + # user.ratio = ratio + user.username = data['username'] + user.mobile = data['mobile'] + user.save() + return CCAIResponse("success") + except Exception as e: + logger.error("user:%s, update sub account failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("更新子账号失败", SERVER_ERROR) + + def destroy(self, request, *args, **kwargs): + try: + with transaction.atomic(): + instance = self.get_object() + self.perform_destroy(instance) + users = UserProfile.objects.filter(username=instance.username) + for user in users: + user.delete() + return CCAIResponse(data="删除账号成功") + except Exception as e: + logger.error("user:%s, delete sub account failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(data="删除子账号失败", msg="删除子账号失败", status=SERVER_ERROR) + + @action(methods=["POST"], detail=False, permission_classes=[IsAuthenticated], + url_path="disable_sub_user", url_name="disable_sub_user") + def disable_sub_user(self, request): + try: + data = request.data.copy() + if 'uid' not in data and data['uid'] is None: + return CCAIResponse(data="缺少参数uid", msg="缺少参数uid", status=BAD) + if 'is_active' not in data: + return CCAIResponse(data="缺少参数is_active", msg="缺少参数is_active", status=BAD) + user = UserProfile.objects.filter(id=data['uid']).first() + if user is None: + return CCAIResponse(data="账号不存在", msg="账号不存在", status=SERVER_ERROR) + if data['is_active']: + user.is_active = 1 + else: + user.is_active = 0 + user.save() + return CCAIResponse(data="修改子账号激活状态") + except Exception as e: + logger.error( + "user: %s disable sub user failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='修改子账号激活状态失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="reset_sub_password", url_name="reset_sub_password") + def reset_subAccount_password(self, request, pk=None): + try: + data = request.data.copy() + if 'uid' not in data and data['uid'] is None: + return CCAIResponse(data="缺少参数uid", msg="缺少参数uid", status=BAD) + user = UserProfile.objects.get(id=data['uid']) + if user and request.user.is_sub == 2: + with transaction.atomic(): + random_password = ''.join(random.sample(string.ascii_lowercase + string.digits, 8)) + password = sha1_encrypt(random_password) + user.set_password(password) + user.save() + return CCAIResponse(data=random_password, status=OK) + else: + return CCAIResponse(data="子账号异常,请联系管理员", status=BAD) + except Exception as e: + logger.error("main account reset sub account password failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='修改子账号密码失败', status=SERVER_ERROR) + + # @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + # url_path="set_sub_ratio", url_name="set_sub_ratio") + # def set_subAcount_ratio(self, request, pk=None): + # try: + # data = request.data.copy() + # if 'uid' not in data and data['uid'] is None: + # return CCAIResponse(data="缺少参数uid", msg="缺少参数uid", status=BAD) + # if 'ratio' not in data and data['ratio'] is None: + # return CCAIResponse(data="缺少参数ratio", msg="缺少参数ratio", status=BAD) + # user = UserProfile.objects.get(id=data['uid']) + # if user and request.user.is_sub == 2: + # user.ratio = data['ratio'] + # user.save() + # return CCAIResponse(data="修改子账号佣金分销比例成功!", msg="修改子账号佣金分销比例成功!", status=OK) + # except Exception as e: + # logger.error("main account set sub account ratio failed: \n%s" % traceback.format_exc()) + # return CCAIResponse(msg='修改子账号佣金分销比例失败', status=SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getCompanyAndRole", url_name="getCompanyAndRole") + def getCompanyAndRole(self, request): + """ + 获取当前用户的关联公司与该公司的角色,及系统默认提供的角色(除去后台角色label=2) + """ + try: + + # 获取公司 + companyList = request.user.company.all() + companyMainIdList = [item.MainId for item in companyList] + + # 获取角色 + rolelist = Role.objects.filter( + ~Q(label=2) & (Q(companyMid__in=companyMainIdList) | Q(companyMid__isnull=True))) + + sysRole = set() # 系统默认角色 + # 将角色封装到对应的公司中 (当前方法时间复杂度较高,有提升空间) + for com in companyList: + com.roles = [] + for role in rolelist: + if role.companyMid and role.companyMid != '': + if role.companyMid == com.MainId: + com.roles.append(role) + else: + sysRole.add(role) + data = { + 'sysRole': RoleListSerializer(sysRole, many=True).data, + 'comRole': CompanyRoleSerializer(companyList, many=True).data, + } + return CCAIResponse(data) + except Exception as e: + logger.error("getCompanyAndRole failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='获取公司与角色映射失败', status=SERVER_ERROR) diff --git a/apps/rbac/views/__init__.py b/apps/rbac/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rbac/views/company.py b/apps/rbac/views/company.py new file mode 100644 index 0000000..5a5cd55 --- /dev/null +++ b/apps/rbac/views/company.py @@ -0,0 +1,560 @@ +import copy +import logging +import time +import traceback +import uuid +from itertools import islice + +import openpyxl +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.db.models import Q +from django.http import FileResponse +from django_filters.rest_framework import DjangoFilterBackend +from openpyxl.comments import Comment +from openpyxl.worksheet.cell_range import CellRange +from openpyxl.worksheet.datavalidation import DataValidation +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans import settings +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import * +from common.models import DataDictionaryDetail, DataDictionary +from common.serializers.dict_serializer import DictDetailSerializer +from rbac.models import Company, UserProfile +from rbac.serializers.user_serializer import CompanySerializer +from utils.custom import CustomViewBase, CommonPagination, RbacPermission, req_operate_by_user, asyncDeleteFile, \ + ErrorTable, MyCustomError, time_int_to_str, time_str_to_int + +err_logger = logging.getLogger('error') + + +class CompanyCustomViewSet(CustomViewBase): + ''' + 公司管理 + ''' + perms_map = ( + {"*": "admin"}, {"*": "company_all"}, {"get": "company_list"}, + {"post": "company_create"}, + {"put": "company_edit"}, {"delete": "company_delete"} + ) + queryset = Company.objects.all() + serializer_class = CompanySerializer + pagination_class = CommonPagination + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + # filter_fields = ("",) + search_fields = ("name", "EUCC") + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + def list(self, request, format=None): + pagination = {} + try: + params = request.GET + keyword = params.get('keyword') + page_size = params.get('size') + page = params.get('page') + sort = params.get('sort') + + order_by = 'N.id desc' + if sort: + order_by = sort + + if page is None: + page = 1 + if page_size is None: + page_size = 10 + start_index = (int(page) - 1) * int(page_size) + where = 'WHERE 1=1 ' + param = [] + if keyword: + where += 'AND (N.name LIKE %s OR N.EUCC LIKE %s ) ' + param.append('%' + keyword + '%') + param.append('%' + keyword + '%') + + sql = 'SELECT N.id, N.name, N.MainId, userMid, U.name as user, U.username,' \ + 'EUCC, employeesCount, hightechCode, hightechDate, AmStart, AmEnd, PmStart, PmEnd, tech, tolerance, checkIn ' \ + 'FROM chace_rnd.rbac_company AS N ' \ + 'LEFT JOIN chace_rnd.rbac_userprofile as U ON U.MainId = N.userMid ' \ + '%s ORDER BY %s LIMIT %s,%s ' % (where, order_by, start_index, page_size) + query_rows = Company.objects.raw(sql, param) + + count_sql = """ select N.id, count(N.id) as count + from chace_rnd.rbac_company AS N %s """ % where + count_result = Company.objects.raw(count_sql, param) + + count = 0 + if len(count_result) > 0: + count = count_result[0].count + rows = [] + for item in query_rows: + item.__dict__.pop('_state') + if item.__dict__['tech'] and item.__dict__['tech'] != '': + item.__dict__['tech'] = item.__dict__['tech'].split(',') + if not item.__dict__['user'] or item.__dict__['user'] == '': + item.__dict__['user'] = item.__dict__['username'] + item.__dict__['tolerance'] = time_int_to_str(item.__dict__['tolerance']) + rows.append(item.__dict__) + + pagination = { + "page": page, + "page_size": page_size + } + + return CCAIResponse(rows, count=count, pagination=pagination) + + except Exception as e: + err_logger.error("user: %s, get company list failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取公司列表失败", SERVER_ERROR) + + @action(methods=["get"], detail=True, permission_classes=[IsAuthenticated], + url_path="getAssociationUser", url_name="getAssociationUser") + def getAssociationUser(self, request, pk=None): + """ + 获取该公司关联的用户列表 + """ + try: + if not pk: + return CCAIResponse("请选择对应公司", BAD) + instance = Company.objects.get(id=int(pk)) + user_list = UserProfile.objects.filter(company=instance).values() + return CCAIResponse(user_list) + except Company.DoesNotExist: + return CCAIResponse("该公司已被删除!", BAD) + except Exception as e: + err_logger.error("user: %s, get getAssociationUser list failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取该公司关联的用户列表失败", SERVER_ERROR) + + def create(self, request, *args, **kwargs): + """ + 直接新增公司 + """ + try: + data = req_operate_by_user(request) + # 用户选择为其添加公司的用户 + userMid = data.get('userMid') + if not userMid: + return CCAIResponse("Missing User Information", BAD) + user = UserProfile.objects.filter(MainId=userMid).first() + if not user: + return CCAIResponse("User Information Error", BAD) + companyMid = uuid.uuid4().__str__() + data['MainId'] = companyMid + tech = data.get('tech') # 技术领域 + if tech: + data['tech'] = ','.join(tech) + tolerance = data.get('tolerance') # 容错时间 + if tolerance: + data['tolerance'] = time_str_to_int(tolerance) + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + with transaction.atomic(): + company = self.perform_create(serializer) + user.company.add(company) + # headers = self.get_success_headers(serializer.data) + return CCAIResponse(data="success") + + except Exception as e: + err_logger.error("user: %s, create company failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("直接新增公司失败", SERVER_ERROR) + + def perform_create(self, serializer): + # 重写序列化创建,返回创建的公司对象 + return serializer.save() + + def update(self, request, *args, **kwargs): + """ + 如果用户修改拥有者 + """ + try: + data = req_operate_by_user(request) + tech = data.get('tech') + tolerance = data.get('tolerance') # 容错时间 + userMid = data.get('userMid') + if tech: + data['tech'] = ','.join(str(x) for x in tech) + if tolerance: + data['tolerance'] = time_str_to_int(tolerance) + partial = kwargs.pop('partial', False) # True:所有字段全部更新, False:仅更新提供的字段 + instance = self.get_object() + with transaction.atomic(): + # 检查是否更改拥有者 + flag = False + if userMid and instance.userMid != userMid: + flag = True + new_user = UserProfile.objects.filter(MainId=userMid).select_for_update().first() # 新的拥有者 + old_user = UserProfile.objects.filter(MainId=instance.userMid).select_for_update().first() # 旧的拥有者 + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + if flag: + if not new_user: + transaction.set_rollback(True) + return CCAIResponse("用户已被删除!", BAD) + else: + new_user.company.add(instance) + if old_user: + old_user.company.remove(instance) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return CCAIResponse(data="success") + except Exception as e: + err_logger.error("user: %s, update company Info failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("修改公司信息失败列表失败", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getActiveUser", url_name="getActiveUser") + def getActiveUser(self, request): + """ + 获取主账号可用用户列表,供公司拥有者选择 + """ + try: + users = UserProfile.objects.filter(is_sub=2, is_active=1, MainId__isnull=False).values("id", "name", "username", "MainId") + return CCAIResponse(users) + except Exception as e: + err_logger.error("user: %s, getActiveUser failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取可选择的公司拥有者列表失败", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getCompanyList", url_name="getCompanyList") + def getCompanyList(self, request): + """ + 后台用户获得公司列表 + """ + try: + if 2 == request.user.label: + companies = Company.objects.all().values('id', 'name') + result = [item for item in companies] + else: + return CCAIResponse() + return CCAIResponse(result) + except Exception as e: + err_logger.error("user: %s, getUserCompanyInfo failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取用户公司信息失败", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getMainUserList", url_name="getMainUserList") + def getMainUserList(self, request): + """ + 获取主账户id,username,name,MainId + """ + try: + keyword = request.GET.get('keyword') + query = Q(is_sub=2) + if keyword: + query &= (Q(username__icontains=keyword) | Q(name__icontains=keyword)) + mainUser = UserProfile.objects.filter(query).values('id', 'username', 'name', 'MainId') + return CCAIResponse(mainUser) + except Exception as e: + err_logger.error("user: %s, getMainUser list failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取主用户列表失败", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getUserCompanyInfo", url_name="getUserCompanyInfo") + def getUserCompanyInfo(self, request): + try: + params = request.GET + companyMid = params.get('companyMid') + query = Q(MainId=companyMid) + try: + company = Company.objects.get(query) + except ObjectDoesNotExist: + return CCAIResponse('缺少公司参数', BAD) + data = CompanySerializer(company).data + if data.get('tech'): + data['tech'] = [int(x) for x in data['tech'].split(',')] + if 'tolerance' in data: + data['tolerance'] = time_int_to_str(data['tolerance']) + return CCAIResponse(data) + except Exception as e: + err_logger.error("user: %s, getUserCompanyInfo failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取用户公司信息失败", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getTechnologyTree", url_name="getTechnologyTree") + def getTechnologyTree(self, request): + """ + 技术领域树 + """ + try: + dataDictionary = DataDictionary.objects.get(DictionaryCode='TechArea') + dataDictionaryDetail = DataDictionaryDetail.objects.filter( + DataDictionaryId=dataDictionary.DataDictionaryId).values('id', 'DataDictionaryDetailId', 'ParentId', + 'DictionaryCode', 'DictionaryValue', 'ParentCode', + 'FullName') + data = DictDetailSerializer(dataDictionaryDetail, many=True) + tree_dict = {} + for item in data.data: + if item["parent_id"] is None: + item["children"] = [] + top_permission = copy.deepcopy(item) + tree_dict[item["dict_detail_id"]] = top_permission + else: + children_permission = copy.deepcopy(item) + tree_dict[item["dict_detail_id"]] = children_permission + + tree_data = [] + for i in tree_dict: + if tree_dict[i]["parent_id"]: + pid = tree_dict[i]["parent_id"] + parent = tree_dict[pid] + parent.setdefault("children", []).append(tree_dict[i]) + # from operator import itemgetter + # parent["children"] = sorted(parent["children"], key=itemgetter("id")) + else: + tree_data.append(tree_dict[i]) + return CCAIResponse(tree_data, status=OK) + except Exception as e: + err_logger.error("user: %s, getUserCompanyInfo failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取用户公司信息失败", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="download", url_name="download") + def download(self, request): + """ + 导入模板下载 + """ + try: + # 查询公司的项目列表 + companymid = request.GET.get('companyMid') + if not companymid: + return CCAIResponse('Missing or invalid company information', BAD) + company = Company.objects.filter(MainId=companymid).first() + if not company: + return CCAIResponse('Company param error', BAD) + + users = UserProfile.objects.filter(is_sub=2, is_active=1, MainId__isnull=False).values("id", "name", "username", "MainId") + username_user = [str((item['name'] if item['name'] else '') + '(' + item['username'] + ')') for item in users] # 使用用户姓名(账号) 做唯一 + + title_data = ["*公司名称", "*拥有者", "企业统一信用代码", "高企编码", "企业员工总数"] + name = str(int(time.time() * 10000)) # 时间戳命名 + workbook = openpyxl.Workbook() + worksheet = workbook.active + worksheet.title = "公司导入模板" + + # 添加隐藏的选项参考表 + worksheet2 = workbook.create_sheet("选项参考表") + worksheet2.sheet_state = "hidden" # 将工作表隐藏起来 + worksheet2.cell(1, 1, '拥有者参考') + + # 写入拥有者参考数据 + for idx, project in enumerate(username_user, start=2): # 从第二行开始写入数据 + worksheet2.cell(idx, 1, str(project)) + + for index, item in enumerate(title_data, start=1): + worksheet.cell(1, index, item) + worksheet['A1'].comment = Comment("必填项,请输入正确的公司名称!", 'Author') + worksheet['B1'].comment = Comment("必填项,请从提供的选项中选择!", 'Author') + + # 创建一个数据有效性验证对象 + usersDv = DataValidation(type="list", formula1=f'=选项参考表!$A$2:$A${len(username_user) + 1}', showErrorMessage=True, errorTitle="输入错误", + error="请从给定的选项中选择") + + # 数据有效性验证应用 + worksheet.add_data_validation(usersDv) + + usersDv.add(CellRange("B2:B10000")) + + # 设置第一列的数据格式为文本类型 + worksheet.column_dimensions['A'].number_format = '@' + worksheet.column_dimensions['C'].number_format = '@' + worksheet.column_dimensions['D'].number_format = '@' + + # worksheet.cell(2, 10).value = '=H2*I2' + # for row in range(2, 10000): + # worksheet.cell(row, 10).value = '=IF(ISBLANK(H{}) OR ISBLANK(I{}), "", H{}*I{})'.format(row, row, row, row) + + workbook.save(filename=name + ".xlsx") + fileName = name + ".xlsx" + file = open(fileName, 'rb') + s_name = "公司导入模板.xlsx" + response = FileResponse(file) + response['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' # 规定返回xlsx + response['Content-Disposition'] = f'attachment;filename="{s_name}"' + response['file_name'] = fileName + + # 异步删除文件 + threadPool.submit(asyncDeleteFile, request, fileName) + + return response + except Exception as e: + err_logger.error("user: %s, get company template file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("下载公司导入模板失败", SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="import_excel", url_name="import_excel") + def import_excel(self, request): + """导入excel表数据""" + try: + data = req_operate_by_user(request) + file_name = data.get('file_name') + + if not file_name: + return CCAIResponse('Missing params', BAD) + + + users = UserProfile.objects.filter(is_sub=2, is_active=1, MainId__isnull=False).values("id", "name", "username", "MainId") + username_user = {str((item['name'] if item['name'] else '') + '(' + item['username'] + ')'): item['MainId'] for item in users} # 使用用户姓名(账号) 做唯一 + + company_list = [] + excel_file_list = [] + + file_paths = file_name.split(",") + for key in range(len(file_paths)): + if file_paths[key] == '' or file_paths[key] is None: + continue + excel_file = file_paths[key].replace(settings.SHOW_UPLOAD_PATH, settings.FILE_PATH) + excel_file_list.append(excel_file) + + # 打开工作文件 + workbook = openpyxl.load_workbook(excel_file, data_only=True) + table = workbook['公司导入模板'] + # table = workbook.active + rows = table.max_row # 总行数 + check_value = [table.cell(row=1, column=i).value for i in range(1, 4)] # 获取第一行的值 + if check_value[0] != '*公司名称' and check_value[1] != '*拥有者' and check_value[2] != '*企业统一信用代码': + return CCAIResponse("文件不符合模板标准", SERVER_ERROR) + user_data = [] # 项目编号列 + for row in islice(table.iter_rows(values_only=True), 1, None): + if row[1]: + user_data.append(row[0]) # 项目编号列 + + try: + columns = ["companyName", "userName", "EUCC", "hightechCode", "employeesCount"] + columns_name = ["企业名称", "拥有者", "企业同一信用代码", "高企编码", "员工人数"] + errorTable = ErrorTable(columns) + # 循环外层控制事务,因为物料记录的创建依赖与项目分摊的创建 + with transaction.atomic(): + for rindex, row in enumerate(islice(table.iter_rows(values_only=True), 1, None)): + # 跳过空白隐藏行 + if all(cell is None for cell in row): + continue + + row_value = [r for r in row] + check_index = [0, 1,] + # 校验必填项 + skip = False + for index in check_index: # 前2项为必填项 + if row[index] is None or row[index] == '': + skip = True + errorTable.add_one_row(row_value, f'{columns_name[index]}为必填项') + break + if skip: + skip = False + continue + + if str(row[1]) not in username_user: + errorTable.add_one_row(row_value, f'用户错误') + continue + + # 获取对应的用户 + userMid = username_user[str(row[1])] + try: + user = UserProfile.objects.get(MainId=userMid) + except UserProfile.DoesNotExist: + errorTable.add_one_row(row_value, f'用户错误') + continue + # 新增公司 + companyMid = uuid.uuid4().__str__() # 公司mid + company = Company() + company.name = str(row_value[0]) + company.MainId = companyMid + company.userMid = userMid + if row_value[2]: + company.EUCC = str(row_value[2]) + if row_value[3]: + company.hightechCode = str(row_value[3]) + company.employeesCount = int(row_value[4]) if row_value[4] else 0 + + company.save() + user.company.add(company) + + if errorTable.has_data(): # 存在错误信息,回滚并返回所有错误行的提示信息 + transaction.set_rollback(True) + return CCAIResponse(errorTable.get_table(), 200) + + except MyCustomError as e: + return CCAIResponse(f'文件读取第{rindex}行数据多于标题列', BAD) + except Exception as e: + err_logger.error("user: %s, 文件路径:%s 记录公司导入错误日志: \n%s" % (request.user.id, excel_file, traceback.format_exc())) + return CCAIResponse('数据有误,请检查后重新导入', BAD) + + try: + for file_path in excel_file_list: + # 异步删除文件 + threadPool.submit(asyncDeleteFile, request, file_path) + # if os.path.exists(file_path): # 如果文件存在 + # # 删除文件,可使用以下两种方法。 + # os.remove(file_path) + # # 删除空目录,不是空目录时候rmdir不会删除 + # # try: + # # os.rmdir(file_path) + # # except Exception as e: + # # logger.error("user: %s, rmdir highTech failed: \n%s" % (request.user.id, traceback.format_exc())) + + except Exception as e: + err_logger.error("user: %s, import_excel view create failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse("导入存入失败", SERVER_ERROR) + + return CCAIResponse(data="success") + except Exception as e: + err_logger.error("user: %s, import aigc file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("导入公司名单失败", SERVER_ERROR) + + + +class CompanyNameListAPIView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + ''' + 获取公司名称列表 + ''' + # throttle_classes = (AnonRateThrottle, UserRateThrottle) + + def get(self, request, format=None): + blurry_company_info_req = '' + try: + params = request.GET + company_name = params.get('company_name') + + rows = [] + qixinbao_failed = False + if company_name: + blurry_company_info_req = getBlurryCompanyInfo(request, company_name, 0) + if blurry_company_info_req: + base_info = blurry_company_info_req.json() + if base_info['status'] == '200': + companys = base_info['data']['items'] + for company in companys: + dict = {'value': company['name']} + rows.append(dict) + + # rows = [ + # {'value':"深圳市骏鼎达新材料股份有限公司"}, + # {'value':"东莞市骏鼎达新材料科技有限公司"}, + # {'value':"昆山骏鼎达电子科技有限公司"}, + # {'value':"苏州骏鼎达新材料科技有限公司"}, + # {'value':"江门骏鼎达新材料科技有限公司"}, + # {'value':"深圳市骏鼎达新材料股份有限公司重庆分公司"}, + # {'value':"深圳市骏鼎达新材料股份有限公司武汉分公司"}, + # {'value':"龙川县骏鼎达新材料有限公司"}, + # {'value':"佛山市骏鼎达五金有限公司"}, + # {'value':"駿鼎達國際有限公司"} + # ] + + return CCAIResponse(rows) + + except Exception as e: + logger.error("user: %s, get blurry company failed: %s, detail: %s" % (request.user.id, traceback.format_exc(), blurry_company_info_req)) + return CCAIResponse("获取地区失败", SERVER_ERROR) \ No newline at end of file diff --git a/apps/rbac/views/menu.py b/apps/rbac/views/menu.py new file mode 100644 index 0000000..a77106d --- /dev/null +++ b/apps/rbac/views/menu.py @@ -0,0 +1,138 @@ +import logging +import traceback + +from rest_framework.viewsets import ModelViewSet + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import OK +from utils.custom import CommonPagination, RbacPermission, TreeAPIView, CustomViewBase +from ..models import Menu +from ..serializers.menu_serializer import MenuSerializer +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from operator import itemgetter + +err_logger = logging.getLogger('error') + + +class MenuViewSet(ModelViewSet, TreeAPIView): + """ + 菜单管理:增删改查 + """ + perms_map = ({"*": "admin"}, {"*": "menu_all"}, {"get": "menu_list"}, {"post": "menu_create"}, {"put": "menu_edit"}, + {"delete": "menu_delete"}) + queryset = Menu.objects.all() + serializer_class = MenuSerializer + pagination_class = CommonPagination + filter_backends = (SearchFilter, OrderingFilter) + search_fields = ("name",) + ordering_fields = ("sort",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + +class MenuTreeView(TreeAPIView): + """ + 菜单树 + """ + queryset = Menu.objects.all() + + def list(self, request, *args, **kwargs): + """ + 根据是否前后台用户返回除去内部权限 + """ + if 1 == request.user.label: # 前台用户 + self.queryset = Menu.objects.filter(label=1).distinct() + response = super().list(request, *args, **kwargs) + return response + + +def get_all_menu_dict(): + """ + 获取所有菜单数据,重组结构 + """ + try: + menus = Menu.objects.all() + serializer = MenuSerializer(menus, many=True) + tree_dict = {} + for item in serializer.data: + if item["pid"] is None: + if item["is_frame"]: + # 判断是否外部链接 + top_menu = { + "id": item["id"], + "path": item["path"], + "component": "Layout", + "children": [{ + "path": item["path"], + "meta": { + "name": item["name"], + "icon": item["icon"] + } + }], + "pid": item["pid"], + "sort": item["sort"] + } + else: + top_menu = { + "id": item["id"], + "route_name": item["route_name"], + "path": "/" + item["path"], + "redirect": "noredirect", + "component": "", + "alwaysShow": True, + "meta": { + "name": item["name"], + "icon": item["icon"] + }, + "pid": item["pid"], + "sort": item["sort"], + "children": [] + } + tree_dict[item["id"]] = top_menu + else: + if item["is_frame"]: + children_menu = { + "id": item["id"], + "route_name": item["route_name"], + "path": item["path"], + "component": "Layout", + "meta": { + "name": item["name"], + "icon": item["icon"], + }, + "pid": item["pid"], + "sort": item["sort"] + } + elif item["is_show"]: + children_menu = { + "id": item["id"], + "route_name": item["route_name"], + "path": item["path"], + "component": item["component"], + "meta": { + "name": item["name"], + "icon": item["icon"], + }, + "pid": item["pid"], + "sort": item["sort"] + } + else: + children_menu = { + "id": item["id"], + "route_name": item["route_name"], + "path": item["path"], + "component": item["component"], + "meta": { + "name": item["name"], + "noCache": True, + }, + "hidden": True, + "pid": item["pid"], + "sort": item["sort"] + } + tree_dict[item["id"]] = children_menu + return tree_dict + except Exception as e: + err_logger.error("get all menu from role failed: \n%s" % traceback.format_exc()) + return tree_dict diff --git a/apps/rbac/views/message.py b/apps/rbac/views/message.py new file mode 100644 index 0000000..11b92f4 --- /dev/null +++ b/apps/rbac/views/message.py @@ -0,0 +1,105 @@ +import datetime +import traceback +import requests, logging, random, re, json + +from rest_framework import status +from rest_framework.throttling import AnonRateThrottle +from rest_framework.views import APIView +from django_redis import get_redis_connection + +from ChaCeRndTrans import settings +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR, BAD # PHONE_NO_BIND, PHONE_IS_BIND, NO_REGISTER_PHONE +from rbac.models import UserProfile + +logger = logging.getLogger('error') + +msg_redis_code = 'rndMsgCode' # 用户短信验证码缓存标志 + + +def random_str(): + _str = '1234567890' + return ''.join(random.choice(_str) for i in range(4)) + + +def send_message(phone, template_type): + msg_code = random_str() + key = msg_redis_code + phone + visit_time_key = "dodo_visit_" + phone # 用于控制一段时间后放行短信发送 + conn = get_redis_connection('default') + visit_key = conn.get(visit_time_key) + + if visit_key is None: + conn.incrby(visit_time_key, 1) + else: + visit_count = int(conn.get(visit_time_key).decode('utf8')) + if visit_count < 10: + conn.incrby(visit_time_key, 1) + else: + # is_ttl = conn.ttl(visit_time_key) # 获取剩余时间,秒 + # if is_ttl != -1: + # return CCAIResponse(msg="发送短信频繁,请稍后再发送。", status=BAD) + conn.expire(visit_time_key, 60) + return CCAIResponse(data="发送短信频繁,请稍后再发送。", msg="发送短信频繁,请稍后再发送。", status=BAD) + # 验证码 + conn.hset(key, 'code', msg_code) + conn.expire(key, 300) + conn.expire(visit_time_key, 300) # 保证5分钟以后这个键会消失 + url = settings.MSG_URL + if settings.DEVELOP_DEBUG: + url = settings.TEST_MSG_URL + # params = '{"name":"szccwl","pwd":"md","address":"bz","phone":"13000000000"}' + params = {} + params['username'] = 'ccw' + params['password'] = 'chacewang123456' + params['phone'] = phone + params['template_type'] = template_type + args = '{"code":"%s"}' % msg_code + params['args'] = args + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + try: + count = 0 + flag = False + result = {} + response = {} + while count < 3 and flag is False: + response = requests.request("GET", url, headers=headers, data=params) + if response.status_code == 200: + flag = True + else: + count = count + 1 + if flag is True: + result = json.loads(response.text) + if 'code' in result.keys() and result['code'] == 200: + return CCAIResponse('success') + else: + return CCAIResponse(data="发送短信失败", msg="发送短信失败!", status=BAD) + except Exception as e: + logger.error("get MessageView failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='发送验证码出错', status=SERVER_ERROR) + + +class MessageView(APIView): + """ + 发送验证码 + """ + + # throttle_classes = (AnonRateThrottle,) + def get(self, request, *args, **kwargs): + try: + params = request.GET + phone = params.get('phone') + if phone == None or phone == '': + return CCAIResponse(data="请输入正确的手机号", msg="手机号不能为空", status=status.HTTP_400_BAD_REQUEST) + ret = re.match(r"^1[3-9]\d{9}$", phone) + if ret: + # return CCAIResponse('success') + return send_message(phone, 'Ccw_TemplateType_Captcha') + else: + return CCAIResponse(data="请输入正确的手机号", msg="请输入正确的手机号", status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + logger.error("send MessageView failed: \n%s" % traceback.format_exc()) + return CCAIResponse(data="发送验证码出错", msg="发送验证码出错", status=SERVER_ERROR) diff --git a/apps/rbac/views/permission.py b/apps/rbac/views/permission.py new file mode 100644 index 0000000..9af2d7a --- /dev/null +++ b/apps/rbac/views/permission.py @@ -0,0 +1,95 @@ +import logging +import traceback + +from rest_framework.viewsets import ModelViewSet + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import OK +from ..models import Permission +from ..serializers.permission_serializer import PermissionListSerializer +from utils.custom import CommonPagination, RbacPermission, TreeAPIView, CustomViewBase +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from operator import itemgetter + +err_logger = logging.getLogger('error') + + +class PermissionViewSet(CustomViewBase, TreeAPIView): + """ + 权限:增删改查 + """ + perms_map = ({"*": "admin"}, {"*": "permission_all"}, {"get": "permission_list"}, {"post": "permission_create"}, + {"put": "permission_edit"}, {"delete": "permission_delete"}) + queryset = Permission.objects.all() + serializer_class = PermissionListSerializer + pagination_class = CommonPagination + filter_backends = (SearchFilter, OrderingFilter) + search_fields = ("name",) + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + def list(self, request, *args, **kwargs): + tree_dict = get_all_permission_dict() + tree_data = [] + for i in tree_dict: + if tree_dict[i]["pid"]: + pid = tree_dict[i]["pid"] + parent = tree_dict[pid] + parent.setdefault("children", []).append(tree_dict[i]) + parent["children"] = sorted(parent["children"], key=itemgetter("id")) + else: + tree_data.append(tree_dict[i]) + return CCAIResponse(tree_data, status=OK) + + +class PermissionTreeView(TreeAPIView): + """ + 权限树 + """ + queryset = Permission.objects.all() + + def list(self, request, *args, **kwargs): + """ + 根据是否前后台用户返回除去内部权限 + """ + if 1 == request.user.label: + self.queryset = Permission.objects.filter(label=1) + response = super().list(request, *args, **kwargs) + return response + + +def get_all_permission_dict(): + """ + 获取所有菜单数据,重组结构 + """ + try: + permission = Permission.objects.all() + serializer = PermissionListSerializer(permission, many=True) + tree_dict = {} + for item in serializer.data: + if item["pid"] is None: + top_permission = { + "id": item["id"], + "name": item["name"], + "method": item["method"], + "pid": item["pid"], + "label": item["label"], + "children": [] + } + tree_dict[item["id"]] = top_permission + else: + children_permission = { + "id": item["id"], + "name": item["name"], + "pid": item["pid"], + "method": item["method"], + "label": item["label"], + } + + tree_dict[item["id"]] = children_permission + return tree_dict + except Exception as e: + err_logger.error("get all menu from role failed: \n%s" % traceback.format_exc()) + return tree_dict diff --git a/apps/rbac/views/role.py b/apps/rbac/views/role.py new file mode 100644 index 0000000..56e8c15 --- /dev/null +++ b/apps/rbac/views/role.py @@ -0,0 +1,73 @@ +import logging +import traceback + +from rest_framework.viewsets import ModelViewSet, GenericViewSet +from rest_framework import mixins + +from ChaCeRndTrans.code import BAD, SERVER_ERROR +from ..models import Role +from ..serializers.role_serializer import RoleListSerializer, RoleModifySerializer +from utils.custom import CommonPagination, RbacPermission, CustomViewBase +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from rest_framework.permissions import IsAuthenticated + +from utils.custom import CCAIResponse + +logger = logging.getLogger('error') + + +class RoleViewSet(CustomViewBase): + """ + 角色管理:增删改查 + """ + perms_map = ( + {"*": "admin"}, + {"*": "role_all"}, + {"get": "role_list"}, + {"post": "role_create"}, + {"put": "role_edit"}, + {"delete": "role_delete"} + ) + queryset = Role.objects.all().order_by('id') + serializer_class = RoleListSerializer + pagination_class = CommonPagination + filter_backends = (SearchFilter, OrderingFilter) + search_fields = ("name",) + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + def get_serializer_class(self): + if self.action == "list": + return RoleListSerializer + return RoleModifySerializer + + def get_queryset(self): + """ + 后台角色列表 + 返回后台角色(label=2)与前台默认角色(label=1,companyMid==None) + """ + label = self.request.query_params.get('label', None) # 1-前台默认角色 2-后台角色列表 + queryset = Role.objects.all() + if not label or int(label) not in (1, 2): + return queryset + if 1 == int(label): + queryset = queryset.filter(label=1, companyMid__isnull=True) + else: + queryset = queryset.filter(label=2) + return queryset + + def list(self, request, *args, **kwargs): + try: + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return CCAIResponse(data=serializer.data) + except Exception as e: + logger.error("user: %s, get role list failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("get list failed: \n %s" % e, SERVER_ERROR) diff --git a/apps/rbac/views/user.py b/apps/rbac/views/user.py new file mode 100644 index 0000000..25a5988 --- /dev/null +++ b/apps/rbac/views/user.py @@ -0,0 +1,1163 @@ +import datetime +import logging +import logging +import os +import random +import re +import time +import traceback +import uuid +from django.contrib.auth.hashers import make_password +from django.db import transaction +from django.db.models import Q +from django_filters.rest_framework import DjangoFilterBackend +from django_redis import get_redis_connection +from operator import itemgetter +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.generics import ListAPIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework_jwt.authentication import JSONWebTokenAuthentication +# from django.contrib.auth import authenticate +from rest_framework_jwt.settings import api_settings + +# APPID_MP, SECRET_MP, \ +# AuthorizationWX_URL, CCW_URL, FILE_HTTP, +from ChaCeRndTrans import settings +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import * +from ChaCeRndTrans.settings import RELEASE_DOMAIN, TEST_DOMAIN, MAX_IMAGE_SIZE +from rbac.views.message import msg_redis_code +from utils.custom import CommonPagination, RbacPermission, CustomViewBase, \ + generate_random_str, sha1_encrypt, is_all_chinese, is_valid_username +from utils.funcs import generate_random_str +from ..models import UserProfile, Menu, Role, Company +from ..serializers.menu_serializer import MenuSerializer +from ..serializers.user_serializer import UserListSerializer, UserCreateSerializer, UserModifySerializer, \ + UserInfoListSerializer, CompanySerializer + +jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER +jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER + +err_logger = logging.getLogger('error') + + +class UserAuthView(APIView): + """ + 用户认证获取token + """ + + def post(self, request, *args, **kwargs): + try: + # type 登录类型 1:免密登录 2:账号登录 + type = request.data.get("type", None) + if type is None: + return CCAIResponse(data="缺少必要参数", msg="缺少必要参数", status=BAD) + if type == 1: + mobile = request.data.get("telephone") + verifycode = request.data.get("verifycode") + if mobile == '' or mobile is None: + return CCAIResponse("手机号不能为空!", status=BAD) + if verifycode == '' or verifycode is None: + return CCAIResponse("验证码不能为空!", status=BAD) + user = UserProfile.objects.filter(mobile=mobile, is_active=1).first() + if user is not None: + try: + conn = get_redis_connection('default') + key = msg_redis_code + mobile + code = conn.hget(key, 'code') + count = conn.hget(key, 'count') + if code is None: + return CCAIResponse(data="验证码失效,请重新获取!", msg="验证码失效,请重新获取!", status=BAD) + elif code.decode('utf8') != verifycode: + if count is None: + conn.hset(key, 'count', 1) + else: + count = int(count.decode('utf8')) + 1 + if count < 5: + conn.hset(key, 'count', count) + else: + conn.delete(key) + return CCAIResponse(data="验证码有误!", msg="验证码有误!", status=VERIFYCODE_ERROR) + else: + # 验证码,验证通过 + payload = jwt_payload_handler(user) + if 'user_id' in payload: + payload.pop('user_id') + return CCAIResponse(data={"token": jwt_encode_handler(payload)}, status=OK) + except Exception as e: + err_logger.error( + "user: %s, connect redis failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(data="手机号登录失败!", msg="手机号登录失败!", status=BAD) + else: + user = UserProfile.objects.filter(mobile=mobile).first() + if user: + if user.is_active == 1: + return CCAIResponse(data="根据相关法律规定,账号需要绑定手机号,请输入账号密码进行绑定!", + status="根据相关法律规定,账号需要绑定手机号,请输入账号密码进行绑定!", code=BAD) + else: + return CCAIResponse(data="用户未激活!", msg="用户未激活!", status=BAD) + else: + return CCAIResponse( + data="该手机号未注册,请点击下方“立即注册”按钮进行注册!", + msg="该手机号未注册,请点击下方“立即注册”按钮进行注册!", + status=BAD + ) + + else: + username = request.data.get("username", None) + password = request.data.get("password", None) + if not username: + return CCAIResponse(data="请输入用户名,手机号或邮箱!", status=BAD) + if not password: + return CCAIResponse(data="请输入密码", status=BAD) + if username: + # 查询用户 # 此处可能会有问题,A用户的账号为B用户的手机号,故账号设置需要有复杂度 + user = UserProfile.objects.filter(Q(username=username) | Q(email=username) | Q(mobile=username)).first() + if user: + if user.check_password(password): + if user.is_active: + payload = jwt_payload_handler(user) + if 'user_id' in payload: + payload.pop('user_id') + return CCAIResponse({"token": jwt_encode_handler(payload)}, status=OK) + else: + return CCAIResponse(data="用户被锁定,请等待联系管理员。", msg="用户被锁定,请等待联系管理员。", status=BAD) + else: + return CCAIResponse(data="用户名或密码错误", msg="用户名或密码错误!", status=BAD) + else: # 用户不存在 + return CCAIResponse(data="用户名或密码错误", msg="用户名或密码错误!", status=BAD) + except Exception as e: + err_logger.error("user login failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg="用户名或密码错误!", status=SERVER_ERROR) + + +class UserInfoView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + """ + 获取当前用户信息和权限 + """ + + @classmethod + def get_permission_from_role(self, request, companyMid=None): + """ + 根据用户角色与公司id,返回对应的权限 + """ + try: + if request.user: + perms_list = [] + # for item in request.user.roles.values("permissions__method").distinct(): + # perms_list.append(item["permissions__method"]) + if not companyMid: + for item in request.user.roles.values("permissions__method").distinct(): + perms_list.append(item["permissions__method"]) + else: + for item in request.user.roles.filter(Q(companyMid=companyMid) | Q(companyMid__isnull=True)).values("permissions__method").distinct(): + perms_list.append(item["permissions__method"]) + return perms_list + except AttributeError: + return None + + @classmethod + def get_company_from_user(self, request): + try: + if request.user: + if 2 == request.user.label: # 后台内部人员可以查看所有公司 + company_list = Company.objects.all() + else: + company_list = request.user.company.all() + return CompanySerializer(company_list, many=True).data + except AttributeError: + return None + + def get(self, request): + try: + companyMid = request.GET.get("companyMid") + checkIn = None + company = None + + if companyMid: + if not Company.objects.filter(MainId=companyMid).exists(): + return CCAIResponse("公司不存在", status=BAD) + else: + checkIn = Company.objects.filter(MainId=companyMid).values("checkIn").first()['checkIn'] + company = Company.objects.filter(MainId=companyMid).values("AmStart", "AmEnd", "PmStart", "PmEnd", "tolerance").first() + # else: + # companyMid = request.user.companyMid + + + if request.user.id is not None: + company_list = self.get_company_from_user(request) + if companyMid is None: + checkIn = company_list[0]['checkIn'] + company = company_list[0] + perms = self.get_permission_from_role(request, companyMid) + data = { + "id": request.user.id, + "username": request.user.username, + "name": request.user.name, + "avatar": request.user.image if request.user.image else "/upload/logo/default.jpg", + "mobile": request.user.mobile, + "email": request.user.email, + "is_active": request.user.is_active, + "createTime": request.user.date_joined, + "roles": perms, + "is_superuser": request.user.is_superuser, + "area_code": request.user.area_code, + "area_name": request.user.area_name, + "is_sub": request.user.is_sub, # 1:子账号;2:主账号 + "nowcompany": companyMid if companyMid else company_list[0]['MainId'], + "checkIn": checkIn, + "AmStart": company["AmStart"], + "AmEnd": company["AmEnd"], + "PmStart": company["PmStart"], + "PmEnd": company["PmEnd"], + "tolerance": company["tolerance"], + "company": company_list, + } + return CCAIResponse(data=data, status=OK) + else: + return CCAIResponse("请登录后访问!", status=FORBIDDEN) + except Exception as e: + err_logger.error("get permission from role failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg="获取当前用户信息和权限失败!", status=SERVER_ERROR) + + +class UserBuildMenuView(APIView): + """ + 绑定当前用户,绑定当前公司的菜单信息 + """ + + def get_menu_from_role(self, request): + menu_dict = {} + try: + if request.user: + companyMid = request.GET.get('companyMid') + if not companyMid: + menus = request.user.roles.values( + "menus__id", + "menus__name", + "menus__route_name", + "menus__path", + "menus__is_frame", + "menus__is_show", + "menus__component", + "menus__icon", + "menus__sort", + "menus__pid" + ).distinct().order_by("menus__sort") + else: + menus = request.user.roles.filter(Q(companyMid=companyMid) | Q(companyMid__isnull=True)).values( + "menus__id", + "menus__name", + "menus__route_name", + "menus__path", + "menus__is_frame", + "menus__is_show", + "menus__component", + "menus__icon", + "menus__sort", + "menus__pid" + ).distinct().order_by("menus__sort") + # 获取公司打卡方案 + company = Company.objects.filter(MainId=companyMid).first() + companyCheckIn = company.checkIn + for item in menus: + # 如果公司打卡方案是A不展示B方案工时菜单 + if item["menus__pid"] is None: + if item["menus__is_frame"]: + # 判断是否外部链接 + top_menu = { + "id": item["menus__id"], + "path": item["menus__path"], + "component": "Layout", + "children": [{ + "path": item["menus__path"], + "meta": { + "title": item["menus__name"], + "icon": item["menus__icon"] + } + }], + "pid": item["menus__pid"], + "sort": item["menus__sort"] + } + else: + top_menu = { + "id": item["menus__id"], + "name": item["menus__route_name"], + "path": "/" + item["menus__path"], + "redirect": "noredirect", + "component": "Layout", + "alwaysShow": True, + "meta": { + "title": item["menus__name"], + "icon": item["menus__icon"] + }, + "pid": item["menus__pid"], + "sort": item["menus__sort"], + "children": [] + } + menu_dict[item["menus__id"]] = top_menu + else: + if item["menus__is_frame"]: + children_menu = { + "id": item["menus__id"], + "name": item["menus__route_name"], + "path": item["menus__path"], + "component": "Layout", + "meta": { + "title": item["menus__name"], + "icon": item["menus__icon"], + }, + "pid": item["menus__pid"], + "sort": item["menus__sort"] + } + elif item["menus__is_show"]: + children_menu = { + "id": item["menus__id"], + "name": item["menus__route_name"], + "path": item["menus__path"], + "component": item["menus__component"], + "meta": { + "title": item["menus__name"], + "icon": item["menus__icon"], + }, + "pid": item["menus__pid"], + "sort": item["menus__sort"] + } + else: + children_menu = { + "id": item["menus__id"], + "name": item["menus__route_name"], + "path": item["menus__path"], + "component": item["menus__component"], + "meta": { + "title": item["menus__name"], + "noCache": True, + }, + "hidden": True, + "pid": item["menus__pid"], + "sort": item["menus__sort"] + } + menu_dict[item["menus__id"]] = children_menu + return menu_dict + except Exception as e: + err_logger.error("get menu from role failed: \n%s" % traceback.format_exc()) + return menu_dict + + def get_all_menu_dict(self): + """ + 获取所有菜单数据,重组结构 + """ + try: + menus = Menu.objects.all().order_by('sort') + serializer = MenuSerializer(menus, many=True) + tree_dict = {} + for item in serializer.data: + if item["pid"] is None: + if item["is_frame"]: + # 判断是否外部链接 + top_menu = { + "id": item["id"], + "path": item["path"], + "component": "Layout", + "children": [{ + "path": item["path"], + "meta": { + "title": item["name"], + "icon": item["icon"] + } + }], + "pid": item["pid"], + "sort": item["sort"] + } + else: + top_menu = { + "id": item["id"], + "name": item["route_name"], + "path": "/" + item["path"], + "redirect": "noredirect", + "component": "Layout", + "alwaysShow": True, + "meta": { + "title": item["name"], + "icon": item["icon"] + }, + "pid": item["pid"], + "sort": item["sort"], + "children": [] + } + tree_dict[item["id"]] = top_menu + else: + if item["is_frame"]: + children_menu = { + "id": item["id"], + "name": item["route_name"], + "path": item["path"], + "component": "Layout", + "meta": { + "title": item["name"], + "icon": item["icon"], + }, + "pid": item["pid"], + "sort": item["sort"] + } + elif item["is_show"]: + children_menu = { + "id": item["id"], + "name": item["route_name"], + "path": item["path"], + "component": item["component"], + "meta": { + "title": item["name"], + "icon": item["icon"], + }, + "pid": item["pid"], + "sort": item["sort"] + } + else: + children_menu = { + "id": item["id"], + "name": item["route_name"], + "path": item["path"], + "component": item["component"], + "meta": { + "title": item["name"], + "noCache": True, + }, + "hidden": True, + "pid": item["pid"], + "sort": item["sort"] + } + tree_dict[item["id"]] = children_menu + return tree_dict + except Exception as e: + err_logger.error("get all menu from role failed: \n%s" % traceback.format_exc()) + return tree_dict + + def get_all_menus(self, request): + try: + perms = UserInfoView.get_permission_from_role(request) + tree_data = [] + if "admin" in perms or request.user.is_superuser: + tree_dict = self.get_all_menu_dict() + else: + tree_dict = self.get_menu_from_role(request) + for i in tree_dict: + if tree_dict[i]["pid"]: + pid = tree_dict[i]["pid"] + try: + parent = tree_dict[pid] + except KeyError as e: + # 缺少父级菜单 + continue + parent.setdefault("redirect", "noredirect") + parent.setdefault("alwaysShow", True) + parent.setdefault("children", []).append(tree_dict[i]) + parent["children"] = sorted(parent["children"], key=itemgetter("sort")) + else: + tree_data.append(tree_dict[i]) + return tree_data + except Exception as e: + err_logger.error("get all menu failed: \n%s" % traceback.format_exc()) + return tree_data + + def get(self, request): + try: + if request.user.id is not None: + menu_data = self.get_all_menus(request) + return CCAIResponse(menu_data, status=OK) + else: + return CCAIResponse(msg="请登录后访问!", status=FORBIDDEN) + except Exception as e: + err_logger.error("get all menu failed: \n%s" % traceback.format_exc()) + return CCAIResponse([], status=OK) + + +class UserViewSet(CustomViewBase): + """ + 用户管理:增删改查 + """ + perms_map = ( + {"*": "admin"}, + {"*": "user_all"}, + {"get": "user_list"}, + {"post": "user_create"}, + {"put": "user_edit"}, + {"delete": "user_delete"} + ) + + queryset = UserProfile.objects.all() + serializer_class = UserListSerializer + pagination_class = CommonPagination + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + filter_fields = ("is_active", "is_sub") + search_fields = ("username", "name", "mobile", "email") + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + + # permission_classes = (RbacPermission,) + + def get_serializer_class(self): + # 根据请求类型动态变更serializer + if self.action == "create": + return UserCreateSerializer + elif self.action == "list": + return UserListSerializer + return UserModifySerializer + + def create(self, request, *args, **kwargs): + # 创建用户默认添加密码 + try: + with transaction.atomic(): + data = request.data.copy() + password = sha1_encrypt("chacewang123456") + data['password'] = password + + # 生成uuid,并设置到用户中,再create + data['MainId'] = uuid.uuid4().__str__() # 用户mid + + companyname = data.get('companyname') + com = Company.objects.filter(name=companyname).first() + if com is None: + companyMid = uuid.uuid4().__str__() # 公司mid + com = Company() + com.MainId = companyMid + com.name = data.get('companyname') + com.EUCC = data.get('eucc') + com.userMid = data['MainId'] + com.save() + else: + companyMid = com.MainId + + data['companyMid'] = companyMid + serializer = self.get_serializer(data=data) + + try: + serializer.is_valid(raise_exception=True) + except Exception as e: + msg = "" + count = 0 + for k, v in e.detail.items(): + count += 1 + if count == 2: + msg += " 且 " + e.detail[k][0] + else: + msg += e.detail[k][0] + return CCAIResponse(msg=msg, status=BAD) + + self.perform_create(serializer) + + user = UserProfile.objects.filter(id=serializer.data['id']).first() + if user: + # # 为注册用户添加角色 + # # 公司管理员(公司的唯一最大权限拥有者)id=2 + # role = Role.objects.filter(id=2).first() + # if role: + # user.roles.add(role) # 添加角色组合 其中角色仅为公司管理员 + user.set_password(password) + # 为注册用户添加公司 + user.company.add(com) + user.save() + + headers = self.get_success_headers(serializer.data) + # data = serializer.data + return CCAIResponse(data="chacewang123456", status=CREATED, headers=headers) + except Exception as e: + err_logger.error("create user failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='创建用户失败', status=SERVER_ERROR) + + def update(self, request, *args, **kwargs): + try: + partial = kwargs.pop('partial', False) + instance = self.get_object() + # 是否封禁用户 + is_active = request.data.get('is_active') + is_active = True if is_active == "true" else False + flag = False + if instance.is_active and not is_active: # 操作为封禁 + flag = True + # 查询是否存在已激活的子账号 + subs = UserProfile.objects.filter(is_sub=1, parent_id=instance.id, is_active=1) + + serializer = self.get_serializer(instance, data=request.data, partial=partial) + try: + serializer.is_valid(raise_exception=True) + except Exception as e: + msg = "" + count = 0 + for k, v in e.detail.items(): + count += 1 + if count == 2: + msg += " 且 " + k + e.detail[k][0] + else: + msg += k + e.detail[k][0] + return CCAIResponse(msg=msg, status=BAD) + with transaction.atomic(): + self.perform_update(serializer) + if flag and subs: + subs.update(is_active=0) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return CCAIResponse(serializer.data, status=200) + except Exception as e: + err_logger.error("update user failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='修改用户失败', status=SERVER_ERROR) + + def list(self, request, *args, **kwargs): + try: + queryset = self.filter_queryset(self.get_queryset().filter().all()) + + page = self.paginate_queryset(queryset) + domain = RELEASE_DOMAIN + if settings.DEVELOP_DEBUG: + domain = TEST_DOMAIN + if page is not None: + serializer = self.get_serializer(page, many=True) + return_data = serializer.data + # for item in return_data: + # start = item['image'].rindex('/media/') + # item['image'] = domain + item['image'][start:] + return self.get_paginated_response(return_data) + + serializer = self.get_serializer(queryset, many=True) + return_data = serializer.data + for item in return_data: + start = item['image'].rindex('/media/') + item['image'] = domain + item['image'][start:] + return CCAIResponse(data=return_data, status=200) + except Exception as e: + err_logger.error("get user list failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='获取用户失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=True, permission_classes=[IsAuthenticated], + url_path="association_company", url_name="association") + def association_company(self, request, pk=None): + """ + 增加或删除账户关联公司,如果账户只剩一个公司,不给删除 + """ + try: + params = request.data + type = params.get('type', 1) # 1:增加关联公司 2:删除关联公司 + companyId = params.get('companyId') # 对应的公司id + + if not companyId: + return CCAIResponse('Missing param', BAD) + + instance = self.get_object() # 获取对应用户 + + company = Company.objects.filter(id=companyId).first() + if company: + if 1 == int(type): + instance.company.add(company) + elif 2 == int(type): + instance.company.remove(company) + else: + pass + return CCAIResponse('success') + else: + return CCAIResponse('查找不到公司', BAD) + except Exception as e: + err_logger.error("user association company failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='用户关联公司操作失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=True, permission_classes=[IsAuthenticated], + url_path="change-passwd", url_name="change-passwd") + def update_password(self, request, pk=None): + try: + perms = UserInfoView.get_permission_from_role(request) + old_password = request.data.get("old_password", "") + new_password1 = request.data.get("new_password1", "") + new_password2 = request.data.get("new_password2", "") + + if new_password1 == "" or new_password2 == "": + return CCAIResponse("密码不允许为空!", status=status.HTTP_400_BAD_REQUEST) + user = UserProfile.objects.get(id=pk if pk else request.user.id) + if "admin" in perms or "user_all" in perms or request.user.is_superuser: + new_password1 = request.data["new_password1"] + new_password2 = request.data["new_password2"] + if new_password1 == new_password2: + # result_dict = check_password_complexity(new_password1) + # if not result_dict["flag"]: + # return CCAIResponse(msg=result_dict["message"], status=BAD) + user.set_password(new_password2) + user.save() + return CCAIResponse(data="密码修改成功!", msg="密码修改成功!", status=OK) + else: + return CCAIResponse(data="新密码两次输入不一致!", msg="新密码两次输入不一致!", + status=status.HTTP_400_BAD_REQUEST) + else: + if old_password: + if not request.user.check_password(old_password): + return CCAIResponse(data="旧密码错误!", msg="旧密码错误!", status=BAD) + if new_password1 == new_password2: + if old_password == new_password1: + return CCAIResponse(data="新密码和旧密码不能一样!", + msg="新密码和旧密码不能一样!", status=BAD) + # 判断验证码是否正确 + user.set_password(new_password1) + user.save() + return CCAIResponse(data="修改密码成功!", status=OK) + else: + return CCAIResponse(data="新密码两次输入不一致!", msg="新密码两次输入不一致!", + status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + err_logger.error("user update password failed: \n%s" % traceback.format_exc()) + return CCAIResponse(msg='修改密码失败', status=SERVER_ERROR) + + # 重置密码 + @action(methods=["get"], detail=False, permission_classes=[RbacPermission], + url_path="reSetPassword", url_name="reSetPassword") + def reSetPassword(self, request): + try: + user_ids = request.query_params.get('ids', None) + if not user_ids: + return CCAIResponse("id参数不对啊!", NOT_FOUND) + password = "" + with transaction.atomic(): + user_list = UserProfile.objects.filter(id__in=user_ids.split(",")) + for userl in user_list: + user = UserProfile.objects.filter(username=userl.username).first() + if user: + # user.password = password + special_str = "*@&%" + password = "ccw" + user.mobile[-6:] + special_str[random.randint(0, len(special_str) - 1)] \ + + generate_random_str(randomlength=3) + password_ = sha1_encrypt(password) + user.password = make_password(password_) + user.save() + return CCAIResponse(data={"pwd": password}, status=OK) + except Exception as e: + err_logger.error( + "user: %s, reset password failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("重新设置密码失败", SERVER_ERROR) + + # @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + # url_path="updateAvatar", url_name="updateAvatar") + # def updateAvatar(self, request): + # try: + # avatar = request.FILES.get('avatar', "") + # image_type = request.data.get("image_type", "") + # + # if avatar is None or avatar == '': + # return CCAIResponse(data="avatar参数缺失!", status=BAD) + # if not image_type: + # return CCAIResponse(data="image_type参数缺失!", status=BAD) + # if avatar.size > MAX_IMAGE_SIZE: + # return CCAIResponse(data="上传文件需小于或等于2M", status=SERVER_ERROR) + # + # file_name = generate_random_str(12) + "." + image_type + # ct = time.time() # 取得系统时间 + # local_time = time.localtime(ct) + # date_head = time.strftime("%Y%m%d%H%M%S", local_time) # 格式化时间 + # date_m_secs = str(datetime.datetime.now().timestamp()).split(".")[-1] # 毫秒级时间戳 + # time_stamp = "%s%.3s" % (date_head, date_m_secs) # 拼接时间字符串 + # # 本地存储目录 + # save_path = os.path.join(settings.FILE_PATH, image_type, time_stamp) + # save_path = save_path.replace('\\', '/') + # # 前端显示目录 + # start = file_name.rindex('.') + # show_path = os.path.join(settings.SHOW_UPLOAD_PATH, image_type, time_stamp, file_name) + # show_path = show_path.replace('\\', '/') + # # 如果不存在则创建目录 + # if not os.path.exists(save_path): + # os.makedirs(save_path) + # os.chmod(save_path, mode=0o777) + # + # file_path = open(save_path + "/" + file_name, 'wb') + # for chunk in avatar.chunks(): + # file_path.write(chunk) + # file_path.close() + # + # # 数据库存储路径 + # request.user.image = show_path + # request.user.save() + # return CCAIResponse(status=200, data=show_path) + # + # except Exception as e: + # err_logger.error("user: %s, user update avatar failed: \n%s" % (request.user.id, traceback.format_exc())) + # return CCAIResponse(msg='修改头像失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="updateAvatar", url_name="updateAvatar") + def updateAvatar(self, request): + """ + 不支持用户上传图片 + """ + try: + avatar = request.data.get('avatar') + + if avatar is None or avatar == '': + return CCAIResponse("参数缺失!", BAD) + + request.user.image = avatar + request.user.save() + + return CCAIResponse("头像修改成功!") + + except Exception as e: + err_logger.error("user: %s, user update avatar failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse('修改头像失败', status=BAD) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="change_mobile", url_name="change_mobile") + def change_mobile(self, request, pk=None): + # 更新手机号 + try: + user = UserProfile.objects.get(id=request.user.id) + params = request.data + mobile = params.get("mobile", "") + if mobile == None or mobile == '': + return CCAIResponse(data="手机号不能为空", msg="手机号不能为空", status=status.HTTP_400_BAD_REQUEST) + is_exist = UserProfile.objects.filter(mobile=mobile).first() + if is_exist: + return CCAIResponse(data="手机号已被绑定", msg="手机号已被绑定", status=BAD) + user.mobile = mobile + user.save() + return CCAIResponse("success") + + except Exception as e: + err_logger.error("user update center mobile failed: \n%s" % traceback.format_exc()) + return CCAIResponse(data='手机号修改失败', msg='手机号修改失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="update_name", url_name="update_name") + def update_name(self, request, pk=None): + try: + params = request.data + name = params.get("name", None) # 真实姓名 + if name: + if len(name) < 2 or len(name) > 5: + return CCAIResponse(data="请输入正确的真实名字!", msg="请输入正确的真实名字!", status=BAD) + if not is_all_chinese(name): + return CCAIResponse(data="格式不对,请输入正确的真实名字!", msg="格式不对,请输入正确的真实名字!", + status=BAD) + request.user.name = name + request.user.save() + return CCAIResponse(data="修改姓名成功!", msg='修改姓名成功!', status=OK) + else: + return CCAIResponse(data='真实姓名不能为空!', msg='真实姓名不能为空!', status=BAD) + except Exception as e: + err_logger.error( + "user: %s update name failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='完善个人信息失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="updateInfo", url_name="updateInfo") + def updateInfo(self, request): + try: + params = request.data + name = params.get("name", "") # 真实姓名 + area_code = params.get("area_code", None) + area_dict = params.get("area", None) # 地区字典 + # if area_dict == None or area_dict == "": + # return CCAIResponse("area参数不能为空!", status=status.HTTP_400_BAD_REQUEST) + + if not name: + return CCAIResponse(msg="真实姓名不能为空!", status=BAD) + + request.user.name = name + if area_dict: + request.user.area_code = area_dict["value"] + request.user.area_name = area_dict['label'] + request.user.last_login = datetime.datetime.now() + request.user.save() + return CCAIResponse("success") + except Exception as e: + err_logger.error( + "user: %s update user information failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='完善个人信息失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="change_username", url_name="change_username") + def change_name(self, request, pk=None): + try: + # user = UserProfile.objects.get(id=request.user.id) + params = request.data + username = params.get("username", None) + if not username: + return CCAIResponse(data="用户名不能为空", msg="用户名不能为空", status=status.HTTP_400_BAD_REQUEST) + if not is_valid_username(username): + return CCAIResponse(data="用户名格式不对,只能输入汉字,英文字母,数字!", + msg="用户名格式不对,只能输入汉字,英文字母,数字!", status=BAD) + is_exist = UserProfile.objects.filter(username=username).first() + if is_exist: + return CCAIResponse(data="用户名已被使用,请重新修改", msg="用户名已被使用,请重新修改", status=BAD) + request.user.username = username + request.user.save() + return CCAIResponse(data="用户名修改成功!", msg="用户名修改成功!", status=OK) + except Exception as e: + err_logger.error("user: %s change user username failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='用户名修改失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="update_email", url_name="update_email") + def update_email(self, request, pk=None): + try: + params = request.data + email = params.get("email", "") + if email == None or email == '': + return CCAIResponse("email参数不能为空!", status=status.HTTP_400_BAD_REQUEST) + ret = re.match(r'^\w+@(\w+.)+(com|cn|net)$', email) + if ret: + email_exist = UserProfile.objects.filter(email=email).count() + if email_exist > 0: + return CCAIResponse("该Email已被注册!", status=status.HTTP_400_BAD_REQUEST) + request.user.email = email + request.user.save() + return CCAIResponse("修改email成功!") + else: + return CCAIResponse("email格式不对,请检查!", status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + err_logger.error( + "user: %s update email failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='Email修改失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="update_area", url_name="update_area") + def update_area(self, request, pk=None): + try: + params = request.data + area_dict = params.get("area", "") + if area_dict == None or area_dict == "": + return CCAIResponse("area参数不能为空!", status=status.HTTP_400_BAD_REQUEST) + request.user.area_code = area_dict["value"] + request.user.area_name = area_dict['label'] + request.user.save() + return CCAIResponse("地区修改成功!") + except Exception as e: + err_logger.error( + "user: %s update area failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse(msg='地区修改失败', status=SERVER_ERROR) + + @action(methods=["post"], detail=False, permission_classes=[], + url_path="forget_password", url_name="forget_password") + def forget_password(self, request, pk=None): + try: + mobile = request.data.get('mobile', None) + verification_code = request.data.get('verification_code', None) + new_password1 = request.data.get('new_password1') + new_password2 = request.data.get('new_password2') + if mobile is None: + return CCAIResponse(data="缺少参数mobile", msg="缺少参数mobile", status=BAD) + conn = get_redis_connection('default') + key = msg_redis_code + mobile + code = conn.hget(key, 'code') + count = conn.hget(key, 'count') + if code is None: + return CCAIResponse(data="验证码失效,请重新获取!", msg="验证码失效,请重新获取!", status=status.HTTP_206_PARTIAL_CONTENT) + elif code.decode('utf8') != verification_code: + if count is None: + conn.hset(key, 'count', 1) + else: + count = int(count.decode('utf8')) + 1 + if count < 5: + conn.hset(key, 'count', count) + else: + conn.delete(key) + return CCAIResponse(data="验证码有误!", msg="验证码有误!", status=BAD) + else: + if new_password1 == new_password2: + user = UserProfile.objects.filter(mobile=mobile, is_active=1).first() + if user: + user.set_password(new_password2) + user.save() + conn.delete(key) + return CCAIResponse(data="密码修改成功!", msg="密码修改成功!", status=OK) + else: + err_logger.error( + "user: %s, connect redis failed: \n%s" % (request.user.id, traceback.format_exc())) + else: + return CCAIResponse(data="新密码两次输入不一致!", msg="新密码两次输入不一致!", + status=status.HTTP_400_BAD_REQUEST) + return CCAIResponse(data="密码修改失败!", msg="密码修改失败!", status=BAD) + except Exception as e: + err_logger.error( + "user: %s forget user password, reset password failed: \n%s" % ( + request.user.id, traceback.format_exc())) + return CCAIResponse(data="忘记密码,重置密码失败!", msg="忘记密码,重置密码失败!", status=SERVER_ERROR) + + +class UserRegisterView(APIView): + """ + 用户注册(注册为主账号) + """ + + def post(self, request, *args, **kwargs): + try: + new_password1 = request.data.get("new_password1", None) + new_password2 = request.data.get("new_password2", None) + username = request.data.get("username", None) # 公司管理员账户 + mobile = request.data.get("telephone", None) # 手机号码 + companyname = request.data.get("companyname", None) # 公司名称 + eucc = request.data.get("eucc", None) # 企业社会统一信用代码,一般为16为数字 + verifycode = request.data.get("verifycode", None) # 短信验证码 + + name = request.data.get("name", None) # 姓名 + + if not companyname or companyname == '': + return CCAIResponse("公司名不能为空!", status=BAD) + + if username == '' or username is None or \ + new_password1 == '' or new_password1 is None or \ + new_password2 == '' or new_password2 is None: + return CCAIResponse("用户名或密码不能为空!", status=BAD) + + # if mobile is None or verifycode is None: + # return CCAIResponse(data="手机号与验证码不能为空!", msg="手机号与验证码不能为空!", status=BAD) + + # 判断账号是否已经存在 + username_exists = UserProfile.objects.filter(username=username).exists() + if username_exists: + return CCAIResponse(data="该账号已被注册!", msg="该账号已被注册!", status=BAD) + + # 判断手机号是否已经被注册 + phone_exist = UserProfile.objects.filter(mobile=mobile).count() + if phone_exist > 0: + return CCAIResponse(data="该手机号已被注册!", msg="该手机号已被注册!", status=BAD) + + if new_password1 != new_password2: + return CCAIResponse(data="密码两次输入不一致!", msg="密码两次输入不一致!", + status=status.HTTP_400_BAD_REQUEST) + + try: + conn = get_redis_connection('default') + key = msg_redis_code + mobile + code = conn.hget(key, 'code') + count = conn.hget(key, 'count') + if code is None: + return CCAIResponse(data="验证码失效,请重新获取!", msg="验证码失效,请重新获取!", + status=status.HTTP_206_PARTIAL_CONTENT) + elif code.decode('utf8') != verifycode: + if count is None: + conn.hset(key, 'count', 1) + else: + count = int(count.decode('utf8')) + 1 + if count < 5: + conn.hset(key, 'count', count) + else: + conn.delete(key) + return CCAIResponse(data="验证码有误!", msg="验证码有误!", status=status.HTTP_202_ACCEPTED) + except Exception as e: + err_logger.error("user: %s, connect redis failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("注册失败!", status=BAD) + + with transaction.atomic(): + # 生成用户与公司的全局id + userMid = uuid.uuid4().__str__() + + company = Company.objects.filter(name=companyname).first() + if company is None: + # 创建绑定的公司 + companyMid = uuid.uuid4().__str__() # 公司mid + company = Company() + company.MainId = companyMid + company.name = companyname + company.EUCC = eucc + company.userMid = userMid + company.save() + else: + companyMid = company.MainId + + # 创建用户 + user = UserProfile() + user.MainId = userMid + user.companyMid = companyMid + user.username = username + user.name = username + user.mobile = mobile + user.is_sub = 2 # 2:主账号 + user.is_active = 0 # 注册时不激活,由后台管理员确认信息后手动激活 + user.label = 1 # 默认前台用户 + user.set_password(new_password1) + user.save() + + # 为注册用户添加公司 + user.company.add(company) + # 为注册用户添加角色 + # 公司管理员(公司的唯一最大权限拥有者)id=2 其中包含的后台角色的权限(companyadmin)与菜单(company_system) + role = Role.objects.filter(id=2).first() + if role: + user.roles.add(role) + return CCAIResponse(data="注册成功", status=OK) + except Exception as e: + err_logger.error("user: %s, new_password1: %s, new_password1: %s, telphone: %s, user resister failed: " + "\n%s" % (request.user.id, new_password1, new_password2, mobile, traceback.format_exc())) + return CCAIResponse("注册失败", SERVER_ERROR) + + +# class UserBindWeChat(APIView): +# """ +# 用户绑定个人微信 +# """ +# def post(self, request, *args): +# try: +# wx_code = request.GET.get('wx_code') # 微信临时code +# encryptedData = request.data.get("encryptedData") # 加密数据 +# iv = request.data.get("iv") # 加密数据 +# response_dict = get_session_key(wx_code, APPID_MP, SECRET_MP, AuthorizationWX_URL) # 获取session_key +# if not response_dict: +# return CCAIResponse("授权登录失败!", status=SERVER_ERROR) +# session_key = response_dict["session_key"] +# decrypted = decode_info(iv, encryptedData, APPID_MP, session_key) # 解密手机号码 +# if not decrypted: +# return CCAIResponse("授权登录失败!", status=SERVER_ERROR) +# +# wxphone = decrypted["purePhoneNumber"] +# +# if wxphone == "" or wxphone is None: +# return CCAIResponse("授权登录失败!", status=SERVER_ERROR) +# user = UserProfile.objects.filter(mobile=wxphone, is_active=1, is_bind=1).first() +# if user is not None: +# # 存放微信相关信息 +# user.wx_phone_info = json.dumps(decrypted) +# user.wx_openid = response_dict["openid"] +# user.save() +# else: +# return CCAIResponse("该微信的手机号码与八爪蛙账号的手机号码不一致!", status=SERVER_ERROR) +# return CCAIResponse("微信绑定成功!", status=OK) +# +# except Exception as e: +# err_logger("user: %s, bind Wechat failed: \n%s" % (request.user.id, traceback.format_exc())) +# return CCAIResponse("注册失败", SERVER_ERROR) + + + + +class UserListView(ListAPIView): + queryset = UserProfile.objects.all() + serializer_class = UserInfoListSerializer + filter_backends = (DjangoFilterBackend, OrderingFilter) + filter_fields = ("name",) + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + +class RefreshTokenView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + """ + 刷新token + """ + def post(self, request, *args, **kwargs): + try: + if request.user.id: + if request.user.is_active: + payload = jwt_payload_handler(request.user) + return CCAIResponse({"token": jwt_encode_handler(payload)}, status=OK) + else: + return CCAIResponse("用户未激活", status=BAD) + else: + return CCAIResponse("用户不存在!", status=BAD) + except Exception as e: + err_logger.error("user: %s, user refresh token failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("用户不存在!", status=BAD) + + diff --git a/apps/staff/__init__.py b/apps/staff/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/staff/admin.py b/apps/staff/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/staff/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/staff/apps.py b/apps/staff/apps.py new file mode 100644 index 0000000..fa420ce --- /dev/null +++ b/apps/staff/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class StaffConfig(AppConfig): + name = 'staff' diff --git a/apps/staff/migrations/0001_initial.py b/apps/staff/migrations/0001_initial.py new file mode 100644 index 0000000..cc98c32 --- /dev/null +++ b/apps/staff/migrations/0001_initial.py @@ -0,0 +1,177 @@ +# Generated by Django 3.1.4 on 2024-03-08 08:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Accrued', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('staffId', models.IntegerField(blank=True, help_text='员工id', null=True, verbose_name='员工id')), + ('accruedDate', models.DateField(blank=True, help_text='预提年月 # 2024-01', null=True, verbose_name='预提年月')), + ('accruedEndDate', models.DateField(blank=True, help_text='预提终止年月 # 2024-01', null=True, verbose_name='预提终止年月')), + ('accruedAmount', models.DecimalField(blank=True, decimal_places=2, help_text='预提金额', max_digits=18, null=True, verbose_name='预提金额')), + ('accruedType', models.IntegerField(blank=True, help_text='预提类型 # (1:工资,2:奖金,3:福利)', null=True, verbose_name='预提类型')), + ('amountType', models.IntegerField(blank=True, help_text='预提金额类型id # 用户自定义的奖金与福利类型', null=True, verbose_name='预提金额类型id')), + ('status', models.IntegerField(blank=True, default=0, help_text='状态 # 0:未审核 1:已审核保存 2:已添加凭证', null=True, verbose_name='状态')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=200, null=True, verbose_name='备注信息')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ], + options={ + 'verbose_name': '预提记录', + 'verbose_name_plural': '预提记录', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Attendance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('staffId', models.IntegerField(blank=True, help_text='被考勤人员id', null=True, verbose_name='被考勤人员id')), + ('attendanceDate', models.DateField(blank=True, help_text='考勤年月 # 2024-01', null=True, verbose_name='考勤年月')), + ('attendanceDays', models.FloatField(blank=True, help_text='月出勤天数', null=True, verbose_name='月出勤天数')), + ('restDay', models.CharField(blank=True, help_text='休息日', max_length=200, null=True, verbose_name='休息日')), + ('rndDay', models.FloatField(blank=True, help_text='研发天数', null=True, verbose_name='研发天数')), + ('status', models.IntegerField(blank=True, default=0, help_text='状态 # 0:未审核 1:已审核保存 2:已添加凭证', null=True, verbose_name='状态')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ], + options={ + 'verbose_name': '考勤记录', + 'verbose_name_plural': '考勤记录', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Reward', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('staffId', models.IntegerField(blank=True, help_text='员工id', null=True, verbose_name='员工id')), + ('startDate', models.DateField(blank=True, help_text='起始年月# 2024-01', null=True, verbose_name='起始年月')), + ('endDate', models.DateField(blank=True, help_text='终止年月 # 2024-01', null=True, verbose_name='终止年月')), + ('amount', models.DecimalField(blank=True, decimal_places=2, help_text='金额', max_digits=18, null=True, verbose_name='金额')), + ('type', models.IntegerField(blank=True, help_text='类型', null=True, verbose_name='类型')), + ('amountType', models.IntegerField(blank=True, help_text='金额类型 # 用户自定义的奖金与福利类型id', null=True, verbose_name='金额类型id')), + ('status', models.IntegerField(blank=True, default=0, help_text='状态 # 0:未审核 1:已审核保存 2:已添加凭证', null=True, verbose_name='状态')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=200, null=True, verbose_name='备注信息')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ], + options={ + 'verbose_name': '奖金福利管理', + 'verbose_name_plural': '奖金福利管理', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Salary', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('staffId', models.IntegerField(blank=True, help_text='员工id', null=True, verbose_name='员工id')), + ('salaryDate', models.DateField(blank=True, help_text='薪资记录年月 # 2024-01', null=True, verbose_name='薪资记录年月')), + ('salaryAmount', models.DecimalField(blank=True, decimal_places=2, help_text='工资', max_digits=18, null=True, verbose_name='工资')), + ('accumulation', models.DecimalField(blank=True, decimal_places=2, help_text='公积金', max_digits=18, null=True, verbose_name='公积金')), + ('endowment', models.DecimalField(blank=True, decimal_places=2, help_text='养老', max_digits=18, null=True, verbose_name='养老')), + ('medical', models.DecimalField(blank=True, decimal_places=2, help_text='医疗', max_digits=18, null=True, verbose_name='医疗')), + ('unemployment', models.DecimalField(blank=True, decimal_places=2, help_text='失业', max_digits=18, null=True, verbose_name='失业')), + ('workInjury', models.DecimalField(blank=True, decimal_places=2, help_text='工伤', max_digits=18, null=True, verbose_name='工伤')), + ('maternity', models.DecimalField(blank=True, decimal_places=2, help_text='生育', max_digits=18, null=True, verbose_name='生育')), + ('illness', models.DecimalField(blank=True, decimal_places=2, help_text='大病医疗', max_digits=18, null=True, verbose_name='大病医疗')), + ('type', models.IntegerField(blank=True, help_text='1:工资SA(salary), 2:公积金AC(accumulation)', null=True, verbose_name='类型')), + ('status', models.IntegerField(blank=True, default=0, help_text='状态 # 0:未审核 1:已审核保存 2:已添加凭证', null=True, verbose_name='状态')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ], + options={ + 'verbose_name': '薪资记录', + 'verbose_name_plural': '薪资记录', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Staff', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, help_text='姓名', max_length=20, null=True, verbose_name='姓名')), + ('gender', models.IntegerField(blank=True, default=1, help_text='性别', null=True, verbose_name='性别')), + ('idCode', models.CharField(blank=True, help_text='身份唯一识别码', max_length=200, null=True, verbose_name='身份唯一识别码')), + ('category', models.IntegerField(blank=True, help_text='人员类别 # 1.研发人员 2.技术人员 3.辅助人员 4.非研发人员', null=True, verbose_name='人员类别')), + ('dept', models.IntegerField(blank=True, help_text='所属部门id', null=True, verbose_name='所属部门id')), + ('education', models.IntegerField(blank=True, help_text='学历 # 1.高中及以下 2.大专 3.本科 4.硕士 5.博士', null=True, verbose_name='学历')), + ('employmentMethod', models.IntegerField(blank=True, help_text='聘用方式 # 1:正式 2:临时 3.兼职', null=True, verbose_name='聘用方式')), + ('isActive', models.BooleanField(blank=True, default=True, help_text='用户是否可用 # 用户锁定与激活用户', null=True, verbose_name='用户是否可用')), + ('duties', models.CharField(blank=True, help_text='职务', max_length=200, null=True, verbose_name='职务')), + ('major', models.CharField(blank=True, help_text='专业', max_length=200, null=True, verbose_name='专业')), + ('isAbroad', models.BooleanField(blank=True, default=False, null=True, verbose_name='是否海归')), + ('isForeign', models.BooleanField(blank=True, default=False, null=True, verbose_name='是否外籍人员')), + ('birthday', models.DateField(blank=True, help_text='出生日期', null=True, verbose_name='出生日期')), + ('entryTime', models.DateField(blank=True, help_text='入职时间', null=True, verbose_name='入职时间')), + ('leaveTime', models.DateField(blank=True, help_text='离职时间', null=True, verbose_name='离职时间')), + ('changeTime', models.DateField(blank=True, help_text='变动时间', null=True, verbose_name='变动时间')), + ('status', models.PositiveSmallIntegerField(blank=True, default=0, help_text='状态 # 0:未离职 1:离职', null=True, verbose_name='状态')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ], + options={ + 'verbose_name': '员工信息', + 'verbose_name_plural': '员工信息', + 'ordering': ['id'], + }, + ), + migrations.CreateModel( + name='Dept', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, help_text='名称', max_length=20, null=True, verbose_name='名称')), + ('is_rnd', models.BooleanField(blank=True, default=False, help_text='是否研发部门,默认否', null=True, verbose_name='是否研发部门,默认否')), + ('CreateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='创建人')), + ('UpdateBy', models.CharField(blank=True, max_length=36, null=True, verbose_name='更新人')), + ('CreateByUid', models.IntegerField(blank=True, null=True, verbose_name='创建人ID')), + ('UpdateByUid', models.IntegerField(blank=True, null=True, verbose_name='更新人ID')), + ('CreateDateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateDateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('companyMid', models.CharField(blank=True, max_length=36, null=True, verbose_name='公司全局id')), + ('pid', models.ForeignKey(blank=True, help_text='上级部门id', null=True, on_delete=django.db.models.deletion.SET_NULL, to='staff.dept', verbose_name='上级部门id')), + ], + options={ + 'verbose_name': '部门信息', + 'verbose_name_plural': '部门信息', + 'ordering': ['id'], + }, + ), + ] diff --git a/apps/staff/migrations/__init__.py b/apps/staff/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/staff/models.py b/apps/staff/models.py new file mode 100644 index 0000000..a41e38e --- /dev/null +++ b/apps/staff/models.py @@ -0,0 +1,650 @@ +from django.db import models + + +# 员工 +class Staff(models.Model): + """ + 员工 + """ + + name = models.CharField(max_length=20, null=True, blank=True, verbose_name="姓名", help_text="姓名") + gender = models.IntegerField(null=True, blank=True, default=1, verbose_name="性别", help_text="性别") # 1:男 2:女 + idCode = models.CharField(max_length=200, null=True, blank=True, verbose_name="身份唯一识别码", + help_text="身份唯一识别码") + category = models.IntegerField(null=True, blank=True, verbose_name="人员类别", + help_text="人员类别 # 1.研发人员 2.技术人员 3.辅助人员 4.非研发人员") + dept = models.IntegerField(null=True, blank=True, verbose_name="所属部门id", help_text="所属部门id") + education = models.IntegerField(null=True, blank=True, verbose_name="学历", + help_text="学历 # 1.高中(中专)及以下 2.大专 3.本科 4.硕士 5.博士") + employmentMethod = models.IntegerField(null=True, blank=True, verbose_name="聘用方式", + help_text="聘用方式 # 1:正式 2:临时 3.兼职") + isActive = models.BooleanField(null=True, blank=True, default=True, verbose_name="用户是否可用", + help_text="用户是否可用 # 用户锁定与激活用户") + education = models.IntegerField(null=True, blank=True, verbose_name="学历", help_text="学历 # 1.高中(中专)及以下 2.大专 3.本科 4.硕士 5.博士") + employmentMethod = models.IntegerField(null=True, blank=True, verbose_name="聘用方式", help_text="聘用方式 # 1:正式 2:临时 3.兼职") + isActive = models.BooleanField(null=True, blank=True, default=True, verbose_name="用户是否可用", help_text="用户是否可用 # 用户锁定与激活用户") + duties = models.CharField(null=True, blank=True, max_length=200, verbose_name="职务", help_text="职务") + major = models.CharField(null=True, blank=True, max_length=200, verbose_name="专业", help_text="专业") + isAbroad = models.BooleanField(default=False, null=True, blank=True, verbose_name="是否海归") + isForeign = models.BooleanField(default=False, null=True, blank=True, verbose_name="是否外籍人员") + birthday = models.DateField(null=True, blank=True, verbose_name="出生日期", help_text="出生日期") + entryTime = models.DateField(null=True, blank=True, verbose_name="入职时间", help_text="入职时间") + leaveTime = models.DateField(null=True, blank=True, verbose_name="离职时间", help_text="离职时间") + changeTime = models.DateField(null=True, blank=True, verbose_name="变动时间", help_text="变动时间") + status = models.PositiveSmallIntegerField(null=True, blank=True, default=0, verbose_name="状态", + help_text="状态 # 0:未离职 1:离职") + # relCount = models.IntegerField(default=0, verbose_name="关联员工的数量,relCount>0时不可删除", help_text="关联员工的数量,relCount>0时不可删除") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + professionalCertificate = models.CharField(null=True, blank=True, max_length=200, verbose_name="专业证书") + personalHonor = models.CharField(null=True, blank=True, max_length=200, verbose_name="个人荣誉") + talentQualification = models.CharField(null=True, blank=True, max_length=10, verbose_name="人才资质", + help_text="1.国家高层次人才 2.地方高层次人才 3.区高层次人才") + graduationSchool = models.CharField(null=True, blank=True, max_length=200, verbose_name="毕业院校") + + class Meta: + verbose_name = "员工信息" + verbose_name_plural = verbose_name + ordering = ["id"] + + def __str__(self): + return self.name + + +class Dept(models.Model): + """ + 部门 + """ + + name = models.CharField(max_length=20, null=True, blank=True, verbose_name="名称", help_text="名称") + # type = models.CharField(max_length=50, null=True, blank=True, verbose_name="类型", help_text="类型") + is_rnd = models.BooleanField(default=False, null=True, blank=True, verbose_name="是否研发部门,默认否", + help_text="是否研发部门,默认否") + pid = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="上级部门id", + help_text="上级部门id") + # relCount = models.IntegerField(default=0, verbose_name="关联部门的数量,relCount>0时不可删除", help_text="关联部门的数量,relCount>0时不可删除") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + verbose_name = "部门信息" + verbose_name_plural = verbose_name + ordering = ["id"] + + def __str__(self): + return self.name + + +# 考勤 +class Attendance(models.Model): + """ + 考勤记录 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="被考勤人员id", help_text="被考勤人员id") + attendanceDate = models.DateField(null=True, blank=True, verbose_name="考勤年月", help_text="考勤年月 # 2024-01") + attendanceDays = models.FloatField(null=True, blank=True, verbose_name="月出勤天数", help_text="月出勤天数") + restDay = models.CharField(max_length=200, null=True, blank=True, verbose_name="休息日", help_text="休息日") + rndDay = models.FloatField(null=True, blank=True, verbose_name="研发天数", help_text="研发天数") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证(已记账)") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + verbose_name = "考勤记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.attendanceDate: + self.attendanceDate = self.attendanceDate.replace(day=1) + super().save(*args, **kwargs) + + +# 实发工资 +class Wages(models.Model): + """ + 工资记录 费用编码 1.1 外聘劳务费 1.3 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工资", help_text="工资") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", help_text="考勤状态是否完成,0-未完成") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_wages" # 不可删除 + verbose_name = "工资记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 实发社保 +class SocialInsurance(models.Model): + """ + 社保记录 费用编码 1.2.1(多种社保,具体类型由用户自定义的社保科目类型决定) + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + # endowment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="养老", help_text="养老") + # medical = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="医疗", help_text="医疗") + # unemployment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="失业", help_text="失业") + # workInjury = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工伤", help_text="工伤") + # maternity = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="生育", help_text="生育") + # illness = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="大病医疗", help_text="大病医疗") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_social_insurance" + verbose_name = "社保记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 实发公积金 +class Accumulation(models.Model): + """ + 公积金记录 费用编码 1.2.2 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + # accumulation = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="公积金", help_text="公积金") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_accumulation" + verbose_name = "公积金记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 实发奖金 +class Bonus(models.Model): + """ + 奖金管理 费用编码 1.4 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, null=True, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + remark = models.CharField(max_length=200, null=True, blank=True, verbose_name="备注信息", help_text="备注信息") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_bonus" + verbose_name = "奖金管理" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 实发福利 +class Welfare(models.Model): + """ + 福利管理 费用编码 7.9.3 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, null=True, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + remark = models.CharField(max_length=200, null=True, blank=True, verbose_name="备注信息", help_text="备注信息") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_welfare" + verbose_name = "福利管理" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# +# 以下为预提管理的model +# + +# 预提工资 +class AccruedWages(models.Model): + """ + 预提工资记录 费用编码 1.1 外聘劳务费 1.3 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工资", + help_text="工资") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_accrued_wages" + verbose_name = "工资记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 预提社保 +class AccruedSocialInsurance(models.Model): + """ + 预提社保记录 费用编码 1.2.1(多种社保,具体类型由用户自定义的社保科目类型决定) + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + # endowment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="养老", help_text="养老") + # medical = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="医疗", help_text="医疗") + # unemployment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="失业", help_text="失业") + # workInjury = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工伤", help_text="工伤") + # maternity = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="生育", help_text="生育") + # illness = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="大病医疗", help_text="大病医疗") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_accrued_social_insurance" + verbose_name = "社保记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 预提公积金 +class AccruedAccumulation(models.Model): + """ + 预提公积金记录 费用编码 1.2.2 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + # accumulation = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="公积金", help_text="公积金") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_accrued_accumulation" + verbose_name = "公积金记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 预提奖金 +class AccruedBonus(models.Model): + """ + 预提奖金管理 费用编码 1.4 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, null=True, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + remark = models.CharField(max_length=200, null=True, blank=True, verbose_name="备注信息", help_text="备注信息") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_accrued_bonus" + verbose_name = "奖金管理" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + +# 预提福利 +class AccruedWelfare(models.Model): + """ + 预提福利管理 费用编码 7.9.3 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + date = models.DateField(null=True, blank=True, verbose_name="薪资入账年月", help_text="薪资入账年月 # 2024-01") + startDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生开始年月", + help_text="薪资费用发生开始年月 # 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="薪资费用发生结束年月", + help_text="薪资费用发生结束年月 # 2024-01") + amount = models.DecimalField(max_digits=18, decimal_places=2, null=True, blank=True, verbose_name="金额", + help_text="金额") + subjectId = models.IntegerField(null=True, blank=True, verbose_name="对应科目id", help_text="对应科目id") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", + help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", + help_text="考勤状态是否完成,0-未完成") + remark = models.CharField(max_length=200, null=True, blank=True, verbose_name="备注信息", help_text="备注信息") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + db_table = "staff_accrued_welfare" + verbose_name = "福利管理" + verbose_name_plural = verbose_name + ordering = ["id"] + + def save(self, *args, **kwargs): + # 强制确保日期为每月的第一天 + if self.date: + self.date = self.date.replace(day=1) + super().save(*args, **kwargs) + + + +class Salary(models.Model): + """ + 薪资记录(工资,公积金) + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + salaryDate = models.DateField(null=True, blank=True, verbose_name="薪资记录年月", help_text="薪资记录年月 # 2024-01") + salaryAmount = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工资", help_text="工资") + accumulation = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="公积金", help_text="公积金") + endowment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="养老", help_text="养老") + medical = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="医疗", help_text="医疗") + unemployment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="失业", help_text="失业") + workInjury = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工伤", help_text="工伤") + maternity = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="生育", help_text="生育") + illness = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="大病医疗", help_text="大病医疗") + # type = models.IntegerField(null=True, blank=True, verbose_name="类型", help_text="1:工资SA(salary), 2:公积金AC(accumulation)") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", help_text="状态 # 0:未审核 1:已审核保存 1+:已添加凭证") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", help_text="考勤状态是否完成,0-未完成") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + verbose_name = "薪资记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + +class Accrued(models.Model): + """ + 预提记录(工资,奖金,福利) + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + accruedDate = models.DateField(null=True, blank=True, verbose_name="预提年月", help_text="预提年月 # 2024-01") + accruedEndDate = models.DateField(null=True, blank=True, verbose_name="预提终止年月", help_text="预提终止年月 # 2024-01") + accruedAmount = models.DecimalField(max_digits=18, decimal_places=2, null=True, blank=True, verbose_name="预提金额", help_text="预提金额") + accruedType = models.IntegerField(null=True, blank=True, verbose_name="预提类型", help_text="预提类型 # (1:工资&社保公积金,2:奖金,3:福利)") + amountType = models.IntegerField(null=True, blank=True, verbose_name="预提金额类型id", help_text="预提金额类型id # 用户自定义的奖金与福利类型") + attendanceStatus = models.BooleanField(default=0, verbose_name="考勤状态是否完成,0-未完成", help_text="考勤状态是否完成,0-未完成") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", help_text="状态 # 0:未审核 1:已审核保存 2:已添加凭证") + remark = models.CharField(max_length=200, null=True, blank=True, verbose_name="备注信息", help_text="备注信息") + accumulation = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="公积金", help_text="公积金") + endowment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="养老", help_text="养老") + medical = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="医疗", help_text="医疗") + unemployment = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="失业", help_text="失业") + workInjury = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="工伤", help_text="工伤") + maternity = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="生育", help_text="生育") + illness = models.DecimalField(max_digits=18, decimal_places=2, default=0, blank=True, verbose_name="大病医疗", help_text="大病医疗") + + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + verbose_name = "预提记录" + verbose_name_plural = verbose_name + ordering = ["id"] + + +class Reward(models.Model): + """ + 奖金福利管理 + """ + + staffId = models.IntegerField(null=True, blank=True, verbose_name="员工id", help_text="员工id") + startDate = models.DateField(null=True, blank=True, verbose_name="起始年月", help_text="起始年月# 2024-01") + endDate = models.DateField(null=True, blank=True, verbose_name="终止年月", help_text="终止年月 # 2024-01") # 2024-01 + amount = models.DecimalField(max_digits=18, decimal_places=2, null=True, blank=True, verbose_name="金额", help_text="金额") + type = models.IntegerField(null=True, blank=True, verbose_name="类型", help_text="类型") # 1:奖金 2:福利 + amountType = models.IntegerField(null=True, blank=True, verbose_name="金额类型id", help_text="金额类型 # 用户自定义的奖金与福利类型id") + attendanceStatus = models.BooleanField(default=False, verbose_name="是否完成对应时间的考勤,0-未完成", help_text="是否完成对应时间的考勤,0-未完成") + status = models.IntegerField(null=True, default=0, blank=True, verbose_name="状态", help_text="状态 # 0:未审核 1:已审核保存 2:已添加凭证") + remark = models.CharField(max_length=200, null=True, blank=True, verbose_name="备注信息", help_text="备注信息") + + CreateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="创建人") + UpdateBy = models.CharField(max_length=36, null=True, blank=True, verbose_name="更新人") + CreateByUid = models.IntegerField(null=True, blank=True, verbose_name="创建人ID") + UpdateByUid = models.IntegerField(null=True, blank=True, verbose_name="更新人ID") + CreateDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + UpdateDateTime = models.DateTimeField(auto_now=True, verbose_name="更新时间") + companyMid = models.CharField(null=True, blank=True, max_length=36, verbose_name="公司全局id") # 用于绑定公司 + + class Meta: + verbose_name = "奖金福利管理" + verbose_name_plural = verbose_name + ordering = ["id"] + diff --git a/apps/staff/serializers/Serializer.py b/apps/staff/serializers/Serializer.py new file mode 100644 index 0000000..d449f3b --- /dev/null +++ b/apps/staff/serializers/Serializer.py @@ -0,0 +1,259 @@ +from rest_framework import serializers + +from staff.models import * +from staff.utils import vaild_attendance +from utils.custom import MyCustomError + + +class DeptSerializer(serializers.ModelSerializer): + """ + 部门序列化 + """ + + class Meta: + model = Dept + fields = "__all__" + + def validate_name(self, value): + # 当修改与创建时,检查idCode是否已存在 + if self.instance: # Update case + existing_dept = Dept.objects.exclude(id=self.instance.id).filter(name=value, companyMid=self.initial_data.get('companyMid')) + else: # Create case + existing_dept = Dept.objects.filter(name=value, companyMid=self.initial_data.get('companyMid')) + + if existing_dept.exists(): + raise MyCustomError("部门已存在") + + return value + + +class DeptModifySerializer(serializers.ModelSerializer): + """ + 部门修改序列化 + """ + + class Meta: + model = Dept + fields = "__all__" + + def validate_name(self, value): + # 当修改与创建时,检查idCode是否已存在 + if self.instance: # Update case + existing_dept = Dept.objects.exclude(id=self.instance.id).filter(name=value, companyMid=self.initial_data.get('companyMid')) + else: # Create case + existing_dept = Dept.objects.filter(name=value, companyMid=self.initial_data.get('companyMid')) + + if existing_dept.exists(): + raise MyCustomError("部门已存在") + + return value + + def validate(self, data): + pid = data.get('pid') + id = self.instance if self.instance else None + + if pid == id: + raise serializers.ValidationError("pid字段不能等于id字段") + + return data + + +class AttendanceSerializer(serializers.ModelSerializer): + """ + 考勤序列化 + """ + + class Meta: + model = Attendance + fields = "__all__" + + def validate(self, data): + attendance = self.Meta.model(**data) or self.instance + staffTime = Staff.objects.filter(id=attendance.staffId if attendance.staffId else self.instance.staffId).values( + 'id', 'entryTime', 'leaveTime').first() + if not staffTime: + raise MyCustomError("员工不存在") + try: + vaild_attendance(attendance, entryTime=staffTime['entryTime'], leaveTime=staffTime['leaveTime']) + data['restDay'] = attendance.restDay + except Exception as e: + raise e + return data + + +class WagesSerializer(serializers.ModelSerializer): + """ + 工资记录序列化 + """ + + class Meta: + model = Wages + fields = "__all__" + + +class SocialInsuranceSerializer(serializers.ModelSerializer): + """ + 社保记录序列化 + """ + + class Meta: + model = SocialInsurance + fields = "__all__" + + +class AccumulationSerializer(serializers.ModelSerializer): + """ + 公积金记录序列化 + """ + + class Meta: + model = Accumulation + fields = "__all__" + + +class BonusSerializer(serializers.ModelSerializer): + """ + 奖金记录序列化 + """ + + class Meta: + model = Accumulation + fields = "__all__" + + +class WelfareSerializer(serializers.ModelSerializer): + """ + 福利记录序列化 + """ + + class Meta: + model = Welfare + fields = "__all__" + + +# 预提管理序列化 + +class AccruedWagesSerializer(serializers.ModelSerializer): + """ + 预提工资记录序列化 + """ + + class Meta: + model = AccruedWages + fields = "__all__" + + +class AccruedSocialInsuranceSerializer(serializers.ModelSerializer): + """ + 预提社保记录序列化 + """ + + class Meta: + model = AccruedSocialInsurance + fields = "__all__" + + +class AccruedAccumulationSerializer(serializers.ModelSerializer): + """ + 预提公积金序列化 + """ + + class Meta: + model = AccruedAccumulation + fields = "__all__" + + +class AccruedBonusSerializer(serializers.ModelSerializer): + """ + 预提奖金序列化 + """ + + class Meta: + model = AccruedBonus + fields = "__all__" + + +class AccruedWelfareSerializer(serializers.ModelSerializer): + """ + 预提福利序列化 + """ + + class Meta: + model = AccruedWelfare + fields = "__all__" + + + +class SalarySerializer(serializers.ModelSerializer): + """ + 薪资记录序列化 + """ + + class Meta: + model = Salary + fields = "__all__" + + def validate_salaryDate(self, value): + # 从context中获取companyMid + # companyMid = self.context.get('companyMid') + # 当修改与创建时,检查idCode是否已存在 + Year, Month = value.year, value.month + if self.instance: # Update case + # existing_staff = Salary.objects.exclude(id=self.instance.id).filter(salaryDate__year=Year, salaryDate__month=Month, type=self.instance.type, staffId=self.initial_data.get('staffId'), companyMid=self.initial_data.get('companyMid')) + existing_staff = Salary.objects.exclude(id=self.instance.id).filter(salaryDate__year=Year, salaryDate__month=Month, staffId=self.initial_data.get('staffId'), companyMid=self.initial_data.get('companyMid')) + else: # Create case + # existing_staff = Salary.objects.filter(salaryDate__year=Year, salaryDate__month=Month, type=self.initial_data.get('type'), staffId=self.initial_data.get('staffId'), companyMid=self.initial_data.get('companyMid')) + existing_staff = Salary.objects.filter(salaryDate__year=Year, salaryDate__month=Month, staffId=self.initial_data.get('staffId'), companyMid=self.initial_data.get('companyMid')) + + if existing_staff.exists(): + raise MyCustomError("该员工该月已存在薪资记录") + + return value + + +class AccruedSerializer(serializers.ModelSerializer): + """ + 预提记录序列化 + """ + + class Meta: + model = Accrued + fields = "__all__" + + def validate_accruedDate(self, value): + # 从context中获取companyMid + # companyMid = self.context.get('companyMid') + # 当修改与创建时,检查idCode是否已存在 + Year, Month = value.year, value.month + existing_accrued = None + if self.instance: # Update case + if 1 == self.initial_data.get('accruedType'): # 仅类型-工资社保公积金需要限制每月一条记录 + existing_accrued = Accrued.objects.exclude(id=self.instance.id).filter(accruedDate__year=Year, + accruedDate__month=Month, + accruedType=self.initial_data.get('accruedType'), + staffId=self.initial_data.get( + 'staffId'), + companyMid=self.initial_data.get( + 'companyMid')) + else: # Create case + if 1 == self.initial_data.get('accruedType'): + existing_accrued = Accrued.objects.filter(accruedDate__year=Year, accruedDate__month=Month, + accruedType=self.initial_data.get('accruedType'), + staffId=self.initial_data.get('staffId'), + companyMid=self.initial_data.get('companyMid')) + + if existing_accrued and existing_accrued.exists(): + raise MyCustomError("该员工该月已存在预提薪资记录") + + return value + + +class RewardSerializer(serializers.ModelSerializer): + """ + 奖金福利序列化 + """ + + class Meta: + model = Reward + fields = "__all__" + diff --git a/apps/staff/serializers/__init__.py b/apps/staff/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/staff/serializers/staffSerializer.py b/apps/staff/serializers/staffSerializer.py new file mode 100644 index 0000000..3861340 --- /dev/null +++ b/apps/staff/serializers/staffSerializer.py @@ -0,0 +1,28 @@ +from rest_framework import serializers + +from staff.models import Staff +from utils.custom import MyCustomError + + +class StaffSerializer(serializers.ModelSerializer): + """ + 员工序列化 + """ + + class Meta: + model = Staff + fields = "__all__" + + def validate_idCode(self, value): + # 从context中获取companyMid + # companyMid = self.context.get('companyMid') + # 当修改与创建时,检查idCode是否已存在 + if self.instance: # Update case + existing_staff = Staff.objects.exclude(id=self.instance.id).filter(idCode=value, companyMid=self.initial_data.get('companyMid')) + else: # Create case + existing_staff = Staff.objects.filter(idCode=value, companyMid=self.initial_data.get('companyMid')) + + if existing_staff.exists(): + raise MyCustomError("该身份码已存在") + + return value \ No newline at end of file diff --git a/apps/staff/tests.py b/apps/staff/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/staff/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/staff/urls.py b/apps/staff/urls.py new file mode 100644 index 0000000..bffe85d --- /dev/null +++ b/apps/staff/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, include +from rest_framework import routers + + +router = routers.SimpleRouter() + +urlpatterns = [ + path(r"api/staff/", include(router.urls)), +] diff --git a/apps/staff/utils.py b/apps/staff/utils.py new file mode 100644 index 0000000..835b81f --- /dev/null +++ b/apps/staff/utils.py @@ -0,0 +1,301 @@ +import calendar +import logging +import re +import traceback +from datetime import datetime +import json +import datetime as oneDatetime + +from staff.models import Attendance +from utils.custom import MyCustomError + + +def vaild_restDays(restDays): + """ + 校验输入的休息日期是否合法 + """ + try: + if isinstance(restDays, float) and restDays.is_integer(): + restDays = str(int(restDays)) + else: + restDays = str(restDays).strip() + check = [0] * 31 # 用于检验休息日,不存在重复休息同一时间的情况 1-上午 2-下午 3-全天 + restDayList = restDays.split(',') + for string in restDayList: + pattern = r'^(1[0-9]|2[0-9]|3[0-1]|[1-9])(am|pm)?$' + match = re.match(pattern, string) + if match: + num = int(match.group(1)) + am_pm = match.group(2) + if am_pm: # 是半天的情况 + if 'am' == am_pm: + check[num-1] += 1 + elif 'pm' == am_pm: + check[num-1] += 2 + else: + return False + else: # 完整一天的情况 + check[num-1] += 3 + # 检验是否已出错 + if check[num-1] > 3: + return False + else: + return False + return True + except Exception as e: + logging.getLogger('error').error("vaild_restDays failed: \n%s" % (traceback.format_exc())) + raise Exception('校验日期字符串失败') + + +def vaild_attendance(attendance: Attendance, entryTime, leaveTime): + """ + 校验考勤数据 + 1. 研发天数 >= 0 + 2. 休息日 + 月出勤天数 = 本月天数(减去本月入职离职时间) + 3. 研发天数 <= 出勤天数 + """ + try: + if isinstance(attendance.attendanceDate, str): + attendance.attendanceDate = datetime.strptime(attendance.attendanceDate, "%Y-%m-%d") + _, days = calendar.monthrange(attendance.attendanceDate.year, attendance.attendanceDate.month) + monthAbleDay = days # 本月可用总天数 + # 判断处理入职与离职时间 + attendance_month = attendance.attendanceDate.strftime('%Y-%m') + entry_month = entryTime.strftime('%Y-%m') + if attendance_month == entry_month: + monthAbleDay = days - (entryTime.day - 1) + if leaveTime: + leave_month = leaveTime.strftime('%Y-%m') + if attendance_month == leave_month: + monthAbleDay -= (days - leaveTime.day) + + # 计算休息日count + rest_count = 0 + rest_day_list = [] + if attendance.restDay: + if attendance_month == entry_month: # 过滤无效休息日 + for day in attendance.restDay.split(','): + dayInt = int(re.search(r'\d+', day).group()) + if dayInt < entryTime.day: + continue + if 'am' in day or 'pm' in day: + rest_count += 0.5 + else: + rest_count += 1 + rest_day_list.append(day) + elif leaveTime and attendance_month == leave_month: # 过滤无效休息日 + for day in attendance.restDay.split(','): + dayInt = int(re.search(r'\d+', day).group()) + if dayInt > leaveTime.day: + continue + if 'am' in day or 'pm' in day: + rest_count += 0.5 + else: + rest_count += 1 + rest_day_list.append(day) + else: + for day in attendance.restDay.split(','): + if 'am' in day or 'pm' in day: + rest_count += 0.5 + else: + rest_count += 1 + rest_day_list.append(day) + attendance.restDay = ','.join(rest_day_list) + # 1.研发天数 >= 0 + if attendance.rndDay < 0: + raise MyCustomError('研发天数不能小于0') + # 2.休息日 + 月出勤天数 = 本月天数 + if rest_count + attendance.attendanceDays != monthAbleDay: + raise MyCustomError('休息日加月出勤天数应等于本月天数减去入职或离职时间') + # 3.研发天数 <= 出勤天数 + if attendance.rndDay > attendance.attendanceDays: + raise MyCustomError('研发天数不能大于出勤天数') + return True + except Exception as e: + raise e + + +def check_entry_leave_time(attendance: Attendance, entryTime: datetime, leaveTime: datetime) -> bool: + """ + 检查考勤休息日是否与入职离职时间冲突,不冲突则返回True,冲突返回False + """ + try: + attendance_month = attendance.attendanceDate.strftime('%Y-%m') + # entry_month, leave_month = map(lambda x: x.strftime('%Y-%m'), [entryTime, leaveTime]).list() + entry_month = entryTime.strftime('%Y-%m') + leave_month = leaveTime.strftime('%Y-%m') + + def check(attendance, checkDate, type=None, checkDateExtra=None): + # 将该月休息日反选,查看是否存在工作日与离职或入职时间冲突 + # checkDateExtra仅一种情况,即该月入职且该月离职 + restDays = attendance.restDay + days = calendar.monthrange(attendance.attendanceDate.year, attendance.attendanceDate.month)[1] + if restDays and restDays != '': + restDaySet = set(re.sub(r'(am|pm)', restDays).split(',')) + workDaySet = {day for day in range(1,days+1) if day not in restDaySet} + else: + workDaySet = {day for day in range(1, days+1)} + checkDay = checkDate.day + if 1 == type: # 入职时间 + return not any(day < checkDay for day in workDaySet) + elif 2 == type: # 离职时间 + return not any(day > checkDay for day in workDaySet) + else: # 当月入职且离职 + checkDayExtra = checkDateExtra.day # 离职日 + return not any(day < checkDay or day > checkDayExtra for day in workDaySet) + + # 判断是否与入职离职时间处于同一月 + if entry_month != leave_month: + if attendance_month == entry_month: + check(attendance, entryTime, 1) + elif attendance_month == leave_month: + check(attendance, entryTime, 2) + else: # 不可能冲突 + return True + else: # 如果当月入职且当月离职 + if attendance_month == entry_month: + check(attendance, entryTime) + else: + return True + + except Exception as e: + raise e + + +def supplyRestDays(attendance: Attendance, entryTime: datetime, leaveTime: datetime) -> bool: + """ + 判断是否当月入职离职,自动补充入职前与离职后的时间为休息日 + """ + try: + attendance_month = attendance.attendanceDate.strftime('%Y-%m') + # entry_month, leave_month = map(lambda x: x.strftime('%Y-%m'), [entryTime, leaveTime]).list() + entry_month = entryTime.strftime('%Y-%m') + leave_month = leaveTime.strftime('%Y-%m') + + days_in_month = calendar.monthrange(attendance.attendanceDate.year, attendance.attendanceDate.month)[1] + restDays = attendance.restDay + + # 解析休息日 + if restDays and restDays != '': + restDaySet = set(map(int, re.findall(r'\d+', restDays))) + else: + restDaySet = set() + + def update_rest_days(start, end): + for day in range(start, end + 1): + restDaySet.add(day) + # 判断是否与入职离职时间处于同一月 + if attendance_month == entry_month: + update_rest_days(1, entryTime.day - 1) + elif attendance_month == leave_month: + update_rest_days(leaveTime.day + 1, days_in_month) + # 更新休息日字符串 + attendance.restDay = ','.join(map(str, sorted(restDaySet))) + except Exception as e: + raise e + +def replace_or_add(original, target, addition): + if target in original: + # 如果目标子字符串存在,则替换它 + return original.replace(target, addition) + else: + # 如果目标子字符串不存在,则添加它 + return original + addition + +def get_reset_dates(year, month, file_path): + try: + # 获取当月的所有日期 + day_range = range(1, calendar.monthrange(year, month)[1] + 1) + + # 初始化一个空列表来存储周末日期 + weekend_dates = set() + + # 从本地读取 media/chinese_holiday/{year}.json + restData = {} + + try: + with open(file_path, 'r', encoding='utf-8') as file: + restData = json.load(file) + except Exception as e: + raise e + + # 遍历每一天,检查是否是周末 + for day in day_range: + current_date = oneDatetime.date(year, month, day) + if current_date.weekday() >= 5: # 星期六是5,星期日是6 + weekend_dates.add(current_date) + restDayStr = str(str(month).zfill(2) + '-' + str(day).zfill(2)) + restDayData = restData['holiday'].get(restDayStr, None) + if restDayData: + if restDayData['holiday']: + weekend_dates.add(current_date) + if not restDayData['holiday']: + weekend_dates.remove(current_date) + + return sorted(weekend_dates) + except Exception as e: + raise e + +def setDateListStrFrontDate(dates, joinDate, leaveDate): + setDates = [] + if leaveDate: + for date in dates: + # 如果离职时间和入职时间不在同一个月 + # 1入职前的时间不算进去,离职后的时间不算进去 + if joinDate.year != leaveDate.year and joinDate.month != leaveDate.month: + if joinDate.year == date.year and joinDate.month == date.month and date.day >= joinDate.day: + setDates.append(str(date.day)) + continue + if leaveDate.year == date.year and leaveDate.month == date.month and date.day <= leaveDate.day: + setDates.append(str(date.day)) + continue + elif leaveDate.year != date.year or leaveDate.month != date.month: + setDates.append(str(date.day)) + # 如果离职时间和入职时间在同一个月 + # 1入职前的时间不算进去,离职后的时间不算进去 + elif joinDate.year == leaveDate.year and joinDate.month == leaveDate.month: + if joinDate.year == date.year and joinDate.month == date.month and (date.day >= joinDate.day and date.day <= leaveDate.day): + setDates.append(str(date.day)) + continue + elif leaveDate.year != date.year or leaveDate.month != date.month: + setDates.append(str(date.day)) + + else: + for date in dates: + if joinDate.year == date.year and joinDate.month == date.month and date.day >= joinDate.day: + setDates.append(str(date.day)) + elif joinDate.year != date.year or joinDate.month != date.month: + setDates.append(str(date.day)) + datesStr = ','.join(setDates) + return datesStr + +def has_nonzero_decimal_part(num): + # 将浮点数转换为字符串 + num_str = f"{num:.16f}".rstrip('0').rstrip('.') # 保留足够多的小数位,并去除末尾的零和点 + # 检查字符串中是否包含小数点,并且小数点后是否有字符 + # 注意:由于我们使用了rstrip去除了末尾的零,所以这里的检查是有效的 + has_decimal = '.' in num_str and num_str.split('.')[1] != '' + # 如果小数点后有字符,再检查这些字符是否全不是'0' + nonzero_decimal = has_decimal and not num_str.split('.')[1].isdigit() and '0' not in num_str.split('.')[1] or ( + has_decimal and any(char != '0' for char in num_str.split('.')[1])) + # 但是上面的逻辑有点复杂,我们可以简化它:只需要检查小数点后是否至少有一个非零字符 + # 下面的逻辑更加直接和清晰: + nonzero_decimal_simplified = '.' in num_str and any(char != '0' for char in num_str.split('.')[1]) + + # 注意:由于浮点数精度问题,直接使用==来判断两个浮点数是否相等通常是不安全的 + # 因此,我们在这里不检查num_str是否等于去掉小数部分后的字符串,而是直接检查小数点后是否有非零字符 + + return nonzero_decimal_simplified + +def transform_staffCodeToStr(idCode): + if isinstance(idCode, str): + staffIdCode = idCode.strip() # 身份码 + elif isinstance(idCode, float) or isinstance(idCode, int): + zeroTag = has_nonzero_decimal_part(idCode) + if zeroTag: + staffIdCode = str(idCode).strip() + else: + staffIdCode = str(int(idCode)).strip() + else: + staffIdCode = str(idCode).strip() # 身份码 + return staffIdCode diff --git a/apps/staff/views.py b/apps/staff/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/staff/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/staff/views/__init__.py b/apps/staff/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/staff/views/dept.py b/apps/staff/views/dept.py new file mode 100644 index 0000000..ddcac82 --- /dev/null +++ b/apps/staff/views/dept.py @@ -0,0 +1,566 @@ +import datetime +import logging +import os +import traceback +from collections import Counter + +import xlrd3 +from django.apps import apps +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.db.models import Q, Count +from django.http import FileResponse +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import SERVER_ERROR, BAD, OK, NOT_FOUND +from ChaCeRndTrans.settings import MEDIA_ROOT, SHOW_UPLOAD_PATH, FILE_PATH +from rbac.models import Company +from staff.models import Staff, Dept +from staff.serializers.Serializer import DeptSerializer, DeptModifySerializer +from utils.custom import CustomViewBase, RbacPermission, CommonPagination, req_operate_by_user, asyncDeleteFile, \ + MyCustomError + +logger = logging.getLogger('error') + + +class DeptCustomView(CustomViewBase): + ''' + 部门管理,增删改查 + ''' + perms_map = ( + {"*": "admin"}, {"*": "comadmin"}, {"*": "dept_all"}, {"get": "dept_list"}, + {"post": "dept_create"}, + {"put": "dept_edit"}, {"delete": "dept_delete"} + ) + queryset = Dept.objects.all() + serializer_class = DeptSerializer + pagination_class = CommonPagination + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + search_fields = ("name",) + ordering_fields = ("id",) + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (RbacPermission,) + + # 关联部门的模型 + modelNameList = {'Equipment': "设备", 'Building': "建筑物", 'Inassets': "无形资产", 'Longterm': "长期待摊", 'Rentequip': "租赁设备", 'Rentbuild': "租赁建筑"} + remark = [{'app': 'asset', 'model': modelName, 'info': info} for modelName, info in modelNameList.items()] + remark.append({'app': 'staff', 'model': 'Staff', 'info': '员工'}) + + def get_serializer_class(self): + # 根据请求类型动态变更serializer + if self.action == "create": + return DeptSerializer + elif self.action == "list": + return DeptSerializer + return DeptModifySerializer + + def get_queryset(self): + # query = self.request.query_params.get('q', None) # q为实际的搜索词 + # field = self.request.query_params.get('field', None) # field为具体字段 + companyMid = self.request.query_params.get('companyMid', None) # field为具体字段 + + queryset = Dept.objects.all() + + # if query and field: + # # 根据传递的字段决定搜索的字段 + # if field == 'subjectCode': + # queryset = queryset.filter(subjectCode__icontains=query) + # elif field == 'subjectName': + # queryset = queryset.filter(subjectName__icontains=query) + if companyMid: + queryset = queryset.filter(companyMid=companyMid) + return queryset + + def list(self, request, format=None): + """ + 列表 + """ + pagination = {} + try: + params = request.GET + keyword = params.get('keyword') # 关键词 + is_rnd = params.get('is_rnd') # 是否研发部 + + companyMid = params.get('companyMid') + page_size = params.get('size') + page = params.get('page') + sort = params.get('sort') + param = [] + order_by = 'N.id desc' + if sort: + order_by = sort + if page is None: + page = 1 + if page_size is None: + page_size = 10 + start_index = (int(page) - 1) * int(page_size) + + where = 'WHERE 1=1 ' + if not companyMid: + return CCAIResponse('公司信息缺失', BAD) + if companyMid: + where += 'AND N.companyMid = %s ' + param.append(companyMid) + if is_rnd: + where += 'AND N.is_rnd = %s ' + param.append(is_rnd) + if keyword: + where += 'AND N.name like %s ' + param.append('%' + keyword + '%') + + sql = 'SELECT N.id, name, pid_id, (SELECT name from chace_rnd.staff_dept where id = N.pid_id) AS parentName, ' \ + '(SELECT COUNT(id) FROM chace_rnd.staff_staff where dept = N.id) AS count, is_rnd, ' \ + 'CreateBy, UpdateBy, CreateByUid, UpdateByUid, CreateDateTime, UpdateDateTime, companyMid ' \ + 'FROM chace_rnd.staff_dept AS N ' \ + '%s ORDER BY %s LIMIT %s,%s ' % (where, order_by, start_index, page_size) + query_rows = Dept.objects.raw(sql, param) + + count_sql = """ select N.id, count(N.id) as count + from chace_rnd.staff_dept AS N %s """ % where + count_result = Dept.objects.raw(count_sql, param) + + count = 0 + if len(count_result) > 0: + count = count_result[0].count + rows = [] + for item in query_rows: + item.__dict__.pop('_state') + item.__dict__['CreateDateTime'] = item.__dict__['CreateDateTime'].strftime('%Y-%m-%d') + item.__dict__['UpdateDateTime'] = item.__dict__['UpdateDateTime'].strftime('%Y-%m-%d') + rows.append(item.__dict__) + + pagination = { + "page": page, + "page_size": page_size + } + + return CCAIResponse(rows, count=count, pagination=pagination) + + except Exception as e: + logger.error("user: %s, get dept list failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("获取部门管理列表失败", SERVER_ERROR) + + def create(self, request, *args, **kwargs): + try: + data = req_operate_by_user(request) + companyMid = request.query_params.get('companyMid', None) + if not companyMid: + return CCAIResponse("Missing company information", BAD) + is_com_exist = Company.objects.filter(MainId=companyMid).exists() + if is_com_exist: + data['companyMid'] = companyMid + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + return CCAIResponse(data="success") + else: + return CCAIResponse("Company information error", BAD) + except MyCustomError as e: + return CCAIResponse(e.message, BAD) + except Exception as e: + logger.error("user: %s, staff create failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("create failed", SERVER_ERROR) + + def update(self, request, *args, **kwargs): + from rest_framework.exceptions import ValidationError + try: + companyMid = request.query_params.get('companyMid', None) + data = req_operate_by_user(request) + data['companyMid'] = companyMid + partial = kwargs.pop('partial', False) # True:所有字段全部更新, False:仅更新提供的字段 + instance = self.get_object() + + children_ids = get_all_children(instance.id) + param_pid = data.get('pid') + if param_pid and (param_pid in children_ids or param_pid == instance.id): + return CCAIResponse("不可选自身与自身的下级部门作为上级部门", BAD) + + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + return CCAIResponse(data="success") + except ValidationError as e: + logger.error( + "user: %s, update dept cause:can not set self pid \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("can not set self as parent", BAD) + except MyCustomError as e: + return CCAIResponse(e.message, BAD) + except Exception as e: + logger.error("user: %s, update dept failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("update dept failed", SERVER_ERROR) + + def destroy(self, request, *args, **kwargs): + try: + instance = self.get_object() + # 查询是否存在关联 + is_association = self.check_association(deptId=instance.id) + if is_association: + return CCAIResponse(is_association, BAD) + self.perform_destroy(instance) + return CCAIResponse(data="delete resource success") + except Exception as e: + logger.error("user: %s, delete dept failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("delete dept failed", SERVER_ERROR) + + def check_association(self, deptId): + """ + 检查部门关联的状态,遍历查询所有与部门关联的表 + return False-不存在关联 True-存在关联 + """ + try: + isExist = False # 不存在关联 + info = None + code = None # 存在关联的人员或设备的编码 + for item in self.remark: # 遍历查询是否存在关联,一旦存在关联就不给删除 + Model = apps.get_model(app_label=item.get('app'), model_name=item.get('model')) + if not isExist: + if 'staff' == item.get('app'): + isExist = Model.objects.filter(dept=deptId).first() + else: + isExist = Model.objects.filter(departmentId=deptId).first() + + if isExist: + info = item.get('info') + code = getattr(isExist, 'code' if 'staff' != item.get('app') else 'idCode') + else: + break + if isExist: + return f'{info}:{code} 与部门存在关联!' + else: + return False + except Exception as e: + logger.error("check staff association failed: \n%s" % traceback.format_exc()) + raise Exception('检查员工关联状态失败') + + @action(methods=['delete'], detail=False) + def multiple_delete(self, request, *args, **kwargs): + try: + delete_id = request.query_params.get('ids', None) + if not delete_id: + return CCAIResponse("参数不对啊!", NOT_FOUND) + for i in delete_id.split(','): + # 查询是否存在关联 + is_association = self.check_association(deptId=i) + if not is_association: # 不存在关联,才删除 + get_object_or_404(self.queryset, pk=int(i)).delete() + else: # 存在关联,不删除,跳过 + return CCAIResponse(is_association, BAD) + return CCAIResponse("批量删除成功", OK) + except Exception as e: + logger.error("multiple delete dept failed: \n%s" % traceback.format_exc()) + return CCAIResponse("批量删除失败", SERVER_ERROR) + + @action(methods=['get'], detail=False, permission_classes=[IsAuthenticated], + url_name='download', url_path='download') + def download(self, request, *args, **kwargs): + """导入模板下载""" + try: + file = open(MEDIA_ROOT + '/部门批量导入模板.xlsx', 'rb') + response = FileResponse(file) + response['Content-Type'] = 'application/octet-stream' + response['Content-Disposition'] = 'attachment;filename="部门批量导入模板.xlsx"' + return response + except Exception as e: + logger.error("user: %s, get dept template file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("下载部门批量导入模板失败", SERVER_ERROR) + + + @action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], + url_path="import_excel", url_name="import_excel") + def import_excel(self, request): + """导入excel表数据""" + try: + params = request.data + companyMid = request.query_params.get('companyMid', None) + file_name = params.get('file_name') + + if not companyMid or not file_name: + return CCAIResponse('Missing params', BAD) + + file_paths = file_name.split(",") + if len(file_paths) >= 2: + return CCAIResponse("部门数据暂不支持同时多文件上传", BAD) + excel_file = file_paths[0].replace(SHOW_UPLOAD_PATH, FILE_PATH) + with xlrd3.open_workbook(excel_file) as data: + sheets = list(data.sheets()) + if len(sheets) > 1: + return CCAIResponse("部门数据暂不支持同时多表格上传", BAD) + + table = sheets[0] # 只读取第一张表 + rows = table.nrows + check_value = table.row_values(0) + if (check_value[0] != '部门名称' and check_value[1] != '是否研发部门' and check_value[2] != '上级部门') or (len(check_value) != 3): + return CCAIResponse("文件不符合模板标准", SERVER_ERROR) + + # 查询该公司已存在的部门记录 + dept_tree_list = [] + db_depts = self.get_queryset().order_by('pid') + deptId_dept_map = {d.id: d for d in db_depts} + db_depts_name = {x.name: deptid for deptid, x in deptId_dept_map.items()} + for d in db_depts: + if not d.pid: + dept_tree_list.append(d) + + dept_col_data = [str(dname).strip() for dname in table.col_values(0)[1:] if dname and str(dname).strip()] # 获取部门列数据 + deptCounter = Counter(dept_col_data) + error_dept_name_set = [item for item, count in deptCounter.items() if count > 1] + if error_dept_name_set: + return CCAIResponse(f"部门名称列存在重复{','.join(error_dept_name_set)}", BAD) + dept_parent_col_data = [str(pName).strip() for pName in table.col_values(2)[1:] if pName and str(pName).strip()] # 获取上级部门列数据 + error_dept_name_set = set(db_depts_name.keys()) - (set(db_depts_name.keys()) - set(dept_col_data)) + if error_dept_name_set: + return CCAIResponse(f"部门已存在:{','.join(error_dept_name_set)}", BAD) + error_dept_name_set = set(dept_parent_col_data) - (set(db_depts_name.keys()) | set(dept_col_data)) + if error_dept_name_set: + return CCAIResponse(f"未知部门:{','.join(error_dept_name_set)}", BAD) + # 数据库事务 + with transaction.atomic(): + try: + for row in range(1, rows): + row_values = table.row_values(row) # 每一行的数据 + + # 校验必填项 + if not row_values[0] or str(row_values[0]).strip() == '': + raise MyCustomError("部门名称必填!") + if len(str(row_values[0]).strip()) > 20: + raise MyCustomError("部门名称过长,请控制在20字符内!") + + is_rnd = False + if row_values[1]: + is_rnd_str = str(row_values[1]).strip() + if is_rnd_str and is_rnd_str not in ['是', '否', '不是']: + raise MyCustomError(f"部门研发属性:{str(row_values[1])}错误,请在「'是', '否', '不是'」中选择!") + if '是' == is_rnd_str: + is_rnd = True + + parent_dept_name = None + parent_id = None + if row_values[2] and str(row_values[2]).strip(): + parent_dept_name = str(row_values[2]).strip() + if parent_dept_name: + if parent_dept_name == str(row_values[0]).strip(): + raise MyCustomError(f"部门:{parent_dept_name},不可以以自身为上级") + parent_id = db_depts_name.get(parent_dept_name) + + dept = Dept() + dept.name = str(row_values[0]).strip() + dept.is_rnd = is_rnd + dept.CreateBy = request.user.name + dept.CreateByUid = request.user.id + dept.CreateDateTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + dept.companyMid = companyMid + if parent_dept_name: + dept.parent_dept_name = parent_dept_name + if parent_id: + dept.pid = deptId_dept_map.get(parent_id) + dept.save() + deptId_dept_map[dept.id] = dept + db_depts_name[dept.name] = dept.id + if not parent_dept_name: + dept_tree_list.append(dept) + + # 查看是否存在未设置pid的部门 + for deptId, d in deptId_dept_map.items(): + if hasattr(d, 'parent_dept_name') and not d.pid: + # if d.pid.id not in deptId_dept_map: + if d.parent_dept_name not in db_depts_name: + raise MyCustomError("数据有误!") + d.pid = deptId_dept_map.get(db_depts_name.get(d.parent_dept_name)) + d.save() + if check_circular_association(request, list(deptId_dept_map.values())): + raise MyCustomError("存在循环上下级关系!") + except MyCustomError as e: + transaction.set_rollback(True) + return CCAIResponse(e.message, BAD) + # 异步删除文件 + try: + if os.path.exists(MEDIA_ROOT + '/' + excel_file): # 如果文件存在 + # 删除文件,可使用以下两种方法。 + os.remove(MEDIA_ROOT + '/' + excel_file) + except Exception as e: + logger.error( + "user: %s, export ccw user xlsx file failed: \n%s" % (request.user.id, traceback.format_exc())) + + return CCAIResponse(data="success") + except Exception as e: + logger.error("user: %s, import dept file failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("导入部门记录失败", SERVER_ERROR) + + @classmethod + def changeCount(cls, method, deptId): + """ + 改变部门关联数量 + method: 1-新增关联 2-取消关联 + return: True操作成功 False操作失败 + """ + if not deptId or not method: + return False + try: + dept = Dept.objects.get(id=deptId) + except Dept.DoesNotExist: + logger.error("dept do not exist: \n%s" % (traceback.format_exc(),)) + return False + else: + try: + if 1 == method: # 新增关联 + dept.relCount += 1 + elif 2 == method: + if dept.relCount > 0: # 大于0才取消关联 + dept.relCount -= 1 + else: + return False + dept.save() + return True + except Exception as e: + logger.error("change dept relCount failed: \n%s" % (traceback.format_exc(),)) + return False + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="getDeptMap", url_name="getDeptMap") + def getDeptMap(self, request): + try: + params = request.GET + companyMid = params.get('companyMid') + if not companyMid: + return CCAIResponse("Missing company information", BAD) + if companyMid: + query = Q(companyMid=companyMid) + dept = Dept.objects.filter(query).values('id', 'name') + return CCAIResponse(dept) + except Exception as e: + logger.error("user: %s, getDeptMap failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("getDeptMap failed", SERVER_ERROR) + + @action(methods=["get"], detail=False, permission_classes=[IsAuthenticated], + url_path="tree", url_name="tree") + def tree(self, request, format=None): + """ + 树状列表 + """ + params = request.GET + companyMid = params.get('companyMid') + if not companyMid: + return CCAIResponse("Missing company information", BAD) + # 统计员工表不同部门人数 + sql = 'SELECT id, dept, COUNT(id) AS count FROM chace_rnd.staff_staff WHERE companyMid = %s and status = 0 GROUP BY dept ' + result = Staff.objects.raw(sql, [companyMid]) + # result = Staff.objects.filter(companyMid=companyMid, status=0).values('dept').annotate(count=Count('id')) + deptCountDict = {item.dept: item.count for item in result} + + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + serializer = self.get_serializer(queryset, many=True) + tree_dict = {} + tree_data = [] + try: + # 初始赋值字典与count + for item in serializer.data: + if item['id'] in deptCountDict: + item['count'] = deptCountDict[item['id']] + else: + item['count'] = 0 + tree_dict[item["id"]] = item + # 设置树状结构 + for i in tree_dict: + if tree_dict[i]["pid"]: + pid = tree_dict[i]["pid"] + parent = tree_dict[pid] + parent.setdefault("children", []).append(tree_dict[i]) + else: + tree_data.append(tree_dict[i]) + # 统计部门人数 1:递归 2:多层遍历 + for dept in tree_data: # 第一层部门 + if 'children' in dept: # 若存在下级部门,调用方法递归累加最终数量 + self.getCountFromTree(dept) + + results = tree_data + except KeyError: + results = serializer.data + if page is not None: + return self.get_paginated_response(results) + return CCAIResponse(results) + + def getCountFromTree(self, parentDept): + """ + (递归)设置并返回当前本部门的下级部门的总人数 + """ + sum = 0 # 子部门人数统计 + for dept in parentDept['children']: + if 'children' in dept: # 存在下级部门,递归调用,设置当前部门人数,并返回上级,本部门人数 + deptCount = self.getCountFromTree(dept) + sum += deptCount + else: # 最底层部门不需要设置,只需要累计到上层 + sum += dept['count'] + parentDept['count'] += sum # 本部门 + 子级部门 + return parentDept['count'] + + # def getCountFromTree(self, parentDept): + # """ + # (模拟栈)设置并返回当前本部门的下级部门的总人数 + # """ + # stack = [parentDept] # 使用栈来模拟递归的过程 + # # results = [] # 用于保存每次调用的结果 + # while stack: + # parentDept = stack.pop() + # sum = 0 # 子部门人数统计 + # for dept in parentDept['children']: + # if 'children' in dept: # 存在下级部门,递归调用,设置当前部门人数,并返回上级,本部门人数 + # stack.append(dept) + # # deptCount = self.getCountFromTree(dept) + # # sum += deptCount + # else: # 最底层部门不需要设置,只需要累计到上层 + # sum += dept['count'] + # parentDept['count'] += sum # 本部门 + 子级部门 + # # results.append(parentDept['count']) + + +def get_all_children(deptId, max_depth=50, current_depth=0): + # 如果设置了最大深度并且当前深度已经达到或超过最大深度,则返回空列表 + if current_depth >= max_depth: + return [] + + children = [] + sub_depts = Dept.objects.filter(pid=deptId) + for sub_dept in sub_depts: + children.append(sub_dept.id) + # 在递归调用时增加当前深度 + children.extend(get_all_children(sub_dept.id, max_depth, current_depth + 1)) + return children + +def check_circular_association(request, dept_list): + """ + 检查部门循环上下级, 存在循环问题会返回True,否则返回False + deptId_dept_map: [ Node1, Node2, ... ] + """ + try: + if not dept_list or 0 == len(dept_list): + return False + + for node in dept_list: + nodeList = [node, ] + n = 0 + while node.pid: + n += 1 + if n >= 50: + return True + if node.pid in nodeList: + return True + node = node.pid + nodeList.append(node) + + return False + except Exception as e: + logger.error("user: %s, check_circular_association failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse("check_circular_association failed", SERVER_ERROR) \ No newline at end of file diff --git a/apps/tasks/__init__.py b/apps/tasks/__init__.py new file mode 100644 index 0000000..ec43e3f --- /dev/null +++ b/apps/tasks/__init__.py @@ -0,0 +1 @@ +default_app_config = "tasks.apps.TasksConfig" diff --git a/apps/tasks/admin.py b/apps/tasks/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/tasks/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/tasks/apps.py b/apps/tasks/apps.py new file mode 100644 index 0000000..2054722 --- /dev/null +++ b/apps/tasks/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TasksConfig(AppConfig): + name = 'tasks' diff --git a/apps/tasks/migrations/__init__.py b/apps/tasks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tasks/models.py b/apps/tasks/models.py new file mode 100644 index 0000000..6ecefbe --- /dev/null +++ b/apps/tasks/models.py @@ -0,0 +1,20 @@ +from django.db import models + + +# Create your models here. +class TasksConsloe(models.Model): + """ + 任务输出 + """ + task = models.CharField(max_length=500, null=True, blank=True, verbose_name="任务") + content = models.CharField(max_length=500, null=True, blank=True, verbose_name="内容") + start_time = models.DateTimeField(auto_now_add=True, verbose_name="执行时间") + finish_time = models.DateTimeField(auto_now_add=True, verbose_name="结束时间") + + class Meta: + verbose_name = "任务输出" + verbose_name_plural = verbose_name + ordering = ["id"] + + def __str__(self): + return self.task diff --git a/apps/tasks/tasks.py b/apps/tasks/tasks.py new file mode 100644 index 0000000..a26c7a9 --- /dev/null +++ b/apps/tasks/tasks.py @@ -0,0 +1,18 @@ +import datetime +import json +import logging +import traceback +import uuid + +from django.db import connections, transaction +from django_redis import get_redis_connection + + +logger = logging.getLogger('error') +logger_info = logging.getLogger("info") + + +class TasksFactory(object): + + def crontest(self): + logger_info.info('测试定时任务管理cron表达式') \ No newline at end of file diff --git a/apps/tasks/tests.py b/apps/tasks/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tasks/urls.py b/apps/tasks/urls.py new file mode 100644 index 0000000..95fb3ed --- /dev/null +++ b/apps/tasks/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from tasks.views import TaskAddView, TaskUpdateView, TaskDeleteView, TaskListView, TaskSearchView, TaskStartView, \ + TaskStopView + +urlpatterns = [ + path(r"api/tasks/add/", TaskAddView.as_view(), name="task_add"), + path(r"api/tasks/update/", TaskUpdateView.as_view(), name="task_update"), + path(r"api/tasks/delete/", TaskDeleteView.as_view(), name="task_delete"), + path(r"api/tasks/list/", TaskListView.as_view(), name="task_list"), + path(r"api/tasks/search/", TaskSearchView.as_view(), name="task_search"), + path(r"api/tasks/stop/", TaskStopView.as_view(), name="task_stop"), + path(r"api/tasks/start/", TaskStartView.as_view(), name="task_start"), +] diff --git a/apps/tasks/views.py b/apps/tasks/views.py new file mode 100644 index 0000000..a024b09 --- /dev/null +++ b/apps/tasks/views.py @@ -0,0 +1,256 @@ +import datetime +import logging +import traceback + +from apscheduler.executors.pool import ThreadPoolExecutor +from apscheduler.schedulers.background import BackgroundScheduler +from django.db import connection +from django_apscheduler.jobstores import register_events +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +from ChaCeRndTrans import settings +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import * +from tasks.tasks import TasksFactory +from utils.custom import is_connection_usable + +logger = logging.getLogger('error') + +MYSQL_URL = "mysql://{user}:{password}@{host}:{port}/{dbname}".format( + user=settings.DATABASES.get('default').get('USER'), + password=settings.DATABASES.get('default').get('PASSWORD'), + host=settings.DATABASES.get('default').get('HOST'), + port=settings.DATABASES.get('default').get('PORT'), + dbname=settings.DATABASES.get('default').get('NAME'), +) + +executors = { + 'default': ThreadPoolExecutor(20) # 最多20个线程同时执行 +} + +# 实例化调度器 +scheduler = BackgroundScheduler(executors=executors) +# 调度器使用默认的DjangoJobStore() +scheduler.add_jobstore(jobstore='sqlalchemy', url=MYSQL_URL, + engine_options={'pool_pre_ping': True, 'pool_recycle': 3200}) +# 注册定时任务并开始 + +register_events(scheduler) +scheduler.start() + + +class TaskListView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, *args, **kwargs): + list = [] + for item in scheduler.get_jobs(): + next_run_time = item.next_run_time + if item.next_run_time: + next_run_time = item.next_run_time.strftime('%Y-%m-%d %H:%M:%S') + task = {'id': item.id, 'name': item.name, 'args': item.args, 'next_run_time': next_run_time} + list.append(task) + return CCAIResponse(list, status=OK) + + +class TaskAddView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + if not is_connection_usable(): + connection.close() + + try: + start_time = request.data.get('start_time') # 用户输入的任务开始时间, '10:00:00' + args = request.data.get('args', '1') # 接收执行任务的各种参数 + task_name = request.data.get('task_name') + task_type = request.data.get('task_type') + if task_type == 'interval': + start_time = start_time.split(':') + hour = int(start_time[0]) + minute = int(start_time[1]) + second = int(start_time[2]) + if task_type == 'cron': + if ':' in start_time: + start_time_list = start_time.split(':') + hour = int(start_time_list[0]) + minute = int(start_time_list[1]) + second = int(start_time_list[2]) + else: + croncode = start_time.split() + if len(croncode) != 5: + return CCAIResponse('cron表达式错误', BAD) + for index, p in enumerate(croncode): + if p == '*': + croncode[index] = None + second, minute, hour, day, month = croncode # 0 30 1 1 1 0 30 1 1 * + + # 创建任务 + if task_type == 'date': + start_time = datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") + scheduler.add_job(getattr(TasksFactory, task_name), task_type, max_instances=10, run_date=start_time, + args=[args]) + if task_type == 'interval': + scheduler.add_job(getattr(TasksFactory, task_name), task_type, max_instances=10, hours=hour, + minutes=minute, seconds=second, args=[args]) + if task_type == 'cron': + if ':' in start_time: + scheduler.add_job(getattr(TasksFactory, task_name), task_type, max_instances=10, hour=hour, + minute=minute, second=second, args=[args]) + else: + scheduler.add_job(getattr(TasksFactory, task_name), 'cron', second=second, minute=minute, hour=hour, + day=day, month=month, args=[args]) + return CCAIResponse('创建任务成功', status=OK) + except Exception as e: + logger.error("user: %s, add task failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse('创建任务失败', status=BAD) + + +class TaskStopView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + if not is_connection_usable(): + connection.close() + + try: + task_name = request.data.get('task_name') + scheduler.pause_job(task_name) + + return CCAIResponse('停止任务成功', status=OK) + except Exception as e: + logger.error("user: %s, stop task failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse('停止任务失败', status=BAD) + + +class TaskStartView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + if not is_connection_usable(): + connection.close() + + try: + task_name = request.data.get('task_name') + scheduler.resume_job(task_name) + + return CCAIResponse('启动任务成功', status=OK) + except Exception as e: + logger.error("user: %s, start task failed: \n%s" % (request.user.id, traceback.format_exc())) + return CCAIResponse('启动任务失败', status=BAD) + + +class TaskUpdateView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + if not is_connection_usable(): + connection.close() + + if request.user.id is not None: + task_id = request.data.get("task_id") + task_name = request.data.get("task_name") + task_type = request.data.get("task_type") + start_time = request.data.get("start_time") + + args = request.data.get('args') # 接收执行任务的各种参数 + if task_type == 'interval': + start_time = start_time.split(':') + hour = int(start_time[0]) + minute = int(start_time[1]) + second = int(start_time[2]) + + if task_type == 'cron': + if ':' in start_time: + start_time_list = start_time.split(':') + hour = int(start_time_list[0]) + minute = int(start_time_list[1]) + second = int(start_time_list[2]) + else: + croncode = start_time.split() + if len(croncode) != 5: + return CCAIResponse('cron表达式错误', BAD) + for index, p in enumerate(croncode): + if p == '*': + croncode[index] = None + second, minute, hour, day, month = croncode # 0 30 1 1 1 + + # 创建任务 + if task_type == 'date': + temp_dict = {"run_date": start_time} + temp_trigger = scheduler._create_trigger(trigger='date', trigger_args=temp_dict) + result = scheduler.modify_job(job_id=task_id, max_instances=10, trigger=temp_trigger) + + if task_type == 'interval': + temp_dict = {'hours': hour, 'minutes': minute, "seconds": second} + temp_trigger = scheduler._create_trigger(trigger='interval', trigger_args=temp_dict) + result = scheduler.modify_job(job_id=task_id, max_instances=10, trigger=temp_trigger) + + if task_type == 'cron': + if ':' in start_time: + temp_dict = {'hour': hour, 'minute': minute, "second": second} + temp_trigger = scheduler._create_trigger(trigger='cron', trigger_args=temp_dict) + result = scheduler.modify_job(job_id=task_id, max_instances=10, trigger=temp_trigger) + else: + job = scheduler.get_job(job_id=task_id) + task_name = job.name + task_name = task_name.split('.')[1] + scheduler.remove_job(job_id=task_id) + scheduler.add_job(getattr(TasksFactory, task_name), 'cron', second=second, minute=minute, hour=hour, + day=day, month=month, args=[1]) + + return CCAIResponse("更新任务成功!", status=OK) + else: + return CCAIResponse("更新任务失败!", status=BAD) + + +class TaskDeleteView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + """ + 删除任务 + :param task_ids: 任务id的list + :return: + """ + if not is_connection_usable(): + connection.close() + + if request.user is not None: + task_name = request.data.get("task_name") + scheduler.remove_job(task_name) + return CCAIResponse("删除任务成功!", status=OK) + else: + return CCAIResponse("删除任务失败!", status=BAD) + + +class TaskSearchView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + """ + 任务查询 + :param task_name: 任务名称 + :param task_queue: 任务队列 + :return: task_name任务名称、task_queue任务队列、task_args任务参数、task_class任务执行类、task_cron任务定时的表达式 + """ + if not is_connection_usable(): + connection.close() + + # 查询目前满足条件的所有周期性任务 + if request.user is not None: + task_name = request.data.get("task_name") + task_queue = request.data.get("task_queue") + + # return CCAIResponse(data) + else: + return CCAIResponse("查询任务失败!", status=BAD) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..627f8bd --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ChaCeRndTrans.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/media/research_descriptions.txt b/media/research_descriptions.txt new file mode 100644 index 0000000..c49e484 --- /dev/null +++ b/media/research_descriptions.txt @@ -0,0 +1,121 @@ +讨论项目进展与研发团队 +评审技术方案参与 +协助新产品功能测试 +编写文档技术 +优化系统架构现有 +参与审查代码 +设计模块新功能 +分析反馈用户并改进产品 +研究应用新技术 +开发测试工具自动化 +参与计划产品发布 +沟通客户技术需求 +改进设计用户界面 +进行测试性能 +解决故障技术 +参与培训技术 +编写接口文档API +设计结构数据库 +测试项目 +参与规划项目 +与团队研发讨论进展项目 +参与评审方案技术 +协助测试功能新产品 +编写技术的文档 +优化架构系统现有 +参与代码的审查 +设计功能模块新 +分析用户的反馈并改进产品 +研究新技术的应用 +开发自动化的测试工具 +参与发布计划产品 +与客户沟通需求技术 +改进用户的界面设计 +进行性能的测试 +解决故障的技术 +参与培训的技术 +编写API的接口文档 +设计数据库的结构 +测试的项目 +参与项目的规划 +讨论研发团队项目进展 +参与技术评审方案 +协助功能测试新产品 +编写文档技术 +优化架构现有系统 +参与代码审查 +设计模块功能新 +分析反馈用户改进产品 +研究应用技术新 +开发工具自动化测试 +参与计划发布产品 +沟通技术需求客户 +改进界面设计用户 +进行测试性能 +解决故障技术 +参与培训技术 +编写文档接口API +设计结构数据库 +测试项目 +参与规划项目 +研发团队讨论进展项目 +评审参与方案技术 +协助测试新功能产品 +编写技术文档 +优化系统架构现有 +代码参与审查 +设计新模块功能 +分析用户反馈改进产品 +研究新应用技术 +开发自动化工具测试 +参与发布产品计划 +客户沟通需求技术 +改进用户设计界面 +进行性能测试 +解决技术故障 +参与技术培训 +编写接口API文档 +设计数据库结构 +项目测试 +规划参与项目 +讨论项目进展与团队研发 +参与方案评审技术 +协助测试功能新产品 +编写文档的技术 +优化架构的现有系统 +参与审查的代码 +设计功能的新模块 +分析反馈的用户并改进产品 +研究应用的新技术 +开发工具的 +自动化测试 +参与计划的产品发布 +沟通需求的客户技术 +改进设计的用户界面 +进行测试的性能 +解决技术的故障 +参与培训的技术 +编写文档的API接口 +设计结构的数据库 +测试的项目 +参与规划的项目 +研发团队进展项目讨论 +方案技术评审参与 +功能新产品测试协助 +文档技术编写 +架构现有系统优化 +审查代码参与 +模块新功能设计 +用户反馈分析改进产品 +新技术应用研究 +自动化测试工具开发 +产品发布计划参与 +技术需求客户沟通 +用户界面设计改进 +性能测试进行 +技术故障解决 +技术培训参与 +API接口文档编写 +数据库结构设计 +项目测试 +项目规划参与 \ No newline at end of file diff --git a/media/人员工时生成表.xlsx b/media/人员工时生成表.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8b5aad3be962aeb9628aba207d775210d410a89a GIT binary patch literal 10239 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
_x;J>L8oa(^l}9WX|zHDM8|Kj+3Z; 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..55da39fbad689e9e22af90c4df56240f6c489a70 GIT binary patch literal 3254 zcma);NpD+65QOI(AU}nINH4JuJ{T|%#E20E3gBG1ij>Gliq8xw{rDvHb-m_1Iu2k6 z+Sprlb@j~r^Y^%{%c?w;LutyO?DW@{vMk&9uPw9kpiigze_!V1oxVopstn5u{d`fb z%a7%U@;w>m%F#yrOGS<%{$&@vDUYg~xnDtpjs;|2I*W!R~hQVk0EzTapuu&x|EX6Yj=E?*{P>{hrOatRo)51pY zWEe#c)izK(JwB<|L)j?TxW}y7h1q_@PMHtEU(-#{*r@QeFAU?Kv%>Rr=Vb+rWls*6 zW6m-a)^_vU2w7(7cexk#ofy~((Oy0~P2Zf8%i1)mlXUxA_|T2D`$j%sGN&J;E&9Jx zwqz8m8J+d4gQ%3)F0}?a=54Moquf<1BxrOT)7y6}g10K@GHzBT-PSe2$L^pW@8nLu zw;jjs1+CNbWe&8qR$T4`+*mXJ^<{1-qi*Q1$J1rrR(_k9@;v$?ig_>@So92F#VlV& z(+9eHj&8^RPPWIp^=(GLN0mj)llWs)wA$&)c&*5O#s7~SXue0~1T#EdcDz@ufjSzi z?p@Evn+1dH;h>z<%{qEWjpa;?Wg+WUb#e;u@>bv07#g1ItC3gkb;g@Lw!W2m*ji5q zyE_KOGWHl1uf(ZUo{BUAA{%AN*}Ms@tn?Y!`r5+d>sZcS&Ms`shRP#xMeUV7XB(MK zvdHv}{9bjZs_WAL)WkdTqhg!buTex=Gx*qk9~x{{ zH;1ijMLrANKwcA7a-ZC9ob#tnt5mFK?7Ev~_FA|4ld|n~wjLCJtG^c=(?*=AH|m#; zC!H0<#spg%)ogXst>wdw%hb)QIm;}K@U^}CFz)?sV%FTy$@Va`m<)6*yM&7cc-~^?R)5LOcaksb3=1k19Yx%Bh=i5=ozbT*qpbSL?zH_XznZ*Es}J$uv>pDfrbr#H&+v3xFn1RkbQ_mgON?z3DcO5`Vf-^qGL^{oVJoVEx3gr`Kyol|r6{kQ7>bgm70Yu9<7y9~S(tTR;f zD4w`gcn9PRJY}qx=Qy9&ox}DhbUsNv-()*0?`0py?yVKe>Bs3t9ZwE+-OINzU*iV%7mumUsA`dyh;j;y0 zUUpTd?t6^WawacgtJZwhSqlSIImNVj6ZvK~qvo58p5+&vLTpSu2`iF=kaC93g@;#@ z^+VOVEA)k*-=Gs&l9|3L4%&g+@^kqmtW@y4xM|!qjPR1k=*rqr*p9N!gruId8TvPL#r!Xlbk@KC literal 0 HcmV?d00001 diff --git a/requirementstest.txt b/requirementstest.txt new file mode 100644 index 0000000000000000000000000000000000000000..7068c05f73c0bc7d0666ee154ab0c0201d39923d GIT binary patch literal 11580 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 literal 0 HcmV?d00001 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/custom.py b/utils/custom.py new file mode 100644 index 0000000..8828e22 --- /dev/null +++ b/utils/custom.py @@ -0,0 +1,1017 @@ +import calendar +import hashlib +import json +import logging +import math +import os +import random +import re +import time +import traceback +from binascii import b2a_hex, a2b_hex +from datetime import timedelta, datetime, date +from decimal import Decimal, ROUND_HALF_UP +from typing import List, Any + +import unicodedata +from Crypto.Cipher import AES +from dateutil.relativedelta import relativedelta +from django.conf import settings +from django.core.paginator import InvalidPage +from django.db import connection +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django_filters import rest_framework +from rest_framework import filters +from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.exceptions import NotFound, Throttled +from rest_framework.generics import ListAPIView +from rest_framework.pagination import PageNumberPagination +from rest_framework.permissions import BasePermission +from rest_framework.permissions import IsAuthenticated +from rest_framework.settings import api_settings +from rest_framework.views import exception_handler +from rest_framework.viewsets import ModelViewSet +from rest_framework_jwt.authentication import JSONWebTokenAuthentication + +import ChaCeRndTrans.code +from ChaCeRndTrans.basic import CCAIResponse +from ChaCeRndTrans.code import * +from hashids import Hashids + +error_logger = logging.getLogger("error") +info_logger = logging.getLogger("info") + + +# 初始化 Hashids 对象 +salt = settings.ID_KEY # 盐值 +min_length = 16 # 生成的最小长度 +ChaCeHashids = Hashids(salt=salt, min_length=min_length) + + +class CustomViewBase(ModelViewSet): + # pagination_class = LargeResultsSetPagination + # filter_class = ServerFilter + queryset = '' + serializer_class = '' + permission_classes = () + filter_fields = () + search_fields = () + filter_backends = (rest_framework.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,) + + def create(self, request, *args, **kwargs): + data = req_operate_by_user(request) + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + return CCAIResponse(data="success") + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return CCAIResponse(data=serializer.data) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance) + return CCAIResponse(data=serializer.data) + + def update(self, request, *args, **kwargs): + data = req_operate_by_user(request) + partial = kwargs.pop('partial', False) # True:所有字段全部更新, False:仅更新提供的字段 + instance = self.get_object() + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} + + return CCAIResponse(data="success") + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + self.perform_destroy(instance) + return CCAIResponse(data="delete resource success") + + @action(methods=['delete'], detail=False) + def multiple_delete(self, request, *args, **kwargs): + try: + delete_id = request.query_params.get('ids', None) + if not delete_id: + return CCAIResponse("参数不对啊!", NOT_FOUND) + for i in delete_id.split(','): + get_object_or_404(self.queryset, pk=int(i)).delete() + return CCAIResponse("批量删除成功", OK) + except Exception as e: + error_logger.error("multiple delete crawler news failed: \n%s" % traceback.format_exc()) + return CCAIResponse("批量删除失败", SERVER_ERROR) + + +class CustomHashViewBase(ModelViewSet): + # pagination_class = LargeResultsSetPagination + # filter_class = ServerFilter + queryset = '' + serializer_class = '' + permission_classes = () + filter_fields = () + search_fields = () + filter_backends = (rest_framework.DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,) + + def create(self, request, *args, **kwargs): + data = req_operate_by_user(request) + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + # headers = self.get_success_headers(serializer.data) + return CCAIResponse(data="success") + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + data = serializer.data + for item in data: + item['id'] = ChaCeHashids.encode(item['id']) + return self.get_paginated_response(data) + + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + for item in data: + item['id'] = ChaCeHashids.encode(item['id']) + return CCAIResponse(data=data) + + def retrieve(self, request, *args, **kwargs): + encoded_id = kwargs.get('pk') + decoded_id = ChaCeHashids.decode(encoded_id) + if not decoded_id: + return CCAIResponse(data="Invalid ID", status=BAD) + + instance = self.get_queryset().get(pk=decoded_id[0]) + serializer = self.get_serializer(instance) + return CCAIResponse(data=serializer.data) + + def update(self, request, *args, **kwargs): + data = req_operate_by_user(request) + encoded_id = kwargs.get('pk') + decoded_id = ChaCeHashids.decode(encoded_id) + if not decoded_id: + return CCAIResponse(data="Invalid ID", status=BAD) + + partial = kwargs.pop('partial', False) + instance = self.get_queryset().get(pk=decoded_id[0]) + serializer = self.get_serializer(instance, data=data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + instance._prefetched_objects_cache = {} + + return CCAIResponse(data="success") + + def destroy(self, request, *args, **kwargs): + encoded_id = kwargs.get('pk') + decoded_id = ChaCeHashids.decode(encoded_id) + if not decoded_id: + return CCAIResponse(data="Invalid ID", status=BAD) + + instance = self.get_queryset().get(pk=decoded_id[0]) + self.perform_destroy(instance) + return CCAIResponse(data="delete resource success") + + @action(methods=['delete'], detail=False) + def multiple_delete(self, request, *args, **kwargs): + try: + delete_id = request.query_params.get('ids', None) + if not delete_id: + return CCAIResponse("参数不对啊!", status=NOT_FOUND) + + for encoded_id in delete_id.split(','): + decoded_id = ChaCeHashids.decode(encoded_id) + if not decoded_id: + return CCAIResponse("无效的ID: {}".format(encoded_id), status=NOT_FOUND) + + get_object_or_404(self.queryset, pk=decoded_id[0]).delete() + + return CCAIResponse("批量删除成功", status=OK) + except Exception as e: + error_logger.error("multiple delete failed: \n%s" % traceback.format_exc()) + return CCAIResponse("批量删除失败", status=SERVER_ERROR) + + +def chacerde_exception_handler(exc, context): + response = exception_handler(exc, context) + + # 限流异常 + if isinstance(exc, Throttled): + msg = "失败" if response.status_code >= 400 else "成功" + notification_response = {} + notification_response["code"] = response.status_code + notification_response["message"] = msg + notification_response["detail"] = response.data + notification_response['wait'] = exc.wait + response.data = notification_response + return response + + if response is not None: + msg = "失败" if response.status_code >= 400 else "成功" + notification_response = {} + notification_response["code"] = response.status_code + notification_response["message"] = msg + notification_response["detail"] = response.data + response.data = notification_response + return response + + +class CommonPagination(PageNumberPagination): + """ + 分页设置 + """ + # 默认每页显示的数据条数 + page_size = 10 + # 获取url参数中设置的每页显示数据条数 + page_size_query_param = settings.MY_PAGE_SIZE_QUERY_PARAM + # 获取url中传入的页码key + page_query_param = settings.MY_PAGE_QUERY_PARAM + # 最大支持的每页显示的数据条数 + max_page_size = settings.MY_MAX_PAGE_SIZE + + """ + 自定义分页方法 + """ + + def get_paginated_response(self, data): + """ + 设置返回内容格式 + """ + return CCAIResponse( + count=self.page.paginator.count, + data=data) + + """ + 自定义列表查询方法; + 当前端请求的页数超过实际有效的页数时,直接返回最后一页的数据 + """ + + def paginate_queryset(self, queryset, request, view=None): + """ + Paginate a queryset if required, either returning a + page object, or `None` if pagination is not configured for this view. + """ + page_size = self.get_page_size(request) + if not page_size: + return None + + # 对queryset进行排序, 根据id升序排列 + # queryset = queryset.order_by('id') + + paginator = self.django_paginator_class(queryset, page_size) + page_number = request.query_params.get(self.page_query_param, 1) + count = paginator.count # 列表累计的总数 + if page_number in self.last_page_strings: + page_number = paginator.num_pages + + # paginator.num_pages:最大的页数 + if count and int(page_number) and (int(page_number) > paginator.num_pages): + self.page = paginator.page(paginator.num_pages) # 返回最后一页的列表 + else: + try: + self.page = paginator.page(page_number) + except InvalidPage as exc: + msg = self.invalid_page_message.format( + page_number=page_number, message=str(exc) + ) + raise NotFound(msg) + + if paginator.num_pages > 1 and self.template is not None: + # The browsable API should display pagination controls. + self.display_page_controls = True + + self.request = request + return list(self.page) + + +class RbacPermission(BasePermission): + """ + 自定义权限 + """ + + # @classmethod + # def get_permission_from_role(self, request): + # try: + # perms = request.user.roles.values( + # "permissions__method", + # ).distinct() + # return [p["permissions__method"] for p in perms] + # except AttributeError: + # return None + + @classmethod + def get_permission_from_role(self, request): + """ + 根据用户角色与公司id,返回对应的权限 + """ + try: + if request.user: + perms_list = [] + # for item in request.user.roles.values("permissions__method").distinct(): + # perms_list.append(item["permissions__method"]) + companyMid = request.query_params.get('companyMid', None) + if not companyMid: + for item in request.user.roles.values("permissions__method").distinct(): + perms_list.append(item["permissions__method"]) + else: + for item in request.user.roles.filter(Q(companyMid=companyMid) | Q(companyMid__isnull=True)).values("permissions__method").distinct(): + perms_list.append(item["permissions__method"]) + return perms_list + except AttributeError: + return None + + # @classmethod + # def get_permission_from_grouprole(self, request): + # """ + # 从grouprole在获取role然后再获取permission + # """ + # try: + # if request.user: + # perms_list = [] + # roleid_set = set() + # for item in request.user.grouprole.values("roles").distinct(): + # if item["roles"] and item["roles"] != '': + # roleid_list = item["roles"].split(',') + # roleid_set.update(roleid_list) + # if roleid_set: + # from rbac.models import Role # 解决循环依赖的问题 + # for item in Role.objects.filter(id__in=roleid_set).values("permissions__method").distinct(): + # perms_list.append(item["permissions__method"]) + # return perms_list + # except AttributeError: + # return None + def has_permission(self, request, view): + perms = self.get_permission_from_role(request) + if perms: + if "admin" in perms: + return True + elif not hasattr(view, "perms_map"): + return True + else: + perms_map = view.perms_map + _method = request._request.method.lower() + for i in perms_map: + for method, alias in i.items(): + if (_method == method or method == "*") and alias in perms: + return True + + +class ObjPermission(BasePermission): + """ + 密码管理对象级权限控制 + """ + + def has_object_permission(self, request, view, obj): + perms = RbacPermission.get_permission_from_role(request) + if "admin" in perms: + return True + elif request.user.id == obj.uid_id: + return True + + +class TreeSerializer(serializers.Serializer): + id = serializers.IntegerField() + label = serializers.CharField(max_length=20, source="name") + pid = serializers.PrimaryKeyRelatedField(read_only=True) + + +class TreeAPIView(ListAPIView): + """ + 自定义树结构View + """ + serializer_class = TreeSerializer + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + serializer = self.get_serializer(queryset, many=True) + tree_dict = {} + tree_data = [] + try: + for item in serializer.data: + tree_dict[item["id"]] = item + for i in tree_dict: + if tree_dict[i]["pid"]: + pid = tree_dict[i]["pid"] + try: + parent = tree_dict[pid] + except KeyError: + # 缺少父级菜单 + continue + parent.setdefault("children", []).append(tree_dict[i]) + else: + tree_data.append(tree_dict[i]) + results = tree_data + except KeyError: + results = serializer.data + + if page is not None: + return self.get_paginated_response(results) + return CCAIResponse(results) + + +def req_operate_by_user(request): + data = request.data.copy() + if request.method == "POST": + data['CreateBy'] = request.user.name + data['CreateByUid'] = request.user.id + data['UpdateBy'] = request.user.name + data['UpdateByUid'] = request.user.id + elif request.method == "PUT": + data['UpdateBy'] = request.user.name + data['UpdateByUid'] = request.user.id + return data + + +def sha1_encrypt(data): + """ + 使用sha1加密算法,返回str加密后的字符串 + """ + sha = hashlib.sha1(data.encode('utf-8')) + encrypts = sha.hexdigest() + return encrypts + + +def generate_random_str(randomlength=10): + """ + 生成一个指定长度的随机字符串 + """ + random_str = '' + base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789*@#$%^-~+' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length - 1)] + return random_str + +def is_connection_usable(): + try: + connection.connection.ping() + except: + return False + else: + return True +def generate_random_str_for_fileName(randomlength=10): + """ + 生成一个指定长度的随机字符串, 用于随机生成文件名,不包含特殊符号 + """ + random_str = '' + base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length - 1)] + return random_str + + +def generate_random_str_16_system(randomlength=16): + """ + 生成一个指定长度的随机字符串, 这个字符串看起来像16进制的 + """ + random_str = '' + base_str = '0123456789abcdef' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length)] + return random_str + + +def generate_random_pwd(the_length=9): + """生成指定长度的随机明文密码""" + if the_length < 9: + the_length = 9 + special_str = "!@#$%^&*+" + special_str_length = random.randint(1, 2) # 特殊字符的长度 + nums = "1234567890" + nums_length = random.randint(3, 4) # 特殊字符的长度 + zi_mu = "ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz" + pwd = "" + for i in range(special_str_length): + pwd += special_str[random.randint(0, len(special_str) - 1)] + for i in range(nums_length): + pwd += nums[random.randint(0, len(nums) - 1)] + for i in range(the_length - special_str_length - nums_length): + pwd += zi_mu[random.randint(0, len(zi_mu) - 1)] + new_pwd = "" + ran = random.sample(range(0, the_length), the_length) # 随机生成9个(0, 9)范围内不重复的数字:不含9 + for i in ran: + new_pwd += pwd[i] + return new_pwd + + +# X-Forwarded-For:简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项。 +def get_ip(request): + '''获取请求者的IP信息''' + try: + remote_addr = request.META.get('HTTP_X_REAL_IP') + if remote_addr: + return remote_addr + else: + xff = request.META.get('HTTP_X_FORWARDED_FOR') + remote_addr = request.META.get('REMOTE_ADDR') + num_proxies = api_settings.NUM_PROXIES + + if num_proxies is not None: + if num_proxies == 0 or xff is None: + return remote_addr + addrs = xff.split(',') + client_addr = addrs[-min(num_proxies, len(addrs))] + return client_addr.strip() + return ''.join(xff.split()) if xff else remote_addr + except Exception as e: + return '' + + +# 生成操作记录存到数据表 +def create_operation_history_log(request, info, TheModel): + try: + if request: + ip = get_ip(request) + if request.user.id: + TheModel.objects.create(ip=ip, des=info["des"], user_id=request.user.id, + username=request.user.username, detail=info["detail"]) + else: + TheModel.objects.create(ip=ip, des=info["des"], + username="智云用户", detail=info["detail"]) + else: + TheModel.objects.create(des=info["des"], username="task", detail=info["detail"]) + + except Exception as e: + error_logger.error("user: %s, create history failed \n%s" % (request.user.id, traceback.format_exc())) + + +# 检测密码复杂度 +def check_password_complexity(password): + must_str = [".", "!", "`", "/" "?", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+", "-"] # 必须包含这其中之二的字符 + must_num = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] # 必须1个 + must_abc = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", + "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", + "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] # 必须1个 + password_list = list(password) + if len(password_list) < 8: + return {"flag": False, "message": "密码长度至少8个字符及以上"} + elif len(password_list) > 25: + return {"flag": False, "message": "密码长度至多25个字符及以下"} + must_str_nums = 0 + must_num_nums = 0 + must_abc_nums = 0 + for each in password_list: + if each in must_str: + must_str_nums += 1 + elif each in must_num: + must_num_nums += 1 + elif each in must_abc: + must_abc_nums += 1 + else: + return {"flag": False, "message": "密码不允许存在其他不规范字符"} + if must_str_nums < 2: + return {"flag": False, "message": "密码必须包含2个英文特殊字符及以上如:.!`/?@#$%^&*()+-"} + if must_num_nums < 1: + return {"flag": False, "message": "密码必须包含数字"} + if must_abc_nums < 1: + return {"flag": False, "message": "密码必须包含字母"} + return {"flag": True, "message": "success"} + + +# 惩罚记录函数 +def record_punishment(data, request, PunishmentInfoModel): + record = PunishmentInfoModel.objects.filter(user_id=request.user.id, new_paper_id=data["new_paper_id"]).first() + if not record: + batch = None + if request.user.batch: + batch = request.user.batch.name + session = PunishmentInfoModel.objects.filter(user_id=request.user.id, + new_exam_id=data["new_exam_id"]).count() # 场次 + data["exam_name"] = data["exam_name"] + "-第" + str(session + 1) + "次测试" + PunishmentInfoModel.objects.create(user_id=request.user.id, username=request.user.username, + exam_id=data["exam_id"], new_exam_id=data["new_exam_id"], + exam_name=data["exam_name"], paper_id=data["paper_id"], + new_paper_id=data["new_paper_id"], warning_count=data["warning_count"], + max_warning_count=data["max_warning_count"], batch=batch, + is_punishment=data["is_punishment"], name=request.user.name) + return + record.warning_count = data["warning_count"] + record.is_punishment = data["is_punishment"] + record.save() + return + + +# AES加密类 +class AESEnDECryptRelated: + def __init__(self): + self.model = AES.MODE_CBC + + # 如果text不足16位的倍数就用空格补足为16位 + # 不同于JS,pycryptodome库中加密方法不做任何padding,因此需要区分明文是否为中文的情况 + def add_to_16(self, text): + pad = 16 - len(text.encode('utf-8')) % 16 + text = text + pad * chr(pad) + return text.encode('utf-8') + + # 加密函数 + def encrypt(self, text, key, iv): + text = self.add_to_16(text) + cryptos = AES.new(key, self.model, iv) + cipher_text = cryptos.encrypt(text) + return b2a_hex(cipher_text).decode('utf-8') + + # 解密函数 + def decrypt(self, text): + # 对应加密对象需要转成字符串,然后加密,解密后还得需要用这个函数去掉最后的一些字符才能loads为最终的结果 + unpad = lambda s: s[0:-ord(s[-1])] + ciphertext, key_str, iv_str = self.get_decrypt_info(text) + text = a2b_hex(ciphertext) + iv = iv_str.encode('utf-8') + key = key_str.encode('utf-8') + cryptos = AES.new(key, self.model, iv) + plain_text = cryptos.decrypt(text) + # print(json.loads(unpad(plain_text))) + return bytes.decode(plain_text) + + def get_decrypt_info(self, text): + text = text[:len(text) - 2] + iv = text[:8] + text[-8:] + key = text[-16:-8] + text[8:16] + ciphertext = text[16:-16] + return ciphertext, key, iv + + # 重组新的密文 + def combination_new_ciphertext(self, ciphertext, iv_str, key_str): + # 前8个向量 + 后8个密钥 + 密文 + 前8个密钥 + 后8个向量 + == + new_ciphertext = iv_str[:8] + key_str[8:] + ciphertext + key_str[:8] + iv_str[8:] + "==" + return new_ciphertext + + def start_encrypt(self, text): + # 整个加密类的入口 + key_str = generate_random_str_16_system(16) # 密钥字符串 + key = key_str.encode('utf-8') # byte类型 + iv_str = generate_random_str_16_system(16) # 向量字符串 + iv = iv_str.encode('utf-8') # byte类型 + if not isinstance(text, str): + e1 = self.encrypt(json.dumps(text), key, iv) # 加密 + else: + e1 = self.encrypt(text, key, iv) # 加密 + new_ciphertext = self.combination_new_ciphertext(e1, iv_str, key_str) + return new_ciphertext + + +# 验证银行卡号是否有效 +def is_valid_debit_card(card_number): + if not card_number.isdigit(): + return False + + length = len(card_number) + if length < 16 or length > 19: + return False + + return True + + +def generate_random_str(randomlength=16): + """ + 生成一个指定长度的随机字符串 + """ + random_str = '' + base_str = 'abcdefghigklmnopqrstuvwxyz0123456789' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length)] + return random_str + + +def sha1_encrypt(data): + """ + 使用sha1加密算法,返回str加密后的字符串 + """ + sha = hashlib.sha1(data.encode('utf-8')) + encrypts = sha.hexdigest() + return encrypts + + +# 验证是否为汉字 +def is_all_chinese(s): + if re.match(r'^[\u4e00-\u9fa5]+$', s): + return True + else: + return False + + +# 验证用户名是否有效,仅为汉字、数字、英文字母,3类 +def is_valid_username(username): + pattern = re.compile(r'^[\u4e00-\u9fa5A-Za-z0-9]+$') + return bool(pattern.match(username)) + + +# 计算子账号的分销比例 +def calculate_sub_ratio(main_ratio, sub_ratio): + main_ratio = Decimal(main_ratio) + sub_ratio = Decimal(sub_ratio) + result = main_ratio * sub_ratio / Decimal(100) + return result.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP) + + +# def asyncDeleteFile(request, fileName): +# try: +# time.sleep(1) # 等待文件被读取完毕 +# if os.path.exists(fileName): # 如果文件存在 +# # 删除文件,可使用以下两种方法。 +# os.remove(fileName) +# except Exception as e: +# logging.getLogger('error').error( +# "user: %s, delete FileName: %s file failed: \n%s" % ( +# request.user.id, fileName, traceback.format_exc())) + + +def asyncDeleteFile(request, fileName): + try: + max_attempts = 10 # 设置最大尝试次数 + attempt_count = 0 + + while attempt_count < max_attempts: + try: + os.remove(fileName) + logging.getLogger('info').info('delete success') + parent_dir = os.path.dirname(fileName) + if parent_dir and parent_dir != '' and not os.listdir(parent_dir): + os.rmdir(parent_dir) + break # 文件删除成功,退出循环 + except PermissionError as e: + # 如果捕获到 PermissionError 异常,则等待一段时间再次尝试删除文件 + logging.getLogger('error').error(f"文件删除失败:{e}") + attempt_count += 1 + if attempt_count >= max_attempts: + logging.getLogger('error').error("已达到最大尝试次数,无法删除文件") + break + time.sleep(1) # 等待1秒后再次尝试删除文件 + except Exception as e: + logging.getLogger('error').error( + "user: %s, delete FileName: %s file failed: \n%s" % ( + request.user.id, fileName, traceback.format_exc())) + +class MyCustomError(Exception): + """ + 自定义异常 + """ + + def __init__(self, message): + self.message = message + super().__init__(self.message) + + +def digit_to_chinese(num): + try: + units = ['', '拾', '佰', '仟'] + digits = ['', '万', '亿', '兆'] + chinese_digits = ['', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'] + chinese_units = ['', '角', '分'] + + result = '' + + num_int = int(num) + num_decimal = round((num - num_int) * 100) # 小数部分四舍五入 + + num_str = str(num_int) + num_str_len = len(num_str) + digit_group_count = (num_str_len + 3) // 4 # 每4位一组 + + for i in range(digit_group_count): + group_str = num_str[max(0, num_str_len - (i + 1) * 4):num_str_len - i * 4] + group_int = int(group_str) + + if group_int == 0: + continue + + # 三位一组处理 + group_result = '' + for j in range(len(group_str)): + digit = int(group_str[j]) + if digit != 0: + group_result += chinese_digits[digit] + units[len(group_str) - j - 1] + + # 加入“万”、“亿”等单位 + group_result += digits[i] + + result = group_result + result + + # 添加“元”、“角”、“分” + if result != '': + result += '元' + if num_decimal > 0: + result += chinese_digits[num_decimal // 10] + chinese_units[1] + chinese_digits[num_decimal % 10] + \ + chinese_units[2] + else: + result += '整' + + return result + except Exception as e: + logging.getLogger('error').error( + "digit: %s changeTo chinese failed: \n%s" % (num, traceback.format_exc())) + return 0 + +def get_first_and_last_day(year_month): + """ + 获取某个月份的第一天与最后一天 + """ + year, month = map(int, year_month.split('-')[:2]) + first_day = datetime(year, month, 1) + if month == 12: + last_day = datetime(year + 1, 1, 1) - timedelta(days=1) + else: + last_day = datetime(year, month + 1, 1) - timedelta(days=1) + return first_day.date(), last_day.date() + + +def get_all_months(start_date, end_date): + """根据两个时间,求出中间的年月列表""" + months = [] + if type(start_date) is not date: + start_date = datetime.strptime(start_date, '%Y-%m-%d') + if type(end_date) is not date: + end_date = datetime.strptime(end_date, '%Y-%m-%d') + current_date = start_date.replace(day=1) # 将日期设置为该月的第一天 + while current_date <= end_date: + months.append(current_date.strftime('%Y-%m')) + current_date += relativedelta(months=1) # 增加一个月 + return months + + +def calculate_hours(AmStart, AmEnd, PmStart, PmEnd, Separate=False): # Separate:是否分割上下午 + """计算公司上班时长""" + time_format = '%H:%M' + am_start_time = datetime.strptime(AmStart, time_format) + am_end_time = datetime.strptime(AmEnd, time_format) + pm_start_time = datetime.strptime(PmStart, time_format) + pm_end_time = datetime.strptime(PmEnd, time_format) + + # am_hours = (am_end_time - am_start_time).seconds / 3600 + (am_end_time - am_start_time).minutes / 60 / 2 + am_hours, remainder = divmod((am_end_time - am_start_time).seconds, 1800) + if remainder > 0: + am_hours += 1 + + # pm_hours = (pm_end_time - pm_start_time).seconds / 3600 + (pm_end_time - pm_start_time).minutes / 60 / 2 + pm_hours, remainder = divmod((pm_end_time - pm_start_time).seconds, 1800) + if remainder > 0: + pm_hours += 1 + if Separate: # 分割上下午分别返回 + return am_hours / 2, pm_hours / 2 + return (am_hours + pm_hours) / 2 + + +def get_month_days(year_month): + """获取该月天数""" + year, month = map(int, year_month.split('-')) + days_in_month = calendar.monthrange(year, month)[1] + return days_in_month + + +class ErrorTable: + """ + 错误信息表格工具 + """ + + def __init__(self, column_names: List[str]): + if 0 == len(column_names): + raise ValueError('初始化列名必填') + self.column_names = column_names + ['ErrorMessage'] + self.data = [] + + def has_data(self) -> bool: + return bool(self.data) + + def add_one_row(self, row_data: List[Any], error_message: str = None): + if len(row_data) != len(self.column_names) - 1: + raise MyCustomError("添加错误信息行失败,表格列数与输入列数不符") + try: + if error_message is None: + error_message = "" + row_data.append(error_message) + row_dict = {} + for index, value in enumerate(row_data): + row_dict[self.column_names[index]] = value + + self.data.append(row_dict) + except KeyError: + logging.getLogger('error').error( + "ErrorTable add_row failed: \n%s" % (traceback.format_exc())) + + + def get_table(self) -> dict: + table_dict = {'name': [name for name in self.column_names], 'data': self.data} + return table_dict + +def common_update_sql(item, table_name): + # 生成更新sql的where之前的语句:item为字典,key是更新的字段名,value是字段值,table_name为表名 + update = ','.join([" {key} = %s".format(key=key) for key in item]) + update_sql = 'update {table} set '.format(table=table_name) + update + return update_sql + +def remove_invisible_characters(value): + # 使用正则表达式匹配所有不可见字符并将其替换为空字符串 + value = re.sub(r'\s+', '', value) + return value + +def transform_characters(value): + # 转换为英文半角字符 + # 转换为英文半角字符 + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') + return value + + +def count_1(n: int) -> int: + """ + 计算打卡次数 + @param n: 二进制打卡记录对应的整数 + @return: 该整数对应的打卡次数(即:二进制中1的数量) + """ + count = 0 + while n: + count += n & 1 + n >>= 1 + return count + + +def get_month_last(_date: date, length: int) -> list[str]: + """ + 获取参数时间前length个月,不包含本月 返回字符串数组 + @param _date: 日期 + @param length: 期望月数 + @return: 参数时间前length个月 + """ + current_month = _date.month + current_year = _date.year + months = [] + + for i in range(length): + if current_month == 1: + month = 12 + year = current_year - 1 + else: + month = current_month - 1 + year = current_year + + months.append(f"{year}-{month:02d}") + current_month = month + current_year = year + return months + + +def round_to_highest_digit(number): + """ + 保留最高位取整 + @param number: + @return: + """ + if number == 0: + return 0 + # 获取数字的位数 + digits = int(math.log10(abs(number))) + # 计算最高位的值 + highest_digit_value = int(number / (10 ** digits)) + # 将最高位值取整,然后还原成最终的值 + result = round(highest_digit_value) * (10 ** digits) + return result + + +def mul_split(text): + """ + 多字符同时划分 + """ + try: + split_string = ' ,.; ,。;' # 定义分隔符 + result = re.split(r'[' + re.escape(split_string) + ']', text) + result = [item.strip() for item in result if item] # 移除空字符串和空白项 + return result + except Exception: + error_logger.error("text: %s, mul_split failed: %s" % (text, traceback.format_exc())) + return + + +def time_str_to_int(time_str): + """分秒字符串转int""" + minutes, seconds = map(int, time_str.split(':')) + return minutes * 60 + seconds + + +def time_int_to_str(total_seconds): + """int转分秒字符串""" + if 0 == total_seconds: + return '00:00' + minutes, seconds = divmod(total_seconds, 60) + return f"{minutes:02d}:{seconds:02d}" \ No newline at end of file diff --git a/utils/funcs.py b/utils/funcs.py new file mode 100644 index 0000000..703e1d4 --- /dev/null +++ b/utils/funcs.py @@ -0,0 +1,83 @@ +# _*_ coding: utf-8 _*_ +# @Time : 2021/5/27 14:58 +# @File : funcs.py +# @Software: PyCharm + +""" +此文件存放一些公用的工具函数 +""" +import datetime +import random +from random import shuffle +from django_redis import get_redis_connection + +from utils.custom import MyCustomError + + +def generate_random_str(randomlength=16): + """ + 生成一个指定长度的随机字符串 + """ + random_str = '' + base_str = 'abcdefghigklmnopqrstuvwxyz0123456789' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length)] + return random_str + + +def generate_random_str_16_system(randomlength=16): + """ + 生成一个指定长度的随机字符串, 这个字符串看起来像16进制的 + """ + random_str = '' + base_str = '0123456789abcdef' + length = len(base_str) - 1 + for i in range(randomlength): + random_str += base_str[random.randint(0, length)] + return random_str + + +# 获取乱序字符串 +def shuffle_str(s): + # 将字符串转换成列表 + str_list = list(s) + # 调用random模块的shuffle函数打乱列表 + shuffle(str_list) + # 将列表转字符串 + return ''.join(str_list) + + +# 获取原来字符串在新字符串中的位置 +def str_map(origin, new_str): + result_str = [] + new_str_list = list(new_str) + for each in origin: + index = new_str_list.index(each) + result_str.append(index) + # 生成轨迹 + track = random.sample(range(0, len(result_str)), len(result_str)) + return result_str, track + + +# 定义尝试的日期格式列表 +sys_formats = ["%Y-%m-%d", "%Y/%m/%d", "%Y年%m月%d日", "%Y%m%d"] + + +def parse_date(date_string, formats=sys_formats): + """ + 尝试按照提供的格式解析日期字符串, 返回日期date + @param date_string: 日期字符串 + @param formats: 尝试格式 + @return: + """ + if not isinstance(formats, list): + if not isinstance(formats, str) or formats not in sys_formats: + raise MyCustomError("日期格式不支持") + formats = [formats, ] + for fmt in formats: + try: + return datetime.datetime.strptime(date_string, fmt).date() + except ValueError: + pass + raise MyCustomError("无法解析日期字符串") diff --git a/utils/http_sms_mt.py b/utils/http_sms_mt.py new file mode 100644 index 0000000..7d41430 --- /dev/null +++ b/utils/http_sms_mt.py @@ -0,0 +1,56 @@ +import requests, json +import time +import hashlib +from ChaCeRndTrans import settings + + +# 发送短信接口 +def send_msg(phone, content): + url = settings.URL + account = settings.ACCOUNT + pwd = settings.PWD + mttime = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time())) + # import md5 #Python2里的引用 + # s.encode()#变成bytes类型才能加密 + m = hashlib.md5((pwd + mttime).encode()) + md = m.hexdigest() + postData = "name=%s&pwd=%s&phone=%s&content=%s&mttime=%s&rpttype=1" % (account, md, phone, content, mttime) + + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': 'secure' + } + response = requests.request("POST", url, headers=headers, data=postData.encode(encoding='UTF-8', errors='strict')) + # result = json.loads(response.text) + return response + + +if __name__ == '__main__': + mttime = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time())) + print(mttime) + url = settings.URL + Account = settings.ACCOUNT + Pwd = settings.PWD + m = hashlib.md5((Pwd + mttime).encode()) + md = m.hexdigest() + phone = '13580505142' + content = '【查策网】验证码:123456' + # params = u'{"name":"szccwl","pwd":"md","address":"bz","phone":"13000000000"}' + postData = "name=szccwl&pwd=%s&phone=%s&content=%s&mttime=%s&rpttype=1" % (md, phone, content, mttime) + # payload = 'name=szccwl&pwd=7e1ae2114ac4238d02e919a72b41026b&phone=18819470249&content=123456&mttime=20210106174609&rpttype=1' + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': 'secure' + } + + response = requests.request("POST", url, headers=headers, data=postData.encode(encoding='UTF-8', errors='strict')) + + print(response.text) + + # r = requests.post(url, data=postData) + # key = json.loads(r.text) + # print(key) + # 这一步将返回值转成json + # key = json.loads(r.text) + # __business_id = uuid.uuid1() + # print(__business_id) diff --git a/utils/middleware.py b/utils/middleware.py new file mode 100644 index 0000000..29b8db7 --- /dev/null +++ b/utils/middleware.py @@ -0,0 +1,128 @@ +import base64 +import http +import json +import logging +import threading +import uuid +import traceback + +from django.utils.deprecation import MiddlewareMixin +from rest_framework import response as rest_response +from utils.custom import get_ip + +local = threading.local() # 获取当前线程对象 + +logger = logging.getLogger("info") +error_logger = logging.getLogger("error") + + +class RequestLogFilter(logging.Filter): + """ + 日志过滤器,将当前请求线程的request信息保存到日志的record上下文 + record带有formater需要的信息。 + """ + + def filter(self, record): + record.method = getattr(local, 'method', "none") + record.path = getattr(local, 'path', "none") + record.request_id = getattr(local, 'request_id', "none") + record.params = getattr(local, 'params', "none") + record.body = getattr(local, 'body', "none") + record.user = getattr(local, 'user', "none") + return True + + +class RequestLogMiddleware(MiddlewareMixin): + """ + 将request的信息记录在当前的请求线程上。 + """ + def process_request(self, request): + # 以下逻辑是 实现请求进来打印 相关请求参数 + local.request_id = str(uuid.uuid1()) # 线程对象里面加入uuid + body = {} + try: + body = request.body.decode() + if body: + body = json.loads(body) + except: + pass + token = request.META.get("HTTP_AUTHORIZATION") + local.user = "guest" + if token: + try: + user_base64 = token.split(".")[1] + missing_padding = 4 - len(user_base64) % 4 + if missing_padding: + user_base64 += '=' * missing_padding + + local.user = str(base64.b64decode(bytes(user_base64, 'utf-8')), encoding='utf-8') + except: + local.user = token + pass + + local.method = request.method + local.params = request.GET.dict() + local.path = request.path_info + local.body = json.dumps(body) + local.ip = get_ip(request) + + request_info = { + "ip": get_ip(request), + "request_id": local.request_id, + 'method': request.method, + 'path': request.path_info, + 'params': request.GET.dict(), + 'body': body, + 'user': local.user, + } + + logger.info(f'requests: {json.dumps(request_info, ensure_ascii=False)}') + + def process_response(self, request, response): + """ + 当请求是媒体请求时,需要设置对应的响应头以方便浏览器可以缓存媒体的当前播放时间,从而解决拉进度条会回弹到原点的bug + """ + if request.path.endswith(".mp3"): + response["Accept-Ranges"] = "bytes" + token = request.META.get("HTTP_AUTHORIZATION") + local.user = "guest" + if token: + try: + user_base64 = token.split(".")[1] + missing_padding = 4 - len(user_base64) % 4 + if missing_padding: + user_base64 += '=' * missing_padding + + local.user = str(base64.b64decode(bytes(user_base64, 'utf-8')), encoding='utf-8') + except Exception as e: + local.user = token + error_logger.error( + "process_response failed: \n%s" % (traceback.format_exc())) + pass + data = response.status_code + # if response.data: + # data = response.data + local.method = request.method + local.params = request.GET.dict() + local.path = request.path_info + local.ip = get_ip(request) + local.response = data + + request_info = { + "ip": get_ip(request), + "request_id": local.request_id, + 'method': request.method, + 'path': request.path_info, + 'params': request.GET.dict(), + 'user': local.user, + "response": data, + } + + logger.info('response: %r' % request_info) + return response + + +class ExceptionLoggingMiddleware(MiddlewareMixin): + def process_exception(self, request, exception): + import traceback + error_logger.error(traceback.format_exc()) diff --git a/utils/sms_client.py b/utils/sms_client.py new file mode 100644 index 0000000..ceb539d --- /dev/null +++ b/utils/sms_client.py @@ -0,0 +1,147 @@ +import logging +import traceback + +from aliyunsdkcore.acs_exception.exceptions import ServerException, ClientException +from aliyunsdkcore.client import AcsClient +from aliyunsdkcore.request import CommonRequest + +from ChaCeRndTrans import settings + +error_logger = logging.getLogger("error") + + +def get_client(): + access_key_id = settings.ACCESS_KEY_ID + access_secret = settings.ACCESS_SECRET + client = AcsClient(access_key_id, access_secret, 'cn-hangzhou') + return client + + +# 获取reques对象 +# method_type 请求方式 get/post +# protocol_type https/http请求 +# action_name 方法名 +def get_request(method_type, protocol_type, action_name): + request = CommonRequest() + request.set_accept_format('json') + request.set_domain('dysmsapi.aliyuncs.com') + request.set_method(method_type) # get | POST + request.set_protocol_type(protocol_type) # https | http + request.set_version('2017-05-25') + request.set_action_name(action_name) # 方法名 + return request + + +# 发送短信接口 +def send_msg(phone_number, sign_name, template_code, template_param): + request = get_request('post', 'https', 'SendSms') + client = get_client() + # 调用发送短信接口 + request.add_query_param('RegionId', "cn-hangzhou") + request.add_query_param('PhoneNumbers', phone_number) + request.add_query_param('SignName', sign_name) # 签名 + request.add_query_param('TemplateCode', template_code) + request.add_query_param('TemplateParam', template_param) + try: + response = client.do_action_with_exception(request) + except ServerException as e: + # ServerException的处理逻辑 + error_logger.error(traceback.format_exc()) + except ClientException as e: + # ClientException的处理逻辑 + error_logger.error(traceback.format_exc()) + print(str(response, encoding='utf-8')) + return response + + +# source_url:原始链接地址,不超过1000个字符长度。 +# short_url_name:短链服务名称,不超过12个字符长度。 +# effective_days:短链服务使用有效期,以天为单位,不超过30天。 +def add_short_url(source_url, short_url_name, effective_days): + request = get_request('post', 'https', 'AddShortUrl') + client = get_client() + request.add_query_param('RegionId', "cn-hangzhou") + request.add_query_param('SourceUrl', source_url) + request.add_query_param('ShortUrlName', short_url_name) + request.add_query_param('EffectiveDays', effective_days) + try: + response = client.do_action_with_exception(request) + except ServerException as e: + error_logger.error(traceback.format_exc()) + except ClientException as e: + error_logger.error(traceback.format_exc()) + print(str(response, encoding='utf-8')) + return response + + +# 添加模板接口 +def add_template(template_type, template_name, template_content, remark): + request = get_request('post', 'https', 'AddSmsTemplate') + client = get_client() + request.add_query_param('RegionId', "cn-hangzhou") + request.add_query_param('TemplateType', template_type) + request.add_query_param('TemplateName', template_name) + request.add_query_param('TemplateContent', template_content) + request.add_query_param('Remark', remark) + try: + response = client.do_action_with_exception(request) + except ServerException as e: + error_logger.error(traceback.format_exc()) + except ClientException as e: + error_logger.error(traceback.format_exc()) + print(str(response, encoding='utf-8')) + return response + + +# 删除模板接口 +def delete_template(template_code): + request = get_request('post', 'https', 'DeleteSmsTemplate') + client = get_client() + + request.add_query_param('RegionId', "cn-hangzhou") + request.add_query_param('TemplateCode', template_code) + try: + response = client.do_action_with_exception(request) + except ServerException as e: + error_logger.error(traceback.format_exc()) + except ClientException as e: + error_logger.error(traceback.format_exc()) + print(str(response, encoding='utf-8')) + return response + + +# 修改模板接口 +def edit_template(template_type, template_name, template_content, remark, template_code): + request = get_request('post', 'https', 'ModifySmsTemplate') + client = get_client() + request.add_query_param('RegionId', "cn-hangzhou") + request.add_query_param('TemplateType', template_type) + request.add_query_param('TemplateName', template_name) + request.add_query_param('TemplateCode', template_code) + request.add_query_param('TemplateContent', template_content) + request.add_query_param('Remark', remark) + try: + response = client.do_action_with_exception(request) + except ServerException as e: + error_logger.error(traceback.format_exc()) + except ClientException as e: + error_logger.error(traceback.format_exc()) + print(str(response, encoding='utf-8')) + return response + + +# 查询模板接口 +def query_template(template_code): + request = get_request('post', 'https', 'QuerySmsTemplate') + client = get_client() + + request.add_query_param('RegionId', "cn-hangzhou") + request.add_query_param('TemplateCode', template_code) + try: + response = client.do_action_with_exception(request) + except ServerException as e: + error_logger.error(traceback.format_exc()) + except ClientException as e: + error_logger.error(traceback.format_exc()) + print(str(response, encoding='utf-8')) + return response diff --git a/utils/thread_pool.py b/utils/thread_pool.py new file mode 100644 index 0000000..a772cfe --- /dev/null +++ b/utils/thread_pool.py @@ -0,0 +1,123 @@ +import math +import sys +import time +from concurrent.futures import ThreadPoolExecutor +import threading + + +class ThreadPool: + def __init__(self, max_thread_num=5): + # 记录全部线程是否已经结束 + self.over = False + # 记录所有的子线程完成后的返回值 + self.results = [] + + # 子线程函数体 + self.func = None + # 需要传进子线程的参数,数组中每一个元素都是一个元组 + # 例如有一个函数定义add(a,b),返回a和b的和 + # 则数组表现为[(1,2),(3,10),...] + # 可以依据数组中的每一个元组建立一个线程 + self.args_list = None + # 需要完成的任务的数量,获取自参数数组的长度 + self.task_num = 0 + # 线程池同时容纳的最大线程数,默认为5 + self.max_thread_num = max_thread_num + # 初始化线程池 + self.pool = ThreadPoolExecutor(max_workers=max_thread_num) + self.cond = threading.Condition() + + # 设置线程池中执行任务的各项参数 + def set_tasks(self, func, args_list): + # 需要完成的任务的数量,获取自参数数组的长度 + self.task_num = len(args_list) + # 参数数组 + self.args_list = args_list + # 线程中执行的函数体 + self.func = func + + # 显示进度条,用以查看所有任务的完成进度 + @staticmethod + def show_process(desc_text, curr, total): + proc = math.ceil(curr / total * 100) + show_line = '\r' + desc_text + ':' + '>' * proc \ + + ' ' * (100 - proc) + '[%s%%]' % proc \ + + '[%s/%s]' % (curr, total) + sys.stdout.write(show_line) + sys.stdout.flush() + time.sleep(0.1) + + # 线程完成后的回调,功能有3 + # 1:监控所有任务的完成进度 + # 2:收集任务完成后的结果 + # 3.继续向线程池中添加新的任务 + def get_result(self, future): + # 监控线程完成进度 + self.show_process('任务完成进度', self.task_num - len(self.args_list), self.task_num) + # 将函数处理的返回值添加到结果集合当中,若没有返回值,则future.result()的值是None + self.results.append(future.result()) + # 若参数数组中含有元素,则说明还有后续的任务 + if len(self.args_list): + # 提取出将要执行的一个任务的参数 + args = self.args_list.pop() + # 向线程池中提交一个新任务,第一个参数是函数体,第二个参数是执行函数时所需要的各项参数 + task = self.pool.submit(self.func, *args) + # 绑定任务完成后的回调 + task.add_done_callback(self.get_result) + else: + # 若结果的数量与任务的数量相等,则说明所有的任务已经完成 + if self.task_num == len(self.results): + print('\n', '任务完成') + # 获取锁 + self.cond.acquire() + # 通知 + self.cond.notify() + # 释放锁 + self.cond.release() + return + + def _start_tasks(self): + # 向线程池中添加到最大数量的线程 + for i in range(self.max_thread_num): + # 作出所有任务是否已经完成的判断,原因如下: + # 如果直接向线程池提交巨大数量的任务,线程池会创建任务队列,占用大量内存 + # 为减少创建任务队列的巨大开销,本类中所有子线程在完成后的回调中,会向线程池中提交新的任务 + # 循环往复,直到所有任务全部完成,而任务队列几乎不存在 + # 1:当提交的任务数量小于线程池容纳的最大线程数,在本循环中,必会出现所有任务已经提交的情况 + # 2:当函数执行速度非常快的时候,也会出现所有任务已经提交的情况 + + # 如果参数数组中还有元素,则说明没有到达线程池的上限 + if len(self.args_list): + # 取出一组参数,同时删除该任务 + args = self.args_list.pop() + # 向线程池中提交新的任务 + task = self.pool.submit(self.func, *args) + # 绑定任务完成后的回调 + task.add_done_callback(self.get_result) + # 所有任务已经全部提交,跳出循环 + else: + break + + # 获取最终所有线程完成后的处理结果 + def final_results(self): + # 开始执行所有任务 + self._start_tasks() + # 获取结果时,会有两种情况 + # 所有的任务都已经完成了,直接返回结果就行 + if self.task_num == len(self.results): + return self.results + # 线程池中还有未完成的线程,只有当线程池中的任务全部结束才能够获取到最终的结果 + # 这种情况会在线程池容量过大或者线程极度耗时时才会出现 + else: + # 获取锁 + self.cond.acquire() + # 阻塞当前线程,等待通知 + self.cond.wait() + # 已经获取到通知,释放锁 + self.cond.release() + # 返回结果集 + return self.results + + +# 创建线程池,最大线程数量为10 +tp = ThreadPool(10) diff --git a/utils/throttles.py b/utils/throttles.py new file mode 100644 index 0000000..45c6e3f --- /dev/null +++ b/utils/throttles.py @@ -0,0 +1,109 @@ +import logging +import traceback + +from django_redis import get_redis_connection +from rest_framework.throttling import ScopedRateThrottle + +import ChaCeRndTrans +from ChaCeRndTrans.basic import CCAIResponse + +err_logger = logging.getLogger('error') + +class CustomThrottle(ScopedRateThrottle): + cache_format = 'throttle_%(path)s_%(scope)s_%(ident)s' + # cache = get_redis_connection('db2') + cache = get_redis_connection('default') + + def parse_rate(self, rate): + """ + Given the request rate string, return a two tuple of: + , + """ + if rate is None: + return None + num, period = rate[:-1], rate[-1] + num = int(num) + duration = num * {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] + return duration + + def allow_request(self, request, view): + try: + # We can only determine the scope once we're called by the view. + self.scope = getattr(view, self.scope_attr, None) + + # If a view does not have a `throttle_scope` always allow the request + if not self.scope: + return True + + # Determine the allowed request rate as we normally would during + # the `__init__` call. + self.rate = self.get_rate() + self.duration = self.parse_rate(self.rate) + + if self.rate is None: + return True + + # 无缓存 + self.key = self.get_cache_key(request, view) + if self.key is None: + return True + + # self.history = self.cache.get(self.cache.make_key(self.key), None) + tmp_history = self.cache.get(self.key) + self.history = float(tmp_history) if tmp_history else None + self.now = self.timer() + # Drop any requests from the history which have now passed the + # throttle duration + if self.history is None: + return self.throttle_success() + if self.history >= self.now - self.duration: + return self.throttle_failure() + return self.throttle_success() + except Exception as e: + err_logger.error("keys: %s throttle failed: \n%s" % (self.key or None, traceback.format_exc(),)) + raise Exception("限流器错误") + + def throttle_success(self): + """ + Inserts the current request's timestamp along with the key + into the cache. + """ + + self.history = self.now + self.cache.set(self.key, self.history, self.duration) + return True + + # def throttle_failure(self): + # """ + # Called when a request to the API has failed due to throttling. + # """ + # return False + + def get_cache_key(self, request, view): + """ + If `view.throttle_scope` is not set, don't apply this throttle. + + Otherwise generate the unique cache key by concatenating the user id + with the '.throttle_scope` property of the view. + """ + if request.user.is_authenticated: + ident = request.user.pk + else: + ident = self.get_ident(request) + + # return self.cache.make_key(self.cache_format % { + # 'scope': self.scope, + # 'ident': ident + # }) + return self.cache_format % { + 'path': request.path, + 'scope': self.scope, + 'ident': ident + } + + def wait(self): + if self.history: + seconds = self.duration - (self.now - self.history) + else: + seconds = self.duration + return seconds \ No newline at end of file