├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── apps ├── indexs │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── filters.py │ ├── indexes.py │ ├── models.py │ ├── serializers.py │ ├── signals.py │ ├── tasks.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── public │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── filters.py │ ├── models.py │ ├── serializers.py │ ├── signals.py │ ├── tasks.py │ ├── tests.py │ ├── urls.py │ └── views.py └── user │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── enums.py │ ├── filters.py │ ├── models.py │ ├── serializers.py │ ├── signals.py │ ├── tasks.py │ ├── tests.py │ ├── urls.py │ ├── user.json │ └── views.py ├── celerybeat-schedule ├── configs ├── __init__.py ├── dev │ └── settings.py ├── generateCode.json ├── nginx.conf ├── prod │ └── settings.py └── swagger.py ├── docker-compose.yml ├── drfAPI ├── __init__.py ├── asgi.py ├── celery.py ├── settings.py ├── urls.py └── wsgi.py ├── extensions ├── BaseModel-delete.py ├── BaseModel.py ├── BaseSerializer.py ├── CustomRealtimeSignal.py ├── ExceptionHandle.py ├── JsonFormater.py ├── JwtAuth.py ├── JwtToken.py ├── MiddleWares.py ├── MyCache.py ├── MyCacheViewset.py ├── MyCustomPagResponse.py ├── MyCustomSwagger.py ├── MyFields.py ├── MyResponse.py ├── Pagination.py ├── Permission.py ├── RenderResponse.py ├── Throttle.py └── __init__.py ├── feature.txt ├── frameHint.txt ├── gunicorn_conf.py ├── i18n └── locale │ └── en │ └── LC_MESSAGES │ └── django.po ├── logs └── .gitkeep ├── manage.py ├── my_app_template ├── __init__.py-tpl ├── admin.py-tpl ├── apps.py-tpl ├── filters.py-tpl ├── models.py-tpl ├── serializers.py-tpl ├── signals.py-tpl ├── tasks.py-tpl ├── tests.py-tpl ├── urls.py-tpl └── views.py-tpl ├── requirements.txt ├── server.sh ├── sources.list ├── start-asgi.sh ├── startmyapp ├── __init__.py ├── apps.py ├── generate_template │ ├── serializer.txt │ ├── url.txt │ └── view.txt └── management │ ├── __init__.py │ └── commands │ ├── __init__.py │ ├── _private.py │ ├── generatecode.py │ └── startmyapp.py ├── static └── .gitkeep ├── templates ├── .gitkeep └── search │ └── indexes │ └── user │ └── user_text.txt ├── utils ├── Ecb.py ├── HtmlToImgPdfKit.py ├── MyDateTime.py ├── OtherRedisLock.py ├── ReadYaml.py ├── RedisCli.py ├── RedisLock.py ├── RedisLockNew.py ├── Singleton.py ├── TableUtil.py └── Utils.py └── uwsgi.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.mo 4 | *~ 5 | .coverage 6 | db.sqlite3 7 | *.log 8 | !log/placeholder 9 | media/** 10 | !media/**/init.txt 11 | **/migrations/** 12 | **/migrations 13 | !**/migrations/__init__.py 14 | 15 | # Environments 16 | .env 17 | .venv 18 | env/ 19 | venv/ 20 | ENV/ 21 | env.bak/ 22 | venv.bak/ 23 | .idea/ 24 | .vscode/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | COPY sources.list /etc/apt 4 | RUN apt update && apt upgrade -y &&\ 5 | apt install -y gcc g++ make vim net-tools python3-dev 6 | 7 | WORKDIR /proj 8 | COPY . . 9 | ENV LC_ALL=zh_CN.utf8 10 | ENV LANG=zh_CN.utf8 11 | ENV LANGUAGE=zh_CN.utf8 12 | ENV PIPURL "https://mirrors.aliyun.com/pypi/simple/" 13 | RUN pip install -i ${PIPURL} -r requirements.txt 14 | CMD ["gunicorn", "-c", "gunicorn_conf.py"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 aeasringnar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drfAPI 2 | 3 | 基于 Django 3.2 的 RESTfulAPI 风格的项目模板,用于快速构建高性能的服务端。 4 | 5 | ## 技术栈 6 | 7 | - **框架选择**:基于 Django 3.2 + django-rest-framework 3.12 8 | - **数据模型**:基于 MySQL 存储,使用 mysqlclient 作为驱动,测试也可使用内置 sqlite3 9 | - **授权验证**:基于 JWT 鉴权,来自 PyJWT,并做了单点登录的优化。 10 | - **内置功能**:自定义命令、代码生成、文件处理、用户系统、异常处理、异步处理、全文检索、动态权限、接口返回格式化、Swagger文档、日志格式化、分页、模糊查询、过滤、排序、动态缓存、分布式锁、国际化等 11 | 12 | ## 快速入门 13 | 14 | 如需进一步了解,参见 [Django 文档](https://docs.djangoproject.com/zh-hans/3.2/)。 15 | 16 | ### 本地开发 17 | 18 | ```bash 19 | pip install -r requirements.txt 20 | python manage.py makemigrations 21 | python manage.py migrate 22 | python manage.py loaddata apps/user/user.json 23 | python manage.py ruserver 0.0.0.0:8000 24 | open http://localhost:8000/swagger/ 25 | ``` 26 | 27 | ### 如何创建新的 App 28 | 1、使用自定义命令创建 App 29 | ```bash 30 | python manage.py startmyapp your_app_name 31 | ``` 32 | 2、将 App 加入到 Settings 中 33 | ```bash 34 | INSTALLED_APPS = [ 35 | ... 36 | 'apps.your_app_name', 37 | ... 38 | ] 39 | ``` 40 | 3、编写您的 models、serializers、views、urls 41 | 42 | 一般来说,您的正常开发流程如下: 43 | 1. 根据业务编写您的模型文件 models.py 44 | 2. 根据模型和您的需求编写您的序列化器 serializers.py 45 | 3. 根据模型、序列化器、权限来编写您的视图文件 views.py 46 | 4. 根据视图文件编写您的路由文件 urls.py 47 | 5. 最后将您的路由添加到入口路由文件中,默认它位于 ./drfAPI/urls.py 48 | 49 | 4、本项目提供了对于 CRUD 代码的自动生成命令 50 | 51 | 首先需要创建好需要的模型文件 52 | 53 | 然后将需要生成的信息更新到生成代码的json文件中,它需要遵循以下的逻辑,它的位置在./configs/generateCode.json 54 | 55 | 注意:json 文件中的 key 是不能更改的 56 | 57 | ```bash 58 | [ 59 | { 60 | "app_name": "public", # 标明你要生成代码的AppName 61 | "models": [ # 标明你要生成代码的模型 62 | { 63 | "model_name": "ConfDict", # 具体的模型类名 64 | "verbose": "系统字典", # 模型的中文标识 65 | "searchs": [ # 标明这个模型生成的代码需要的搜索字段,如果不需要可以删除 66 | "dict_key", 67 | "dict_value" 68 | ], 69 | "filters": [ # 标明这个模型生成的代码需要的过滤字段,如果不需要可以删除 70 | "dict_type" 71 | ] 72 | } 73 | ] 74 | } 75 | ] 76 | ``` 77 | 78 | 执行生成代码的命令 79 | 80 | ```bash 81 | python manage.py generatecode 82 | ``` 83 | 84 | 然后查看你的 App,再进行自定义的微调。 85 | 86 | ### 国际化支持 87 | 88 | 当前模版使用英文作为默认语言 89 | 在 settings 中已经做了中英文适配,要实现 i18n 的流程如下 90 | 91 | 1、修改 settings 配置文件 92 | ```python 93 | ... 94 | from django.utils.translation import gettext_lazy as _ 95 | ... 96 | 97 | MIDDLEWARE = [ 98 | 'django.middleware.security.SecurityMiddleware', 99 | 'django.contrib.sessions.middleware.SessionMiddleware', 100 | 'django.middleware.locale.LocaleMiddleware', # 国际化中间件 101 | ... 102 | ] 103 | ... 104 | LOCALE_PATHS = [ 105 | Path.joinpath(Path.joinpath(BASE_DIR, 'i18n'), 'locale') 106 | ] 107 | LANGUAGES = [ 108 | ('en', _('English')), 109 | ('zh-hans', _('Simplified Chinese')), 110 | ] 111 | LANGUAGE_CODE = 'en' # 默认使用英文 112 | ... 113 | ``` 114 | 115 | 2、标记需要翻译的文本 116 | 注意的是你在返回 json 时需要翻译的文本,需要先进行标记,例如: 117 | 118 | ```python 119 | from django.utils.translation import gettext_lazy 120 | 121 | 122 | class TestView(APIView): 123 | authentication_classes = (JwtAuthentication, ) 124 | permission_classes = (AllowAny, ) 125 | throttle_classes = (VisitThrottle, ) 126 | 127 | def get(self, request): 128 | # 使用 gettext_lazy 标记需要翻译的文本 129 | res = MyJsonResponse(res_data={'msg': gettext_lazy('test success')}) 130 | return res.data 131 | ``` 132 | 133 | 3、使用命令生成消息文件 134 | 135 | 在项目根目录执行命令 136 | 137 | 如果你在根目录使用了虚拟环境,需要使用 -i 指定你的虚拟环境目录,用去忽略该目录。如果不这么做可能会发生问题 138 | 139 | -l en 表示生成英文的翻译文件 140 | 141 | -l zh_hans 表示生成中文的翻译文件 142 | ```bash 143 | python manage.py makemessages -l zh_hans -i venv 144 | ``` 145 | 如果看到输出 processing locale zh_hans 表明消息文件生成好了 146 | 147 | 并且可以看到设置 LOCALE_PATHS 目录内会生成好指定的 django.po 文件,内容如下 148 | 149 | ```bash 150 | ... 151 | #: apps/public/views.py:101 152 | msgid "test success" 153 | msgstr "" 154 | 155 | #: drfAPI/settings.py:144 156 | msgid "English" 157 | msgstr "" 158 | 159 | #: drfAPI/settings.py:145 160 | msgid "Simplified Chinese" 161 | msgstr "" 162 | ... 163 | ``` 164 | 165 | 这个文件需要手动修改 msgstr 为对应的中文翻译,如下示 166 | 167 | ```bash 168 | ... 169 | #: apps/public/views.py:101 170 | msgid "test success" 171 | msgstr "测试成功" 172 | 173 | #: drfAPI/settings.py:144 174 | msgid "English" 175 | msgstr "英语" 176 | 177 | #: drfAPI/settings.py:145 178 | msgid "Simplified Chinese" 179 | msgstr "简体中文" 180 | ... 181 | ``` 182 | 183 | 4、生成二进制 .mo 文件 184 | 185 | 在项目根目录执行命令 186 | ```bash 187 | python manage.py compilemessages -i venv 188 | ``` 189 | 如果看到输出 processing file django.po in 你的项目目录/django-RESTfulAPI/i18n/locale/zh_hans/LC_MESSAGES 表明二进制文件生成好了 190 | ### 线上部署 191 | 192 | ```bash 193 | bash server.sh start # 获取帮助:bash sever.sh help 默认启动端口为 8001 194 | ``` -------------------------------------------------------------------------------- /apps/indexs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/apps/indexs/__init__.py -------------------------------------------------------------------------------- /apps/indexs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | # Register your models here. 5 | -------------------------------------------------------------------------------- /apps/indexs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class IndexsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.indexs' 7 | 8 | def ready(self): 9 | import apps.indexs.signals -------------------------------------------------------------------------------- /apps/indexs/filters.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from datetime import datetime 4 | from django.db.models import Q, F 5 | from django.db import transaction 6 | from django_filters.rest_framework import FilterSet, CharFilter 7 | 8 | 9 | # create your filters here 10 | -------------------------------------------------------------------------------- /apps/indexs/indexes.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | from haystack import indexes 3 | from apps.user.models import User 4 | 5 | 6 | class UserIndex(indexes.SearchIndex, indexes.Indexable): 7 | text = indexes.CharField(document=True, use_template=True) 8 | username = indexes.CharField(model_attr="username") 9 | id = indexes.CharField(model_attr="id") 10 | 11 | def get_model(self): 12 | return User 13 | 14 | def index_queryset(self, using=None): 15 | return self.get_model().objects.all().exclude(id=1) -------------------------------------------------------------------------------- /apps/indexs/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MaxValueValidator, MinValueValidator 3 | from extensions.BaseModel import BaseModel 4 | 5 | 6 | # Create your models here. 7 | -------------------------------------------------------------------------------- /apps/indexs/serializers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.db.models import F, Q, Count, Sum, Max, Min 13 | from django.contrib.auth.hashers import check_password, make_password 14 | from rest_framework import serializers 15 | from rest_framework.serializers import SerializerMethodField, ModelSerializer 16 | from rest_framework.validators import UniqueValidator, UniqueTogetherValidator 17 | from extensions.BaseSerializer import BaseModelSerializer 18 | from drf_haystack.serializers import HaystackSerializer 19 | from .indexes import UserIndex 20 | from .models import * 21 | # from .tasks import * 22 | 23 | 24 | class SearchCollectionAndSpotGoodSerializer(HaystackSerializer): 25 | model_type = SerializerMethodField() 26 | 27 | class Meta: 28 | # 指定要索引的index方法,在上面建立好的 29 | index_classes = [ UserIndex ] 30 | # 指定哪些字段可以被搜索 31 | fields = [ 32 | "username", "id" 33 | ] 34 | search_fields = ["text"] 35 | 36 | # def get_model_type(self, obj): 37 | # models = [ 38 | # (Collection, 'collection'), 39 | # (SpotGood, 'spotgood'), 40 | # (User, 'user'), 41 | # (Nft, 'nft'), 42 | # ] 43 | # for m in models: 44 | # if obj.model is m[0]: 45 | # return m[1] 46 | # return None 47 | -------------------------------------------------------------------------------- /apps/indexs/signals.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.dispatch import receiver 13 | from django.db.models.signals import pre_save, post_save, pre_delete, post_delete 14 | from .models import * 15 | 16 | 17 | # create your signals here 18 | -------------------------------------------------------------------------------- /apps/indexs/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.forms.models import model_to_dict 13 | from celery import shared_task 14 | from drfAPI.celery import app 15 | 16 | 17 | # create your async task here 18 | 19 | # @app.task(bind=True) 20 | # def async_task(self, *args, **kwargs): 21 | # try: 22 | # pass 23 | # except Exception as e: 24 | # logging.error("async task has error {}".fromat(e)) 25 | # logging.exception(e) 26 | # # 执行失败重试,本例设置3分钟后重试 27 | # self.retry(countdown=60 * 3, exc=e) -------------------------------------------------------------------------------- /apps/indexs/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | # Create your tests here. 5 | -------------------------------------------------------------------------------- /apps/indexs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | 4 | 5 | router = DefaultRouter() -------------------------------------------------------------------------------- /apps/indexs/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from rest_framework.views import APIView 10 | from rest_framework import status, mixins 11 | from rest_framework.response import Response 12 | from rest_framework.permissions import AllowAny 13 | from rest_framework.generics import GenericAPIView 14 | from rest_framework.filters import SearchFilter, OrderingFilter 15 | from rest_framework.viewsets import ModelViewSet, GenericViewSet, ViewSetMixin 16 | from django.conf import settings 17 | from django.db import transaction 18 | from django.core.cache import caches 19 | from django.forms.models import model_to_dict 20 | from django.http.response import HttpResponseNotFound 21 | from django.db.models import F, Q, Count, Sum, Max, Min 22 | from django.contrib.auth.hashers import check_password, make_password 23 | from django_filters.rest_framework import DjangoFilterBackend 24 | from drf_yasg.utils import swagger_auto_schema 25 | from drf_haystack.viewsets import ListModelMixin 26 | from drf_haystack.generics import HaystackGenericAPIView 27 | from drf_haystack.filters import HaystackAutocompleteFilter 28 | from extensions.JwtToken import JwtToken 29 | from extensions.Pagination import Pagination 30 | from extensions.Throttle import VisitThrottle 31 | from extensions.MyResponse import MyJsonResponse 32 | from extensions.JwtAuth import JwtAuthentication 33 | from extensions.Permission import IsAuthPermission 34 | from .models import * 35 | from .serializers import * 36 | from apps.user.models import User 37 | # from .tasks import * 38 | # from .filters import * 39 | # 不建议导入所有,建议按需导入 40 | 41 | 42 | class IndexSearchViewSet(ListModelMixin, ViewSetMixin, HaystackGenericAPIView): 43 | 44 | # 指定模型 45 | index_models = (User, ) 46 | # 指定序列化器 47 | serializer_class = SearchCollectionAndSpotGoodSerializer 48 | # 配置检索分页 49 | pagination_class = Pagination 50 | filter_backends = [HaystackAutocompleteFilter] -------------------------------------------------------------------------------- /apps/public/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/apps/public/__init__.py -------------------------------------------------------------------------------- /apps/public/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | # Register your models here. 5 | -------------------------------------------------------------------------------- /apps/public/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PublicConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.public' 7 | 8 | def ready(self): 9 | import apps.public.signals -------------------------------------------------------------------------------- /apps/public/filters.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from datetime import datetime 4 | from django.db.models import Q, F 5 | from django.db import transaction 6 | from django_filters.rest_framework import FilterSet, CharFilter 7 | 8 | 9 | # create your filters here 10 | -------------------------------------------------------------------------------- /apps/public/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MaxValueValidator, MinValueValidator 3 | from extensions.BaseModel import BaseModel 4 | 5 | 6 | class ConfDict(BaseModel): 7 | dict_type_choices = ( 8 | ('ip_white', 'IP白名单'), 9 | ('ip_black', 'IP黑名单'), 10 | ) 11 | dict_key = models.CharField(max_length=255, verbose_name='字典键') 12 | dict_value = models.CharField(max_length=255, verbose_name='字典值') 13 | dict_type = models.CharField(max_length=255, default='', choices=dict_type_choices, verbose_name='字典类型') 14 | 15 | class Meta: 16 | db_table = 'api_conf_dict' 17 | verbose_name = '系统字典表' 18 | verbose_name_plural = verbose_name 19 | indexes = [ 20 | models.Index(fields=['dict_key']), 21 | ] 22 | unique_together = [ 23 | ['dict_key', 'deleted'], 24 | ] -------------------------------------------------------------------------------- /apps/public/serializers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.db.models import F, Q, Count, Sum, Max, Min 13 | from django.contrib.auth.hashers import check_password, make_password 14 | from rest_framework import serializers 15 | from rest_framework.serializers import SerializerMethodField, ModelSerializer 16 | from rest_framework.validators import UniqueValidator, UniqueTogetherValidator 17 | from extensions.BaseSerializer import BaseModelSerializer, NormalResponseSerializer 18 | from .models import ConfDict 19 | # from .tasks import * 20 | from drf_yasg import openapi 21 | 22 | 23 | class GetAllEnumDataResponse(NormalResponseSerializer): 24 | pass 25 | 26 | 27 | UploadParameter = [ 28 | openapi.Parameter(name='file', in_=openapi.IN_FORM, description="上传文件", type=openapi.TYPE_FILE, required=True) 29 | ] -------------------------------------------------------------------------------- /apps/public/signals.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.dispatch import receiver 13 | from django.db.models.signals import pre_save, post_save, pre_delete, post_delete 14 | from .models import * 15 | 16 | 17 | # create your signals here 18 | -------------------------------------------------------------------------------- /apps/public/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.forms.models import model_to_dict 13 | from celery import shared_task 14 | from drfAPI.celery import app 15 | 16 | 17 | # create your async task here 18 | 19 | # @app.task(bind=True) 20 | # def async_task(self, *args, **kwargs): 21 | # try: 22 | # pass 23 | # except Exception as e: 24 | # logging.error("async task has error {}".fromat(e)) 25 | # logging.exception(e) 26 | # # 执行失败重试,本例设置3分钟后重试 27 | # self.retry(countdown=60 * 3, exc=e) -------------------------------------------------------------------------------- /apps/public/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | # Create your tests here. 5 | -------------------------------------------------------------------------------- /apps/public/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from django.conf import settings 3 | from rest_framework.routers import DefaultRouter 4 | from .views import UploadLocalFileView, TestView, GetAllEnumDataView 5 | 6 | 7 | router = DefaultRouter() 8 | urlpatterns = [ 9 | path('', include(router.urls)), 10 | path('uploadFile/', UploadLocalFileView.as_view(), name='上传文件到本地接口'), 11 | path('test/', TestView.as_view(), name='测试接口'), 12 | ] 13 | 14 | if settings.CURRENT_ENV == 'dev': 15 | urlpatterns.extend([ 16 | path('getAllEnumData/', GetAllEnumDataView.as_view(), name='获取内部所有枚举类型数据'), 17 | ]) -------------------------------------------------------------------------------- /apps/public/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from uuid import uuid4 8 | from decimal import Decimal 9 | from datetime import datetime, timedelta 10 | from rest_framework.views import APIView 11 | from rest_framework import status, mixins 12 | from rest_framework.response import Response 13 | from rest_framework.permissions import AllowAny 14 | from rest_framework.generics import GenericAPIView 15 | from rest_framework.parsers import MultiPartParser 16 | from rest_framework.filters import SearchFilter, OrderingFilter 17 | from rest_framework.viewsets import ModelViewSet, GenericViewSet 18 | from django.conf import settings 19 | from django.db import transaction 20 | from django.core.cache import caches 21 | from django.forms.models import model_to_dict 22 | from django.utils.translation import gettext_lazy 23 | from django.http.response import HttpResponseNotFound 24 | from django.db.models import F, Q, Count, Sum, Max, Min 25 | from django.contrib.auth.hashers import check_password, make_password 26 | from django_filters.rest_framework import DjangoFilterBackend 27 | from drf_yasg.utils import swagger_auto_schema 28 | from extensions.JwtToken import JwtToken 29 | from extensions.Pagination import Pagination 30 | from extensions.Throttle import VisitThrottle 31 | from extensions.MyResponse import MyJsonResponse 32 | from extensions.JwtAuth import JwtAuthentication 33 | from extensions.Permission import IsAuthPermission 34 | from extensions.MyCache import RedisCacheForDecoratorV1 35 | from PIL import Image 36 | from extensions.BaseSerializer import NormalResponseSerializer 37 | from .models import * 38 | from .serializers import * 39 | from .tasks import * 40 | # from .filters import * 41 | # 不建议导入所有,建议按需导入 42 | 43 | 44 | class UploadLocalFileView(APIView): 45 | authentication_classes = (JwtAuthentication, ) 46 | # permission_classes = (IsAuthPermission, ) 47 | throttle_classes = (VisitThrottle, ) 48 | parser_classes = [MultiPartParser] # 适用于上传文件的form/data请求体,不设置的话,默认是application/json 49 | 50 | @swagger_auto_schema(responses={200: NormalResponseSerializer}, manual_parameters=UploadParameter) 51 | def post(self, request): 52 | ''' 53 | 上传接口-写入本地 54 | ''' 55 | res = MyJsonResponse() 56 | try: 57 | file_i = request.FILES.items() 58 | key_name, up_file = next(file_i) 59 | logging.debug("%s %s %s" % (key_name, up_file.name, up_file.size)) 60 | file_name = up_file.name 61 | file_size = up_file.size 62 | check_file = os.path.splitext(file_name)[1] 63 | if check_file[1:].lower() not in settings.IMAGE_FILE_CHECK: 64 | res.update(msg='{} Not the specified type, allow type({})! '.format(file_name, '/'.join(settings.IMAGE_FILE_CHECK)), code=2) 65 | return res.data 66 | if file_size > settings.IMAGE_FILE_SIZE: 67 | res.update(msg="{} file more than {} mb, Can't upload! ".format(file_name, settings.IMAGE_FILE_SIZE / 1024 / 1024), code=2) 68 | return res.data 69 | # 创建当月的目录 70 | base_dir = os.path.join(settings.UPLOAD_DIR, datetime.now().strftime('%Y-%m-%d')) 71 | if not os.path.exists(base_dir): 72 | os.makedirs(base_dir, exist_ok=True) 73 | os.chmod(base_dir, mode=0o755) 74 | # 得到目录+文件名 75 | file_name = os.path.join(datetime.now().strftime('%Y-%m-%d'), '%su' % request.user.id + str(uuid4()).replace('-', '') + check_file.lower()) 76 | # 实际保存的路径 77 | file_path = os.path.join(settings.UPLOAD_DIR, file_name) 78 | if check_file[1:].lower() in ('jpg', 'jpeg'): 79 | im = Image.open(up_file) 80 | im.save(file_path, quality=75) 81 | else: 82 | with open(file_path, 'wb') as u_file: 83 | for part in up_file.chunks(): 84 | u_file.write(part) 85 | host_file_url = settings.SERVER_NAME + '/files/' + file_name 86 | res.update(data={key_name: host_file_url}) 87 | os.chmod(file_path, mode=0o644) 88 | return res.data 89 | except StopIteration as e: 90 | logging.exception(e) 91 | res.update(msg="File not uploaded", code=2) 92 | return res.data 93 | except Exception as e: 94 | logging.error('happen error: %s' % e) 95 | logging.exception(e) 96 | res.update(msg="An unexpected view error occurred: {}".format(e), code=1) 97 | return res.data 98 | 99 | 100 | class TestView(APIView): 101 | authentication_classes = (JwtAuthentication, ) 102 | permission_classes = (AllowAny, ) 103 | throttle_classes = (VisitThrottle, ) 104 | 105 | def get(self, request): 106 | '''测试接口''' 107 | res = MyJsonResponse(res_data={'msg': gettext_lazy('测试成功')}) 108 | return res.data 109 | 110 | 111 | class GetAllEnumDataView(GenericAPIView): 112 | authentication_classes = (JwtAuthentication, ) 113 | permission_classes = (AllowAny, ) 114 | throttle_classes = (VisitThrottle, ) 115 | 116 | @swagger_auto_schema(responses={200: GetAllEnumDataResponse}) 117 | @RedisCacheForDecoratorV1('r') 118 | def get(self, request): 119 | '''获取所有枚举类型的数据''' 120 | res = MyJsonResponse(res_data={'msg': gettext_lazy('测试成功')}) 121 | import importlib 122 | import inspect 123 | apps = [item for item in settings.INSTALLED_APPS if item.startswith('apps')] 124 | enums = [] 125 | for app in apps: 126 | try: 127 | module = importlib.import_module(f"{app}.enums") 128 | sub_enums = [item for item in inspect.getmembers( 129 | module, inspect.isclass 130 | ) if item[0] not in {'IntegerChoices', 'TextChoices'}] 131 | enums.extend(sub_enums) 132 | except ModuleNotFoundError as e: 133 | continue 134 | res_data = {} 135 | for sub_enum in enums: 136 | res_data[sub_enum[0].lower()] = dict(zip(sub_enum[1].values, sub_enum[1].labels)) 137 | res.update(data=res_data) 138 | return res.data -------------------------------------------------------------------------------- /apps/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/apps/user/__init__.py -------------------------------------------------------------------------------- /apps/user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | # Register your models here. 5 | -------------------------------------------------------------------------------- /apps/user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.user' 7 | 8 | def ready(self): 9 | import apps.user.signals -------------------------------------------------------------------------------- /apps/user/enums.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy 2 | from django.db.models.enums import IntegerChoices, TextChoices 3 | 4 | 5 | class Gender(TextChoices): 6 | female = 'female', gettext_lazy('女') 7 | male = 'male', gettext_lazy('男') 8 | privacy = 'privacy', gettext_lazy('保密') -------------------------------------------------------------------------------- /apps/user/filters.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from datetime import datetime 4 | from django.db.models import Q, F 5 | from django.db import transaction 6 | from django_filters.rest_framework import FilterSet, CharFilter 7 | 8 | 9 | # create your filters here 10 | -------------------------------------------------------------------------------- /apps/user/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MaxValueValidator, MinValueValidator 3 | from extensions.BaseModel import BaseModel 4 | from django.utils.translation import gettext_lazy 5 | 6 | 7 | class Group(BaseModel): 8 | group_type_choices = ( 9 | ('SuperAdmin', '超级管理员'), 10 | ('Admin', '管理员'), 11 | ('NormalUser', '普通用户'), 12 | ) 13 | group_type = models.CharField(max_length=128, choices=group_type_choices, verbose_name='用户组类型') 14 | group_type_cn = models.CharField(max_length=128, verbose_name='用户组类型_cn') 15 | 16 | class Meta: 17 | db_table = 'a_group' 18 | verbose_name = '用户组表' 19 | verbose_name_plural = verbose_name 20 | 21 | 22 | class User(BaseModel): 23 | # 管理员时使用账户密码登录 24 | class Suit(models.TextChoices): 25 | female = 'female', gettext_lazy('女') 26 | male = 'male', gettext_lazy('男') 27 | privacy = 'privacy', gettext_lazy('保密') 28 | 29 | 30 | group = models.ForeignKey(Group, on_delete=models.PROTECT, null=True, verbose_name='用户组') 31 | username = models.CharField(max_length=32, default='', blank=True, verbose_name='用户账号') 32 | password = models.CharField(max_length=255, default='',blank=True, verbose_name='用户密码') 33 | mobile = models.CharField(max_length=16, default='', blank=True, verbose_name='手机号') 34 | email = models.EmailField(default='', blank=True, verbose_name='邮箱') 35 | nick_name = models.CharField(max_length=32, default='', blank=True, verbose_name='昵称') 36 | region = models.CharField(max_length=255, default='', blank=True, verbose_name='地区') 37 | avatar_url = models.CharField(max_length=255, default='', blank=True, verbose_name='头像') 38 | gender = models.CharField(max_length=64, choices=Suit.choices, default='privacy', verbose_name='性别') 39 | birth_date = models.CharField(max_length=10, default='', blank=True, verbose_name='生日') 40 | is_freeze = models.BooleanField(default=False, verbose_name='是否冻结/是否封号') 41 | bf_logo_time = models.BigIntegerField(default=0, verbose_name='上次登录时间戳') 42 | jwt_version = models.IntegerField(default=0, verbose_name='jwt token版本') 43 | # is_admin = models.BooleanField(default=False, verbose_name='是否管理员') 44 | # group = models.ForeignKey(Group, on_delete=models.PROTECT, verbose_name='用户组') 45 | # 组权分离后 当有权限时必定为管理员类型用户,否则为普通用户 46 | # auth = models.ForeignKey(Auth, on_delete=models.PROTECT, null=True, blank=True, verbose_name='权限组') # 当auth被删除时,当前user的auth会被保留,但是auth下的auth_permissions会被删除,不返回 47 | 48 | 49 | class Meta: 50 | db_table = 'a_user_table' 51 | verbose_name = '用户表' 52 | verbose_name_plural = verbose_name -------------------------------------------------------------------------------- /apps/user/serializers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from uuid import uuid1 8 | from decimal import Decimal 9 | from datetime import datetime, timedelta 10 | from django.conf import settings 11 | from django.db import transaction 12 | from django.core.cache import caches 13 | from django.db.models import F, Q, Count, Sum, Max, Min 14 | from django.contrib.auth.hashers import check_password, make_password 15 | from rest_framework import serializers 16 | from rest_framework.serializers import SerializerMethodField, ModelSerializer 17 | from rest_framework.validators import UniqueValidator, UniqueTogetherValidator 18 | from extensions.BaseSerializer import BaseModelSerializer 19 | from .models import * 20 | from .tasks import * 21 | 22 | 23 | class UserViewsetSerializer(BaseModelSerializer, ModelSerializer): 24 | 25 | class Meta: 26 | model = User 27 | # exclude = ('deleted', 'is_freeze') 28 | fields = "__all__" 29 | read_only_fields = ('id', 'deleted', 'is_freeze', 'sort_timestamp', 'create_timestamp', 'update_timestamp', 'bf_logo_time') 30 | 31 | 32 | class CreateUserViewsetSerializer(BaseModelSerializer, ModelSerializer): 33 | 34 | class Meta: 35 | model = User 36 | fields = ('username', 'password', 'mobile', 'email', 'nick_name', 'region', 'avatar_url', 37 | 'gender', 'birth_date') 38 | 39 | 40 | class UpdateUserViewsetSerializer(BaseModelSerializer, ModelSerializer): 41 | 42 | class Meta: 43 | model = User 44 | fields = ('nick_name', 'region', 'avatar_url', 'gender', 'birth_date') 45 | 46 | 47 | class ReturnUserViewsetSerializer(BaseModelSerializer, ModelSerializer): 48 | 49 | class Meta: 50 | model = User 51 | exclude = ('deleted', 'is_freeze', 'bf_logo_time', 'jwt_version') 52 | 53 | 54 | class OwnerUserViewsetSerializer(BaseModelSerializer, ModelSerializer): 55 | 56 | class Meta: 57 | model = User 58 | exclude = ('deleted', 'is_freeze', 'password', 'jwt_version') 59 | 60 | 61 | class AdminLoginSerializer(serializers.Serializer): 62 | account = serializers.CharField(required=True, max_length=64, label="账号") 63 | password = serializers.CharField(required=True, max_length=64, label="密码") -------------------------------------------------------------------------------- /apps/user/signals.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.dispatch import receiver 13 | from django.db.models.signals import pre_save, post_save, pre_delete, post_delete 14 | from .models import * 15 | 16 | 17 | # create your signals here 18 | -------------------------------------------------------------------------------- /apps/user/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.forms.models import model_to_dict 13 | from celery import shared_task 14 | from drfAPI.celery import app 15 | 16 | 17 | # create your async task here 18 | 19 | # @app.task(bind=True) 20 | # def async_task(self, *args, **kwargs): 21 | # try: 22 | # pass 23 | # except Exception as e: 24 | # logging.error("async task has error {}".fromat(e)) 25 | # logging.exception(e) 26 | # # 执行失败重试,本例设置3分钟后重试 27 | # self.retry(countdown=60 * 3, exc=e) -------------------------------------------------------------------------------- /apps/user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | # Create your tests here. 5 | -------------------------------------------------------------------------------- /apps/user/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | from .views import TestView, UserViewSet, OwnerUserInfoViewset, AdminLoginView 4 | 5 | 6 | router = DefaultRouter() 7 | router.register(r'user', UserViewSet, basename="用户管理") 8 | router.register(r'ownerUserInfo', OwnerUserInfoViewset, basename="获取自己的用户信息") 9 | urlpatterns = [ 10 | path('', include(router.urls)), 11 | path('adminLogin/', AdminLoginView.as_view(), name='后台登录接口'), 12 | path('test/', TestView.as_view(), name='测试接口'), 13 | ] 14 | -------------------------------------------------------------------------------- /apps/user/user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "user.Group", 4 | "pk": 1, 5 | "fields": { 6 | "group_type": "SuperAdmin", 7 | "group_type_cn": "超级管理员", 8 | "sort_timestamp": 1663684491004977, 9 | "create_timestamp": 1663684491004977, 10 | "update_timestamp": 1663684491004977 11 | } 12 | }, 13 | { 14 | "model": "user.Group", 15 | "pk": 2, 16 | "fields": { 17 | "group_type": "Admin", 18 | "group_type_cn": "管理员", 19 | "sort_timestamp": 1663684491004977, 20 | "create_timestamp": 1663684491004977, 21 | "update_timestamp": 1663684491004977 22 | } 23 | }, 24 | { 25 | "model": "user.Group", 26 | "pk": 3, 27 | "fields": { 28 | "group_type": "NormalUser", 29 | "group_type_cn": "普通用户", 30 | "sort_timestamp": 1663684491004977, 31 | "create_timestamp": 1663684491004977, 32 | "update_timestamp": 1663684491004977 33 | } 34 | }, 35 | { 36 | "model": "user.User", 37 | "pk": 1, 38 | "fields": { 39 | "username": "superadmin", 40 | "password": "123456", 41 | "group": 1, 42 | "sort_timestamp": 1663684491004977, 43 | "create_timestamp": 1663684491004977, 44 | "update_timestamp": 1663684491004977 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /apps/user/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from rest_framework.views import APIView 10 | from rest_framework import status, mixins 11 | from rest_framework.response import Response 12 | from rest_framework.permissions import AllowAny 13 | from rest_framework.generics import GenericAPIView 14 | from rest_framework.filters import SearchFilter, OrderingFilter 15 | from rest_framework.viewsets import ModelViewSet, GenericViewSet 16 | from django.conf import settings 17 | from django.db import transaction 18 | from django.core.cache import caches 19 | from django.forms.models import model_to_dict 20 | from django.http.response import HttpResponseNotFound 21 | from django.db.models import F, Q, Count, Sum, Max, Min 22 | from django.contrib.auth.hashers import check_password, make_password 23 | from django_filters.rest_framework import DjangoFilterBackend 24 | from drf_yasg.utils import swagger_auto_schema 25 | from extensions.JwtToken import JwtToken 26 | from extensions.Pagination import Pagination 27 | from extensions.Throttle import VisitThrottle 28 | from extensions.MyResponse import MyJsonResponse 29 | from extensions.JwtAuth import JwtAuthentication 30 | from extensions.Permission import IsAuthPermission 31 | from extensions.MyCacheViewset import MyModelViewSet 32 | from .models import * 33 | from .serializers import * 34 | from .tasks import * 35 | # from .filters import * 36 | # 不建议导入所有,建议按需导入 37 | 38 | 39 | class UserViewSet(MyModelViewSet): 40 | ''' 41 | partial_update: 更新指定ID的用户,局部更新 42 | create: 创建用户 43 | retrieve: 检索指定ID的用户 44 | update: 更新指定ID的用户 45 | destroy: 删除指定ID的用户 46 | list: 获取用户列表 47 | ''' 48 | queryset = User.objects.filter() 49 | serializer_class = UserViewsetSerializer 50 | authentication_classes = (JwtAuthentication, ) 51 | permission_classes = (AllowAny, ) 52 | throttle_classes = (VisitThrottle, ) 53 | pagination_class = Pagination 54 | filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) 55 | search_fields = ('name', 'desc') # 注意 要针对有索引的字段进行搜索 56 | # filterset_fields = ('status', ) 57 | ordering_fields = ('id', 'create_timestamp', 'update_timestamp', 'sort_timestamp') 58 | 59 | def get_serializer_class(self): 60 | if self.action == 'create': 61 | return CreateUserViewsetSerializer 62 | if self.action in {'update', 'partial_update'}: 63 | return UpdateUserViewsetSerializer 64 | return ReturnUserViewsetSerializer 65 | 66 | 67 | class OwnerUserInfoViewset(mixins.ListModelMixin, GenericViewSet): 68 | ''' 69 | list: 获取自己的用户信息 70 | ''' 71 | serializer_class = OwnerUserViewsetSerializer 72 | authentication_classes = (JwtAuthentication, ) 73 | permission_classes = (IsAuthPermission, ) 74 | throttle_classes = (VisitThrottle, ) 75 | 76 | def get_queryset(self): 77 | # 重写get_queryset,用来返回目标用户的数据,因为在token验证那里已经确定了用户是否存在 78 | if self.request.auth: 79 | return User.objects.filter(id=self.request.user.id) 80 | return None 81 | 82 | def list(self, request, *args, **kwargs): 83 | # 重写list方法,使接口返回的数据是一个对象而不是数组 84 | instance = User.objects.filter(id=self.request.user.id).first() 85 | serializer = self.get_serializer(instance) 86 | return Response(serializer.data) 87 | 88 | 89 | class AdminLoginView(GenericAPIView): 90 | """后台登录的视图类""" 91 | serializer_class = AdminLoginSerializer 92 | 93 | @transaction.atomic 94 | def post(self, request): 95 | '''后台登录接口''' 96 | res = MyJsonResponse() 97 | serializer = self.get_serializer(data=request.data) 98 | serializer.is_valid(raise_exception=True) # 新版验证写法,使异常通过自定义的异常处理器抛出,也不需要在视图函数里捕获异常了,有统一的异常处理器 99 | username = serializer.data.get("account") 100 | password = serializer.data.get("password") 101 | user = User.objects.filter(username=username).first() 102 | if not user: 103 | res.update(msg="User not found.", code=2) 104 | return res.data 105 | if user.password != password: 106 | res.update(msg="Wrong password.", code=2) 107 | return res.data 108 | user.jwt_version += 1 109 | payload = { 110 | 'id': user.id, 111 | 'jwt_version': user.jwt_version 112 | } 113 | logging.debug(payload) 114 | jwt_token = JwtToken().encode_user(payload) 115 | user.save() 116 | res.update(data={'token': jwt_token}) 117 | return res.data 118 | 119 | 120 | class TestView(APIView): 121 | # @swagger_auto_schema(operation_summary="测试接口", operation_description="新建的测试接口") 122 | def post(self, request): 123 | """测试接口""" 124 | logging.info('test' * 3) 125 | raise ValueError('test error') -------------------------------------------------------------------------------- /celerybeat-schedule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/celerybeat-schedule -------------------------------------------------------------------------------- /configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/configs/__init__.py -------------------------------------------------------------------------------- /configs/dev/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 5 | CONFIG_DIR = Path(__file__).resolve().parent.parent 6 | 7 | 8 | # SECURITY WARNING: don't run with debug turned on in production! 9 | DEBUG = True 10 | # 是否查看运行时的 SQL语句 11 | SHOWSQL = False 12 | # 设置服务host 13 | SERVER_NAME = '' 14 | 15 | 16 | # dev atabase 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', 20 | 'NAME': BASE_DIR / 'db.sqlite3', 21 | } 22 | } 23 | 24 | 25 | # 日志配置 26 | LOGGING = { 27 | 'version': 1, # 指明dictConnfig的版本 28 | 'disable_existing_loggers': False, # 表示是否禁用所有的已经存在的日志配置 29 | 'formatters': { # 格式器 30 | 'verbose': { # 详细 31 | 'format': '[%(levelname)s] %(asctime)s %(module)s.%(funcName)s [%(process)d:%(thread)d] - %(filename)s[line:%(lineno)d] %(message)s' 32 | }, 33 | 'standard': { # 标准 34 | 'format': '[%(asctime)s] %(filename)s[line:%(lineno)d] %(levelname)s %(message)s' 35 | }, 36 | "debug": { # 调试 37 | "format": "[%(asctime)s] [%(process)d:%(thread)d] %(filename)s[line:%(lineno)d] (%(name)s)[%(levelname)s] %(message)s", 38 | }, 39 | "json": { 40 | "()": "extensions.JsonFormater.JSONFormatter" 41 | } 42 | }, 43 | 'handlers': { 44 | 'console': { 45 | 'level': 'DEBUG', 46 | 'class': 'logging.StreamHandler', 47 | 'stream': 'ext://sys.stdout', 48 | 'formatter': 'standard' 49 | }, 50 | 'null': { 51 | 'level': 'DEBUG', 52 | 'class': 'logging.NullHandler', 53 | }, 54 | "debug_file_handler": { 55 | "class": "logging.handlers.RotatingFileHandler", 56 | "formatter": "debug", 57 | "level": "DEBUG", 58 | "encoding": "utf8", 59 | "filename": "./logs/debug.log", 60 | "mode": "w" 61 | }, 62 | "info_file_handler": { 63 | "class": "logging.handlers.RotatingFileHandler", 64 | "formatter": "standard", 65 | "level": "INFO", 66 | "encoding": "utf8", 67 | "filename": "./logs/info.log", 68 | "mode": "w" 69 | }, 70 | "error_file_handler": { 71 | "class": "logging.handlers.RotatingFileHandler", 72 | "formatter": "debug", 73 | "level": "ERROR", 74 | "encoding": "utf8", 75 | "filename": "./logs/error.log", 76 | "mode": "w" 77 | } 78 | }, 79 | 'loggers': { 80 | 'django': { 81 | 'handlers': ['console'], 82 | 'level': 'INFO', 83 | 'propagate': True, 84 | }, 85 | 'django.db.backends': { 86 | 'handlers': ['null'], 87 | 'level': 'INFO', 88 | 'propagate': False, 89 | }, 90 | # 用于关闭django默认的request日志 必要时可开启 91 | 'django.request': { 92 | 'handlers': ['null'], 93 | 'level': 'INFO', 94 | 'propagate': False, 95 | }, 96 | }, 97 | # 设置默认的root handle 用于将开发手动输出的日志输出到指定文件中 98 | 'root': { 99 | 'level': 'DEBUG', 100 | 'handlers': ['debug_file_handler', 'info_file_handler', 'error_file_handler'] 101 | } 102 | } -------------------------------------------------------------------------------- /configs/generateCode.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "app_name": "classes", 4 | "models": [ 5 | { 6 | "model_name": "Classes", 7 | "verbose": "班级", 8 | "searchs": [ 9 | "name" 10 | ], 11 | "filters": [ 12 | "name" 13 | ] 14 | }, 15 | { 16 | "model_name": "Student", 17 | "verbose": "学生", 18 | "searchs": [ 19 | "sex" 20 | ], 21 | "filters": [ 22 | "sex" 23 | ] 24 | } 25 | ] 26 | } 27 | ] -------------------------------------------------------------------------------- /configs/nginx.conf: -------------------------------------------------------------------------------- 1 | # api server 2 | server { 3 | client_max_body_size 32M; # 设置请求的大小 4 | server_name api.test.com; 5 | 6 | location / { 7 | include uwsgi_params; 8 | uwsgi_pass 127.0.0.1:8001; # 反向代理到 uwsgi 服务器 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header X-Forwarded-Protocol $scheme; 13 | } 14 | 15 | location /static/ { 16 | alias /var/www/amg-api/static/; # 处理静态文件 17 | } 18 | 19 | location /files/ { 20 | alias /var/www/amg-api/files/; # 处理文件 21 | } 22 | 23 | error_page 404 /404.html; 24 | location = /404.html { 25 | root /var/www/html; 26 | } 27 | 28 | error_page 500 502 503 504 /50x.html; 29 | location = /50x.html { 30 | root /var/www/html; 31 | } 32 | } 33 | # vue 34 | server { 35 | root /var/www/amg-admin; 36 | index index.html; 37 | server_name vue.test.com; 38 | # allow 115.236.184.202; # 现在只有这个ip可用访问 39 | # deny all; 40 | 41 | location /static/css/static/ { 42 | alias /var/www/amg-admin/static/; 43 | } 44 | 45 | error_page 404 /404.html; 46 | location = /404.html { 47 | root /var/www/html; 48 | } 49 | 50 | error_page 500 502 503 504 /50x.html; 51 | location = /50x.html { 52 | root /var/www/html; 53 | } 54 | } 55 | # website 56 | server { 57 | root /var/www/amg-website; 58 | index index.html; 59 | server_name www.test.com; 60 | 61 | location / { 62 | try_files $uri $uri/ =404; 63 | } 64 | 65 | error_page 404 /404.html; 66 | location = /404.html { 67 | root /var/www/html; 68 | } 69 | 70 | error_page 500 502 503 504 /50x.html; 71 | location = /50x.html { 72 | root /var/www/html; 73 | } 74 | } -------------------------------------------------------------------------------- /configs/prod/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | CONFIG_DIR = Path(__file__).resolve().parent.parent 5 | 6 | 7 | # SECURITY WARNING: don't run with debug turned on in production! 8 | DEBUG = False 9 | # 是否查看运行时的 SQL语句 10 | SHOWSQL = False 11 | # 设置服务host 12 | SERVER_NAME = '' 13 | 14 | 15 | # line database 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.mysql', 19 | 'NAME': 'drfAPI', 20 | 'USER': 'debian-sys-maint', 21 | 'PASSWORD': 'O34wGEAp4Afmay51', 22 | 'HOST': '127.0.0.1', 23 | 'PORT': '3306', 24 | 'OPTIONS': { 25 | "init_command": "SET foreign_key_checks = 0;", # 去除强制外键约束 26 | 'charset': 'utf8mb4', 27 | 'sql_mode': 'traditional' 28 | }, 29 | 'CONN_MAX_AGE': 100 # 持久连接 定义了一个MySQL链接最大寿命,是一个秒数,表明一个连接的存活时间。 30 | } 31 | } 32 | ''' 33 | sql_mode 34 | ANSI模式:宽松模式,对插入数据进行校验,如果不符合定义类型或长度,对数据类型调整或截断保存,报warning警告。 35 | TRADITIONAL 模式:严格模式,当向mysql数据库插入数据时,进行数据的严格校验,保证错误数据不能插入,报error错误。用于事物时,会进行事物的回滚。 36 | STRICT_TRANS_TABLES模式:严格模式,进行数据的严格校验,错误数据不能插入,报error错误。 37 | ''' 38 | 39 | 40 | # 日志配置 41 | LOGGING = { 42 | 'version': 1, # 指明dictConnfig的版本 43 | 'disable_existing_loggers': False, # 表示是否禁用所有的已经存在的日志配置 44 | 'formatters': { # 格式器 45 | 'verbose': { # 详细 46 | 'format': '[%(levelname)s] %(asctime)s %(module)s.%(funcName)s [%(process)d:%(thread)d] - %(filename)s[line:%(lineno)d] %(message)s' 47 | }, 48 | 'standard': { # 标准 49 | 'format': '[%(asctime)s] %(filename)s[line:%(lineno)d] %(levelname)s %(message)s' 50 | }, 51 | "debug": { # 调试 52 | "format": "[%(asctime)s] [%(process)d:%(thread)d] %(filename)s[line:%(lineno)d] (%(name)s)[%(levelname)s] %(message)s", 53 | } 54 | }, 55 | 'handlers': { 56 | 'console': { 57 | 'level': 'INFO', 58 | 'class': 'logging.StreamHandler', 59 | 'stream': 'ext://sys.stdout', 60 | 'formatter': 'standard' 61 | }, 62 | 'null': { 63 | 'level': 'DEBUG', 64 | 'class': 'logging.NullHandler', 65 | }, 66 | "debug_file_handler": { 67 | "class": "logging.handlers.RotatingFileHandler", 68 | "formatter": "debug", 69 | "level": "DEBUG", 70 | "encoding": "utf8", 71 | "filename": "./logs/debug.log", 72 | "mode": "w" 73 | }, 74 | "info_file_handler": { 75 | "class": "logging.handlers.RotatingFileHandler", 76 | "formatter": "standard", 77 | "level": "INFO", 78 | "encoding": "utf8", 79 | "filename": "./logs/info.log", 80 | "mode": "w" 81 | }, 82 | "error_file_handler": { 83 | "class": "logging.handlers.RotatingFileHandler", 84 | "formatter": "debug", 85 | "level": "ERROR", 86 | "encoding": "utf8", 87 | "filename": "./logs/error.log", 88 | "mode": "w" 89 | } 90 | }, 91 | 'loggers': { 92 | 'django': { 93 | 'handlers': ['console'], 94 | 'level': 'INFO', 95 | 'propagate': True, 96 | }, 97 | 'django.db.backends': { 98 | 'handlers': ['null'], 99 | 'level': 'INFO', 100 | 'propagate': False, 101 | }, 102 | # 用于关闭django默认的request日志 必要时可开启 103 | # 'django.request': { 104 | # 'handlers': ['null'], 105 | # 'level': 'INFO', 106 | # 'propagate': False, 107 | # }, 108 | }, 109 | # 设置默认的root handle 用于将开发手动输出的日志输出到指定文件中 110 | 'root': { 111 | 'level': 'DEBUG', 112 | 'handlers': ['debug_file_handler', 'info_file_handler', 'error_file_handler'] 113 | } 114 | } -------------------------------------------------------------------------------- /configs/swagger.py: -------------------------------------------------------------------------------- 1 | from drf_yasg.generators import OpenAPISchemaGenerator 2 | from django.urls import get_resolver, URLPattern, URLResolver 3 | 4 | 5 | def get_all_url(resolver=None, pre='/'): 6 | if resolver is None: 7 | resolver = get_resolver() 8 | for r in resolver.url_patterns: 9 | if isinstance(r, URLPattern): 10 | if '' in str(r.pattern) or r.name == 'api-root' or '\\.(?P[a-z0-9]+)/?' in str(r.pattern): 11 | continue 12 | yield pre + str(r.pattern).replace('^', '').replace('$', ''), r.name 13 | if isinstance(r, URLResolver): 14 | yield from get_all_url(r, pre + str(r.pattern)) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | django_app: 4 | build: . 5 | container_name: django-server 6 | restart: always 7 | ports: 8 | - "8080:8080" 9 | volumes: 10 | - ./logs:/proj/logs -------------------------------------------------------------------------------- /drfAPI/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ('celery_app',) -------------------------------------------------------------------------------- /drfAPI/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for drfAPI project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drfAPI.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /drfAPI/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | # Set the default Django settings module for the 'celery' program. 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drfAPI.settings') 6 | 7 | app = Celery('drfAPI') 8 | 9 | app.config_from_object('django.conf:settings', namespace='CELERY') 10 | 11 | # Load task modules from all registered Django apps. 12 | app.autodiscover_tasks() 13 | 14 | 15 | @app.task(bind=True) 16 | def debug_task(self): 17 | print(f'Request: {self.request!r}') -------------------------------------------------------------------------------- /drfAPI/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for drfAPI project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.15. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | from datetime import timedelta 15 | from django.utils.translation import gettext_lazy as _ 16 | 17 | 18 | # config environment default dev 19 | CURRENT_ENV = os.getenv('ENV', 'dev') 20 | 21 | 22 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 23 | BASE_DIR = Path(__file__).resolve().parent.parent 24 | MY_APP_TEMPLATE = Path.joinpath(BASE_DIR, 'my_app_template') 25 | MY_APPS_DIR = Path.joinpath(BASE_DIR, 'apps') 26 | UPLOAD_DIR = Path.joinpath(BASE_DIR, 'media', 'upload') 27 | 28 | 29 | # Quick-start development settings - unsuitable for production 30 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 31 | # SECURITY WARNING: keep the secret key used in production secret! 32 | SECRET_KEY = 'django-insecure-g7!-t_sp)c$lpy4%oq)kv95$b&1+51=6d+0elqui%*b)^^!o#s' 33 | AES_KEY = '16ed9ecc7d9011eab9c63c6aa7c68b67' 34 | INTERFACE_TIMEOUT = 60 35 | DISPATCH_KEYS = ['admin4b67e4c11eab49a3c6aa7c68b67', 'mobile347e4c11eab49a3c6aa7c68b67', 'mini235a7e4c11eab49a3c6aa7c68b67'] 36 | 37 | 38 | ALLOWED_HOSTS = [ 39 | '*' 40 | ] 41 | 42 | 43 | # cors config 44 | CORS_ORIGIN_ALLOW_ALL = True 45 | CORS_ALLOW_CREDENTIALS= True 46 | CORS_ALLOW_HEADERS = [ 47 | '*' 48 | ] 49 | CORS_ALLOW_METHODS = [ 50 | 'DELETE', 51 | 'GET', 52 | 'OPTIONS', 53 | 'PATCH', 54 | 'POST', 55 | 'PUT', 56 | 'VIEW', 57 | ] 58 | 59 | 60 | # Application definition 61 | INSTALLED_APPS = [ 62 | 'django.contrib.admin', 63 | 'django.contrib.auth', 64 | 'django.contrib.contenttypes', 65 | 'django.contrib.sessions', 66 | 'django.contrib.messages', 67 | 'django.contrib.staticfiles', 68 | 'startmyapp', 69 | 'corsheaders', 70 | 'rest_framework', 71 | 'drf_yasg', 72 | 'django_filters', 73 | 'django_celery_results', 74 | 'apps.user', 75 | 'apps.public', 76 | ] 77 | 78 | 79 | MIDDLEWARE = [ 80 | 'django.middleware.security.SecurityMiddleware', 81 | 'django.contrib.sessions.middleware.SessionMiddleware', 82 | 'django.middleware.locale.LocaleMiddleware', # 国际化中间件 83 | "corsheaders.middleware.CorsMiddleware", # 解决跨域中间件 84 | 'django.middleware.common.CommonMiddleware', 85 | 'django.middleware.csrf.CsrfViewMiddleware', 86 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 87 | 'django.contrib.messages.middleware.MessageMiddleware', 88 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 89 | 'extensions.MiddleWares.PUTtoPATCHMiddleware', 90 | 'extensions.MiddleWares.LogMiddleware', 91 | # 'extensions.MiddleWares.PermissionMiddleware', 92 | 'extensions.MiddleWares.FormatReturnJsonMiddleware', 93 | ] 94 | 95 | 96 | ROOT_URLCONF = 'drfAPI.urls' 97 | # RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. 98 | APPEND_SLASH=False 99 | 100 | 101 | TEMPLATES = [ 102 | { 103 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 104 | 'DIRS': [Path.joinpath(BASE_DIR, 'templates')], 105 | 'APP_DIRS': True, 106 | 'OPTIONS': { 107 | 'context_processors': [ 108 | 'django.template.context_processors.debug', 109 | 'django.template.context_processors.request', 110 | 'django.contrib.auth.context_processors.auth', 111 | 'django.contrib.messages.context_processors.messages', 112 | ], 113 | }, 114 | }, 115 | ] 116 | 117 | 118 | WSGI_APPLICATION = 'drfAPI.wsgi.application' 119 | 120 | 121 | # Password validation 122 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 123 | AUTH_PASSWORD_VALIDATORS = [ 124 | { 125 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 126 | }, 127 | { 128 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 129 | }, 130 | { 131 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 132 | }, 133 | { 134 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 135 | }, 136 | ] 137 | 138 | 139 | # Internationalization 140 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 141 | LOCALE_PATHS = [ 142 | Path.joinpath(Path.joinpath(BASE_DIR, 'i18n'), 'locale') 143 | ] 144 | LANGUAGES = [ 145 | ('en', _('英语')), 146 | ('zh-hans', _('简体中文')), 147 | ] 148 | LANGUAGE_CODE = 'zh-hans' # 默认使用中文 149 | TIME_ZONE = 'Asia/Shanghai' # 中文时区 150 | USE_I18N = True 151 | USE_L10N = True 152 | USE_TZ = False # 不使用UTC格式时间 153 | 154 | 155 | # Static files (CSS, JavaScript, Images) 156 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 157 | STATIC_URL = '/static/' 158 | STATICFILES_DIRS = [ 159 | # 指定文件目录,BASE_DIR指的是项目目录,static是指存放静态文件的目录。 160 | os.path.join(BASE_DIR , 'static'), 161 | ] 162 | # 迁移静态文件的目录,这个是线上是需要使用的 python manage.py collectstatic 163 | STATIC_ROOT = os.path.join(BASE_DIR , 'static/static') 164 | 165 | 166 | # 媒体文件位置 167 | MEDIA_URL = '/media/' 168 | MEDIA_ROOT = Path.joinpath(BASE_DIR, 'media') 169 | 170 | 171 | # Default primary key field type 172 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 173 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 174 | 175 | 176 | CACHES = { 177 | "default": { 178 | "BACKEND": "django_redis.cache.RedisCache", 179 | "LOCATION": "redis://127.0.0.1:6379/0", 180 | "OPTIONS": { 181 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 182 | # "SERIALIZER": "django_redis.serializers.msgpack.MSGPackSerializer", 183 | # "PASSWORD": "" 184 | } 185 | }, 186 | "redis_cli": { 187 | "BACKEND": "django_redis.cache.RedisCache", 188 | "LOCATION": "redis://127.0.0.1:6379/1", 189 | "OPTIONS": { 190 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 191 | } 192 | } 193 | } 194 | 195 | 196 | # django rest framework config 197 | REST_FRAMEWORK = { 198 | 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), 199 | # 'DEFAULT_PERMISSION_CLASSES': ( 200 | # 'rest_framework.permissions.IsAuthenticated', 201 | # ), 202 | # 'DEFAULT_AUTHENTICATION_CLASSES': ( 203 | # 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 204 | # ), 205 | 'DEFAULT_RENDERER_CLASSES': ( 206 | 'rest_framework.renderers.JSONRenderer', 207 | 'rest_framework.renderers.BrowsableAPIRenderer', 208 | 'drf_renderer_xlsx.renderers.XLSXRenderer', 209 | ), 210 | 'DEFAULT_PARSER_CLASSES': ( 211 | 'rest_framework.parsers.JSONParser', 212 | 'rest_framework.parsers.FormParser', 213 | 'rest_framework.parsers.MultiPartParser', 214 | ), 215 | # 格式化时间 216 | 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 217 | 'DATETIME_INPUT_FORMATS': ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M'), 218 | 'DATE_FORMAT': '%Y-%m-%d', 219 | 'DATE_INPUT_FORMATS': ('%Y-%m-%d',), 220 | 'TIME_FORMAT': '%H:%M:%S', 221 | 'TIME_INPUT_FORMATS': ('%H:%M:%S',), 222 | # DRF异常定制处理方法 223 | 'EXCEPTION_HANDLER': 'extensions.ExceptionHandle.base_exception_handler', 224 | # DRF返回response定制json 225 | 'DEFAULT_RENDERER_CLASSES': ( 226 | 'extensions.RenderResponse.BaseJsonRenderer', 227 | ), 228 | } 229 | 230 | 231 | SWAGGER_SETTINGS = { 232 | # 使用这个时需要使用django-rest的admin 也就是需要配置 url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 233 | # 'LOGIN_URL': 'rest_framework:login', 234 | # 'LOGOUT_URL': 'rest_framework:logout', 235 | # 自定义swagger的路由tag 236 | 'DEFAULT_GENERATOR_CLASS': 'extensions.MyCustomSwagger.BaseOpenAPISchemaGenerator', 237 | # 'DEFAULT_AUTO_SCHEMA_CLASS': 'configs.swagger.CustomSwaggerAutoSchema', 238 | 'DEFAULT_AUTO_SCHEMA_CLASS': 'extensions.MyCustomSwagger.MySwaggerAutoSchema', 239 | 'DEFAULT_PAGINATOR_INSPECTORS': [ 240 | 'extensions.MyCustomPagResponse.MyDjangoRestResponsePagination', 241 | ], 242 | 'USE_SESSION_AUTH': False, 243 | # 'SHOW_EXTENSIONS': False, 244 | 'DOC_EXPANSION': 'none', 245 | 'SECURITY_DEFINITIONS': { 246 | 'Bearer': { 247 | 'type': 'apiKey', 248 | 'name': 'Authorization', 249 | 'in': 'header' 250 | } 251 | } 252 | } 253 | 254 | 255 | JWT_SETTINGS = { 256 | 'ACCESS_TOKEN_LIFETIME': timedelta(days=7), # 指定token有效期 257 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # 指定刷新token有效期 258 | 'ROTATE_REFRESH_TOKENS': False, 259 | 'BLACKLIST_AFTER_ROTATION': False, 260 | 'UPDATE_LAST_LOGIN': False, 261 | 'ALGORITHMS': ['HS256'], # 指定加密的哈希函数 262 | 'SIGNING_KEY': SECRET_KEY, # jwt的密钥 263 | 'VERIFY_SIGNATURE': True, # 开启验证密钥 264 | 'VERIFY_EXP': True, # 开启验证token是否过期 265 | 'AUDIENCE': None, 266 | 'ISSUER': None, 267 | 'LEEWAY': 0, 268 | 'REQUIRE': ['exp'], 269 | 'AUTH_HEADER_TYPES': 'Bearer', 270 | 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', 271 | } 272 | 273 | 274 | # Celery配置 275 | # 设置任务接受的类型,默认是{'json'} 276 | CELERY_ACCEPT_CONTENT = ['application/json'] 277 | # 设置task任务序列列化为json 278 | CELERY_TASK_SERIALIZER = 'json' 279 | # 请任务接受后存储时的类型 280 | CELERY_RESULT_SERIALIZER = 'json' 281 | # 时间格式化为中国时间 282 | CELERY_TIMEZONE = 'Asia/Shanghai' 283 | # 是否使用UTC时间 284 | CELERY_ENABLE_UTC = False 285 | # 指定borker为redis 如果指定rabbitmq CELERY_BROKER_URL = 'amqp://guest:guest@localhost:5672//' 286 | CELERY_BROKER_URL = 'redis://127.0.0.1:6379/3' 287 | # 指定存储结果的地方,支持使用rpc、数据库、redis等等,具体可参考文档 # CELERY_RESULT_BACKEND = 'db+mysql://scott:tiger@localhost/foo' # mysql 作为后端数据库 288 | # CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/4' 289 | # 使用django数据库存储结果 来自 django_celery_results 290 | CELERY_RESULT_BACKEND = 'django-db' 291 | # 结果的缓存配置 来自 django_celery_results 292 | CELERY_CACHE_BACKEND = 'django-cache' 293 | # 设置任务过期时间 默认是一天,为None或0 表示永不过期 294 | CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 295 | # 设置异步任务结果永不过期,如果不设置的话,每天04点celery会自动清空过期的异步任务结果 296 | CELERY_RESULT_EXPIRES = 0 297 | # 设置worker并发数,默认是cpu核心数 298 | # CELERYD_CONCURRENCY = 12 299 | # 设置每个worker最大任务数 300 | CELERYD_MAX_TASKS_PER_CHILD = 100 301 | # 使用队列分流每个任务 302 | # CELERY_QUEUES = ( 303 | # Queue("add", Exchange("add"), routing_key="task_add"), 304 | # Queue("mul", Exchange("mul"), routing_key="task_mul"), 305 | # Queue("xsum", Exchange("xsum"), routing_key="task_xsum"), 306 | # ) 307 | # 配置队列分流路由,注意可能无效,需要在运行异步任务时来指定不同的队列 308 | # CELERY_ROUTES = { 309 | # 'public.tasks.add': {'queue': 'add', 'routing_key':'task_add'}, 310 | # 'public.tasks.mul': {'queue': 'add', 'routing_key':'task_add'}, 311 | # 'public.tasks.xsum': {'queue': 'add', 'routing_key':'task_add'}, 312 | # # 'public.tasks.mul': {'queue': 'mul', 'routing_key':'task_mul'}, 313 | # # 'public.tasks.xsum': {'queue': 'xsum', 'routing_key':'task_xsum'}, 314 | # } 315 | 316 | 317 | # 媒体文件格式限制 318 | MEDIA_FILE_CHECK = ('png', 'jpg', 'jpeg', 'gif', 'svg', 'mp4', 'mkv', 'avi', 'mp3', 'webm', 'wav', 319 | 'ogg', 'glb', 'gltf', 'vrm', 'txt', 'pdf', 'epub', 'mobi', 'azw3') 320 | # 媒体文件大小限制 321 | MEDIA_FILE_SIZE = 1024 * 1024 * 64 322 | # 图像文件大小限制 323 | IMAGE_FILE_SIZE = 1024 * 1024 * 8 324 | IMAGE_FILE_CHECK = ('png', 'jpg', 'jpeg', 'gif', 'svg') 325 | VIDEO_FILE_CHECK = ('mp4', 'mkv', 'avi') 326 | SOUND_FILE_CHECK = ('mp3', 'webm', 'wav', 'ogg') 327 | MODEL_FILE_CHECK = ('glb', 'gltf', 'vrm') 328 | EBOOK_FILE_CHECK = ('txt', 'pdf', 'epub', 'mobi', 'azw3') 329 | 330 | 331 | # 限制每个接口请求频次 332 | MINUTE_HZ = 30 333 | 334 | 335 | # 全文检索配置 336 | HAYSTACK_CONNECTIONS = { 337 | 'default': { 338 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', 339 | 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), 340 | }, 341 | } 342 | # 使用自定义的自动更新索引类 343 | HAYSTACK_SIGNAL_PROCESSOR = 'extensions.CustomRealtimeSignal.RealtimeSignalProcessor' 344 | 345 | 346 | if CURRENT_ENV == 'dev': 347 | from configs.dev.settings import * 348 | else: 349 | from configs.prod.settings import * -------------------------------------------------------------------------------- /drfAPI/urls.py: -------------------------------------------------------------------------------- 1 | """drfAPI URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include, re_path 18 | from rest_framework.permissions import AllowAny 19 | from drf_yasg.views import get_schema_view 20 | from drf_yasg import openapi 21 | from extensions.MyCache import CacheVersionControl 22 | from configs.swagger import get_all_url 23 | from django.conf import settings 24 | 25 | 26 | schema_view = get_schema_view( 27 | openapi.Info( 28 | title="Django RESTfulAPI", 29 | default_version='v3.0', 30 | description="Ddescription", 31 | terms_of_service="https://blog.csdn.net/haeasringnar", 32 | contact=openapi.Contact(email="aeasringnar@163.com"), 33 | license=openapi.License(name="MIT License"), 34 | ), 35 | public=True, 36 | permission_classes=[AllowAny], 37 | ) 38 | 39 | urlpatterns = [ 40 | path('user/', include(("apps.user.urls", '用户管理')), name="用户管理"), 41 | path('public/', include(("apps.public.urls", '公共接口')), name="公共接口") 42 | ] 43 | 44 | # 初始化缓存版本号 45 | all_paths = [item[0] for item in get_all_url()] 46 | CacheVersionControl(all_paths).init_data() 47 | 48 | if settings.CURRENT_ENV == "dev": 49 | urlpatterns += [ 50 | re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), 51 | re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), 52 | re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), 53 | ] -------------------------------------------------------------------------------- /drfAPI/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for drfAPI project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drfAPI.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /extensions/BaseModel-delete.py: -------------------------------------------------------------------------------- 1 | # from typing import * 2 | from datetime import datetime 3 | from django.db import models, transaction 4 | from django.db.models import Model, Manager 5 | from django.core.validators import MaxValueValidator, MinValueValidator 6 | from utils.MyDateTime import MyDateTime 7 | from extensions.MyFields import TimestampField 8 | 9 | 10 | class BigDataFilterManager(Manager): 11 | 12 | def all(self, filter_time=None): 13 | if filter_time: 14 | if ',' in filter_time: 15 | start_time = MyDateTime.datetime_to_timestamp(datetime.strptime(filter_time.split(',')[0] + '-01 00:00:00', '%Y-%m-%d %H:%M:%S')) 16 | end_time = MyDateTime.datetime_to_timestamp(datetime.strptime(filter_time.split(',')[1] + '-01 00:00:00', '%Y-%m-%d %H:%M:%S')) 17 | return super().all().filter(create_timestamp__gte=start_time, update_timestamp__lte=end_time) 18 | return super().all() 19 | return super().all() 20 | 21 | 22 | class BaseModel(Model): 23 | # sort = models.IntegerField(default=1, verbose_name='排序') 24 | remark = models.TextField(max_length=1024, default='', blank=True, verbose_name='备注') 25 | sort_timestamp = TimestampField(auto_now_add=True, validators=[MinValueValidator(limit_value=0)], verbose_name='排序时间戳,默认等于创建时间戳') 26 | create_timestamp = TimestampField(auto_now_add=True, validators=[MinValueValidator(limit_value=0)], verbose_name='创建时间戳') 27 | update_timestamp = TimestampField(auto_now=True,validators=[MinValueValidator(limit_value=0)], verbose_name='更新时间戳') 28 | # objects = BigDataFilterManager() # 是否开放大数据时的日期过滤 29 | 30 | class Meta: 31 | abstract = True 32 | 33 | # 已废弃。废弃原因:在批量创建和删除时无法覆盖到save和delete,现在的方案是利用自定义字段和自定义manager queryset 34 | # @transaction.atomic 35 | # def save(self, *args, **kwargs) -> None: 36 | # now_time = int(time.time() * 1e6) 37 | # # print(dir(self)) 38 | # if self.pk is None: 39 | # # print("数据正则创建") 40 | # self.create_timestamp = now_time 41 | # self.sort_timestamp = now_time 42 | # else: 43 | # pass 44 | # # print("数据正则修改") 45 | # self.update_timestamp = now_time 46 | # super().save(*args, **kwargs) 47 | 48 | @transaction.atomic 49 | def delete(self, *args, **kwargs) -> None: 50 | self.update_timestamp = MyDateTime.datetime_to_timestamp(datetime.now()) 51 | super().delete(*args, **kwargs) -------------------------------------------------------------------------------- /extensions/BaseModel.py: -------------------------------------------------------------------------------- 1 | import six 2 | from uuid import uuid1 3 | from datetime import datetime 4 | from operator import attrgetter 5 | from collections import Counter 6 | from django.db import models, router, transaction 7 | from django.db.models import signals, sql, Manager 8 | from django.contrib.admin.utils import NestedObjects 9 | from django.core.exceptions import FieldDoesNotExist 10 | from django.core.validators import MaxValueValidator, MinValueValidator 11 | from utils.MyDateTime import MyDateTime 12 | from extensions.MyFields import TimestampField 13 | 14 | 15 | __all__ = ['BaseModel'] 16 | 17 | 18 | class BigDataFilterManager(Manager): 19 | '''用于进行时间过滤数据''' 20 | 21 | def all(self, filter_time=None): 22 | if filter_time: 23 | if ',' in filter_time: 24 | start_time = MyDateTime.datetime_to_timestamp(datetime.strptime(filter_time.split(',')[0] + '-01 00:00:00', '%Y-%m-%d %H:%M:%S')) 25 | end_time = MyDateTime.datetime_to_timestamp(datetime.strptime(filter_time.split(',')[1] + '-01 00:00:00', '%Y-%m-%d %H:%M:%S')) 26 | return super().all().filter(create_timestamp__gte=start_time, update_timestamp__lte=end_time) 27 | return super().all() 28 | return super().all() 29 | 30 | 31 | class SoftDeleteHelper(): 32 | def __init__(self, using='default', delete_type='soft_delete'): 33 | self.using = using 34 | self.delete_type = delete_type 35 | 36 | def collect_objects(self, objs): 37 | ''' 38 | Collect all related objects 39 | ''' 40 | collector = NestedObjects(using=self.using) 41 | collector.collect(objs) 42 | 43 | if self.delete_type == 'soft_delete': 44 | collector = self.get_un_soft_deleted_objects(collector) 45 | 46 | return collector 47 | 48 | def get_un_soft_deleted_objects(self, collector): 49 | '''filter all those objects from collector which are already 50 | soft-deleted''' 51 | for model, instances in collector.data.items(): 52 | try: 53 | if model._meta.get_field("deleted"): 54 | collector.data[model] = set(filter(lambda x: not x.deleted, 55 | instances)) 56 | except FieldDoesNotExist: 57 | # if deleted field does not exist in model, do nothing 58 | pass 59 | return collector 60 | 61 | def sort_all_objects(self, collector): 62 | ''' 63 | If possible, bring the models in an order suitable for databases that 64 | don't support transactions or cannot defer constraint checks until the 65 | end of a transaction. 66 | ''' 67 | for model, instances in collector.data.items(): 68 | collector.data[model] = sorted(instances, key=attrgetter("pk")) 69 | collector.sort() 70 | 71 | def sql_model_wise_batch_update(self, model, instances, deleted=None): 72 | query = sql.UpdateQuery(model) 73 | pks = [obj.pk for obj in instances] 74 | query.update_batch(pks, 75 | {'deleted': deleted, 'update_timestamp': MyDateTime.datetime_to_timestamp(datetime.now())}, self.using) 76 | 77 | def sql_hard_delete(self, model, instances): 78 | query = sql.DeleteQuery(model) 79 | query.delete_batch([obj.pk for obj in instances], self.using) 80 | 81 | def send_signal(self, model, instances, signal_type): 82 | ''' 83 | Handle pre/post delete/save signal callings 84 | ''' 85 | if not model._meta.auto_created: 86 | for obj in instances: 87 | if signal_type.__contains__('save'): 88 | getattr(signals, signal_type).send( 89 | sender=model, instance=obj, 90 | created=False, using=self.using 91 | ) 92 | else: 93 | getattr(signals, signal_type).send( 94 | sender=model, instance=obj, using=self.using 95 | ) 96 | 97 | @transaction.atomic 98 | def do_work(self, objs): 99 | ''' 100 | Method, call all helper methods to do soft-delete/undelete or 101 | hard-delete 102 | ''' 103 | if not objs: 104 | # no object to delete/undelete 105 | return None 106 | # collect all related objects 107 | collector = self.collect_objects(objs) 108 | # sort collected objects 109 | self.sort_all_objects(collector) 110 | 111 | deleted_counter = Counter() 112 | # soft/hard-delete all nested instnaces in batch - model-wise 113 | if self.delete_type == 'hard_delete': 114 | return collector.delete() 115 | for model, instances in six.iteritems(collector.data): 116 | # send pre-delete signals 117 | if self.delete_type == 'soft_delete': 118 | self.send_signal(model, instances, "pre_delete") 119 | else: 120 | self.send_signal(model, instances, "pre_save") 121 | try: 122 | if self.delete_type == 'soft_delete': 123 | self.sql_model_wise_batch_update(model, instances, deleted=uuid1()) 124 | else: 125 | self.sql_model_wise_batch_update(model, instances, deleted=None) 126 | deleted_counter[model._meta.model_name] += len(instances) 127 | except FieldDoesNotExist: 128 | # hard-delete instnaces of those model that are not made to 129 | # soft-delete 130 | self.sql_hard_delete(model, instances) 131 | deleted_counter[model._meta.model_name] += len(instances) 132 | 133 | # send post-delete signals 134 | if self.delete_type == 'soft_delete': 135 | self.send_signal(model, instances, "post_delete") 136 | else: 137 | self.send_signal(model, instances, "post_save") 138 | return sum(deleted_counter.values()), dict(deleted_counter) 139 | 140 | 141 | class SoftDeleteQuerySet(models.QuerySet): 142 | 143 | @transaction.atomic 144 | def delete(self, using=None): 145 | '''setting deleted attribtue to new UUID', also soft-deleting all its 146 | related objects if they are on delete cascade''' 147 | using = using or "default" 148 | 149 | assert self.query.can_filter(), \ 150 | "Cannot use 'limit' or 'offset' with delete." 151 | try: 152 | if self._fields is not None: 153 | raise TypeError("Cannot call delete() after .values() or\ 154 | .values_list()") 155 | except AttributeError: 156 | pass 157 | 158 | helper = SoftDeleteHelper(using=using, delete_type='soft_delete') 159 | return helper.do_work(self) 160 | 161 | @transaction.atomic 162 | def undelete(self, using=None): 163 | '''setting deleted attribtue to True', also soft-deleting all its 164 | related objects if they are on delete cascade''' 165 | using = using or "default" 166 | 167 | assert self.query.can_filter(), \ 168 | "Cannot use 'limit' or 'offset' with delete." 169 | 170 | try: 171 | if self._fields is not None: 172 | raise TypeError("Cannot call delete() after .values() or\ 173 | .values_list()") 174 | except AttributeError: 175 | pass 176 | 177 | helper = SoftDeleteHelper(using=using, delete_type='soft_undelete') 178 | return helper.do_work(self) 179 | 180 | @transaction.atomic 181 | def hard_delete(self, using=None): 182 | using = using or "default" 183 | 184 | assert self.query.can_filter(), \ 185 | "Cannot use 'limit' or 'offset' with delete." 186 | 187 | try: 188 | if self._fields is not None: 189 | raise TypeError("Cannot call delete() after .values() or\ 190 | .values_list()") 191 | except AttributeError: 192 | pass 193 | helper = SoftDeleteHelper(using=using, delete_type='hard_delete') 194 | helper.do_work(self) 195 | 196 | def only_deleted(self): 197 | if self.deleted_also: 198 | return self.exclude(deleted=None) 199 | raise ValueError('only_deleted can only be called with all_objects') 200 | 201 | 202 | class SoftDeleteManager(models.Manager): 203 | def __init__(self, *args, **kwargs): 204 | self.deleted_also = kwargs.pop('deleted_also', False) 205 | super(SoftDeleteManager, self).__init__(*args, **kwargs) 206 | 207 | def get_queryset(self): 208 | '''return all unsoft-deleted objects if deleted_also is False''' 209 | # return super(SoftDeleteManager, self).get_queryset( 210 | # ).filter(deleted=False) 211 | if self.deleted_also: 212 | return SoftDeleteQuerySet(self.model) 213 | return SoftDeleteQuerySet(self.model).filter(deleted=None) 214 | 215 | def only_deleted(self): 216 | if self.deleted_also: 217 | return self.exclude(deleted=None) 218 | raise ValueError('only_deleted can only be called with all_objects') 219 | 220 | 221 | class BaseModel(models.Model): 222 | ''' 223 | Abstract model that holds: 224 | 1. one attribute: 225 | deleted - default is False, when object is soft-deleted it is set to 226 | new UUID 227 | 228 | 2. objects manager which have following methods: 229 | delete() - to soft delete instance 230 | hard_delete() - to hard delete instance 231 | 3. all_objects manager which have following methods: 232 | delete() - to soft delete instance 233 | hard_delete() - to hard delete instance 234 | only_deleted() - to return all those instances that are soft-deleted 235 | undelete() - to undelete soft deleted objects 236 | 237 | 238 | It override default method delete(), that soft-deletes the object by 239 | setting deleted to new UUID. 240 | ''' 241 | deleted = models.UUIDField(default=None, null=True, blank=True, verbose_name='删除标志') 242 | remark = models.TextField(max_length=1024, default='', blank=True, verbose_name='备注') 243 | sort_timestamp = TimestampField(auto_now_add=True, db_index=True, validators=[MinValueValidator(limit_value=0)], verbose_name='排序时间戳,默认等于创建时间戳') 244 | create_timestamp = TimestampField(auto_now_add=True, db_index=True, validators=[MinValueValidator(limit_value=0)], verbose_name='创建时间戳') 245 | update_timestamp = TimestampField(auto_now=True, db_index=True, validators=[MinValueValidator(limit_value=0)], verbose_name='更新时间戳') 246 | 247 | objects = SoftDeleteManager() 248 | all_objects = SoftDeleteManager(deleted_also=True) 249 | 250 | @transaction.atomic 251 | def delete(self, using=None): 252 | ''' 253 | Setting deleted attribtue to new UUID', 254 | also if related objects are on delete cascade: 255 | they will be soft deleted if those related objects have soft deletion 256 | capability 257 | else they will be hard deleted. 258 | ''' 259 | using = using or router.db_for_write(self.__class__, instance=self) 260 | 261 | helper = SoftDeleteHelper(using=using, delete_type='soft_delete') 262 | return helper.do_work([self]) 263 | 264 | @transaction.atomic 265 | def undelete(self, using=None): 266 | '''setting deleted attribtue to False of current object and all its 267 | related objects if they are on delete cascade''' 268 | using = using or router.db_for_write(self.__class__, instance=self) 269 | helper = SoftDeleteHelper(using=using, delete_type='soft_undelete') 270 | return helper.do_work([self]) 271 | 272 | @transaction.atomic 273 | def hard_delete(self, using=None): 274 | '''setting deleted attribtue to False of current object and all its 275 | related objects if they are on delete cascade''' 276 | using = using or router.db_for_write(self.__class__, instance=self) 277 | helper = SoftDeleteHelper(using=using, delete_type='hard_delete') 278 | return helper.do_work([self]) 279 | 280 | class Meta: 281 | abstract = True -------------------------------------------------------------------------------- /extensions/BaseSerializer.py: -------------------------------------------------------------------------------- 1 | import time 2 | from django.conf import settings 3 | from rest_framework import serializers 4 | from utils.MyDateTime import MyDateTime 5 | 6 | 7 | class BaseModelSerializer(serializers.Serializer): 8 | create_time_format = serializers.SerializerMethodField(label="创建时间") 9 | update_time_format = serializers.SerializerMethodField(label='更新时间') 10 | 11 | def get_create_time_format(self, obj): 12 | return MyDateTime.timestamp_to_str(obj.create_timestamp, settings.REST_FRAMEWORK['DATETIME_FORMAT']) 13 | 14 | def get_update_time_format(self, obj): 15 | return MyDateTime.timestamp_to_str(obj.update_timestamp, settings.REST_FRAMEWORK['DATETIME_FORMAT']) 16 | 17 | 18 | class NormalResponseSerializer(serializers.Serializer): 19 | data = serializers.JSONField(required=True, label="响应数据") 20 | msg = serializers.CharField(required=True, label="响应描述") 21 | code = serializers.IntegerField(required=True, label="响应状态码") -------------------------------------------------------------------------------- /extensions/CustomRealtimeSignal.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from haystack.exceptions import NotHandled 3 | from haystack.signals import BaseSignalProcessor 4 | 5 | 6 | class RealtimeSignalProcessor(BaseSignalProcessor): 7 | """ 8 | Allows for observing when saves/deletes fire & automatically updates the 9 | search engine appropriately. 10 | """ 11 | 12 | def handle_save(self, sender, instance, **kwargs): 13 | """ 14 | Given an individual model instance, determine which backends the 15 | update should be sent to & update the object on those backends. 16 | """ 17 | using_backends = self.connection_router.for_write(instance=instance) 18 | # 只在发现是创建数据的时候才更新索引。原因是不知道什么导致,model在保存是更新索引导致大量的重复索引。 19 | if not kwargs.get('created', False): 20 | return 21 | for using in using_backends: 22 | try: 23 | index = self.connections[using].get_unified_index().get_index(sender) 24 | index.update_object(instance, using=using) 25 | except NotHandled: 26 | # TODO: Maybe log it or let the exception bubble? 27 | pass 28 | 29 | def handle_delete(self, sender, instance, **kwargs): 30 | """ 31 | Given an individual model instance, determine which backends the 32 | delete should be sent to & delete the object on those backends. 33 | """ 34 | using_backends = self.connection_router.for_write(instance=instance) 35 | 36 | for using in using_backends: 37 | try: 38 | index = self.connections[using].get_unified_index().get_index(sender) 39 | index.remove_object(instance, using=using) 40 | except NotHandled: 41 | # TODO: Maybe log it or let the exception bubble? 42 | pass 43 | 44 | def setup(self): 45 | # Naive (listen to all model saves). 46 | models.signals.post_save.connect(self.handle_save) 47 | models.signals.post_delete.connect(self.handle_delete) 48 | # Efficient would be going through all backends & collecting all models 49 | # being used, then hooking up signals only for those. 50 | 51 | def teardown(self): 52 | # Naive (listen to all model saves). 53 | models.signals.post_save.disconnect(self.handle_save) 54 | models.signals.post_delete.disconnect(self.handle_delete) -------------------------------------------------------------------------------- /extensions/ExceptionHandle.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from rest_framework import status 4 | from rest_framework.response import Response 5 | from rest_framework.views import exception_handler 6 | from extensions.MyResponse import MyJsonResponse 7 | 8 | 9 | def handle_re_str(datas: dict): 10 | '''处理默认的异常信息,将字典数据转为字符串数据''' 11 | finally_str = '' 12 | for key in datas: 13 | res = '%s: ' % key 14 | if isinstance(datas[key], str): 15 | finally_str += (res + datas[key]) 16 | elif isinstance(datas[key], list): 17 | for item in datas[key]: 18 | if isinstance(item, str): 19 | res += item 20 | elif isinstance(item, dict): 21 | res += handle_re_str(item) 22 | finally_str += res 23 | else: 24 | finally_str += res 25 | finally_str += '; ' 26 | return finally_str 27 | 28 | 29 | def base_exception_handler(exc, context): 30 | ''' 31 | 用于处理drf的异常定制返回,目的是统一返回信息,只有drf出现异常时才会执行,其他情况不执行 32 | ''' 33 | logging.error('DRF主动提示异常') 34 | error_msg = ["DRF主动提示异常"] 35 | response = exception_handler(exc, context) 36 | if response: # 主要是用来处理字段验证的异常,将异常改为可读性更高的返回值 37 | error_msg.append("可处理的异常") 38 | logging.error('可处理的异常') 39 | logging.error(response.data) 40 | msg = '' 41 | new_data = json.loads(json.dumps(response.data)) 42 | msg = handle_re_str(new_data)[:-2] 43 | code = 0 if response.status_code == 200 else 2 44 | return MyJsonResponse({"msg": msg, "code": code}, status=status.HTTP_200_OK).data 45 | logging.error('未处理的异常') 46 | logging.exception(exc) 47 | error_msg.append("未处理的异常") 48 | return MyJsonResponse({"msg": str(exc), "code": 1}, status=status.HTTP_200_OK).data -------------------------------------------------------------------------------- /extensions/JsonFormater.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | import json 4 | 5 | 6 | class JSONFormatter(logging.Formatter): 7 | REMOVE_ATTR = ["filename", "module", "exc_text", "stack_info", "created", "msecs", "relativeCreated", "exc_info", "msg"] 8 | 9 | def format(self, record): 10 | extra = self.build_record(record) 11 | self.set_format_time(extra) # set time 12 | extra['message'] = record.msg # set message 13 | if record.exc_info: 14 | extra['exc_info'] = self.formatException(record.exc_info) 15 | if self._fmt == 'pretty': 16 | return json.dumps(extra, indent=1, ensure_ascii=False) 17 | else: 18 | return json.dumps(extra, ensure_ascii=False) 19 | 20 | @classmethod 21 | def build_record(cls, record): 22 | return { 23 | attr_name: record.__dict__[attr_name] 24 | for attr_name in record.__dict__ 25 | if attr_name not in cls.REMOVE_ATTR 26 | } 27 | 28 | @classmethod 29 | def set_format_time(cls, extra): 30 | now = datetime.utcnow() 31 | format_time = now.strftime("%Y-%m-%dT%H:%M:%S" + ".%03d" % (now.microsecond / 1000) + "Z") 32 | extra['@timestamp'] = format_time 33 | return format_time -------------------------------------------------------------------------------- /extensions/JwtAuth.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import AuthenticationFailed 2 | from rest_framework.authentication import BaseAuthentication 3 | from extensions.JwtToken import JwtToken 4 | from apps.user.models import User 5 | 6 | 7 | class JwtAuthentication(BaseAuthentication): 8 | www_authenticate_realm = 'api' 9 | 10 | def __init__(self) -> None: 11 | super().__init__() 12 | self.jwt = JwtToken() 13 | 14 | def authenticate(self, request): 15 | token = request.META.get(self.jwt.header_name, '') 16 | if not token: 17 | return None 18 | token, msg = self.jwt.check_headers_jwt(token) 19 | if not token: 20 | raise AuthenticationFailed(msg) 21 | user, msg = self.jwt.decode_user(token, User) 22 | if not user: 23 | raise AuthenticationFailed(msg) 24 | return user, token 25 | 26 | 27 | def authenticate_header(self, request): 28 | """ 29 | Return a string to be used as the value of the `WWW-Authenticate` 30 | header in a `401 Unauthenticated` response, or `None` if the 31 | authentication scheme should return `403 Permission Denied` responses. 32 | """ 33 | return '{0} realm="{1}"'.format(self.jwt.header_type, self.www_authenticate_realm) -------------------------------------------------------------------------------- /extensions/JwtToken.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import logging 3 | # from typing import * 4 | from datetime import datetime 5 | from django.conf import settings 6 | from django.db.models import Model 7 | from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError, DecodeError 8 | 9 | 10 | class JwtToken: 11 | 12 | def __init__(self) -> None: 13 | self.key = settings.JWT_SETTINGS['SIGNING_KEY'] 14 | self.algrithms = settings.JWT_SETTINGS['ALGORITHMS'] 15 | self.algrithm = settings.JWT_SETTINGS['ALGORITHMS'][0] 16 | self.header_type = settings.JWT_SETTINGS['AUTH_HEADER_TYPES'] 17 | self.header_name = settings.JWT_SETTINGS['AUTH_HEADER_NAME'] 18 | self.options = { 19 | 'verify_signature': settings.JWT_SETTINGS['VERIFY_SIGNATURE'], 20 | 'verify_exp': settings.JWT_SETTINGS['VERIFY_EXP'], 21 | 'require': settings.JWT_SETTINGS['REQUIRE'], 22 | } 23 | # 对于自建的,这个没什么意义 24 | # self.audience = settings.JWT_SETTINGS['AUDIENCE'] 25 | # self.issuer = settings.JWT_SETTINGS['ISSUER'] 26 | # self.leeway = settings.JWT_SETTINGS['LEEWAY'] 27 | 28 | def encode(self, payload: dict) -> str: 29 | '''将目标信息转为jwt的值''' 30 | tmp = { 31 | 'exp': datetime.utcnow() + settings.JWT_SETTINGS['ACCESS_TOKEN_LIFETIME'] 32 | } 33 | payload.update(tmp) 34 | return jwt.encode(payload=payload, key=self.key, algorithm=self.algrithm) 35 | 36 | def decode(self, s: str) -> tuple: 37 | '''将jwt的值,也就是token转为目标对象''' 38 | try: 39 | res = jwt.decode( 40 | jwt=s, 41 | key=self.key, 42 | algorithms=self.algrithms, 43 | options=self.options 44 | ) 45 | return res, '' 46 | except ExpiredSignatureError as e: 47 | return None, 'Token expired.' 48 | except InvalidSignatureError as e: 49 | return None, 'Token is not valid.' 50 | except DecodeError as e: 51 | return None, 'Not enough segments.' 52 | 53 | def encode_user(self, payload: dict) -> str: 54 | '''将输入的payload,主要是要加密的数据,转为jwt的token字符串''' 55 | if not isinstance(payload, dict): 56 | raise ValueError("Payload must be a dict type.") 57 | if 'id' not in payload or 'jwt_version' not in payload: 58 | raise KeyError("Payload must contain the 'id' and 'jwt_ersion' fields.") 59 | return self.encode(payload) 60 | 61 | def decode_user(self, s: str, User: Model) -> tuple: 62 | '''将传入的jwt的token值转为内置的User对象''' 63 | try: 64 | obj, msg = self.decode(s) 65 | if not obj: return obj, msg 66 | user = User.objects.values("id", "jwt_version", "is_freeze").filter(id=obj["id"]).first() 67 | if not user: return None, "The account does not exist." 68 | if user.get("jwt_version") != obj["jwt_version"]: return None, "The token has been refreshed." 69 | if user.get("is_freeze"): return None, "The account was frozen." 70 | return User.objects.filter(id=obj["id"]).first(), "" 71 | except Exception as e: 72 | logging.error(f"将jwt解析为用户时发生异常:{e}") 73 | logging.exception(e) 74 | return None, str(e) 75 | 76 | def check_headers_jwt(self, target: str) -> tuple: 77 | '''检查请求头中的jwt''' 78 | target_ls = target.strip().split(" ") 79 | if len(target_ls) != 2: return None, "Invalid Authorization header. No credentials provided. Need 'Bearer '." 80 | header_type, value = target_ls 81 | if header_type != self.header_type: return None, "The message header is invalid, need 'Bearer '." 82 | return value, "" -------------------------------------------------------------------------------- /extensions/MiddleWares.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import logging 4 | from decimal import Decimal 5 | from django.conf import settings 6 | from django.db import connection 7 | from django.http import QueryDict 8 | from django.shortcuts import render 9 | from django.core.cache import cache, caches 10 | from django.utils.deprecation import MiddlewareMixin 11 | from django.http.response import HttpResponseNotFound, HttpResponseServerError, JsonResponse, HttpResponse 12 | from rest_framework import utils, status 13 | from rest_framework.response import Response 14 | from utils.Ecb import ECBCipher 15 | ''' 16 | 0 没有错误 17 | 1 未知错误 针对此错误 线上版前端弹出网络错误等公共错误 18 | 2 前端弹窗错误(包括:字段验证错误、自定义错误、账号或数据不存在、提示错误) 19 | ''' 20 | 21 | 22 | class PUTtoPATCHMiddleware(MiddlewareMixin): 23 | '''将 put 请求转为 patch 请求 中间件''' 24 | 25 | def process_request(self, request): 26 | if request.method == 'PUT': 27 | request.method = 'PATCH' 28 | 29 | 30 | class LogMiddleware(MiddlewareMixin): 31 | '''日志中间件''' 32 | 33 | def process_request(self, request): 34 | try: 35 | logging.info('************************************************* 下面是新的一条日志 ***************************************************') 36 | logging.info('拦截请求的地址:%s;请求的方法:%s' % (request.path, request.method)) 37 | logging.info('==================================== headers 头信息 ====================================================') 38 | for key in request.META: 39 | if key[:5] == 'HTTP_': 40 | logging.debug('%s %s' % (str(key), str(request.META[key]))) 41 | logging.debug(f"Content-Type {request.META.get('CONTENT_TYPE')}") 42 | logging.info('代理IP:%s' % request.META.get('REMOTE_ADDR')) 43 | logging.info('真实IP:%s' % request.META.get('HTTP_X_FORWARDED_FOR')) # HTTP_X_REAL_IP 44 | logging.info('==================================== request body信息 ==================================================') 45 | logging.info('params参数:%s' % request.GET) 46 | if request.content_type in ('application/json', 'text/plain', 'application/xml'): 47 | if request.path not in ('/callpresell/', ): 48 | logging.info('body参数:%s' % request.body.decode()) 49 | if request.content_type in ('multipart/form-data', 'application/x-www-form-urlencoded'): 50 | logging.info('是否存在文件类型数据:%s', bool(request.FILES)) 51 | logging.info('data参数:%s', request.POST) 52 | logging.info('================================== View视图函数内部信息 ================================================') 53 | if request.method in {'DELETE', 'delete'}: 54 | logging.info(f"{'>'*9} 发现删除数据 {'<'*9}") 55 | logging.info(f"删除请求的地址:{request.path},执行用户:{request.user}") 56 | except Exception as e: 57 | logging.error('未知错误:%s' % str(e)) 58 | return JsonResponse({"msg": "请求日志输出异常:%s" % e, "code": 1, "data": {}}) 59 | 60 | def process_exception(self, request, exception): 61 | logging.error('发生错误的请求地址:{};错误原因:{};错误详情:'.format(request.path, str(exception))) 62 | logging.exception(exception) 63 | return JsonResponse({"msg": "An unexpected view error occurred: %s" % exception.__str__(), "code": 1, "data": {}}) 64 | 65 | def process_response(self, request, response): 66 | if settings.SHOWSQL: 67 | for sql in connection.queries: 68 | logging.debug(sql) 69 | return response 70 | 71 | 72 | class PermissionMiddleware(MiddlewareMixin): 73 | '''接口加密中间件''' 74 | 75 | def process_view(self, request): 76 | white_paths = ['/wechat/wxnotifyurl', '/', '/__debug__/', '/__debug__', '/favicon.ico'] 77 | if request.path not in white_paths and not re.match(r'/swagger.*', request.path, re.I) and not re.match(r'/redoc/.*', request.path, re.I) and not re.match(r'/export.*', request.path, re.I): 78 | # print('查看authkey',request.META.get('HTTP_INTERFACEKEY')) 79 | auth_key = request.META.get('HTTP_INTERFACEKEY') # key顺序必须符合要求:毫秒时间戳+后端分配的key+32位随机字符串(uuid更佳) 80 | if auth_key: 81 | # print('查看秘钥:', cache.get(auth_key)) 82 | if cache.get(auth_key): 83 | logging.info('发现秘钥被多次使用,应当记录ip加入预备黑名单。') 84 | return JsonResponse({"msg": "非法访问!已禁止操作!" , "code": 10, "data": {}}) 85 | # 先解密 86 | target_obj = ECBCipher(settings.AES_KEY) 87 | target_key = target_obj.decrypted(auth_key) 88 | # 无法解密时直接禁止访问 89 | if not target_key: return JsonResponse({"msg": "非法访问!已禁止操作!" , "code": 10, "data": {}}) 90 | # 解密成功后 91 | # 设置一个redis 记录当前时间戳 92 | time_int = int(time.time()) # 记录秒 93 | target_time, backend_key, random_str = target_key.split('+') 94 | if backend_key not in settings.DISPATCH_KEYS: return JsonResponse({"msg": "非法访问!已禁止操作!" , "code": 10, "data": {}}) 95 | if (time_int - int(int(target_time) / 1000)) > settings.INTERFACE_TIMEOUT: 96 | logging.info('发现秘钥被多次使用,应当记录ip加入预备黑名单。') 97 | return JsonResponse({"msg": "非法访问!已禁止操作!" , "code": 10, "data": {}}) 98 | cache.set(auth_key, "true", timeout=settings.INTERFACE_TIMEOUT) 99 | pass 100 | else: 101 | return JsonResponse({"msg": "接口秘钥未找到!禁止访问!" , "code": 10, "data": {}}) 102 | 103 | 104 | class DevelopSecurityInterFaceMiddleware(MiddlewareMixin): 105 | '''开发安全中间件,禁止非IP白名单的地址访问''' 106 | 107 | def process_request(self, request): 108 | white_ips = ['150.158.55.39', '35.227.152.73', '39.182.53.220', '127.0.0.1', '39.182.53.138', '115.236.184.202', '39.182.53.102'] 109 | white_list = [] 110 | white_ips += white_list 111 | rip = request.META.get('REMOTE_ADDR') 112 | ip = request.META.get('HTTP_X_FORWARDED_FOR') 113 | if (rip not in white_ips and ip not in white_ips): 114 | return JsonResponse({"msg": "bad request." , "code": 11, "data": {}}, status=400) 115 | 116 | 117 | class FormatReturnJsonMiddleware(MiddlewareMixin): 118 | '''格式化 response 中间件''' 119 | 120 | def process_response(self, request, response): 121 | try: 122 | if isinstance(response, HttpResponseNotFound): 123 | return JsonResponse({"msg": response.reason_phrase, "code": 2,"data": {}}, status=response.status_code) 124 | if isinstance(response, HttpResponseServerError): 125 | return JsonResponse({"msg": response.reason_phrase, "code": 1,"data": {}}, status=response.status_code) 126 | if response.status_code == 204 : 127 | return JsonResponse({"msg": 'delete success', "code": 0,"data": {}}, status=status.HTTP_200_OK) 128 | # print('-'*128) 129 | except Exception as e: 130 | logging.exception(e) 131 | return response 132 | -------------------------------------------------------------------------------- /extensions/MyCache.py: -------------------------------------------------------------------------------- 1 | from typing import * 2 | import time 3 | import pickle 4 | from utils.RedisLockNew import RedisLock 5 | from utils.RedisCli import RedisCli, RedisHash 6 | from utils.Utils import NormalObj 7 | import logging 8 | import hashlib 9 | from .MyResponse import MyJsonResponse 10 | from rest_framework.response import Response 11 | from django.conf import settings 12 | from functools import wraps 13 | ''' 14 | 装饰器类的第一种写法:__call__ 这个魔术方法,这个方法可以使类实例化后的对象可以像函数一样被调用,然后在这里直接接受被装饰的函数, 15 | 这种方式在使用装饰器类时,是必须带括号的,也就是装饰器类是要被实例化的。具体实现如下 16 | 缓存方案: 17 | 如何初始化缓存版本号: 18 | 考虑在项目初始化时通过某些方法来初始化缓存版本号 19 | 在项目初始化的时候,将所有路由进行初始化缓存版本号 20 | 需要一个缓存版本号的服务类 21 | 可以根据path查找缓存版本号 22 | 可以更新缓存版本号 23 | 可以初始化缓存版本号 24 | 本质上是一个操作Redis缓存哈希表的操作类 25 | 缓存装饰器: 26 | 接收参数: 27 | cache_type 读缓存或写缓存 28 | cache_time 缓存的有效期 以秒为单位 29 | block 并发获取缓存时是否阻塞 30 | time_out 阻塞的超时时间 31 | retry 重试次数 32 | ''' 33 | 34 | 35 | class CacheVersionControl: 36 | '''缓存版本号操作类''' 37 | 38 | def __init__(self, paths: List[str]=None) -> None: 39 | self._key = f"cache_version+{NormalObj.to_sha256(settings.SECRET_KEY)}" 40 | self._cache_dict = RedisHash(self._key) 41 | self._paths = paths 42 | 43 | def update(self, key: str) -> bool: 44 | '''更新某个path的缓存版本号''' 45 | # 每次项目运行时都将初始化一个缓存版本号,默认使用当前时间戳作为起始版本号,以保证每次初始化的版本号不同 46 | data = self._cache_dict.get(key, int(time.time())) 47 | data += 1 48 | return bool(self._cache_dict.setdefault(key, data)) 49 | 50 | def get(self, key: str) -> int: 51 | return self._cache_dict.get(key) 52 | 53 | def init_data(self): 54 | '''初始化缓存版本数据''' 55 | if not self._cache_dict.is_empty: 56 | self._cache_dict.clear() 57 | for path in self._paths: 58 | self._cache_dict[path] = 0 59 | 60 | def flush_date(self): 61 | '''销毁缓存版本数据''' 62 | self._cache_dict.clear() 63 | 64 | 65 | class RedisCacheForDecoratorV1: 66 | '''第一版本的缓存装饰器类''' 67 | 68 | def __init__(self, cache_type: str, cache_timeout: int=300) -> None: 69 | '''装饰器类同样是一个类,它拥有类的特性,因此我们可以在装饰时设定一些参数,方便在装饰器中做一些特殊操作''' 70 | self._redis = RedisCli() 71 | self._cache_type = cache_type 72 | self._cache_timeout = cache_timeout 73 | self._is_public = True 74 | self._response = MyJsonResponse() 75 | 76 | def __call__(self, func: Callable) -> Callable: 77 | '''使类实例化后的对象可以直接被当做函数调用,入参就是调用使传入的参数,利用这个可以实现装饰器类,入参就是要装饰的函数''' 78 | @wraps(func) 79 | def warpper(re_self, request, *args: Any, **kwds: Any) -> Any: 80 | '''进行缓存计算或进行变更操作''' 81 | try: 82 | if hasattr(re_self, 'is_public'): 83 | self._is_public = getattr(re_self, 'is_public') 84 | path_key = request.path 85 | operate_lock = RedisLock(self._redis.coon, path_key, self._cache_type) 86 | locked = operate_lock.acquire(timeout=30) 87 | if not locked: 88 | self._response.update(status=400, msg="Another user is operating, please try again later.", code=2) 89 | return self._response.data 90 | # 加锁成功就执行业务逻辑 91 | # 判断是写操作的话,直接执行方法,方法执行完毕后进行更新缓存版本号 92 | if self._cache_type == 'w': 93 | res = func(re_self, request, *args, **kwds) 94 | # 更新缓存版本号的逻辑 95 | CacheVersionControl().update(request.path) 96 | # 释放操作锁 97 | operate_lock.release() 98 | return res 99 | # 否则进行缓存相关的逻辑操作 100 | ''' 101 | todo list 102 | 1、计算缓存的key key = 接口path的hash + 请求的参数hash(如果is_public为假,需要加入token来计算哈希) + 接口的缓存版本号 103 | 2、根据缓存key 来加锁,设置超时时间,超时后先在判断一次缓存是否被生成:如果生成,那就返回缓存结果,否则返回超时。 104 | 3、如果加锁成功,先判断缓存是否存在,如果不存在就进行 查库 设置缓存,返回。 105 | 这样能解决: 106 | 1、高并发下的热点缓存失效,导致的数据库压力激增。 107 | 2、高并发下的大并发创建缓存问题。 108 | ''' 109 | payload = f"{request.path}+{request.GET}+{CacheVersionControl().get(request.path)}" 110 | cache_base_key = NormalObj.to_sha256(payload) if self._is_public else NormalObj.to_sha256(f"{payload}+{request.user}") 111 | target_cache_key = cache_base_key+':cache' 112 | cache_lock = RedisLock(self._redis.coon, cache_base_key+':lock', 'w') 113 | locked = cache_lock.acquire(timeout=20) 114 | if not locked: 115 | # 假设没有获得锁,那么就会因为超时而退出,此时再查一次缓存,如果存在就返回,否则就返回有人在操作。 116 | cache_val = self._redis.coon.get(target_cache_key) 117 | if cache_val: 118 | content, status, headers = pickle.loads(cache_val) 119 | return Response(data=content, status=status, headers=headers) 120 | self._response.update(status=400, msg="Another user is operating, please try again later.", code=2) 121 | return self._response.data 122 | # 如果加锁成功,就先查缓存,没有就落库并设置缓存 123 | cache_val = self._redis.coon.get(target_cache_key) 124 | if not cache_val: 125 | response = func(re_self, request, *args, **kwds) 126 | 127 | if response.status_code == 200: 128 | if hasattr(response, '_headers'): 129 | headers = response._headers.copy() 130 | else: 131 | headers = {k: (k, v) for k, v in response.items()} 132 | response_triple = ( 133 | response.data, 134 | response.status_code, 135 | headers 136 | ) 137 | self._redis.coon.setex(target_cache_key, self._cache_timeout, pickle.dumps(response_triple)) 138 | cache_lock.release() 139 | return response 140 | content, status, headers = pickle.loads(cache_val) 141 | cache_lock.release() 142 | return Response(data=content, status=status, headers=headers) 143 | except Exception as e: 144 | logging.exception(e) 145 | self._response.update(status=500, msg=f"MyCache Error: {e}", code=2) 146 | return self._response.data 147 | return warpper 148 | -------------------------------------------------------------------------------- /extensions/MyCacheViewset.py: -------------------------------------------------------------------------------- 1 | from rest_framework.viewsets import ModelViewSet 2 | from extensions.MyCache import RedisCacheForDecoratorV1 3 | 4 | 5 | class MyModelViewSet(ModelViewSet): 6 | is_public = True 7 | 8 | @RedisCacheForDecoratorV1('r') 9 | def list(self, request, *args, **kwargs): 10 | return super().list(request, *args, **kwargs) 11 | 12 | @RedisCacheForDecoratorV1('w') 13 | def update(self, request, *args, **kwargs): 14 | return super().update(request, *args, **kwargs) 15 | 16 | @RedisCacheForDecoratorV1('w') 17 | def partial_update(self, serializer): 18 | return super().partial_update(serializer) 19 | 20 | @RedisCacheForDecoratorV1('w') 21 | def create(self, request, *args, **kwargs): 22 | return super().create(request, *args, **kwargs) 23 | 24 | @RedisCacheForDecoratorV1('w') 25 | def destroy(self, request, *args, **kwargs): 26 | return super().destroy(request, *args, **kwargs) 27 | 28 | def retrieve(self, request, *args, **kwargs): 29 | return super().retrieve(request, *args, **kwargs) -------------------------------------------------------------------------------- /extensions/MyCustomPagResponse.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from drf_yasg import openapi 4 | from drf_yasg.inspectors import PaginatorInspector 5 | from rest_framework.pagination import CursorPagination, LimitOffsetPagination, PageNumberPagination 6 | 7 | 8 | class MyDjangoRestResponsePagination(PaginatorInspector): 9 | """自定义分页返回的schema""" 10 | 11 | def get_paginated_response(self, paginator, response_schema): 12 | assert response_schema.type == openapi.TYPE_ARRAY, "array return expected for paged response" 13 | paged_schema = None 14 | if isinstance(paginator, (LimitOffsetPagination, PageNumberPagination, CursorPagination)): 15 | has_count = not isinstance(paginator, CursorPagination) 16 | paged_schema = openapi.Schema( 17 | type=openapi.TYPE_OBJECT, 18 | properties=OrderedDict(( 19 | ('total', openapi.Schema(type=openapi.TYPE_INTEGER) if has_count else None), 20 | ('msg', openapi.Schema(type=openapi.TYPE_STRING)), 21 | ('code', openapi.Schema(type=openapi.TYPE_INTEGER)), 22 | ('next', openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI, x_nullable=True)), 23 | ('previous', openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI, x_nullable=True)), 24 | ('data', response_schema), 25 | )), 26 | required=['data'] 27 | ) 28 | 29 | if has_count: 30 | paged_schema.required.insert(0, 'total') 31 | 32 | return paged_schema 33 | -------------------------------------------------------------------------------- /extensions/MyCustomSwagger.py: -------------------------------------------------------------------------------- 1 | from drf_yasg.inspectors.view import SwaggerAutoSchema 2 | from drf_yasg.generators import OpenAPISchemaGenerator 3 | from drf_yasg.utils import filter_none, force_real_str 4 | from drf_yasg.openapi import Operation 5 | from django.urls import get_resolver, URLPattern, URLResolver 6 | 7 | 8 | def get_urlresoler(): 9 | for r in get_resolver().url_patterns: 10 | if isinstance(r, URLResolver): 11 | yield str(r.pattern).replace('^', '').replace('$', '').replace('/', ''), r.namespace 12 | 13 | 14 | class BaseOpenAPISchemaGenerator(OpenAPISchemaGenerator): 15 | '''重写 OpenAPISchemaGenerator 手动为每个路由添加 tag''' 16 | 17 | def get_schema(self, request=None, public=False): 18 | '''重写父类方法''' 19 | swagger = super().get_schema(request, public) 20 | swagger.tags = [ 21 | { 22 | 'name': item[0], 23 | 'description': item[1] 24 | } for item in get_urlresoler() 25 | ] 26 | # 在这里初始化缓存版本号 27 | # paths = list(swagger.paths.keys()) 28 | # CacheVersionControl(paths=paths).init_data() 29 | return swagger 30 | 31 | 32 | class MySwaggerAutoSchema(SwaggerAutoSchema): 33 | 34 | def get_operation(self, operation_keys=None): 35 | '''重写父类的方法,进行自定义''' 36 | 37 | operation_keys = operation_keys or self.operation_keys 38 | 39 | consumes = self.get_consumes() 40 | produces = self.get_produces() 41 | 42 | body = self.get_request_body_parameters(consumes) 43 | query = self.get_query_parameters() 44 | parameters = body + query 45 | parameters = filter_none(parameters) 46 | parameters = self.add_manual_parameters(parameters) 47 | 48 | operation_id = self.get_operation_id(operation_keys) 49 | summary, description = self.get_summary_and_description() 50 | if not summary: summary = description # set description and summary 51 | security = self.get_security() 52 | assert security is None or isinstance(security, list), "security must be a list of security requirement objects" 53 | deprecated = self.is_deprecated() 54 | tags = self.get_tags(operation_keys) 55 | 56 | responses = self.get_responses() 57 | 58 | return Operation( 59 | operation_id=operation_id, 60 | description=force_real_str(description), 61 | summary=force_real_str(summary), 62 | responses=responses, 63 | parameters=parameters, 64 | consumes=consumes, 65 | produces=produces, 66 | tags=tags, 67 | security=security, 68 | deprecated=deprecated 69 | ) 70 | 71 | # def get_tags(self, operation_keys=None): # 暂时无用 72 | # tags = super().get_tags(operation_keys) 73 | # print('-' * 128) 74 | # print(tags) 75 | # print(operation_keys) 76 | # if "v1" in tags and operation_keys: 77 | # # `operation_keys` 内容像这样 ['v1', 'prize_join_log', 'create'] 78 | # tags[0] = operation_keys[1] 79 | 80 | # return tags 81 | 82 | 83 | -------------------------------------------------------------------------------- /extensions/MyFields.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from django.db.models import BigIntegerField 3 | from utils.MyDateTime import MyDateTime 4 | 5 | 6 | class TimestampField(BigIntegerField): 7 | 8 | description = "Custom timestamp field." 9 | 10 | def __init__(self, verbose_name=None, name=None, auto_now=False, 11 | auto_now_add=False, **kwargs): 12 | self.auto_now, self.auto_now_add = auto_now, auto_now_add 13 | if auto_now or auto_now_add: 14 | kwargs['editable'] = False 15 | kwargs['blank'] = True 16 | super().__init__(verbose_name, name, **kwargs) 17 | 18 | def pre_save(self, model_instance, add): 19 | if self.auto_now or (self.auto_now_add and add): 20 | value = MyDateTime.datetime_to_timestamp(datetime.now()) 21 | setattr(model_instance, self.attname, value) 22 | return value 23 | else: 24 | return super().pre_save(model_instance, add) -------------------------------------------------------------------------------- /extensions/MyResponse.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | 4 | class MyJsonResponse: 5 | 6 | def __init__(self, res_data: dict={}, status: int=200, headers=None) -> None: 7 | self.__res_format = { 8 | "msg": 'ok', 9 | "code": 0, 10 | "data": {} 11 | } 12 | self.__status = status 13 | self.__headers = headers 14 | if res_data: 15 | self.__res_format.update(res_data) 16 | 17 | def update(self, status:int = None, **kwargs): 18 | self.__res_format.update(kwargs) 19 | if status: 20 | self.__status = status 21 | 22 | @property 23 | def data(self): 24 | return Response(data=self.__res_format, status=self.__status, headers=self.__headers) -------------------------------------------------------------------------------- /extensions/Pagination.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.conf import settings 3 | from rest_framework.response import Response 4 | from rest_framework.pagination import PageNumberPagination 5 | from extensions.MyResponse import MyJsonResponse 6 | 7 | 8 | class Pagination(PageNumberPagination): 9 | '''自定义分页类''' 10 | page_size = 10 11 | page_size_query_param = 'page_size' 12 | page_query_param = 'page' 13 | max_page_size = 100 14 | 15 | def get_my_next(self): 16 | # logging.info("{}-{}".format(8888, self.request.path)) 17 | # logging.info("{}-{}".format(8888, settings.SERVER_NAME)) 18 | # logging.info("{}-{}".format(8888, self.get_next_link().split(self.request.path))) 19 | return settings.SERVER_NAME + self.request.path + self.get_next_link().split(self.request.path)[1] 20 | 21 | def get_my_pre(self): 22 | return settings.SERVER_NAME + self.request.path + self.get_previous_link().split(self.request.path)[1] 23 | 24 | def get_paginated_response(self, data): 25 | return MyJsonResponse({ 26 | 'total': self.page.paginator.count, 27 | 'next': self.get_next_link(), 28 | 'previous': self.get_previous_link(), 29 | 'data': data 30 | }).data 31 | 32 | def get_paginated_response_schema(self, schema): 33 | return { 34 | 'type': 'object', 35 | 'properties': { 36 | 'total': { 37 | 'type': 'integer', 38 | 'example': 123, 39 | }, 40 | 'next': { 41 | 'type': 'string', 42 | 'nullable': True, 43 | 'format': 'uri', 44 | 'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format( 45 | page_query_param=self.page_query_param) 46 | }, 47 | 'previous': { 48 | 'type': 'string', 49 | 'nullable': True, 50 | 'format': 'uri', 51 | 'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format( 52 | page_query_param=self.page_query_param) 53 | }, 54 | 'data': schema, 55 | }, 56 | } -------------------------------------------------------------------------------- /extensions/Permission.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission 2 | ''' 3 | mixins.CreateModelMixin create POST 创建数据 4 | mixins.RetrieveModelMixin retrieve GET 检索数据 5 | mixins.UpdateModelMixin update PUT 更新数据 perform_update PATCH 局部更新数据 6 | mixins.DestroyModelMixin destroy DELETE 删除数据 7 | mixins.ListModelMixin list GET 获取数据 8 | ''' 9 | 10 | 11 | class IsAuthPermission(BasePermission): 12 | '''检查必须登录权限''' 13 | 14 | def has_permission(self, request, view): 15 | return bool(request.auth) 16 | 17 | 18 | # 暂时关闭,等待动态全新完工后再加 19 | # class BaseAuthPermission(object): 20 | 21 | # def need_auth_list_check(self, auth_name): 22 | # # 只需登录白名单 23 | # if auth_name in ['userinfo', 'export*', ]: 24 | # return True 25 | # else: 26 | # return False 27 | 28 | # def has_permission(self, request, view): 29 | # # 动态权限层 后期拆分 这里只验证动态权限 30 | # # print('请求的path:', request.path) 31 | # # print('请求的path拆分:', request.path.split('/')[1]) 32 | # auth_name = request.path.split('/')[1] 33 | # # 无用户登录时 34 | # if not bool(request.auth): 35 | # return False 36 | # # 当是超级管理员时 37 | # if request.user.group.group_type == 'SuperAdmin': 38 | # return True 39 | # # 访问只需登录路由时 40 | # if self.need_auth_list_check(auth_name): 41 | # return True 42 | # admin_auth = AuthPermission.objects.filter(object_name=auth_name, auth_id=request.user.auth_id).first() 43 | # if request.user.group.group_type in ['SuperAdmin', 'Admin'] and admin_auth: 44 | # if view.action in ['list', 'retrieve']: 45 | # # 查看权限 46 | # return bool(admin_auth.auth_list == True) 47 | # elif view.action == 'create': 48 | # # 创建权限 49 | # return bool(admin_auth.auth_create == True) 50 | # elif view.action in ['update', 'partial_update']: 51 | # # 修改权限 52 | # return bool(admin_auth.auth_update == True) 53 | # elif view.action == 'destroy': 54 | # # 删除权限 55 | # return bool(admin_auth.auth_destroy == True) 56 | # else: 57 | # return False 58 | # else: 59 | # return False 60 | 61 | # def has_object_permission(self, request, view, obj): 62 | # return self.has_permission(request, view) -------------------------------------------------------------------------------- /extensions/RenderResponse.py: -------------------------------------------------------------------------------- 1 | from rest_framework.renderers import JSONRenderer 2 | 3 | 4 | class BaseJsonRenderer(JSONRenderer): 5 | ''' 6 | 重构render方法,定制DRF返回的response格式 7 | ''' 8 | def render(self, data, accepted_media_type=None, renderer_context=None): 9 | if renderer_context: 10 | if isinstance(data, dict) and 'msg' in data and 'code' in data: return super().render(data, accepted_media_type, renderer_context) 11 | ret = { 12 | 'msg': 'ok', 13 | 'code': 0, 14 | 'data': data, 15 | } 16 | return super().render(ret, accepted_media_type, renderer_context) 17 | else: 18 | return super().render(data, accepted_media_type, renderer_context) -------------------------------------------------------------------------------- /extensions/Throttle.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.conf import settings 3 | from rest_framework.throttling import BaseThrottle 4 | from utils.RedisCli import RedisCli 5 | 6 | 7 | class VisitThrottle(BaseThrottle): 8 | '''自定义频率限制类''' 9 | def __init__(self): 10 | self.history = None 11 | self.cache = RedisCli() 12 | 13 | def allow_request(self, request, view): 14 | remote_addr = request.META.get('HTTP_X_REAL_IP') if not request.META.get('REMOTE_ADDR') else request.META.get('REMOTE_ADDR') 15 | # logging.info("{}-{}".format('请求的IP:', remote_addr)) 16 | # logging.info("{}-{}".format('请求的路径:', request.path)) 17 | # logging.info("{}-{}".format('请求的方法:', request.method)) 18 | if request.user and request.user.id: 19 | remote_addr = 'user%s' % request.user.id 20 | self.visit_key = remote_addr + request.path 21 | # logging.info("{}-{}".format('构造请求的key', self.visit_key)) 22 | counts = self.cache.coon.get(self.visit_key) 23 | if not counts: 24 | self.cache.coon.set(self.visit_key, 1, 60, nx=True) 25 | return True 26 | if int(counts.decode()) >= settings.MINUTE_HZ: 27 | return False 28 | self.cache.coon.incr(self.visit_key) 29 | self.cache.coon.expire(self.visit_key, 60) 30 | return True 31 | 32 | def wait(self): 33 | es = self.cache.coon.pttl(self.visit_key) # 以毫秒为单位返回键的剩余过期时间 34 | es = (int(es) / 1000) 35 | return es 36 | -------------------------------------------------------------------------------- /extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/extensions/__init__.py -------------------------------------------------------------------------------- /feature.txt: -------------------------------------------------------------------------------- 1 | 未来开发计划 2 | 1、增加表格导出示例 3 | 2、增加大文件断点上传和下载研究 4 | 3、全新的基于REBAC的权限控制系统 5 | 4、加入多表连查的 6 | 5、加入读写分离 7 | 6、加入自建的缓存模块 已完成 8 | 7、增加启动服务的脚本,支持uwgsi、asgi、docker的启动方式,需要包含celery分布式、定时器的启动。 9 | 8、增加国际化 已完成 -------------------------------------------------------------------------------- /frameHint.txt: -------------------------------------------------------------------------------- 1 | 对于Django、DRF的一些框架功能提示: 2 | 1、模型相关 3 | 字段的常用、通用Option 4 | null = True # 该字段允许值为空,也就是MySQL上的NULL,默认为False 5 | blank = True # 允许键为空,指定的字段可以不传,注意,这与null不同。null纯属数据库相关,而blank则与验证相关。默认为False 6 | choices = ((0,'男'),('1','女'),) # 选项类型,类似于枚举类型,会被模型验证,如果传入的值不在选项中会引发验证异常。 7 | default = 1 # 指定默认值,可以是一个值或者是个可调用的对象,如果是个可调用对象,每次实例化模型时都会调用该对象。 8 | db_index=True # 为字段设置普通索引 9 | verbose_name = '描述' # 指定label 或字段描述,相当于MySQL中字段的备注。 10 | validators = [] # 接收一个验证器列表,用于对指定字段传入数据的验证。 11 | db_column = 'test' # 指定这个字段要使用的数据库列名,如果没有给出,将使用字段名。 12 | primary_key = False # 指定字段为主键,通常用不到,使用内置的ID作为主键即可。 13 | unique = True # 指定字段唯一性约束也是唯一索引,通常会使用联合唯一,因此这个也不常用。 14 | editable = True # 如果为False表示字段不会被修改,因为会被模型验证跳过。默认为True 15 | 16 | 常用模型字段类型: 17 | CharField # 字符串类型,存在一个特殊的必传参数: 18 | max_length = 255 # 指定字段容量,无论中英文值得都是字符总长度,必须要指定,并且最大为255。 19 | 那么需要更大一点但是又不需要使用大字段时怎么办呢?其实就是使用TextField设置max_length而已 20 | TextField # 大文本类型,这个字段比较特殊,当你设置了max_length,并且没有超过65535个字节时,它实际在数据库使用的varchar类型。 21 | JSONField # 存储json字符串类型,Django3.1开始新增的的一个类型,本质上是字符串类型。 22 | DateField # 日期类型,存在两个特殊参数 23 | auto_now = False # 每次保存对象时,自动将该字段设置为现在。对于 最后修改 的时间戳很有用。默认为False 24 | auto_now_add = False # 当第一次创建对象时,自动将该字段设置为现在。对创建时间戳很有用。默认为False 25 | 注:目前 将 auto_now 或 auto_now_add 设置为 True,将导致该字段设置为 editable=False 和 blank=True。 26 | auto_now_add、auto_now 和 default 选项是相互排斥的。这些选项的任何组合都会导致错误。 27 | DateTimeField # 时间类型,同样支持 auto_now_add、auto_now 28 | TimeField # 时间类型,同样支持 auto_now_add、auto_now 29 | IntegerField # 一个整数类型,32位的,取值 -2147483648 到 2147483647 30 | BigIntegerField # 一个大整数,64位的,取值 -9223372036854775808 到 9223372036854775807 31 | BooleanField # bool类型,注:建议定义默认值,否则会导致null为True,就是允许为NULL 32 | DecimalField # 一个固定精度的十进制数 可以指定长度和小数位数,存在两个必传参数: 33 | max_digits = 15 # 指定这个数的最大长度,包括小数位在内 34 | decimal_places = 2 # 指定小数点后小数位数 35 | EmailField # 邮箱类型,本质上是字符串,好处是会自动进行邮箱格式验证。 36 | FloatField # 浮点数类型 37 | UUIDField # 一个用于存储唯一UUID的类型,可以通过default=uuid.uuid1 来赋默认值。 38 | 例如想指定主键为UUID:id = UUIDField(primary_key=True, default=uuid.uuid1, editable=False) 39 | 40 | 字段关系 41 | ForeignKey(OtherModel, on_delete=PROTECT, verbose_name='label', related_name='related_name') # 外键类型 42 | OneToOneField(OtherModel, on_delete=PROTECT, verbose_name='label', related_name='related_name') # 一对一的外键类型 43 | ManyToManyField(OtherModel, verbose_name='label', blank=True, related_name='related_name') # 多对多类型,django会维护一个中间表。如果需求更多,可以自行使用中间表实现 44 | related_name 的作用是将被外键关联的数据,通过related_name定义的命令来访问所有关联到它的数据,并且要求这个字段对于被关联表是唯一的。 45 | on_delete属性选项的意义: 46 | PROTECT:当外键数据删除时,指向外键的的数据不删除。 47 | CASCADE:当外键数据删除时,指向外键的数据一起删除。 48 | SET_NULL:当外键数据删除时,指向外键的数据该列设置为null。 49 | SET_DEFAULT:当外键数据删除时,指向外键的数据该列恢复默认值。 50 | SET(自定义方法):当外键数据删除时,指向外键的数据使用自定义方法来设置值。具体可参考官方文档。 51 | 52 | 模型元选项,也称Meta选项 53 | abstract = True # 表示该模型是抽象基类,不能作为实际的模型,用于其他模型继承这个模型。 54 | app_label = 'myapp' # 声明模型所属的app,可以不写,会默认获取。 55 | db_table = 'Table_Name' # 自定义数据表在数据库中的名称 56 | verbose_name = 'A表' # 自定义数据表的表述,官方叫 对象的人类可读名称 单数形式 57 | verbose_name_plural = verbose_name # 对verbose_name的复数描述 58 | ordering = ['-create_date'] # 指定数据的默认排序,该例表示根据创建时间倒序(不加-为升序)。建议不写,默认是主键升序。 59 | unique_together = [['name1', 'name2']] # 设置联合唯一性约束、联合唯一索引,可以设置多个,为了方便起见,unique_together在处理一组字段时可以是一个一维列表。 60 | index_together = [['name1', 'name2']] # 设置联合普通索引,可以设置多个,为了方便起见,index_together在处理一组字段时可以是一个一维列表。 61 | indexes = [ 62 | models.Index(fields=['last_name', 'first_name']), 63 | models.Index(fields=['first_name'], name='first_name_idx'), 64 | ] # 定义索引列表 索引列可以统一在这里定义 65 | constraints = [ 66 | models.CheckConstraint(check=models.Q(age__gte=18), name='age_gte_18'), # 假设存在一个age字段,这个约束就是保证年龄必须大等于18 67 | ] # 定义模型的约束列表 68 | 69 | 2、ORM相关 70 | ORM操作指南 71 | 由于本项目使用了假删除(或称软删除),因此在实际使用上会多出一些功能。 72 | objects 返回没有删除的所有数据 73 | all_objects 返回所有数据,包括已删除的数据 74 | delete 删除数据,假删 75 | undelete 恢复假删数据 意味着之前获取的实例是通过 all_objects 获取的 76 | hard_delete 硬删除,数据会真的从数据表中删除 77 | 假设有一个 User 模型类如下示: 78 | class User(models.Model): 79 | gender_choice = ( 80 | ('privacy', '保密'), 81 | ('male', '男'), 82 | ('female', '女') 83 | ) 84 | name = models.CharField(max_length=32, verbose_name='用户账号') 85 | mobile = models.CharField(max_length=12, verbose_name='用户手机号') 86 | email = models.EmailField(default='', null=True, verbose_name='用户邮箱') 87 | gender = models.CharField(max_length=16, choices=gender_choice, default='privacy', verbose_name='性别') 88 | 89 | class Meta: 90 | db_table = 'user' 91 | verbose_name = '用户表' 92 | verbose_name_plural = verbose_name 93 | indexes = [ 94 | models.Index(fields=['name']), 95 | ] 96 | unique_together = [ 97 | ['mobile'], 98 | ] 99 | 新增数据: 100 | 方法一: 101 | user = User(name='asd', mobile='123').save() 102 | 方法二: 103 | User.objects.create(name='asd', mobile='123') 104 | 方法三: 105 | users = [User(name='asd', mobile='123'), User(name='qwe', mobile='345')] 106 | User.objects.bulk_create(users) # 批量创建 107 | 理解QuerySet: 108 | 在Django中模型建立后,可以通过ORM和数据库交互,其中最重要的一个就是QuerySet对象,它包含在模型中,通过模型类的 Manager 构建一个 QuerySet。 109 | 一个 QuerySet 代表来自数据库中对象的一个集合。它可以有 0 个,1 个或者多个 filters. Filters,可以根据给定参数缩小查询结果量。 110 | 简单来说QuerySet就是专门用来通过ORM查询数据库数据,并且返回后会映射为你定义好的模型实例。 111 | QuerySet支持链式调用,也就意味着你得到一个QuerySet对象后,可以返回的通过链式写法调用不同的QuerySet方法。 112 | 最简单的 例如 User.objects.all() 返回了一个 QuerySet。示例一个链式调用 User.objects.filter().all() 113 | 详细的QuerySet文档地址:https://docs.djangoproject.com/zh-hans/3.2/ref/models/querysets/#filter 114 | 主要的QuerySet方法释义: 115 | filter(*args, **kwargs) 用于传递参数做where查询,多个字段为and 116 | all() 用于返回所有数据 117 | excluede(*args, **kwargs) 用于查询不匹配的数据 118 | order_by(*fields) 用于对传入字段进行排序 119 | reverse() 用于翻转查询得到的数据集 120 | distinct(*fields) 用于查询去重,可以指定比较哪些字段,不传表示比较所有字段 121 | union(*other_qs, all=False) 用于组合两个或多个 QuerySet 的结果 例如 qs1.union(qs2, qs3) 122 | values(*fields, **expressions) 用于查询指定的字段,会得到一个字典数据集 123 | raw(raw_query, params=(), translations=None, using=None) 用于执行原生SQL语句 124 | select_for_update(nowait=False, skip_locked=False, of=(), no_key=False) 用于加排它锁查,例如 User.objects.select_for_update().filter(id=1).first() 125 | 126 | 查找数据: 127 | User.objects.all() # 查询所有数据 128 | User.objects.filter(id=1).exists() # 查询是否存在数据,如果 QuerySet 包含任何结果,则返回 True,如果不包含,则返回 False。 129 | User.objects.filter(id=1).first() # 得到一个对象,如果没有返回None 130 | User.objects.filter(id=1).last() # 查询最后一个数据 131 | User.objects.get(id=1) # 得到一个对象,如果没有会抛出 Entry.DoesNotExist 异常 132 | User.objects.filter(id__gte=1) # 查询id大等于1的数据 __gt 大于 133 | User.objects.filter(id__lte=1) # 查询id小等于1的数据 __lt 小于 134 | User.objects.filter(id__in=[1,2,3]) # 查询id指定区间的数据 135 | User.objects.exclude(id=10) # 查询id不等于10的数据 136 | User.objects.filter(name='test').exclude(id=10) # 查询name为test但id不等于10的数据,ORM支持链式调用 并且exclude内同样支持范围查询 137 | User.objects.order_by('date') # 对结果集进行排序,使用date字段升序,-date表示倒序 138 | User.objects.count() # 返回数据的总数 139 | User.objects.filter(name__icontains='文') # 查询用户名字里面包含 文 140 | __iexact 精确等于 忽略大小写 ilike 'aaa' 141 | __contains 包含 like '%aaa%' 142 | __icontains 包含 忽略大小写 ilike '%aaa%',但是对于sqlite来说,contains的作用效果等同于icontains 143 | User.objects.filter(username__isnull=True) # 查询用户名为空的用户 144 | User.objects.exclude(username__isnull=True) # 查询用户名不为空的用户 145 | User.objects.filter(name='test', statu='1') # 表示与查询,名称为test且status为1的数据 146 | User.objects.filter(Q(state=0) | Q(state=1)) # 利用django.db.models.Q实现或查询 147 | User.objects.values('id', 'name').filter(id=1).first() # 只查询数据的id、name字段,返回不是模型实例,而是一个类似字典的结构,可以通过key获取数据 148 | User.objects.values('id', 'name').filter(id=1)# 只查询数据的id、name字段,返回一个列表,列表内元素是类似字典的结构 149 | 更详细的字段查找参考:https://docs.djangoproject.com/zh-hans/3.2/ref/models/querysets/#field-lookups 150 | 更详细的聚合函数参考:https://docs.djangoproject.com/zh-hans/3.2/ref/models/querysets/#aggregation-functions 151 | 修改数据: 152 | 方法一: 153 | user = User.objects.filter(id=1).first() # 得到一个对象,如果没有返回None 154 | user.name = 'zxc' 155 | user.save() 156 | 方法二: 157 | User.objects.filter(id=1).update(name='zxc') # 这个可以批量更新一批数据,但是有个缺点是无法触发signals中的信号,如果需要触发信号需要使用save 158 | 官方对QuerySet.update的说法 159 | QuerySet.update() 用于保存更改,所以这比遍历模型列表并对每个模型调用 save() 更有效,但它有一些注意事项: 160 | 你不能更新模型的主键。 161 | 每个模型的 save() 方法没有被调用,而且 pre_save 和 post_save 信号没有被发送。 162 | 如果更新大量行中的大量列,生成的 SQL 可能非常大。通过指定一个合适的 batch_size 来避免这种情况。 163 | 更新定义在多表继承祖先上的字段将给每个祖先带来额外的查询。 164 | 当单个批处理包含重复项时,该批处理中只有第一个实例会产生更新。 165 | 方法三:批量更新 166 | users = User.objects.all() 167 | users[0].name = 'test1' 168 | users[1].name = 'test2' 169 | Entry.objects.bulk_update(users, ['name']) # 比逐个更新性能更好 170 | 删除数据: 171 | 方法一: 172 | user = User.objects.filter(id=1).first() 173 | user.delete() 174 | 方法二: 175 | User.objects.filter(id__gte=1).delete() # 批量删除一批数据 176 | F对象的使用: 177 | class Test(models.Model): 178 | all_num= models.IntegerField(default=0, verbose_name='总数量 ') 179 | have_num= models.IntegerField(default=0, verbose_name='已有数量') 180 | 181 | class Meta: 182 | db_table = 'A_Test_Table' 183 | verbose_name = '测试表' 184 | verbose_name_plural = verbose_name 185 | 要实现SQL语句:select * from a_test_tablewhere all_num > have_num 186 | from django.db.models import F 187 | test= Test.objects.filter(all_num__gt=F('have_num')) 188 | 执行原始SQL语句: 189 | 方法一: 190 | Manager.raw(raw_query, params=(), translations=None) 191 | 该方法接受一个原生 SQL 查询语句,执行它,并返回一个 django.db.models.query.RawQuerySet 实例。这个 RawQuerySet 能像普通的 QuerySet 一样被迭代获取对象实例。 192 | User.objects.raw("select * from user") 等价于 User.objects.all() 193 | User.objects.raw("select * from user")[0] 等价于 User.objects.first() 194 | users = Person.objects.raw('SELECT *, age(birth_date) AS age FROM user') # 支持别名 195 | for u in users: 196 | print("%s is %s." % (u.first_name, u.age)) # 支持别名 197 | Person.objects.raw('SELECT * FROM user WHERE name = %s', ['lname']) # 支持传递参数 198 | 方法二:有时候,甚至 Manager.raw() 都无法满足需求:你可能要执行不明确映射至模型的查询语句,或者就是直接执行 UPDATE, INSERT 或 DELETE 语句。 199 | from django.db import connection 200 | def my_custom_sql(self): 201 | with connection.cursor() as cursor: 202 | cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz]) 203 | cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz]) 204 | row = cursor.fetchone() 205 | return row 206 | 注意,要避免 SQL 注入,你绝对不能在 SQL 字符串中用引号包裹 %s 占位符。 207 | 注意,若要在查询中包含文本的百分号,你需要在传入参数使用两个百分号: 208 | cursor.execute("SELECT foo FROM bar WHERE baz = '30%'") 209 | cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND id = %s", [self.id]) 210 | 显式的使用事务 211 | from django.db import transaction 212 | @transaction.atomic 213 | def viewfunc(request): 214 | # This code executes inside a transaction. 215 | do_stuff() 216 | 217 | from django.db import transaction 218 | def viewfunc(request): 219 | # This code executes in autocommit mode (Django's default). 220 | do_stuff() 221 | with transaction.atomic(): 222 | # This code executes inside a transaction. 223 | do_more_stuff() 224 | 225 | 226 | 3、序列化器相关 227 | 序列化器字段的通用、常用Option 228 | read_only = True # 表示字段只读,默认为False 229 | write_only = True # 表示字段只写,默认为False 230 | required = True # 表示字段必传,会经过序列化器验证,默认为True 231 | default = None # 表示字段的默认值,需要传入一个明确的值或可调用的对象 232 | allow_blank = False # 表示字段允许传入空值,包括None和空字符串,也可不传 233 | label = "描述" # 用于对字段做描述,短文本描述 234 | help_text = "长文本描述" # 和label的区别就是这个常用于长文本描述 235 | error_messages = { 236 | 237 | } # 接收一个异常信息的提示,错误消息的错误代码字典。 238 | validators = [ 239 | ] # 接收一个验证器列表,用对字段做特殊验证 240 | 241 | 序列化器常用字段类型 242 | BooleanField() # bool类型 243 | CharField() # 字符串类型,存在多个特殊选项 244 | max_length = 255# 用于指定输入字符串的最大长度,可选 245 | min_length = 10 # 用于指定输入字符串的最小长度,可选 246 | trim_whitespace = True # 为真会自动去除字符串首位空格,默认为True 247 | EmailField() # 邮件类型,本质上是字符串类型,使用它的好处是带有邮箱格式验证。同样支持 max_length min_length选项 248 | IntegerField() # int类型,存在两个特殊选项 249 | max_value = 1000 # 验证提供的数字不大于此值。 250 | min_value = 1 # 验证提供的数字不小于此值。 251 | FloatField(max_value=None, min_value=None) # float类型,同样支持 max_value min_value 252 | DecimalField() # 一个固定精度的十进制数,存在多个选项 253 | max_digits # 数字中允许的最大位数。它必须是None或大于或等于 的整数decimal_places。 254 | decimal_places # 与数字一起存储的小数位数。 255 | coerce_to_string # 设置为True是否应为表示返回字符串值,或者False是否Decimal应返回对象。COERCE_DECIMAL_TO_STRING默认为与设置键相同的值,True除非被覆盖。如果Decimal序列化器返回对象,则最终输出格式将由渲染器确定。请注意,设置localize会将值强制为True。 256 | max_value # 验证提供的数字不大于此值。 257 | min_value # 验证提供的数字不小于此值。 258 | localize # 设置为True启用基于当前语言环境的输入和输出本地化。这也将coerce_to_string迫使True。默认为False. 请注意,如果您USE_L10N=True在设置文件中进行了设置,则会启用数据格式。 259 | rounding # 设置量化为配置精度时使用的舍入模式。有效值为decimal模块舍入模式。默认为None. 260 | DateTimeField() # 时间类型,支持多个特殊参数 261 | format = "%Y-%m-%d %H:%:M:%S" # 用于指定输出时间的格式 262 | input_format = ["%Y-%m-%d %H:%:M:%S", ] # 用于指定输入时间的格式字符串列表 263 | DateField() # 日期格式,同样支持 format input_format 264 | ChoiceField() # 选择类型或枚举类型,只能选择一个 265 | choices = ((1, 'A'), (2, 'B')) # 有效值列表或(key, display_name)元组列表。 266 | allow_blank = Fasle # 如果设置为,True则空字符串应被视为有效值。如果设置为,False则空字符串被视为无效并会引发验证错误。默认为False. 267 | MultipleChoiceField() # 多选类型,和选择类型类似,区别在于这个允许选择多个选项。 268 | ListField(child=IntegerField(min_value=0, max_value=100)) # 复合类型,用于验证数组类型数据,并且支持对数组内元素验证 269 | child # 应用于验证列表中的对象的字段实例。如果未提供此参数,则不会验证列表中的对象。 270 | allow_empty # 指定是否允许空列表。 271 | min_length # 验证列表包含不少于此数量的元素。 272 | max_length # 验证列表包含不超过此数量的元素。 273 | DictField(child=, allow_empty=True) DictField(child=CharField()) # 复合类型,用于类似于字典的数据,会验证key和value 274 | child # 应该用于验证字典中的值的字段实例。如果未提供此参数,则不会验证映射中的值。 275 | allow_empty # 指定是否允许空字典。 276 | JSONField() # 复合类型,用于验证json数据,实际中常用的是通过多个序列化器的组合来实现,因为那样更友好、便捷 277 | binary # 如果设置为,True则该字段将输出并验证 JSON 编码字符串,而不是原始数据结构。默认为False. 278 | encoder # 使用此 JSON 编码器序列化输入对象。默认为None. 279 | 特殊的字段类型 280 | HiddenField() # 隐藏字段,隐藏后验证器不可见、不可修改,例如 serializers.HiddenField(default=timezone.now) 一般情况会给默认值用于入库的数据初始化 281 | 它不根据用户输入获取值,而是从默认值或可调用值中获取值。 282 | 通常仅当HiddenField您需要基于某些预先提供的字段值运行一些验证时才需要该类,但您不希望将所有这些字段公开给最终用户。 283 | 注意:被HiddenField处理的字段,将会对前端无感,前端在增删改查时都将无感该字段 284 | 注意:HiddenField 工作在创建数据时,只有POST时才有效 285 | 注意:如果对同一个字段使用HiddenField和read_only_fields,只有HiddenField有效 286 | SerializerMethodField(method_name=None) # 序列化器方法字段,这个字段用于序列化器返回数据时,可以对某些字段进行加工后再返回,可以用来追加返回的字段。 287 | method_name # 指定利用的函数名称,如果不传,则默认为get_. 288 | 示例 new_name = SerializerMethodField() 此时需要在对应的类中存在一个名为get_new_name的方法,并且返回了合适的值。 289 | 注意:SerializerMethodField 只在返回数据时有效,也就是工作在 GET 请求的场景。 290 | 291 | 基于模型的序列化器元类选项Option 292 | model = User # 用于指定模型 293 | fields = '__all__' or ('id', 'name') # 用于指定哪些字段可以操作,__all__表示所有字段 294 | 注意:如果启用了fields,并且存在SerializerMethodField,要保证SerializerMethodField定义的字段包含在fields中 295 | exclude = ('id', ) # 表示禁止序列化的字段,和fields左右是相反的。直接无视该字段的增删改查,一般用于字段被数据库或框架管理,例如是否被删除的标志字段。 296 | 注意:模型序列化器中 fields和exclude必须选择其中之一 297 | read_only_fields = ('age', ) # 表示只读的字段,和字段中read_only=True效果一样,好处时可以统一设定只读字段。 298 | 注意:已editable=False设置的模型字段,和AutoField字段默认设置为只读,不需要添加到read_only_fields选项中。 299 | 注意:只读的字段可以在后头validate中用代码赋值。 300 | depth = 2 # 表示对外键和嵌套关系的处理,例如有外键指向别的模型或别的模型指向自己,可通过depth快速进行层次序列化,缺点是无法指定字段,造成浪费和无法控制 301 | 因此建议自行实现管理数据的序列化。后面会给出实例 302 | 303 | 4、信号 304 | 什么是信号: 305 | Django有一个 信号调度器(signal dispatcher),用来帮助解耦的应用获知框架内任何其他地方发生了操作。 306 | 简单地说,信号允许某些 发送器 去通知一组 接收器 某些操作发生了。当许多代码段都可能对同一事件感兴趣时,信号特别有用。 307 | Django 提供了 内置信号集 使用户代码能够获得 Django 自身某些操作的通知。其中包括一些有用的通知: 308 | django.db.models.signals.pre_save & django.db.models.signals.post_save 309 | 一个模型的 save() 方法被调用之前或之后发出。 310 | 311 | django.db.models.signals.pre_delete & django.db.models.signals.post_delete 312 | 一个模型的 delete() 方法或查询结果集的 delete() 方法被调用之前或之后发出。 313 | 314 | django.db.models.signals.m2m_changed 315 | 一个模型的 ManyToManyField 更改后发出。 316 | 317 | django.core.signals.request_started & django.core.signals.request_finished 318 | Django 发起或结束一个 HTTP 请求后发出。 319 | 详细的信号文档参考:https://docs.djangoproject.com/zh-hans/3.2/topics/signals/ 320 | 内置信号文档:https://docs.djangoproject.com/zh-hans/3.2/ref/signals/ 321 | 定义和发送信号:https://docs.djangoproject.com/zh-hans/3.2/topics/signals/#defining-and-sending-signals 322 | 323 | 5、如何使用自定义缓存 324 | 什么是自定义缓存: 325 | 本项目实现了一个动态缓存装饰器,可以用来装饰视图函数,并写了一个基于缓存的viewset类,可以实现接口完全基于缓存,获取数据时只有首次是查库,剩余的都是存在缓存中,只有发生变更时才会更新缓存。 326 | 实现方案: 327 | 1、对所有请求加锁,并且进行读写分离加锁,互斥锁,当有读请求时,会阻塞写操作,同读不阻塞。 328 | 2、第一次请求后,会根据请求的参数生成一个指纹,会将这个请求缓存起来,直到发生写操作才将缓存失效,失效后,当新的查询请求结束后会设置上新的缓存。如此往复。 329 | 3、对于CRUD操作,可以直接继承内置的MyViewSet来实现。对于自定义的接口,可以通过缓存装饰器来实现。 330 | 331 | 332 | # 针对自定义缓存的示例: 333 | from extensions.MyCacheViewset import MyModelViewSet 334 | class UserViewSet(MyModelViewSet): # 基于自定义缓存的ViewSet类 335 | ''' 336 | partial_update: 更新指定ID的用户,局部更新 337 | create: 创建用户 338 | retrieve: 检索指定ID的用户 339 | update: 更新指定ID的用户 340 | destroy: 删除指定ID的用户 341 | list: 获取用户列表 342 | ''' 343 | queryset = User.objects.filter() 344 | serializer_class = UserViewsetSerializer 345 | authentication_classes = (JwtAuthentication, ) 346 | permission_classes = (AllowAny, ) 347 | throttle_classes = (VisitThrottle, ) 348 | pagination_class = Pagination 349 | filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) 350 | search_fields = ('name', 'desc') # 注意 要针对有索引的字段进行搜索 351 | # filterset_fields = ('status', ) 352 | ordering_fields = ('id', 'create_timestamp', 'update_timestamp', 'sort_timestamp') 353 | is_public = True # 针对自定义缓存的一个配置项,如果为真表示这个是公共接口不会根据用户id来设置缓存,否则会根据用户id来设置缓存key 354 | 355 | def get_serializer_class(self): 356 | if self.action == 'create': 357 | return CreateUserViewsetSerializer 358 | if self.action in {'update', 'partial_update'}: 359 | return UpdateUserViewsetSerializer 360 | return ReturnUserViewsetSerializer 361 | 362 | class GetAllEnumDataView(GenericAPIView): 363 | authentication_classes = (JwtAuthentication, ) 364 | permission_classes = (AllowAny, ) 365 | throttle_classes = (VisitThrottle, ) 366 | 367 | @swagger_auto_schema(responses={200: GetAllEnumDataResponse}) 368 | @RedisCacheForDecoratorV1('r') 369 | def get(self, request): 370 | '''针对自定义的视图方法使用自定义的缓存装饰器类''' 371 | res = MyJsonResponse(res_data={'msg': gettext_lazy('测试成功')}) 372 | pass 373 | return res.data 374 | 375 | 376 | # 针对序列化器的详细示例: 377 | from rest_framework import serializers 378 | def get_ObjectFlow(type): 379 | ''' 380 | 返回对象 381 | ''' 382 | if type == 0: 383 | approval_flow = TableClass.objects.filter(flow_name='请假审批').first() 384 | elif type == 1: 385 | approval_flow = TableClass.objects.filter(flow_name='出差审批').first() 386 | else: 387 | return None 388 | if approval_flow: 389 | object_flow = TableClass() 390 | return object_flow 391 | else: 392 | return None 393 | 394 | class CurrentUser(object): 395 | ''' 396 | 返回指定的值 397 | 使用该方法可以做一些后端数据自动计算的工作 398 | ''' 399 | def set_context(self, serializer_field): 400 | self.user_obj = serializer_field.context['request'].user 401 | 402 | def __call__(self): 403 | return self.user_obj 404 | 405 | class AddUserSerializer(serializers.ModelSerializer): 406 | # 在源头上防止出现重复数据 407 | name = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False, 408 | validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")]) 409 | # 防止出现重复数据的同时,对数据进行验证 410 | mobile = serializers.CharField(label="手机号", help_text="手机号", required=True, allow_blank=False, 411 | validators=[UniqueValidator(queryset=User.all_objects.all(), message="手机号已经存在")], 412 | error_messages={"required": '手机号不能为空', 'blank': '手机号不能为空', 'null': '手机号不能为空'}) 413 | # 将一个外键设为只读,防止前端传入值进来修改 414 | object_flow = serializers.PrimaryKeyRelatedField(read_only=True) 415 | # 在执行新增操作的时候,在前端隐藏user的传入,后端默认使用当前用户插入 416 | # 第二种方法 user = serializers.HiddenField(default=serializers.CurrentUserDefault(), label='用户') 其实就是restframwork实现了CurrentUser 417 | user = serializers.HiddenField(default=CurrentUser()) 418 | # 指定这个字段在返回给前端之前,也就是序列化时 可以按照指定的函数进行序列化 419 | order_int = serializers.SerializerMethodField(label='订单数') 420 | # 当有模型的外键指向当前模型时,可以通过related_name反向插入所有关联的子数据 接收的子的Serializer 421 | oilcan_datas = DepotUseOilcanDataSerializer(read_only=True, many=True) # 通过related_name和模型的序列化器来返回序列化的反向数据集,也可以通过函数字段自行实现,父找子 422 | # 将对应的外键通过 父Serializer序列化后返回给前端 423 | depart = ReturnDepartSerializer() # 通过外键和外键模型的序列化器来返回序列化的外键数据,也可以通过函数字段自行实现,子找父 424 | # 用于指定日期的输入格式、返回格式,注意可以在settings中统一设置后,可以省略这里的代码 425 | start_date = serializers.DateTimeField(label='开始日期', format='%Y-%m-%d %H:%M:%S', input_formats=['%Y-%m-%d %H:%M:%S', '%Y-%m-%d'], required=False, allow_null=True) 426 | 427 | class Meta: 428 | model = User 429 | fields = '__all__' # __all__用于序列化所有字段 or exclude = ('deleted',)指定,某些字段不被序列化返回 or fields = ['company','username',]指定序列化的字段 430 | read_only_fields = ('user', 'object_flow',) # 指定只读的字段,这样就不会被前端修改 431 | validators = [UniqueTogetherValidator(queryset=Auth.objects.all(), fields=['auth_type',], message='该权限已经存在')] # 多字段联合唯一 432 | 433 | def validate(self, attrs): 434 | # 重写验证器 435 | now_user = self.context['request'].user 436 | # 查看前端传来的所有数据 437 | print('查看attrs:', attrs) 438 | # 查看前端是否有通过pk检索数据 来做出相应的改变 439 | print('查看pk:', self.context['view'].kwargs.get('pk')) 440 | # 在这里可以获取body参数 441 | print('查看body参数:', self.context['request'].data) 442 | flow_obj = get_ObjectFlow(0) 443 | if not flow_obj: 444 | raise serializers.ValidationError("审批流不存在") 445 | attrs['object_flow'] = flow_obj 446 | return attrs 447 | 448 | def validate_name(self, value): 449 | try: 450 | # 用于独立验证name字段 451 | # do something 452 | return value 453 | except Exception as e: 454 | raise serializers.ValidationError(e) 455 | 456 | @transaction.atomic 457 | def update(self, instance, validated_data): 458 | # 重写更新方法 459 | obj = super().update(instance, validated_data) 460 | # do something 461 | return obj 462 | 463 | @transaction.atomic 464 | def create(self, validated_data): 465 | # 重写创建方法 466 | obj = super().create(validated_data) 467 | # do something 468 | return obj 469 | 470 | def get_order_int(self, obj): 471 | # 这种方法只会在返回时被调用 472 | # 在这里可以通过GET参数 做一些返回操作 注意:只有在使用viewset里面才有效 473 | print('查看GET参数:', self.context['request'].query_params) 474 | num = 0 475 | return num 476 | 总结: 477 | 1、隐藏字段和只读字段的区别 478 | 隐藏字段会将字段完全隐藏,可以设置默认值,但是默认值前端不可见,例如在创建数据时 创建者 字段是一个隐藏字段,默认值是当前调用者,此时数据创建成功后,该字段也不会返回给用户。 479 | 只读字段会禁止前端传入数据给字段赋值,赋值可以在后台完成,这样的好处是被操作的字段可以在数据创建成功后返回给用户。 480 | 隐藏字段只在创建数据时有效,只读字段在数据创建和修改时都有效。 481 | 2、前端新增或修改时 即不需要设置也不需要返回 使用 exclude 包含进去 482 | 前端新增时 即不需要设置也不需要返回 但需要后端设置值的 使用 HiddenField 因为 HiddenField 只在新增时有效 483 | 前端新增或修改时 不需要设置但需要返回的 使用 read_only_fields 包含进去 484 | 2、单独验证字段和在validate中统一验证的区别 485 | 单独验证字段,可以进行同时验证,被验证的字段会被统一返回验证信息 486 | 在validate中统一验证,存在先后顺序,先验证的字段会触发异常,并返回验证信息 487 | 488 | 489 | # 针对ViewSet的相关示例: 490 | from rest_framework.viewsets import ModelViewSet 491 | class UserViewSet(ModelViewSet): 492 | ''' 493 | 更新指定ID的用户,局部更新 494 | create: 创建用户 495 | retrieve: 检索指定ID的用户 496 | update: 更新指定ID的用户 497 | destroy: 删除指定ID的用户 498 | list: 获取用户列表 499 | ''' 500 | # 指定使用的QuerySet 501 | queryset = User.objects.filter() 502 | # 指定使用的序列化器 503 | serializer_class = UserViewsetSerializer 504 | # 指定使用的认证类 505 | authentication_classes = (JwtAuthentication, ) 506 | # 指定使用的权限类 507 | permission_classes = (AllowAny, ) 508 | # 指定使用的频率限制类 509 | throttle_classes = (VisitThrottle, ) 510 | # 指定使用的分页类 511 | pagination_class = Pagination 512 | # 指定搜索、过滤、排序使用的类 513 | filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) 514 | # 指定支持搜索的字段 515 | search_fields = ('name', 'desc') # 注意 尽量针对有索引的字段进行搜索 516 | # 指定支持过滤的字段,搜索和过滤的区别:搜索是部分匹配,过滤是全匹配 517 | filterset_fields = ('status', ) 518 | # filterset_class = YourFilter # 如果使用了filter类的写法,和filterset_fields只能存在其中一个 519 | # 指定支持排序的字段,注意 尽量使用有索引的字段进行排序 520 | ordering_fields = ('id', 'create_timestamp', 'update_timestamp', 'sort_timestamp') 521 | 522 | def get_serializer_class(self): 523 | '''重写返回序列化的方法,可以根据自己的需求动态的返回序列化器类''' 524 | if self.action in ['create']: 525 | return AddUserSerializer 526 | if self.action in ['update', 'partial_update']: 527 | return UpdateUserSerializer 528 | return ReturnUserSerializer 529 | 530 | # 针对Swagger可以指定描述和tag,可以提现在Swagger文档中 531 | @swagger_auto_schema(operation_description="创建用户", operation_summary="创建用户") 532 | def create(self, request, *args, **kwargs): 533 | '''重写创建对象的类,可以在这里进行自定义,一般这里需要自定义的逻辑可以转移到序列化中,重写序列化器的创建方法''' 534 | obj = super().create(request, *args, **kwargs) 535 | return obj 536 | 537 | def get_queryset(self): 538 | '''重写get_queryset,这样就可以根据不同的情况返回不同的QuerySet''' 539 | if self.request.auth: 540 | return User.objects.filter(id=self.request.user.id) 541 | return None 542 | 543 | def list(self, request, *args, **kwargs): 544 | '''重写返回数据方法,可以自定义实现,本例使接口返回的数据是一个对象而不是数组''' 545 | instance = User.objects.filter(id=self.request.user.id).first() 546 | serializer = self.get_serializer(instance) 547 | return Response(serializer.data) 548 | 549 | def destroy(self, request, *args, **kwargs): 550 | '''重写删除数据的方法,在这里可以实现自己的逻辑,比如删除数据时校验是否支持删除''' 551 | res = super().destroy(request, *args, **kwargs) 552 | return res 553 | 554 | def update(self, request, *args, **kwargs): 555 | '''重写更新数据的方法,一般这里需要自定义的逻辑可以转移到序列化中,重写序列化器的更新方法''' 556 | res = super().update(request, *args, **kwargs) 557 | return res 558 | 559 | ModelViewSet本质上就是 560 | from rest_framework import mixins 561 | from rest_framework.viewsets import GenericViewSet 562 | class ModelViewSet(mixins.CreateModelMixin, 563 | mixins.RetrieveModelMixin, 564 | mixins.UpdateModelMixin, 565 | mixins.DestroyModelMixin, 566 | mixins.ListModelMixin, 567 | GenericViewSet): 568 | """ 569 | A viewset that provides default `create()`, `retrieve()`, `update()`, 570 | `partial_update()`, `destroy()` and `list()` actions. 571 | """ 572 | pass 573 | 因此如果你的接口可以按需继承不同的类,来实现需要的请求方式。 574 | 例如现在需要一个接口通过用户的token返回用户信息,就可以这么写: 575 | class OwnerUserInfoViewset(mixins.ListModelMixin, GenericViewSet): 576 | ''' 577 | list: 获取自己的用户信息 578 | ''' 579 | serializer_class = OwnerUserViewsetSerializer 580 | authentication_classes = (JwtAuthentication, ) 581 | permission_classes = (IsAuthPermission, ) 582 | throttle_classes = (VisitThrottle, ) 583 | 584 | def get_queryset(self): 585 | '''重写get_queryset,用来返回目标用户的数据,因为在token验证那里已经确定了用户是否存在''' 586 | if self.request.auth: 587 | return User.objects.filter(id=self.request.user.id) 588 | return None 589 | 590 | def list(self, request, *args, **kwargs): 591 | '''重写list方法,使接口返回的数据是一个对象而不是数组''' 592 | instance = User.objects.filter(id=self.request.user.id).first() 593 | serializer = self.get_serializer(instance) 594 | return Response(serializer.data) 595 | 596 | 597 | # 针对过滤类的相关示例: 598 | # 详细的官方文档:https://django-filter.readthedocs.io/en/main/ 599 | from django_filters.rest_framework import FilterSet, CharFilter 600 | from django.db.models import Q 601 | from django.db import transaction 602 | class MyFilter(FilterSet): 603 | ''' 604 | 过滤类 605 | ''' 606 | my_type = CharFilter(field_name='my_type', method='return_my_type', label='自定义类型过滤') 607 | filter_date = CharFilter(field_name='filter_date', method='return_filter_date', label='自定义时间段过滤') 608 | 609 | class Meta: 610 | model = YourModel 611 | fields = ['status', ] # 原本支持的过滤字段 612 | 613 | def return_my_type(self, queryset, name, value): 614 | logging.debug(f'查看传递的值:{value}') 615 | if value == '0': 616 | return queryset.filter(Q(my_type=0) | Q(my_type=2) | Q(my_type=4)) 617 | elif value == '1': 618 | return queryset.filter(Q(my_type=1) | Q(my_type=3) | Q(my_type=5)) 619 | else: 620 | return queryset.all() 621 | 622 | def return_filter_date(self, queryset, name, value): 623 | logging.debug(f'查看传递的值:{value}') 624 | if ',' not in value: 625 | return queryset.all() 626 | else: 627 | date_list = value.split(',') 628 | start_date = datetime.strptime(date_list[0], '%Y-%m-%d') 629 | end_date = datetime.strptime(date_list[1] + ' 23:59:59', '%Y-%m-%d %H:%M:%S') 630 | return queryset.filter(update_time__gt=start_date, update_time__lt=end_date) 631 | 632 | 633 | # 针对signals的相关示例: 634 | import logging 635 | from django.db.models.signals import pre_save, post_save, pre_delete, post_delete 636 | from django.dispatch import receiver 637 | from django.db import transaction 638 | 639 | @transaction.atomic 640 | @receiver(pre_save, sender=YourModel) 641 | def pre_save_object(sender, instance=None, **kwargs): 642 | '''接收模型调用save前的信号 643 | args: 644 | sender Model:发送信号的模型类 645 | instance object:实际被保存的实例 646 | ''' 647 | logging.info('即将被保存的对象:{}'.format(instance)) 648 | 649 | @transaction.atomic 650 | @receiver(post_save, sender=YourModel) 651 | def create_update_object(sender, instance=None, created=False, **kwargs): 652 | '''接收模型调用save后的信号 653 | args: 654 | sender Model:发送信号的模型类 655 | instance object:实际被保存的实例 656 | created bool:是否是创建数据 657 | ''' 658 | if created: 659 | logging.info('新增时:{}'.format(instance)) 660 | else: 661 | logging.info('修改时:{}'.format(instance)) 662 | 663 | @transaction.atomic 664 | @receiver(pre_delete, sender=YourModel) 665 | def pre_delete_object(sender, instance=None, **kwargs): 666 | '''接收模型调用delete前的信号 667 | args: 668 | sender Model:发送信号的模型类 669 | instance object:实际被保存的实例 670 | ''' 671 | logging.info('即将删除的对象:{}'.format(instance)) 672 | 673 | @transaction.atomic 674 | @receiver(post_delete, sender=YourModel) 675 | def delete_object(sender, instance=None, **kwargs): 676 | '''接收模型调用delete后的信号 677 | args: 678 | sender Model:发送信号的模型类 679 | instance object:实际被保存的实例 680 | ''' 681 | logging.info('已经被删除的对象:{}'.format(instance)) 682 | 683 | # 针对urls的相关示例: 684 | from django.urls import path, include 685 | from rest_framework.routers import DefaultRouter 686 | from .views import YourViewSet, YourView 687 | 688 | router = DefaultRouter() 689 | router.register(r'yourpath', YourViewSet, basename="ViewSetPath") 690 | urlpatterns = [ 691 | path('', include(router.urls)), 692 | path('yourpath/', YourView.as_view(), name='ViewPath'), 693 | ] -------------------------------------------------------------------------------- /gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | 2 | name = 'drfAPI' 3 | wsgi_app = 'drfAPI.asgi:application' 4 | bind = '127.0.0.1:8080' 5 | workers = 2 6 | threads = 2 7 | worker_class = 'uvicorn.workers.UvicornWorker' 8 | worker_connections = 2000 # 协程并发模式下还存在一个并发文件日志写入的问题,就是并发时会导致日志文件重写、混乱写入等问题,待解决 9 | # 将日志全部交给框架内的日志输出处理 使用uvicorn worker 还是存在一些问题,日志无法按照预期的输出,wsgi模式下无影响。 10 | # accesslog = f'./logs/{name}_access.log' 11 | # access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' 12 | # errorlog = f'./logs/{name}_error.log' 13 | loglevel = 'error' -------------------------------------------------------------------------------- /i18n/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-10-09 10:01+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: apps/public/views.py:101 22 | msgid "测试成功" 23 | msgstr "test success" 24 | 25 | #: drfAPI/settings.py:144 26 | msgid "英语" 27 | msgstr "English" 28 | 29 | #: drfAPI/settings.py:145 30 | msgid "简体中文" 31 | msgstr "Simplified Chinese" 32 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/logs/.gitkeep -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'drfAPI.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /my_app_template/__init__.py-tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/my_app_template/__init__.py-tpl -------------------------------------------------------------------------------- /my_app_template/admin.py-tpl: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | # Register your models here. 5 | -------------------------------------------------------------------------------- /my_app_template/apps.py-tpl: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class {{ camel_case_app_name }}Config(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.{{ app_name }}' 7 | 8 | def ready(self): 9 | import apps.{{ app_name }}.signals -------------------------------------------------------------------------------- /my_app_template/filters.py-tpl: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from datetime import datetime 4 | from django.db.models import Q, F 5 | from django.db import transaction 6 | from django_filters.rest_framework import FilterSet, CharFilter 7 | 8 | 9 | # create your filters here 10 | -------------------------------------------------------------------------------- /my_app_template/models.py-tpl: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MaxValueValidator, MinValueValidator 3 | from extensions.BaseModel import BaseModel 4 | 5 | 6 | # Create your models here. 7 | -------------------------------------------------------------------------------- /my_app_template/serializers.py-tpl: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.db.models import F, Q, Count, Sum, Max, Min 13 | from django.contrib.auth.hashers import check_password, make_password 14 | from rest_framework import serializers 15 | from rest_framework.serializers import SerializerMethodField, ModelSerializer 16 | from rest_framework.validators import UniqueValidator, UniqueTogetherValidator 17 | from extensions.BaseSerializer import BaseModelSerializer 18 | from .models import * 19 | # from .tasks import * 20 | 21 | 22 | -------------------------------------------------------------------------------- /my_app_template/signals.py-tpl: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.dispatch import receiver 13 | from django.db.models.signals import pre_save, post_save, pre_delete, post_delete 14 | from .models import * 15 | 16 | 17 | # create your signals here 18 | -------------------------------------------------------------------------------- /my_app_template/tasks.py-tpl: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from django.conf import settings 10 | from django.db import transaction 11 | from django.core.cache import caches 12 | from django.forms.models import model_to_dict 13 | from celery import shared_task 14 | from drfAPI.celery import app 15 | 16 | 17 | # create your async task here 18 | 19 | # @app.task(bind=True) 20 | # def async_task(self, *args, **kwargs): 21 | # try: 22 | # pass 23 | # except Exception as e: 24 | # logging.error("async task has error {}".fromat(e)) 25 | # logging.exception(e) 26 | # # 执行失败重试,本例设置3分钟后重试 27 | # self.retry(countdown=60 * 3, exc=e) -------------------------------------------------------------------------------- /my_app_template/tests.py-tpl: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | # Create your tests here. 5 | -------------------------------------------------------------------------------- /my_app_template/urls.py-tpl: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter -------------------------------------------------------------------------------- /my_app_template/views.py-tpl: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import time 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from datetime import datetime, timedelta 9 | from rest_framework.views import APIView 10 | from rest_framework import status, mixins 11 | from rest_framework.response import Response 12 | from rest_framework.permissions import AllowAny 13 | from rest_framework.generics import GenericAPIView 14 | from rest_framework.filters import SearchFilter, OrderingFilter 15 | from rest_framework.viewsets import ModelViewSet, GenericViewSet 16 | from django.conf import settings 17 | from django.db import transaction 18 | from django.core.cache import caches 19 | from django.forms.models import model_to_dict 20 | from django.http.response import HttpResponseNotFound 21 | from django.db.models import F, Q, Count, Sum, Max, Min 22 | from django.contrib.auth.hashers import check_password, make_password 23 | from django_filters.rest_framework import DjangoFilterBackend 24 | from drf_yasg.utils import swagger_auto_schema 25 | from extensions.JwtToken import JwtToken 26 | from extensions.Pagination import Pagination 27 | from extensions.Throttle import VisitThrottle 28 | from extensions.MyResponse import MyJsonResponse 29 | from extensions.JwtAuth import JwtAuthentication 30 | from extensions.Permission import IsAuthPermission 31 | from .models import * 32 | from .serializers import * 33 | # from .tasks import * 34 | # from .filters import * 35 | # 不建议导入所有,建议按需导入 36 | 37 | 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.2.15 2 | djangorestframework==3.12.4 3 | djangorestframework-simplejwt==5.2.0 4 | django-filter==22.1 5 | django-cors-headers==3.13.0 6 | drf-yasg==1.21.3 7 | django-redis==5.2.0 8 | mysqlclient==2.1.1 9 | redis==4.3.4 10 | jinja2==3.1.2 11 | requests==2.28.1 12 | urllib3==1.26.12 13 | pyjwt==2.4.0 14 | PyYAML==6.0 15 | lxml==4.9.1 16 | Markdown==3.4.1 17 | pycrypto==2.6.1 18 | six==1.16.0 19 | pillow==9.2.0 20 | celery==5.2.7 21 | django-celery-beat==2.3.0 22 | django-celery-results==2.4.0 23 | django-haystack==3.1.1 24 | drf-haystack==1.8.11 25 | whoosh==2.7.4 26 | imgkit==1.2.2 27 | pdfkit==1.0.0 28 | pandas==1.4.0 29 | numpy==1.23.3 30 | prettytable==3.4.1 31 | pyecharts==1.9.1 32 | simplejson==3.17.6 33 | BeautifulSoup4==4.11.0 34 | uwsgi==2.0.20 35 | gunicorn==20.1.0 36 | uvicorn==0.18.3 37 | gevent==20.12.1 -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | current_dir=$(pwd $(readlink -f $0)) 3 | proj_name="drfAPI" 4 | wsgi_patch="${current_dir}/${proj_name}/wsgi.py" 5 | port=8080 6 | # 获取web服务进程 7 | pid_list=`lsof -i:${port} | grep -v PID | awk '{print $2}'` 8 | # 获取Celery worker进程 9 | celery_list=`ps -ef | grep "${proj_name} worker" | grep -v grep | awk '{print $2}'` 10 | # 获取Celery beat进程 11 | beat_list=`ps -ef | grep "${proj_name} beat" | grep -v grep | awk '{print $2}'` 12 | # 设置传参的默认值 13 | arg1=${1:-help} 14 | arg2=${2:-dev} 15 | if [ $# != 0 ] 16 | then 17 | if [ $arg1 == "start" ] 18 | then 19 | echo "项目地址:${current_dir}" 20 | echo "web服务绑定的端口:${port}" 21 | if [ $arg2 == "prod" ] 22 | then 23 | echo "启动生产环境..." 24 | export ENV="prod" 25 | else 26 | echo "启动开发环境..." 27 | export ENV="dev" 28 | fi 29 | if [ "$pid_list" ] 30 | then 31 | kill -9 ${pid_list} 32 | `rm -f ${current_dir}/logs/${proj_name}.log` 33 | `sleep 1` 34 | fi 35 | `uwsgi --chdir ${current_dir} --wsgi-file ${wsgi_patch} --socket 127.0.0.1:${port} uwsgi.ini` 36 | echo "web服务启动成功..." 37 | if [ "$celery_list" ] 38 | then 39 | kill -9 ${celery_list} 40 | `sleep 1` 41 | fi 42 | `nohup celery -A ${proj_name} worker -l info > ${current_dir}/logs/celery.log 2>&1 &` 43 | echo "celery服务启动成功..." 44 | if [ "$beat_list" ] 45 | then 46 | kill -9 ${beat_list} 47 | `sleep 1` 48 | fi 49 | `nohup celery -A ${proj_name} beat -l info > ${current_dir}/logs/beat.log 2>&1 &` 50 | echo "celery beat服务启动成功..." 51 | # tail -f /dev/null 52 | elif [ $arg1 == 'stop' ] 53 | then 54 | if [ "$pid_list" ] 55 | then 56 | kill -9 ${pid_list} 57 | `rm -f ${current_dir}/logs/${proj_name}.log` 58 | echo "web服务停止成功..." 59 | else 60 | echo "web服务未启动,无需停止..." 61 | fi 62 | if [ "$celery_list" ] 63 | then 64 | kill -9 ${celery_list} 65 | echo "celery服务停止成功..." 66 | else 67 | echo "celery服务未启动,无需停止..." 68 | fi 69 | if [ "$beat_list" ] 70 | then 71 | kill -9 ${beat_list} 72 | echo "celery beat服务停止成功..." 73 | else 74 | echo "celery beat服务未启动,无需停止..." 75 | fi 76 | elif [ $arg1 == 'show' ] 77 | then 78 | if [ "$pid_list" ] 79 | then 80 | echo "web服务运行中..." 81 | echo "进程信息:" 82 | pid=`lsof -i:${port} | grep -v PID | head -1 | awk '{print $2}'` && if [ "${pid}" ];then ps -ef | grep ${pid} | grep -v grep;fi 83 | echo "监听的tcp:" 84 | lsof -i:${port} 85 | else 86 | echo "web服务未启动..." 87 | fi 88 | if [ "$celery_list" ] 89 | then 90 | echo "celery服务运行中..." 91 | echo "进程信息:" 92 | ps -ef | grep "${proj_name} worker" | grep -v grep 93 | else 94 | echo "celery服务未启动..." 95 | fi 96 | if [ "$beat_list" ] 97 | then 98 | echo "celery beat服务运行中..." 99 | echo "进程信息:" 100 | ps -ef | grep beat | grep -v grep 101 | else 102 | echo "celery beat服务未启动..." 103 | fi 104 | else 105 | echo "start:开启(or 重启)服务。" 106 | echo "stop:停止服务。" 107 | echo "show:显示详情。" 108 | echo "help:帮助信息。" 109 | fi 110 | else 111 | echo "参数错误,可以尝试命令:bash server.sh help" 112 | fi -------------------------------------------------------------------------------- /sources.list: -------------------------------------------------------------------------------- 1 | deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free 2 | # deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free 3 | deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free 4 | # deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free 5 | deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free 6 | # deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free 7 | deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free 8 | # deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free -------------------------------------------------------------------------------- /start-asgi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | current_dir=$(pwd $(readlink -f $0)) 3 | proj_name="drfAPI" 4 | wsgi_patch="${current_dir}/${proj_name}/wsgi.py" 5 | asgi_app="${proj_name}.asgi:application" 6 | port=8080 7 | # 获取web服务进程 8 | pid_list=`lsof -i:${port} | grep -v PID | awk '{print $2}'` 9 | # 获取Celery worker进程 10 | celery_list=`ps -ef | grep "${proj_name} worker" | grep -v grep | awk '{print $2}'` 11 | # 获取Celery beat进程 12 | beat_list=`ps -ef | grep "${proj_name} beat" | grep -v grep | awk '{print $2}'` 13 | # 设置传参的默认值 14 | arg1=${1:-help} 15 | arg2=${2:-dev} 16 | if [ $# != 0 ] 17 | then 18 | if [ $arg1 == "start" ] 19 | then 20 | echo "项目地址:${current_dir}" 21 | echo "web服务绑定的端口:${port}" 22 | if [ $arg2 == "prod" ] 23 | then 24 | echo "启动生产环境..." 25 | export ENV="prod" 26 | else 27 | echo "启动开发环境..." 28 | export ENV="dev" 29 | fi 30 | if [ "$pid_list" ] 31 | then 32 | kill -9 ${pid_list} 33 | `rm -f ${current_dir}/logs/${proj_name}.log` 34 | `sleep 1` 35 | fi 36 | `gunicorn ${asgi_app} -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:${port}` 37 | echo "web服务启动成功..." 38 | if [ "$celery_list" ] 39 | then 40 | kill -9 ${celery_list} 41 | `sleep 1` 42 | fi 43 | `nohup celery -A ${proj_name} worker -l info > ${current_dir}/logs/celery.log 2>&1 &` 44 | echo "celery服务启动成功..." 45 | if [ "$beat_list" ] 46 | then 47 | kill -9 ${beat_list} 48 | `sleep 1` 49 | fi 50 | `nohup celery -A ${proj_name} beat -l info > ${current_dir}/logs/beat.log 2>&1 &` 51 | echo "celery beat服务启动成功..." 52 | # tail -f /dev/null 53 | elif [ $arg1 == 'stop' ] 54 | then 55 | if [ "$pid_list" ] 56 | then 57 | kill -9 ${pid_list} 58 | `rm -f ${current_dir}/logs/${proj_name}.log` 59 | echo "web服务停止成功..." 60 | else 61 | echo "web服务未启动,无需停止..." 62 | fi 63 | if [ "$celery_list" ] 64 | then 65 | kill -9 ${celery_list} 66 | echo "celery服务停止成功..." 67 | else 68 | echo "celery服务未启动,无需停止..." 69 | fi 70 | if [ "$beat_list" ] 71 | then 72 | kill -9 ${beat_list} 73 | echo "celery beat服务停止成功..." 74 | else 75 | echo "celery beat服务未启动,无需停止..." 76 | fi 77 | elif [ $arg1 == 'show' ] 78 | then 79 | if [ "$pid_list" ] 80 | then 81 | echo "web服务运行中..." 82 | echo "进程信息:" 83 | pid=`lsof -i:${port} | grep -v PID | head -1 | awk '{print $2}'` && if [ "${pid}" ];then ps -ef | grep ${pid} | grep -v grep;fi 84 | echo "监听的tcp:" 85 | lsof -i:${port} 86 | else 87 | echo "web服务未启动..." 88 | fi 89 | if [ "$celery_list" ] 90 | then 91 | echo "celery服务运行中..." 92 | echo "进程信息:" 93 | ps -ef | grep "${proj_name} worker" | grep -v grep 94 | else 95 | echo "celery服务未启动..." 96 | fi 97 | if [ "$beat_list" ] 98 | then 99 | echo "celery beat服务运行中..." 100 | echo "进程信息:" 101 | ps -ef | grep beat | grep -v grep 102 | else 103 | echo "celery beat服务未启动..." 104 | fi 105 | else 106 | echo "start:开启(or 重启)服务。" 107 | echo "stop:停止服务。" 108 | echo "show:显示详情。" 109 | echo "help:帮助信息。" 110 | fi 111 | else 112 | echo "参数错误,可以尝试命令:bash server.sh help" 113 | fi -------------------------------------------------------------------------------- /startmyapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/startmyapp/__init__.py -------------------------------------------------------------------------------- /startmyapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class StartmyappConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'startmyapp' 7 | -------------------------------------------------------------------------------- /startmyapp/generate_template/serializer.txt: -------------------------------------------------------------------------------- 1 | # 新增{verbose}序列化器 2 | class Create{model_name}ModelSerializer(BaseModelSerializer, ModelSerializer): 3 | class Meta: 4 | model = {model_name} 5 | exclude = ('deleted', ) # or fields = '__all__' or fields = ('field01', 'field01', ) 6 | # read_only_fields = ('field01', ) 7 | 8 | # 修改{verbose}序列化器 9 | class Update{model_name}ModelSerializer(BaseModelSerializer, ModelSerializer): 10 | class Meta: 11 | model = {model_name} 12 | exclude = ('deleted', ) # or fields = '__all__' or fields = ('field01', 'field01', ) 13 | # read_only_fields = ('field01', ) 14 | 15 | # 返回{verbose}序列化器 16 | class Return{model_name}ModelSerializer(BaseModelSerializer, ModelSerializer): 17 | class Meta: 18 | model = {model_name} 19 | exclude = ('deleted', ) # or fields = '__all__' or fields = ('field01', 'field01', ) 20 | # read_only_fields = ('field01', ) -------------------------------------------------------------------------------- /startmyapp/generate_template/url.txt: -------------------------------------------------------------------------------- 1 | from apps.{app_name}.views import {viewsets} 2 | router.register(r'{lower}', {model_name}Viewset, basename='{verbose}接口') # {verbose}接口 -------------------------------------------------------------------------------- /startmyapp/generate_template/view.txt: -------------------------------------------------------------------------------- 1 | # {verbose}ModelViewSet视图 2 | class {model_name}Viewset(ModelViewSet): 3 | ''' 4 | partial_update: 更新指定ID的{verbose},局部更新 5 | create: 创建{verbose} 6 | retrieve: 检索指定ID的{verbose} 7 | update: 更新指定ID的{verbose} 8 | destroy: 删除指定ID的{verbose} 9 | list: 获取{verbose}列表 10 | ''' 11 | queryset = {model_name}.objects.all() 12 | serializer_class = Return{model_name}ModelSerializer 13 | authentication_classes = (JwtAuthentication, ) 14 | permission_classes = (AllowAny, ) # or IsAuthPermission 15 | throttle_classes = (VisitThrottle, ) 16 | pagination_class = Pagination 17 | filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter){search_str}{filter_str} 18 | ordering_fields = ('id', 'create_timestamp', 'update_timestamp', 'sort_timestamp') 19 | 20 | def get_serializer_class(self): 21 | if self.action == 'create': 22 | return Create{model_name}ModelSerializer 23 | if self.action in {{'update', 'partial_update'}}: 24 | return Update{model_name}ModelSerializer 25 | return Return{model_name}ModelSerializer 26 | 27 | def get_queryset(self): 28 | # return {model_name}.objects.all(filter_time=self.request.query_params.get('filter_time')).filter() 29 | if self.request.auth and self.request.user.group_id == 1: 30 | return {model_name}.objects.all() 31 | elif self.request.auth: 32 | return {model_name}.objects.filter(user_id=self.request.user.id) 33 | return {model_name}.objects.filter(id=0) -------------------------------------------------------------------------------- /startmyapp/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/startmyapp/management/__init__.py -------------------------------------------------------------------------------- /startmyapp/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/startmyapp/management/commands/__init__.py -------------------------------------------------------------------------------- /startmyapp/management/commands/_private.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/startmyapp/management/commands/_private.py -------------------------------------------------------------------------------- /startmyapp/management/commands/generatecode.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.conf import settings 3 | from os import system, makedirs 4 | from pathlib import Path 5 | import json 6 | import logging 7 | import sys 8 | 9 | 10 | class Command(BaseCommand): 11 | help = "Command for generate template code." 12 | now_dir = Path(__file__).resolve().parent.parent.parent 13 | generate_template_dir = Path.joinpath(now_dir, 'generate_template') 14 | base_str_path_dic = { 15 | 'serializer': Path.joinpath(generate_template_dir, 'serializer.txt'), 16 | 'url': Path.joinpath(generate_template_dir, 'url.txt'), 17 | 'view': Path.joinpath(generate_template_dir, 'view.txt') 18 | } 19 | 20 | def get_base_str(self, str_type: str) -> str: 21 | with open(self.base_str_path_dic[str_type], 'r') as f: 22 | return f.read() 23 | 24 | def generate_item_code(self, app_path: str, file_name: str, content: str) -> bool: 25 | '''生成文件的方法''' 26 | with open(Path.joinpath(app_path, file_name), 'a') as f: 27 | f.write(content) 28 | return True 29 | 30 | def handle(self, *args, **options): 31 | json_path = Path.joinpath(settings.CONFIG_DIR, 'generateCode.json') 32 | with open(json_path, 'r') as f: 33 | generate_data = json.loads(f.read()) 34 | for data in generate_data: 35 | app_name = data['app_name'] 36 | app_path = Path(Path.joinpath(settings.MY_APPS_DIR, app_name)) 37 | if not app_path.exists(): 38 | raise CommandError("This app(%s) is not exists." % app_name) 39 | # 开启生成代码的流程 40 | print('即将开始生成代码的App名称:{}'.format(app_name)) 41 | models = data.get('models') 42 | w_serializer = [] 43 | w_view = [] 44 | w_url_router = [] 45 | viewsets = [] 46 | w_url_import, base_url_router = self.get_base_str('url').split('\n') 47 | for model_item in models: 48 | model_name = model_item.get('model_name') 49 | verbose = model_item.get('verbose') 50 | base_ser = self.get_base_str('serializer') 51 | w_serializer.append(base_ser.format(model_name=model_name, verbose=verbose)) 52 | if model_item.get('searchs'): 53 | searchs = ["'{}'".format(item) for item in model_item.get('searchs')] 54 | search_str = ", ".join(searchs) if len(searchs) > 1 else "%s, " % searchs[0] 55 | search_str = '''\n search_fields = (%s)''' % search_str 56 | else: 57 | search_str = '' 58 | if model_item.get('filters'): 59 | filters = ["'{}'".format(item) for item in model_item.get('filters')] 60 | filter_str = ", ".join(filters) if len(filters) > 1 else "%s, " % filters[0] 61 | filter_str = '''\n filterset_fields = (%s)''' % filter_str 62 | else: 63 | filter_str = '' 64 | base_view = self.get_base_str('view') 65 | w_view.append(base_view.format(verbose=verbose, model_name=model_name, search_str=search_str, filter_str=filter_str)) 66 | viewsets.append("{}Viewset".format(model_name)) 67 | lower = "{}{}".format(model_name[0].lower(), model_name[1:]) 68 | w_url_router.append(base_url_router.format(lower=lower, model_name=model_name, verbose=verbose)) 69 | 70 | w_url_import = w_url_import.format(app_name=app_name, viewsets=', '.join(viewsets)) 71 | w_w_url_router = "{}\nurlpatterns = [\n path('', include(router.urls)),\n]\n".format('\n'.join(w_url_router)) 72 | self.generate_item_code(app_path, 'serializers.py', '\n\n\n'.join(w_serializer)) 73 | print("App:{} 的序列化器文件生成完毕。".format(app_name)) 74 | self.generate_item_code(app_path, 'views.py', '\n\n\n'.join(w_view)) 75 | print("App:{} 的视图文件生成完毕。".format(app_name)) 76 | self.generate_item_code(app_path, 'urls.py', f'\n{w_url_import}\n') 77 | self.generate_item_code(app_path, 'urls.py', '\n\nrouter = DefaultRouter()\n') 78 | self.generate_item_code(app_path, 'urls.py', w_w_url_router) 79 | print("App:{} 的路由文件生成完毕。".format(app_name)) -------------------------------------------------------------------------------- /startmyapp/management/commands/startmyapp.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.conf import settings 3 | from os import system, makedirs 4 | from pathlib import Path 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Command for creating a custom app." 9 | 10 | def add_arguments(self, parser): 11 | '''为命令添加一个参数,就是要创建app的名称,用来调用内置的startapp的必传参数。''' 12 | parser.add_argument('app_name') 13 | 14 | def handle(self, *args, **options): 15 | '''实际是业务逻辑,根据自己的需要,变更原来的创建新的app的逻辑。''' 16 | app_name = options['app_name'] 17 | app_path = Path(Path.joinpath(settings.MY_APPS_DIR, app_name)) 18 | if app_path.exists(): 19 | raise CommandError("This app(%s) is exists." % app_name) 20 | makedirs(app_path) 21 | cmd = "python manage.py startapp --template={} {} {}".format(settings.MY_APP_TEMPLATE, app_name, app_path) 22 | # 执行实际的创建app的命令 23 | system(cmd) -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/static/.gitkeep -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aeasringnar/django-RESTfulAPI/7353efb2967c2e9ec54e600199eaaead8222c166/templates/.gitkeep -------------------------------------------------------------------------------- /templates/search/indexes/user/user_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.uername }} 2 | {{ object.id }} -------------------------------------------------------------------------------- /utils/Ecb.py: -------------------------------------------------------------------------------- 1 | import time 2 | from base64 import b64decode 3 | from base64 import b64encode 4 | from Crypto.Cipher import AES # pip install pycrypto==2.6.1 5 | from django.conf import settings 6 | 7 | 8 | class ECBCipher(object): 9 | ''' 10 | 定义一个基于AES的ECB模式的加解密类 11 | ''' 12 | def __init__(self, key: str='') -> None: 13 | ''' 14 | 定义构造方法,初始化key和加解密对象 15 | :params key:长度必须为16位 16 | ''' 17 | if not key : key = settings.AES_KEY 18 | if len(key) % 16 != 0: 19 | raise ValueError('key的长度必须为16的倍数。') 20 | self.key = key 21 | self.__cipher = AES.new(self.key.encode(), AES.MODE_ECB) 22 | 23 | def __pad(self, s: str) -> str: # 私有方法 24 | ''' 25 | 定义PKCS7填充的私有方法,用于对目标进行补位填充 26 | ''' 27 | return s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size) 28 | 29 | def __unpad(self, s: str) -> str: 30 | ''' 31 | 定义去除填充的私有方法,用于对目标进行解码得到原始值 32 | ''' 33 | return s[:-ord(s[-1:])] 34 | 35 | def encrypted(self, msg: str) -> str: 36 | ''' 37 | 定义加密方法,对目标进行加密,并返回一个byte类型的字符串 38 | :params msg:需要加密的明文 39 | ''' 40 | try: 41 | return b64encode(self.__cipher.encrypt(self.__pad(msg).encode())).decode() 42 | except: 43 | return None 44 | 45 | def decrypted(self, encode_str: str) -> str: 46 | ''' 47 | 定义解密方法,对目标进行解密,并返回一个解密得到的字符串 48 | :params encode_str:需要解密的密文 49 | ''' 50 | try: 51 | decode_str = self.__unpad(self.__cipher.decrypt(b64decode(encode_str))).decode() 52 | return decode_str if decode_str else None 53 | except: 54 | return None 55 | 56 | 57 | if __name__ == '__main__': 58 | ecb_obj = ECBCipher('16ed9ecc7d9011eab9c63c6aa7c68b67') 59 | encode_text = ecb_obj.encrypted('e2d8fae47d4c11ea942cc8d9d2066a43+%s' % str(int(time.time()))) 60 | decode_text = ecb_obj.decrypted(encode_text) 61 | print('加密:', encode_text) 62 | print('解密:', decode_text) -------------------------------------------------------------------------------- /utils/HtmlToImgPdfKit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | from enum import Enum 5 | from io import BytesIO 6 | from shutil import copy 7 | from tempfile import mkdtemp 8 | from typing import Any, Tuple, Union 9 | from uuid import uuid4 10 | 11 | try: 12 | import imgkit 13 | import pdfkit 14 | except Exception as e: 15 | raise ImportError( 16 | 'imgkit pdftkit not found, pelease run pip install imgkit pdfkit') 17 | 18 | 19 | class DataType(int, Enum): 20 | FILE = 1 21 | STRING = 2 22 | URL = 3 23 | 24 | @classmethod 25 | def keys(cls) -> set: 26 | return set(cls.__members__.keys()) 27 | 28 | @classmethod 29 | def values(cls) -> list: 30 | return list(cls.__members__.values()) 31 | 32 | 33 | class HtmlToImgPdfKit: 34 | 35 | IMGKIT_FUC = { 36 | 1: imgkit.from_file, 37 | 2: imgkit.from_string, 38 | 3: imgkit.from_url 39 | } 40 | 41 | PDFKIT_FUC = { 42 | 1: pdfkit.from_file, 43 | 2: pdfkit.from_string, 44 | 3: pdfkit.from_url 45 | } 46 | 47 | def __init__(self, kit_path: Union[str, None] = None, is_debug: bool = False) -> None: 48 | if kit_path and not os.path.exists(kit_path): 49 | raise FileNotFoundError( 50 | 'kit_path is not file path or not found file') 51 | self.imgkit_conf = imgkit.config( 52 | wkhtmltoimage=kit_path) if kit_path else None 53 | self.pdfkit_conf = pdfkit.configuration( 54 | wkhtmltopdf=kit_path) if kit_path else None 55 | self.default_imgkit_options = { 56 | 'format': 'jpg', 57 | # 'crop-w': '832', # set image weight 58 | 'crop-y': '0', 59 | 'crop-x': '0', 60 | 'encoding': "UTF-8", 61 | } 62 | # options doc https://wkhtmltopdf.org/usage/wkhtmltopdf.txt 63 | self.default_pdfkit_options = { 64 | "encoding": "UTF-8", 65 | # 'margin-top': '0', 66 | # 'margin-right': '0', # Set the page right margin (default 10mm) 67 | # 'margin-bottom': '0', 68 | # 'margin-left': '0', # Set the page left margin (default 10mm) 69 | } 70 | self.debug = is_debug 71 | self.tmp_path = mkdtemp() 72 | 73 | def to_img(self, data: str, data_type: DataType = DataType.FILE, out_path: str = '', options: dict = {}) -> Tuple[bool, Union[BytesIO, None], str]: 74 | '''将HTML生成为图像的方法 75 | args: 76 | data str:传入的数据,可以是文件路径、文本内容、URL,必填。 77 | data_type int:传入的数据类型,可选值为 1 文件地址 2 文本内容 3 URL,必填 78 | out_path str:传入的输出地址,选填。 79 | options dict:传入选项字典,用于设置生成图片的格式,选填。 80 | returns: 81 | (bool, BytesIO/None, str):返回成功标识、文件流/None、异常描述 82 | ''' 83 | img_format = options.get( 84 | 'format', 'jpg') if 'format' in options else self.default_imgkit_options.get('format') 85 | out_path = f"{os.path.splitext(out_path)[0]}.{img_format}" 86 | self.default_imgkit_options.update(options) 87 | tmp_file_path = os.path.join(self.tmp_path, f"{uuid4()}.{img_format}") 88 | if self.debug: 89 | print(tmp_file_path) 90 | try: 91 | if data_type not in DataType.values(): 92 | raise Exception('this data type not found') 93 | handle_fuc = self.IMGKIT_FUC[data_type] 94 | handle_fuc(data, tmp_file_path, 95 | options=self.default_imgkit_options, config=self.imgkit_conf) 96 | if out_path: 97 | copy(tmp_file_path, out_path) 98 | with open(tmp_file_path, 'rb') as f: 99 | return True, BytesIO(f.read()), 'ok' 100 | except Exception as e: 101 | logging.error(e) 102 | logging.exception(e) 103 | return False, None, str(e) 104 | finally: 105 | if os.path.exists(tmp_file_path) and not self.debug: 106 | os.remove(tmp_file_path) 107 | 108 | def to_pdf(self, data: str, data_type: DataType = DataType.FILE, out_path: str = '', options: dict = {}) -> Tuple[bool, Union[BytesIO, None], str]: 109 | '''将HTML生成为PDF的方法 110 | args: 111 | data str:传入的数据,可以是文件路径、文本内容、URL,必填。 112 | data_type int:传入的数据类型,可选值为 1 文件地址 2 文本内容 3 URL,必填 113 | out_path str:传入的输出地址,选填。 114 | options dict:传入选项字典,用于设置生成PDF的格式,选填。 115 | returns: 116 | (bool, BytesIO/None, str):返回成功标识、文件流/None、异常描述 117 | ''' 118 | self.default_pdfkit_options.update(options) 119 | tmp_file_path = os.path.join(self.tmp_path, f"{uuid4()}.pdf") 120 | if self.debug: 121 | print(tmp_file_path) 122 | try: 123 | if data_type not in DataType.values(): 124 | raise Exception('this data type not found') 125 | handle_fuc = self.PDFKIT_FUC[data_type] 126 | handle_fuc(data, tmp_file_path, options=self.default_pdfkit_options, 127 | configuration=self.pdfkit_conf) 128 | if out_path: 129 | copy(tmp_file_path, out_path) 130 | with open(tmp_file_path, 'rb') as f: 131 | return True, BytesIO(f.read()), 'ok' 132 | except Exception as e: 133 | logging.error(e) 134 | logging.exception(e) 135 | return False, None, str(e) 136 | finally: 137 | if os.path.exists(tmp_file_path) and not self.debug: 138 | os.remove(tmp_file_path) 139 | 140 | def __del__(self): 141 | try: 142 | if os.path.exists(self.tmp_path) and not self.debug: 143 | os.removedirs(self.tmp_path) 144 | except Exception as e: 145 | logging.error(e) 146 | logging.exception(e) 147 | 148 | 149 | if __name__ == "__main__": 150 | kit = HtmlToImgPdfKit() 151 | kit.to_img('card.html', DataType.FILE, options={ 152 | 'crop-w': '526'}, out_path='current.jpg') 153 | kit.to_pdf('card.html', DataType.FILE, out_path='current.pdf') 154 | -------------------------------------------------------------------------------- /utils/MyDateTime.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from datetime import datetime, timedelta 4 | 5 | 6 | class MyDateTime: 7 | '''自定义的时间处理类 8 | 定义时间戳为整型的,微妙时间戳13位,微妙时间戳当前时间到百年之内位16位,当时间更大,会出现16位以上。 9 | 本例时间戳定义为微妙级别,更加精确。 10 | 秒时间戳:10位 11 | 1秒 = 10**3毫秒 = 10**6微秒 = 10**纳秒 12 | 注意:unix时间戳是自1970-01-01 08:00:00以来经过的秒数。 13 | Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。 14 | ''' 15 | 16 | @staticmethod 17 | def _validate_timestamp_for_ns(timestamp: int): 18 | '''验证传入的微妙时间戳是否合法''' 19 | if not isinstance(timestamp, int): 20 | raise ValueError("The timestamp need int type.") 21 | if len(str(timestamp)) != 16: 22 | raise ValueError("The timestamp is invalid.") 23 | 24 | @staticmethod 25 | def _validate_time_str(time_str: str, format: str="%Y-%m-%d %H:%M:%S"): 26 | '''验证传入的时间字符串是否合法''' 27 | if not isinstance(time_str, str): 28 | raise ValueError("The timestamp need str type.") 29 | try: 30 | datetime.strptime(time_str, format) 31 | except Exception: 32 | raise ValueError("The time_str is invalid.") 33 | 34 | @staticmethod 35 | def _validate_datetime_obj(date_time: datetime): 36 | '''验证传入的时间对象是否合法''' 37 | if not isinstance(date_time, datetime): 38 | raise ValueError("The timestamp need datetime type.") 39 | 40 | @staticmethod 41 | def _validate_timetuple(timetuple: time.struct_time): 42 | '''验证传入的时间元组是否合法''' 43 | if not isinstance(timetuple, time.struct_time): 44 | raise ValueError("The timestamp need struct_time type.") 45 | 46 | @staticmethod 47 | def timestamp_to_str(timestamp: int, format: str="%Y-%m-%d %H:%M:%S") -> str: 48 | '''将 微妙 时间戳转为字符串的时间,可以指定格式化字符串''' 49 | try: 50 | MyDateTime._validate_timestamp_for_ns(timestamp) 51 | timestamp /= 1e6 52 | # time.strftime(format, time.localtime(timestamp)) # 废弃,因为精度不高,不支持微妙的精度 53 | return datetime.fromtimestamp(timestamp).strftime(format) 54 | except Exception as e: 55 | logging.error(str(e)) 56 | raise e 57 | 58 | @staticmethod 59 | def timestamp_to_datetime(timestamp: int) -> datetime: 60 | '''将 微妙 时间戳转为datetime对象''' 61 | try: 62 | MyDateTime._validate_timestamp_for_ns(timestamp) 63 | timestamp /= 1e6 64 | return datetime.fromtimestamp(timestamp) 65 | except Exception as e: 66 | logging.error(str(e)) 67 | raise e 68 | 69 | @staticmethod 70 | def str_to_timestamp(time_str: str, format: str="%Y-%m-%d %H:%M:%S") -> int: 71 | '''将时间字符串转为 微妙 时间戳,传入的时间字符串要和格式化字符串相匹配 72 | 注:支持微妙的时间戳转换,例如 2022-01-01 00:10:10.567 对应的时间格式化字符串为 %Y-%m-%d %H:%M:%S.%f 73 | ''' 74 | try: 75 | # int(time.mktime(time.strptime(time_str, format)) * 1000) # 废弃,因为无法处理带微妙的时间字符串 76 | MyDateTime._validate_time_str(time_str, format) 77 | return int(datetime.strptime(time_str, format).timestamp() * 1e6) 78 | except Exception as e: 79 | logging.error(str(e)) 80 | raise e 81 | 82 | @staticmethod 83 | def str_to_datetime(time_str: str, format: str="%Y-%m-%d %H:%M:%S") -> datetime: 84 | '''将时间字符串转为datetime对象''' 85 | try: 86 | MyDateTime._validate_time_str(time_str, format) 87 | return datetime.strptime(time_str, format) 88 | except Exception as e: 89 | logging.error(str(e)) 90 | raise e 91 | 92 | @staticmethod 93 | def datetime_to_timestamp(date_time: datetime) -> int: 94 | '''将datetime对象转为 微妙 时间戳''' 95 | try: 96 | MyDateTime._validate_datetime_obj(date_time) 97 | return int(date_time.timestamp() * 1e6) 98 | except Exception as e: 99 | logging.error(str(e)) 100 | raise e 101 | 102 | @staticmethod 103 | def datetime_to_str(date_time: datetime, format: str="%Y-%m-%d %H:%M:%S") -> str: 104 | '''将datetime对象转为时间字符串''' 105 | try: 106 | MyDateTime._validate_datetime_obj(date_time) 107 | return datetime.strftime(date_time, format) 108 | except Exception as e: 109 | logging.error(str(e)) 110 | raise e 111 | 112 | @staticmethod 113 | def datetime_to_timetuple(date_time: datetime) -> time.struct_time: 114 | '''将datetime对象转为时间元组''' 115 | try: 116 | MyDateTime._validate_datetime_obj(date_time) 117 | return date_time.timetuple() 118 | except Exception as e: 119 | logging.error(str(e)) 120 | raise e 121 | 122 | @staticmethod 123 | def timetuple_to_datetime(timetuple: time.struct_time) -> datetime: 124 | '''将时间元组转为datetime对象''' 125 | try: 126 | MyDateTime._validate_timetuple(timetuple) 127 | return datetime.fromtimestamp(time.mktime(timetuple)) 128 | except Exception as e: 129 | logging.error(str(e)) 130 | raise e 131 | 132 | @staticmethod 133 | def timestr_to_newtimestr(time_str: str, old_format: str, new_format: str) -> str: 134 | '''将一个时间字符串格式转为新的时间字符串格式''' 135 | try: 136 | MyDateTime._validate_time_str(time_str, old_format) 137 | return datetime.strptime(time_str, old_format).strftime(new_format) 138 | except Exception as e: 139 | logging.error(str(e)) 140 | raise e 141 | 142 | 143 | if __name__ == "__main__": 144 | t = int(time.time() * 1e6) 145 | # print(MyDateTime.timestamp_to_str(1662886461423443)) 146 | # print(MyDateTime.timestamp_to_str(t, "%Y-%m-%d %H:%M:%S")) 147 | # print(MyDateTime.timestamp_to_datetime(t)) 148 | 149 | # print(MyDateTime.str_to_timestamp("2022-09-10 10:10:10.324567", "%Y-%m-%d %H:%M:%S.%f")) 150 | # print(MyDateTime.str_to_timestamp("2022-09-10 10:10:10", "%Y-%m-%d %H:%M:%S")) 151 | # print(MyDateTime.timestamp_to_str(MyDateTime.str_to_timestamp("2022-09-10 10:10:10", "%Y-%m-%d %H:%M:%S"))) 152 | # print(MyDateTime.str_to_datetime("2020-09-10 10:10:10")) 153 | 154 | # print(MyDateTime.datetime_to_timestamp(datetime.now())) 155 | # print(MyDateTime.datetime_to_str(datetime.now())) 156 | # print(MyDateTime.datetime_to_timetuple(datetime.now())) 157 | 158 | print(MyDateTime.timetuple_to_datetime(time.localtime())) 159 | print(MyDateTime.timestr_to_newtimestr("2022-09-10 10:10:10", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S")) 160 | print(MyDateTime.timestr_to_newtimestr("2022-09-10 10:10:10", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d")) -------------------------------------------------------------------------------- /utils/OtherRedisLock.py: -------------------------------------------------------------------------------- 1 | from redis_lock import Lock 2 | from utils.RedisCli import RedisCli 3 | 4 | 5 | def get_redis_lock(key): 6 | return Lock(RedisCli().coon, key) -------------------------------------------------------------------------------- /utils/ReadYaml.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from pathlib import Path 3 | from django.conf import settings 4 | 5 | 6 | class ReadYaml: 7 | '''用于读取yaml文件的类''' 8 | 9 | def __init__(self, file_name: str, is_path: bool=False) -> None: 10 | self.filepath = file_name if is_path else Path.joinpath(settings.CONFIG_DIR, file_name) 11 | 12 | def get_data(self): 13 | with open(self.filepath, "r", encoding="utf-8")as f: 14 | return yaml.load(f, Loader=yaml.FullLoader) 15 | 16 | 17 | if __name__ == '__main__': 18 | data = ReadYaml("data.yaml").get_data() 19 | print(data.get('pro').get('admin_chengdui')) -------------------------------------------------------------------------------- /utils/RedisCli.py: -------------------------------------------------------------------------------- 1 | import time 2 | import redis 3 | import logging 4 | import pickle 5 | from django.conf import settings 6 | from utils.Singleton import Singleton 7 | from typing import Optional, Any, List, Dict, Tuple 8 | from uuid import uuid1 9 | 10 | 11 | class RedisCli(Singleton): 12 | 13 | def __init__(self) -> None: 14 | '''初始化''' 15 | self._connect_url = settings.CACHES['redis_cli']['LOCATION'] # settings.CACHES['redis_cli']['LOCATION'] 16 | self.current_db = int(self._connect_url[-1]) 17 | self.coon = redis.Redis(connection_pool=redis.ConnectionPool().from_url(self._connect_url)) 18 | 19 | def key_exists(self, key: str) -> bool: 20 | '''判断键是否存在''' 21 | return bool(self.coon.exists(key)) 22 | 23 | def select_db(self, db: int) -> int: 24 | '''切换Redis数据库''' 25 | self.coon.select(db) 26 | self.current_db = db 27 | return db 28 | 29 | 30 | class RedisHash(object): 31 | '''构建操作哈希表的类,使哈希表可以想字典一样使用''' 32 | 33 | def __init__(self, key: str=None) -> None: 34 | '''初始化操作类,接收一个key作为哈希表的键 35 | args: 36 | key Redis中存储数据使用的key,给这个参数的目的是为了方便重复操作哈希表,如果为None,说明作为纯字典使用,一旦实例被销毁,Redis中的数据也将销毁 37 | destory_cache 38 | ''' 39 | self._conn = RedisCli().coon 40 | self._data_key = key if not key is None else str(int(uuid1())) 41 | self._destory_cache = True if key is None else False 42 | 43 | def __repr__(self) -> str: 44 | to_dict = {key: self.get(key) for key in map(lambda x: x.decode(), self._conn.hkeys(self._data_key))} if not self.is_empty else {} 45 | return str(to_dict) 46 | 47 | __str__ = __repr__ 48 | 49 | @property 50 | def id(self): 51 | '''返回哈希表的key,也作为实例的id''' 52 | return self._data_key 53 | 54 | @property 55 | def is_empty(self) -> bool: 56 | '''检查哈希表是否为空''' 57 | return not self._conn.exists(self._data_key) 58 | 59 | def get(self, key: str, not_exist_val: Any=None) -> Any: 60 | '''获取键值,支持设置不存在的默认值,默认为None''' 61 | val = self._conn.hget(self._data_key, key) 62 | if val is not None: 63 | return pickle.loads(val) 64 | return not_exist_val 65 | 66 | def setdefault(self, key: str, data: Any=None) -> Any: 67 | '''设置键值''' 68 | self._conn.hset(self._data_key, key, pickle.dumps(data)) 69 | return data 70 | 71 | def __del__(self): 72 | '''如果是一次性的,对象被销毁时,清除Redis中的哈希表''' 73 | if self._destory_cache: 74 | self._conn.delete(self._data_key) 75 | 76 | def __len__(self) -> int: 77 | '''获取哈希表的长度''' 78 | if self.is_empty: 79 | return 0 80 | return self._conn.hlen(self._data_key) 81 | 82 | def _check_key(self, key:str) -> None: 83 | if key not in self: 84 | raise KeyError(f"this key {key} is notfound") 85 | 86 | def __getitem__(self, key: str) -> Any: 87 | '''通过 d['key'] 来获取值''' 88 | self._check_key(key) 89 | return self.get(key) 90 | 91 | def __setitem__(self, key: str, data: Any) -> Any: 92 | '''通过 d['key'] = val 来设置值''' 93 | return self.setdefault(key, data) 94 | 95 | def __delitem__(self, key: str) -> bool: 96 | '''通过 del d['key'] 来删除值''' 97 | self._check_key(key) 98 | return bool(self._conn.hdel(self._data_key, key)) 99 | 100 | def values(self) -> List[Any]: 101 | '''返回哈希表的所有值组成的列表''' 102 | res_ls = [] 103 | for key in self.keys(): 104 | res_ls.append(self.get(key)) 105 | return res_ls 106 | 107 | def __iter__(self): 108 | '''使这个类的实例支持迭代,可以在for循环中像字典一样被使用''' 109 | return iter(self.keys()) 110 | 111 | def keys(self) -> List[str]: 112 | '''返回哈希表的所有key组成的列表''' 113 | return list(map(lambda x: x.decode(), self._conn.hkeys(self._data_key))) 114 | 115 | def pop(self, key: str, not_exist_val: Any=None) -> Any: 116 | '''移除字典中指定的键''' 117 | if key not in self: 118 | return not_exist_val 119 | val = self.get(key) 120 | del self[key] 121 | return val 122 | 123 | def clear(self) -> None: 124 | '''清空这个字典''' 125 | self._conn.delete(self._data_key) 126 | 127 | 128 | if __name__ == '__main__': 129 | # redis_cli = RedisHash('new') 130 | # print(redis_cli.get_val('a')) 131 | # print(redis_cli.get_val('b')) 132 | 133 | # redis_cli = RedisHash('ns') 134 | # print(redis_cli.set_val('x', 123)) 135 | # print(redis_cli.set_val('y', 345)) 136 | # print(redis_cli.set_val('z', '567')) 137 | # print(redis_cli.del_val('x')) 138 | # print(redis_cli.length) 139 | 140 | r1 = RedisCli() 141 | r2 = RedisCli() 142 | r3 = RedisCli() 143 | r4 = RedisCli() 144 | print(id(r1), id(r2), id(r3), id(r4)) 145 | print(id(r1.pool), id(r2.pool), id(r3.pool), id(r4.pool)) 146 | print(id(r1.coon), id(r2.coon), id(r3.coon), id(r4.coon)) 147 | while 1: 148 | r1.coon.ping() 149 | r2.coon.ping() 150 | r3.coon.ping() 151 | time.sleep(1) -------------------------------------------------------------------------------- /utils/RedisLock.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import threading 4 | from utils.RedisCli import RedisCli 5 | 6 | 7 | class RedisReadWriteLock(object): 8 | def __init__(self, cache_key, cache_type, time_out=60): 9 | self.redis = RedisCli() 10 | self.redis_con = self.redis.coon 11 | self.cache_key = cache_key 12 | self.cache_type = cache_type 13 | self.time_out = time_out 14 | 15 | def get_lock(self, val): 16 | val = val + ':' + self.cache_type 17 | while True: 18 | res = self.redis_con.set(self.cache_key, val, nx=True, ex=self.time_out) 19 | if res: 20 | # 表示获得锁成功,跳出循环 21 | print('写操作获得锁成功' if self.cache_type == 'write' else "读操作获得锁成功") 22 | break 23 | else: 24 | # 此时说明已经存在数据 25 | # 表示等待锁的过程,但是有一种情况是:如果检测到锁为读锁,来的操作也是读操作,那么不阻塞 26 | if self.cache_type == 'read': 27 | try: 28 | check_type = str(self.redis_con.get(self.cache_key).decode()).split(':')[1] 29 | if check_type == 'read': 30 | print('读操作,无需获得锁,直接读取。') 31 | break 32 | except: 33 | res = self.redis_con.set(self.cache_key, val, nx=True, ex=self.time_out) 34 | if res: 35 | break 36 | time.sleep(0.1) 37 | 38 | def del_lock(self, val): 39 | val = val + ':' + self.cache_type 40 | old_val = self.redis_con.get(self.cache_key) 41 | if old_val == val.encode(): 42 | self.redis_con.delete(self.cache_key) 43 | 44 | 45 | SUMS = 0 46 | 47 | def test_lock(name, num, val): 48 | try: 49 | if num % 2 == 0: 50 | lock = RedisReadWriteLock('new_cache_key', 'write') 51 | else: 52 | lock = RedisReadWriteLock('new_cache_key', 'read') 53 | print('%s 开始工作' % name) 54 | print('%s 准备获取锁并加锁' % name) 55 | lock.get_lock(val) 56 | print('%s 得到锁,继续工作' % name) 57 | global SUMS 58 | if num % 2 == 0: 59 | SUMS += 15 60 | time.sleep(5) 61 | print('+++++++++++++++++++写操作++++++++++++++++') 62 | else: 63 | time.sleep(1) 64 | print('**********************读操作******************') 65 | print(SUMS) 66 | except Exception as e: 67 | print('发生异常:%s' % str(e)) 68 | finally: 69 | print('%s 操作完成,准备释放锁'%name) 70 | lock.del_lock(val) 71 | 72 | 73 | def test_main(): 74 | start_time = time.time() 75 | tasks = [] 76 | for num in range(1, 6): 77 | t = threading.Thread(target=test_lock, args=('任务%d'%num, num, 'lock%d'%num)) 78 | tasks.append(t) 79 | t.start() 80 | [item.join() for item in tasks] 81 | print('总耗时:', time.time() - start_time) 82 | 83 | if __name__ == '__main__': 84 | test_main() -------------------------------------------------------------------------------- /utils/RedisLockNew.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | from enum import Enum 4 | from typing import Optional, Tuple 5 | from uuid import uuid1 6 | from redis import StrictRedis 7 | import redis 8 | ''' 9 | 新版的分布式可重入读写锁 10 | 实现原理: 11 | 1、基本的分布式锁原理,通过设置redis的key nx 即如果key存在则创建,否则返回none,创建锁时增加过期时间参数,默认30秒 12 | 2、可以同个key的值加上锁类型,读锁或写锁 以及锁id(防止锁被别人释放) 加上加锁次数 目的是支持可重入的锁,当加锁次数为0时,释放锁 13 | 3、在加锁时增加timeout参数,毫秒值,如果有值那么表示加锁时,如果锁被人锁着,那么就进行重试等待 值表示等待的时间,为None时表示不阻塞,有锁时直接返回加锁失败。 14 | 4、使用上下文管理器,进入时加锁,离开时释放锁, 15 | 5、id有创建锁实例时自动生成,不支持传入,好处时防止别人通过id生成相同的锁实例造成的危害 16 | ''' 17 | 18 | 19 | class TimeoutNotUsable(RuntimeError): 20 | __module__ = 'builtins' 21 | 22 | 23 | class InvalidTimeout(RuntimeError): 24 | __module__ = 'builtins' 25 | 26 | 27 | class TimeoutTooLarge(RuntimeError): 28 | __module__ = 'builtins' 29 | 30 | 31 | class LockedTimeout(RuntimeError): 32 | __module__ = 'builtins' 33 | 34 | 35 | class LockNotExists(RuntimeError): 36 | __module__ = 'builtins' 37 | 38 | 39 | class RedisLock: 40 | 41 | def __init__(self, redis_conn: StrictRedis, key: str, lock_type: str='w', expire: int=30) -> None: 42 | '''锁的初始化 43 | args: 44 | redis_conn Redis连接对象 45 | key 锁的key 46 | lock_type 加锁的类型,可选值为 r 读锁 w 写锁,默认为写锁 47 | expire 锁的过期时间,单位为秒,默认为30秒 48 | ''' 49 | if not isinstance(redis_conn, StrictRedis): 50 | raise ValueError("redis_conn is not StrictRedis") 51 | if lock_type not in {'r', 'w'}: 52 | raise ValueError("lock_type is not allowed") 53 | if not isinstance(expire, int): 54 | raise ValueError("expire need int type") 55 | if expire < 0: 56 | raise ValueError("expire must large zero") 57 | if not isinstance(key, str): 58 | raise ValueError("key need str type") 59 | self._conn = redis_conn 60 | self._key = key 61 | self._id = int(uuid1()) 62 | self._lock_type = lock_type 63 | self._expire = expire 64 | 65 | def __repr__(self) -> str: 66 | return f"" 67 | 68 | __str__ = __repr__ 69 | 70 | @property 71 | def lock_val(self) -> Tuple[str, str, str]: 72 | if not self._conn.exists(self._key): 73 | raise LockNotExists("The lock is not exists") 74 | return self._conn.get(self._key).decode().split("+") 75 | 76 | @property 77 | def locked(self) -> bool: 78 | return bool(self._conn.exists(self._key)) 79 | 80 | @property 81 | def id(self) -> int: 82 | return self._id 83 | 84 | @property 85 | def is_my_lock(self) -> bool: 86 | if not self._conn.exists(self._key): 87 | return False 88 | _id, _, _ = self.lock_val 89 | return self._id == int(_id) 90 | 91 | def acquire(self, timeout: Optional[int]=None) -> bool: 92 | '''加锁操作 93 | args: 94 | timeout 为None表示不阻塞,存在值时表示阻塞的毫秒值 95 | ''' 96 | if all([not isinstance(timeout, int), not timeout is None]): 97 | raise TimeoutNotUsable("The time_out need None or int type") 98 | if timeout and timeout < 0: 99 | raise InvalidTimeout("The time_out must be greater than zero") 100 | if timeout and timeout > 60: 101 | raise TimeoutTooLarge("The time_out must be less than 60") 102 | init_lock_val = f"{self._id}+{self._lock_type}+{1}" 103 | busy = not self._conn.set(self._key, init_lock_val, nx=True, ex=self._expire) # 如果加锁成功那么 busy就为False,否则为True,加锁失败 104 | if busy: 105 | # 如果是你自己的锁,再次加锁时,会将times+1 106 | # 如果这里被执行,说明已经存在锁 107 | _id, lock_type, times = self.lock_val 108 | if self.is_my_lock: 109 | times = int(times) + 1 110 | self._conn.set(self._key, f"{_id}+{lock_type}+{times}", ex=self._expire) 111 | return True 112 | # 如果都是读锁,那么直接返回,不需要获得锁 113 | if self._lock_type == lock_type == 'r': 114 | return True 115 | start_time = time.time() 116 | while busy: 117 | busy = not self._conn.set(self._key, init_lock_val, nx=True, ex=self._expire) 118 | if busy: 119 | if timeout is None: 120 | return False 121 | if time.time() - timeout > start_time: # 超时退出 122 | return False 123 | return True 124 | 125 | def release(self) -> None: 126 | '''释放锁,考虑如何支持重入释放 127 | 如果锁存在、并且时自己的锁,那么就将锁的times减一,直达times==0时直接删除key 128 | ''' 129 | if not self.is_my_lock: 130 | raise ValueError("This lock is not your") 131 | _id, lock_type, times = self.lock_val 132 | times = int(times) - 1 133 | if times == 0: 134 | self._conn.delete(self._key) 135 | return 136 | self._conn.set(self._key, f"{_id}+{lock_type}+{times}", ex=self._expire) 137 | 138 | def __del__(self) -> None: 139 | '''销毁对象的时候释放锁''' 140 | self._conn.delete(self._key) 141 | 142 | 143 | if __name__ == "__main__": 144 | conn = redis.StrictRedis() 145 | lock1 = RedisLock(conn, 'new_lock', 'w') 146 | print(lock1) 147 | print(lock1.locked) 148 | print(lock1.is_my_lock) 149 | lock1.acquire() 150 | print(lock1.is_my_lock) 151 | # lock1.release() 152 | print(lock1.lock_val) 153 | lock1.acquire() 154 | print(lock1.lock_val) 155 | lock2 = RedisLock(conn, 'new_lock', 'w') 156 | print(lock1.locked) 157 | print(lock2.locked) 158 | print(lock2.acquire(timeout=5)) 159 | print(lock2) 160 | print('lock2') 161 | print(lock2.lock_val) -------------------------------------------------------------------------------- /utils/Singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(object): 2 | instance = None 3 | 4 | def __new__(cls, *args, **kwargs): 5 | if not cls.instance: 6 | cls.instance = super().__new__(cls) 7 | return cls.instance -------------------------------------------------------------------------------- /utils/TableUtil.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from tempfile import NamedTemporaryFile 3 | from typing import Tuple, Union 4 | 5 | from bs4 import BeautifulSoup 6 | from pyecharts.components import Table 7 | from pyecharts.options import ComponentTitleOpts 8 | 9 | from utils.HtmlToImgPdfKit import HtmlToImgPdfKit 10 | 11 | 12 | class TableUtil: 13 | '''操作表格数据的类,包括二维数组和DataFrame''' 14 | 15 | def __init__(self, table_title: str, headers: list, rows: list) -> None: 16 | self.table_css_style = "" 17 | self.table_title = table_title 18 | self.headers = headers 19 | self.rows = rows 20 | self.img_pdf_kit = HtmlToImgPdfKit() 21 | 22 | def to_html(self) -> Tuple[bool, Union[None, str], str]: 23 | '''处理数据,将数据转为目标html的str内容''' 24 | try: 25 | table = Table() 26 | table.add(headers=self.headers, rows=self.rows) 27 | table.set_global_opts( 28 | title_opts=ComponentTitleOpts(title=self.table_title) 29 | ) 30 | # 创建临时文件 31 | tmp_file = NamedTemporaryFile() 32 | # 输出表格到临时文件 33 | table.render(tmp_file.name) 34 | # 使用 BeautifulSoup 读取html 35 | soup = BeautifulSoup(tmp_file.read(), 'html.parser') 36 | # 修改样式 如果有定制样式的话 37 | if self.table_css_style: 38 | for item in soup.find_all('style'): 39 | item.string = self.table_css_style 40 | return True, soup.prettify(), 'ok' 41 | except Exception as e: 42 | return False, None, str(e) 43 | 44 | def get_img_or_pdf(self, out_type: str = 'img', out_path: str = '', options: dict = {}) -> Tuple[bool, Union[BytesIO, None], str]: 45 | flag, data, msg = self.to_html() 46 | if not flag: 47 | return flag, data, msg 48 | if out_type == 'img': 49 | return self.img_pdf_kit.to_img(data, data_type=2, out_path=out_path, options=options) 50 | return self.img_pdf_kit.to_pdf(data, data_type=2, out_path=out_path, options=options) 51 | 52 | 53 | if __name__ == "__main__": 54 | tu = TableUtil(table_title='test', headers=[ 55 | 'a', 'b'], rows=[[1, 2], [3, 4]]) 56 | tu.get_img_or_pdf(out_path='test.png') 57 | # tu.get_img_or_pdf(out_type='pdf', out_path='test.pdf') 58 | -------------------------------------------------------------------------------- /utils/Utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | import hmac 3 | import time 4 | import random 5 | import hashlib 6 | from uuid import uuid1 7 | from datetime import datetime 8 | from typing import Union 9 | ''' 10 | Python中UUID的区别 11 | uuid1(node=None, clock_seq=None) 根据主机 ID、序列号和当前时间生成一个 UUID。 如果没有给出 node,则使用 getnode() 来获取硬件地址。 12 | 如果给出了 clock_seq,它将被用作序列号;否则将选择一个随机的 14 比特位序列号。 13 | uuid3(namespace, name) 根据命名空间标识符(这是一个UUID)和名称(这是一个字符串)的MD5哈希值,生成一个UUID。 14 | uuid4() 生成一个随机的UUID。注意:可能会重复。 15 | uuid5(namespace, name) 根据命名空间标识符(这是一个UUID)和名称(这是一个字符串)的SHA-1哈希值生成一个UUID。 16 | ''' 17 | 18 | 19 | class NormalObj: 20 | '''一个统一的类,目的是将分散的一些通用方法进行归集''' 21 | 22 | @staticmethod 23 | def to_sha256(payload: str) -> str: 24 | '''生成哈希256的散列值,返回长度为64位的字符串,其值为64位的16进制数组成''' 25 | h = hashlib.sha256() 26 | h.update(payload.encode(encoding='utf8')) 27 | return h.hexdigest() 28 | 29 | @staticmethod 30 | def create_random_code(length: int=6, abc: bool=True) -> str: 31 | '''生成随机验证码''' 32 | base_str = '0123456789qwerrtyuioplkjhgfdsazxcvbnm' if abc else '01234567890123456789' 33 | # return ''.join([random.choice(base_str) for _ in range(length)]) 34 | return ''.join(random.choices(list(base_str), k=length)) 35 | 36 | @staticmethod 37 | def create_unique_order_no() -> str: 38 | now_date_time_str = str( 39 | datetime.now().strftime('%Y%m%d%H%M%S%f')) 40 | base_str = '01234567890123456789' 41 | random_num = ''.join(random.sample(base_str, 6)) 42 | random_num_two = ''.join(random.sample(base_str, 6)) 43 | return ''.join(now_date_time_str, random_num, random_num_two) 44 | 45 | @staticmethod 46 | def uuid1_int() -> int: 47 | return int(uuid1()) 48 | 49 | @staticmethod 50 | def get_distance(lat1: float, lng1: float, lat2: float, lng2: float) -> float: 51 | '''计算两经纬度之间的距离 返回距离单位为公里''' 52 | radLat1 = (lat1 * math.pi / 180.0) 53 | radLat2 = (lat2 * math.pi / 180.0) 54 | a = radLat1 - radLat2 55 | b = (lng1 * math.pi / 180.0) - (lng2 * math.pi / 180.0) 56 | s = 2 * math.asin(math.sqrt(math.pow(math.sin(a/2), 2) + math.cos(radLat1) * math.cos(radLat2) * math.pow(math.sin(b/2), 2))) 57 | s = s * 6378.137 58 | return s 59 | 60 | @staticmethod 61 | def check_number_divisible(dividend: Union[float, int], divisor: Union[float, int]): 62 | '''检查被除数是否能被除数除尽''' 63 | mod = dividend % divisor 64 | mod_set = set() 65 | while mod != 0 and mod not in mod_set: 66 | mod_set.add(mod) 67 | if mod * 10 > divisor: 68 | mod = (mod * 10) % divisor 69 | else: 70 | mod = (mod * 10 ** len(str(divisor))) % divisor 71 | if mod == 0: 72 | return True 73 | return False 74 | 75 | 76 | if __name__ == "__main__": 77 | print(NormalObj.check_number_divisible(4950, 156)) -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | 3 | # 配置前导路径 4 | ; base = /root/bin/django-RESTfulAPI 5 | # 配置项目名称 6 | name = drfAPI 7 | # 守护进程 8 | master = true 9 | # 进程个数 10 | processes = 4 11 | # 线程个数 12 | threads = 2 13 | # 配置gevent的并发。线上建议:gevent设置500 process设置4-8。例如:process=4 gevent=500,那么最大并发数为:4 * 500 = 2000 14 | ; gevent = 500 15 | # 在不修改任何代码的情况下,使用猴子补丁,使django服务可以以协程并发的形式运行 16 | ; gevent-monkey-patch = true 17 | # 这是uwsgi监听队列的长度,默认较小,当需要大的并发时需要增加该值。需要配合Linux系统内核的配置net.core.somaxconn,也就是listen的值不能大于Linux系统设置的net.core.somaxconn值 18 | # 可以配置Linux系统net.core.somaxconn = 10240。然后可以配置uwsgi listen = 10240 19 | ; listen = 10240 20 | # 通信的地址和端口(自己服务器的IP地址和端口) 同时监听socket和http 同时监听socket和http会导致没法使用nginx反向代理 全部放在server.sh内指定 21 | ; http-socket = 127.0.0.1:8001 22 | ; socket = 127.0.0.1:8001 23 | # 绑定http的端口 24 | ; http = 0.0.0.0:8002 25 | # wsgi-file文件地址 26 | ; wsgi-file = %(base)/%(name)/wsgi.py 27 | # 项目地址 28 | ; chdir = %(base) 29 | # 虚拟环境 docker环境中不指定虚拟环境和pythonpath,因为docker本身就是一个环境 30 | home = /root/bin/django-RESTfulAPI/venv 31 | # 指定python解释器,部分系统需要指定到 site-packages 目录才可以正常运行 32 | pythonpath = %(home)/bin/python 33 | # 日志文件地址,没有指定日志地址时,启动时会在前台启动。指定日志时在后台启动 34 | daemonize = ./logs/%(name).log 35 | # 格式化日志格式 36 | logformat = %(addr) [%(ltime)] [%(method) %(uri)] [%(status) %(size)] --------------------------------------------------------------------------------