Luffyプロジェクトのバックエンド実装ガイド

1. カルーセル(バナー)データベースの作成

作成時間や削除フラグなど、多くのモデルで共通して使用されるフィールドがあるため、これらを抽象ベースモデルとして定義し、データベース生成時に各テーブルに含まれないようにします。以下のコードは再利用可能です。まず、BaseModelを継承します。

from django.db import models

class AbstractBaseModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='作成日時')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='最終更新日時')
    is_deleted = models.BooleanField(default=False, verbose_name='削除済み')
    is_active = models.BooleanField(default=True, verbose_name='公開中')
    display_order = models.IntegerField(verbose_name='表示順序')

    class Meta:
        abstract = True

次に、このベースモデルを継承してCarousel(カルーセル)モデルを定義します。

from utils.models import AbstractBaseModel
from django.db import models

class CarouselModel(AbstractBaseModel):
    title = models.CharField(max_length=16, unique=True, verbose_name='タイトル')
    image = models.ImageField(upload_to='carousel', verbose_name='画像')
    link_url = models.CharField(max_length=64, verbose_name='リンク先')
    description = models.TextField(verbose_name='説明')

    class Meta:
        db_table = 'luffy_carousel'
        verbose_name_plural = 'カルーセル一覧'

    def __str__(self):
        return self.title

これでCarouselテーブルのモデルが完成しました。マイグレーションを実行します。

# アプリを登録した後、マイグレーションを実行します
python manage.py makemigrations
python manage.py migrate

2. カルーセルAPIの作成

GenericViewSetとListModelMixinを継承することで、APIを迅速に実装できます。しかし、特定のフォーマットでデータを返す必要がある場合、ListModelMixinのlistメソッドをオーバーライドする必要があります。リストデータを別の変数に渡し、関数の指定パラメータに基づいて値を渡せるようにします。このlistメソッドは複数の場所で再利用される可能性があるため、さらにラッピングすることができます。

from rest_framework.mixins import ListModelMixin
from .response import APIResponse

class CarouselListView(ListModelMixin):
    def list(self, request, *args, **kwargs):
        response_data = super().list(request, *args, **kwargs)
        return APIResponse(data=response_data.data)
from luffyapi.utils.listviews import CarouselListView
from .models import CarouselModel
from .serializer import CarouselSerializer
# ビューを作成します
from rest_framework.viewsets import GenericViewSet
from django.conf import settings

class CarouselViewSet(GenericViewSet, CarouselListView):
    queryset = CarouselModel.objects.all().filter(is_deleted=False, is_active=True).order_by('display_order')[:settings.AREA]
    serializer_class = CarouselSerializer

3. 多様な携帯電話番号ログインインターフェースと電話番号存在確認インターフェースの作成

from rest_framework.decorators import action
from django.shortcuts import render
from rest_framework.viewsets import ViewSet
from .serializer import UserAuthSerializer, VerificationCodeSerializer
from utils.response import APIResponse
from .models import UserProfile
from rest_framework.exceptions import APIException
from txy_sms import get_verification_code, send_sms
from django.core.cache import cache
import re

class MobileAuthViewSet(ViewSet):
    @action(methods=['POST'], detail=False)
    def authenticate(self, request):
        serializer = UserAuthSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        auth_token = serializer.context.get('auth_token')
        profile_image = serializer.context.get('profile_image')
        username = serializer.context.get('username')
        return APIResponse(token=auth_token, icon=profile_image, username=username)

    @action(methods=['GET'], detail=False)
    def send_code(self, request):
        phone_number = request.query_params.get('phone_number')
        if re.match(r'^1[3-9][0-9]{9}$', phone_number):
            code = get_verification_code()
            cache.set('verification_code_%s' % phone_number, code)
            result = send_sms(phone_number, code)
            if result:
                return APIResponse(msg='認証コードを送信しました')
            else:
                return APIResponse(msg='認証コードの送信に失敗しました', code=101)
        else:
            return APIResponse(code=102, msg='電話番号が無効です')

    @action(methods=['POST'], detail=False)
    def verify_and_login(self, request):
        serializer = VerificationCodeSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        auth_token = serializer.context.get('auth_token')
        profile_image = serializer.context.get('profile_image')
        username = serializer.context.get('username')
        return APIResponse(token=auth_token, icon=profile_image, username=username)

class MobileCheckViewSet(ViewSet):
    @action(methods=['GET'], detail=False)
    def check_phone(self, request):
        try:
            phone_number = request.query_params.get('phone_number')
            UserProfile.objects.get(phone_number=phone_number)
            return APIResponse(msg='電話番号は既に存在します')
        except Exception as e:
            raise APIException('電話番号が存在しません')
