一 项目背景
路飞学成项目,有课程,学位课(不同的课程字段不一样),价格策略
问题,1 如何设计表结构,来表示这种规则
2 为专题课,添加三个价格策略
3 查询所有价格策略,并且显示对应的课程名称
4 通过课程id,获取课程信息和价格策略
二 版本一
一个课程表,包含学位课和专题课,一个价格策略表,一对多关联
三 版本二
学位课表,专题课表,装逼课表,价格策略表(在价格策略课表中加入多个FK跟课程表做关联):后期再加其它课程,可维护性差
四 最终版(使用ContentType)
通过Django提供的ContentType表,来构建
models层创建:
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation class Course(models.Model): title = models.CharField(max_length=32) # 不会在数据库中生成字段,只用于数据库操作 # policy = GenericRelation('PricePolicy',object_id_field='object_id',content_type_field='contentType') class DegreeCourse(models.Model): title = models.CharField(max_length=32) class PricePolicy(models.Model): # 跟ContentType表做外键关联 contentType = models.ForeignKey(to=ContentType) # 正数 object_id = models.PositiveIntegerField() # 引入一个字段,不会在数据库中创建,只用来做数据库操作 # content_obj = GenericForeignKey('contentType', 'object_id') period = models.CharField(max_length=32) price = models.FloatField()
views层:
from app01 import models def test(request): import json # 方式一插入价格规则 # ret=models.ContentType.objects.filter(model='course').first() # course=models.Course.objects.filter(pk=1).first() # print(ret.id) # models.PricePolicy.objects.create(period='30',price=100,object_id=course.id,contentType_id=ret.id) # 方式二插入价格规则 # course=models.Course.objects.filter(pk=1).first() # # content_obj=course 会自动的把课程id放到object_id上,并且去ContentType表中查询课程表的id,放到contentType上 # models.PricePolicy.objects.create(period='60',price=800,content_obj=course) # 增加学位课,价格规则 # degreecourse = models.DegreeCourse.objects.filter(pk=1).first() # models.PricePolicy.objects.create(period='60', price=800, content_obj=degreecourse) # 查询所有价格策略,并且显示对应的课程名称 # ret=models.PricePolicy.objects.all() # for i in ret: # print(i.price) # print(i.period) # # content_obj 就是代指关联的课程,或者学位课程的那个对象 # print(type(i.content_obj)) # print(i.content_obj.title) # 通过课程id,获取课程信息和价格策略 course=models.Course.objects.filter(pk=1).first() print(course.policy.all()) return render(request,'test.html')
ContentType组件
ContentType是Django的内置的一个应用,可以追踪项目中所有的APP和model的对应关系,并记录在ContentType表中。
当项目做数据迁移后,会有很多django自带的表,其中就有django_content_type表
ContentType组件应用
- 在model中定义ForeignKey字段,并关联到ContentType表,通常这个字段命名为content-type
- 在model中定义PositiveIntergerField字段, 用来存储关联表中的主键,通常用object_id
- 在model中定义GenericForeignKey字段,传入上面两个字段的名字
- 方便反向查询可以定义GenericRelation字段
- postman
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation # Create your models here. class Food(models.Model): """ id title 1 面包 2 牛奶 """ title = models.CharField(max_length=32) # 不会生成字段 只用于反向查询 coupons = GenericRelation(to="Coupon") class Fruit(models.Model): """ id title 1 苹果 2 香蕉 """ title = models.CharField(max_length=32) # 如果有40张表 # class Coupon(models.Model): # """ # id title food_id fruit_id # 1 面包九五折 1 null # 2 香蕉满10元减5元 null 2 # """ # title = models.CharField(max_length=32) # food = models.ForeignKey(to="Food") # fruit = models.ForeignKey(to="Fruit") # class Coupon(models.Model): # """ # id title table_id object_id # 1 面包九五折 1 1 # 2 香蕉满10元减5元 2 2 # """ # title = models.CharField(max_length=32) # table = models.ForeignKey(to="Table") # object_id = models.IntegerField() # # # class Table(models.Model): # """ # id app_name table_name # 1 demo food # 2 demo fruit # """ # app_name = models.CharField(max_length=32) # table_name = models.CharField(max_length=32) class Coupon(models.Model): title = models.CharField(max_length=32) # 第一步:注意没有引号因为是导入的 content_type = models.ForeignKey(to=ContentType, on_delete=None) # 第二步 object_id = models.IntegerField() # 第三步 不会生成字段,用来操作增删改查 content_object = GenericForeignKey("content_type", "object_id")
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from .models import Food, Coupon from django.contrib.contenttypes.models import ContentType # Create your views here. class DemoView(APIView): def get(self, request): # 给面包创建一个优惠券 food_obj = Food.objects.filter(id=1).first() # Coupon.objects.create(title="面包九五折", content_type_id=8, object_id=1) # Coupon.objects.create(title="双十一面包九折促销", content_object=food_obj) #查询食物都有哪些优惠券 #定义了反向查询 coupons = food_obj.coupons.all() print(coupons) # 如果没定义反向查询 content = ContentType.objects.filter(app_label="app01", model="food").first() coupons = Coupon.objects.filter(content_type=content, object_id=1).all() print(coupons) # 优惠券查对象 # 查询优惠券id=1绑定了哪个商品 coupon_obj = Coupon.objects.filter(id=1).first() content_obj = coupon_obj.content_object print(coupon_obj.title,content_obj.title) # 通过ContentType表找表模型 content = ContentType.objects.filter(app_label="app01", model="food").first() # content=food 获取food表的表模型用model_class() model_class = content.model_class() ret = model_class.objects.all() print(ret) return Response("ContentType测试")
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.auth.models import User class Post(models.Model): """帖子表""" author = models.ForeignKey(User) title = models.CharField(max_length=72) comments = GenericRelation('Comment') # 支持反向查找评论数据(不会在数据库中创建字段) class Picture(models.Model): """图片表""" author = models.ForeignKey(User) image = models.ImageField() comments = GenericRelation('Comment') # 支持反向查找评论数据(不会在数据库中创建字段) class Comment(models.Model): """评论表""" author = models.ForeignKey(User) content = models.TextField() content_type = models.ForeignKey(ContentType) # 外键关联django_content_type表 object_id = models.PositiveIntegerField() # 关联数据的主键 # 告诉Django,content_type和object_id是一组动态关联的数据,不会生成字段,用来操作增删改查 content_object = GenericForeignKey('content_type', 'object_id') ###################################################################################### class Post(models.Model): """帖子表""" author = models.ForeignKey(User) title = models.CharField(max_length=72) comments = GenericRelation('Comment',object_id_field='obj_id',content_type_field='table_name') # 支持反向查找评论数据(不会在数据库中创建字段) class Picture(models.Model): """图片表""" author = models.ForeignKey(User) image = models.ImageField() comments = GenericRelation('Comment',object_id_field='obj_id',content_type_field='table_name') # 支持反向查找评论数据(不会在数据库中创建字段) class Comment(models.Model): """评论表""" author = models.ForeignKey(User) content = models.TextField() table_name = models.ForeignKey(ContentType) # 外键关联django_content_type表 obj_id = models.PositiveIntegerField() # 关联数据的主键 content_object = GenericForeignKey('table_name', 'obj_id') #不会生成字段,用来操作增删改查 ###################################################################################### import os if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "about_contenttype.settings") import django django.setup() from app01.models import Post, Picture, Comment from django.contrib.auth.models import User # 准备测试数据 user_1 = User.objects.create_user(username='aaa', password='123') user_2 = User.objects.create_user(username='bbb', password='123') user_3 = User.objects.create_user(username='ccc', password='123') post_1 = Post.objects.create(author=user_1, title='Python入门教程') post_2 = Post.objects.create(author=user_2, title='Python进阶教程') post_3 = Post.objects.create(author=user_1, title='Python入土教程') picture_1 = Picture.objects.create(author=user_1, image='小姐姐01.jpg') picture_2 = Picture.objects.create(author=user_1, image='小姐姐02.jpg') picture_3 = Picture.objects.create(author=user_3, image='小哥哥01.jpg') # 给帖子创建评论数据 comment_1 = Comment.objects.create(author=user_1, content='好文!', content_object=post_1) # 给图片创建评论数据 comment_2 = Comment.objects.create(author=user_2, content='好美!', content_object=picture_1) #查询示例: #定义了反向查询 post_1 = Post.objects.filter(id=1).first() comment_list = post_1.comments.all()
media的配置
#静态文件 STATIC_URL = '/static/' STATICFILES_DIRS=( os.path.join(BASE_DIR,'static'), ) # Media配置 MEDIA_URL = "media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media")
from django.conf.urls import url, include from django.contrib import admin from django.views.static import serve from new_luffy import settings urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^api/course/', include("course.urls")), # media路径配置 url(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}) ]
项目路由配置
from django.contrib import admin from django.urls import path, include, re_path from django.views.static import serve from LuffyCity import settings from Login.views import GeetestView urlpatterns = [ path('admin/', admin.site.urls), path('api/course/', include("Course.urls")), path('api/shop/', include("shopping.urls")), path('api/', include("Login.urls")), path('pc-geetest/register', GeetestView.as_view()), path('pc-geetest/ajax_validate', GeetestView.as_view()), # media路径配置 # path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}) re_path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}) ]
一、课程模块
- 课程模块,包括了免费课程以及专题课程
- 主要是课程的展示,点击课程进入课程详细页面
- 课程详细页面展示,课程的概述,课程的价格策略,课程章节,评价以及常见问题
1、设计表结构
from django.db import models # Create your models here. from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType # Create your models here. __all__ = ["Category", "Course", "CourseDetail", "Teacher", "DegreeCourse", "CourseChapter", "CourseSection", "PricePolicy", "OftenAskedQuestion", "Comment", "Account", "CourseOutline"] class Category(models.Model): """课程分类表""" title = models.CharField(max_length=32, unique=True, verbose_name="课程的分类") def __str__(self): return self.title class Meta: verbose_name = "01-课程分类表" db_table = verbose_name verbose_name_plural = verbose_name class Course(models.Model): """课程表""" title = models.CharField(max_length=128, unique=True, verbose_name="课程的名称") course_img = models.ImageField(upload_to="course/%Y-%m", verbose_name='课程的图片') category = models.ForeignKey(to="Category", verbose_name="课程的分类", on_delete=None) COURSE_TYPE_CHOICES = ((0, "付费"), (1, "vip专享"), (2, "学位课程")) course_type = models.SmallIntegerField(choices=COURSE_TYPE_CHOICES) degree_course = models.ForeignKey(to="DegreeCourse", blank=True, null=True, help_text="如果是学位课程,必须关联学位表", on_delete=None) brief = models.CharField(verbose_name="课程简介", max_length=1024) level_choices = ((0, '初级'), (1, '中级'), (2, '高级')) level = models.SmallIntegerField(choices=level_choices, default=1) status_choices = ((0, '上线'), (1, '下线'), (2, '预上线')) status = models.SmallIntegerField(choices=status_choices, default=0) pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True) order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排") study_num = models.IntegerField(verbose_name="学习人数", help_text="只要有人买课程,订单表加入数据的同时给这个字段+1") # order_details = GenericRelation("OrderDetail", related_query_name="course") # coupon = GenericRelation("Coupon") # 只用于反向查询不生成字段 price_policy = GenericRelation("PricePolicy") often_ask_questions = GenericRelation("OftenAskedQuestion") course_comments = GenericRelation("Comment") def save(self, *args, **kwargs): if self.course_type == 2: if not self.degree_course: raise ValueError("学位课必须关联学位课程表") super(Course, self).save(*args, **kwargs) def __str__(self): return self.title class Meta: verbose_name = "02-课程表" db_table = verbose_name verbose_name_plural = verbose_name class CourseDetail(models.Model): """课程详细表""" course = models.OneToOneField(to="Course", on_delete=None) hours = models.IntegerField(verbose_name="课时", default=7) course_slogan = models.CharField(max_length=125, blank=True, null=True, verbose_name="课程口号") video_brief_link = models.CharField(max_length=255, blank=True, null=True) summary = models.TextField(max_length=2048, verbose_name="课程概述") why_study = models.TextField(verbose_name="为什么学习这门课程") what_to_study_brief = models.TextField(verbose_name="我将学到哪些内容") career_improvement = models.TextField(verbose_name="此项目如何有助于我的职业生涯") prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024) recommend_courses = models.ManyToManyField("Course", related_name="recommend_by", blank=True) teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师") def __str__(self): return self.course.title class Meta: verbose_name = "03-课程详细表" db_table = verbose_name verbose_name_plural = verbose_name class Teacher(models.Model): """讲师表""" name = models.CharField(max_length=32, verbose_name="讲师名字") brief = models.TextField(max_length=1024, verbose_name="讲师介绍") def __str__(self): return self.name class Meta: verbose_name = "04-教师表" db_table = verbose_name verbose_name_plural = verbose_name class DegreeCourse(models.Model): """ 字段大体跟课程表相同,哪些不同根据业务逻辑去区分 """ title = models.CharField(max_length=32, verbose_name="学位课程名字") def __str__(self): return self.title class Meta: verbose_name = "05-学位课程表" db_table = verbose_name verbose_name_plural = verbose_name class CourseChapter(models.Model): """课程章节表""" course = models.ForeignKey(to="Course", related_name="course_chapters", on_delete=None) chapter = models.SmallIntegerField(default=1, verbose_name="第几章") title = models.CharField(max_length=32, verbose_name="课程章节名称") def __str__(self): return self.title class Meta: verbose_name = "06-课程章节表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ("course", "chapter") class CourseSection(models.Model): """课时表""" chapter = models.ForeignKey(to="CourseChapter", related_name="course_sections", on_delete=None) title = models.CharField(max_length=32, verbose_name="课时") section_order = models.SmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时") section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频')) free_trail = models.BooleanField("是否可试看", default=False) section_type = models.SmallIntegerField(default=2, choices=section_type_choices) section_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填vid,若是文档,填link") def course_chapter(self): return self.chapter.chapter def course_name(self): return self.chapter.course.title def __str__(self): return "%s-%s" % (self.chapter, self.title) class Meta: verbose_name = "07-课程课时表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ('chapter', 'section_link') class PricePolicy(models.Model): """价格策略表""" content_type = models.ForeignKey(ContentType, on_delete=None) # 关联course or degree_course object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') valid_period_choices = ((1, '1天'), (3, '3天'), (7, '1周'), (14, '2周'), (30, '1个月'), (60, '2个月'), (90, '3个月'), (120, '4个月'), (180, '6个月'), (210, '12个月'), (540, '18个月'), (720, '24个月'), (722, '24个月'), (723, '24个月'), ) valid_period = models.SmallIntegerField(choices=valid_period_choices) price = models.FloatField() def __str__(self): return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price) class Meta: verbose_name = "08-价格策略表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ("content_type", 'object_id', "valid_period") class OftenAskedQuestion(models.Model): """常见问题""" content_type = models.ForeignKey(ContentType, on_delete=None) # 关联course or degree_course object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') question = models.CharField(max_length=255) answer = models.TextField(max_length=1024) def __str__(self): return "%s-%s" % (self.content_object, self.question) class Meta: verbose_name = "09-常见问题表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ('content_type', 'object_id', 'question') class Comment(models.Model): """通用的评论表""" content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=None) object_id = models.PositiveIntegerField(blank=True, null=True) content_object = GenericForeignKey('content_type', 'object_id') content = models.TextField(max_length=1024, verbose_name="评论内容") account = models.ForeignKey("Account", verbose_name="会员名", on_delete=None) date = models.DateTimeField(auto_now_add=True) def __str__(self): return self.content class Meta: verbose_name = "10-评价表" db_table = verbose_name verbose_name_plural = verbose_name class Account(models.Model): username = models.CharField(max_length=32, verbose_name="用户姓名") pwd = models.CharField(max_length=32, verbose_name="密文密码") # head_img = models.CharField(max_length=256, default='/static/frontend/head_portrait/logo@2x.png', # verbose_name="个人头像") balance = models.IntegerField(verbose_name="贝里余额", default=0) def __str__(self): return self.username class Meta: verbose_name = "11-用户表" db_table = verbose_name verbose_name_plural = verbose_name class CourseOutline(models.Model): """课程大纲""" course_detail = models.ForeignKey(to="CourseDetail", related_name="course_outline", on_delete=None) title = models.CharField(max_length=128) order = models.PositiveSmallIntegerField(default=1) # 前端显示顺序 content = models.TextField("内容", max_length=2048) def __str__(self): return "%s" % self.title class Meta: verbose_name = "12-课程大纲表" db_table = verbose_name verbose_name_plural = verbose_name unique_together = ('course_detail', 'title')
2、接口的编写
- 课程这个模块,所有的功能都是展示,基于数据展示的,通常称为数据接口
- 课程页面:有课程所有分类这个接口,有展示课程的接口
- 课程详情页面:详情页面的数据接口
- 详情页面下的子路由对应子组件的数据接口:课程章节课时、课程的评论、课程的常见问题
from django.urls import path from .views import CategoryView, CourseView, CourseDetailView, CourseChapterView, CourseCommentView, QuestionView from .video_view import PolyvView urlpatterns = [ path('category', CategoryView.as_view()), path('list', CourseView.as_view()), path('detail/<int:pk>', CourseDetailView.as_view()), path('chapter/<int:pk>', CourseChapterView.as_view()), path('comment/<int:pk>', CourseCommentView.as_view()), path('question/<int:pk>', QuestionView.as_view()), path('polyv', PolyvView.as_view()), ]
from rest_framework import serializers from . import models class CategorySerializer(serializers.ModelSerializer): class Meta: model = models.Category fields = "__all__" class CourseSerializer(serializers.ModelSerializer): level = serializers.CharField(source="get_level_display") price = serializers.SerializerMethodField() def get_price(self, obj): print(obj.price_policy.all()) return obj.price_policy.all().order_by("price").first().price class Meta: model = models.Course fields = ["id", "title", "course_img", "brief", "level", "study_num", "price"] class CourseDetailSerializer(serializers.ModelSerializer): level = serializers.CharField(source="course.get_level_display") study_num = serializers.IntegerField(source="course.study_num") recommend_courses = serializers.SerializerMethodField() teachers = serializers.SerializerMethodField() price_policy = serializers.SerializerMethodField() course_outline = serializers.SerializerMethodField() def get_course_outline(self, obj): return [{"id": outline.id, "title": outline.title, "content": outline.content} for outline in obj.course_outline.all().order_by("order")] def get_price_policy(self, obj): return [{"id": price.id, "valid_price_display": price.get_valid_period_display(), "price": price.price} for price in obj.course.price_policy.all()] def get_teachers(self, obj): return [{"id": teacher.id, "name": teacher.name} for teacher in obj.teachers.all()] def get_recommend_courses(self, obj): return [{"id": course.id, "title": course.title} for course in obj.recommend_courses.all()] class Meta: model = models.CourseDetail fields = ["id", "hours", "summary", "level", "study_num", "recommend_courses", "teachers", "price_policy", "course_outline"] class CourseChapterSerializer(serializers.ModelSerializer): sections = serializers.SerializerMethodField() def get_sections(self, obj): return [{"id": section.id, "title": section.title, "free_trail": section.free_trail} for section in obj.course_sections.all().order_by("section_order")] class Meta: model = models.CourseChapter fields = ["id", "title", "sections"] class CourseCommentSerializer(serializers.ModelSerializer): account = serializers.CharField(source="account.username") class Meta: model = models.Comment fields = ["id", "account", "content", "date"] class QuestionSerializer(serializers.ModelSerializer): class Meta: model = models.OftenAskedQuestion fields = ["id", "question", "answer"]
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from . import models from .serializers import CategorySerializer, CourseSerializer, CourseDetailSerializer, CourseChapterSerializer from .serializers import CourseCommentSerializer, QuestionSerializer # Create your views here. class CategoryView(APIView): """课程分类接口""" def get(self, request): # 通过ORM操作获取所有分类数据 queryset = models.Category.objects.all() # 利用序列化器去序列化我们的数据 ser_obj = CategorySerializer(queryset, many=True) # 返回 return Response(ser_obj.data) class CourseView(APIView): """查看所有免费课程的接口""" def get(self, request): # 获取过滤条件中的分类ID category_id = request.query_params.get("category", 0) # 根据分类获取课程 if category_id == 0: # 证明没有分类,可以拿所有的课程数据 queryset = models.Course.objects.all().order_by("order") else: queryset = models.Course.objects.filter(category_id=category_id).all().order_by("order") # 序列化课程数据 ser_obj = CourseSerializer(queryset, many=True) # 返回 return Response(ser_obj.data) class CourseDetailView(APIView): """课程详情页面""" def get(self, request, pk): # 根据pk获取到课程详情对象 course_detail_obj = models.CourseDetail.objects.filter(course__id=pk).first() if not course_detail_obj: return Response({"code": 1001, "error": "查询的课程详情不存在"}) # 序列化课程详情 ser_obj = CourseDetailSerializer(course_detail_obj) # 返回 return Response(ser_obj.data) class CourseChapterView(APIView): """课程章节接口""" def get(self, request, pk): # 数据结构["第一章": {课时一, 课时二}] queryset = models.CourseChapter.objects.filter(course_id=pk).all().order_by("chapter") # 序列化章节对象 ser_obj = CourseChapterSerializer(queryset, many=True) # 返回 return Response(ser_obj.data) class CourseCommentView(APIView): def get(self, request, pk): # 通过课程id找到课程所有的评论 queryset = models.Course.objects.filter(id=pk).first().course_comments.all() # 序列化 ser_obj = CourseCommentSerializer(queryset, many=True) # 返回 return Response(ser_obj.data) class QuestionView(APIView): def get(self, request, pk): queryset = models.Course.objects.filter(id=pk).first().often_ask_questions.all() ser_obj = QuestionSerializer(queryset, many=True) return Response(ser_obj.data)
from django.contrib import admin # Register your models here. from . import models for table in models.__all__: admin.site.register(getattr(models, table))
二、登录认证模块(token存Redis)
- 以前前后端不分离用cookie,session解决,现在前后端分离使用token令牌。
- 用户登录成功后,生成一个随机字符串token给前端返回
- 前端以后都携带这个token来访问,这样后端只需要鉴别这个token就可以做认证
import redis POOL = redis.ConnectionPool(host="127.0.0.1", port=6379, decode_responses=True, max_connections=10)
class BaseResponse(object): def __init__(self): self.code = 1000 self.data = None self.error = None @property def dict(self): return self.__dict__
from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from .redis_pool import POOL from Course.models import Account import redis CONN = redis.Redis(connection_pool=POOL) class LoginAuth(BaseAuthentication): def authenticate(self, request): # 从请求头中获取前端带过来的token token = request.META.get("HTTP_AUTHENTICATION", "") if not token: raise AuthenticationFailed("没有携带token") # 去redis比对 user_id = CONN.get(str(token)) if user_id == None: raise AuthenticationFailed("token过期") user_obj = Account.objects.filter(id=user_id).first() return user_obj, token
urlpatterns = [ path('register', RegisterView.as_view()), path('login', LoginView.as_view()), path('test_auth', TestView.as_view()), ]
from rest_framework import serializers from Course.models import Account import hashlib class RegisterSerializer(serializers.ModelSerializer): class Meta: model = Account fields = "__all__" def create(self, validated_data): pwd = validated_data["pwd"] pwd_salt = "luffy_password" + pwd md5_str = hashlib.md5(pwd_salt.encode()).hexdigest() user_obj = Account.objects.create(username=validated_data["username"], pwd=md5_str) return user_obj
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from .serializers import RegisterSerializer from utils.base_response import BaseResponse from Course.models import Account from utils.redis_pool import POOL import redis import uuid from utils.my_auth import LoginAuth from utils.geetest import GeetestLib from django.http import HttpResponse import json # Create your views here. class RegisterView(APIView): def post(self, request): res = BaseResponse() # 用序列化器做校验 ser_obj = RegisterSerializer(data=request.data) if ser_obj.is_valid(): ser_obj.save() res.data = ser_obj.data else: res.code = 1020 res.error = ser_obj.errors return Response(res.dict) class LoginView(APIView): def post(self, request): res = BaseResponse() username = request.data.get("username", "") pwd = request.data.get("pwd", "") user_obj = Account.objects.filter(username=username, pwd=pwd).first() if not user_obj: res.code = 1030 res.error = "用户名或密码错误" return Response(res.dict) # 用户登录成功生成一个token写入redis # 写入redis token : user_id conn = redis.Redis(connection_pool=POOL) try: token = uuid.uuid4() # conn.set(str(token), user_obj.id, ex=10) conn.set(str(token), user_obj.id) res.data = token except Exception as e: print(e) res.code = 1031 res.error = "创建令牌失败" return Response(res.dict) class TestView(APIView): authentication_classes = [LoginAuth, ] def get(self, request): return Response("认证测试") pc_geetest_id = "b46d1900d0a894591916ea94ea91bd2c" pc_geetest_key = "36fc3fe98530eea08dfc6ce76e3d24c4" REDIS_CONN = redis.Redis(connection_pool=POOL) class GeetestView(APIView): def get(self, request): user_id = 'test' gt = GeetestLib(pc_geetest_id, pc_geetest_key) status = gt.pre_process(user_id) # request.session[gt.GT_STATUS_SESSION_KEY] = status REDIS_CONN.set(gt.GT_STATUS_SESSION_KEY, status) # request.session["user_id"] = user_id REDIS_CONN.set("gt_user_id", user_id) response_str = gt.get_response_str() return HttpResponse(response_str) def post(self, request): # print(request.session.get("user_id")) print(request.META.get("HTTP_AUTHENTICATION")) print(request.data) gt = GeetestLib(pc_geetest_id, pc_geetest_key) challenge = request.data.get(gt.FN_CHALLENGE, '') validate = request.data.get(gt.FN_VALIDATE, '') seccode = request.data.get(gt.FN_SECCODE, '') # username # pwd # status = request.session.get(gt.GT_STATUS_SESSION_KEY) # print(status) # user_id = request.session.get("user_id") # print(user_id) status = REDIS_CONN.get(gt.GT_STATUS_SESSION_KEY) user_id = REDIS_CONN.get("gt_user_id") if status: result = gt.success_validate(challenge, validate, seccode, user_id) else: result = gt.failback_validate(challenge, validate, seccode) result = {"status": "success"} if result else {"status": "fail"} # if result: # # 证明验证码通过 # # 判断用户名和密码 # else: # # 返回验证码错误 return HttpResponse(json.dumps(result))
三、登录认证模块(token存mysql)
# 拓展之前课程模块下的用户表 class Account(models.Model): username = models.CharField(max_length=32, verbose_name="用户姓名", unique=True) password = models.CharField(max_length=32, verbose_name="用户密码") # head_img = models.CharField(max_length=256, default='/static/frontend/head_portrait/logo@2x.png', # verbose_name="个人头像") token = models.UUIDField(null=True, blank=True) def __str__(self): return self.username class Meta: verbose_name = "11-用户表" db_table = verbose_name verbose_name_plural = verbose_name
class BaseResponse(object): def __init__(self): self.code = 1000 self.data = None self.error = None @property def dict(self): return self.__dict__
from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from Course.models import Account # django 提供的拿时间的接口 提供的是根据django配置的时区拿到的当前时间 from django.utils.timezone import now class LoginAuth(BaseAuthentication): def authenticate(self, request): # 从请求头中获取前端带过来的token token = request.META.get("HTTP_AUTHENTICATION", "") if not token: raise AuthenticationFailed("没有携带token") # 去redis比对 user_obj = Account.objects.filter(token=token).first() if not user_obj: raise AuthenticationFailed("token过期") else: old_time = user_obj.create_token_time if (now() - old_time).days > 7: raise AuthenticationFailed({"code": 1020, "error": "无效的token"}) return user_obj, token
urlpatterns = [ path('register', RegisterView.as_view()), path('login', LoginView.as_view()), path('test_auth', TestView.as_view()), ]
from rest_framework import serializers from Course.models import Account import hashlib class RegisterSerializer(serializers.ModelSerializer): class Meta: model = Account fields = "__all__" def create(self, validated_data): pwd = validated_data["pwd"] pwd_salt = "luffy_password" + pwd md5_str = hashlib.md5(pwd_salt.encode()).hexdigest() user_obj = Account.objects.create(username=validated_data["username"], pwd=md5_str) return user_obj
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from .serializers import RegisterSerializer from utils.base_response import BaseResponse from Course.models import Account import uuid # Create your views here. class RegisterView(APIView): def post(self, request): res = BaseResponse() # 用序列化器做校验 ser_obj = RegisterSerializer(data=request.data) if ser_obj.is_valid(): ser_obj.save() res.data = ser_obj.data else: res.code = 1020 res.error = ser_obj.errors return Response(res.dict) class LoginView(APIView): def post(self, request): res = BaseResponse() username = request.data.get("username", "") pwd = request.data.get("pwd", "") user_obj = Account.objects.filter(username=username, pwd=pwd).first() if not user_obj: res.code = 1030 res.error = "用户名或密码错误" return Response(res.dict) try: token = uuid.uuid4() ### user_obj.update(token=token) res.data = token except Exception as e: print(e) res.code = 1031 res.error = "创建令牌失败" return Response(res.dict) # 所有这是一个需要认证的接口 class TestView(APIView): authentication_classes = [LoginAuth, ] def get(self, request): return Response("认证测试")
四、购物车模块
- 用户点击商品加入购物车,个人中心可以查看自己所有购物车中数据
- 在购物车中可以删除课程,还可以更新购物车中课程的价格策略
- 所以接口应该有四种请求方式, get,post,patch,delete
- 因为购物车是属于中间状态数据而且很多时候需要过期时间所以选择存储到redis
from django.urls import path from .views import ShoppingCarView from .settlement_view import SettlementView from .payment_view import PaymentView urlpatterns = [ path('shopping_car', ShoppingCarView.as_view()), path('settlement', SettlementView.as_view()), path('payment', PaymentView.as_view()), ]
from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from .redis_pool import POOL from Course.models import Account import redis CONN = redis.Redis(connection_pool=POOL) class LoginAuth(BaseAuthentication): def authenticate(self, request): # 从请求头中获取前端带过来的token token = request.META.get("HTTP_AUTHENTICATION", "") if not token: raise AuthenticationFailed("没有携带token") # 去redis比对 user_id = CONN.get(str(token)) if user_id == None: raise AuthenticationFailed("token过期") user_obj = Account.objects.filter(id=user_id).first() return user_obj, token
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from utils.base_response import BaseResponse from utils.my_auth import LoginAuth from utils.redis_pool import POOL from Course.models import Course import json import redis # Create your views here. # 前端传过来 course_id price_policy_id # 把购物车数据放入redis """ { SHOPPINGCAR_USERID_COURSE_ID: { "id", 课程id "title", 课程标题 "course_img", "price_policy_dict": { price_policy_id: "{valid_period, price, valid_period_display}" price_policy_id2: "{valid_period, price, valid_period_display}" price_policy_id3: "{valid_period, price, valid_period_display}" }, "default_price_policy_id": 1 默认选中的价格id } } """ SHOPPINGCAR_KEY = "SHOPPINGCAR_%s_%s" CONN = redis.Redis(connection_pool=POOL) class ShoppingCarView(APIView): authentication_classes = [LoginAuth, ] # 给购物车增加商品 def post(self, request): res = BaseResponse() try: # 1, 获取前端传过来的数据以及user_id course_id = request.data.get("course_id", "") price_policy_id = request.data.get("price_policy_id", "") user_id = request.user.pk # 2, 校验数据的合法性 # 2.1 校验课程id合法性 course_obj = Course.objects.filter(id=course_id).first() if not course_obj: res.code = 1040 res.error = "课程id不合法" return Response(res.dict) # 2.2 校验价格策略id是否合法 price_policy_queryset = course_obj.price_policy.all() price_policy_dict = {} for price_policy in price_policy_queryset: price_policy_dict[price_policy.id] = { "price": price_policy.price, "valid_period": price_policy.valid_period, "valid_period_display": price_policy.get_valid_period_display() } if price_policy_id not in price_policy_dict: res.code = 1041 res.error = "价格策略id不合法" return Response(res.dict) # 3,构建redisKEY key = SHOPPINGCAR_KEY % (user_id, course_id) # 4,构建数据结构 course_info = { "id": course_obj.id, "title": course_obj.title, "course_img": str(course_obj.course_img), "price_policy_dict": json.dumps(price_policy_dict, ensure_ascii=False), "default_price_policy_id": price_policy_id } # 5 写入redis CONN.hmset(key, course_info) res.data = "加入购物车成功" except Exception as e: res.code = 1012 res.error = "加入购物车失败" return Response(res.dict) def get(self, request): res = BaseResponse() try: # 1, 拼接redis key user_id = request.user.pk shopping_car_key = SHOPPINGCAR_KEY % (user_id, "*") # 2, 去redis中读取数据 # 2.1 模糊匹配所有的keys # 3,构建数据结构展示 all_keys = CONN.scan_iter(shopping_car_key) ret = [] for key in all_keys: ret.append(CONN.hgetall(key)) res.data = ret except Exception as e: res.code = 1013 res.error = "获取购物车失败" return Response(res.dict) def put(self, request): # 前端 course_id price_policy_id res = BaseResponse() try: # 1, 获取前端传过来的数据以及user_id course_id = request.data.get("course_id", "") price_policy_id = request.data.get("price_policy_id", "") user_id = request.user.pk # 2, 校验数据的合法性 # 2.1 course_id是否合法 key = SHOPPINGCAR_KEY % (user_id, course_id) if not CONN.exists(key): res.code = 1043 res.error = "课程id不合法" return Response(res.dict) # 2,2 price_policy_id是否合法 price_policy_dict = json.loads(CONN.hget(key, "price_policy_dict")) if str(price_policy_id) not in price_policy_dict: res.code = 1044 res.error = "价格策略不合法" return Response(res.dict) # 3, 更新redis default_price_policy_id CONN.hset(key, "default_price_policy_id", price_policy_id) res.data = "更新成功" except Exception as e: res.code = 1014 res.error = "更新购物车失败" return Response(res.dict) def delete(self, request): # course_list = [course_id, ] res = BaseResponse() try: # 1 获取前端传来的数据以及user_id course_list = request.data.get("course_list", "") user_id = request.user.pk # 2 校验course_id是否合法 for course_id in course_list: key = SHOPPINGCAR_KEY % (user_id, course_id) if not CONN.exists(key): res.code = 1045 res.error = "课程ID不合法" return Response(res.dict) # 3, 删除redis数据 CONN.delete(key) res.data = "删除成功" except Exception as e: res.code = 1014 res.error = "删除购物车失败" return Response(res.dict)
五、结算中心模块
结算中心要开始选择优惠券了,有单独的课程优惠券还有全局优惠券。
from django.db import models # Create your models here. from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from Course.models import Account # Create your models here. __all__ = ["Coupon", "CouponRecord", "Order", "OrderDetail", "TransactionRecord"] class Coupon(models.Model): """优惠券生成规则""" name = models.CharField(max_length=64, verbose_name="活动名称") brief = models.TextField(blank=True, null=True, verbose_name="优惠券介绍") coupon_type_choices = ((0, '通用券'), (1, '满减券'), (2, '折扣券')) coupon_type = models.SmallIntegerField(choices=coupon_type_choices, default=0, verbose_name="券类型") money_equivalent_value = models.IntegerField(verbose_name="等值货币", null=True, blank=True, default=0) off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True, default=100) minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段", null=True, blank=True) content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=None) object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定") # 不绑定代表全局优惠券 content_object = GenericForeignKey('content_type', 'object_id') open_date = models.DateField("优惠券领取开始时间") close_date = models.DateField("优惠券领取结束时间") valid_begin_date = models.DateField(verbose_name="有效期开始时间", blank=True, null=True) valid_end_date = models.DateField(verbose_name="有效结束时间", blank=True, null=True) coupon_valid_days = models.PositiveIntegerField(verbose_name="优惠券有效期(天)", blank=True, null=True, help_text="自券被领时开始算起") date = models.DateTimeField(auto_now_add=True) class Meta: verbose_name_plural = "13. 优惠券生成规则记录" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s(%s)" % (self.get_coupon_type_display(), self.name) def save(self, *args, **kwargs): if not self.coupon_valid_days or (self.valid_begin_date and self.valid_end_date): if self.valid_begin_date and self.valid_end_date: if self.valid_end_date <= self.valid_begin_date: raise ValueError("valid_end_date 有效期结束日期必须晚于 valid_begin_date ") if self.coupon_valid_days == 0: raise ValueError("coupon_valid_days 有效期不能为0") if self.close_date < self.open_date: raise ValueError("close_date 优惠券领取结束时间必须晚于 open_date优惠券领取开始时间 ") super(Coupon, self).save(*args, **kwargs) class CouponRecord(models.Model): """优惠券发放、消费纪录""" coupon = models.ForeignKey("Coupon", on_delete=None) number = models.CharField(max_length=64, unique=True, verbose_name="用户优惠券记录的流水号") account = models.ForeignKey(to=Account, verbose_name="拥有者", on_delete=None) status_choices = ((0, '未使用'), (1, '已使用'), (2, '已过期')) status = models.SmallIntegerField(choices=status_choices, default=0) get_time = models.DateTimeField(verbose_name="领取时间", help_text="用户领取时间") used_time = models.DateTimeField(blank=True, null=True, verbose_name="使用时间") order = models.ForeignKey("Order", blank=True, null=True, verbose_name="关联订单", on_delete=None) # 一个订单可以有多个优惠券 class Meta: verbose_name_plural = "14. 用户优惠券领取使用记录表" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return '%s-%s-%s' % (self.account, self.number, self.status) class Order(models.Model): """订单""" payment_type_choices = ((0, '微信'), (1, '支付宝'), (2, '优惠码'), (3, '贝里')) payment_type = models.SmallIntegerField(choices=payment_type_choices) payment_number = models.CharField(max_length=128, verbose_name="支付第3方订单号", null=True, blank=True) order_number = models.CharField(max_length=128, verbose_name="订单号", unique=True) # 考虑到订单合并支付的问题 account = models.ForeignKey(to=Account, on_delete=None) actual_amount = models.FloatField(verbose_name="实付金额") status_choices = ((0, '交易成功'), (1, '待支付'), (2, '退费申请中'), (3, '已退费'), (4, '主动取消'), (5, '超时取消')) status = models.SmallIntegerField(choices=status_choices, verbose_name="状态") date = models.DateTimeField(auto_now_add=True, verbose_name="订单生成时间") pay_time = models.DateTimeField(blank=True, null=True, verbose_name="付款时间") cancel_time = models.DateTimeField(blank=True, null=True, verbose_name="订单取消时间") class Meta: verbose_name_plural = "15. 订单表" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s" % self.order_number class OrderDetail(models.Model): """订单详情""" order = models.ForeignKey("Order", on_delete=None) content_type = models.ForeignKey(ContentType, on_delete=None) # 可关联普通课程或学位 object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') original_price = models.FloatField("课程原价") price = models.FloatField("折后价格") valid_period_display = models.CharField("有效期显示", max_length=32) # 在订单页显示 valid_period = models.PositiveIntegerField("有效期(days)") # 课程有效期 memo = models.CharField(max_length=255, blank=True, null=True, verbose_name="备忘录") def __str__(self): return "%s - %s - %s" % (self.order, self.content_type, self.price) class Meta: verbose_name_plural = "16. 订单详细" db_table = verbose_name_plural verbose_name = verbose_name_plural class TransactionRecord(models.Model): """贝里交易纪录""" account = models.ForeignKey(to=Account, on_delete=None) amount = models.IntegerField("金额") balance = models.IntegerField("账户余额") transaction_type_choices = ((0, '收入'), (1, '支出'), (2, '退款'), (3, "提现")) # 2 为了处理 订单过期未支付时,锁定期贝里的回退 transaction_type = models.SmallIntegerField(choices=transaction_type_choices) transaction_number = models.CharField(unique=True, verbose_name="流水号", max_length=128) date = models.DateTimeField(auto_now_add=True) memo = models.CharField(max_length=128, blank=True, null=True, verbose_name="备忘录") class Meta: verbose_name_plural = "17. 贝里交易记录" db_table = verbose_name_plural verbose_name = verbose_name_plural def __str__(self): return "%s" % self.transaction_number
from rest_framework.views import APIView from rest_framework.response import Response from utils.base_response import BaseResponse from utils.redis_pool import POOL from django.utils.timezone import now from utils.my_auth import LoginAuth import redis from .views import SHOPPINGCAR_KEY from .models import CouponRecord import json CONN = redis.Redis(connection_pool=POOL) SETTLEMENT_KEY = "SETTLEMENT_%s_%s" GLOBAL_COUPON_KEY = "GLOBAL_COUPON_%s" """ 结算中心 在购物车里选择了商品以及价格策略点击结算 才进入结算中心 在结算中心用户可以选择优惠券 前端传过来数据 course_list redis = { settlement_userid_courseid: { id, 课程id, title, course_img, valid_period_display, price, course_coupon_dict: { coupon_id: {优惠券信息} coupon_id2: {优惠券信息} coupon_id3: {优惠券信息} } # 默认不给你选 这个字段只有更新的时候才添加 default_coupon_id: 1 } global_coupon_userid: { coupon_id: {优惠券信息} coupon_id2: {优惠券信息} coupon_id3: {优惠券信息}, # 这个字段只有更新的时候才添加 # 在用户进入结算中心选择优惠券的时候 也就是更新请求的时候更改 default_global_coupon_id: 1 } } """ class SettlementView(APIView): authentication_classes = [LoginAuth, ] def post(self, request): res = BaseResponse() try: # 1 获取前端的数据以及user_id course_list = request.data.get("course_list", "") user_id = request.user.pk # 2 校验数据的合法性 for course_id in course_list: # 2.1 判断course_id 是否在购物车中 shopping_car_key = SHOPPINGCAR_KEY % (user_id, course_id) if not CONN.exists(shopping_car_key): res.code = 1050 res.error = "课程ID不合法" return Response(res.dict) # 3 构建数据结构 # 3.1 获取用户的所有合法优惠券 user_all_coupons = CouponRecord.objects.filter( account_id=user_id, status=0, coupon__valid_begin_date__lte=now(), coupon__valid_end_date__gte=now(), ).all() print(user_all_coupons) # 3.2 构建优惠券dict course_coupon_dict = {} global_coupon_dict = {} for coupon_record in user_all_coupons: coupon = coupon_record.coupon if coupon.object_id == course_id: course_coupon_dict[coupon.id] = { "id": coupon.id, "name": coupon.name, "coupon_type": coupon.get_coupon_type_display(), "object_id": coupon.object_id, "money_equivalent_value": coupon.money_equivalent_value, "off_percent": coupon.off_percent, "minimum_consume": coupon.minimum_consume } elif coupon.object_id == "": global_coupon_dict[coupon.id] = { "id": coupon.id, "name": coupon.name, "coupon_type": coupon.get_coupon_type_display(), "money_equivalent_value": coupon.money_equivalent_value, "off_percent": coupon.off_percent, "minimum_consume": coupon.minimum_consume } # 3.3 构建写入redis的数据结构 course_info = CONN.hgetall(shopping_car_key) price_policy_dict = json.loads(course_info["price_policy_dict"]) default_policy_id = course_info["default_price_policy_id"] valid_period = price_policy_dict[default_policy_id]["valid_period_display"] price = price_policy_dict[default_policy_id]["price"] settlement_info = { "id": course_info["id"], "title": course_info["title"], "course_img": course_info["course_img"], "valid_period": valid_period, "price": price, "course_coupon_dict": json.dumps(course_coupon_dict, ensure_ascii=False) } # 4 写入redis settlement_key = SETTLEMENT_KEY % (user_id, course_id) global_coupon_key = GLOBAL_COUPON_KEY % user_id CONN.hmset(settlement_key, settlement_info) if global_coupon_dict: CONN.hmset(global_coupon_key, global_coupon_dict) # 5 删除购物车中的数据 CONN.delete(shopping_car_key) res.data = "加入结算中心成功" except Exception as e: res.code = 1020 res.error = "结算失败" return Response(res.dict) def get(self, request): res = BaseResponse() try: # 1, 获取user_id user_id = request.user.pk # 2, 拼接所有key # 3, 去redis取数据 settlement_key = SETTLEMENT_KEY % (user_id, "*") global_coupon_key = GLOBAL_COUPON_KEY % user_id all_keys = CONN.scan_iter(settlement_key) ret = [] for key in all_keys: ret.append(CONN.hgetall(key)) global_coupon_info = CONN.hgetall(global_coupon_key) res.data = { "settlement_info": ret, "global_coupon_dict": global_coupon_info } except Exception as e: res.code = 1024 res.error = "获取结算中心失败" return Response(res.dict) def put(self, request): # course_id course_coupon_id global_coupon_id res = BaseResponse() try: # 1, 获取前端传过来数据 course_id = request.data.get("course_id", "") course_coupon_id = request.data.get("course_coupon_id", "") global_coupon_id = request.data.get("global_coupon_id", "") user_id = request.user.pk # 2, 校验数据合法性 # 2.1 校验course_id key = SETTLEMENT_KEY % (user_id, course_id) if course_id: if not CONN.exists(key): res.code = 1060 res.error = "课程ID不合法" return Response(res.dict) # 2.2 校验 course_coupon_id if course_coupon_id: course_coupon_dict = json.loads(CONN.hget(key, "course_coupon_dict")) if str(course_coupon_id) not in course_coupon_dict: res.code = 1061 res.error = "课程优惠券ID不合法" return Response(res.dict) # 2.3 校验global_coupon_id if global_coupon_id: global_coupon_key = GLOBAL_COUPON_KEY % user_id if not CONN.exists(global_coupon_key): res.code = 1062 res.error = "全局优惠券ID不合法" return Response(res.dict) CONN.hset(global_coupon_key, "default_global_coupon_id", global_coupon_id) # 3,修改redis中数据 CONN.hset(key, "default_coupon_id", course_coupon_id) res.data = "更新优惠券成功" except Exception as e: res.code = 1026 res.error = "更改优惠券失败" return Response(res.dict)
六、支付中心模块
from rest_framework.views import APIView from rest_framework.response import Response from utils.my_auth import LoginAuth from utils.base_response import BaseResponse from .settlement_view import SETTLEMENT_KEY, GLOBAL_COUPON_KEY from utils.redis_pool import POOL import redis from Course.models import Course from .models import Coupon from django.utils.timezone import now COON = redis.Redis(connection_pool=POOL) """ 点击去支付 -POST-》 校验 课程 价格策略 课程优惠券 通用优惠券 贝里 和 应收价钱是否一致 生成订单(未支付状态) ----> 引导你跳转到支付宝 二维码页面 支付宝 支付成功 - POST请求 -》 我 我收到请求去修改订单的状态为已支付 5秒之后 会 - GET 请求 -》 我给他返回一个订单页面(支付成功) """ # price balance class PaymentView(APIView): authentication_classes = [LoginAuth, ] def post(self, request): res = BaseResponse() # 1 获取数据 balance = request.data.get("balance", 0) price = request.data.get("price", "") user_id = request.user.pk # 2 校验数据的合法性 # 2.1 校验贝里数是否合法 if int(balance) > request.user.balance: res.code = 1070 res.error = "抵扣的贝里错误" return Response(res.dict) # 2.2 从用户的结算中心拿数据 跟数据库比对是否合法 settlement_key = SETTLEMENT_KEY % (user_id, "*") all_keys = COON.scan_iter(settlement_key) # 课程id是否合法 course_rebate_total_price = 0 for key in all_keys: settlement_info = COON.hgetall(key) course_id = settlement_info["id"] course_obj = Course.objects.filter(id=course_id).first() if not course_obj or course_obj.status == 1: res.code = 1071 res.error = "课程id不合法" return Response(res.dict) # 课程优惠券是否过期 course_coupon_id = settlement_info.get("default_coupon_id", 0) if course_coupon_id: coupon_dict = Coupon.objects.filter( id=course_coupon_id, couponrecord__status=0, couponrecord__account_id=user_id, object_id=course_id, valid_begin_date__lte=now(), valid_end_date__gte=now(), ).values("coupon_type", "money_equivalent_value", "off_percent", "minimum_consume") if not coupon_dict: res.code = 1072 res.error = "优惠券不合法" return Response(res.dict) # 2.3 校验price # 得到所有的课程的折后价格和 course_pirce = settlement_info["price"] course_rebate_price = self.account_price(coupon_dict, course_pirce) if course_rebate_price == -1: res.code = 1074 res.error = "课程优惠券不符合要求" return Response(res.dict) course_rebate_total_price += course_rebate_price # 跟全局优惠券做折扣 # 校验全局优惠券是否合法 global_coupon_key = GLOBAL_COUPON_KEY % user_id global_coupon_id = int(COON.hget(global_coupon_key, "default_global_coupon_id")) if global_coupon_id: global_coupon_dict = Coupon.objects.filter( id=global_coupon_id, couponrecord__status=0, couponrecord__account_id=user_id, valid_begin_date__lte=now(), valid_end_date__gte=now(), ).values("coupon_type", "money_equivalent_value", "off_percent", "minimum_consume") if not global_coupon_dict: res.code = 1073 res.error = "全局优惠券id不合法" return Response(res.dict) global_rebate_price = self.account_price(global_coupon_dict, course_rebate_total_price) if global_rebate_price == -1: res.code = 1076 res.error = "全局优惠券不符合要求" return Response(res.dict) # 抵扣贝里 balance_money = balance / 100 balance_rebate_price = global_rebate_price - balance if balance_rebate_price < 0: balance_rebate_price = 0 # 终极校验price if balance_rebate_price != price: res.code = 1078 res.error = "价格不合法" return Response(res.dict) # 先去创建订单 订单状态未支付状态 # 3 调用支付宝接口支付 # 如果成功支付支付宝会给我们发回调 # 改变订单的状态 # 注意订单详情表有多个记录 # 更改优惠券的使用状态 # 更改用户表里的贝里 贝里要添加交易记录 def account_price(self, coupon_dict, price): coupon_type = coupon_dict["coupon_type"] if coupon_type == 0: # 通用优惠券 money_equivalent_value = coupon_dict["money_equivalent_value"] if price - money_equivalent_value >=0: rebate_price = price - money_equivalent_value else: rebate_price = 0 elif coupon_type == 1: # 满减 money_equivalent_value = coupon_dict["money_equivalent_value"] minimum_consume = coupon_dict["minimum_consume"] if price >= minimum_consume: rebate_price = price - money_equivalent_value else: return -1 elif coupon_type == 2: # 折扣 minimum_consume = coupon_dict["minimum_consume"] off_percent = coupon_dict["off_percent"] if price >= minimum_consume: rebate_price = price * (off_percent / 100) else: return -1 return rebate_price