734 lines
21 KiB
Markdown
734 lines
21 KiB
Markdown
|
|
---
|
|||
|
|
name: django-patterns
|
|||
|
|
description: Django 架構模式、使用 DRF 的 REST API 設計、ORM 最佳實踐、快取、信號 (Signals)、中間件 (Middleware) 以及生產等級的 Django 應用程序。
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Django 開發模式 (Django Development Patterns)
|
|||
|
|
|
|||
|
|
適用於可擴充、易維護應用程序的生產等級 Django 架構模式。
|
|||
|
|
|
|||
|
|
## 何時啟用
|
|||
|
|
|
|||
|
|
- 建構 Django 網頁應用程序。
|
|||
|
|
- 設計 Django REST Framework (DRF) API。
|
|||
|
|
- 使用 Django ORM 和模型 (Models)。
|
|||
|
|
- 設定 Django 專案結構。
|
|||
|
|
- 實作快取、信號 (Signals)、中間件 (Middleware)。
|
|||
|
|
|
|||
|
|
## 專案結構
|
|||
|
|
|
|||
|
|
### 建議版面配置 (Recommended Layout)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
myproject/
|
|||
|
|
├── config/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ ├── settings/
|
|||
|
|
│ │ ├── __init__.py
|
|||
|
|
│ │ ├── base.py # 基礎設定
|
|||
|
|
│ │ ├── development.py # 開發環境設定
|
|||
|
|
│ │ ├── production.py # 生產環境設定
|
|||
|
|
│ │ └── test.py # 測試環境設定
|
|||
|
|
│ ├── urls.py
|
|||
|
|
│ ├── wsgi.py
|
|||
|
|
│ └── asgi.py
|
|||
|
|
├── manage.py
|
|||
|
|
└── apps/
|
|||
|
|
├── __init__.py
|
|||
|
|
├── users/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ ├── models.py
|
|||
|
|
│ ├── views.py
|
|||
|
|
│ ├── serializers.py
|
|||
|
|
│ ├── urls.py
|
|||
|
|
│ ├── permissions.py
|
|||
|
|
│ ├── filters.py
|
|||
|
|
│ ├── services.py
|
|||
|
|
│ └── tests/
|
|||
|
|
└── products/
|
|||
|
|
└── ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 設定檔拆分模式 (Split Settings Pattern)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# config/settings/base.py
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|||
|
|
|
|||
|
|
SECRET_KEY = env('DJANGO_SECRET_KEY')
|
|||
|
|
DEBUG = False
|
|||
|
|
ALLOWED_HOSTS = []
|
|||
|
|
|
|||
|
|
INSTALLED_APPS = [
|
|||
|
|
'django.contrib.admin',
|
|||
|
|
'django.contrib.auth',
|
|||
|
|
'django.contrib.contenttypes',
|
|||
|
|
'django.contrib.sessions',
|
|||
|
|
'django.contrib.messages',
|
|||
|
|
'django.contrib.staticfiles',
|
|||
|
|
'rest_framework',
|
|||
|
|
'rest_framework.authtoken',
|
|||
|
|
'corsheaders',
|
|||
|
|
# 在地 App
|
|||
|
|
'apps.users',
|
|||
|
|
'apps.products',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
MIDDLEWARE = [
|
|||
|
|
'django.middleware.security.SecurityMiddleware',
|
|||
|
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
|||
|
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
|||
|
|
'corsheaders.middleware.CorsMiddleware',
|
|||
|
|
'django.middleware.common.CommonMiddleware',
|
|||
|
|
'django.middleware.csrf.CsrfViewMiddleware',
|
|||
|
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|||
|
|
'django.contrib.messages.middleware.MessageMiddleware',
|
|||
|
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
ROOT_URLCONF = 'config.urls'
|
|||
|
|
WSGI_APPLICATION = 'config.wsgi.application'
|
|||
|
|
|
|||
|
|
DATABASES = {
|
|||
|
|
'default': {
|
|||
|
|
'ENGINE': 'django.db.backends.postgresql',
|
|||
|
|
'NAME': env('DB_NAME'),
|
|||
|
|
'USER': env('DB_USER'),
|
|||
|
|
'PASSWORD': env('DB_PASSWORD'),
|
|||
|
|
'HOST': env('DB_HOST'),
|
|||
|
|
'PORT': env('DB_PORT', default='5432'),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# config/settings/development.py (開發環境)
|
|||
|
|
from .base import *
|
|||
|
|
|
|||
|
|
DEBUG = True
|
|||
|
|
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
|
|||
|
|
|
|||
|
|
DATABASES['default']['NAME'] = 'myproject_dev'
|
|||
|
|
|
|||
|
|
INSTALLED_APPS += ['debug_toolbar']
|
|||
|
|
|
|||
|
|
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
|
|||
|
|
|
|||
|
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|||
|
|
|
|||
|
|
# config/settings/production.py (生產環境)
|
|||
|
|
from .base import *
|
|||
|
|
|
|||
|
|
DEBUG = False
|
|||
|
|
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
|
|||
|
|
SECURE_SSL_REDIRECT = True
|
|||
|
|
SESSION_COOKIE_SECURE = True
|
|||
|
|
CSRF_COOKIE_SECURE = True
|
|||
|
|
SECURE_HSTS_SECONDS = 31536000
|
|||
|
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|||
|
|
SECURE_HSTS_PRELOAD = True
|
|||
|
|
|
|||
|
|
# 記錄日誌 (Logging)
|
|||
|
|
LOGGING = {
|
|||
|
|
'version': 1,
|
|||
|
|
'disable_existing_loggers': False,
|
|||
|
|
'handlers': {
|
|||
|
|
'file': {
|
|||
|
|
'level': 'WARNING',
|
|||
|
|
'class': 'logging.FileHandler',
|
|||
|
|
'filename': '/var/log/django/django.log',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
'loggers': {
|
|||
|
|
'django': {
|
|||
|
|
'handlers': ['file'],
|
|||
|
|
'level': 'WARNING',
|
|||
|
|
'propagate': True,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 模型設計模式 (Model Design Patterns)
|
|||
|
|
|
|||
|
|
### 模型最佳實踐 (Model Best Practices)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from django.db import models
|
|||
|
|
from django.contrib.auth.models import AbstractUser
|
|||
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|||
|
|
|
|||
|
|
class User(AbstractUser):
|
|||
|
|
"""擴展 AbstractUser 的自定義使用者模型。"""
|
|||
|
|
email = models.EmailField(unique=True)
|
|||
|
|
phone = models.CharField(max_length=20, blank=True)
|
|||
|
|
birth_date = models.DateField(null=True, blank=True)
|
|||
|
|
|
|||
|
|
USERNAME_FIELD = 'email'
|
|||
|
|
REQUIRED_FIELDS = ['username']
|
|||
|
|
|
|||
|
|
class Meta:
|
|||
|
|
db_table = 'users'
|
|||
|
|
verbose_name = 'user'
|
|||
|
|
verbose_name_plural = 'users'
|
|||
|
|
ordering = ['-date_joined']
|
|||
|
|
|
|||
|
|
def __str__(self):
|
|||
|
|
return self.email
|
|||
|
|
|
|||
|
|
def get_full_name(self):
|
|||
|
|
return f"{self.first_name} {self.last_name}".strip()
|
|||
|
|
|
|||
|
|
class Product(models.Model):
|
|||
|
|
"""具備正確欄位配置的產品模型。"""
|
|||
|
|
name = models.CharField(max_length=200)
|
|||
|
|
slug = models.SlugField(unique=True, max_length=250)
|
|||
|
|
description = models.TextField(blank=True)
|
|||
|
|
price = models.DecimalField(
|
|||
|
|
max_digits=10,
|
|||
|
|
decimal_places=2,
|
|||
|
|
validators=[MinValueValidator(0)]
|
|||
|
|
)
|
|||
|
|
stock = models.PositiveIntegerField(default=0)
|
|||
|
|
is_active = models.BooleanField(default=True)
|
|||
|
|
category = models.ForeignKey(
|
|||
|
|
'Category',
|
|||
|
|
on_delete=models.CASCADE,
|
|||
|
|
related_name='products'
|
|||
|
|
)
|
|||
|
|
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
|
|||
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|||
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|||
|
|
|
|||
|
|
class Meta:
|
|||
|
|
db_table = 'products'
|
|||
|
|
ordering = ['-created_at']
|
|||
|
|
indexes = [
|
|||
|
|
models.Index(fields=['slug']),
|
|||
|
|
models.Index(fields=['-created_at']),
|
|||
|
|
models.Index(fields=['category', 'is_active']),
|
|||
|
|
]
|
|||
|
|
constraints = [
|
|||
|
|
models.CheckConstraint(
|
|||
|
|
check=models.Q(price__gte=0),
|
|||
|
|
name='price_non_negative'
|
|||
|
|
)
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def __str__(self):
|
|||
|
|
return self.name
|
|||
|
|
|
|||
|
|
def save(self, *args, **kwargs):
|
|||
|
|
if not self.slug:
|
|||
|
|
self.slug = slugify(self.name)
|
|||
|
|
super().save(*args, **kwargs)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### QuerySet 最佳實踐
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from django.db import models
|
|||
|
|
|
|||
|
|
class ProductQuerySet(models.QuerySet):
|
|||
|
|
"""產品模型的自定義 QuerySet。"""
|
|||
|
|
|
|||
|
|
def active(self):
|
|||
|
|
"""僅回傳啟用的產品。"""
|
|||
|
|
return self.filter(is_active=True)
|
|||
|
|
|
|||
|
|
def with_category(self):
|
|||
|
|
"""預先選取相關類別以避免 N+1 查詢問題。"""
|
|||
|
|
return self.select_related('category')
|
|||
|
|
|
|||
|
|
def with_tags(self):
|
|||
|
|
"""針對多對多關係預先抓取 (Prefetch) 標籤。"""
|
|||
|
|
return self.prefetch_related('tags')
|
|||
|
|
|
|||
|
|
def in_stock(self):
|
|||
|
|
"""回傳庫存量大於 0 的產品。"""
|
|||
|
|
return self.filter(stock__gt=0)
|
|||
|
|
|
|||
|
|
def search(self, query):
|
|||
|
|
"""依據名稱或描述搜尋產品。"""
|
|||
|
|
return self.filter(
|
|||
|
|
models.Q(name__icontains=query) |
|
|||
|
|
models.Q(description__icontains=query)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
class Product(models.Model):
|
|||
|
|
# ... 欄位定義 ...
|
|||
|
|
|
|||
|
|
objects = ProductQuerySet.as_manager() # 使用自定義 QuerySet
|
|||
|
|
|
|||
|
|
# 使用範例
|
|||
|
|
Product.objects.active().with_category().in_stock()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 管理員 (Manager) 方法
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class ProductManager(models.Manager):
|
|||
|
|
"""用於複雜查詢的自定義管理員。"""
|
|||
|
|
|
|||
|
|
def get_or_none(self, **kwargs):
|
|||
|
|
"""回傳物件或 None,而非抛出 DoesNotExist 異常。"""
|
|||
|
|
try:
|
|||
|
|
return self.get(**kwargs)
|
|||
|
|
except self.model.DoesNotExist:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def create_with_tags(self, name, price, tag_names):
|
|||
|
|
"""建立產品及其關聯標籤。"""
|
|||
|
|
product = self.create(name=name, price=price)
|
|||
|
|
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
|
|||
|
|
product.tags.set(tags)
|
|||
|
|
return product
|
|||
|
|
|
|||
|
|
def bulk_update_stock(self, product_ids, quantity):
|
|||
|
|
"""批次更新多個產品的庫存。"""
|
|||
|
|
return self.filter(id__in=product_ids).update(stock=quantity)
|
|||
|
|
|
|||
|
|
# 在模型中使用
|
|||
|
|
class Product(models.Model):
|
|||
|
|
# ... 欄位 ...
|
|||
|
|
custom = ProductManager()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Django REST Framework 模式
|
|||
|
|
|
|||
|
|
### 序列化程式 (Serializer) 模式
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from rest_framework import serializers
|
|||
|
|
from django.contrib.auth.password_validation import validate_password
|
|||
|
|
from .models import Product, User
|
|||
|
|
|
|||
|
|
class ProductSerializer(serializers.ModelSerializer):
|
|||
|
|
"""產品模型的序列化程式。"""
|
|||
|
|
|
|||
|
|
category_name = serializers.CharField(source='category.name', read_only=True)
|
|||
|
|
average_rating = serializers.FloatField(read_only=True)
|
|||
|
|
discount_price = serializers.SerializerMethodField()
|
|||
|
|
|
|||
|
|
class Meta:
|
|||
|
|
model = Product
|
|||
|
|
fields = [
|
|||
|
|
'id', 'name', 'slug', 'description', 'price',
|
|||
|
|
'discount_price', 'stock', 'category_name',
|
|||
|
|
'average_rating', 'created_at'
|
|||
|
|
]
|
|||
|
|
read_only_fields = ['id', 'slug', 'created_at']
|
|||
|
|
|
|||
|
|
def get_discount_price(self, obj):
|
|||
|
|
"""計算適用折扣後的價格。"""
|
|||
|
|
if hasattr(obj, 'discount') and obj.discount:
|
|||
|
|
return obj.price * (1 - obj.discount.percent / 100)
|
|||
|
|
return obj.price
|
|||
|
|
|
|||
|
|
def validate_price(self, value):
|
|||
|
|
"""確保價格為非負數。"""
|
|||
|
|
if value < 0:
|
|||
|
|
raise serializers.ValidationError("價格不能為負數。")
|
|||
|
|
return value
|
|||
|
|
|
|||
|
|
class ProductCreateSerializer(serializers.ModelSerializer):
|
|||
|
|
"""用於建立產品的序列化程式。"""
|
|||
|
|
|
|||
|
|
class Meta:
|
|||
|
|
model = Product
|
|||
|
|
fields = ['name', 'description', 'price', 'stock', 'category']
|
|||
|
|
|
|||
|
|
def validate(self, data):
|
|||
|
|
"""針對多個欄位的自定義驗證。"""
|
|||
|
|
if data['price'] > 10000 and data['stock'] > 100:
|
|||
|
|
raise serializers.ValidationError(
|
|||
|
|
"高價產品不應有過多庫存。"
|
|||
|
|
)
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
class UserRegistrationSerializer(serializers.ModelSerializer):
|
|||
|
|
"""使用者註冊序列化程式。"""
|
|||
|
|
|
|||
|
|
password = serializers.CharField(
|
|||
|
|
write_only=True,
|
|||
|
|
required=True,
|
|||
|
|
validators=[validate_password],
|
|||
|
|
style={'input_type': 'password'}
|
|||
|
|
)
|
|||
|
|
password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'})
|
|||
|
|
|
|||
|
|
class Meta:
|
|||
|
|
model = User
|
|||
|
|
fields = ['email', 'username', 'password', 'password_confirm']
|
|||
|
|
|
|||
|
|
def validate(self, data):
|
|||
|
|
"""驗證兩次輸入的密碼是否一致。"""
|
|||
|
|
if data['password'] != data['password_confirm']:
|
|||
|
|
raise serializers.ValidationError({
|
|||
|
|
"password_confirm": "密碼欄位不一致。"
|
|||
|
|
})
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
def create(self, validated_data):
|
|||
|
|
"""建立帶有雜湊密碼的使用者。"""
|
|||
|
|
validated_data.pop('password_confirm')
|
|||
|
|
password = validated_data.pop('password')
|
|||
|
|
user = User.objects.create(**validated_data)
|
|||
|
|
user.set_password(password)
|
|||
|
|
user.save()
|
|||
|
|
return user
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ViewSet 模式
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from rest_framework import viewsets, status, filters
|
|||
|
|
from rest_framework.decorators import action
|
|||
|
|
from rest_framework.response import Response
|
|||
|
|
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
|||
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|||
|
|
from .models import Product
|
|||
|
|
from .serializers import ProductSerializer, ProductCreateSerializer
|
|||
|
|
from .permissions import IsOwnerOrReadOnly
|
|||
|
|
from .filters import ProductFilter
|
|||
|
|
from .services import ProductService
|
|||
|
|
|
|||
|
|
class ProductViewSet(viewsets.ModelViewSet):
|
|||
|
|
"""產品模型的 ViewSet。"""
|
|||
|
|
|
|||
|
|
queryset = Product.objects.select_related('category').prefetch_related('tags')
|
|||
|
|
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
|||
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|||
|
|
filterset_class = ProductFilter
|
|||
|
|
search_fields = ['name', 'description']
|
|||
|
|
ordering_fields = ['price', 'created_at', 'name']
|
|||
|
|
ordering = ['-created_at']
|
|||
|
|
|
|||
|
|
def get_serializer_class(self):
|
|||
|
|
"""根據 Action 回傳適當的序列化程式。"""
|
|||
|
|
if self.action == 'create':
|
|||
|
|
return ProductCreateSerializer
|
|||
|
|
return ProductSerializer
|
|||
|
|
|
|||
|
|
def perform_create(self, serializer):
|
|||
|
|
"""保存時帶入使用者上下文。"""
|
|||
|
|
serializer.save(created_by=self.request.user)
|
|||
|
|
|
|||
|
|
@action(detail=False, methods=['get'])
|
|||
|
|
def featured(self, request):
|
|||
|
|
"""回傳精選產品。"""
|
|||
|
|
featured = self.queryset.filter(is_featured=True)[:10]
|
|||
|
|
serializer = self.get_serializer(featured, many=True)
|
|||
|
|
return Response(serializer.data)
|
|||
|
|
|
|||
|
|
@action(detail=True, methods=['post'])
|
|||
|
|
def purchase(self, request, pk=None):
|
|||
|
|
"""購買產品。"""
|
|||
|
|
product = self.get_object()
|
|||
|
|
service = ProductService()
|
|||
|
|
result = service.purchase(product, request.user)
|
|||
|
|
return Response(result, status=status.HTTP_201_CREATED)
|
|||
|
|
|
|||
|
|
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
|||
|
|
def my_products(self, request):
|
|||
|
|
"""回傳目前使用者建立的產品。"""
|
|||
|
|
products = self.queryset.filter(created_by=request.user)
|
|||
|
|
page = self.paginate_queryset(products)
|
|||
|
|
serializer = self.get_serializer(page, many=True)
|
|||
|
|
return self.get_paginated_response(serializer.data)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 自定義 Action (Custom Actions)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from rest_framework.decorators import api_view, permission_classes
|
|||
|
|
from rest_framework.permissions import IsAuthenticated
|
|||
|
|
from rest_framework.response import Response
|
|||
|
|
|
|||
|
|
@api_view(['POST'])
|
|||
|
|
@permission_classes([IsAuthenticated])
|
|||
|
|
def add_to_cart(request):
|
|||
|
|
"""將產品加入使用者購物車。"""
|
|||
|
|
product_id = request.data.get('product_id')
|
|||
|
|
quantity = request.data.get('quantity', 1)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
product = Product.objects.get(id=product_id)
|
|||
|
|
except Product.DoesNotExist:
|
|||
|
|
return Response(
|
|||
|
|
{'error': '找不到產品'},
|
|||
|
|
status=status.HTTP_404_NOT_FOUND
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
cart, _ = Cart.objects.get_or_create(user=request.user)
|
|||
|
|
CartItem.objects.create(
|
|||
|
|
cart=cart,
|
|||
|
|
product=product,
|
|||
|
|
quantity=quantity
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return Response({'message': '已加入購物車'}, status=status.HTTP_201_CREATED)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 服務層模式 (Service Layer Pattern)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# apps/orders/services.py
|
|||
|
|
from typing import Optional
|
|||
|
|
from django.db import transaction
|
|||
|
|
from .models import Order, OrderItem
|
|||
|
|
|
|||
|
|
class OrderService:
|
|||
|
|
"""針對訂單相關業務邏輯的服務層。"""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
@transaction.atomic
|
|||
|
|
def create_order(user, cart: Cart) -> Order:
|
|||
|
|
"""從購物車建立訂單。"""
|
|||
|
|
order = Order.objects.create(
|
|||
|
|
user=user,
|
|||
|
|
total_price=cart.total_price
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
for item in cart.items.all():
|
|||
|
|
OrderItem.objects.create(
|
|||
|
|
order=order,
|
|||
|
|
product=item.product,
|
|||
|
|
quantity=item.quantity,
|
|||
|
|
price=item.product.price
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 清空購物車
|
|||
|
|
cart.items.all().delete()
|
|||
|
|
|
|||
|
|
return order
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def process_payment(order: Order, payment_data: dict) -> bool:
|
|||
|
|
"""處理訂單付款。"""
|
|||
|
|
# 串接支付閘道器 (Payment Gateway)
|
|||
|
|
payment = PaymentGateway.charge(
|
|||
|
|
amount=order.total_price,
|
|||
|
|
token=payment_data['token']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if payment.success:
|
|||
|
|
order.status = Order.Status.PAID
|
|||
|
|
order.save()
|
|||
|
|
# 發送確認郵件
|
|||
|
|
OrderService.send_confirmation_email(order)
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def send_confirmation_email(order: Order):
|
|||
|
|
"""發送訂單確認郵件。"""
|
|||
|
|
# 郵件發送邏輯
|
|||
|
|
pass
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 快取策略 (Caching Strategies)
|
|||
|
|
|
|||
|
|
### 視圖層級快取 (View-Level Caching)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from django.views.decorators.cache import cache_page
|
|||
|
|
from django.utils.decorators import method_decorator
|
|||
|
|
|
|||
|
|
@method_decorator(cache_page(60 * 15), name='dispatch') # 快取 15 分鐘
|
|||
|
|
class ProductListView(generic.ListView):
|
|||
|
|
model = Product
|
|||
|
|
template_name = 'products/list.html'
|
|||
|
|
context_object_name = 'products'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 範本片段快取 (Template Fragment Caching)
|
|||
|
|
|
|||
|
|
```django
|
|||
|
|
{% load cache %}
|
|||
|
|
{% cache 500 sidebar %}
|
|||
|
|
... 高成本的側邊欄內容 ...
|
|||
|
|
{% endcache %}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 低階快取 (Low-Level Caching)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from django.core.cache import cache
|
|||
|
|
|
|||
|
|
def get_featured_products():
|
|||
|
|
"""獲取精選產品並執行快取。"""
|
|||
|
|
cache_key = 'featured_products'
|
|||
|
|
products = cache.get(cache_key)
|
|||
|
|
|
|||
|
|
if products is None:
|
|||
|
|
products = list(Product.objects.filter(is_featured=True))
|
|||
|
|
cache.set(cache_key, products, timeout=60 * 15) # 15 分鐘
|
|||
|
|
|
|||
|
|
return products
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### QuerySet 快取
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from django.core.cache import cache
|
|||
|
|
|
|||
|
|
def get_popular_categories():
|
|||
|
|
cache_key = 'popular_categories'
|
|||
|
|
categories = cache.get(cache_key)
|
|||
|
|
|
|||
|
|
if categories is None:
|
|||
|
|
categories = list(Category.objects.annotate(
|
|||
|
|
product_count=Count('products')
|
|||
|
|
).filter(product_count__gt=10).order_by('-product_count')[:20])
|
|||
|
|
cache.set(cache_key, categories, timeout=60 * 60) # 1 小時
|
|||
|
|
|
|||
|
|
return categories
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 信號 (Signals)
|
|||
|
|
|
|||
|
|
### 信號模式 (Signal Patterns)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# apps/users/signals.py
|
|||
|
|
from django.db.models.signals import post_save
|
|||
|
|
from django.dispatch import receiver
|
|||
|
|
from django.contrib.auth import get_user_model
|
|||
|
|
from .models import Profile
|
|||
|
|
|
|||
|
|
User = get_user_model()
|
|||
|
|
|
|||
|
|
@receiver(post_save, sender=User)
|
|||
|
|
def create_user_profile(sender, instance, created, **kwargs):
|
|||
|
|
"""建立使用者時同時建立 Profile。"""
|
|||
|
|
if created:
|
|||
|
|
Profile.objects.create(user=instance)
|
|||
|
|
|
|||
|
|
@receiver(post_save, sender=User)
|
|||
|
|
def save_user_profile(sender, instance, **kwargs):
|
|||
|
|
"""保存使用者時同時保存 Profile。"""
|
|||
|
|
instance.profile.save()
|
|||
|
|
|
|||
|
|
# apps/users/apps.py
|
|||
|
|
from django.apps import AppConfig
|
|||
|
|
|
|||
|
|
class UsersConfig(AppConfig):
|
|||
|
|
default_auto_field = 'django.db.models.BigAutoField'
|
|||
|
|
name = 'apps.users'
|
|||
|
|
|
|||
|
|
def ready(self):
|
|||
|
|
"""當 App 就緒時導入信號。"""
|
|||
|
|
import apps.users.signals
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 中間件 (Middleware)
|
|||
|
|
|
|||
|
|
### 自定義中間件
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# middleware/active_user_middleware.py
|
|||
|
|
import time
|
|||
|
|
from django.utils.deprecation import MiddlewareMixin
|
|||
|
|
|
|||
|
|
class ActiveUserMiddleware(MiddlewareMixin):
|
|||
|
|
"""用於追蹤活躍使用者的中間件。"""
|
|||
|
|
|
|||
|
|
def process_request(self, request):
|
|||
|
|
"""處理傳入的請求。"""
|
|||
|
|
if request.user.is_authenticated:
|
|||
|
|
# 更新最後活動時間
|
|||
|
|
request.user.last_active = timezone.now()
|
|||
|
|
request.user.save(update_fields=['last_active'])
|
|||
|
|
|
|||
|
|
class RequestLoggingMiddleware(MiddlewareMixin):
|
|||
|
|
"""用於記錄請求日誌的中間件。"""
|
|||
|
|
|
|||
|
|
def process_request(self, request):
|
|||
|
|
"""記錄請求開始時間。"""
|
|||
|
|
request.start_time = time.time()
|
|||
|
|
|
|||
|
|
def process_response(self, request, response):
|
|||
|
|
"""記錄請求耗時。"""
|
|||
|
|
if hasattr(request, 'start_time'):
|
|||
|
|
duration = time.time() - request.start_time
|
|||
|
|
logger.info(f'{request.method} {request.path} - {response.status_code} - {duration:.3f}s')
|
|||
|
|
return response
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 效能優化
|
|||
|
|
|
|||
|
|
### 預防 N+1 查詢問題
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 不良的做法 - 會產生 N+1 個查詢
|
|||
|
|
products = Product.objects.all()
|
|||
|
|
for product in products:
|
|||
|
|
print(product.category.name) # 會針對每個產品發送獨立查詢
|
|||
|
|
|
|||
|
|
# 推薦的做法 - 使用 select_related 進行單一查詢
|
|||
|
|
products = Product.objects.select_related('category').all()
|
|||
|
|
for product in products:
|
|||
|
|
print(product.category.name)
|
|||
|
|
|
|||
|
|
# 多對多關係推薦使用 prefetch_related
|
|||
|
|
products = Product.objects.prefetch_related('tags').all()
|
|||
|
|
for product in products:
|
|||
|
|
for tag in product.tags.all():
|
|||
|
|
print(tag.name)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 資料庫索引
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class Product(models.Model):
|
|||
|
|
name = models.CharField(max_length=200, db_index=True)
|
|||
|
|
slug = models.SlugField(unique=True)
|
|||
|
|
category = models.ForeignKey('Category', on_delete=models.CASCADE)
|
|||
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|||
|
|
|
|||
|
|
class Meta:
|
|||
|
|
indexes = [
|
|||
|
|
models.Index(fields=['name']),
|
|||
|
|
models.Index(fields=['-created_at']),
|
|||
|
|
models.Index(fields=['category', 'created_at']),
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 批次操作 (Bulk Operations)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 批次建立 (Bulk create)
|
|||
|
|
Product.objects.bulk_create([
|
|||
|
|
Product(name=f'Product {i}', price=10.00)
|
|||
|
|
for i in range(1000)
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
# 批次更新 (Bulk update)
|
|||
|
|
products = Product.objects.all()[:100]
|
|||
|
|
for product in products:
|
|||
|
|
product.is_active = True
|
|||
|
|
Product.objects.bulk_update(products, ['is_active'])
|
|||
|
|
|
|||
|
|
# 批次刪除
|
|||
|
|
Product.objects.filter(stock=0).delete()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 快速參考
|
|||
|
|
|
|||
|
|
| 模式 | 描述 |
|
|||
|
|
|---------|-------------|
|
|||
|
|
| 設定檔拆分 (Split settings) | 分離開發/生產/測試環境設定 |
|
|||
|
|
| 自定義 QuerySet | 可重用的查詢方法 |
|
|||
|
|
| 服務層 (Service Layer) | 業務邏輯分離 |
|
|||
|
|
| ViewSet | REST API 端點 |
|
|||
|
|
| 序列化與驗證 | 請求/回應轉換 |
|
|||
|
|
| select_related | 外鍵 (Foreign key) 優化 |
|
|||
|
|
| prefetch_related | 多對多關係優化 |
|
|||
|
|
| 快取優先 (Cache first) | 先行快取高成本操作 |
|
|||
|
|
| 信號 (Signals) | 事件驅動行為 |
|
|||
|
|
| 中間件 (Middleware) | 請求/回應處理 |
|
|||
|
|
|
|||
|
|
請記住:Django 提供了許多捷徑,但對於生產環境的應用程序而言,良好的結構與組織比精簡的程式碼更為重要。請為「可維護性」而建構。
|