【Django】代碼最佳實踐【原創】

目錄

參考

概要

目錄結構

API返回格式

步驟

1. 建立虛擬環境

2. 安裝和建立Django

3. 新建應用App

4. 修改settings.py

5. 測試

6. 增加函數庫

7. 修改hello方法

8. 規範目錄

9. 增加日誌配置

10. 引入Sentry - 非必須

11. 引入Redis

12. 使用MySQL 

13. 建立超管

14. 引入Celery - 非必須

15. 引入rest-framework


參考

https://www.freebuf.com/column/165233.html

https://www.freebuf.com/column/165968.html

 

概要

基於Django的項目最佳實踐

注意:不支持Django3.0,會報錯

  • 接入Sentry,實現錯誤日誌管理
  • 使用rest-framework,實現API的restful風格
  • 使用數據庫來作爲存儲
  • 支持使用Redis
  • 接入Celery,實現異步和定時任務
  • 定時任務支持由admin管理後臺來控制
  • 增加日誌配置

代碼可以參考:代碼倉庫

 

 

目錄結構

大概的目錄結構如下:

項目名
|—— 項目名
|     |—— __init__.py
|     |—— settings.py
|     |—— urls.py
|     |—— wsgi.py
|     |—— views.py
|     |—— config
|           |—— __init__.py
|           |—— local.py
|           |—— dev.py
|           |—— prod.py
|—— 應用名
|     |—— migrations
|           |—— __init__.py
|     |—— models
|           |—— __init__.py
|           |—— base_model.py
|           |—— user.py
|     |—— views
|           |—— __init__.py
|           |—— user.py
|     |—— services
|           |—— __init__.py
|           |—— user_service.py
|     |—— serializers
|           |—— __init__.py
|           |—— user.py
|     |—— tasks
|           |—— __init__.py
|           |—— xxx_task.py
|     |—— __init__.py
|     |—— tests.py
|     |—— apps.py
|     |—— admin.py
|     |—— urls.py
|—— sql
|     |—— init.sql
|     |—— 20190808_create_table.sql
|—— lib
|     |—— __init__.py
|     |—— functions.py
|—— supervisor
|     |—— __init__.py
|     |—— gunicorn_conf.conf
|     |—— local.conf
|     |—— dev.conf
|     |—— prod.conf
|—— collectedstatic
|     |—— admin
|           |—— css
|           	 |—— vendor
|           	 |—— xxx.css
|           |—— fonts
|           |—— img
|           	 |—— xxx.jpg
|           |—— js
|           	 |—— vendor
|           	 |—— xxx.js
|     |—— 應用名
|           |—— css
|           	 |—— vendor
|           	 |—— xxx.css
|           |—— fonts
|           |—— img
|           	 |—— xxx.jpg
|           |—— js
|           	 |—— vendor
|           	 |—— xxx.js
|—— manage.py
|—— requirements.txt
|—— manage.py
|—— manage.py

注意:應用目錄下的tasks目錄是Celery任務需要的,非必須

 

原則

應用的views層裏面不寫具體的業務邏輯,只從請求中獲取參數以及返回數據給前端即可,具體的業務邏輯都寫在服務層(services),通過服務層去調用模型層(model)的數據,進行篩選和處理然後返回給views層,views層再返回給前端

 

API返回格式

API的返回Json格式,需要返回三個字段:

{
    "code": 0,
    "message": "success",
    "data": {}
}

 

步驟

1. 建立虛擬環境

注意:怎麼安裝和使用虛擬環境可以參考:【Virtualenv】Python的虛擬環境Virtualenv和Virtualenvwrapper【原創】

mkvirtualenv 項目名
workon 項目名

 

2. 安裝和建立Django

pip install django
django-admin startprojrct 項目名

注意:建議使用Django 2.2版本,不使用最新的Django3.0版本,會存在admin管理頁面進不去(Django 3.0.3和Python 3.7有可能會衝突導致Django停止運行)以及和Celeat-beat衝突

 

