Django工程的分層結構

来源:https://www.cnblogs.com/rexcheny/archive/2019/07/18/11207630.html
-Advertisement-
Play Games

Django工程的分層結構,網上大部分代碼都是功能性的,當你面對稍微複雜一點的場景就應該考慮代碼分層,頁面、路由、表單驗證、業務邏輯、數據應該如何安排,看完這篇文章你或許就有了思路。 ...


前言

傳統上我們都知道在Django中的MTV模式,具體內容含義我們再來回顧一下:

  • M:是Model的簡稱,它的目標就是通過定義模型來處理和資料庫進行交互,有了這一層或者這種類型的對象,我們就可以通過對象來操作數據。

  • V:是View的簡稱,它的工作很少,就是接受用戶請求換句話說就是通過HTTP請求接受用戶的輸入;另外把輸入信息發送給處理程並獲取結果;最後把結果發送給用戶,當然最後這一步還可以使用模板來修飾數據。

  • T:是Template的簡稱,這裡主要是通過標記語言來定義頁面,另外還可以嵌入模板語言讓引擎來渲染動態數據。

這時候我們看到網上大多數的列子包括有些視頻課程裡面只講MVT以及語法和其他功能實現等,但大家有沒有想過一個問題,你的業務邏輯放在哪裡?課程中的邏輯通常放在了View裡面,就像下麵:

# urls.py
path('hello/', Hello),
path('helloworld/', HelloWorld.as_view())

# View
from django.views import View

# FVB
def Hello(request):
    if request.method == "GET":
        return HttpResponse("Hello world")

# CVB
class HelloWorld(View):
    def get(self, request):
        pass
    def post(self, request):
        pass

無論是FBV還是CBV,當用戶請求進來並通過URL路由找到對應的方法或者類,然後對請求進行處理,比如可以直接返回模型數據、驗證用戶輸入或者校驗用戶名和密碼等。在學習階段或者功能非常簡單的時候使用這種寫法沒問題,但是對於相對大一點的項目來說你很多具體的處理流程開始出現,而這些東西都寫到View里顯然你自己都看不下去。

FBV全名Function-based views,基於函數的視圖;CBV全名Class-based views,基於類的視圖

所以View,它就是一個控制器,它不應該包含業務邏輯,事實上它應該是一個很薄的層。

業務邏輯到底放哪裡

網上也有很多文章回答了這個問題,提到了Form層,這個其實是用於驗證用戶輸入數據的格式,比如郵件地址是否正確、是否填寫了用戶名和密碼,至於這個用戶名或者郵箱到底在資料庫中是否真實存在則不是它應該關心的,它只是一個數據格式驗證器。所以業務邏輯到底放哪裡呢?顯然要引入另外一層。

關於這一層的名稱有些人叫做UseCase,也有些人叫做Service,至於什麼名字無所謂只要是大家一看就明白的名稱就好。如果我們使用UseCase這個名字,那麼我們的Djaong工程架構就變成了MUVT,如果是Service那麼就MSVT。

這一層的目標是什麼呢?它專註於具體業務邏輯,也就是不同用例的具體操作,比如用戶註冊、登陸和註銷都一個用例。所有模型都只是工作流程的一部分並且這一層也知道模型有哪些API。這麼說有些空洞,我們用一個例子來說明:

場景是用戶註冊:

  1. 信息填寫規範且用戶不存在則註冊成功併發送賬戶激活郵件

  2. 如果用戶已存在則程式引發錯誤,然後傳遞到上層併進行告知用戶名已被占用

Django 2.2.1、Python 3.7

下圖是整個工程的結構

Models層

models.py

from django.db import models
from django.utils.translation import gettext as _

# Create your models here.

from django.contrib.auth.models import AbstractUser, UserManager, User

class UserAccountManager(UserManager):
    # 管理器
    def find_by_username(self, username):
        queryset = self.get_queryset()
        return queryset.filter(username=username)


class UserAccount(AbstractUser):
    # 擴展一個欄位,家庭住址
    home_address = models.CharField(_('home address'), max_length=150, blank=True)
    # 賬戶是否被激活,與users表裡預設的is_active不是一回事
    is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新賬戶註冊後是否通過郵件驗證激活。'),)

    # 指定該模型的manager類
    objects = UserAccountManager()

