考试第二部分:MySQL数据库

6. MySQL中char和varchar的区别(1分)

  1. char是定长,varchar是变长。
  2. char的查询速度比varchar要快。

7. MySQL中varchar(50)的50表示什什么意思?(1分)

  1. 是字符长度。一个中文,也是一个字符。

8. left join、right join以及inner join的区别?(2分)

  1. left join,表示左连接,以左表为基准,如果左表有不匹配的,显示为空
  2. right join,表示右连接,以右表为基准,如果右表有不匹配的,显示为空
  3. inner join,表示内连接,只显示2个表条件符合的记录,不匹配的不显示

9. MySQL组合索引(2分)

where⼦子句句中有a、b、c 三个查询条件,创建⼀一个组合索引 abc(a,b,c),那么如下那中情况会命 中索引:

a. where (a)

b. where (b)

c. where (c)

d. where (a,b)

e. where (b,c)

f. where (a,c)

g. where (a,b,c)

  1. adfg 会命中索引

解释:

索引有2个功能:加快查询和约束。

这里的约束指的是唯一索引,联合唯一索引。

索引遵循的原则: 最左前缀原则

  1. 你可以认为联合索引是闯关游戏的设计
  2. 例如你这个联合索引是state/city/zipCode
  3. 那么state就是第一关 city是第二关, zipCode就是第三关
  4. 你必须匹配了第一关,才能匹配第二关,匹配了第一关和第二关,才能匹配第三关
  5. 你不能直接到第二关的
  6. 索引的格式就是第一层是state,第二层才是city
  7. 索引是因为B+树结构 所以查找快 如果单看第三列 是非排序的。
  8. 多列索引是先按照第一列进行排序,然后在第一列排好序的基础上再对第二列排序,如果没有第一列的话,直接访问第二列,那第二列肯定是无序的,直接访问后面的列就用不到索引了。
  9. 所以如果不是在前面列的基础上而是但看后面某一列,索引是失效的。

简而言之,只要where条件包含最左边的字段,那么它就会用到组合索引,反之亦然!

如果创建了组合索引,但是却没有命中,这是浪费磁盘空间。因为索引也占用磁盘!

10. 假设学⽣生Student和教师Teacher关系模型如下:(4分) Student(学号、姓名、性别、类型、身份证号) Teacher(教师号、姓名、性别、类型、身份证号、工资)

其中,学⽣生表中类别为“本科生”和“研究生”两类;性别为“男”和“女”两类。

a. 性别为女的所有学生。

  1. select * from Student where 性别='女'

b. 学生表中类别分别对应的个数。

select 类型,count(1) from Student group by 类型

c.工资少于10000的女教师的身份证和姓名。

select 身份证,姓名 from Teacher where 性别= '女' and 类型='研究生' and 工资 < 10000

d. 研究生教师平均工资、最⾼高和最低工资。

select AVG(工资),MAX(工资),MIN(工资) from Teacher wherer 类型='研究生'

11. 根据如下表结构建表:(2分)Day104 DRF用户认证,结算中心,django-redis - 图1

CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `balance` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

12.根据如下表查询每个⽤用户第⼀一次下订单的时间。(2分)

Day104 DRF用户认证,结算中心,django-redis - 图2

# 第一次下单时间,就是时间最早的
select name,MIN(order_time) from table group by name

13. 有⼀一个订单系统包含订单信息、商品信息、价格信息且还要⼀一些状态,如何设计表结构(2分)

#最简单的设计

商品表
- id
- 名称
- 价格
- 描述信息

订单表
- id
- 订单号(唯一)
- 商品id
- 用户id

用户表
- id
- username
- password

14. 有如下表:(3分)

products(商品表) columns为 id、name、price

orders(商城订单表) columns为 id、reservations_id、product_id、quantity(数量量)

reservations(酒店订单表) columns为 id、user_id、price、created_at

ps:这个一个真实面试题!

应用场景:比如万达酒店,需要订购商品,比如红酒,红木家具…

a. 各个商品的售卖情况,需要字段:商品名、购买总数、商品收⼊入(单价*数量量)

SELECT
    products. NAME,
    sum(orders.quantity),
    products.price * sum(orders.quantity)
FROM
    orders
LEFT JOIN products ON products.id = orders.product_id
GROUP BY
    orders.product_id

b. 所有用户在2018-01-01至2018-02-01下单次数、下单金额、商城下单次数、商城下单金额

# 注意:最后的期限要加1天。因为23:59:59也是属于当天的
SELECT
    count(1),
    sum(reservations.price),
    sum(orders.quantity),
    products.price * sum(orders.quantity)
FROM
    reservations
LEFT JOIN orders ON orders.reservations_id = reservations.id
LEFT JOIN products ON products.id = orders.product_id
WHERE
    reservations.created_at BETWEEN 2018-01-01
AND reservations.created_at '2018-02-02'

c. 历月下单用户数:下单1次的用户数、下单2次的用户数、下单3次及以上的用户数

# 下单1次的用户数
select DATE_FORMAT(created_at,'%Y-%m'),user_id,count(1) from reservations group by DATE_FORMAT(created_at,'%Y-%m'),user_id having count(user_id) = 1;

# 下单2次的用户数
select DATE_FORMAT(created_at,'%Y-%m'),user_id,count(1) from reservations group by DATE_FORMAT(created_at,'%Y-%m'),user_id having count(user_id) = 1;

# 下单3次及以上的用户数
select DATE_FORMAT(created_at,'%Y-%m'),user_id,count(1) from reservations group by DATE_FORMAT(created_at,'%Y-%m'),user_id having count(user_id) >= 3;

15. 根据表写SQL语句句:(5分)

Day104 DRF用户认证,结算中心,django-redis - 图3

• 查询所有同学的学号、姓名、班级名称。(1分)

select student.sid,student.sname,class.caption from student left jon class on class.cid = student.class_id

• 查询没有学⽣生的所有班级。(2分)

select class.caption from student left jon class on class.cid = student.class_id where class.cid is null

• 查询有学⽣生的所有班级的名称和学数量量。(2分)

select class.caption,count(1) from student left jon class on class.cid = student.class_id

一、DRF用户认证

流程图

Day104 DRF用户认证,结算中心,django-redis - 图4

请求到达视图的时候,需要进行认证。

认证是在中间件之后的。如果一旦认证失败,则返回信息给用户

启动项目luffcity,访问购物车

Day104 DRF用户认证,结算中心,django-redis - 图5

注意:要启动redis,否则提示获取购物车数据失败

添加认证

购物车需要登录才能查看,登录成功后,返回一个token

修改views目录下的auth.py

from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from api import models
from api.utils.response import BaseResponse
import uuid