3. 新建應用App

cd 項目名
python manage.py startapp app

 

4. 修改settings.py

增加config目錄,config目錄裏面新增3個文件:local.py,dev.py,prod.py,除了prod.py的Debug模式需要關閉外其他都要開啓

local.py:

# -*- coding:UTF-8 -*-

# debug模式爲開啓
DEBUG = True

修改settings.py:

INSTALLED_APPS = [
    ...
    'app'
]

# 允許其他的機器訪問該服務器的Django應用
ALLOWED_HOSTS = ['*']

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False

# 獲取環境變量
ENV_PROFILE = os.getenv("ENV")

# 根據環境來加載不同的配置文件
if ENV_PROFILE == "prod":
    from .config.prod import *
elif ENV_PROFILE == "dev":
    from .config.dev import *
else:
    ENV_PROFILE = 'local'
    from .config.local import *
    

注意:註釋掉settings.py中的DEBUG

 

5. 測試

app/views.py:

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def hello(request):
    result = {
        'code': 0,
        'message': 'success',
        'data': [],
    }
    return JsonResponse(result)

 

app目錄增加urls.py:

from django.urls import path
from app import views

urlpatterns = [
    path('hello', views.hello),
]

 

urls.py增加app的路由:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/app/', include('app.urls'))
]

瀏覽器訪問127.0.0.1:8000/api/app/hello來測試

 

6. 增加函數庫

先下載相關依賴:

pip install requests
pip install rarfile
pip install IPy

 

增加common目錄,common目錄下新增functions.py:

from django.http import JsonResponse
import os
import signal
import subprocess
import requests
import json
import platform
import datetime
import re


def render_json(code=0, msg='success', data={}):
    result = {
        'code': code,
        'message': msg,
        'data': data,
    }
    return JsonResponse(result)


def is_ip(address):
    """
    校驗是否是IP地址
    :param address:
    :return:
    """
    compile_ip = re.compile(
        '^(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|[1-9])\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)$')
    if compile_ip.match(address):
        return True
    else:
        return False


def get_ip(request):
    """
    獲取IP
    :param request:
    :return:
    """
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')

    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip


def get_ips_list(ips):
    """
    根據正則表達式獲取字符串中的IP地址
    """
    pattern = re.compile(
        r"((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d))))")
    a = pattern.findall(ips)
    ips_list = [g[0] for g in a]

    return ips_list


def get_request(url, params={}, headers={}, timeout=120):
    """
    HTTP請求-Get請求
    :param url:
    :param params:  string URL中的參數
    :param headers:
    :param timeout:
    :return:
    """
    if not headers:
        headers = {
            "Content-Type": "application/json",
        }

    response = requests.request('GET', url, headers=headers, params=params, timeout=timeout)
    return response.text


def post_request(url, params={}, data={}, headers={}, timeout=120):
    """
    HTTP請求-Post請求
    :param url:
    :param params:  string URL中的參數
    :param data:    dict body中的參數
    :param headers: dict
    :param timeout:
    :return:
    """
    if not headers:
        headers = {
            "Content-Type": "application/json",
        }

    response = requests.request('POST', url, headers=headers, params=params, data=json.dumps(data), timeout=timeout)
    return response.text


def run_cmd(cmd_string, timeout=600):
    """
    執行命令
    :param cmd_string:  string 字符串
    :param timeout:  int 超時設置
    :return:
    """
    p = subprocess.Popen(cmd_string, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True, close_fds=True,
                         start_new_session=True)

    format = 'utf-8'
    if platform.system() == "Windows":
        format = 'gbk'

    try:
        (msg, errs) = p.communicate(timeout=timeout)
        ret_code = p.poll()
        if ret_code:
            code = 1
            msg = "[Error]Called Error : " + str(msg.decode(format))
        else:
            code = 0
            msg = str(msg.decode(format))
    except subprocess.TimeoutExpired:
        # 注意:不能使用p.kill和p.terminate,無法殺乾淨所有的子進程,需要使用os.killpg
        p.kill()
        p.terminate()
        os.killpg(p.pid, signal.SIGUSR1)

        # 注意:如果開啓下面這兩行的話,會等到執行完成才報超時錯誤,但是可以輸出執行結果
        # (outs, errs) = p.communicate()
        # print(outs.decode('utf-8'))

        code = 1
        msg = "[ERROR]Timeout Error : Command '" + cmd_string + "' timed out after " + str(timeout) + " seconds"
    except Exception as e:
        code = 1
        msg = "[ERROR]Unknown Error : " + str(e)

    return code, msg


def get_uuid():
    """
    生成唯一的uuid
    :return:
    """
    import uuid
    uid = str(uuid.uuid4())
    return ''.join(uid.split('-'))


def UTC2Local(utc_str):
    """
    處理UTC時間
        類似:2019-10-23T06:00:34.882747 或者是  2020-01-13T19:53:56Z時間
    :param utc_str
    :return:
    """
    # 先去掉小數點
    utc_str = utc_str.split('.')[0]

    # 第一次替換爲空格,第二次替換爲空字符串
    utc_time = utc_str.replace("T", " ").replace("Z", "")

    # UTC轉本地時間+8h
    utc_str = datetime.datetime.strptime(utc_time, "%Y-%m-%d %H:%M:%S") + datetime.timedelta(hours=8)

    # 控制輸出格式
    return utc_str.strftime("%Y-%m-%d %H:%M:%S")


def validate_password(password, min_len=8, max_len=26):
    """
    校驗密碼
        長度:8-26位
        密碼至少包含:大寫字母、小寫字母、數字、特殊字符(!@$%^-_=+[{}]:,./?)中的三種
    :param max_len:
    :param min_len:
    :param password:
    :return:
    """
    length = len(password)
    if length < min_len:
        return 200, '密碼長度不符'

    if length > max_len:
        return 200, '密碼長度不符'

    reg = "[A-Za-z0-9!@$%^-_=+\[{}\]:,./?]"
    if len(re.findall(reg, password)) < length:
        return 201, '密碼有非法字符'

    first = re.search('[A-Z]', password)
    num1 = 1 if first else 0

    second = re.search('[a-z]', password)
    num2 = 1 if second else 0

    third = re.search('[0-9]', password)
    num3 = 1 if third else 0

    fourth = re.search("[!@$%^-_=+\[{}\]:,./?]", password)
    num4 = 1 if fourth else 0

    if num1 + num2 + num3 + num4 < 3:
        return 202, '密碼必須包含大寫字母、小寫字母、數字和特殊字符中的三種'

    return 0, 'success'


def is_dir(path):
    """
    判斷目錄是否存在
    :param path:
    :return:
    """
    if path and os.path.isdir(path):
        return True

    return False


def is_file(path):
    """
    判斷文件是否存在
    :param path:
    :return:
    """
    if path and os.path.isfile(path):
        return True

    return False


def traverse_path(path):
    """
    遍歷目錄,獲取文件和目錄
    :param path:
    :return:
    """
    g = os.walk(path)

    dirs = []
    files = []

    # 三個參數:分別返回 1.父目錄 2.所有文件夾名字(不含路徑) 3.所有文件名字
    for path, dir_list, file_list in g:
        for dir_name in dir_list:
            dirs.append(os.path.join(path, dir_name))

        for file_name in file_list:
            files.append(os.path.join(path, file_name))

    return dirs, files