我們知道Django會為我們自動建立一個叫做auth_user的表,也就是它自己的認證內容,這個user表本身就是一個模型,它就是繼承了AbstractUser類,而這個類有繼承了AbstractBaseUser,而這個類繼承了models.Model,所以我們這裡就是一個模型。再說回AbstractUser類,這個類裡面定義了一些username、first_name、email、is_active等用戶屬性相關的欄位,如果你覺得不夠用還可以自己擴展。

為了讓Django使用我們擴展的用戶模型,所以需要在settings.py中添加如下內容:

AUTH_USER_MODEL = "users.UserAccount"

工具類

這個文件主要是放一些通用工具,比如發送郵件這種公共會調用的功能,utils.py內容如下:

from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from mysite import settings


class TokenGenerator(PasswordResetTokenGenerator):
    def __init__(self):
        super(TokenGenerator, self).__init__()

    # def _make_hash_value(self, user, timestamp):
    #     return (
    #         six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)
    #     )


class WelcomeEmail:
    subject = 'Activate Your Account'

    @classmethod
    def send_to(cls, request, user_account):
        try:
            current_site = get_current_site(request)
            account_activation_token = TokenGenerator()
            message = render_to_string('activate_account.html', {
                'username': user_account.username,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user_account.id)),
                'token': account_activation_token.make_token(user_account),
            })

            send_mail(
                subject=cls.subject,
                message=message,
                from_email=settings.EMAIL_HOST_USER,
                recipient_list=[user_account.email]
            )
        except Exception as err:
            print(err)

TokenGenerator這個東西使用還是它父類本身的功能,之所以這樣做是為了在必要的時候可以重寫一些功能。父類PasswordResetTokenGenerator的功能主要是根據用戶主鍵來生成token,之後還會根據傳遞的token和用戶主鍵去檢查傳遞的token是否一致。

針對郵件發送我這裡使用Django提供的封裝,你需要在settings.py中添加如下內容:

# 郵件設置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = ''   # 發件人郵箱地址
EMAIL_HOST_PASSWORD = ''  # 發件人郵箱密碼
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

Services層

這層主要是根據用例來實現業務邏輯,比如註冊用戶賬號和激活用戶賬號。

"""
Service層,針對不同用例實現的業務邏輯代碼
"""
from django.utils.translation import gettext as _
from django.shortcuts import render
from .utils import (
    WelcomeEmail,
    TokenGenerator,
)
from users.models import (
    UserAccount
)


class UsernameAlreadyExistError(Exception):
    pass


class UserIdIsNotExistError(Exception):
    """
    用戶ID,主鍵不存在
    """
    pass


class ActivatitionTokenError(Exception):
    pass


class RegisterUserAccount:
    def __init__(self, request, username, password, confirm_password, email):
        self._username = username
        self._password = password
        self._email = email
        self._request = request

    def valid_data(self):
        """
        檢查用戶名是否已經被註冊
        :return:
        """
        user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
        if user_query_set:
            error_msg = ('用戶名 {} 已被註冊,請更換。'.format(self._username))
            raise UsernameAlreadyExistError(_(error_msg))
        return True

    def _send_welcome_email_to(self, user_account):
        """
        註冊成功後發送電子郵件
        :param user_account:
        :return:
        """
        WelcomeEmail.send_to(self._request, user_account)

    def execute(self):
        self.valid_data()
        user_account = self._factory_user_account()
        self._send_welcome_email_to(user_account)
        return user_account

    def _factory_user_account(self):
        """
        這裡是創建用戶
        :return:
        """

        # 這樣創建需要調用save()
        # ua = UserAccount(username=self._username, password=self._password, email=self._email)
        # ua.save()
        # return ua

        # 直接通過create_user則不需要調用save()
        return UserAccount.objects.create_user(
            self._username,
            self._email,
            self._password,
        )


class ActivateUserAccount:
    def __init__(self, uid, token):
        self._uid = uid
        self._token = token

    def _account_valid(self):
        """
        驗證用戶是否存在
        :return: 模型對象或者None
        """
        return UserAccount.objects.all().get(id=self._uid)

    def execute(self):
        # 查詢是否有用戶
        user_account = self._account_valid()
        account_activation_token = TokenGenerator()
        if user_account is None:
            error_msg = ('激活用戶失敗,提供的用戶標識 {} 不正確,無此用戶。'.format(self._uid))
            raise UserIdIsNotExistError(_(error_msg))

        if not account_activation_token.check_token(user_account, self._token):
            error_msg = ('激活用戶失敗,提供的Token {} 不正確。'.format(self._token))
            raise ActivatitionTokenError(_(error_msg))

        user_account.is_activated = True
        user_account.save()
        return True

這裡定義的異常類比如UsernameAlreadyExistError等裡面的內容就是空的,目的是raise異常到自定義的異常中,這樣調用方通過try就可以捕獲,有些時候代碼執行的結果影響調用方後續的處理,通常大家可能認為需要通過返回值來判斷,比如True或者False,但通常這不是一個好辦法或者說在有些時候不是,因為那樣會造成代碼冗長,比如下麵的代碼:

這是上面代碼中的一部分,

    def valid_data(self):
        """
        檢查用戶名是否已經被註冊
        :return:
        """
        user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
        if user_query_set:
            error_msg = ('用戶名 {} 已被註冊,請更換。'.format(self._username))
            raise UsernameAlreadyExistError(_(error_msg))
        return True

    def execute(self):
        self.valid_data()
        user_account = self._factory_user_account()
        self._send_welcome_email_to(user_account)
        return user_account

execute函數會執行valid_data()函數,如果執行成功我才會向下執行,可是你看我在execute函數中並沒有這樣的語句,比如:

def execute(self):
    if self.valid_data():
        user_account = self._factory_user_account()
        self._send_welcome_email_to(user_account)
        return user_account
    else:
        pass

換句話說你的每個函數都可能有返回值,如果每一個你都這樣寫代碼就太啰嗦了。其實你可以看到在valid_data函數中我的確返回了True,但是我希望你也應該註意,如果用戶存在的話我並沒有返回False,而是raise一個異常,這樣這個異常就會被調用方獲取而且還能獲取錯誤信息,這種方式將是一個很好的處理方式,具體你可以通過views.py中看到。

Forms表單驗證

這裡是對於用戶輸入做檢查

"""
表單驗證功能
"""
from django import forms
from django.utils.translation import gettext as _


class RegisterAccountForm(forms.Form):
    username = forms.CharField(max_length=50, required=True, error_messages={
        'max_length': '用戶名不能超過50個字元',
        'required': '用戶名不能為空',
    })
    
    email = forms.EmailField(required=True)
    password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
    confirm_password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())

    def clean_confirm_password(self) -> str:  # -> str 表示的含義是函數返回值類型是str,在列印函數annotation的時候回顯示。
        """
        clean_XXXX XXXX是欄位名
        比如這個方法是判斷兩次密碼是否一致,密碼框輸入的密碼就算符合規則但是也不代表兩個密碼一致,所以需要自己來進行檢測
        :return:
        """
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')

        if confirm_password != password:
            raise forms.ValidationError(message='Password and confirmation do not match each other')

        return confirm_password

前端可以實現輸入驗證,但是也很容易被跳過,所以後端肯定也需要進行操作,當然我這裡並沒有做預防XSS攻擊的措施,因為這個不是我們今天要討論的主要內容。

Views

from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from rest_framework.views import APIView
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from .forms import (
    RegisterAccountForm,
)
from .services import (
    RegisterUserAccount,
    UsernameAlreadyExistError,
    ActivateUserAccount,
    ActivatitionTokenError,
    UserIdIsNotExistError,
)
# Create your views here.


class Register(APIView):
    def get(self, request):
        return render(request, 'register.html')

    def post(self, request):
        # print("request.data 的內容: ", request.data)
        # print("request.POST 的內容: ", request.POST)

        # 針對數據輸入做檢查,是否符合規則
        ra_form = RegisterAccountForm(request.POST)
        if ra_form.is_valid():
            # print("驗證過的數據:", ra_form.cleaned_data)
            rua = RegisterUserAccount(request=request, **ra_form.cleaned_data)
            try:
                rua.execute()
            except UsernameAlreadyExistError as err:
                # 這裡就是捕獲自定義異常,並給form對象添加一個錯誤信息,並通過模板渲染然後返回前端頁面
                ra_form.add_error('username', str(err))
                return render(request, 'register.html', {'info': ra_form.errors})

            return HttpResponse('We have sent you an email, please confirm your email address to complete registration')
            # return HttpResponseRedirect("/account/login/")
        else:
            return render(request, 'register.html', {'info': ra_form.errors})


class Login(APIView):
    def get(self, request):
        return render(request, 'login.html')

    def post(self, request):
        print("request.data 的內容: ", request.data)
        print("request.POST 的內容: ", request.POST)
        pass