class AuthView(ViewSetMixin,APIView):
    def login(self,request,*args,**kwargs):
        """
        用户登陆认证
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        response = BaseResponse()  # 默认状态
        try:
            user = request.data.get('username')
            pwd = request.data.get('password')
            # 验证用户和密码
            obj = models.Account.objects.filter(username=user,password=pwd).first()
            if not obj:
                response.code = 10002
                response.error = '用户名或密码错误'
            else:
                uid = str(uuid.uuid4())  # 生成唯一id
                response.code = 99999
                response.data = uid

        except Exception as e:
            response.code = 10005
            response.error = '操作异常'

        return Response(response.dict)

请确保已经生成了表api_account,并添加了一条记录

如果没有,请参考昨天的文档!

Day104 DRF用户认证,结算中心,django-redis - 图6

测试用户和密码

Day104 DRF用户认证,结算中心,django-redis - 图7

查看返回结果

Day104 DRF用户认证,结算中心,django-redis - 图8

输入正确的用户名和密码

Day104 DRF用户认证,结算中心,django-redis - 图9

查看返回结果,返回一个随机码。这个就是token

9999表示登录成功

Day104 DRF用户认证,结算中心,django-redis - 图10

既然token已经生成了,并返回给了客户端。那么服务器如何验证客户端的token是否合法呢?

答案是,服务器需要保存token。推荐加一个有效期,比如微信公众号的token有效期为8个小时!

增加token表

修改models.py,增加token表

它和用户表是一对一的关系!

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.db import models
import hashlib


# ######################## 课程相关 ########################

class CourseCategory(models.Model):
    """课程大类, e.g 前端  后端..."""
    name = models.CharField(max_length=64, unique=True)

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

    class Meta:
        verbose_name_plural = "01.课程大类"


class CourseSubCategory(models.Model):
    """课程子类, e.g python linux """
    category = models.ForeignKey("CourseCategory")
    name = models.CharField(max_length=64, unique=True)

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

    class Meta:
        verbose_name_plural = "02.课程子类"


class DegreeCourse(models.Model):
    """学位课程"""
    name = models.CharField(max_length=128, unique=True)
    course_img = models.CharField(max_length=255, verbose_name="缩略图")
    brief = models.TextField(verbose_name="学位课程简介", )
    total_scholarship = models.PositiveIntegerField(verbose_name="总奖学金(贝里)", default=40000)  # 2000 2000
    mentor_compensation_bonus = models.PositiveIntegerField(verbose_name="本课程的导师辅导费用(贝里)", default=15000)
    period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=150)  # 为了计算学位奖学金
    prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024)
    teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师")

    # 用于GenericForeignKey反向查询, 不会生成表字段,切勿删除
    # coupon = GenericRelation("Coupon")

    # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除
    degreecourse_price_policy = GenericRelation("PricePolicy")

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "03.学位课"


class Teacher(models.Model):
    """讲师、导师表"""
    name = models.CharField(max_length=32)
    role_choices = ((0, '讲师'), (1, '导师'))
    role = models.SmallIntegerField(choices=role_choices, default=0)
    title = models.CharField(max_length=64, verbose_name="职位、职称")
    signature = models.CharField(max_length=255, help_text="导师签名", blank=True, null=True)
    image = models.CharField(max_length=128)
    brief = models.TextField(max_length=1024)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "04.导师或讲师"


class Scholarship(models.Model):
    """学位课程奖学金"""
    degree_course = models.ForeignKey("DegreeCourse")
    time_percent = models.PositiveSmallIntegerField(verbose_name="奖励档位(时间百分比)", help_text="只填百分值,如80,代表80%")
    value = models.PositiveIntegerField(verbose_name="奖学金数额")

    def __str__(self):
        return "%s:%s" % (self.degree_course, self.value)

    class Meta:
        verbose_name_plural = "05.学位课奖学金"


class Course(models.Model):
    """专题课/学位课模块表"""
    name = models.CharField(max_length=128, unique=True)
    course_img = models.CharField(max_length=255)
    sub_category = models.ForeignKey("CourseSubCategory")
    course_type_choices = ((0, '付费'), (1, 'VIP专享'), (2, '学位课程'))
    course_type = models.SmallIntegerField(choices=course_type_choices)

    # 不为空;学位课的某个模块
    # 为空;专题课
    degree_course = models.ForeignKey("DegreeCourse", blank=True, null=True, help_text="若是学位课程,此处关联学位表")

    brief = models.TextField(verbose_name="课程概述", max_length=2048)
    level_choices = ((0, '初级'), (1, '中级'), (2, '高级'))
    level = models.SmallIntegerField(choices=level_choices, default=1)
    pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True)
    period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=7)  #
    order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排")
    attachment_path = models.CharField(max_length=128, verbose_name="课件路径", blank=True, null=True)
    status_choices = ((0, '上线'), (1, '下线'), (2, '预上线'))
    status = models.SmallIntegerField(choices=status_choices, default=0)
    template_id = models.SmallIntegerField("前端模板id", default=1)

    coupon = GenericRelation("Coupon")

    # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除
    price_policy = GenericRelation("PricePolicy")

    asked_question = GenericRelation("OftenAskedQuestion")


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


    def save(self, *args, **kwargs):
        if self.course_type == 2:
            if not self.degree_course:
                raise ValueError("学位课程必须关联对应的学位表")
        super(Course, self).save(*args, **kwargs)


    class Meta:
        verbose_name_plural = "06.专题课或学位课模块"


class CourseDetail(models.Model):
    """课程详情页内容"""
    course = models.OneToOneField("Course")
    hours = models.IntegerField("课时")
    course_slogan = models.CharField(max_length=125, blank=True, null=True)
    video_brief_link = models.CharField(verbose_name='课程介绍', max_length=255, blank=True, null=True)
    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 "%s" % self.course

    class Meta:
        verbose_name_plural = "07.课程或学位模块详细"


class OftenAskedQuestion(models.Model):
    """常见问题"""
    content_type = models.ForeignKey(ContentType)  # 关联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:
        unique_together = ('content_type', 'object_id', 'question')
        verbose_name_plural = "08. 常见问题"


class CourseOutline(models.Model):
    """课程大纲"""
    course_detail = models.ForeignKey("CourseDetail")
    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:
        unique_together = ('course_detail', 'title')
        verbose_name_plural = "09. 课程大纲"


class CourseChapter(models.Model):
    """课程章节"""
    course = models.ForeignKey("Course")
    chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
    name = models.CharField(max_length=128)
    summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)

    class Meta:
        unique_together = ("course", 'chapter')
        verbose_name_plural = "10. 课程章节"

    def __str__(self):
        return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)


class CourseSection(models.Model):
    """课时目录"""
    chapter = models.ForeignKey("CourseChapter")
    name = models.CharField(max_length=128)
    order = models.PositiveSmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时")
    section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频'))
    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")
    video_time = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
    pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
    free_trail = models.BooleanField("是否可试看", default=False)

    class Meta:
        unique_together = ('chapter', 'section_link')
        verbose_name_plural = "11. 课时"

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


class Homework(models.Model):
    chapter = models.ForeignKey("CourseChapter")
    title = models.CharField(max_length=128, verbose_name="作业题目")
    order = models.PositiveSmallIntegerField("作业顺序", help_text="同一课程的每个作业之前的order值间隔1-2个数")
    homework_type_choices = ((0, '作业'), (1, '模块通关考核'))
    homework_type = models.SmallIntegerField(choices=homework_type_choices, default=0)
    requirement = models.TextField(max_length=1024, verbose_name="作业需求")
    threshold = models.TextField(max_length=1024, verbose_name="踩分点")
    recommend_period = models.PositiveSmallIntegerField("推荐完成周期(天)", default=7)
    scholarship_value = models.PositiveSmallIntegerField("为该作业分配的奖学金(贝里)")
    note = models.TextField(blank=True, null=True)
    enabled = models.BooleanField(default=True, help_text="本作业如果后期不需要了,不想让学员看到,可以设置为False")

    class Meta:
        unique_together = ("chapter", "title")
        verbose_name_plural = "12. 章节作业"

    def __str__(self):
        return "%s - %s" % (self.chapter, self.title)


# class CourseReview(models.Model):
#     """课程评价"""
#     enrolled_course = models.OneToOneField("EnrolledCourse")
#     about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰")
#     about_video = models.FloatField(default=0, verbose_name="内容实用")
#     about_course = models.FloatField(default=0, verbose_name="课程内容通俗易懂")
#     review = models.TextField(max_length=1024, verbose_name="评价")
#     disagree_number = models.IntegerField(default=0, verbose_name="踩")
#     agree_number = models.IntegerField(default=0, verbose_name="赞同数")
#     tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签")
#     date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期")
#     is_recommend = models.BooleanField("热评推荐", default=False)
#     hide = models.BooleanField("不在前端页面显示此条评价", default=False)
#
#     def __str__(self):
#         return "%s-%s" % (self.enrolled_course.course, self.review)
#
#     class Meta:
#         verbose_name_plural = "13. 课程评价(购买课程后才能评价)"
#
#
# class DegreeCourseReview(models.Model):
#     """学位课程评价
#     为了以后可以定制单独的评价内容,所以不与普通课程的评价混在一起,单独建表
#     """
#     enrolled_course = models.ForeignKey("EnrolledDegreeCourse")
#     course = models.ForeignKey("Course", verbose_name="评价学位模块", blank=True, null=True,
#                                help_text="不填写即代表评价整个学位课程", limit_choices_to={'course_type': 2})
#     about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰")
#     about_video = models.FloatField(default=0, verbose_name="视频质量")
#     about_course = models.FloatField(default=0, verbose_name="课程")
#     review = models.TextField(max_length=1024, verbose_name="评价")
#     disagree_number = models.IntegerField(default=0, verbose_name="踩")
#     agree_number = models.IntegerField(default=0, verbose_name="赞同数")
#     tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签")
#     date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期")
#     is_recommend = models.BooleanField("热评推荐", default=False)
#     hide = models.BooleanField("不在前端页面显示此条评价", default=False)
#
#     def __str__(self):
#         return "%s-%s" % (self.enrolled_course, self.review)
#
#     class Meta:
#         verbose_name_plural = "14. 学位课评价(购买课程后才能评价)"