def unzip_file(zip_src, dst_dir=None):
    """
    解壓縮(Zip格式)- 注意:如果不傳目的路徑,則默認解壓到源路徑+_files
    :param zip_src:  源路徑
    :param dst_dir:  目的解壓路徑,如爲空則默認爲源路徑+_files
    :return:
    """
    import zipfile
    r = zipfile.is_zipfile(zip_src)

    if not zip_src:
        return 1, '源路徑爲空', ''

    # 判斷是否是zip壓縮包
    if not r:
        return 1, '非zip壓縮包', ''

    # 如果目的解壓路徑參數不傳,則默認爲源路徑+_files
    if not dst_dir:
        dst_dir = zip_src + "_files"
        if not os.path.isdir(dst_dir):
            os.mkdir(dst_dir)

    # 判斷目的解壓的目錄是否存在
    if not os.path.isdir(dst_dir):
        return 1, '目的解壓目錄不存在', ''

    fz = zipfile.ZipFile(zip_src, 'r')
    for file in fz.namelist():
        # Mac電腦壓縮Zip會增加__MACOSX目錄,自動跳過
        if '__MACOSX' not in file:
            fz.extract(file, dst_dir)
    fz.close()

    return 0, 'success', dst_dir


def unrar_file(rar_src, dst_dir=None):
    """
    解壓縮(rar格式)- 注意:如果不傳目的路徑,則默認解壓到源路徑+_files
    注意:需要安裝rarfile, pip install rarfile
    注意:如果是Linux需要安裝unrar
    :param rar_src:  源路徑
    :param dst_dir:  目的解壓路徑,如爲空則默認爲源路徑+_files
    :return:
    """
    import rarfile

    if not rar_src:
        return 1, '源路徑爲空', ''

    # 如果目的解壓路徑參數不傳,則默認爲源路徑+_files
    if not dst_dir:
        dst_dir = rar_src + "_files"
        if not os.path.isdir(dst_dir):
            os.mkdir(dst_dir)

    # 判斷目的解壓的目錄是否存在
    if not os.path.isdir(dst_dir):
        return 1, '目的解壓目錄不存在', ''

    rar = rarfile.RarFile(rar_src, mode='r')
    rar.extractall(dst_dir)
    rar.close()

    return 0, 'success', dst_dir


def get_file_type(file):
    """
    獲取文件的後綴名
    :param file:
    :return:
    """
    if not file:
        return ''

    file_list = os.path.splitext(file)

    if len(file_list) >= 2:
        return file_list[1]

    return ''


def get_ips(cidr):
    """
    獲取某個網段的所有IP
    注意:需要安裝IPy,pip install IPy
    :param cidr:
    :return:
    """
    from IPy import IP
    ips = IP(cidr)
    return ips


def get_next_value(value, list):
    """
    獲取list中value的下一個值(如果value爲最後一個,則下一個爲第一個)
    :param value:
    :param list:
    :return:
    """
    if value not in list:
        return ''

    list_count = len(list)
    index = list.index(value)

    next_index = index + 1 if (index + 1) < list_count else 0
    return list[next_index]


def md5_string(content):
    """
    MD5加密
    :param content:
    :return:
    """
    if not content:
        return ''

    import hashlib
    return hashlib.md5(content.encode(encoding="UTF-8")).hexdigest()


def get_python_version():
    """
    獲取Python版本
    :return:
    """
    import sys
    return sys.version_info

 

7. 修改hello方法

app/views.py:

from common.functions import render_json
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def hello(request):
    return render_json(0, 'success', 'Hello')

 

8. 規範目錄

增加sql、logs目錄

增加requirements.txt文件:

pip freeze > requirement.txt

app目錄下增加models包目錄和services包目錄,app下刪掉models.py文件

app/models目錄下新增:base_model.py:

from django.db import models


class BaseModel(models.Model):
    """
    模型基類(抽象類)- 所有的模型都應該繼承
    """
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(blank=True, null=True)

    class Meta:
        abstract = True

 

9. 增加日誌配置

修改settings.py:

# 日誌位置
LOG_DIR = BASE_DIR + '/logs/'

# 日誌設置
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'standard': {
            'format': '%(asctime)s [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'filters': {
    },
    'handlers': {
        'default': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': os.path.join(LOG_DIR, 'django.log'),
            'backupCount': 30,
            'encoding': 'utf8',
            'formatter': 'standard',
            'when': 'midnight',
            'interval': 1,
        },
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'request_handler': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': os.path.join(LOG_DIR, 'django_request.log'),
            'backupCount': 30,
            'encoding': 'utf8',
            'formatter': 'standard',
            'when': 'midnight',
            'interval': 1,
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'include_html': True,
        },
        'app_handler': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': os.path.join(LOG_DIR, 'app.log'),
            'backupCount': 30,
            'encoding': 'utf8',
            'formatter': 'standard',
            'when': 'midnight',
            'interval': 1,
        },
    },
    'loggers': {
        'django': {
            'handlers': ['default', 'console'],
            'level': 'INFO',
            'propagate': False
        },
        'django.request': {
            'handlers': ['request_handler'],
            'level': 'INFO',
            'propagate': False
        },
        'app': {
            'handlers': ['app_handler'],
            'level': 'INFO',
            'propagate': False
        },
    }
}

 

10. 引入Sentry - 非必須

目的是爲了實時獲取事件日誌,具體可參考:【Sentry】實時事件日誌平臺【原創】

 

安裝sentry:

pip install sentry-sdk

 

修改settings.py:

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

# Sentry的設置
sentry_sdk.init(
    dsn="https://[email protected]/1831747",
    integrations=[DjangoIntegration()],
    environment=ENV_PROFILE
)

 

11. 引入Redis

安裝依賴:

pip install django-redis
pip install redis

 

修改local.py、dev.py、prod.py,增加以下代碼:

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
        # 前綴
        "KEY_PREFIX": "DjangoDemo"
    }
}

 

12. 使用MySQL 

安裝依賴:

pip install mysqlclient

 

修改local.py、dev.py、prod.py,增加以下代碼:

# 數據庫配置
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'djangodemo',
        'USER': 'root',
        'PASSWORD': '123456',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'OPTIONS': {'charset': 'utf8mb4'}
    }
}

 

數據庫遷移:

python manage.py makemigrations
python manage.py migrate

 

13. 建立超管

python manage.py createsuperuser

 

14. 引入Celery - 非必須

目的是爲了實現定時任務和異步任務,具體可參考:【異步/定時任務】Django中使用Celery實現異步和定時任務【原創】

 

安裝依賴:

pip install celery
pip install django-celery-results
pip install django-celery-beat

 

settings.py同級目錄新增celery.py文件:

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from django.conf import settings

# 獲取當前文件夾名,即爲該Django的項目名
project_name = os.path.split(os.path.abspath('.'))[-1]
project_settings = '%s.settings' % project_name

# 設置環境變量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', project_settings)

# 實例化Celery
app = Celery(project_name)

# 使用django的settings文件配置celery
app.config_from_object('django.conf:settings', namespace='CELERY')

# Celery加載所有註冊的應用
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)


@app.task(bind=True)
def debug_task(self):
    print('Request: {0!r}'.format(self.request))

 

settings.py同級目錄__init__.py修改:

from __future__ import absolute_import, unicode_literals

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ['celery_app']

 

修改settings.py:

# celery結果返回,可用於跟蹤結果,默認是存儲到redis,需要安裝django_celery_results,這裏就可以使用db存儲
CELERY_RESULT_BACKEND = 'django-db'

# 設置worker的併發數量
CELERY_CONCURRENCY = 4

# celery內容等消息的格式設置
CELERY_ACCEPT_CONTENT = ['application/json', ]
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'

# celery時區設置,使用settings中TIME_ZONE同樣的時區
CELERY_TIMEZONE = TIME_ZONE

# 導入task所在的文件路徑
CELERY_IMPORTS = ('app.tasks.tasks',)

# 定時任務來源從數據庫中讀取
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

