路飞项目----day11(执行异步任务,延迟任务,定时任务, django中使用celery, 轮播图接口加缓存, 双写一致性, 首页轮播图定时更新,课程前端页面,课程功能表分析)

上节回顾

# 1 redis 是什么---》非关系型数据库,数据存在内存中,速度快,5大数据类型,主要用来做缓存

# 2 安装  图形化客户端RESP

# 3 python 操作redis
	-两种连接方式
	-连接池的链接, 池对象是单例的

# 4 字符串,列表,hash     -----集合   有序集合

# 5 字符串  get  set

# 6 hash操作------key与vuale这种形式-----底层基于数组(连续存储的内存空间)
# 根据key值使用hash函数运算,得到一个地址,把value值存到数组对应的地址中
# 由于hash函数得到的数字不是一个有序的,所以字典是无序
# 字典的键必须可哈希,不可变类型的数据都可哈希,可变类型数据不可哈希!!!
# hset  hget  hmget  hlen


# 7 列表  lpush  lpop  llen  lrange


# 8 通用操作  conn.expire(name ,time)   conn.delete(names)
# conn.move('hobby111',8)


# 9 管道
	pipline---多条命令先放到管道中不执行 ,执行execute,
	一次性执行管道中的所有命令,保证要么都成功,要么都失败
	所以才保证事务


# 10 django中使用
-1 自己写
	-写个py文件  POOL=ConnectionPool(xxxx)     # 拿到连接池
	-以后在用的地方,导入POOL,从池中拿一个连接用,即可
	conn = redis.Redis(connection_pool=POOL)


-2 django-redis
	-配置文件配置
		-django缓存  cache.set  cache.set
		# 可以缓存任意的python数据类型


-3 导入conn=get_redis_connection()
	conn.set('name','lqz')
--------------------------------------------

# 11 celery  分布式异步任务框架   分布式就是可以并发运行在不同的电脑上
	-框架,能够做 异步任务,定时任务,延迟任务
	-独立的,架构
		-消息中间件:存储任务的地方,借助于第三方 redis
		-任务执行单元:worker 真正执行任务的进程,执行消息中间件中的任务(函数)
		-结果存储:任务执行的的返回值,放到里面  redis

	-快速使用
        app=Celery(broker=,backend=, include=[])

        @app.task
        def add():
            pass

       add.delay()   # 提交异步任务

        执行worker命令
        把结果存到redis
        查看结果

     -包结构
    	-clelery_task
        	-__init__
        	-celery.py    include
            -task1.py
            -task2.py

     -提交任务
     -查看任务执行结果


------------------------------------

# 补充 字符编码重点梳理
1 ascii  英文字母大小写,数字,标点符号
        -127 够了   1个字节  8个比特位[每个比特位只能放01]  28次方中变化 256种变化
        -ab!  00001001  000010010 000010010

----------------------
2 中国,表示中文
    	GBK编码:用两个字节 表示一个字符
        216次方:65536

----------------------
3 韩国,表示韩文
    	998998      한

----------------------
4 unicode编码  把世界上所有的象形文字都有个数字对应   4个字节表示一个字符
    	99828893    한

----------------------
5 unicode编码存储---》硬盘上---》ab文한   4个字节 4个字节 4个字节 4个字节
    	-utf-8  unicode编码 编码,可变长

    	存的时候,编码的时候直接将文字编成对应的二进制数字
        a   一个字节
        b   一个字节
        文   两个字节
        한   三个字节

    	解的时候
        1个字节     两个字节  三个字节
        如何知道是按一个字节还是两个字节,还是三个字节解码?
        如果是一个字节8个比特位,第一个位置固定是0,后7位才是正真存数据的
        如果是两个字节16个比特位,开头位置固定是110,
        三个字节开头位置固定是1110     四个字节开头位置固定是11110
        所以只要在解码的时候,先根据开头的数字,断出要解码字符是用几个字节来解的
        然后再根据后面对应数字去解码

--------------------------------------------

.
.
.
.
.
.
.

今日内容

1 celery 执行异步任务,延迟任务,循环定时任务

# 提交异步任务
	任务函数.delay(函数要传的参数)     # 右键运行