class PricePolicy(models.Model):
    """价格与有课程效期表"""
    content_type = models.ForeignKey(ContentType)  # 关联course or degree_course
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    # course = models.ForeignKey("Course")
    valid_period_choices = ((1, '1天'), (3, '3天'),
                            (7, '1周'), (14, '2周'),
                            (30, '1个月'),
                            (60, '2个月'),
                            (90, '3个月'),
                            (180, '6个月'), (210, '12个月'),
                            (540, '18个月'), (720, '24个月'),
                            )
    valid_period = models.SmallIntegerField(choices=valid_period_choices)
    price = models.FloatField()

    class Meta:
        unique_together = ("content_type", 'object_id', "valid_period")
        verbose_name_plural = "15. 价格策略"

    def __str__(self):
        return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price)


# ################################### 优惠券相关 #################################

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="等值货币")
    off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True)
    minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段")

    content_type = models.ForeignKey(ContentType, blank=True, null=True)
    object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定")
    content_object = GenericForeignKey('content_type', 'object_id')

    quantity = models.PositiveIntegerField("数量(张)", default=1)
    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 = "31. 优惠券生成记录"

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


class CouponRecord(models.Model):
    """优惠券发放、消费纪录"""
    coupon = models.ForeignKey("Coupon")
    account = models.ForeignKey("Account", verbose_name="拥有者")

    number = models.CharField(max_length=64, unique=True)

    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="关联订单")  # 一个订单可以有多个优惠券
    order_id = models.IntegerField(verbose_name='关联订单ID')


    class Meta:
        verbose_name_plural = "32. 用户优惠券"

    def __str__(self):
        return '%s-%s-%s' % (self.account, self.number, self.status)


class Account(models.Model):
    username = models.CharField("用户名", max_length=64, unique=True)
    email = models.EmailField(
        verbose_name='邮箱',
        max_length=255,
        unique=True,
        blank=True,
        null=True
    )
    password = models.CharField('密码', max_length=128)

    class Meta:
        verbose_name_plural = "33. 用户表"

class UserToken(models.Model):
    user = models.OneToOneField(to='Account')
    token = models.CharField(max_length=36)

    class Meta:
        verbose_name_plural = "34. token表"

使用2个命令,生成表

python manage.py makemigrations
python manage.py migrate

修改views目录下的auth.py,保存token到数据库中

from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from api import models
from api.utils.response import BaseResponse
import uuid

class AuthView(ViewSetMixin,APIView):
    def login(self,request,*args,**kwargs):
        """
        用户登陆认证
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        response = BaseResponse()  # 默认状态
        try:
            user = request.data.get('username')
            pwd = request.data.get('password')
            # 验证用户和密码
            obj = models.Account.objects.filter(username=user,password=pwd).first()
            if not obj:
                response.code = 10002
                response.error = '用户名或密码错误'
            else:
                uid = str(uuid.uuid4())  # 生成唯一id
                # 保存到数据库中,update_or_create表示更新或者创建
                # user=obj,这个是判断条件。当条件成立,更新token字段,值为uid
                # 当条件不成立时,增加一条记录。注意:增加时,有2个字段,分别是user和token
                models.UserToken.objects.update_or_create(user=obj, defaults={'token': uid})
                response.code = 99999
                response.data = uid

        except Exception as e:
            response.code = 10005
            response.error = '操作异常'

        return Response(response.dict)

再次发送POST请求,输入正确的用户名和密码

Day104 DRF用户认证,结算中心,django-redis - 图11

查看表api_usertoken,发现和返回结果是一样的!

Day104 DRF用户认证,结算中心,django-redis - 图12

再发送一次

Day104 DRF用户认证,结算中心,django-redis - 图13

表的数据随之更新

Day104 DRF用户认证,结算中心,django-redis - 图14

增加认证

不光购物车会用到用户认证,结算中心也需要用到认证,还有其他的视图,也同样需要登录才能使用。

所以,这个认证类需要放到utils里面

在utils目录中新建文件auth.py

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

from api import models



class LuffyAuthentication(BaseAuthentication):

    def authenticate(self, request):
        """
        用户认证
        :param request:
        :return:
        """
        # 获取get参数中的token
        token = request.query_params.get('token')
        # 判断token是否在数据库中
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            # 认证失败
            raise AuthenticationFailed({'code':1008,'error':'认证失败'})
        # 认证成功
        # return 必须返回2个参数,请参考源码解析
        # 这里的token_obj.user,表示UserToken表中的user字段
        # token_obj就是UserToken表的一条记录,也就是一个object
        return (token_obj.user,token_obj)

修改views目录下的shoppingcart.py,导入utils下的auth模块

from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.response import Response
from api import models
from api.utils.response import BaseResponse
import json
import redis
from django.conf import settings
from api.utils.auth import LuffyAuthentication

# redis连接
CONN = redis.Redis(host=settings.REDIS_SERVER.get('host'),port=settings.REDIS_SERVER.get('port'))

# print(settings.REDIS_SERVER.get('host'))

SHOPPING_CAR = {}

USER_ID = 1  # 用户id

# SHOPPING_CAR = {
#     1:{
#         2:{
#             'title':'xxxx',
#             'price':1,
#             'price_list':[
#                 {'id':11,},
#                 {'id':22},
#                 {'id':33},
#             ]
#         },
#         3:{},
#         5:{}
#     },
#     2:{},
#     3:{},
# }

class ShoppingCartView(ViewSetMixin,APIView):
    # 开启认证,指定认证类
    authentication_classes = [LuffyAuthentication,]

    def list(self, request, *args, **kwargs):
        """
        查看购物车信息
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        ret = {'code':10000,'data':None,'error':None}
        try:
            # request.user和request.auth是源码返回的
            # 如果自定义认证类返回了一个元组,元组里面有2个值。
            # 它会覆盖上面2个值,request.user和request.auth
            print(request.user)  # 认证类返回的第一个值
            print(request.auth)  # 认证类返回的第二个值
            # 获取token
            print('shopping',request.query_params.get('token'))

            shopping_car_course_list = []

            # pattern = "shopping_car_%s_*" % (USER_ID,)
            pattern = "shopping_car_%s_%s" % (USER_ID,'*',)

            user_key_list = CONN.keys(pattern)
            for key in user_key_list:
                temp = {
                    'id': CONN.hget(key, 'id').decode('utf-8'),
                    'name': CONN.hget(key, 'name').decode('utf-8'),
                    'img':CONN.hget(key, 'img').decode('utf-8'),
                    'default_price_id':CONN.hget(key, 'default_price_id').decode('utf-8'),
                    'price_policy_dict': json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8'))
                }
                shopping_car_course_list.append(temp)

            ret['data'] = shopping_car_course_list
        except Exception as e:
            # print(e)
            ret['code'] = 10005
            ret['error']  = '获取购物车数据失败'

        # print(ret)
        # print(json.dumps(ret))
        return Response(ret)

    def create(self, request, *args, **kwargs):
        """
        加入购物车
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        """
        1. 接受用户选中的课程ID和价格策略ID
        2. 判断合法性
            - 课程是否存在?
            - 价格策略是否合法?
        3. 把商品和价格策略信息放入购物车 SHOPPING_CAR

        注意:用户ID=1
        """
        # 1.接受用户选中的课程ID和价格策略ID
        """
            相关问题:
                a. 如果让你编写一个API程序,你需要先做什么?
                    - 业务需求
                    - 统一数据传输格式
                    - 表结构设计
                    - 程序开发
                b. django restful framework的解析器的parser_classes的作用?
                    根据请求中Content-Type请求头的值,选择指定解析对请求体中的数据进行解析。
                    如:
                        请求头中含有Content-type: application/json 则内部使用的是JSONParser,JSONParser可以自动去请求体request.body中
                        获取请求数据,然后进行 字节转字符串、json.loads反序列化;

                c. 支持多个解析器(一般只是使用JSONParser即可)

        """
        course_id = request.data.get('courseid')
        policy_id = request.data.get('policyid')

        # 2. 判断合法性
        #   - 课程是否存在?
        #   - 价格策略是否合法?

        # 2.1 课程是否存在?
        course = models.Course.objects.filter(id=course_id).first()
        if not course:
            return Response({'code': 10001, 'error': '课程不存在'})

        # 2.2 价格策略是否合法?
        price_policy_queryset = course.price_policy.all()
        price_policy_dict = {}
        for item in price_policy_queryset:
            temp = {
                'id': item.id,
                'price': item.price,
                'valid_period': item.valid_period,
                'valid_period_display': item.get_valid_period_display()
            }
            price_policy_dict[item.id] = temp

        print(price_policy_dict,type(price_policy_dict))
        print(policy_id,type(policy_id))
        if policy_id not in price_policy_dict:
            return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'})

        # 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR
        """
        购物车中要放:
            课程ID
            课程名称
            课程图片
            默认选中的价格策略
            所有价格策略
        {
            shopping_car_1_1:{
                id:课程ID
                name:课程名称
                img:课程图片
                defaut:默认选中的价格策略
                price_list:所有价格策略
            },

        }

        """

        pattern = "shopping_car_%s_%s" % (USER_ID, '*',)
        keys = CONN.keys(pattern)
        if keys and len(keys) >= 1000:
            return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'})

        # key = "shopping_car_%s_%s" %(USER_ID,course_id,)
        key = "shopping_car_%s_%s" % (USER_ID, course_id,)
        print(key,'1111111111')
        CONN.hset(key, 'id', course_id)
        CONN.hset(key, 'name', course.name)
        CONN.hset(key, 'img', course.course_img)
        CONN.hset(key, 'default_price_id', policy_id)
        CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict))

        CONN.expire(key, 60 * 60 * 24)  # 有效期,单位秒。表示一天

        return Response({'code': 10000, 'data': '购买成功'})

    def destroy(self,request,*args,**kwargs):
        """
        删除购物车中的某个课程
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        response = BaseResponse()
        try:
            # courseid = request.GET.get('courseid')
            courseid = request.data.get('courseid')
            print(courseid)
            # key = "shopping_car_%s_%s" % (USER_ID,courseid)
            key = "shopping_car_%s_%s" % (USER_ID, courseid,)

            CONN.delete(key)
            response.data = '删除成功'
        except Exception as e:
            response.code = 10006
            response.error = '删除失败'
        return Response(response.dict)

    def update(self,request,*args,**kwargs):
        """
        修改用户选中的价格策略
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        """
        1. 获取课程ID、要修改的价格策略ID
        2. 校验合法性(去redis中)
        """
        response = BaseResponse()
        try:
            course_id = request.data.get('courseid')
            policy_id = str(request.data.get('policyid')) if request.data.get('policyid') else None

            # key = 'shopping_car_%s_%s' %(USER_ID,course_id,)
            key = "shopping_car_%s_%s" % (USER_ID, course_id,)

            if not CONN.exists(key):
                response.code = 10007
                response.error = '课程不存在'
                return Response(response.dict)

            price_policy_dict = json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8'))
            if policy_id not in price_policy_dict:
                response.code = 10008
                response.error = '价格策略不存在'
                return Response(response.dict)

            CONN.hset(key,'default_price_id',policy_id)
            CONN.expire(key, 20 * 60)  # 有效期20分钟
            response.data = '修改成功'
        except Exception as e:
            response.code = 10009
            response.error = '修改失败'

        return Response(response.dict)

参数说明:

CONN.expire 表示设置有效期,单位是秒。60 60 24,表示一天

使用postman,发送get请求

提示认证失败

Day104 DRF用户认证,结算中心,django-redis - 图15

发送一个错误的token

提示认证失败!注意:这里直接被认证组件拦截了,并没有到达视图

Day104 DRF用户认证,结算中心,django-redis - 图16

发送一个正确的token,从数据库里面copy一下

返回code为10000,表示认证成功!

Day104 DRF用户认证,结算中心,django-redis - 图17

查看Pycharm控制台输出:

Account object
UserToken object
shopping c8aa8609-fb14-43ea-a6cf-96b2c2469b01

上面2个值,就被自定义类覆盖了!

既然得到了用户对象,那么常量USER_ID就可以删除了

修改shoppingcart.py

from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.response import Response
from api import models
from api.utils.response import BaseResponse
import json
import redis
from django.conf import settings
from api.utils.auth import LuffyAuthentication

# redis连接
CONN = redis.Redis(host=settings.REDIS_SERVER.get('host'),port=settings.REDIS_SERVER.get('port'))

class ShoppingCartView(ViewSetMixin,APIView):
    # 开启认证,指定认证类
    authentication_classes = [LuffyAuthentication,]

    def list(self, request, *args, **kwargs):
        """
        查看购物车信息
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        ret = {'code':10000,'data':None,'error':None}
        try:
            # request.user和request.auth是源码返回的
            # 如果自定义认证类返回了一个元组,元组里面有2个值。
            # 它会覆盖上面2个值,request.user和request.auth
            print(request.user)  # 认证类返回的第一个值
            print(request.auth)  # 认证类返回的第二个值
            # 获取token
            print('shopping',request.query_params.get('token'))

            shopping_car_course_list = []

            # pattern = "shopping_car_%s_*" % (request.user.id,)
            pattern = "shopping_car_%s_%s" % (request.user.id,'*',)

            user_key_list = CONN.keys(pattern)
            for key in user_key_list:
                temp = {
                    'id': CONN.hget(key, 'id').decode('utf-8'),
                    'name': CONN.hget(key, 'name').decode('utf-8'),
                    'img':CONN.hget(key, 'img').decode('utf-8'),
                    'default_price_id':CONN.hget(key, 'default_price_id').decode('utf-8'),
                    'price_policy_dict': json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8'))
                }
                shopping_car_course_list.append(temp)

            ret['data'] = shopping_car_course_list
        except Exception as e:
            # print(e)
            ret['code'] = 10005
            ret['error']  = '获取购物车数据失败'

        # print(ret)
        # print(json.dumps(ret))
        return Response(ret)

    def create(self, request, *args, **kwargs):
        """
        加入购物车
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        """
        1. 接受用户选中的课程ID和价格策略ID
        2. 判断合法性
            - 课程是否存在?
            - 价格策略是否合法?
        3. 把商品和价格策略信息放入购物车 SHOPPING_CAR

        注意:用户ID=1
        """
        # 1.接受用户选中的课程ID和价格策略ID
        """
            相关问题:
                a. 如果让你编写一个API程序,你需要先做什么?
                    - 业务需求
                    - 统一数据传输格式
                    - 表结构设计
                    - 程序开发
                b. django restful framework的解析器的parser_classes的作用?
                    根据请求中Content-Type请求头的值,选择指定解析对请求体中的数据进行解析。
                    如:
                        请求头中含有Content-type: application/json 则内部使用的是JSONParser,JSONParser可以自动去请求体request.body中
                        获取请求数据,然后进行 字节转字符串、json.loads反序列化;

                c. 支持多个解析器(一般只是使用JSONParser即可)

        """
        course_id = request.data.get('courseid')
        policy_id = request.data.get('policyid')

        # 2. 判断合法性
        #   - 课程是否存在?
        #   - 价格策略是否合法?

        # 2.1 课程是否存在?
        course = models.Course.objects.filter(id=course_id).first()
        if not course:
            return Response({'code': 10001, 'error': '课程不存在'})

        # 2.2 价格策略是否合法?
        price_policy_queryset = course.price_policy.all()
        price_policy_dict = {}
        for item in price_policy_queryset:
            temp = {
                'id': item.id,
                'price': item.price,
                'valid_period': item.valid_period,
                'valid_period_display': item.get_valid_period_display()
            }
            price_policy_dict[item.id] = temp

        print(price_policy_dict,type(price_policy_dict))
        print(policy_id,type(policy_id))
        if policy_id not in price_policy_dict:
            return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'})

        # 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR
        """
        购物车中要放:
            课程ID
            课程名称
            课程图片
            默认选中的价格策略
            所有价格策略
        {
            shopping_car_1_1:{
                id:课程ID
                name:课程名称
                img:课程图片
                defaut:默认选中的价格策略
                price_list:所有价格策略
            },

        }

        """

        pattern = "shopping_car_%s_%s" % (request.user.id, '*',)
        keys = CONN.keys(pattern)
        if keys and len(keys) >= 1000:
            return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'})

        # key = "shopping_car_%s_%s" %(request.user.id,course_id,)
        key = "shopping_car_%s_%s" % (request.user.id, course_id,)
        print(key,'1111111111')
        CONN.hset(key, 'id', course_id)
        CONN.hset(key, 'name', course.name)
        CONN.hset(key, 'img', course.course_img)
        CONN.hset(key, 'default_price_id', policy_id)
        CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict))

        CONN.expire(key, 60 * 12 * 24)  # 有效期

        return Response({'code': 10000, 'data': '购买成功'})

    def destroy(self,request,*args,**kwargs):
        """
        删除购物车中的某个课程
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        response = BaseResponse()
        try:
            # courseid = request.GET.get('courseid')
            courseid = request.data.get('courseid')
            print(courseid)
            # key = "shopping_car_%s_%s" % (request.user.id,courseid)
            key = "shopping_car_%s_%s" % (request.user.id, courseid,)

            CONN.delete(key)
            response.data = '删除成功'
        except Exception as e:
            response.code = 10006
            response.error = '删除失败'
        return Response(response.dict)

    def update(self,request,*args,**kwargs):
        """
        修改用户选中的价格策略
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        """
        1. 获取课程ID、要修改的价格策略ID
        2. 校验合法性(去redis中)
        """
        response = BaseResponse()
        try:
            course_id = request.data.get('courseid')
            policy_id = str(request.data.get('policyid')) if request.data.get('policyid') else None

            # key = 'shopping_car_%s_%s' %(request.user.id,course_id,)
            key = "shopping_car_%s_%s" % (request.user.id, course_id,)

            if not CONN.exists(key):
                response.code = 10007
                response.error = '课程不存在'
                return Response(response.dict)

            price_policy_dict = json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8'))
            if policy_id not in price_policy_dict:
                response.code = 10008
                response.error = '价格策略不存在'
                return Response(response.dict)

            CONN.hset(key,'default_price_id',policy_id)
            CONN.expire(key, 20 * 60)
            response.data = '修改成功'
        except Exception as e:
            response.code = 10009
            response.error = '修改失败'

        return Response(response.dict)

测试get请求

Day104 DRF用户认证,结算中心,django-redis - 图18

全局配置

假设有100个类,有98个视图要认证。可以加到全局里面,修改settings.py

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS':'rest_framework.versioning.URLPathVersioning',
    'VERSION_PARAM':'version',
    'DEFAULT_VERSION':'v1',
    'ALLOWED_VERSIONS':['v1','v2'],
    'PAGE_SIZE':20,
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'DEFAULT_AUTHENTICATION_CLASSES':['api.utils.auth.LuffyAuthentication',]
}

那么登录和查看课程,是不需要认证的。怎么忽略呢?

修改views目录下的auth.py,定义认证类为空列表,表示不认证!

from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from api import models
from api.utils.response import BaseResponse
import uuid

class AuthView(ViewSetMixin,APIView):
    authentication_classes = []  # 空列表表示不认证

    def login(self,request,*args,**kwargs):
        """
        用户登陆认证
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        response = BaseResponse()  # 默认状态
        try:
            user = request.data.get('username')
            pwd = request.data.get('password')
            # 验证用户和密码
            obj = models.Account.objects.filter(username=user,password=pwd).first()
            if not obj:
                response.code = 10002
                response.error = '用户名或密码错误'
            else:
                uid = str(uuid.uuid4())  # 生成唯一id
                # 保存到数据库中,update_or_create表示更新或者创建
                # user=obj,这个是判断条件。当条件成立,更新token字段,值为uid
                # 当条件不成立时,增加一条记录。注意:增加时,有2个字段,分别是user和token
                models.UserToken.objects.update_or_create(user=obj, defaults={'token': uid})
                response.code = 99999
                response.data = uid

        except Exception as e:
            response.code = 10005
            response.error = '操作异常'

        return Response(response.dict)

使用postman测试登录

Day104 DRF用户认证,结算中心,django-redis - 图19

查看返回结果

Day104 DRF用户认证,结算中心,django-redis - 图20

修改settings.py,注释掉全局认证。因为这里用到的登录认证的视图不多

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS':'rest_framework.versioning.URLPathVersioning',
    'VERSION_PARAM':'version',
    'DEFAULT_VERSION':'v1',
    'ALLOWED_VERSIONS':['v1','v2'],
    'PAGE_SIZE':20,
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    # 'DEFAULT_AUTHENTICATION_CLASSES':['api.utils.auth.LuffyAuthentication',]
}

问题:认证类为什么要继承BaseAuthentication?

查看源码BaseAuthentication

class BaseAuthentication(object):
    """
    All authentication classes should extend BaseAuthentication.
    """

    def authenticate(self, request):
        """
        Authenticate the request and return a two-tuple of (user, token).
        """
        raise NotImplementedError(".authenticate() must be overridden.")

    def authenticate_header(self, request):
        """
        Return a string to be used as the value of the `WWW-Authenticate`
        header in a `401 Unauthenticated` response, or `None` if the
        authentication scheme should return `403 Permission Denied` responses.
        """
        pass

发现,只要执行了authenticate方法,它会执行raise。它会主动报错

为了不让它报错,子类继承BaseAuthentication后,必须重写authenticate方法,才不会报错。

这样做的目的,是为了约束子类,哪些方法,必须要定义!

二、结算中心

Day104 DRF用户认证,结算中心,django-redis - 图21

点击去结算,会发送一次post请求。那么它该发送什么数据呢?

只需要发送课程id就可以了?为什么呢?

因为redis中有购物车相关数据!后台根据课程id去购物车中获取,要结算的课程就可以了!

结算中心和购物车一样,也是一个临时数据。它也需要放到redis中!

先来看购物车的数据结构

购物车 = {
    'shopping_car_1_3':{
        name:'',
        src:'xx'
        price_id:1,
        price_dict = {
            1:....
        }
    },
    'shopping_car_1_1':{
        ...
    },
    'shopping_car_1_5':{
        ...
    },

}

再来看结算中新的数据结构

结算中心 = {
    'payment_1_3':{
        id:3,
        mame:Django框架学习,
        price_id:1,
        price_priod:30,
        price:199,
        defaul_coupon_id:0,
        coupon_dict: {                ----> 绑定了课程3的优惠券
            0: '请选择课程优惠券',
            1:'xxx',
            2:'xxx',
            3:'xxx',
            4:'xxx',
        }
    },
    'payment_1_1':{
        id:1,
        mame:Django框架学习,
        price_id:1,
        price_priod:30,
        price:199,
        defaul_coupon_id:0,
        coupon_dict: {                ----> 绑定了课程1的优惠券
            0: '请选择课程优惠券',
            1:'xxx',
            2:'xxx',
            3:'xxx',
            4:'xxx',
        }
    },
}

优惠券

优惠券分为2大类:绑定课程和非绑定课程

点击去结算

Day104 DRF用户认证,结算中心,django-redis - 图22

在左下角,展示的是非绑定课程的优惠券。

在右边的下拉菜单中,展示的是绑定课程的优惠券

在views目录下,创建文件payment.py

import json
import redis
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.response import Response
from api.utils.auth import LuffyAuthentication
from api import models
from api.utils.response import BaseResponse

# redis连接
CONN = redis.Redis(host=settings.REDIS_SERVER.get('host'),port=settings.REDIS_SERVER.get('port'))


class PaymentView(ViewSetMixin, APIView):
    authentication_classes = [LuffyAuthentication, ]

    def create(self, request, *args, **kwargs):
        """
        在结算中添加课程
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        # 1.接受用户选择的要结算的课程ID列表

        # 2.清空当前用户request.user.id结算中心的数据
        #   key = payment_1*

        # 3.循环要加入结算中的所有课程ID列表

        """
        for course_id in 用户提交课程ID列表:
            3.1 根据course_id,request.user.id去购物车中获取商品信息:商品名称、图片、价格(id,周期,显示周期,价格)
            3.2 根据course_id,request.user.id获取 
                    - 当前用户
                    - 当前课程
                    - 可用的优惠券

            加入结算中心

            提示:可以使用contenttypes
        """

        # 4.获取当前用户所有未绑定课程优惠券
        #       - 未使用
        #       - 有效期内
        #       - 加入结算中心:glocal_coupon_用户ID

    def list(self, request, *args, **kwargs):
        """
        查看结算中心
        :param request:
        :param args:
        :param kwargs:
        :return:
        """

        # 1. 根据用户ID去结算中心获取该用户所有要结算课程

        # 2. 根据用户ID去结算中心获取该用户所有可用未绑定课程的优惠券

        # 3. 用户表中获取贝里余额

        # 4. 以上数据构造成一个字典
        return Response('...')

    def update(self, request, *args, **kwargs):
        """
        更新优惠券
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        # 1. 获取用户提交:
        #       course_id=1,coupon_id=3
        #       course_id=0,coupon_id=6

        # 2. course_id=1 --> 去结算中心获取当前用户所拥有的绑定当前课程优惠,并进行校验
        #       - 成功:defaul_coupon_id=3
        #       - 否则:非法请求

        # 2. course_id=0 --> 去结算中心获取当前用户所拥有的未绑定课程优惠,并进行校验
        #       - 成功:defaul_coupon_id=3
        #       - 否则:非法请求

course_id为空,表示 未绑定课程,否则为绑定课程

这里面展示的是一些业务逻辑,需要自己用代码来填充

提示你的代码编写能力!

三、django-redis

介绍

django-redis 基于 BSD 许可, 是一个使 Django 支持 Redis cache/session 后端的全功能组件

django-redis 中文文档,请参考

http://django-redis-chs.readthedocs.io/zh_CN/latest/

为何要用 django-redis ?