# 避免時區的問題
CELERY_ENABLE_UTC = False
DJANGO_CELERY_BEAT_TZ_AWARE = False

修改local.py、dev.py、prod.py:

# celery中間人 redis://redis服務所在的ip地址:端口/數據庫號
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/3'

 

app目錄下新建tasks目錄,tasks目錄下新建tasks.py文件:

from __future__ import absolute_import, unicode_literals
from celery import shared_task
import datetime


@shared_task
def add(x, y):
    res = x + y
    time_format = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print('當前時間爲:' + time_format + ' ,兩個數相加的結果爲:')
    print(res)
    return res


@shared_task
def mul(x, y):
    res = x * y
    time_format = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print('當前時間爲:' + time_format + ' ,兩個數相乘的結果爲:')
    print(res)
    return res


@shared_task
def xsum(numbers):
    res = sum(numbers)
    print(res)
    return res

 

執行遷移:

python manage.py migrate

 

啓動:

# 啓動worker
celery -A 項目名 worker --pool=solo -l info

# 啓動定時任務beat
celery -A 項目名 beat -l info

 

測試:

python manage.py shell


from app.tasks.tasks import add, mul, xsum

res = add.delay(2,3)
res.get()

也可以在admin管理頁面中新建定時任務來進行測試,如圖:

 

15. 引入rest-framework

目的是爲了序列化數據庫的時間字段,默認讀取出來的字段是加了T或者是Z的,另外也可以控制輸出的字段範圍

爲什麼引入rest-framework,可以參考:【模型】Django數據庫的數據轉成Json返回【原創】

 

安裝依賴:

pip install djangorestframework

 

app目錄下新增模型:

app\models\devices_info.py

from django.db.models import Q
from app.models.base_model import BaseModel
from django.db import models

STATUS_VALUE = (
    (0, u'代運營'),
    (1, u'隔離中'),
    (2, u'在線'),
    (3, u'離線'),
    (4, u'下架'),
)

POWER_STATE = (
    (0, u'pending'),
    (1, u'running'),
    (2, u'paused'),
    (3, u'shutdown'),
    (4, u'crashed'),
)