from rest_framework import serializers
from django.contrib.auth import authenticate
from rest_framework.exceptions import APIException

from rest_framework.exceptions import ValidationError
from django.contrib.auth.hashers import make_password
from django.core.cache import cache
from .models import UserProfile
import re
from utils.jwt_utils import generate_jwt_token

class UserAuthSerializer(serializers.ModelSerializer):
    username = serializers.CharField()

    class Meta:
        model = UserProfile
        fields = ['username', 'password']

    def _get_user(self, request_data):
        username = request_data.get('username')
        password = request_data.get('password')
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = UserProfile.objects.filter(phone_number=username).first()
            user = authenticate(username=user.username, password=password)
        elif re.match(r'^.+@.+$', username):
            user = UserProfile.objects.filter(email=username).first()
            user = authenticate(username=user.username, password=password)
        else:
            user = authenticate(username=username, password=password)

        if user:
            return user
        else:
            raise ValidationError('ユーザー名またはパスワードが間違っています')

    def validate(self, attrs):
        user = self._get_user(attrs)
        auth_token = generate_jwt_token(user)
        self.context['auth_token'] = auth_token
        self.context['profile_image'] = 'http://127.0.0.1:8000/media/' + str(user.profile_image)
        self.context['username'] = user.username
        return attrs

class VerificationCodeSerializer(serializers.ModelSerializer):
    phone_number = serializers.CharField()
    code = serializers.CharField()

    class Meta:
        model = UserProfile
        fields = ['phone_number', 'code']

    def _get_user(self, request_data):
        phone_number = request_data.get('phone_number')
        code = request_data.get('code')

        if re.match(r'^1[3-9][0-9]{9}$', phone_number):
            user = UserProfile.objects.filter(phone_number=phone_number).first()
        else:
            raise APIException('電話番号が不正です')
        if user and code == cache.get('verification_code_%s' % phone_number):
            cache.set('verification_code_%s' % phone_number, '')
            return user
        else:
            raise ValidationError('認証コードが間違っています')

    def validate(self, attrs):
        user = self._get_user(attrs)
        auth_token = generate_jwt_token(user)
        self.context['auth_token'] = auth_token
        self.context['profile_image'] = 'http://127.0.0.1:8000/media/' + str(user.profile_image)
        self.context['username'] = user.username
        return attrs
from rest_framework_jwt.settings import api_settings
from rest_framework.exceptions import ValidationError

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

def generate_jwt_token(user):
    try:
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token
    except Exception as e:
        raise ValidationError(str(e))

4. 登録インターフェース

 @action(methods=['POST'], detail=False)
    def signup(self, request):
        serializer = RegistrationSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return APIResponse(msg='登録に成功しました')
class RegistrationSerializer(serializers.ModelSerializer):
    code = serializers.CharField()

    class Meta:
        model = UserProfile
        fields = ['phone_number', 'code', 'password']

    def validate(self, attrs):
        code = attrs.get('code')
        phone_number = attrs.get('phone_number')
        if not(code == cache.get('verification_code_%s' % phone_number)):
            raise APIException('認証コードが間違っています')
        attrs['username'] = phone_number
        attrs.pop('code')
        return attrs

    def create(self, validated_data):
        user = UserProfile.objects.create_user(**validated_data)
        return user

5. コースカテゴリインターフェース

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseCategory
        fields = ['id', 'name']
class CategoryViewSet(GenericViewSet, CommonListModelMixin):
    queryset = CourseCategory.objects.all().filter(is_deleted=False, is_active=True).order_by('display_order')
    serializer_class = CategorySerializer

5.1 全コースインターフェース

class InstructorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Instructor
        fields = ['id','name','role','title','signature','image','bio']

class CourseListSerializer(serializers.ModelSerializer):
    # 方法3:サブシリアライザ:講師のシリアライザを使用してシリアライズを実装します
    instructor = InstructorSerializer()
    class Meta:
        model = Course
        fields = [
            'id',
            'name',
            'course_image',
            'price',  # コース価格
            'enrolled_students',  # 学生数
            'published_lessons',  # 公開されたレッスン数
            'total_lessons',  # 総レッスン数

            'description',  # コース説明
            'attachment_path',  # コース資料のパス
            'recommended_duration',  # 推奨学習期間

            'course_type_name',  # コースタイプ名、このフィールドはテーブルにありません。モデルでメソッドをオーバーライドします
            'difficulty_level_name',  # 難易度名
            'status_name',  # ステータス名

            'instructor',
            'preview_lessons', # プレビューレッスン、最大4つ [{},{},{},{}]
        ]

    # シリアライズするフィールドを指定する:3つの方法
    # 方法1:テーブルモデルに書く

    # 方法2:シリアライザークラスに書く
    # instructor = serializers.SerializerMethodField()
    # def get_instructor(self, obj):
    #     return {'name': obj.instructor.name, 'title': obj.instructor.title, 'image': str(obj.instructor.image)}

    # 方法3:サブシリアライザ:講師のシリアライザを使用してシリアライズを実装します