class ActivateAccount(APIView):
    # 用戶激活賬戶
    def get(self, request, uidb64, token):
        try:
            # 獲取URL中的用戶ID
            uid = force_bytes(urlsafe_base64_decode(uidb64))
            # 激活用戶
            aua = ActivateUserAccount(uid, token)
            aua.execute()
            return render(request, 'login.html')
        except(ActivatitionTokenError, UserIdIsNotExistError) as err:
            return HttpResponse('Activation is failed.')

這裡就是視圖層不同URL由不同的類來處理,這裡只做基本的接收輸入和返回輸出功能,至於接收到的輸入該如何處理則有其他組件來完成,針對輸入格式規範則由forms中的類來處理,針對數據驗證過後的具體業務邏輯則由services中的類來處理。

Urls

from django.urls import path, re_path, include
from .views import (
    Register,
    Login,
    ActivateAccount,
)


app_name = 'users'
urlpatterns = [
    re_path(r'^register/$', Register.as_view(), name='register'),
    re_path(r'^login/$', Login.as_view(), name='login'),
    re_path(r'^activate/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
            ActivateAccount.as_view(), name='activate'),
]

Templates

是我用到的html模板,我就不放在這裡了,大家可以去這裡https://files.cnblogs.com/files/rexcheny/mysite.zip

下載全部的代碼

頁面效果

激活郵件內容

點擊後就會跳轉到登陸頁。下麵我們從Django admin中查看,2個用戶是激活狀態的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、wx:showActionSheet(上拉菜單) 二、wx:showModal(彈窗) 三、showToast / hideToast(載入) Object object 屬性類型預設值必填說明 success function 否 介面調用成功的回調函數 fail function 否 介面調 ...
  • 普通的插槽裡面的數據是在父組件里定義的,而作用域插槽里的數據是在子組件定義的。 有時候作用域插槽很有用,比如使用Element-ui表格自定義模板時就用到了作用域插槽,Element-ui定義了每個單元格數據的顯示格式,我們可以通過作用域插槽自定義數據的顯示格式,對於二次開發來說具有很強的擴展性。 ...
  • express中間件分成三種 內置中間件 static 自定義中間件 第三方中間件 (body-parser) (攔截器) 全局自定義中間件 在請求介面時 有幾個介面都要驗證傳來的內容是否存在或者是否正確 不可能每個介面都寫一段驗證,所以這一段驗證可以提取出來,通過中間件實現 const expre ...
  • CSS Grid網格佈局全攻略 所有奇技淫巧都只在方寸之間。 幾乎從我們踏入前端開發這個領域開始,就不停地接觸不同的佈局技術。從常見的浮動到表格佈局,再到如今大行其道的flex佈局,css佈局技術一直在不斷地推陳出新。其中網格佈局(grid)作為css3的產物,它更加貼近網頁設計師所使用的佈局策略, ...
  • javascript實現父子頁面相互調用 By:授客 QQ:1033553122 場景1 父頁面調用子頁面 如上圖,在iframe子頁面的<script>元素中,定義了taskStatus全局變數,如果希望在其父頁面中獲取該全局變數的值,則可在父頁面的<script>元素中新增js腳本如下: var ...
  • 當前IT的發展,已經成為我國的重大產業之一,很多的企業為了爭奪優秀的專業人才,不惜給出豐厚的薪資待遇。據知名部門統計,每年IT行業的人才缺口可達到數百萬,尤其是前端軟體人才的缺口。而我國信息化人才培養還處於發展階段,導致社會實際需求人才基數遠遠大於信息化人才的培養基數,使得數以萬計的中小企業急需全面 ...
  • 抖音聯盟,抖友會,抖音聯盟會員,抖音聯盟學員,抖音批量做號團隊,工作室帶隊,聯盟學員統一官網認證可查,統一變現渠道擔保,成熟技術技術後盾,實時工作室真機實測規則,抖音情感勵志書單模式2.0升級,拒絕落後規則。 歡迎每一位同學跟著我們工作室一起在抖音發展,更歡迎能批量帶隊者和人才加入我們,抖音聯盟期待 ...
  • 一、生活場景簡介 1、引入場景 2、源代碼實現 1)、關係圖譜 2)、代碼實現 3、缺點分析 1)、OCP原則:軟體實體,如類、模塊和函數,應當對擴展開放,但對修改關閉。 2)、違反設計模式的OCP原則,新增食品類不方便擴展,代碼改動較大。 二、簡單工廠模式 1、基本概念 簡單工廠模式是屬於創建型模 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...