class DevicesInfo(BaseModel):
    name = models.CharField(max_length=50, default='', verbose_name=u'名稱', blank=True)
    description = models.CharField(max_length=1000, default='', verbose_name=u'描述')
    ip_version = models.IntegerField(default=0, verbose_name=u'IP地址類型,值爲4或6,4:IPv4,6:IPv6', blank=True)
    lan_ip = models.CharField(max_length=16, default='', verbose_name=u'內網IP', blank=True)
    wan_ip = models.CharField(max_length=16, default='', verbose_name=u'外網IP', blank=True)
    city_id = models.IntegerField(default=0, verbose_name=u'城市ID', blank=True)
    city_name = models.CharField(max_length=10, default='', verbose_name=u'城市名', blank=True)
    area_id = models.IntegerField(default=0, verbose_name=u'可用區ID', blank=True)
    area_name = models.CharField(max_length=10, default='', verbose_name=u'可用區名', blank=True)
    device_class = models.CharField(max_length=20, default='', verbose_name=u'機型', blank=True)
    owner_main = models.CharField(max_length=100, default='', verbose_name=u'主負責人', blank=True)
    owner_back = models.CharField(max_length=100, default='', verbose_name=u'備負責人', blank=True)
    device_system = models.CharField(max_length=60, default='', verbose_name=u'操作系統', blank=True)
    status = models.IntegerField(default=0, choices=STATUS_VALUE, verbose_name=u'狀態值', blank=True)
    issued_time = models.DateTimeField(auto_now_add=True, verbose_name=u'上架時間', blank=True)
    created_user = models.IntegerField(default=0, verbose_name=u'創建用戶id', blank=True)
    availability_zone = models.CharField(max_length=30, default='', verbose_name=u'可用區編碼', blank=True)
    server_id = models.CharField(max_length=100, default='', verbose_name=u'雲服務器唯一標識ID', blank=True)
    power_state = models.IntegerField(default=0, choices=POWER_STATE, verbose_name=u'雲服務器電源狀態', blank=True)
    host = models.CharField(max_length=50, default='', verbose_name=u'雲服務器宿主名稱', blank=True)
    image_id = models.CharField(max_length=100, default='', verbose_name=u'雲服務器鏡像ID', blank=True)
    server_created = models.DateTimeField(null=True, verbose_name=u'雲服務器創建時間', blank=True)
    security_groups = models.CharField(max_length=50, default='', verbose_name=u'雲服務器所屬安全組名稱或者uuid', blank=True)
    server_launched_at = models.DateTimeField(null=True, verbose_name=u'雲服務器啓動時間', blank=True)
    server_updated = models.DateTimeField(null=True, verbose_name=u'雲服務器上一次更新時間', blank=True)
    server_status = models.CharField(max_length=20, default='', verbose_name=u'雲服務器當前狀態信息', blank=True)
    server_flavor = models.CharField(max_length=20, default='', verbose_name=u'雲服務器規格名稱', blank=True)
    server_vcpus = models.IntegerField(default=0, verbose_name=u'雲服務器規格對應的CPU核數', blank=True)
    server_ram = models.IntegerField(default=0, verbose_name=u'雲服務器規格對應的內存大小,單位爲MB', blank=True)
    disk_used_percent = models.CharField(max_length=15, default='', verbose_name=u'磁盤使用率', blank=True)

    @staticmethod
    def get_one(id):
        return DevicesInfo.objects.filter(id=id)

    @staticmethod
    def get_list(data_filter):
        """
        獲取特定條件的事件列表,如爲空則獲取全部
        :param data_filter:
        :return:
        """
        # 獲取篩選條件
        con = DevicesInfo.get_query(data_filter)

        # 獲取記錄(倒序)
        res = DevicesInfo.objects.filter(con).order_by('-id')
        return res

    @staticmethod
    def add(data):
        """
        創建一條記錄,先獲取,如存在則更新,如不存在則新建
        :param data:  dict 數據
        :return:
        """
        server_id = data['server_id']

        res = DevicesInfo.objects.update_or_create(server_id=server_id, defaults=data)

        return 0, 'success', res

    @staticmethod
    def get_query(data_filter):
        """
        獲取搜索條件
        :param data_filter:
        :return:
        """
        con = Q()
        # 狀態搜索
        if 'status' in data_filter:
            con.children.append(('status', data_filter['status']))

        # id搜索
        if 'id' in data_filter:
            con.children.append(('id', data_filter['id']))

        # 根據app_id搜索device
        if 'device_id_list' in data_filter:
            con.children.append(('id__in', data_filter['device_id_list']))

        #  主機名模糊搜索(單個主機或多個主機模糊搜索)
        if 'name' in data_filter:
            con.children.append(('name__contains', data_filter['name']))
        elif 'name_list' in data_filter:
            if data_filter['name_list']:
                q1 = Q()
                q1.connector = 'OR'
                for single_name in data_filter['name_list']:
                    q1.children.append(('name__contains', single_name))
                con.add(q1, 'AND')

        # ip模糊搜索
        if 'ip' in data_filter:
            q1 = Q()
            q1.connector = 'OR'
            q1.children.append(('lan_ip__contains', data_filter['ip']))
            q1.children.append(('wan_ip__contains', data_filter['ip']))
            con.add(q1, 'AND')

        #  區域名搜索
        if 'area_name' in data_filter:
            con.children.append(('area_name', data_filter['area_name']))

        # 事件時間篩選,篩選出時間>=from的
        if 'time_from' in data_filter:
            con.children.append(('server_created__gte', data_filter['time_from']))

        # 事件時間篩選,篩選出時間<=to+1的
        if 'time_to' in data_filter:
            data_filter['time_to'] += ' 23:59:59'
            con.children.append(('server_created__lte', data_filter['time_to']))

        # 默認搜索delete_at爲空的數據
        con.children.append(('deleted_at', None))

        return con

    class Meta:
        db_table = 'devices_info'

