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個用戶是激活狀態的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM