commit a7bd6e0f6b4d0f91fe904569536b8161463db58d Author: 崔伟栋_28095 <1361575048@qq.com> Date: Wed Feb 19 16:15:13 2025 +0800 first commit 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 0000000..8b5aad3 Binary files /dev/null and b/media/人员工时生成表.xlsx differ diff --git a/media/人员工资导入模板.xlsx b/media/人员工资导入模板.xlsx new file mode 100644 index 0000000..7a82aef Binary files /dev/null and b/media/人员工资导入模板.xlsx differ diff --git a/media/场景一模版.zip b/media/场景一模版.zip new file mode 100644 index 0000000..f3c83ba Binary files /dev/null and b/media/场景一模版.zip differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55da39f Binary files /dev/null and b/requirements.txt differ diff --git a/requirementstest.txt b/requirementstest.txt new file mode 100644 index 0000000..7068c05 Binary files /dev/null and b/requirementstest.txt differ 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