# 提交延迟任务
	任务函数.apply_async(args=[参数],eta=时间对象)      # 右键运行
	# 如果没有修改时区,需要使用utc时间


# 循环定时任务
	-需要启动beat和启动worker
		beat    定时提交任务的进程---》配置在app.conf.beat_schedule的任务
		worker  执行任务的

# beat才是真正按规定的时间建个提交任务的进程
# worker只是用来执行任务的进程
------------------------------------------

## 循环定时任务使用步骤
from datetime import datetime, timedelta
from celery.schedules import crontab
from celery import Celery

# 提交的异步任务,放在里面
broker = 'redis://127.0.0.1:6379/1'

# 执行完的结果,放在这里
backend = 'redis://127.0.0.1:6379/2'

app = Celery('test', broker=broker, backend=backend, include=['celery_task.user_task', 'celery_task.order_task'])

# 第一步:在celery的py文件中写入 celery的配置
    app.conf.timezone = 'Asia/Shanghai'
    app.conf.enable_utc = False  # 是否使用UTC

# 任务的定时配置
app.conf.beat_schedule = {
    'send_sms': {
        'task': 'celery_task.user_task.send_sms',
        # 'schedule': timedelta(seconds=5),  # 可以是时间对象,每隔多长时间执行一次
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        'schedule': crontab(hour=16, minute=45),  # 每天16点45 执行一次
        'args': ('18888888', '6666'),
    },
    # 如果还想写其他定时任务,模仿上面的继续写就行了
}

---------------------------

# 第二步:启动beat的命令
    celery -A 包名 beat -l info
    例如  celery -A celery_task beat -l info

# 第三步:启动worker的命令
    celery -A celery_task worker -l info -P eventlet


# P是大P  l是小写的l
-----------------------------------------

# 注意点:
	1 启动命令的执行位置,如果是包结构,一定在包所在这一层
	2 include=['celery_task.order_task'],路径从包名下开始导入,
	因为我们在包这层执行的命令

.
.
.

# 异步任务与延时任务代码

from celery_task.user_task import send_sms

# 运行异步任务的代码,先将异步任务放到brokwer里面去
# 然后worker从brokwer里面拿任务执行
# 最后将执行后的结果放到backend里面存储



# 延时任务
# 运行延时任务的代码,延时任务也是先提交到brokwer里面去
# 然后worker从brokwer里面拿到任务,到了规定的时间后,
# 才开始执行延时任务函数
# 需要传入时间对象
from datetime import datetime, timedelta

res1 = send_sms.delay('18752386213', 8888)
print(res1)

eta = datetime.utcnow() + timedelta(seconds=60)
# 拿到utc时间对象 datetime.utcnow()
# 拿当前时间对象 datetime.now()
# 因为apply_async异步任务的运行参照的就是utc时间
# timedelta为了方便前面的时间对象的运算用的

res = send_sms.apply_async(args=['18752386213', 101], eta=eta)
print(res)

---------------------------------------
启动beat有时候会报错
CRITICAL/MainProcess] beat raised exception <class 'EOFError'>: EOFError('Ran out of input',)

beat在执行的时候会生成的一些记录,有时候需要删掉,才能正常重启beat
需要删除luffy_api项目下的3个elerybeat-schedule文件后,再启动beat才能正常

.
.
.
.

补充:如果在公司中,不做异步任务,就可以用APSchedule模块执行定时任务

详见文章 https://www.cnblogs.com/tengyifan888/articles/17342347.html

.
.
.
.
.
.

2 django中使用celery


# 使用步骤:
-1 把咱们写的包,复制到项目目录下

-luffy_api  # 项目路径
	-celery_task  # celery的包路径
	-luffy_api   # 源代码路径


-2 在使用提交异步任务的位置,导入使用即可
	视图函数中使用,导入任务函数名
	任务函数名.delay(传函数需要的参数)  # 提交任务


-3 启动worker,如果有定时任务,也启动beat


-4 等待任务被worker执行


-5 在视图函数中,查询任务执行的结果


.
.
.
.

2.1 秒杀功能案例