因为:

  • 持续更新
  • 本地化的 redis-py URL 符号连接字符串
  • 可扩展客户端
  • 可扩展解析器
  • 可扩展序列器
  • 默认客户端主/从支持
  • 完善的测试
  • 已在一些项目的生产环境中作为 cache 和 session 使用
  • 支持永不超时设置
  • 原生进入 redis 客户端/连接池支持
  • 高可配置 ( 例如仿真缓存的异常行为 )
  • 默认支持 unix 套接字
  • 支持 Python 2.7, 3.4, 3.5 以及 3.6

安装

安装 django-redis 最简单的方法就是用 pip :

pip install django-redis

作为 cache backend 使用配置

为了使用 django-redis , 你应该将你的 django cache setting 改成这样:

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

举例:

在上面购物车中,使用了缓存。结算中心也需要使用缓存,那么就可以定义一个全局配置。当需要使用时,导入一下配置即可!

修改settings.py,最后一行添加

# ######django-redis的配置 #################
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://192.168.218.140:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
            # "PASSWORD": "密码",
        }
    }
}

参数解释:

BACKEND 表示后台连接

OPTIONS 表示参数

CONNECTION_POOL_KWARGS 表示连接池。max_connections表示最大连接数

连接池,请参考链接:

https://baike.baidu.com/item/%E8%BF%9E%E6%8E%A5%E6%B1%A0%E6%8A%80%E6%9C%AF/523659?fr=aladdin

上面定义了100个连接池,假设100进程,都在使用连接池。当地101个访问时,会等待。直到有空闲的进程时,才处理!

不过redis的处理是很快的,很少会出现等待的情况!

使用连接池,有很多优点:

1.减少连接创建时间

2.简化的编程模式

3.受控的资源使用

使用连接池,性能会更高好!

视图中使用

加上2行代码,就可以了

from django_redis import get_redis_connection
CONN = get_redis_connection("default")

这里的default指的是settings.py中CACHES配置项的default

修改views目录下的shoppingcar.py

from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.response import Response
from api import models
from api.utils.response import BaseResponse
import json
from api.utils.auth import LuffyAuthentication
from django_redis import get_redis_connection

CONN = get_redis_connection("default")  # 使用redis连接池

class ShoppingCartView(ViewSetMixin,APIView):
    # 开启认证,指定认证类
    authentication_classes = [LuffyAuthentication,]

    def list(self, request, *args, **kwargs):
        """
        查看购物车信息
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        ret = {'code':10000,'data':None,'error':None}
        try:
            # request.user和request.auth是源码返回的
            # 如果自定义认证类返回了一个元组,元组里面有2个值。
            # 它会覆盖上面2个值,request.user和request.auth
            print(request.user)  # 认证类返回的第一个值
            print(request.auth)  # 认证类返回的第二个值
            # 获取token
            print('shopping',request.query_params.get('token'))

            shopping_car_course_list = []

            # pattern = "shopping_car_%s_*" % (request.user.id,)
            pattern = "shopping_car_%s_%s" % (request.user.id,'*',)

            user_key_list = CONN.keys(pattern)
            for key in user_key_list:
                temp = {
                    'id': CONN.hget(key, 'id').decode('utf-8'),
                    'name': CONN.hget(key, 'name').decode('utf-8'),
                    'img':CONN.hget(key, 'img').decode('utf-8'),
                    'default_price_id':CONN.hget(key, 'default_price_id').decode('utf-8'),
                    'price_policy_dict': json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8'))
                }
                shopping_car_course_list.append(temp)

            ret['data'] = shopping_car_course_list
        except Exception as e:
            # print(e)
            ret['code'] = 10005
            ret['error']  = '获取购物车数据失败'

        # print(ret)
        # print(json.dumps(ret))
        return Response(ret)

    def create(self, request, *args, **kwargs):
        """
        加入购物车
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        """
        1. 接受用户选中的课程ID和价格策略ID
        2. 判断合法性
            - 课程是否存在?
            - 价格策略是否合法?
        3. 把商品和价格策略信息放入购物车 SHOPPING_CAR

        注意:用户ID=1
        """
        # 1.接受用户选中的课程ID和价格策略ID
        """
            相关问题:
                a. 如果让你编写一个API程序,你需要先做什么?
                    - 业务需求
                    - 统一数据传输格式
                    - 表结构设计
                    - 程序开发
                b. django restful framework的解析器的parser_classes的作用?
                    根据请求中Content-Type请求头的值,选择指定解析对请求体中的数据进行解析。
                    如:
                        请求头中含有Content-type: application/json 则内部使用的是JSONParser,JSONParser可以自动去请求体request.body中
                        获取请求数据,然后进行 字节转字符串、json.loads反序列化;

                c. 支持多个解析器(一般只是使用JSONParser即可)

        """
        course_id = request.data.get('courseid')
        policy_id = request.data.get('policyid')

        # 2. 判断合法性
        #   - 课程是否存在?
        #   - 价格策略是否合法?

        # 2.1 课程是否存在?
        course = models.Course.objects.filter(id=course_id).first()
        if not course:
            return Response({'code': 10001, 'error': '课程不存在'})

        # 2.2 价格策略是否合法?
        price_policy_queryset = course.price_policy.all()
        price_policy_dict = {}
        for item in price_policy_queryset:
            temp = {
                'id': item.id,
                'price': item.price,
                'valid_period': item.valid_period,
                'valid_period_display': item.get_valid_period_display()
            }
            price_policy_dict[item.id] = temp

        print(price_policy_dict,type(price_policy_dict))
        print(policy_id,type(policy_id))
        if policy_id not in price_policy_dict:
            return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'})

        # 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR
        """
        购物车中要放:
            课程ID
            课程名称
            课程图片
            默认选中的价格策略
            所有价格策略
        {
            shopping_car_1_1:{
                id:课程ID
                name:课程名称
                img:课程图片
                defaut:默认选中的价格策略
                price_list:所有价格策略
            },

        }

        """

        pattern = "shopping_car_%s_%s" % (request.user.id, '*',)
        keys = CONN.keys(pattern)
        if keys and len(keys) >= 1000:
            return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'})

        # key = "shopping_car_%s_%s" %(request.user.id,course_id,)
        key = "shopping_car_%s_%s" % (request.user.id, course_id,)
        print(key,'1111111111')
        CONN.hset(key, 'id', course_id)
        CONN.hset(key, 'name', course.name)
        CONN.hset(key, 'img', course.course_img)
        CONN.hset(key, 'default_price_id', policy_id)
        CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict))

        CONN.expire(key, 60 * 12 * 24)  # 有效期

        return Response({'code': 10000, 'data': '购买成功'})

    def destroy(self,request,*args,**kwargs):
        """
        删除购物车中的某个课程
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        response = BaseResponse()
        try:
            # courseid = request.GET.get('courseid')
            courseid = request.data.get('courseid')
            print(courseid)
            # key = "shopping_car_%s_%s" % (request.user.id,courseid)
            key = "shopping_car_%s_%s" % (request.user.id, courseid,)

            CONN.delete(key)
            response.data = '删除成功'
        except Exception as e:
            response.code = 10006
            response.error = '删除失败'
        return Response(response.dict)

    def update(self,request,*args,**kwargs):
        """
        修改用户选中的价格策略
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        """
        1. 获取课程ID、要修改的价格策略ID
        2. 校验合法性(去redis中)
        """
        response = BaseResponse()
        try:
            course_id = request.data.get('courseid')
            policy_id = str(request.data.get('policyid')) if request.data.get('policyid') else None

            # key = 'shopping_car_%s_%s' %(request.user.id,course_id,)
            key = "shopping_car_%s_%s" % (request.user.id, course_id,)

            if not CONN.exists(key):
                response.code = 10007
                response.error = '课程不存在'
                return Response(response.dict)

            price_policy_dict = json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8'))
            if policy_id not in price_policy_dict:
                response.code = 10008
                response.error = '价格策略不存在'
                return Response(response.dict)

            CONN.hset(key,'default_price_id',policy_id)
            CONN.expire(key, 20 * 60)
            response.data = '修改成功'
        except Exception as e:
            response.code = 10009
            response.error = '修改失败'

        return Response(response.dict)

使用postman测试访问,要带上正确的token

Day104 DRF用户认证,结算中心,django-redis - 图23

访问正常

作为 session backend 使用配置

Django 默认可以使用任何 cache backend 作为 session backend, 将 django-redis 作为 session 储存后端不用安装任何额外的 backend

SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

举例:

修改settings.py

# ######django-redis的配置 #################
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://192.168.218.140:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
            # "PASSWORD": "密码",
        }
    }
}

###使用redis缓存session
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'  # 引擎
SESSION_CACHE_ALIAS = 'default'  # 使用的缓存别名(默认内存缓存,也可以是memcache),此处别名依赖缓存的设置