更多模型的複雜查詢可以參考:【數據庫】Django數據庫的詳細文檔【原創】

 

app目錄下新增serializers目錄,新增文件:

app\serializers\devices_info_serializers.py

from rest_framework import serializers
from app.models.devices_info import DevicesInfo


class DevicesInfoSerializer(serializers.ModelSerializer):
    """
    序列化DevicesInfo表數據
    """
    created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
    updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
    issued_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
    server_created = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
    server_launched_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
    server_updated = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")

    class Meta:
        model = DevicesInfo
        fields = '__all__'

 

app目錄下services目錄新增文件:

app\services\devices_service.py

import logging
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from app.models.devices_info import DevicesInfo
from app.serializers.devices_info_serializers import DevicesInfoSerializer

logger = logging.getLogger(__name__)


class DeviceService:
    def __init__(self):
        pass

    @staticmethod
    def get_list(data_filter, is_page=0, page_num=1, page_size=15):
        """
        獲取主機列表
        :param data_filter:
        :param is_page: 是否分頁
        :param page_num:
        :param page_size:
        :return:
        """
        data = DevicesInfo.get_list(data_filter)
        count = data.count()
        pages_num = 1

        # 是否分頁
        if is_page:
            paginator = Paginator(data, page_size)

            try:
                data = paginator.page(page_num)
            except PageNotAnInteger:
                # 如果請求的頁數不是整數,返回第一頁。
                data = paginator.page(1)
            except EmptyPage:
                # 如果請求的頁數不在合法的頁數範圍內,返回結果的最後一頁。
                data = paginator.page(paginator.num_pages)

            count = paginator.count
            pages_num = paginator.num_pages

        # 序列化數據庫數據
        res = DevicesInfoSerializer(data, many=True)

        data = {
            'data': res.data,
            'count': count,
            'pages_num': pages_num
        }
        return data

 

app的views.py:

from common.functions import render_json
from django.views.decorators.csrf import csrf_exempt
from app.services.devices_service import DeviceService
import json


@csrf_exempt
def hello(request):
    return render_json(0, 'success', [])


@csrf_exempt
def get_devices(request):
    """
    獲取列表
    :param request:
    :return:
    """
    if request.method == 'POST':
        if request.body:
            request_data = json.loads(request.body.decode())
            page_size = request_data.get('pageSize', 15)
            page_num = request_data.get('pageNo', 1)
            is_page = request_data.get('is_page', 0)
            status = request_data.get('status', '')
            name = request_data.get('name', '')
            server_id = request_data.get('server_id', '')
            name_list = request_data.get('name_list', [])

            data_filter = {}
            if status:
                data_filter['status'] = status
            if name:
                data_filter['name'] = name
            if server_id:
                data_filter['server_id'] = server_id
            if name_list:
                data_filter['name_list'] = name_list

            res = DeviceService.get_list(data_filter, is_page, page_num, page_size)

            data = {
                'data': res['data'],
                'pageSize': int(page_size),
                'pageNo': int(page_num),
                'totalPage': res['pages_num'],
                'totalCount': res['count'],
            }

            return render_json(data=data)
        else:
            return render_json(code=200, msg='請求數據不能爲空')
    else:
        return render_json(code=100, msg='請求方式錯誤')

 

app/urls.py:

from django.urls import path
from app import views

urlpatterns = [
    path('hello', views.hello),
    path('get_devices', views.get_devices),
]

這樣就實現了一個簡單的API接口,列表接口,支持分頁,並且返回的字段是有經過序列化的(主要是時間字段),支持複雜的查詢

可以通過代碼倉庫中的sql目錄來導入測試的數據進行測試

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章