----------------------------------------------------
# 秒杀逻辑分析  重要!!!  逻辑不理解,代码看不懂

1 前端秒杀按钮,用户点击---》携带商品id数据发送ajax请求到后端
2 视图函数---》提交秒杀任务---》借助于celery,提交到中间件中了
3 后端返回任务id,被前端的ajax的异步回调函数接收到后,

4 前端用js开启循环定时任务,每隔3s钟,带着任务id,向后端发送请求,查看是否秒杀成功

5 后端的情况
	1 任务还在等待被执行----》返回给前端,前端继续每隔3s发送一次请求
	2 任务执行完了,秒杀成功了---》返回给前端,恭喜您秒杀成功--》关闭前端循环定时器
	3 任务执行完了,秒杀失败了---》返回给前端,秒杀失败--》关闭前端循环定时器


# 大白话将就是,后端有个视图函数是用来提交异步秒杀任务的
# 前端通过秒杀按钮,发送ajax请求,带着商品的id号,到后端的秒杀视图函数
# 后端秒杀视图函数拿着商品的id去提交异步秒杀任务,并返回该任务的id号给前端
# 前端拿到该任务的id号后,用js代码起一个循环定时任务,在循环定时任务里面
# 再发生ajax请求,带着任务id,到后端的获取异步任务结果的视图函数
# 后端拿着任务id去backend里面查,将获得的结果再返回给前端
# 前端再根据后端返回的结果来决定是否再继续运行循环定时任务,向后端询问该任务的执行情况
# 显然当后端秒杀成功与秒杀失败后,返给前段后,前端就没必要再继续执行循环定时任务
# 向后端查询秒杀的任务的执行情况了,只有秒杀的任务还在队列里排队,还未执行的时候
# 后端才需要继续运行循环定时任务,继续向后端发送请求,查询秒杀任务的执行情况
-----------------------------------------------------

# 秒杀的任务,正常应该查数据库,生成订单,改库存状态,
# 生成订单与改库存是两条sql,应该在一个事务里去完成
from django.db import transaction

with transaction.atomic():
	需要在一个事务里的sql语句1
	需要在一个事务里的sql语句2

.
.
.

2.1.1 视图

#### 秒杀逻辑,CBV
from rest_framework.viewsets import ViewSet

from celery_task.order_task import sckill_task
from celery_task.celery import app
from celery.result import AsyncResult

class SckillView(ViewSet):
    @action(methods=['GET', ], detail=False)
    def sckill888(self, request):
        # 秒杀商品的请求,到后端后,无论是get请求还是post请求,都会把要秒杀商品的id号带过来的
        goods_id = request.query_params.get('id')  # 假设商品的id是在地址里面携带过的
        # 异步,提交一个秒杀任务s
        res = sckill_task.delay(goods_id)  # res 是对象,可以直接打印,结果是任务的id

        return APIResponse(task_id=res.id)  # 要返回任务的id,要用对象点一下id,才能拿到对应id号


    @action(methods=['GET', ], detail=False)
    def get_result(self, request):
        task_id = request.query_params.get('task_id')
        a = AsyncResult(id=task_id, app=app)

        if a.successful():  # 执行完了
            result = a.get()  # 拿到结果
            if result:
                return APIResponse(msg='秒杀成功')
            else:
                return APIResponse(msg='秒杀失败')

        elif a.status == 'PENDING':
            print('任务等待被执行888')
            return APIResponse(code=777, msg='还在秒杀中')



.
.
.
.

2.1.2 任务 写在user_task.py

# 秒杀任务
from .celery import app
import random
import time
from django.db import transaction

@app.task
def sckill_task(goods_id):
    # 模拟生成订单,减库存,要在一个事务中
    with transaction.atomic():
        print('商品:%s ,秒杀开始' % goods_id)
        time.sleep(random.choice([6, 7, 8]))
        print('商品:%s ,秒杀结束' % goods_id)

    # print('商品id:%s ,秒杀开始' % goods_id)
    # time.sleep(random.choice([6, 7, 8]))
    # print('商品id:%s ,秒杀结束' % goods_id)

    return random.choice([True, False])
    # 模拟秒杀所需要的不同时间,与模拟秒杀可能成功也可能失败的结果