SESSION_COOKIE_NAME = "sessionid"  # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串
SESSION_COOKIE_PATH = "/"  # Session的cookie保存的路径
SESSION_COOKIE_DOMAIN = None  # Session的cookie保存的域名
SESSION_COOKIE_SECURE = False  # 是否Https传输cookie
SESSION_COOKIE_HTTPONLY = True  # 是否Session的cookie只支持http传输
SESSION_COOKIE_AGE = 1209600  # Session的cookie失效日期(2周)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False  # 是否关闭浏览器使得Session过期
SESSION_SAVE_EVERY_REQUEST = False  # 是否每次请求都保存Session,默认修改之后才保存

简单来讲,加上2行就可以了。下面的那些配置,是参考源码设置的。

比如session失效时间是2周

如果需要修改,在这里指定一下,就可以了!

注意:里面的defalut就是redis配置的defalut,名字是一一对应的!

总结:

1. django-redis的作用
    - 连接redis并在redis中进行操作(含redis连接池)。

2. 帮助用户将session放到redis
    - django-redis的配置
    - session的配置

作业:

完整结算中心的代码,实现以下功能:

  1. 添加

  2. 查看

  3. 修改

注意:使用认证+django-redis

修改utils目录下的auth.py

当为GET请求时,从url中取token,否则从请求体中获取token

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from api import models

class LuffyAuthentication(BaseAuthentication):

    def authenticate(self, request):
        """
        用户认证
        :param request:
        :return:
        """
        # print(request.method)
        # 判断请求方式
        if request.method == "GET":
            token = request.query_params.get('token')
        else:
            token = request.data.get('token')

        # print('auth',token)
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            # 认证失败
            raise AuthenticationFailed({'code':1008,'error':'认证失败'})
        # 认证成功
        # return (token_obj.user,token_obj)
        return (token_obj.user,token_obj)

修改models.py,在用户表增加字段

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.db import models
import hashlib


# ######################## 课程相关 ########################

class CourseCategory(models.Model):
    """课程大类, e.g 前端  后端..."""
    name = models.CharField(max_length=64, unique=True)

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

    class Meta:
        verbose_name_plural = "01.课程大类"


class CourseSubCategory(models.Model):
    """课程子类, e.g python linux """
    category = models.ForeignKey("CourseCategory")
    name = models.CharField(max_length=64, unique=True)

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

    class Meta:
        verbose_name_plural = "02.课程子类"


class DegreeCourse(models.Model):
    """学位课程"""
    name = models.CharField(max_length=128, unique=True)
    course_img = models.CharField(max_length=255, verbose_name="缩略图")
    brief = models.TextField(verbose_name="学位课程简介", )
    total_scholarship = models.PositiveIntegerField(verbose_name="总奖学金(贝里)", default=40000)  # 2000 2000
    mentor_compensation_bonus = models.PositiveIntegerField(verbose_name="本课程的导师辅导费用(贝里)", default=15000)
    period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=150)  # 为了计算学位奖学金
    prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024)
    teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师")

    # 用于GenericForeignKey反向查询, 不会生成表字段,切勿删除
    # coupon = GenericRelation("Coupon")

    # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除
    degreecourse_price_policy = GenericRelation("PricePolicy")

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "03.学位课"


class Teacher(models.Model):
    """讲师、导师表"""
    name = models.CharField(max_length=32)
    role_choices = ((0, '讲师'), (1, '导师'))
    role = models.SmallIntegerField(choices=role_choices, default=0)
    title = models.CharField(max_length=64, verbose_name="职位、职称")
    signature = models.CharField(max_length=255, help_text="导师签名", blank=True, null=True)
    image = models.CharField(max_length=128)
    brief = models.TextField(max_length=1024)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "04.导师或讲师"


class Scholarship(models.Model):
    """学位课程奖学金"""
    degree_course = models.ForeignKey("DegreeCourse")
    time_percent = models.PositiveSmallIntegerField(verbose_name="奖励档位(时间百分比)", help_text="只填百分值,如80,代表80%")
    value = models.PositiveIntegerField(verbose_name="奖学金数额")

    def __str__(self):
        return "%s:%s" % (self.degree_course, self.value)

    class Meta:
        verbose_name_plural = "05.学位课奖学金"


class Course(models.Model):
    """专题课/学位课模块表"""
    name = models.CharField(max_length=128, unique=True)
    course_img = models.CharField(max_length=255)
    sub_category = models.ForeignKey("CourseSubCategory")
    course_type_choices = ((0, '付费'), (1, 'VIP专享'), (2, '学位课程'))
    course_type = models.SmallIntegerField(choices=course_type_choices)

    # 不为空;学位课的某个模块
    # 为空;专题课
    degree_course = models.ForeignKey("DegreeCourse", blank=True, null=True, help_text="若是学位课程,此处关联学位表")

    brief = models.TextField(verbose_name="课程概述", max_length=2048)
    level_choices = ((0, '初级'), (1, '中级'), (2, '高级'))
    level = models.SmallIntegerField(choices=level_choices, default=1)
    pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True)
    period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=7)  #
    order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排")
    attachment_path = models.CharField(max_length=128, verbose_name="课件路径", blank=True, null=True)
    status_choices = ((0, '上线'), (1, '下线'), (2, '预上线'))
    status = models.SmallIntegerField(choices=status_choices, default=0)
    template_id = models.SmallIntegerField("前端模板id", default=1)

    coupon = GenericRelation("Coupon")

    # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除
    price_policy = GenericRelation("PricePolicy")

    asked_question = GenericRelation("OftenAskedQuestion")


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


    def save(self, *args, **kwargs):
        if self.course_type == 2:
            if not self.degree_course:
                raise ValueError("学位课程必须关联对应的学位表")
        super(Course, self).save(*args, **kwargs)


    class Meta:
        verbose_name_plural = "06.专题课或学位课模块"


class CourseDetail(models.Model):
    """课程详情页内容"""
    course = models.OneToOneField("Course")
    hours = models.IntegerField("课时")
    course_slogan = models.CharField(max_length=125, blank=True, null=True)
    video_brief_link = models.CharField(verbose_name='课程介绍', max_length=255, blank=True, null=True)
    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 "%s" % self.course

    class Meta:
        verbose_name_plural = "07.课程或学位模块详细"


class OftenAskedQuestion(models.Model):
    """常见问题"""
    content_type = models.ForeignKey(ContentType)  # 关联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:
        unique_together = ('content_type', 'object_id', 'question')
        verbose_name_plural = "08. 常见问题"


class CourseOutline(models.Model):
    """课程大纲"""
    course_detail = models.ForeignKey("CourseDetail")
    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:
        unique_together = ('course_detail', 'title')
        verbose_name_plural = "09. 课程大纲"


class CourseChapter(models.Model):
    """课程章节"""
    course = models.ForeignKey("Course")
    chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
    name = models.CharField(max_length=128)
    summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)

    class Meta:
        unique_together = ("course", 'chapter')
        verbose_name_plural = "10. 课程章节"

    def __str__(self):
        return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)


class CourseSection(models.Model):
    """课时目录"""
    chapter = models.ForeignKey("CourseChapter")
    name = models.CharField(max_length=128)
    order = models.PositiveSmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时")
    section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频'))
    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")
    video_time = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
    pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
    free_trail = models.BooleanField("是否可试看", default=False)

    class Meta:
        unique_together = ('chapter', 'section_link')
        verbose_name_plural = "11. 课时"

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


class Homework(models.Model):
    chapter = models.ForeignKey("CourseChapter")
    title = models.CharField(max_length=128, verbose_name="作业题目")
    order = models.PositiveSmallIntegerField("作业顺序", help_text="同一课程的每个作业之前的order值间隔1-2个数")
    homework_type_choices = ((0, '作业'), (1, '模块通关考核'))
    homework_type = models.SmallIntegerField(choices=homework_type_choices, default=0)
    requirement = models.TextField(max_length=1024, verbose_name="作业需求")
    threshold = models.TextField(max_length=1024, verbose_name="踩分点")
    recommend_period = models.PositiveSmallIntegerField("推荐完成周期(天)", default=7)
    scholarship_value = models.PositiveSmallIntegerField("为该作业分配的奖学金(贝里)")
    note = models.TextField(blank=True, null=True)
    enabled = models.BooleanField(default=True, help_text="本作业如果后期不需要了,不想让学员看到,可以设置为False")

    class Meta:
        unique_together = ("chapter", "title")
        verbose_name_plural = "12. 章节作业"

    def __str__(self):
        return "%s - %s" % (self.chapter, self.title)


# class CourseReview(models.Model):
#     """课程评价"""
#     enrolled_course = models.OneToOneField("EnrolledCourse")
#     about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰")
#     about_video = models.FloatField(default=0, verbose_name="内容实用")
#     about_course = models.FloatField(default=0, verbose_name="课程内容通俗易懂")
#     review = models.TextField(max_length=1024, verbose_name="评价")
#     disagree_number = models.IntegerField(default=0, verbose_name="踩")
#     agree_number = models.IntegerField(default=0, verbose_name="赞同数")
#     tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签")
#     date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期")
#     is_recommend = models.BooleanField("热评推荐", default=False)
#     hide = models.BooleanField("不在前端页面显示此条评价", default=False)
#
#     def __str__(self):
#         return "%s-%s" % (self.enrolled_course.course, self.review)
#
#     class Meta:
#         verbose_name_plural = "13. 课程评价(购买课程后才能评价)"
#
#
# class DegreeCourseReview(models.Model):
#     """学位课程评价
#     为了以后可以定制单独的评价内容,所以不与普通课程的评价混在一起,单独建表
#     """
#     enrolled_course = models.ForeignKey("EnrolledDegreeCourse")
#     course = models.ForeignKey("Course", verbose_name="评价学位模块", blank=True, null=True,
#                                help_text="不填写即代表评价整个学位课程", limit_choices_to={'course_type': 2})
#     about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰")
#     about_video = models.FloatField(default=0, verbose_name="视频质量")
#     about_course = models.FloatField(default=0, verbose_name="课程")
#     review = models.TextField(max_length=1024, verbose_name="评价")
#     disagree_number = models.IntegerField(default=0, verbose_name="踩")
#     agree_number = models.IntegerField(default=0, verbose_name="赞同数")
#     tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签")
#     date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期")
#     is_recommend = models.BooleanField("热评推荐", default=False)
#     hide = models.BooleanField("不在前端页面显示此条评价", default=False)
#
#     def __str__(self):
#         return "%s-%s" % (self.enrolled_course, self.review)
#
#     class Meta:
#         verbose_name_plural = "14. 学位课评价(购买课程后才能评价)"


class PricePolicy(models.Model):
    """价格与有课程效期表"""
    content_type = models.ForeignKey(ContentType)  # 关联course or degree_course
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    # course = models.ForeignKey("Course")
    valid_period_choices = ((1, '1天'), (3, '3天'),
                            (7, '1周'), (14, '2周'),
                            (30, '1个月'),
                            (60, '2个月'),
                            (90, '3个月'),
                            (180, '6个月'), (210, '12个月'),
                            (540, '18个月'), (720, '24个月'),
                            )
    valid_period = models.SmallIntegerField(choices=valid_period_choices)
    price = models.FloatField()

    class Meta:
        unique_together = ("content_type", 'object_id', "valid_period")
        verbose_name_plural = "15. 价格策略"

    def __str__(self):
        return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price)


# ################################### 优惠券相关 #################################

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="等值货币")
    off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True)
    minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段")

    content_type = models.ForeignKey(ContentType, blank=True, null=True)
    object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定")
    content_object = GenericForeignKey('content_type', 'object_id')

    quantity = models.PositiveIntegerField("数量(张)", default=1)
    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 = "31. 优惠券生成记录"

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


class CouponRecord(models.Model):
    """优惠券发放、消费纪录"""
    coupon = models.ForeignKey("Coupon")
    account = models.ForeignKey("Account", verbose_name="拥有者")

    number = models.CharField(max_length=64, unique=True)

    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="关联订单")  # 一个订单可以有多个优惠券
    order_id = models.IntegerField(verbose_name='关联订单ID')


    class Meta:
        verbose_name_plural = "32. 用户优惠券"

    def __str__(self):
        return '%s-%s-%s' % (self.account, self.number, self.status)


class Account(models.Model):
    username = models.CharField("用户名", max_length=64, unique=True)
    email = models.EmailField(
        verbose_name='邮箱',
        max_length=255,
        unique=True,
        blank=True,
        null=True
    )
    password = models.CharField('密码', max_length=128)
    balance = models.FloatField('贝里',default=0)

    class Meta:
        verbose_name_plural = "33. 用户表"

class UserToken(models.Model):
    user = models.OneToOneField(to='Account')
    token = models.CharField(max_length=36)

    class Meta:
        verbose_name_plural = "34. token表"

执行2个命令,生成字段

python manage.py makemigrations
python manage.py migrate

为用户加点钱

Day104 DRF用户认证,结算中心,django-redis - 图24

修改admin.py,注册所有表

from django.contrib import admin

# Register your models here.
from api import models
admin.site.register(models.CourseCategory)
admin.site.register(models.CourseSubCategory)
admin.site.register(models.DegreeCourse)
admin.site.register(models.Teacher)
admin.site.register(models.Scholarship)
admin.site.register(models.Course)
admin.site.register(models.CourseDetail)
admin.site.register(models.OftenAskedQuestion)
admin.site.register(models.CourseOutline)
admin.site.register(models.CourseChapter)
admin.site.register(models.CourseSection)
admin.site.register(models.Homework)
admin.site.register(models.PricePolicy)
admin.site.register(models.Coupon)
admin.site.register(models.CouponRecord)
admin.site.register(models.Account)

进入admin后台,添加几条优惠券,并绑定用户

Day104 DRF用户认证,结算中心,django-redis - 图25

list

修改payment.py,先做get请求的

import json
import redis
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.response import Response
from api.utils.auth import LuffyAuthentication
from api import models
from api.utils.response import BaseResponse

from django_redis import get_redis_connection

CONN = get_redis_connection("default")  # 使用redis连接池


class PaymentView(ViewSetMixin, APIView):
    authentication_classes = [LuffyAuthentication, ]

    def create(self, request, *args, **kwargs):
        """
        在结算中添加课程
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        # 1.接收用户选择的要结算的课程ID列表


        # 2.清空当前用户request.user.id结算中心的数据
        #   key = payment_1*

        # 3.循环要加入结算中的所有课程ID列表

        """
        for course_id in 用户提交课程ID列表:
            3.1 根据course_id,request.user.id去购物车中获取商品信息:商品名称、图片、价格(id,周期,显示周期,价格)
            3.2 根据course_id,request.user.id获取 
                    - 当前用户
                    - 当前课程
                    - 可用的优惠券

            加入结算中心

            提示:可以使用contenttypes
        """

        # 4.获取当前用户所有未绑定课程优惠券
        #       - 未使用
        #       - 有效期内
        #       - 加入结算中心:glocal_coupon_用户ID

    def list(self, request, *args, **kwargs):
        """
        查看结算中心
        :param request:
        :param args:
        :param kwargs:
        :return:
        """

        # 1. 根据用户ID去结算中心获取该用户所有要结算课程
        course_id = request.query_params.get('course_id')
        print('课程id',course_id)
        obj = models.Course.objects.filter(id=course_id).first()
        print('结算课程',obj.name)
        # 2. 根据用户ID去结算中心获取该用户所有可用未绑定课程的优惠券
        user_id =request.user.id
        print('用户id', user_id)
        obj2 = models.CouponRecord.objects.filter(account=user_id, coupon__object_id__isnull=True).first()
        # print(obj2.coupon.get_coupon_type_display())

        if obj2.coupon.coupon_type == 0:
            print('{}{}'.format(obj2.coupon.get_coupon_type_display(),obj2.coupon.money_equivalent_value))
        elif obj2.coupon.coupon_type == 1:
            print('满{}减{}'.format(obj2.coupon.minimum_consume,obj2.coupon.money_equivalent_value))
        else:
            print(obj2.coupon.id)
            print('{}折'.format(obj2.coupon.off_percent))

        # 3. 用户表中获取贝里余额
        beili = models.Account.objects.filter(id=user_id).first()
        print('用户贝里',beili.balance)

        # 4. 以上数据构造成一个字典

        return Response('...')

    def update(self, request, *args, **kwargs):
        """
        更新优惠券
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        # 1. 获取用户提交:
        #       course_id=1,coupon_id=3
        #       course_id=0,coupon_id=6

        # 2. course_id=1 --> 去结算中心获取当前用户所拥有的绑定当前课程优惠,并进行校验
        #       - 成功:defaul_coupon_id=3
        #       - 否则:非法请求

        # 3. course_id=0 --> 去结算中心获取当前用户所拥有的未绑定课程优惠,并进行校验
        #       - 成功:defaul_coupon_id=3
        #       - 否则:非法请求

使用postman发送GET请求

Day104 DRF用户认证,结算中心,django-redis - 图26

查看Pycharm控制台输出

课程id 1
结算课程 Python开发入门7天特训营
用户id 1
立减10
用户贝里 100.0