class CourseListView(GenericViewSet, CommonListModelMixin, RetrieveModelMixin):
    queryset = Course.objects.all().filter(is_deleted=False, is_active=True).order_by('display_order')
    serializer_class = CourseListSerializer
    pagination_class = CustomPagination
    filter_backends = [OrderingFilter, DjangoFilterBackend]
    ordering_fields = ['display_order', 'price', 'enrolled_students']
    # フィルタリングの追加:コースカテゴリでフィルタリング -- django-filterを使用して実装
    filterset_fields = ['course_category']
class CourseModel(BaseModel):
    """コース"""
    course_type_choices = (
        (0, '有料'),
        (1, 'VIP専用'),
        (2, '学位コース')
    )
    difficulty_level_choices = (
        (0, '入門'),
        (1, '中級'),
        (2, '上級'),
    )
    status_choices = (
        (0, '公開中'),
        (1, '非公開'),
        (2, '準備中'),
    )
    name = models.CharField(max_length=128, verbose_name="コース名")
    course_image = models.ImageField(upload_to="courses", max_length=255, verbose_name="カバー画像", blank=True, null=True)
    course_type = models.SmallIntegerField(choices=course_type_choices, default=0, verbose_name="コースタイプ")
    description = models.TextField(max_length=2048, verbose_name="詳細説明", null=True, blank=True)
    difficulty_level = models.SmallIntegerField(choices=difficulty_level_choices, default=0, verbose_name="難易度")
    publish_date = models.DateField(verbose_name="公開日", auto_now_add=True)
    recommended_duration = models.IntegerField(verbose_name="推奨学習期間(日)", default=7)
    attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="資料パス", blank=True, null=True)
    status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="コースステータス")
    enrolled_students = models.IntegerField(verbose_name="受講者数", default=0)
    total_lessons = models.IntegerField(verbose_name="総レッスン数", default=0)
    published_lessons = models.IntegerField(verbose_name="公開レッスン数", default=0)
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="コース価格", default=0)

    instructor = models.ForeignKey("Instructor", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="講師", db_constraint=False)
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True, blank=True, verbose_name="コースカテゴリ")

    class Meta:
        db_table = "luffy_course"
        verbose_name = "コース"
        verbose_name_plural = "コース"

    def __str__(self):
        return "%s" % self.name

    def course_type_name(self):
        return self.get_course_type_display()

    def difficulty_level_name(self):
        return self.get_difficulty_level_display()

    def status_name(self):
        return self.get_status_display()

    def get_preview_lessons(self):
        lesson_list = []
        course_chapters = self.coursechapters.all()
        for chapter in course_chapters:
            course_sections = chapter.coursesections.all()
            for section in course_sections:
                lesson_list.append({'id': section.id,
                                     'name': section.name,
                                     'section_link': section.section_link,
                                     'duration': section.duration,
                                     'is_preview': section.is_preview,
                                     })
                if len(lesson_list) >= 4:
                    return lesson_list
        return lesson_list
class CustomPagination(PageNumberPagination):
    page_size = 2
    page_query_param = 'page'
    page_size_query_param = 'size'
    max_page_size = 5

5.2 コース詳細ページインターフェース

class ChapterViewSet(GenericViewSet, CommonListModelMixin):
    queryset = CourseChapter.objects.all().filter(is_deleted=False, is_active=True).order_by('display_order')
    serializer_class = ChapterSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['course']
class SectionSerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseSection
        fields = '__all__'


class ChapterSerializer(serializers.ModelSerializer):
    coursesections = SectionSerializer(many=True)

    class Meta:
        model = CourseChapter
        fields = ['id', 'chapter_number', 'title', 'summary', 'coursesections']

6. 検索バックエンドインターフェース

# es:分散型の全文検索エンジン、百度検索のような機能を実現します
class CourseSearchViewSet(GenericViewSet, ListModelMixin):
    queryset = Course.objects.all().filter(is_deleted=False, is_active=True).order_by('display_order')
    serializer_class = CourseListSerializer
    pagination_class = CustomPagination
    filter_backends = [SearchFilter]
    search_fields = ['name']
    # 現在、検索できるのは実践コースのみ:軽量コース、実践コース、無料コース

    # def list(self, request, *args, **kwargs):
    #     res = super().list( request, *args, **kwargs)
    #     # res.data は検索された実践コース

    #     # 軽量コースを検索
    #     keywords = request.query_params.get('search')
    #     # 軽量コーステーブルで検索
    #     # 無料コーステーブルで検索

    #     #{code:100,msg:成功,actual_list:[{},{},{}],free_list:[{},{},{}],light_list:[{},{},{}]}
    #
    #     return APIResponse(code=100,msg='成功',actual_list=res.data,free_list='',light_list='')