.
.
.

2.1.3 前端Sckill.vue

<template>
  <div>
    <button @click="handleClick">点击开始秒杀</button>
  </div>
</template>

<script>
export default {
  name: "Sckill",
  data() {
    return {
      task_id: '',
      t: null
    }
  },

  methods: {
    handleClick() {
      this.$axios.get(this.$settings.BASE_URL + '/user/sckill/sckill888/?id=999'
      ).then(res => {
            this.task_id = res.data.task_id
            // 拿到任务的id后,前端开启循环定时任务,每隔2秒,向后端查任务接口发送ajax请求,携带任务的id
            this.t = setInterval(() => {
              this.$axios.get(this.$settings.BASE_URL + '/user/sckill/get_result/?task_id=' + this.task_id).then(
                  res => {
                    // 当获取到后端返回的状态码是777,说明秒杀任务还未被执行,不关循环定时任务,继续
                    if (res.data.code == 777) {

                      // 弹窗用
                      this.$message({
                        message: res.data.msg,
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                          this.mobile = '';
                        }
                      });

                      console.log(res.data.msg)  // 在控制台看用
                    } else {
                      // 不是777,说明要么秒杀成功了,要么秒杀失败了,都要关掉查秒杀任务的 循环定时任务
                      clearInterval(this.t)
                      this.t = null

                      this.$message({
                        message: res.data.msg,
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                          this.mobile = '';
                        }
                      });


                    }
                  }
              )
            }, 2000)
          }
      ).catch(res => {
            // 转态码不是200的时候会执行该里面的函数
          }
      )

    }
  }
}
</script>

<style scoped>

</style>

.
.
.
.
.
.
.
.

2.2 django 中使用celery 总结

------------------------------------------------

-1 把咱们写的包,复制到项目目录下
-luffy_api
	-celery_task

	celery.py  # 文件里面如果用到django的东西,一定不要忘了配这两行代码
		import os
		os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

	-luffy_api  #源代码路径

# 配了那两句话,就相当于和django的test测试文件一样了
------------------------------------------------

-2 在使用提交异步任务的位置,导入使用即可
    	-视图函数中使用,导入任务
        -任务.delay()  # 提交任务

------------------------------------------------
    -3 启动worker,如果有定时任务,启动beat
------------------------------------------------
    -4 等待任务被worker执行
------------------------------------------------
    -5 在视图函数中,查询任务执行的结果

------------------------------------------------
# 重点:celery中使用djagno,有时候,任务中会使用django的orm,缓存,表模型等等
# 一定要加
	import os
	os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

.
.
.
.
.
.

3 轮播图接口加缓存

一般访问频率高的接口可以考虑加缓存,
或者是那种要查询表里面所有数据的接口也可以考虑加缓存,这样效率就变高了!!!

第一次请求的时候会走数据库查询数据,返给前端前先存到redis缓存中
这样后续的请求过来时,先到缓存中查看一下,缓存中有,直接将该数据返给前端


# 1 网站首页被访问的频率很高,瞬间 1w个人在访问,
# 首页的轮播图接口会执行1w次,1w次查询轮播图表的sql在执行,
# 然而轮播图基本不变


# 2 想一种方式,让这1w个人访问,效率更高一些,不查数据库了,
# 直接走缓存----redis------效率高


# 3 现在的逻辑变成了
	1 轮播图接口请求来了,先去缓存中看,如果有,直接返回缓存中的数据
	2 如果没有,查数据库,然后把轮播图数据,放到redis中,缓存起来
	3 下次轮播图接口请求再来,缓存中有,直接返回缓存中的数据
	4 这样轮播图接口请求,只有第一次是走的查询mysql数据库
	5 后面的9999次轮播图接口请求,里面查询轮播图走的都是redis缓存了

-----------------------------------------------

# 改接口
    # 加入缓存的轮播图接口
    def list(self, request, *args, **kwargs):
        # 查看缓存有没有数据,如果没有,再走数据库
        banner_list = cache.get('banner_list')
        if banner_list:
            print('走了缓存')
            return APIResponse(data=banner_list)
        else:  # 走数据库
            print('走了数据库')
            res = super().list(request, *args, **kwargs)
            # 把序列化后的数据存到缓存中,redis中
            cache.set('banner_list', res.data)
            return APIResponse(data=res.data)

.
.
.
.
.
.
.
.

4 双写一致性


# 加入缓存后,缓存中有数据,先去缓存拿
# 但是如果mysql中数据变了,缓存不会自动变化,出现数据不一致的问题
	mysql 和 缓存数据库 数据不一致

-------------------------------------------

# 双写一致性
	写入mysql,redis没动,数据不一致存在问题

-------------------------------------------

#如何解决?
	1 修改数据,删除缓存
	2 修改数据,更新缓存
	3 定时更新缓存 ----------实时性差


# 可以采用循环定时任务:celery    定时更新缓存
-------------------------------------------

.
.
.
.
.

5 首页轮播图定时更新代码

-----------------------------------------------------
# 第一步: 先写更新轮播图缓存的任务   在home_task文件夹下写

from home.models import Banner
from home.serializer import BannerSerializer
from django.core.cache import cache
from django.conf import settings

# 只要调用该函数,完成更新轮播图的缓存
@app.task
def update_banner():
    # 自己写逻辑,先查数据库拿到轮播图的queryset,然后用序列化类序列化成前端要用的大字典
    # 然后放到缓存里面去
    banner_queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')
    ser = BannerSerializer(instance=banner_queryset, many=True)
    # cache.set('banner_list', ser.data)  # 会出问题,轮播图显示不出来,轮播图地址显示不全
    for item in ser.data:
        item['image'] = settings.BACKEND_URL + item['image']
    cache.set('banner_list', ser.data)  # 把每一个字典里面的图片路径前都拼上后端地址http://127.0.0.1:80

    return True
----------------------------------

# 第二步:在celery配置 循环定时任务

app.conf.beat_schedule = {
  'update_banner': {
        'task': 'celery_task.home_task.update_banner',
        # 每隔10分钟执行一下update_banner函数 ,查一下数据库,
        # 用序列化类序列化成字典,然后更新一下缓存

        'schedule': timedelta(minutes=10),  # 可以是时间对象,每隔多长时间执行一次

    },

}

-----------------------------------------------------

# 第三步:启动worker,启动beat

    # 这样就具有的效果就是,每10分钟,去数据库里查出对象,用序列化类序列化成字典,
    # 然后将新字典更新到缓存里面去

    # 就拿轮播图接口来说,一开始假如是4张轮播图,现在将数据库里删掉一张后,
    # 由于有循环定时任务,10分钟一到,

    # 缓存里面就也会自动更新为3张,从而就保证了双写一致性了!!!

轮播图没了,因为图片的路径没有后端的地址了 'http://127.0.0.1:80'
原因是如果在视图类里面,序列化后,由于有request对象,
会自动把后端的地址匹配到图片的路径前面去
由于在任务的文件里面没有request对象,所以需要我们自己去拼前面的路径
image
.
.
.
.
.
.
.
.
.
.

6 课程前端页面

# 免费课,实战课,轻课
	-不要钱的,免费的
	-实战课:python面向对象  39.9
	-线下课程完整版的视频   19999

--------------------------------------------

# 课程列表页数据
1 新建3个页面组件
	FreeCourserView
	ActualCourserView
	LightCourseView

--------------------------------------------

2 配置路由
      {
        path: '/free-course',
        name: 'free',
        component: FreeCourserView
    },
    {
        path: '/actual-course',
        name: 'actual-course',
        component: ActualCourserView
    },
    {
        path: '/light-course',
        name: 'light-course',
        component: LightCourseView
    },

3 复制代码

<template>
  <div class="course">
    <Header></Header>
    <div class="main">
      <!-- 筛选条件 -->
      <div class="condition">
        <ul class="cate-list">
          <li class="title">课程分类:</li>
          <li class="this">全部</li>
          <li>Python</li>
          <li>Linux运维</li>
          <li>Python进阶</li>
          <li>开发工具</li>
          <li>Go语言</li>
          <li>机器学习</li>
          <li>技术生涯</li>
        </ul>

        <div class="ordering">
          <ul>
            <li class="title">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
            <li class="default this">默认</li>
            <li class="hot">人气</li>
            <li class="price">价格</li>
          </ul>
          <p class="condition-result">共21个课程</p>
        </div>

      </div>
      <!-- 课程列表 -->
      <div class="course-list">
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <Footer></Footer>
  </div>