7. 注文生成インターフェース

from django.shortcuts import render

# ビューを作成します
from .models import PurchaseOrder
# 自動ルーティング、新規機能:注文テーブルにデータを保存
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from .serializer import OrderCreationSerializer
from utils.response import APIResponse

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class OrderCreationViewSet(GenericViewSet):
    queryset = PurchaseOrder.objects.all()
    serializer_class = OrderCreationSerializer
    # ログイン後のみアクセス可能
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

    # この新規作成インターフェースは、新規作成と支払いリンクの返却の両方を行うため、createメソッドをオーバーライドします
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        serializer.save()
        payment_link = serializer.context.get('payment_link')
        return APIResponse(payment_link=payment_link)
from rest_framework import serializers
from .models import PurchaseOrder, OrderItem
from course.models import CourseModel
from rest_framework.exceptions import APIException
from django.conf import settings


class OrderCreationSerializer(serializers.ModelSerializer):
    # coursesはorderテーブルのフィールドではないため、必ず再定義する必要があります
    # フロントエンドから渡されるのは courses:[1,3,4] -----> コースオブジェクトに変換 [コースオブジェクト1, コースオブジェクト3, コースオブジェクト4]
    selected_courses = serializers.PrimaryKeyRelatedField(queryset=CourseModel.objects.all(), write_only=True, many=True)

    class Meta:
        model = PurchaseOrder
        fields = ['selected_courses', 'total_amount', 'order_subject', 'payment_method']  # データ検証と逆シリアル化にのみ使用

    def _validate_total_amount(self, attrs):
        total_amount = attrs.get('total_amount')
        # すべてのコースをループし、価格を合計して総価格を取得
        calculated_total = 0
        for course in attrs.get('selected_courses'):
            calculated_total += course.price
        if not total_amount == calculated_total:  # 異常な場合、例外をスロー
            raise APIException('価格が一致しません')
        else:
            return calculated_total

    def _generate_order_number(self):
        import uuid
        return str(uuid.uuid4())

    def _get_current_user(self):
        # 現在のログインユーザー、request.user
        request = self.context.get('request')
        return request.user  # ログイン認証を通過している必要があります

    def _generate_payment_link(self, total_amount, subject, order_number):
        from libs.alipay_common import GATEWAY, alipay_client
        payment_string = alipay_client.api_alipay_trade_page_pay(
            out_trade_no=order_number,
            total_amount=float(total_amount),
            subject=subject,
            return_url=settings.RETURN_URL,  # getコールバックアドレス
            notify_url=settings.NOTIFY_URL  # postコールバックアドレス
        )
        return GATEWAY + payment_string

    def _prepare_for_creation(self, payment_link, attrs, user, order_number):
        self.context['payment_link'] = payment_link
        attrs['user'] = user
        attrs['order_number'] = order_number
        # selected_coursesを除外する必要はありますか? attrs:{selected_courses:[オブジェクト1, オブジェクト2], total_amount:11, order_subject:xx, payment_method:1, user:user, order_number:3333}

    def validate(self, attrs):
        # 1. 価格を検証:合計価格とバックエンドで計算された合計価格が一致するかどうかを確認
        total_amount = self._validate_total_amount(attrs)
        # 2. 注文番号を生成:一意である必要があります
        order_number = self._generate_order_number()
        # 3. 支払いユーザー:request.user
        user = self._get_current_user()
        # 4. 支払いリンクを生成し、self.contextに追加
        payment_link = self._generate_payment_link(total_amount, attrs.get('order_subject'), order_number)
        # 5. データベース(2つのテーブル)に保存するための情報の準備:createメソッドをオーバーライド
        self._prepare_for_creation(payment_link, attrs, user, order_number)

        return attrs

    def create(self, validated_data):
        # {selected_courses:[オブジェクト1, オブジェクト2], total_amount:11, order_subject:xx, payment_method:1, user:user, order_number:3333}
        courses = validated_data.pop('selected_courses')
        order = PurchaseOrder.objects.create(**validated_data)
        # 注文明細テーブルに保存
        for course in courses:
            OrderItem.objects.create(order=order, course=course, price=course.price, actual_price=course.price)

        return order

タグ: Django RESTframework JWT認証 データベース設計 API開発

6月28日 02:24 投稿