</template>

<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"

export default {
  name: "Course",
  data() {
    return {
      category: 0,
    }
  },
  components: {
    Header,
    Footer,
  }
}
</script>

<style scoped>
.course {
  background: #f6f6f6;
}

.course .main {
  width: 1100px;
  margin: 35px auto 0;
}

.course .condition {
  margin-bottom: 35px;
  padding: 25px 30px 25px 20px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 4px 0 #f0f0f0;
}

.course .cate-list {
  border-bottom: 1px solid #333;
  border-bottom-color: rgba(51, 51, 51, .05);
  padding-bottom: 18px;
  margin-bottom: 17px;
}

.course .cate-list::after {
  content: "";
  display: block;
  clear: both;
}

.course .cate-list li {
  float: left;
  font-size: 16px;
  padding: 6px 15px;
  line-height: 16px;
  margin-left: 14px;
  position: relative;
  transition: all .3s ease;
  cursor: pointer;
  color: #4a4a4a;
  border: 1px solid transparent; /* transparent 透明 */
}

.course .cate-list .title {
  color: #888;
  margin-left: 0;
  letter-spacing: .36px;
  padding: 0;
  line-height: 28px;
}

.course .cate-list .this {
  color: #ffc210;
  border: 1px solid #ffc210 !important;
  border-radius: 30px;
}

.course .ordering::after {
  content: "";
  display: block;
  clear: both;
}

.course .ordering ul {
  float: left;
}

.course .ordering ul::after {
  content: "";
  display: block;
  clear: both;
}

.course .ordering .condition-result {
  float: right;
  font-size: 14px;
  color: #9b9b9b;
  line-height: 28px;
}

.course .ordering ul li {
  float: left;
  padding: 6px 15px;
  line-height: 16px;
  margin-left: 14px;
  position: relative;
  transition: all .3s ease;
  cursor: pointer;
  color: #4a4a4a;
}

.course .ordering .title {
  font-size: 16px;
  color: #888;
  letter-spacing: .36px;
  margin-left: 0;
  padding: 0;
  line-height: 28px;
}

.course .ordering .this {
  color: #ffc210;
}

.course .ordering .price {
  position: relative;
}

.course .ordering .price::before,
.course .ordering .price::after {
  cursor: pointer;
  content: "";
  display: block;
  width: 0px;
  height: 0px;
  border: 5px solid transparent;
  position: absolute;
  right: 0;
}

.course .ordering .price::before {
  border-bottom: 5px solid #aaa;
  margin-bottom: 2px;
  top: 2px;
}

.course .ordering .price::after {
  border-top: 5px solid #aaa;
  bottom: 2px;
}

.course .course-item:hover {
  box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}

.course .course-item {
  width: 1100px;
  background: #fff;
  padding: 20px 30px 20px 20px;
  margin-bottom: 35px;
  border-radius: 2px;
  cursor: pointer;
  box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
  /* css3.0 过渡动画 hover 事件操作 */
  transition: all .2s ease;
}

.course .course-item::after {
  content: "";
  display: block;
  clear: both;
}

/* 顶级元素 父级元素  当前元素{} */
.course .course-item .course-image {
  float: left;
  width: 423px;
  height: 210px;
  margin-right: 30px;
}

.course .course-item .course-image img {
  width: 100%;
}

.course .course-item .course-info {
  float: left;
  width: 596px;
}

.course-item .course-info h3 {
  font-size: 26px;
  color: #333;
  font-weight: normal;
  margin-bottom: 8px;
}

.course-item .course-info h3 span {
  font-size: 14px;
  color: #9b9b9b;
  float: right;
  margin-top: 14px;
}

.course-item .course-info h3 span img {
  width: 11px;
  height: auto;
  margin-right: 7px;
}

.course-item .course-info .teather-info {
  font-size: 14px;
  color: #9b9b9b;
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid #333;
  border-bottom-color: rgba(51, 51, 51, .05);
}

.course-item .course-info .teather-info span {
  float: right;
}

.course-item .lesson-list::after {
  content: "";
  display: block;
  clear: both;
}

.course-item .lesson-list li {
  float: left;
  width: 44%;
  font-size: 14px;
  color: #666;
  padding-left: 22px;
  /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
  background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
  margin-bottom: 15px;
}

.course-item .lesson-list li .lesson-title {
  /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
  display: inline-block;
  max-width: 200px;
}

.course-item .lesson-list li:hover {
  background-image: url("/src/assets/img/play-icon-yellow.svg");
  color: #ffc210;
}

.course-item .lesson-list li .free {
  width: 34px;
  height: 20px;
  color: #fd7b4d;
  vertical-align: super;
  margin-left: 10px;
  border: 1px solid #fd7b4d;
  border-radius: 2px;
  text-align: center;
  font-size: 13px;
  white-space: nowrap;
}

.course-item .lesson-list li:hover .free {
  color: #ffc210;
  border-color: #ffc210;
}

.course-item .pay-box::after {
  content: "";
  display: block;
  clear: both;
}

.course-item .pay-box .discount-type {
  padding: 6px 10px;
  font-size: 16px;
  color: #fff;
  text-align: center;
  margin-right: 8px;
  background: #fa6240;
  border: 1px solid #fa6240;
  border-radius: 10px 0 10px 0;
  float: left;
}

.course-item .pay-box .discount-price {
  font-size: 24px;
  color: #fa6240;
  float: left;
}

.course-item .pay-box .original-price {
  text-decoration: line-through;
  font-size: 14px;
  color: #9b9b9b;
  margin-left: 10px;
  float: left;
  margin-top: 10px;
}

.course-item .pay-box .buy-now {
  width: 120px;
  height: 38px;
  background: transparent;
  color: #fa6240;
  font-size: 16px;
  border: 1px solid #fd7b4d;
  border-radius: 3px;
  transition: all .2s ease-in-out;
  float: right;
  text-align: center;
  line-height: 38px;
}

.course-item .pay-box .buy-now:hover {
  color: #fff;
  background: #ffc210;
  border: 1px solid #ffc210;
}
</style>

.
.
.
.
.
.
.
.

6 课程功能表分析


# 轻课 实战课 免费课    需要几个表
	所有课程使用一个表  通过类型区分,
	但是可能出现3种课所需要的字段不完全一样,数据量越来越多,导致表查询速度慢

	一种课程一个表,比较合理

-------------------------------------------------

# 实战课页面---》分析出5张表
	-课程分类表  一个分类下有多个课程跟课程一对多

	-实战课表  一个实战课,会有多个章节,跟章节一对多
	-章节表   一个章节下有多个课时,章节和课时一对多
	-课时表
	-老师表   跟实战课一对多

	-正常课程要有评论,需要建立评论表,评论板块没写,这个表就不写了

# 表里面的字段,一般是根据原型图分析出来的

.
.
.


# 先创course的app目录,先cd到apps目录下
# python ../../manage.py startapp course       创建course的app
# 再到配置文件里面注册该app

----------------------------------------------

from django.db import models
from utils.common_model import BaseModel


# Create your models here.
# 5张:课程分类,实战课,章节,课时,老师表


# 1 课程分类表
class CourseCategory(BaseModel):
    # 继承BaseModel就有了BaseModel表里面的所有字段了
    name = models.CharField(max_length=64, unique=True, verbose_name='分类名称')

    class Meta:
        db_table = 'luffy_course_category'  # 指定表名
        verbose_name = '课程分类表'  #
        verbose_name_plural = '课程分类表'  # 改admin里面表的英文名

    # 让admin里面创建出的表模型对象显示由原来object 变成表里面name字段对应的值
    def __str__(self):
        return '%s' % self.name


# 2 实战课表
class Course(BaseModel):
    """课程"""
    course_type = (
        (0, '付费'),
        (1, 'VIP专享'),
        (2, '学位课程')
    )
    level_choices = (
        (0, '初级'),
        (1, '中级'),
        (2, '高级'),
    )
    status_choices = (
        (0, '上线'),
        (1, '下线'),
        (2, '预上线'),
    )
    name = models.CharField(max_length=128, verbose_name="课程名称")
    course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="课程封面图片", blank=True, null=True)
    course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")

    # 课程详情介绍
    brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
    level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
    period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)

    # 课程课件路径
    attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True,
                                       null=True)
    # 课程状态
    status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")

    # 学习人数是个优化字段,正常应该是没有该字段
    # 课程的学习人数,是课程表和用户表建立多对多外键关系,假设外键建在用户表里
    # 通过Course.objects.filter(pk=2).annotate(num=(Count(user__pk))).value('num')
    # 先筛选出课程,再对课程分组,再用聚合函数求出关联的用户有几个
    # 或者还可以先查出课程对象,再通过课程对象点user_set再点all() 拿到queryset[用户对象1,用户对象2]
    students = models.IntegerField(verbose_name="学习人数", default=0)

    sections = models.IntegerField(verbose_name="总课时数量", default=0)
    pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)

    # 一对多外键字段 课程分类 与 老师
    # on_delete=models.SET_NULL
    # 课程分类表里面删掉一个课程分类,与该课程分类关联的课程表里面,课程分类外键字段数据就置为空
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
                                        blank=True, verbose_name="课程分类")
    # 老师表里老师删掉一个,关联的课程表里面,对应老师的课程,不删除!!!
    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")

    class Meta:
        db_table = "luffy_course"
        verbose_name = "课程"
        verbose_name_plural = "课程"

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


# 3 章节表
class CourseChapter(BaseModel):
    """章节"""
    #
    chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
    name = models.CharField(max_length=128, verbose_name="章节标题")

    summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)

    # 只要课程删掉了,对应的关联表里面章节的数据也一起删掉!!!
    # 一个课程有多个章节,所以课程与章节是一对多的关系
    course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE, verbose_name="课程名称")

    class Meta:
        db_table = "luffy_course_chapter"
        verbose_name = "章节"
        verbose_name_plural = verbose_name

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


# 4 老师表
class Teacher(BaseModel):
    """导师"""
    role_choices = (
        (0, '讲师'),
        (1, '导师'),
        (2, '班主任'),
    )
    name = models.CharField(max_length=32, verbose_name="导师名")
    role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="导师身份")
    title = models.CharField(max_length=64, verbose_name="职位、职称")
    signature = models.CharField(max_length=255, verbose_name="导师签名", help_text="导师签名", blank=True, null=True)
    image = models.ImageField(upload_to="teacher", null=True, verbose_name="导师封面")
    brief = models.TextField(max_length=1024, verbose_name="导师描述")

    class Meta:
        db_table = "luffy_teacher"
        verbose_name = "导师"
        verbose_name_plural = verbose_name

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


# 5 课时表
class CourseSection(BaseModel):
    """课时"""
    section_type_choices = (
        (0, '文档'),
        (1, '练习'),
        (2, '视频')
    )
    # 课时与章节一对多,一个章节对多个课时,章节一删,对应的关联表里面课程数据也删掉
    chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
                                verbose_name="课程章节")

    name = models.CharField(max_length=128, verbose_name="课时标题")
    orders = models.PositiveSmallIntegerField(verbose_name="课时排序")

    section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
    section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接",
                                    help_text="若是video,填vid,若是文档,填link")

    # 视频时长
    duration = 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(verbose_name="是否可试看", default=False)

    class Meta:
        db_table = "luffy_course_section"
        verbose_name = "课时"
        verbose_name_plural = verbose_name

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


---------------------------------------------------

# 执行迁移命令
cd到项目根路径下
python manage.py makemigrations

python manage.py migrate

.
.
.
.
.

作业

# 秒杀功能,自己写完
# 轮播图加缓存
	-高级一些,写一个可以缓存的视图类,以后只要继承这个视图类,接口就有缓存,不继承就没有缓存


# 轮播图定时更新

# 课程5个表迁移完成

posted @   tengyifan  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示