luffy

企业项目类型

# 1 面向互联网用户:商城类项目
	-微信小程序商城
    -app商城
    -得物
    -饿了么
    -问卷网
# 2 面向互联网用户:二手交易类的
	-咸鱼
  	-转转
    
# 3 公司内部项目:python写的重点
# 传统软件行业,互联网
	-给客户做软件:国家电网,社保局,银行 ,医院,大客户
    -互联网:饿了么,美团,抖音,阿里
    
    
	-oa系统
    -故障上报系统
    -打卡系统工资核算系统
    -第三方公司做的:
    	-给医院 互联网,内部的项目
        -银行 内部系统
        -政府 
        -钢材市场,商户
    - 微信小程序订餐
    	-二维火 餐饮行业
    -零售行业
    -问卷网
    -考试系统
    -django+simpleui:二次定制
    
# 4 个人博客

# 5 内容收费站
	-掘金
    -思否
    
# 6 房屋租赁
  -青客
  -蛋壳
  -自如
    
# 7 软硬件结合
	-工业互联网
    -智慧农业

企业项目开发流程

# 开发流程
	-立项:高层
    -需求分析
        # 互联网项目
            -需求调研和分析:产品经理设计出来的
        # 传统软件
            -需求调研和分析:市场人员,开发 跟客户对接
    	# 需求说明书
	-原型设计:产品经理    根据需求说明书
    	-墨刀----》画原型图
		-懂业务
    -分任务开发
    	-ui团队
        	-根据原型图设计切图
    	-前端团队
            -UI设计
            -前端写代码(pc,小程序,移动端)
        -后端团队
        	-架构,数据库设计
            -分任务开发:用户,商品板块
           
        -联调测试
    -测试 
    -项目上线
    
    
 # 你平时工作的流程是什么样的
	-到了公司打开电脑---》登录公司的协作平台(禅道)---》看自己的任务和bug----》开始开发---》开发完提交到git----》如果有需求不明确,找产品经理讨论,bug找测试讨论

路飞项目需求

# 线上销售课程的
	-商城
    -知识付费类
    
# 需求
	-首页功能
    	-轮播图接口
    	-推荐课程接口
        
    -用户功能
    	-用户名密码登录
        -手机号验证码登录
        -发送手机验证码
        -验证手机号是否注册过
        -注册接口
        
   -课程列表功能
		-课程列表接口
    	-排序,过滤,分页
   -课程详情
		-课程详情接口
    	-视频播放功能
        -视频托管(第三方,自己平台)
   -下单功能
		-支付宝支付:生成支付链接,付款,回调修改订单状态
   		-购买成功功能
        
        
 # 原型图参照博客的具体图片

pip 永久换源

# pip install 下载比较慢----》第三方包都在 pypi 上,国外的,下载起来比较慢
# 临时换源:pip install -i 源地址(清华,阿里,豆瓣)

# 永久换源
> windows
-1 在文件地址栏输入:%APPDATA% 回车,快速进入 C:\Users\电脑用户\AppData\Roaming 文件夹中
-2 新建 pip 文件夹并
-3 在文件夹中新建 pip.ini 配置文件
-4 配置文件写入:
[global]
index-url = https://mirrors.aliyun.com/pypi/simple
[install]
use-mirrors =true
mirrors =https://mirrors.aliyun.com/pypi/simple
trusted-host =mirrors.aliyun.com


> mac配置或linux
1、在用户根目录下 ~ 下创建 .pip 隐藏文件夹,如果已经有了可以跳过
	-- 打开terminal,敲 cd
	-- mkdir ./.pip
2、进入 .pip 隐藏文件夹并创建 pip.conf 配置文件
	-- cd ~/.pip && touch pip.conf
3、新增 pip.conf 配置文件内容
[global]
index-url = https://mirrors.aliyun.com/pypi/simple
[install]
use-mirrors =true
mirrors =https://mirrors.aliyun.com/pypi/simple
trusted-host =mirrors.aliyun.com

虚拟环境和虚拟环境搭建

# 场景
	-写了个项目,使用djagno2.x版本---》django2.x装在了解释器上
    -后来又有个项目,使用使用djagno3.x版本---》django3.x装在解释器上
    -以后要打开第一个项目运行,需要卸载django3,安装django2
    -有种方式解决这个问:
    	-每个项目自己有个环境,装的模块,都是这个项目自己的
        
# 使用虚拟环境解决上述问题
	-Virtualenv  第三方的,用的多
    -pipenv      官方的

# Virtualenv使用步骤 win 平台
 	1 安装两个模块
    	pip3.8 install virtualenv   # 第三方虚拟环境
		pip3.8 install virtualenvwrapper-win # 增加模块,使虚拟环境在win上更好用
    2  配置环境变量:
	# 控制面板 => 系统和安全 => 系统 => 高级系统设置 => 环境变量 => 系统变量 => 点击新建 => 填入变量名与值
    变量名:WORKON_HOME  变量值:自定义存放虚拟环境的绝对路径
     WORKON_HOME: D:\Virtualenvs
        
   3 同步配置信息
	# 去向Python3的安装目录 => Scripts文件夹 => virtualenvwrapper.bat => 双击
        
   4 重新打开命令窗口,可以执行下面的命令
        # 1、创建虚拟环境到配置的WORKON_HOME路径下,一旦进入到虚拟环境,所有安装模块操作,都是操作虚拟环境
        # 1 选取默认Python环境创建虚拟环境:
            -- mkvirtualenv 虚拟环境名称   # 默认以 python  这个解释器来创建虚拟环境
        # 基于某Python环境创建虚拟环境:创建虚拟环境并进入虚拟环境
            -- mkvirtualenv -p python3.8 虚拟环境名称

        # 2、查看已有的虚拟环境
            -- workon

        # 3、使用某个虚拟环境
            -- workon 虚拟环境名称

        # 4、进入|退出 该虚拟环境的Python环境
            -- python | exit()

        # 5、为虚拟环境安装模块
            -- pip或pip3 install 模块名

        # 6、退出当前虚拟环境
            -- deactivate

        # 7、删除虚拟环境(删除当前虚拟环境要先退出)
            -- rmvirtualenv 虚拟环境名称
            -- 直接删文件夹
	
        
# mac或linux下安装虚拟环境   
	1 安装模块
        pip3 install -i https://pypi.douban.com/simple virtualenv
        pip3 install -i https://pypi.douban.com/simple virtualenvwrapper
    2 复制virtualenvwrapper.sh到/usr/local/bin路径下
        # 先找到virtualenvwrapper的工作文件 virtualenvwrapper.sh,该文件可以刷新自定义配置,但需要找到它
    # MacOS可能存在的位置 /Library/Frameworks/Python.framework/Versions/版本号文件夹/bin
    # Linux可能所在的位置 /usr/local/bin  |  ~/.local/bin  |  /usr/bin
    # 建议不管virtualenvwrapper.sh在哪个目录,保证在 /usr/local/bin 目录下有一份
    # 如果不在 /usr/local/bin 目录,如在 ~/.local/bin 目录,则复制一份到 /usr/local/bin 目录
        -- sudo cp -rf /路径/virtualenvwrapper.sh /usr/local/bin
   3  配置环境变量
    # 在 ~/.bash_profile 完成配置,virtualenvwrapper的默认默认存放虚拟环境路径是 ~/.virtualenvs
    # WORKON_HOME=自定义存放虚拟环境的绝对路径,需要自定义就解注
    VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3  # 指定的是那个解释器
    source /usr/local/bin/virtualenvwrapper.sh       # 指定的是virtualenvwrapper.sh 
     4 在终端让配置生效:
        source ~/.bash_profile     
        
# 什么是环境变量
	-我们在命令行中,执行一个命令,写命令名字执行
    	-1 当前路径下有这个可执行文件
        -2 这个可执行文件,在环境变量中
        
    -加环境变量的目的
    	-在任意路径下敲 可执行文件都可以,原因是当前敲的可执行文件路径在环境变量中
        
    
    -两层
    	-用户环境变量:只有当前用户生效
        -系统环境变量:所有用户都生效
        
# mac平台,需要用命令行操作,文件
	用户环境变量:.bash_profile
    

luffy后台创建目录调整

# 创建django项目  两种方式
	-命令行
    	django-admin startproject 项目名
    	
    -pycharm创建
    
# 命令行 
	1 workon luffy
    2 pip install django==3.2.12
    3 django-admin startproject luffy_api
    
    
# pycharm 创建项目
	-要选中咱们的虚拟环境---》如果虚拟环境不在,要新增进去
    
# 打开的项目没有使用虚拟环境,在pycharm中如何配置

调整目录结构

├── luffyapi
    ├── logs/				# 项目运行时/开发时日志目录 - 包
    ├── manage.py			# 脚本文件
    ├── luffyapi/      		# 项目主应用,开发时的代码保存 - 包
     	├── apps/      		# 开发者的代码保存目录,以模块[子应用]为目录保存 - 包
        ├── libs/      		# 第三方类库的保存目录[第三方组件、模块] - 包
    	├── settings/  		# 配置目录 - 包
			├── dev.py   	# 项目开发时的本地配置
			└── prod.py  	# 项目上线时的运行配置
		├── urls.py    		# 总路由
		└── utils/     		# 多个模块[子应用]的公共函数类库[自己开发的组件]
    └── scripts/       		# 保存开发项目的脚本文件 - 文件夹
    
    
    
    
#### 调整目录后,以后app全都放在apps文件夹下
> 创建app,进入到apps的路径
  python ../../manage.py startapp user  # 在apps目录下创建出一个userapp, 一定要注意路径
        
# 创建完app需要注册
> 两种方式:
	- 全路径
    - 讲apps所在路径加入环境变量
    
# 加入环境变量
BASE_DIR = Path(__file__).resolve().parent.parent  # D:\路飞项目\luffy_api\luffy_api
sys.path.append(os.path.join(BASE_DIR, 'apps'))   # 根据BASE_DIR来拼接路径
sys.path.append(str(BASE_DIR))  # 将D:\路飞项目\luffy_api\luffy_api加入环境变量方便后期导入
    
    
## django项目运行,优先运行settings.py 配置文件
	命令运行  python manage.py runserver--->所以,manage中的配置文件路径要正确
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

### 重要:项目上线,不适用manage.py 运行---》使用uwsgi运行wsgi.py 文件----》修改这个文件的配置
	-asgi.py
    -wsgi.py 
   	配置文件指定  prod.py  以后上线使用这个配置文件

启动django设置

如上操作不行,尝试以下方式

项目开始

数据库


# 软件开发模式
	-学bbs,设计创建出所有表,直接迁移,后期没改过  --》瀑布开发模式
    -学路飞,先设计,开发一点,测试上线一点-----》敏捷开发
    	
# 如果用户表想用 auth的user表扩写,在一开始就要定好

#1  使用mysql 数据库,创建一个库,navicate创建即可

#2 新建一个user app,基于auth的user表扩写用户表
    class User(AbstractUser):
        mobile = models.CharField(max_length=11, unique=True)
        # 需要pillow包的支持
        icon = models.ImageField(upload_to='icon', default='icon/default.png')

        class Meta:
            db_table = 'luffy_user'   # 指定表名
            verbose_name = '用户表'   # 后台管理看到的中文
            verbose_name_plural = verbose_name

        def __str__(self):  # 打印对象,显示的
            return self.username

# 3 迁移数据(mysql-->配置文件配置)
	-项目的数据库用户,不使用root用,新建一个mysql用户给项目用
    -因为root用户权限太大了,新建用户权限小一些
    -创建一个luffy用户,授权,只授予luffy库的权限
    # 查看当前数据库有哪些用户
    select user,host,password  from mysql.user;
    # 创建luffy用户
    # 创建一个用户叫luffy,密码是:Luffy123?,可以本地链接,对luffy库所有表有权限
    grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123?';
     # 创建一个用户叫luffy,密码是:Luffy123?,可以远程地链接,对luffy库所有表有权限
	grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
    # 删除一个用户
    DROP USER '用户'@'连接方式';(localhost, %)
    
    
    # django项目如果使用pymsql链接mysql,需要加两句话,加在哪不重要,重要的是它一定要执行
    	import pymysql
		pymysql.install_as_MySQLdb()
         django 2 高一点的版本就会报错,需要改源码,麻烦
        
   # 咱们以后,使用mysqlclient 操作mysql不需要任何配置,就可以操作mysql
		- pip install mysqlclient
    	- pip install pillow  # 下载这个是因为数据库里面要用
    
# 4 执行迁移文件
	python.exe manage.py makemigrations
	python.exe manage.py migrate       

数据库之隐藏密码

# 我们直接把mysql的用户名和密码 写死在了代码中----》后期可能会存在风险----》代码如果泄露----》mysql的用户密码泄露----》可以远程登录----》脱裤(拖库)----》所有数据会被黑客获取到

# 我们不把敏感信息写死在代码中
	-使用从环境变量中获取
    	-PATH:  任意路径下敲可执行文件能找到
        -其它key value 是该机器的全局配置,可以直接拿到
        -使用python代码拿到

        
        
##### python代码获取环境变量的值#####
# 配置完环境变量,重启一下pycharm
# 如果环境变量没配置,就用默认的
# 如果配置了,就用配置的(项目上线时候,运维配置环境变量---》运维配置的环境变量的值----》开发根本不知道)
import os
res=os.environ.get('PWD','123?')
print(res)
res=os.environ.get('USER','gjl')
print(res)
    

    
###### 具体操作##### 
# 使用get获取值,后面给上默认值,当在本地时,可以通过环境变量获取到用户名和密码,当在别的机器上是使用默认值登录

user = os.environ.get('USER', 'gjl')
pwd = os.environ.get('PWD', '123')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',  # 数据库名字
        'USER': user,  # luffy用户
        'PASSWORD': pwd,
        'HOST': 'localhost',
        'PORT': 3306
    }
}

封装logger

# 项目运行,会产生很多日志


# 日志作用:记录程序运行过程中 错误,警告,程序员的输出,观察项目运行情况
# 每个项目都需要记录日志

# django中加入记录日志的功能
1 把如下代码,copy到配置文件中
	LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {          # 日志的输出格式
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
        },
    },
    'filters': {
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {   # 日志输出的路径 console输出在终端
            # 实际开发建议使用WARNING
            'level': 'WARNING',  # 日志级别
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {  # 日志输出到文件
            # 实际开发建议使用ERROR
            'level': 'ERROR',  # 日志级别
            'class': 'logging.handlers.RotatingFileHandler',
            # 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
            'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"),
            # 日志文件的最大值,这里我们设置300M
            'maxBytes': 300 * 1024 * 1024,
            # 日志文件的数量,设置最大日志数量为10
            'backupCount': 10,
            # 日志格式:详细格式
            'formatter': 'verbose',
            # 文件内容编码
            'encoding': 'utf-8'
        },
    },
    # 日志对象
    'loggers': {
        'django': {
            'handlers': ['console', 'file'],
            'propagate': True,  # 是否让日志信息继续冒泡给其他的日志处理系统
        },
    }
}
2 获取logger对象:utils/common_logger.py
    # 日志对象获取,以后想用日志的地方,直接导入,使用 logger.info  error....
    import logging
    logger=logging.getLogger('django')
    
3 以后想用日志的地方,直接导入使用即可
  	# DEBUG < INFO < WARNING < ERROR < CRITICAL
    logger.debug('debug级别')
    logger.info('info级别')
    logger.warning('warning级别')
    logger.error('error级别')
    logger.critical('CRITICAL级别')
    
4 以后想用print的位置,都用logger.info,以后项目上线,调高日志输出级别,虽然代码中写了日志输出,实际上并不会输出

环境变量设置

# 1 为什么一定要设置默认值
user = os.environ.get('USER', 'gjl')
pwd = os.environ.get('PWD', '123')

# 2 这种环境变量,一般写死在系统,而是临时设置,系统重启就失效
	win:图形化界面的设置
    mac:
    	.bash_profile:只在当前会话生效,当前窗口中生效,关掉窗口,再打开,就失效了,source .bash_profile
        .zshrc:当前用户永久生效
        
   linxu: 文件可能名字不一样,配置方式跟mac完全一样
    
    
	# 临时设置:运维上线时候,手动设置,当前会话生效
    win:
    	set xx=lqz  # 设置环境变量
        echo %xx%
        
    linux ,mac: 
        export xx=lqz
        echo $xx
        
    项目部署:打开一个命令窗口
    设置一堆环境变量
    python manage.py runserver 

封装全局异常

# utils/common_exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
from utils.common_logger import logger


#### 加入日志记录,只要走到这,说明程序出error了,程序的error,咱们都要记录日志,方便后期排查
### 日志记录尽量详细:ip;如果用户登录了,记录用户;请求地址是;执行那个视图类出的错
def common_exception_handler(exc, context):
    request = context.get('request')
    view = context.get('view')
    ip = request.META.get('REMOTE_ADDR')
    try:
        user_id = request.user.pk
    except:
        user_id = '【未登录用户】'
    path = request.get_full_path()
    view_str = str(view)
    res = exception_handler(exc, context)
    logger.error('用户地址为:%s,用户id号为:%s,请求地址为:%s,执行的视图函数为:%s' % (ip, user_id, path, view_str))


    if res:
        # drf的异常,一种是从res.data这个字典的detail中取,一种是  直接取data
        if isinstance(res.data, dict):
            data = {'code': 999, 'msg': res.data.get('detail', '系统错误,请联系系统管理员')}
        else:
            data = {'code': 998, 'msg': res.data}

    else:
        # django的异常
        data = {'code': 888, 'msg': str(exc)}
    return Response(data)


### 配置文件配置
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'utils.common_exceptions.common_exception_handler',
}

封装Response

# 咱们之前使用drf提供的Response类,用它的时候,需要传很多参数,基于它做封装,方便我们的使用
	data=None, 
    status=None, 
	headers=None,

 # 封装后达成的效果是 APIResponse
	return APIResponse()  
	----->前端收到的是  {code:100,msg:成功}
    
	return APIResponse(token=12q4dfasdf,username=lqz)  
	----->前端收到的是  {code:100,msg:成功,token:12q4dfasdf,username:lqz}
    
    return APIResponse(data=[{name:红楼梦,price:33},{name:西游记,price:33}]) 
	----->前端收到的是  {code:100,msg:成功,data:[{name:红楼梦,price:33},{name:西游记,price:33}]}
    
    return APIResponse(code=101,msg='失败') 
	----->前端收到的是  {code:101,msg:失败}
    
    return APIResponse(headers={'xx':'xx'}) 
	----->前端收到的是  {code:100,msg:成功},但是相应中有xx:xx
    
    return APIResponse(name=lqz,status=201) 
	----->前端收到的是  {code:100,msg:成功,name:lqz},但是响应状态码是201
    
    
 

# utils/common_response.py

from rest_framework.response import Response
class APIResponse(Response):
    def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs):
        data = {'code': code, 'msg': msg}
        # kwargs 有可能是  {token:asdfad,name:lqz}
        if kwargs:
            data.update(kwargs)
        # 调用父类(Response)的__init__完成初始化
        super().__init__(data=data, status=status, headers=headers)

开启media访问

# 头像,课程图片,放在项目的某个目录下 (media),后期需要能够访问
# 需要开启media的访问

urlpatterns = [
    path('admin/', admin.site.urls),
    path('media/<path:path>', serve,{'document_root':settings.MEDIA_ROOT}),

]

前端配置

# 使用vue2创建一个新的前端项目
vue create luffy_city


# 安装第三方
 - axios : cnpm install -S axios
 - elementui : cnpm install -S vue-cookies
 - vue-cookies : cnpm i element-ui@2.6.2 -S
    
    
# 配置第三方
// elementui配置
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
    
    
// axios配置
import axios from "axios";
vue.prototype.$axios=axios

// vue-cookies配置
import cookies from 'vue-cookies'
Vue.prototype.$cookies=cookies
    
# vue.prototype
vue.prototype是注册全局变量的一种方式,使用vue.prototype的变量可以全局访问,如上axios和cookies以后使用就是this.$axios和this.$cookies使用,就不需要再导入使用,再定义变量时前面加上$是为了防止命名冲突

# 写一个全局配置settings.js 文件
	# assets--》js--》settings.js
    export default {
        BASE_URL: 'http://127.0.0.1:8000/api/v1/'
    }
    # main.js中
    import settings from '@/assets/js/settings'
	Vue.prototype.$settings=settings
    # 以后再任意组件中
    this.$settings.BASE_URL
    
    
# 全局样式
	html的标签a ul li ,都会有默认样式,正常的前端,都会去掉所有标签的默认样式, 自己写样式
    
    # 1 assets--》css--》global.css
    /* 声明全局样式和项目的初始化样式 */
    body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea {
        margin: 0;
        padding: 0;
        font-size: 15px;
    }

    a {
        text-decoration: none;
        color: #333;
    }

    ul {
        list-style: none;
    }

    table {
        border-collapse: collapse; /* 合并边框 */
    }
    
    # 2 main.js 配置,样式全局生效
    // 使用全局样式,取出所有标签默认样式
	import '@/assets/css/global.css'

后台主页模块设计

# 1 创建后台主页模块(一个模块一个app)
python ../../manage.py startapp home

# 2 在models中写轮播图表
	-写一个基表BaseModel
    -写轮播图表
# 3 迁移

'''考虑到轮播图与课程表有相同的字段,所以就将公共的字段提取成一个虚拟表'''
### BaseModel##########
from django.db import models
class BaseModel(models.Model):
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
    is_delete = models.BooleanField(default=False, verbose_name='是否删除')
    is_show = models.BooleanField(default=True, verbose_name='是否上架')
    orders = models.IntegerField(verbose_name='优先级')


    class Meta:
        abstract = True  # 这个表模型只用来继承,不用来在数据库中生成表
        
        
##########Banner########
from django.db import models

from utils.common_models import BaseModel
class Banner(BaseModel):
    # 1 id  img图片地址        长传时间  是否删除 是否显示 order
    # 把公共字段抽取到某个基表中,以后要使用,直接继承基表,扩写自己的字段就可以了---》用过的AbstractUser就是这个原理
    title = models.CharField(max_length=16, unique=True, verbose_name='名称')
    image = models.ImageField(upload_to='banner', verbose_name='图片')
    # 点击图片,调整到的路径
    # 前端跳转的地址: 前端路由     完整的http链接
    link = models.CharField(max_length=64, verbose_name='跳转链接')
    info = models.TextField(verbose_name='详情')  # 也可以用详情表,宽高出处

    class Meta:
        db_table = 'luffy_banner'
        verbose_name_plural = '轮播图表'

    def __str__(self):
        return self.title

simpleui后台管理

# 官网
https://simpleui.72wo.com/docs/simpleui/QUICK.html#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AAdjango%E9%A1%B9%E7%9B%AE

# 下载与配置
pip install django-simpleui -i https://pypi.tuna.tsinghua.edu.cn/simple

  INSTALLED_APPS = [
      'simpleui',
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',
      ...
  ]

# 录入数据

## 再admin中注册
from .models import Banner
admin.site.register(Banner)

###创建admin用户添加数据
python.exe manage.py createsuperuser


# 正常在公司中,网站分主站和后台管理

# 后台管理,主要是运营录入数据,使用simpleui

轮播图接口

封装自己的mixin

from rest_framework.mixins import ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin, \
    RetrieveModelMixin
from utils.common_response import APIResponse


class CommonListModelMixin(ListModelMixin):
    def list(self, request, *args, **kwargs):
        res = super().list(request, *args, **kwargs)
        return APIResponse(data=res.data)


class CommonCreateModelMixin(CreateModelMixin):
    def create(self, request, *args, **kwargs):
        res = super().create(request, *args, **kwargs)
        return APIResponse(data=res.data)  # {code:100,msg:成功,data:{}}


class CommonUpdateModelMixin(UpdateModelMixin):
    def update(self, request, *args, **kwargs):
        res = super(CommonUpdateModelMixin, self).update(request, *args, **kwargs)
        return APIResponse(data=res.data)


class CommonRetrieveModelMixin(RetrieveModelMixin):
    def retrieve(self, request, *args, **kwargs):
        res = super(CommonRetrieveModelMixin, self).retrieve(request, *args, **kwargs)
        return APIResponse(data=res.data)


class CommonDestroyModelMixin(DestroyModelMixin):
    def destroy(self, request, *args, **kwargs):
        res = super().destroy(request, *args, **kwargs)
        return APIResponse(msg='删除成功')

视图

# 查询所有 轮播图接口
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from .serialzier import BannerSerializer
from .models import Banner
from utils.common_mixin import CommonListModelMixin as ListModelMixin


class BannerView(GenericViewSet, ListModelMixin):
    queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = BannerSerializer

路由

# 使用路由分发

## 总路由
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/home/', include('home.urls')),
    # static 默认开启的,后期咱们会开启media文件夹,除此之外的其它文件夹,尽量不要开放,让外部访问
    path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),

]

# 分路由
from rest_framework.routers import SimpleRouter

router = SimpleRouter()
from .views import BannerView

router.register('banner', BannerView, 'banner')
urlpatterns = [
    # path('admin/', admin.site.urls),

]
urlpatterns += router.urls

序列化类

from rest_framework import serializers
from .models import Banner


class BannerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Banner
        fields = ['id', 'image', 'title', 'link']

自定义配置文件

# 通过配置文件控制显示多少条轮播图
# 使用步骤
    1 在settings下新建:common_settings.py
    2 在dev.py中加入
    from .common_settings import *
    3 在轮播图接口上
    class BannerView(GenericViewSet, ListModelMixin):
        queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
        serializer_class = BannerSerializer

跨域问题详解

# 以后只要前后端分离项目,都会出现跨域问题,咱们要解决

# 同源策略
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现

请求的url地址,必须与浏览器上的url地址处于同域上:也就是[域名],[端口],[协议]相同.
http://127.0.0.1:8080
ftf://127.0.0.1:8080

比如:我在本地上的域名是127.0.0.1:8000,请求另外一个域名:127.0.0.1:8001一段数据

浏览器上就会报错,个就是同源策略的保护,如果浏览器对javascript没有同源策略的保护,那么一些重要的机密网站将会很危险

请求发送,服务的执行,数据也正常返回,只是被浏览器拦截了

# 正因为同源策略的存在,咱们写前后端分离的项目,无法正常获取到数据


# 解决跨域问题:
	1  jsonp 跨域(不了解)
    2  跨域资源共享(CORS)  后端技术
    3  Nginx代理


# CORS:跨域资源共享
CORS需要浏览器和服务器同时支持,所有浏览器都支持该功能
只需要服务的处理即可:只需要在在响应头中加入固定的头就实现cors---》比如在响应头中加入Access-Control-Allow-Origin='*'---->get请求就没有跨域了---》但是put请求还会有


# cors的请求分两种
	-简单请求,浏览器直接发起
    -非简单请求,浏览器先发送要给options预检请求,服务端允许,再发送真正的请求
# 什么是简单请求,什么是非简单请求
	# 如果属于下面,就是简单请求
    1 请求方法是以下三种方法之一:
        HEAD
        GET
        POST
     2 HTTP的头信息不超出以下几种字段:
        Accept
        Accept-Language
        Content-Language
        Last-Event-ID
        Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

        
# 使用cors解决跨域,就是再响应头中加入固定的一些东西,专门写个中间件
	    res['Access-Control-Allow-Headers'] = 'token'
        res['Access-Control-Allow-Methods'] = 'DELETE'
    	res['Access-Control-Allow-Origin'] = 'http://192.168.1.252:8080'


### 补充:######
前端访问的后端地址,一定要准确

自定义中间件,解决跨域问题

##### common_mideleware.py
from django.utils.deprecation import MiddlewareMixin


### 自定义中间件解决跨域问题---》以后其它框架都是这个原理---》django上有人做了
class CorsMiddleware(MiddlewareMixin):
    def process_response(self, request, response):
        if request.method == 'OPTIONS':
            response['Access-Control-Allow-Headers'] = 'token'
            response['Access-Control-Allow-Methods'] = 'DELETE'
        response['Access-Control-Allow-Origin'] = '*'
        return response
    
 ### 配置文件配置中间件
    MIDDLEWARE = [
        'utils.common_mideleware.CorsMiddleware'
    ]

使用第三方 django-cors-headers

####使用pip安装
pip install django-cors-headers
#####添加到setting的app中
INSTALLED_APPS = (
	...
	'corsheaders',
	...
)
#### 添加中间件
MIDDLEWARE = [  
	...
	'corsheaders.middleware.CorsMiddleware',
	...
]
#### 4、setting下面添加下面的配置
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_METHODS = (
	'DELETE',
	'GET',
	'OPTIONS',
	'PATCH',
	'POST',
	'PUT',
	'VIEW',
)

CORS_ALLOW_HEADERS = (
	'XMLHttpRequest',
	'X_FILENAME',
	'accept-encoding',
	'authorization',
	'content-type',
	'dnt',
	'origin',
	'user-agent',
	'x-csrftoken',
	'x-requested-with',
	'Pragma',
	'token'
)

django-cors-headers源码

# 核心代码再中间件的---》process_response  3.0.14 版本
class CorsMiddleware(MiddlewareMixin):
    def process_response(self, request, response):
        if (
            not conf.CORS_ALLOW_ALL_ORIGINS
            and not self.origin_found_in_white_lists(origin, url)
            and not self.check_signal(request)
        ):
            return response
        if conf.CORS_ALLOW_ALL_ORIGINS and not conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
        else:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
        if request.method == "OPTIONS":
            response[ACCESS_CONTROL_ALLOW_HEADERS] = ", ".join(conf.CORS_ALLOW_HEADERS)
            response[ACCESS_CONTROL_ALLOW_METHODS] = ", ".join(conf.CORS_ALLOW_METHODS) 
        return response

前台主页功能

Banner.vue

<template>
  <div>
    <el-carousel height="400px">
      <el-carousel-item v-for="item in banner_list" :key="item">
        <!--        router-link只能跳内部,不能跳外部-->

        <router-link :to="item.link" v-if="item.link.indexOf('http:')<0">
          <img :src="item.image" :alt="item.title">
        </router-link>
        <a :href="item.link" v-else>
          <img :src="item.image" :alt="item.title">
        </a>

      </el-carousel-item>
    </el-carousel>
  </div>
</template>

<script>
export default {
  name: "Banner",
  data() {
    return {banner_list: []}
  },
  created() {
    this.$axios.get(`${this.$settings.BASE_URL}home/banner/`).then(res => {
      this.banner_list = res.data.data
    })
  }
}
</script>

<style scoped>
.el-carousel__item h3 {
  color: #475669;
  font-size: 18px;
  opacity: 0.75;
  line-height: 300px;
  margin: 0;
}


.el-carousel__item {
  height: 400px;
  min-width: 1200px;
}

.el-carousel__item img {
  height: 400px;
  margin-left: calc(50% - 1920px / 2);
}
</style>

Footer.vue

<template>
  <div class="footer">
    <ul>
      <li>关于我们</li>
      <li>联系我们</li>
      <li>商务合作</li>
      <li>帮助中心</li>
      <li>意见反馈</li>
      <li>新手指南</li>
    </ul>
    <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p>
  </div>
</template>

<script>
export default {
  name: "Footer"
}
</script>

<style scoped>
.footer {
  width: 100%;
  height: 128px;
  background: #25292e;
  color: #fff;
}

.footer ul {
  margin: 0 auto 16px;
  padding-top: 38px;
  width: 810px;
}

.footer ul li {
  float: left;
  width: 112px;
  margin: 0 10px;
  text-align: center;
  font-size: 14px;
}

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

.footer p {
  text-align: center;
  font-size: 12px;
}
</style>

Header.vue

<template>
  <div class="header">
    <div class="slogan">
      <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
    </div>
    <div class="nav">
      <ul class="left-part">
        <li class="logo">
          <router-link to="/">
            <img src="../assets/img/head-logo.svg" alt="">
          </router-link>
        </li>
        <li class="ele">
          <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
        </li>
        <li class="ele">
          <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
        </li>
        <li class="ele">
          <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
        </li>
      </ul>

      <div class="right-part">
        <div>
          <span>登录</span>
          <span class="line">|</span>
          <span>注册</span>
        </div>
      </div>
    </div>
  </div>

</template>

<script>

export default {
  name: "Header",
  data() {
    return {
      url_path: sessionStorage.url_path || '/',
    }
  },
  methods: {
    goPage(url_path) {
      // 已经是当前路由就没有必要重新跳转
      if (this.url_path !== url_path) {
        this.$router.push(url_path);
      }
      sessionStorage.url_path = url_path;
    },
  },
  created() {
    sessionStorage.url_path = this.$route.path;
    this.url_path = this.$route.path;
  }
}
</script>

<style scoped>
.header {
  background-color: white;
  box-shadow: 0 0 5px 0 #aaa;
}

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

.slogan {
  background-color: #eee;
  height: 40px;
}

.slogan p {
  width: 1200px;
  margin: 0 auto;
  color: #aaa;
  font-size: 13px;
  line-height: 40px;
}

.nav {
  background-color: white;
  user-select: none;
  width: 1200px;
  margin: 0 auto;

}

.nav ul {
  padding: 15px 0;
  float: left;
}

.nav ul:after {
  clear: both;
  content: '';
  display: block;
}

.nav ul li {
  float: left;
}

.logo {
  margin-right: 20px;
}

.ele {
  margin: 0 20px;
}

.ele span {
  display: block;
  font: 15px/36px '微软雅黑';
  border-bottom: 2px solid transparent;
  cursor: pointer;
}

.ele span:hover {
  border-bottom-color: orange;
}

.ele span.active {
  color: orange;
  border-bottom-color: orange;
}

.right-part {
  float: right;
}

.right-part .line {
  margin: 0 10px;
}

.right-part span {
  line-height: 68px;
  cursor: pointer;
}
</style>

HomeView.vue

<template>
  <div class="home">
    <Header></Header>
    <Banner></Banner>
    <br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
    <Footer></Footer>
  </div>
</template>

<script>
import Banner from "@/components/Banner";
import Footer from '@/components/Footer'
import Header from "@/components/Header";

export default {
  name: 'HomeView',
  // created() {
  //
  //   this.$axios.get('http://127.0.0.1:8000/api/v1/home/test/', {
  //     headers: {
  //       token: 'sdfafasdf'
  //     }
  //
  //   }).then(res => {
  //     console.log(res.data.data)
  //   })
  // }

  components: {
    Header, Footer, Banner
  }

}
</script>

导出项目依赖

# 以后所有python项目的根路径下,都会有个 requirements.txt 【约定俗称的名字】,这里面记录了当前项目所有的依赖---》格式如下
Django==3.2.12
redis # 最新版

# 我们也要设置
	-笨办法
    	直接手动建立
    -一键导出(虚拟环境中:虚拟环境用的模块,就是当前项目的模块,一般不会少,不会多,如果多,可以手动删掉)
    	-pip list
        -pip freeze >requirements.txt

首页推荐课程前端

HomeView.vue

<template>
  <div class="home">
    <Header></Header>
    <Banner></Banner>
    <div>
      <el-row>
        <el-col :span="6" v-for="(o, index) in 8" :key="o" class="course_detail">
          <el-card :body-style="{ padding: '0px' }">
            <img src="http://photo.liuqingzheng.top/2023%2002%2022%2021%2057%2011%20/image-20230222215707795.png"
                 class="image">
            <div style="padding: 14px;">
              <span>热门课程</span>
              <div class="bottom clearfix">
                <time class="time">价格:199</time>
                <el-button type="text" class="button">查看详情</el-button>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <img src="http://photo.liuqingzheng.top/2023%2003%2001%2016%2010%2034%20/1.png" alt="" width="100%" height="500px">
    <Footer></Footer>
  </div>
</template>

<script>
import Banner from "@/components/Banner";
import Footer from '@/components/Footer'
import Header from "@/components/Header";

export default {
  name: 'HomeView',
  // created() {
  //
  //   this.$axios.get('http://127.0.0.1:8000/api/v1/home/test/', {
  //     headers: {
  //       token: 'sdfafasdf'
  //     }
  //
  //   }).then(res => {
  //     console.log(res.data.data)
  //   })
  // }

  components: {
    Header, Footer, Banner
  }

}
</script>
<style scoped>
.time {
  font-size: 13px;
  color: #999;
}

.bottom {
  margin-top: 13px;
  line-height: 12px;
}

.button {
  padding: 0;
  float: right;
}

.image {
  width: 100%;
  display: block;
}

.clearfix:before,
.clearfix:after {
  display: table;
  content: "";
}

.clearfix:after {
  clear: both
}

.course_detail {
  padding: 50px;
}
</style>

git

介绍和安装

# 版本管理软件
	-1 对代码版本进行管理---》首页功能完成---》课程功能完成---》可以回退到某个版本
    -2 协同开发--》多人开发--》合并代码---》可能会有冲突,解决冲突
    
# 版本管理软件:主流就两个
	-git:现在用的最多(学git)
    -svn:老
    
# git与svn比较
	-svn:cs架构 一个服务端,多个客户端,如果服务端挂掉,整个代码合并,提交代码就做不了了,只能本地开发代码
    -git:分布式管理,装git的客户端,既可以当客户端,又可以当服务端,如果git远程仓库挂掉,本地可以继续做代码版本的管理
    
    
# 安装git,就是个软件
	-https://git-scm.com/download/win
	-官网下载,一路下一步
    -再命令行中:git version  如果有翻译,说明装好了
    

git,github,gitee,gitlab

# git :版本管理软件,可以做版本管理
# github:它是一个网站:https://github.com/ 全球最大的开源代码管理仓库,git远程仓库
	-运营商不让访问
# gitee:中国最大的开源代码管理仓库(私有仓库)
	-https://gitee.com/kitego/hashmart
    
    
# gitlab: 公司内部搭建自己的远程仓库,只在公司内部用,外网访问不到(到公司用这个多)

git使用流程

# https://www.cnblogs.com/liuqingzheng/p/15328319.html
# https://www.cnblogs.com/liuqingzheng/articles/17146214.html


# git 分3个区---》三个区的来回操作
	-工作区:存放代码的文件夹,只要工作区文件发生变(修改,新增,删除)---》
    -暂存区:工作的变更,提交到暂存区  git add . 把工作区变更提交到暂存区
    -版本库:暂存区内容,放到版本库,被版本管理---》git commit -m ''

git常用命令

# 0 再某个位置,右键---》git bash here ----》打开命令窗口---》等同于cmd---》在这个命令窗口里可以执行 linux命令,来操作win



# 1  初始化仓库,在某个文件夹下执行
	git init   # 在当前文件夹下就会创建出  .git 文件夹,这个就会被git管理
    git init xxx # 在当前路径下创建 xxx文件夹,并用git管理xxx文件夹
    
# 1.1 配置用户
#### 全局配置  以后所有的版本提交时,都用这个用户和邮箱--》C:\Users\oldboy\.gitconfig 
git config --global user.name '用户名'
git config --global user.email '用户邮箱'
#### 局部配置  只在当前 仓库生效--》仓库路径下  .git 文件夹下 config文件中配置的
git config user.name '用户名'
git config user.email '用户邮箱'
# 查看用户
git config -l


# 2 查看仓库状态
	git status  # 红  绿
    # 如果是红色,表明是在工作发生了变化,没有提交到暂存区
    # 如果是绿色:表明,暂存区数据没有提交到版本库
    # 如果没有东西,表示当前目录下所有文件被git管理了,被版本管理了
    
    
# 3 把工作区变更,提交到暂存区
	git add  .  # 当前目录下所有变更都提交
    git add 1.txt # 只提交当前目录下 1.txt这个文件的变更
   


# 4 把暂存区内容,提交到版本库(只要被版本管理的东西,你尽管操作,后期都能回退回来)
	git commit -m '我的第一次,提交'  # 如果不设置用户,提交不了,不知道是谁提交

# 5 查看版本信息(提交过哪些版本,注释是什么)【可以按作者,时间过滤】
    git log
    git relog
    
 ---------------了解-----------   
# 6 把工作区变更回退
git checkout . # 当前路径下所有

# 7 把暂存区内容,拉回到工作区(由绿变红)
git reset HEAD

# 8 从版本库拉回到暂存区(版本库内容回退,变绿)---》需要写上一个版本
git reset --soft  1603edf06d7d302ba50c22373c963af15725eda5


# 9 把版本库退回到工作区(版本库内容回退,变红)
git reset --mix 1603edf06d7d302ba50c22373c963af15725eda5

# 10 把版本库直接完整回退会工作区(增加的也没了)
git reset --hard 1603edf06d7d302ba50c22373c963af15725eda5


# 11 回退到某个版本的样子(可能会用)
git reset --hard 19f5891


# 总结:
 git add
 git commit -m ''



# 只要被版本管理的文件,即便以后删除了,也能回来

git忽略文件

# 写项目,会有一些文件或文件夹,不希望被git管理,忽略掉它, 不被git管理
	-.idea
    -node_models
    -xx
    
    
# 需要写个忽略文件 .gitignore   必须叫它,没有后缀名
在里面写忽略的文件或文件夹,写法如下
.idea   # 忽略idea文件夹及其下面所有的文件
lqz.txt  # 忽略仓库中所有的lqz.txt
/lqz.txt # 忽略当前路径下的lqz.txt
a/lqz.txt # 只忽略当前路径下a文件夹下lqz.txt
*x*:名字中有一个x的都会被过滤(*代表0~n个任意字符)


# 给路飞做忽略文件
.idea
*.log
__pycache__
*.pyc

scripts

git多分支

# 查看分枝
git branch # 查看本地分支
git branch -a # 查看所有分区本地与远程

# 创建分枝
git branch [分支名称]

# 删除分支
git branch -d [分支名称]

# 合并分支
	git branch dev
    git checkout dev
    新增一个文件 xx.txt,加入一行
    git add .
    git commit -m 'dev分支增加了xx.txt'
    修改lqz.txt  加入一样
    git add .
    git commit -m 'lqz.txt 加入内容'
    
    
    # 切回到主分支(增的东西,都没有)
    	# 新增的文件,看不到
   	# 把dev合并到master上,要在master身上
    git merge dev

git远程仓库

# 我们要协同开发,代码要提交到远程仓库,可以使用gitee,github,gitlab,本文以gitee为例
#1  以gitee为例,注册账号
	
#2 在账号中,新建一个仓库 [本地仓库,推送到远程仓库]
#3 如图所示  ----》[如果创建远程仓库不是空的,就会有问题]
#4 本地仓库,推到远端
	-本地已经有了
    # git remote 查看有哪些远程仓库
    # git remote remove origin   # 删除本地跟远程仓库的链接关系
    cd lqz
    # git remote add 远程仓库名字  远程仓库地址
	git remote add origin https://gitee.com/liuqingzheng/lqz.git
	git push origin master  # 把本地仓库中所有的内容,提交到远程仓库
    # 弹出框,要求输入用户名和密码(之前输入过,保存了)
    
    # 本地仓库代码就会被推送到远端了


ssh方式链接远程仓库

# 刚刚的远程仓库,推送,走的都是https的协议,需要用户名密码

# 加密方式
	-对称:AES,DES
   	-非对称:
# 公司里,常用ssh协议方式推送代码
	-git@gitee.com:liuqingzheng/lqz.git
    -它不需要用户名密码,而需要:公钥私钥【非对称加密】
    -在本地机器生成公钥[可以给任何人]和私钥[自己留着]
    
    ssh-keygen -t ed25519 -C "1660828294@qq.com"
# 具体操作
	-1 先删除原来使用https链接的remote
    -2 增加一个跟远程仓库的链接 origin ---》是ssh协议的
    	git remote add origin git@gitee.com:liuqingzheng/lqz.git
    -3 本地机器,生成公钥私钥[使用命令生成]
    	-https://help.gitee.com/base/account/SSH%E5%85%AC%E9%92%A5%E8%AE%BE%E7%BD%AE
    	-打开cmd 执行 ,一路回车
        	ssh-keygen -t ed25519 -C "1660828294@qq.com"
        - 用户家路径,生成 .ssh文件夹,里面有公钥和私钥
   - 4 把公钥配置在gitee上【打开公钥】--》可以配多个
		-https://gitee.com/profile/sshkeys
    
    -5 以后放心大胆的
    	git push origin master # 提交代码即可
    
    
# 你在公司中
	-1 用gitlab,可能你自己注册账号,也可能别人直接给你注册号了,你直接登录,修改密码即可
    -2 登录gitlab能看到代码仓库,仓库的所有者[你们领导],会把你加成这个仓库的【开发者】
    -3 基于这个仓库继续开发,提交代码
    -4 ssh方案:本地生成公钥私钥【本地有了,就不用重新生成了】
    -5 把公钥,配置在自己的gitlab账号里面
    -6 以后就免密对仓库有操作权限了
    

协同开发

# 仓库管理员[领导],创建仓库,邀请成员,成为开发者

# 被邀请的人,登录自己账号,就能看到仓库了

# 1 从远端克隆代码
# 2 进到文件夹中,改东西
# 3 git add .
# 4 git commit -m '改了一行'
# 5 git pull origin master  # 拉去仓库中最新的代码   否则提交不了
# 6 git push origin master


# 多人操作统一仓库,就是协同开发,但是咱们这个操作,没有遇到冲突

冲突解决

# 出现冲突的原因
	-1 多人在同一分支,修改了同一个地方的代码,出现的冲突
    -2 分支合并时出冲突
    
    
       
# 1 多人统一分支开发,修改了同样的代码
	
	-某人修改了1.txt的第四行,提交了
    -我操作:
    	-修改了1.txt第四行
        -git add .
        -git commit -m ' 注释'
        -git pull origin master
        -出冲突了
            <<<<<<< HEAD
            我的代码
            =======
            别人的代码
            >>>>>>> af38b6ae4d9e126bd88b9b039e475e8ddbc23510
	   -处理冲突
    		-选择要保留的代码,要么删自己的,要么删别人的,要么都留着
       -重复操作
    	git add .
        git commot -m '解决冲突' 
        git push origin master

# 大原则,多人同一分支开发,如果尽量避免冲突,要不停的拉去代码



# 分支合并出冲突
	  # 新建dev分支,切换,增加代码
    	git branch dev
        git checkout dev
        在1.txt最后一行增加 lqz nb1
        git add 
        git commit -m '注释'
      # 切换回主分支操作
    	git checkout master
         在1.txt最后一行增加 lqz nb2
        git add 
        git commit -m '注释'
      # 合并分支
        -出冲突了
            <<<<<<< master
            我的代码
            =======
            别人的代码
            >>>>>>> dev
      # 解决冲突,提交
    	git add
        git cmommit 

线上分支合并

# 本地分支合并----》git merge dev

# 有主分支----》开发分支开发完了----》合并到主分支


# 远端创建dev分支---》本地没有,拉去一下就有了 git pull origin dev
# 本地创建dev分支----》远端没有,推送一下就有了 git push origin dev

# 远端创建dev分支,拉去到本地
	-远端,在网页中点点点创建分支
    -本地:git pull origin dev
    -切换过去才能看到:git checkout dev
    
# 本地和远端现在都有了master和dev分支
	-本地的dev分支,删除东西
    -提交到本地版本库
    -推送到远程  git push origin dev
    
    -远程分支合并
    	-组员新建pull request---》pr---》(merge request)mr
        -组长审核---》同意---》dev就被合并到master

远程仓库回滚(你不要去做)

# 本地 
git reset --hard 最初状态
git reset --hard 88aa1e64fa288af495ab6c283b139b7f7f0a237a

git push origin master -f


# 本地代码要提交,本地版本库的内容必须是最新的,git pull 就是最新

为开源项目贡献代码

# 1  gitee 找一个开源项目
# 2 点 fork---》复制一份到你的仓库中
# 3 在咱们仓库中,clone---》修改代码---》提交代码---》自己仓库
# 4 在自己仓库中提交pr---》我们本地dev分支申请提交到作者的dev分支
#5 等作者审核过,同意,你就是贡献者了

git工作流,git pull和git fetch,变基

# git 工作流:git flow---》分支方案
	-我们没有采用
    
    
# git pull和git fetch
	-git pull 从远程仓库拉取代码:从远程获取最新版本并merge到本地
    -git fetch 从远程仓库拉取代码:会将数据拉取到本地仓库 - 它并不会自动合并或修改当前的工作
    -git pull =git fetch +merge
    
    
    
# 变基 rebase
	-1 多个提交记录整合成一个
    -2 解决多次合并分叉问题
	

pycharm操作git

# 实际开发中,可以完全一点命令都不敲,通过pycharm 点点点
# pycharm 配置好git

# clone 代码

# git add 命令

# git commit 
# git push
# git 分支操作

# 实用的,代码对比
	git reflog 。git log 命令

登录注册页面分析

# 根据原型图分析出:要写的功能
    # 用户名密码登录接口
    # 注册功能接口
    # 手机号验证码登录接口
    # 发送短信验证码接口
    # 验证手机号是否存在接口

验证手机号是否存在接口

class UserView(ViewSet):
    # 验证手机号是否存在接口---》get请求---》跟数据库有关系,但不需要序列化----》自动生成路由
    @action(methods=['GET'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        try:
            mobile = request.query_params.get('mobile', None)
            User.objects.get(mobile=mobile)  # 有且只有一条才不报错,如果没有或多余一条,就报错
            return APIResponse(msg='手机号存在')
        except Exception as e:
            raise APIException('手机号不存在')

腾讯云短信申请

# 发送短信功能
	-网上会有第三方短信平台,为我们提供api,花钱,向它的某个地址发送请求,携带手机号,内容---》它替我们发送短信
    
    -腾讯云短信---》以这个为例
    -阿里 大于短信
    -容联云通信
    
    
    
# 申请一个公众号---》自行百度---》个人账号



# 如何申请腾讯云短信
	-1 地址:https://cloud.tencent.com/act/pro/csms
        -2 登录后,进入控制台,搜短信https://console.cloud.tencent.com/smsv2
    -3 创建签名:使用公众号
    	-身份证,照片
    -4 模板创建
    -5 发送短信
    	-使用腾讯提供的sdk发送
        -https://cloud.tencent.com/document/product/382/43196
# 1 今天讲的所有操作一遍
# 2 写完手机号是否存在接口
# 3 手机号(用户名,邮箱)密码登录接口
# 4 申请腾讯云短信

---------------
# 借助于redis:安装()
	-https://github.com/tporadowski/redis/releases/
# 研究 websocket协议,集成都django项目,写一个聊天室
	https://zhuanlan.zhihu.com/p/371500343
    
# websocket 实现不了,就用定时器+http

后端登录注册接口

# 登录注册相关功能
	- 1 校验手机号是否存在(已经完成)
    - 2 多方式登录接口(手机号,邮箱,用户名都可以登录成功)
    - 3 发送短信接口(腾讯云短信)
    - 4 短信登录接口
    - 5 短信注册接口

视图类

from django.shortcuts import render, HttpResponse
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from django.core.cache import cache
# Create your views here.
from utils.common_logger import logger
from threading import Thread
from user.models import User
from utils.common_response import APIResponse
from rest_framework.exceptions import APIException
from libs.send_tx_sms import get_code, send_sms_by_phone
from .serialzier import LoginSerializer, LoginUserSMSSerializer, UserRegisterSerializer
from rest_framework.mixins import CreateModelMixin


class UserView(GenericViewSet):
    serializer_class = LoginSerializer

    """判断使用哪个序列化类"""
	
    def get_serializer_class(self):
        if self.action == 'sms_login':
            return LoginUserSMSSerializer
        elif self.action == 'register':
            return UserRegisterSerializer
        return self.serializer_class

    """多方式登录"""

    @action(methods=['POST'], detail=False)
    def login(self, request, *args, **kwargs):
        return self._common_login(request, *args, **kwargs)

    """短信登录"""

    @action(methods=['POST'], detail=False)
    def sms_login(self, request, *args, **kwargs):
        return self._common_login(request, *args, **kwargs)

    """判断手机号是否注册"""

    @action(methods=['GET'], detail=False)
    def check_mobile(self, request, *args, **kwargs):
        try:
            mobile = request.GET.get('mobile', None)
            User.objects.get(mobile=mobile)
            return APIResponse(msg='手机号已存在')
        except Exception as e:
            raise APIException('手机号不存在')

    """发送短信"""

    @action(methods=['GET'], detail=False)
    def send_sms(self, request):
        mobile = request.query_params.get('mobile', None)
        code = get_code()
        cache.set(f'send_sms_code_{mobile}', code)
        if mobile:                                                          # 开启线程,提高并发
            t = Thread(target=send_sms_by_phone, args=[mobile, code])
            t.start()
            return APIResponse(msg='信息已发送')
        raise APIException('请输入手机号')

    """隐藏属性:因为多方式登录与手机验证码登录的视图类中的代码重复,所以将重复的部分写成一个类的隐藏属性,在类中调用"""
    def _common_login(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        username = ser.context.get('username')
        token = ser.context.get('token')
        icon = ser.context.get('icon')
        return APIResponse(username=username, token=token, icon=icon)

    """注册"""

    # 如果继承CreateModelMixin注册可以不用写,但是上面序列化类的判断地址要改成self.action == create
    @action(methods=['POST'], detail=False)
    def register(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        ser.is_valid(raise_exception=True)
        ser.save()
        return APIResponse(msg='注册成功')

序列化类

from rest_framework import serializers
from .models import User
from django.contrib.auth import authenticate
from faker import Faker
from rest_framework_jwt.settings import api_settings
from rest_framework.exceptions import APIException
from django.conf import settings
from django.core.cache import cache
from faker import Faker
from django.contrib.auth.hashers import make_password

jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER


class CommonLoginSerializer():
    def _get_user(self, attrs):
        raise APIException('你必须重写')

    def _get_token(self, user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

    def validate(self, attrs):
        user = self._get_user(attrs)
        token = self._get_token(user)
        self.context['username'] = user.username
        self.context['icon'] = settings.BACKEND_URL + user.icon.url
        self.context['token'] = token
        return attrs

# 因为ModelSerializer中有全局钩子,所以要将CommonLoginSerializer写在前面,不然前端self.context获取不到数据
class LoginSerializer(CommonLoginSerializer, serializers.ModelSerializer):
    username = serializers.CharField()

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

    def _get_user(self, attrs):
        credentials = {
            'username': attrs.get('username'),
            'password': attrs.get('password')
        }
        user = authenticate(**credentials)
        if not user:
            raise APIException('用户名或密码错误')
        return user


class LoginUserSMSSerializer(CommonLoginSerializer, serializers.Serializer):
    mobile = serializers.CharField()
    code = serializers.CharField()

    def _get_user(self, attrs):
        mobile = attrs.get('mobile')
        code = attrs.get('code')

        old_code = cache.get(f'send_sms_code_{mobile}')
        if code != old_code:
            raise APIException('验证码错误')
        user = User.objects.filter(mobile=mobile).first()
        if not user:
            raise APIException('该手机没有注册')
        return user


class UserRegisterSerializer(serializers.ModelSerializer):
    """{"mobile":xxxx, "code":8888, "password":xxx}"""
    code = serializers.CharField(max_length=4, min_length=4, write_only=True)

    class Meta:
        model = User
        fields = ['mobile', 'password', 'code']

    """校验验证码"""

    def _check_code(self, attrs):
        mobile = attrs.get('mobile')
        code = attrs.get('code')

        old_code = cache.get(f'send_sms_code_{mobile}')
        if not (code == old_code or (settings.DEBUG or code == '8888')):   # 测试的时候可以使用万能验证码测试
            raise APIException('验证码错误')

    """生成随机用户名"""

    def _generate_user(self):
        fake = Faker()
        return fake.first_name()

    """处里入前的数据"""

    def _pre_save(self, attrs):
        attrs.pop('code')
        pwd = attrs.get('password')
        attrs['password'] = make_password(pwd)   # 对密码进行加密
        attrs['username'] = self._generate_user() # 短信注册的话,自动生成一个随机用户名

    def validate(self, attrs):
        self._check_code(attrs)
        self._pre_save(attrs)
        return attrs

    
    # 前端如果是继承CreateModelMixin,再处理数据的时候已经将密码加密的话就可以不用写create
    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
        return user

多方式登录重写authenticate

from django.contrib.auth.backends import ModelBackend
from django.db.models import Q


def get_account_by_user(username):
    from user.models import User
    try:

        user = User.objects.get(Q(username=username) | Q(mobile=username) | Q(email=username))
    except User.DoesNotExist:
        user = None
    return user


class UsernameMobileAuthBackend(ModelBackend):
    """实现用户多条件登录"""

    def authenticate(self, request, username=None, password=None, **kwargs):
        user = get_account_by_user(username)
        if user is not None and user.check_password(password) and user.is_active:
            return user

# 在配置文件中配置
AUTHENTICATION_BACKENDS = [
    'utils.authenticates.UsernameMobileAuthBackend'
]

发送短信

# 获取n位的随机验证码
import random

from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 导入对应产品模块的client models。
from tencentcloud.sms.v20210111 import sms_client, models

# 导入可选配置类
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from .settings import *


def get_code(num=4):
    code = ''
    for i in range(num):
        random_num = random.randint(0, 9)
        code += str(random_num)
    return code


# 发送短信函数
def send_sms_by_phone(mobile, code):
    try:

        cred = credential.Credential(SECRET_Id, SECRET_KEY)

        httpProfile = HttpProfile()

        httpProfile.reqMethod = "POST"
        httpProfile.reqTimeout = 30
        httpProfile.endpoint = "sms.tencentcloudapi.com"

        # 非必要步骤:
        # 实例化一个客户端配置对象,可以指定超时时间等配置
        clientProfile = ClientProfile()
        clientProfile.signMethod = "TC3-HMAC-SHA256"
        clientProfile.language = "en-US"
        clientProfile.httpProfile = httpProfile

        client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)

        req = models.SendSmsRequest()

        req.SmsSdkAppId = APP_ID

        req.SignName = SIGN_NAME

        req.TemplateId = TEMPLATED_ID

        req.TemplateParamSet = [code, '10']

        req.PhoneNumberSet = ["+86" + mobile]

        req.SessionContext = ""

        req.ExtendCode = ""
        req.SenderId = ""

        resp = client.SendSms(req)

        # 输出json格式的字符串回包
        res_dict = resp._serialize(allow_none=True)
        print(res_dict)
        if res_dict['SendStatusSet'][0]['Code'] == "Ok":
            return True
        else:
            return False

    except TencentCloudSDKException as err:
        return False

异步发送短信

# 原来的发送短信,是同步
	-前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》调用腾讯发送短信---》腾讯去发短信---》发完后----》回复给我们后端发送成功---》我们后端收到发送成功---》给我们前端返回发送成功
    
# 把腾讯发送短信的过程,变成异步
-前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》开启线程,去调用腾讯短信发送(异步)---》我们后端继续往后走----》直接返回给前端,告诉前端短信已发送
-另一条发短信线程线程会去发送短信,至于是否成功,我们不管了

    @action(methods=['GET'], detail=False)
    def send_sms(self, request):
        mobile = request.query_params.get('mobile', None)
        code = get_code()
        cache.set(f'send_sms_code_{mobile}', code)
        if mobile:                                                          # 开启线程,提高并发
            t = Thread(target=send_sms_by_phone, args=[mobile, code])
            t.start()
            return APIResponse(msg='信息已发送')
        raise APIException('请输入手机号')

前端注册页面分析

# 登录,注册,都写成组件----》在任意页面中,都能点击显示登录模态框
# 写好的组件,应该放在那个组件中----》不是页面组件(小组件)
# 点击登录按钮,把Login.vue 通过定位,占满全屏,透明度设为 0.5 ,纯黑色悲剧,覆盖在组件上
# 在Login.vue点关闭,要把Login.vue隐藏起来,父子通信之子传父,自定义事件

Header.vue

## 页面中
<span @click="go_login">登录</span>


<Login v-if="login_show" @close_login="close_login"></Login>
## data
login_show: false
### methods
   go_login() {
      this.login_show = true
    },
    close_login() {
      this.login_show = false
    }

Login.vue

<template>
  <div class="login">
    <span @click="close">X</span>

  </div>
</template>

<script>
export default {
  name: "Login",
  methods: {
    close() {
      this.$emit('close_login')
    }
  }

}
</script>

<style scoped>
.login {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  background-color: rgba(0, 0, 0, 0.5);
}
</style>

前端登录注册页面复制

login.vue

<template>
  <div class="login">
    <div class="box">
      <i class="el-icon-close" @click="close_login"></i>
      <div class="content">
        <div class="nav">
         <span :class="{active: login_method === 'is_pwd'}" @click="change_login_method('is_pwd')">密码登录</span>
          <span :class="{active: login_method === 'is_sms'}" @click="change_login_method('is_sms')">短信登录</span>
        </div>
        <el-form v-if="login_method === 'is_pwd'">
          <el-input
              placeholder="用户名/手机号/邮箱"
              prefix-icon="el-icon-user"
              v-model="username"
              clearable>
          </el-input>
          <el-input
              placeholder="密码"
              prefix-icon="el-icon-key"
              v-model="password"
              clearable
              show-password>
          </el-input>
          <el-button type="primary">登录</el-button>
        </el-form>
        <el-form v-if="login_method === 'is_sms'">
          <el-input
              placeholder="手机号"
              prefix-icon="el-icon-phone-outline"
              v-model="mobile"
              clearable
              @blur="check_mobile">
          </el-input>
          <el-input
              placeholder="验证码"
              prefix-icon="el-icon-chat-line-round"
              v-model="sms"
              clearable>
            <template slot="append">
              <span class="sms" @click="send_sms">{{ sms_interval }}</span>
            </template>
          </el-input>
          <el-button type="primary">登录</el-button>
        </el-form>
        <div class="foot">
          <span @click="go_register">立即注册</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Login",
  data() {
    return {
      username: '',
      password: '',
      mobile: '',
      sms: '',
      login_method: 'is_pwd',
      sms_interval: '获取验证码',
      is_send: false,
    }
  },
  methods: {
    close_login() {
      this.$emit('close')
    },
    go_register() {
      this.$emit('go')
    },
    change_login_method(method) {
      this.login_method = method;
    },
    check_mobile() {
      if (!this.mobile) return;
      if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
        this.$message({
          message: '手机号有误',
          type: 'warning',
          duration: 1000,
          onClose: () => {
            this.mobile = '';
          }
        });
        return false;
      }
      this.is_send = true;
    },
    send_sms() {

      if (!this.is_send) return;
      this.is_send = false;
      let sms_interval_time = 60;
      this.sms_interval = "发送中...";
      let timer = setInterval(() => {
        if (sms_interval_time <= 1) {
          clearInterval(timer);
          this.sms_interval = "获取验证码";
          this.is_send = true; // 重新回复点击发送功能的条件
        } else {
          sms_interval_time -= 1;
          this.sms_interval = `${sms_interval_time}秒后再发`;
        }
      }, 1000);
    }
  }
}
</script>

<style scoped>
.login {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  background-color: rgba(0, 0, 0, 0.5);
}

.box {
  width: 400px;
  height: 420px;
  background-color: white;
  border-radius: 10px;
  position: relative;
  top: calc(50vh - 210px);
  left: calc(50vw - 200px);
}

.el-icon-close {
  position: absolute;
  font-weight: bold;
  font-size: 20px;
  top: 10px;
  right: 10px;
  cursor: pointer;
}

.el-icon-close:hover {
  color: darkred;
}

.content {
  position: absolute;
  top: 40px;
  width: 280px;
  left: 60px;
}

.nav {
  font-size: 20px;
  height: 38px;
  border-bottom: 2px solid darkgrey;
}

.nav > span {
  margin: 0 20px 0 35px;
  color: darkgrey;
  user-select: none;
  cursor: pointer;
  padding-bottom: 10px;
  border-bottom: 2px solid darkgrey;
}

.nav > span.active {
  color: black;
  border-bottom: 3px solid black;
  padding-bottom: 9px;
}

.el-input, .el-button {
  margin-top: 40px;
}

.el-button {
  width: 100%;
  font-size: 18px;
}

.foot > span {
  float: right;
  margin-top: 20px;
  color: orange;
  cursor: pointer;
}

.sms {
  color: orange;
  cursor: pointer;
  display: inline-block;
  width: 70px;
  text-align: center;
  user-select: none;
}
</style>

Register.vue

<template>
    <div class="register">
        <div class="box">
            <i class="el-icon-close" @click="close_register"></i>
            <div class="content">
                <div class="nav">
                    <span class="active">新用户注册</span>
                </div>
                <el-form>
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary">注册</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_login">立即登录</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Register",
        data() {
            return {
                mobile: '',
                password: '',
                sms: '',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_register() {
                this.$emit('close', false)
            },
            go_login() {
                this.$emit('go')
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                this.is_send = true;
            },
            send_sms() {
                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            }
        }
    }
</script>

<style scoped>
    .register {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 480px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 240px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin-left: 90px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>

Header.vue

 <div class="right-part">
        <div>
          <span @click="put_login">登录</span>
           <span class="line">|</span>
          <span @click="put_register">注册</span>

        </div>
        <Login v-if="is_login" @close="close_login" @go="put_register"/>
        <Register v-if="is_register" @close="close_register" @go="put_login"/>
      </div>
    
    

    
    ## data
      is_login: false,
      is_register: false,
    
    
    # methods
    put_login() {
      this.is_login = true;
      this.is_register = false;
    },
    put_register() {
      this.is_login = false;
      this.is_register = true;
    },
    close_login() {
      this.is_login = false;
    },
    close_register() {
      this.is_register = false;
    }
    

前端登录功能

# 1  多方式登录的axios请求,保存cookie---》Header.vue中要写created 生命周期,取出当前登录用户--》close_login取出当前登录用户
# 2 手机号验证码登录,axios请求,保存cookie
# 3 发送短信验证码
<template>
  <div class="login">
    <div class="box">
      <i class="el-icon-close" @click="close_login"></i>
      <div class="content">
        <div class="nav">
          <span :class="{active: login_method === 'is_pwd'}" @click="change_login_method('is_pwd')">密码登录</span>
          <span :class="{active: login_method === 'is_sms'}" @click="change_login_method('is_sms')">短信登录</span>
        </div>
        <el-form v-if="login_method === 'is_pwd'">
          <el-input
              placeholder="用户名/手机号/邮箱"
              prefix-icon="el-icon-user"
              v-model="username"
              clearable>
          </el-input>
          <el-input
              placeholder="密码"
              prefix-icon="el-icon-key"
              v-model="password"
              clearable
              show-password>
          </el-input>
          <el-button type="primary" @click="handleLogin">登录</el-button>
        </el-form>
        <el-form v-if="login_method === 'is_sms'">
          <el-input
              placeholder="手机号"
              prefix-icon="el-icon-phone-outline"
              v-model="mobile"
              clearable
              @blur="check_mobile">
          </el-input>
          <el-input
              placeholder="验证码"
              prefix-icon="el-icon-chat-line-round"
              v-model="sms"
              clearable>
            <template slot="append">
              <span class="sms" @click="send_sms">{{ sms_interval }}</span>
            </template>
          </el-input>
          <el-button type="primary" @click="handleSmSLogin">登录</el-button>
        </el-form>
        <div class="foot">
          <span @click="go_register">立即注册</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Login",
  data() {
    return {
      username: '',
      password: '',
      mobile: '',
      sms: '',
      login_method: 'is_pwd',
      sms_interval: '获取验证码',
      is_send: false,   // 如果是true,就是能发送验证码,是false就不能发送验证码
    }
  },
  methods: {
    close_login() {
      this.$emit('close')
    },
    go_register() {
      this.$emit('go')
    },
    change_login_method(method) {
      this.login_method = method;
    },
    check_mobile() {
      if (!this.mobile) return;
      if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
        this.$message({
          message: '手机号有误',
          type: 'warning',
          duration: 1000,
          onClose: () => {
            this.mobile = '';
          }
        });
        return false;
      }

      // 跟后端交互,查询该手机号是否注册过,如果注册过,才能发送
      this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/check_mobile/?mobile=${this.mobile}`).then(res => {
        if (res.data.code != 100) {
          // 提示该手机号未注册
          this.$message({
            message: '该手机号未注册,请先注册',
            type: 'success'
          });
          // 把手机号清空
          this.mobile = ''
        } else {
          this.is_send = true;   // 可以发送验证码
        }
      })


    },
    // 发送手机验证码
    send_sms() {

      if (!this.is_send) return;  // 如果是false,不能点击发送
      this.is_send = false;    // 点了一次,不能再点了,1 m,定时器执行完 后才能再点
      let sms_interval_time = 60;
      this.sms_interval = "发送中...";
      let timer = setInterval(() => {
        if (sms_interval_time <= 1) {
          clearInterval(timer);
          this.sms_interval = "获取验证码";
          this.is_send = true; // 是true表示,能够点击发送验证码按钮
        } else {
          sms_interval_time -= 1;
          this.sms_interval = `${sms_interval_time}秒后再发`;
        }
      }, 1000);

      // 调用接口发送验证码
      this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/send_sms/?mobile=${this.mobile}`).then(res => {
        this.$message({
          message: res.data.msg,
          type: 'success'
        });
      })

    },

    // 用户名密码登录
    handleLogin() {
      if (this.username && this.password) {
        this.$axios.post(`${this.$settings.BASE_URL}user/userinfo/mul_login/`, {
          username: this.username,
          password: this.password
        }).then(res => {
          if (res.data.code == 100) {
            // 登录成功 返回了token,用户名,头像--->把他们存到cookie中
            this.$cookies.set('token', res.data.token, '7d')
            this.$cookies.set('username', res.data.username, '7d')
            this.$cookies.set('icon', res.data.icon, '7d')
            // 页面关闭,子传父,调用 自定义事件close,就会触发父组件中的close_login方法
            this.$emit('close')
          } else {
            this.$message({
              message: res.data.msg,
              type: 'warning'
            });
          }
        })
      } else {
        this.$message({
          message: '用户名或密码不能为空',
          type: 'warning'
        });
      }
    },
    // 验证码登录
    handleSmSLogin() {
      if (this.mobile && this.sms) {
        this.$axios.post(`${this.$settings.BASE_URL}user/userinfo/sms_login/`, {
          mobile: this.mobile,
          code: this.sms
        }).then(res => {
          if (res.data.code == 100) {
            this.$cookies.set('token', res.data.token, '7d')
            this.$cookies.set('username', res.data.username, '7d')
            this.$cookies.set('icon', res.data.icon, '7d')
            this.$emit('close')
          } else {
            this.$message({
              message: res.data.msg,
              type: 'warning'
            });
          }
        })
      } else {
        this.$message({
          message: '用户名或密码不能为空',
          type: 'warning'
        });
      }
    }
  }
}
</script>

<style scoped>
.login {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  background-color: rgba(0, 0, 0, 0.5);
}

.box {
  width: 400px;
  height: 420px;
  background-color: white;
  border-radius: 10px;
  position: relative;
  top: calc(50vh - 210px);
  left: calc(50vw - 200px);
}

.el-icon-close {
  position: absolute;
  font-weight: bold;
  font-size: 20px;
  top: 10px;
  right: 10px;
  cursor: pointer;
}

.el-icon-close:hover {
  color: darkred;
}

.content {
  position: absolute;
  top: 40px;
  width: 280px;
  left: 60px;
}

.nav {
  font-size: 20px;
  height: 38px;
  border-bottom: 2px solid darkgrey;
}

.nav > span {
  margin: 0 20px 0 35px;
  color: darkgrey;
  user-select: none;
  cursor: pointer;
  padding-bottom: 10px;
  border-bottom: 2px solid darkgrey;
}

.nav > span.active {
  color: black;
  border-bottom: 3px solid black;
  padding-bottom: 9px;
}

.el-input, .el-button {
  margin-top: 40px;
}

.el-button {
  width: 100%;
  font-size: 18px;
}

.foot > span {
  float: right;
  margin-top: 20px;
  color: orange;
  cursor: pointer;
}

.sms {
  color: orange;
  cursor: pointer;
  display: inline-block;
  width: 70px;
  text-align: center;
  user-select: none;
}
</style>

前端注册功能

<template>
  <div class="register">
    <div class="box">
      <i class="el-icon-close" @click="close_register"></i>
      <div class="content">
        <div class="nav">
          <span class="active">新用户注册</span>
        </div>
        <el-form>
          <el-input
              placeholder="手机号"
              prefix-icon="el-icon-phone-outline"
              v-model="mobile"
              clearable
              @blur="check_mobile">
          </el-input>
          <el-input
              placeholder="密码"
              prefix-icon="el-icon-key"
              v-model="password"
              clearable
              show-password>
          </el-input>
          <el-input
              placeholder="验证码"
              prefix-icon="el-icon-chat-line-round"
              v-model="sms"
              clearable>
            <template slot="append">
              <span class="sms" @click="send_sms">{{ sms_interval }}</span>
            </template>
          </el-input>
          <el-button type="primary" @click="handleRegister">注册</el-button>
        </el-form>
        <div class="foot">
          <span @click="go_login">立即登录</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Register",
  data() {
    return {
      mobile: '',
      password: '',
      sms: '',
      sms_interval: '获取验证码',
      is_send: false,
    }
  },
  methods: {
    close_register() {
      this.$emit('close', false)
    },
    go_login() {
      this.$emit('go')
    },
    check_mobile() {
      if (!this.mobile) return;
      if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
        this.$message({
          message: '手机号有误',
          type: 'warning',
          duration: 1000,
          onClose: () => {
            this.mobile = '';
          }
        });
        return false;
      }

      // 校验手机号是否能发验证码---》只有没注册过的,才能发
      this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/check_mobile/?mobile=${this.mobile}`).then(res => {
        if (res.data.code != 100) {
          // 提示该手机号未注册
          this.$message({
            message: '该手机号未注册,可以发送短信',
            type: 'success'
          });
          this.is_send = true;   // 可以发送验证码
        } else {
          // 手机号注册过了,不用注册了,直接登录即可
          this.$message({
            message: '该手机号已经注册过,可以直接登录',
            type: 'success',
            onClose: () => {
              // 跳转到登录页面
              this.$emit('go')
            }
          });


        }
      })

    },
    send_sms() {
      if (!this.is_send) return;
      this.is_send = false;
      let sms_interval_time = 60;
      this.sms_interval = "发送中...";
      let timer = setInterval(() => {
        if (sms_interval_time <= 1) {
          clearInterval(timer);
          this.sms_interval = "获取验证码";
          this.is_send = true; // 重新回复点击发送功能的条件
        } else {
          sms_interval_time -= 1;
          this.sms_interval = `${sms_interval_time}秒后再发`;
        }
      }, 1000);

      // 调用接口发送验证码
      this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/send_sms/?mobile=${this.mobile}`).then(res => {
        this.$message({
          message: res.data.msg,
          type: 'success'
        });
      })
    },
    handleRegister() {
      if (this.mobile && this.sms && this.password) {
        this.$axios.post(`${this.$settings.BASE_URL}user/userinfo/register/`, {
          mobile: this.mobile,
          code: this.sms,
          password: this.password
        }).then(res => {
          if (res.data.code == 100) {
            // 注册成功
            this.$message({
              message: res.data.msg,
              type: 'success'
            });
            // 跳转到登录--->写自定义事件的名字
            this.$emit('go')
          }
        })
      } else {
        this.$message({
          message: '手机号,验证码,密码不能为空',
          type: 'success'
        });
      }
    }
  }
}
</script>

<style scoped>
.register {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  background-color: rgba(0, 0, 0, 0.3);
}

.box {
  width: 400px;
  height: 480px;
  background-color: white;
  border-radius: 10px;
  position: relative;
  top: calc(50vh - 240px);
  left: calc(50vw - 200px);
}

.el-icon-close {
  position: absolute;
  font-weight: bold;
  font-size: 20px;
  top: 10px;
  right: 10px;
  cursor: pointer;
}

.el-icon-close:hover {
  color: darkred;
}

.content {
  position: absolute;
  top: 40px;
  width: 280px;
  left: 60px;
}

.nav {
  font-size: 20px;
  height: 38px;
  border-bottom: 2px solid darkgrey;
}

.nav > span {
  margin-left: 90px;
  color: darkgrey;
  user-select: none;
  cursor: pointer;
  padding-bottom: 10px;
  border-bottom: 2px solid darkgrey;
}

.nav > span.active {
  color: black;
  border-bottom: 3px solid black;
  padding-bottom: 9px;
}

.el-input, .el-button {
  margin-top: 40px;
}

.el-button {
  width: 100%;
  font-size: 18px;
}

.foot > span {
  float: right;
  margin-top: 20px;
  color: orange;
  cursor: pointer;
}

.sms {
  color: orange;
  cursor: pointer;
  display: inline-block;
  width: 70px;
  text-align: center;
  user-select: none;
}
</style>

# 2 写个短信登录注册接口(只需要传手机号和验证码,生成一个默认密码,以短信形式通知用户)
# 3 登录接口,如果是用默认密码,不允许登录
# 4 记录三次用户密码修改记录,每次改密码,不能跟之前用过的相同

Redis安装合介绍

# Redis :软件,存储数据的,速度非常快,redis是一个key-value存储系统(没有表的概念),cs架构的软件
	-服务端  客户端(python作为客户端,java,go,图形化界面,命令窗口的命令)
# es:存数据的地方

# 关系型数据库和非关系型数据库
	-关系型:mysql,PostgreSQL,oracle,sqlserver,db2
    	-PG
        -去 IOE:国产化
        	-IBM---》浪潮信息,曙光,联想
            -Oracle---》数据----》达梦。。。。
            -EMC存储--》国产存储
    -非关系型数据库(nosql):redis(缓存),mongodb(json文档数据存储),es(大数据量存储)。。。。
    	-nosql 指非关系型数据库: no only sql,对关系型数据库的补充
        
# redis特点:
	-开源软件,存数据,cs架构
    -key-value存储 ,5大数据类型  value的类型是5种:字符串,hash(字典),列表,集合,有序集合
    -速度快:
    	-1 纯内存存储(核心)
        -2 使用了IO多路复用的网络模型
        -3 数据操作是单线程,避免了线程间切换,而且没有锁,也不会数据错乱
    -支持持久化
    	-纯内存,可以存到硬盘上,防止数据丢失
    -redis又被称之为 缓存数据库

安装redis

# redis 是用c语言编写的,需要在不同平台编译成可执行文件,才能在这个平台上运行
	-redis 使用了io多路复用种的epoll模型,win不支持epoll
    -redis官方,不支持win版本
    -微软官方,就把redis改动,编译成可执行,能运行在win上,滞后 3.x版本
    -第三方:5.x版本
    
# redis 官方网:https://redis.io/download/
# redis中文网:http://redis.cn
# win:3.x:https://github.com/microsoftarchive/redis/releases
# win:5.x:https://github.com/tporadowski/redis/releases/


# 安装:一路下一步
	-安装完成后,在安装路径下有
    	-redis-cli.exe     # mysql
        -redis-server.exe   # mysqld
        -redis.windows-service.conf  # my.ini
     -并且会自动做成服务
    	-服务的命令:redis-server.exe  redis.windows-service.conf
        
        
        
        
# 启动redis服务端
	-1 命令行中  redis 就可以启动服务
    -2 命令行中,启动服务,并指定配置文件
    	redis-server 配置文件路径
    -3 使用服务启动

# 客户端链接
	-1 命令行客户端:
    	-redis-cli  # 默认连本地的6379端口
        -redis-cli -p 6379 -h 127.0.0.1
    -2 图形化客户端链接
    	-1 最新版的Navicate支持链接redis了(收费的)
        -2 Redis Desktop Manager(https://resp.app/)  收费的  用的多  qt写图形化界面
        	-qt是个平台,做GUI[图形化界面]开发
            -用c写,用python写  pyqt5
            
    -3 python的模块
    	-pip install redis
        

redis的五大数据类型

# string:字符
# list:列表/数组
# set:集合
# zset:有序集合
# hash:哈希

redis普通链接和连接池

from redis import Redis

# conn = Redis()  # 建立redis的链接
conn = Redis(host="127.0.0.1",
             port=6379,
             db=0,decode_responses=True)  # 建立redis的链接   decode_responses=True,查询回来返回的结果是字符串类型,否则是byte格式
res = conn.get('name')  # 获取key名为name的value值
print(res)
conn.close()  # 关闭链接

连接池链接

# 一定要保证,池是单例的,以模块导入的形式做成了单例

# pool.py
import redis
POOL = redis.ConnectionPool(max_connections=1000,host='127.0.0.1',port=6379)

# 其它.py

import redis
from pool import POOL # 模块导入的方式, 天然单例
conn = redis.Redis(connection_pool=POOL)  # 以后拿到链接,是从POOL种取,如果没有可用的了,默认不阻塞,可以通过某个参数配置,设置阻塞等待
res = conn.get('name')  # 获取key名为name的value值
print(res)
conn.close()  # 关闭链接

redis字符串类型

import redis

conn = redis.Redis(decode_responses=True)
对象操作Redis

conn.close()

1. set(name, value, ex=None, px=None, nx=False, xx=False)

在Redis中设置值,默认,不存在则创建,存在则修改
参数:
     ex,过期时间(秒)
     px,过期时间(毫秒)
     nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果
     xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值
        
2. setnx(name, value)
设置值,只有name不存在时,执行设置操作(添加),如果存在,不会修改
# 相当于: set('test', 'gjl', nx=True)

3. setex(name, value, time)
# 设置值
# 参数:
     time,过期时间(数字秒 或 timedelta对象)
# 相当于: set('test', 'gjl', ex=3)

4. psetex(name, time_ms, value)
# 设置值
# 参数:
     time_ms,过期时间(数字毫秒 或 timedelta对象
# 相当于: set('test', 'gjl', px=3000)


5. mset(*args, **kwargs)
# 批量设置值
	mset({'name': 'ggg', 'age': 18})
    
6. get(name)
# 获取值
conn.get('name')
# 获取的值是需要解码的,或者配置参数decode_responses=True

7. mget(keys, *args)
# 批量获取
conn.mget('name', 'age')

8. getset(name, value)
# 设置新值并获取原来的值
conn.getset('name', 'test')

9. getrange(key, start, end)
# 获取子序列(根据字节获取,非字符)
conn.getrange('name', 0, 2)
# 获取的是字节,不是字符

10. setrange(name, offset, value)
# 修改字符串内容,从指定字符串索引开始向后替换(新值太长时,则向后添加)
conn.setrange('name', 2, 'gjl')

11. strlen(name)
# 返回name对应值的字节长度(一个汉字3个字节)
conn.strlen('name')

12. incr(name, amount=1)
# 自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
conn.incr('name', 2)
# 参数:
     name,Redis的name
     amount,自增数(必须是整数)

13. incrbyfloat(name, amount=1.0)
# 自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
# 参数:
     name,Redis的name
     amount,自增数(浮点型)
        
        
14. decr(self, name, amount=1)
# 自减 name对应的值,当name不存在时,则创建name=amount,否则,则自减。
conn.decr('name', 4)
# 参数:
     name,Redis的name
     amount,自减数(整数)
        
15. append(key, value)
# 在redis name对应的值后面追加内容
conn.append('name', 'xxx')

"""
 重点掌握:
 	get、set、append、strlen
"""

redis hash类型

8import redis

conn = redis.Redis(decode_responses=True)
对象操作Redis

conn.close()



1. hset(name, key, value)
# name对应的hash中设置一个键值对(不存在,则创建;否则,修改)
 conn.hset('userinfo', 'name', 'gg')
    
# 参数:
     name,redis的name
     key,name对应的hash中的key
     value,name对应的hash中的value
 
# 注:
     hsetnx(name, key, value),当name对应的hash中不存在当前key时则创建(相当于添加)
    
# 批量设置
conn.hset('userinfo', mapping={'name': 'ggg', 'age': 19})

2. hget(name,key)
# 在name对应的hash中获取根据key获取value
conn.hget('userinfo', 'name')

3. hmget(name, keys, *args)
# 在name对应的hash中获取多个key的值
conn.hmget('userinfo', ['name', 'age'])

4. hgetall(name)
# 获取name对应hash的所有键值
conn.hgetall('userinfo')
# 尽量不也要用,获取数据如果过大会撑爆内存

5. hlen(name)
# # 获取name对应的hash中键值对的个数
conn.hlen('userinfo')

6. hkeys(name)
# 获取name对应的hash中所有的key的值
conn.hkeys('userinfo')

7. hvals(name)
# 获取name对应的hash中所有的value的值
conn.hvals('userinfo')

8. hexists(name, key)
# 检查name对应的hash是否存在当前传入的key
conn.hexists('userinfo', 'name')

9. hdel(name,*keys)
# 将name对应的hash中指定key的键值对删除
conn.hdel('userinfo', 'name')

10. hincrby(name, key, amount=1)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
# 参数:
     name,redis中的name
     key, hash对应的key
     amount,自增数(整数)
conn.hincrby('userinfo', 'name')

11. hincrbyfloat(name, key, amount=1.0)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
 
# 参数:
     name,redis中的name
     key, hash对应的key
     amount,自增数(浮点数
    
    
12. hscan(name, cursor=0, match=None, count=None)
# 增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆
 
# 参数:
     name,redis的name
     cursor,游标(基于游标分批取获取数据)
     match,匹配指定key,默认None 表示所有的key
     count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
        
# 如:
    # 第一次:cursor1, data1 = r.hscan('xx', cursor=0, match=None, count=None)
    # 第二次:cursor2, data1 = r.hscan('xx', cursor=cursor1, match=None, count=None)
    # ...
    # 直到返回值cursor的值为0时,表示数据已经通过分片获取完毕
    
13. hscan_iter(name, match=None, count=None)
# 利用yield封装hscan创建生成器,实现分批去redis中获取数据
 
# 参数:
    # match,匹配指定key,默认None 表示所有的key
    # count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
 
# 如:
    # for item in r.hscan_iter('xx'):
    #     print item
    
    
    
"""
 重点掌握:
 	
"""

Redis 列表类型List

'''添加到最左边相当于从索引为0的位置往里插入,添加到最右边相当于从尾部追加,在Redis可视化软件中,是将列表竖着显示'''
1. lpush(name,values)
# 在name对应的list中添加元素,每个新的元素都添加到列表的最左边
res = conn.lpush('hobby', '乒乓球', '台球')
# 保存顺序为: '台球','乒乓球' 

2. rpush(name, values)
# 在name对应的list中添加元素,表示从右向左操作
res = conn.rpush('hobby', '冰球', '保龄球')
# 保存顺序为:'冰球', '保龄球'

3. lpushx(name,value)
# 在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边
conn.lpushx('hobby', '123')


4. rpushx(name,value)
# 在name对应的list中添加元素,只有name已经存在时,值添加到列表的最右边
conn.rpushx('hobby', '123')

5. llen(name)
# name对应的list元素的个数
conn.llen('hobby')

6. linsert(name, where, refvalue, value)
# 在name对应的列表的某一个值前或后插入一个新值
# 参数:
     name,redis的name
     where,before/after (小写也可以)
     refvalue,标杆值,即:在它前后插入数据(如果存在多个标杆值,以找到的第一个为准)
     value,要插入的数据
conn.linsert('hobby', 'after', '足球', '333')        
# 在key值为hobby中,values值为足球的后面插入一个333

7. lset(name, index, value)
# 对name对应的list中的某一个索引位置重新赋值
# 参数:
     name,redis的name
     index,list的索引位置
     value,要设置的值
conn.lset('hobby', 4, '排球')
# 将索引为4的值重新赋值为排球

8. lrem(name, value, num)
# 在name对应的list中删除指定的值
# 参数:
     name,redis的name
     value,要删除的值
     num,  num=0,删除列表中所有的指定值;
            num=2,从前到后,删除2个;
            num=-2,从后向前,删除2个
            
conn.lrem('hobby', 0, '排球')   # 删除所有值为排球的
conn.lrem('hobby', 2, '冰球')   # 在Redis可视化工具中,从上往下删除两个值为冰球的
conn.lrem('hobby', -2, '保龄球') # 在Redis可视化工具中,从下往上删除两个值为保龄球的

9. lpop(name)
# 在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素
conn.lpop('hobby') # 从左侧弹出

conn.rpop('hobby') # 从右侧弹出

10. lindex(name, index)
# 在name对应的列表中根据索引获取列表元素
conn.lindex('hobby', 2)


11. lrange(name, start, end)
# # 在name对应的列表分片获取数据
# 参数:
     name,redis的name
     start,索引的起始位置
     end,索引结束位置 
    
# 搭配llen使用
conn.lrange('hobby', 0, coon.llen('aa'))

12. ltrim(name, start, end)
# 在name对应的列表中移除没有在start-end索引之间的值

# 参数:
     name,redis的name
     start,索引的起始位置
     end,索引结束位置(大于列表长度,则代表不移除任何)
conn.ltrim('hobby', 0, 2)

13. rpoplpush(src, dst)
# 从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边

# 参数:
     src,要取数据的列表的name
     dst,要添加数据的列表的name
        
conn.rpoplpush('hobby', 'db1')
# 将key值为hobby中最右的一个值取出并添加到key值为db1的最左边

14. blpop(keys, timeout)
# 将多个列表排列,按照从左到右去pop对应列表的元素
# 参数:
     keys,redis的name的集合
     timeout,超时时间,当元素所有列表的元素获取完之后,阻塞等待列表内有数据的时间(秒), 0 表示永远阻塞
        
conn.blpop('hobby')
# 有值是会顺利执行,没有值时会一直阻塞,所以要设置timeout时间

15. brpoplpush(src, dst, timeout=0)
# 从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧
# 参数:
    src,取出并要移除元素的列表对应的name
    dst,要插入元素的列表对应的name
    timeout,当src对应的列表中没有数据时,阻塞等待其有数据的超时时间(秒),0 表示永远阻塞
    
'''
重点掌握:
        lpush
        lpop
        linsert
        lset
        llen
        lrange
'''
    
    
    
############################################自定义增量迭代######################################################
# 使用lrange与llen可以将列表所有的数据取出,但是面临一个问题就是,如果数据量过大将会撑爆整个内存,所以才会自定义一个迭代器

import redis

conn = redis.Redis(decode_responses=True)


def test(key, count):
    num = 0
    while True:
        print('=========================')
        res = conn.lrange(key, num, num + count + 1)
        num = num + count
        if res:
            for item in res:
                yield item
        else:
            break


for item in test('eggs', 10):
    print(item)
conn.close()

redis其它操作

1. delete(*names)
# 根据删除redis中的任意数据类型
conn.delete('eggs')

2. exists(name)
# 检测redis的name是否存在
conn.exists('name3')
# 返回值:如果存在返回1,不存在返回0

3. keys(pattern='*')
# 根据模型获取redis的key
# 更多:
     KEYS * 匹配数据库中所有 key 。
     KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
     KEYS h*llo 匹配 hllo 和 heeeeello 等。
     KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 
  
conn.keys('name?')
# 可以使用正则匹配

4. expire(name ,time)
# 为某个redis的某个name设置超时时间
conn.expire('name', 3)

5. rename(src, dst)
# 对redis的name重命名为
conn.rename('name1', 'name')

6. move(name, db)
# 将redis的某个值移动到指定的db下
conn.move('name', 1)
# 将key值为name移动到db1下

7. randomkey()
# 随机获取一个redis的name(不删除)
conn.randomkey()

8. type(name)
# 获取name对应值的类型
conn.type('nnn')

redis管道

# 事务四大特性
	-原子性:要么都成功,要么都失败
    -一致性:数据前后要一致
    -隔离性:多个事务之间相互不影响
    -持久性:事务一旦完成,数据永久改变
    
    
# redis 有没有事务?支持事务
	-redis要支持事务,要完成事务的几大特性,需要使用管道来支持
    -单实例redis是支持管道的
    -集群模式下,不支持管道,就不支持事务

# redis通过管道实现事务
import redis
conn = redis.Redis()
pipline = conn.pipeline(transaction=True)
pipline.decrby('a1', 10)  # 没有真正执行,把命令先放到管道中
raise Exception('出错了')
pipline.incrby('a2', 10)

pipline.execute()  # 把管道中的命令,一次性执行
conn.close()
	

django中使用redis

# 两种方式
	-方式一:自定义的通用方案(跟框架无关)
    	-写一个py文件:redis_pool.py
        	import redis
            POOL=redis.ConnectionPool(max_connections=10)
        -在用的位置,导入直接使用
             conn = redis.Redis(connection_pool=POOL)
        	conn.incrby('a1')
            
            
            
    -django中有个模块,django-redis,方便我们快速集成redis
    	-1 下载:pip install django-redis
        -2 配置文件配置:
            CACHES = {
                "default": {
                    "BACKEND": "django_redis.cache.RedisCache",
                    "LOCATION": "redis://127.0.0.1:6379",
                    "OPTIONS": {
                        "CLIENT_CLASS": "django_redis.client.DefaultClient",
                        "CONNECTION_POOL_KWARGS": {"max_connections": 100}
                        # "PASSWORD": "123",
                    }
                }
            }
            
        -3 在使用的地方,导入直接使用
        from django_redis import get_redis_connection
        class MyResponseView(APIView):
            def get(self, request):
                conn = get_redis_connection()  # 从连接池中拿出一个链接
                conn.incrby('a1')
                conn.set('name','彭于晏')
                return APIResponse()
        

django缓存

# django 是大而全的框架,内置了很多web开发需要的东西,缓存内置了

# 缓存:可以把django中的一个变量(数据),存放到某个位置,下次还可以取出来

# 之前用过:默认放在:内存中,其实可以放在文件中,数据库,redis。。。。
from django.core.cache import cache
cache.set('key','value',5) # 存放值
res=cache.get('key') # 取值


# 通过配置,控制存放在哪,只要如下写,就会放在redis中
   CACHES = {
       "default": {
           "BACKEND": "django_redis.cache.RedisCache",
           "LOCATION": "redis://127.0.0.1:6379",
           "OPTIONS": {
               "CLIENT_CLASS": "django_redis.client.DefaultClient",
               "CONNECTION_POOL_KWARGS": {"max_connections": 100}
               # "PASSWORD": "123",
           }
       }
   }

# django缓存最强大之处在于,只要是python的变量,任意类型都可以,尽管使用set设置值
       l = [1, 'lqz', [1, 3, 4, 5, 6], '彭于晏']
       cache.set('ll1', l)
	
# 以后再django中往redis放数据,就用cache即可

# redis的5大数据类型,只支持一层


# 看一下这篇文章:https://www.cnblogs.com/liuqingzheng/articles/9803351.html

# 超过5k就要交税
	-公司正常报税,工资中扣
    -6月份开始工作,按1w工资报税, 退税

补充

#序列化
	-json序列化---》得到字符串
    	json不能序列化对象(自定义的类的对象) 
        	-数据结构:数据的组织形式跟下面不一样
        能序列化: 数字,字符串,布尔,列表,字典  时间对象
    -pickle序列化
    	-python独有的,二进制形式
        -python可以序列化所有对象---》二进制形式
        -二进制---》返序列化回来---》对象:属性,有方法

celery介绍和安装

# celery :分布式的异步任务框架,主要用来做:
	- 异步任务
    - 延迟任务
    - 定时任务---》如果只想做定时任务,可以不使用celery,有别的选择
    
# celery 框架,原理
1)可以不依赖任何服务器,通过自身命令,启动服务(内部支持socket)
2)celery服务为为其他项目服务提供异步解决任务需求的
注:会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要时异步完成项目的需求

人是一个独立运行的服务 | 医院也是一个独立运行的服务
	正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
	人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求
    
    
# celery架构
消息中间件(broker):消息队列:可以使用redis,rabbitmq,咱们使用redis
任务执行单元(worker):真正的执行 提交的任务
任务执行结果存储(banckend):可以使用mysql,redis,咱们使用redis

    
# 安装celery
	-pip install Celery
    -释放出可执行文件:celery,由于 python解释器的script文件夹再环境变量,任意路径下执行celery都能找到
    
    
    
# celery不支持win,所以想再win上运行,需要额外安装eventlet
windows系统需要eventlet支持:pip3 install eventlet
Linux与MacOS直接执行:
	3.x,4.x版本:celery worker -A demo -l info
    5.x版本:     celery -A demo worker -l info -P eventlet

celery架构

celery执行异步任务

# 基本使用
	1 在啥都没,虚拟环境中装celery和eventlet
    2 写个py文件,实例化得到app对象,注册任务
        from celery import Celery
        import time
        broker = 'redis://127.0.0.1:6379/1'  # 消息中间件 redis
        backend = 'redis://127.0.0.1:6379/2'  # 结果存储 redis
        app = Celery(__name__, broker=broker, backend=backend) # 指定消息队列中间件的地址,指定存储结果的地址
        @app.task # 变成celery的任务了
        def add(a, b):
            print('运算结果是',a + b)
            time.sleep(1)
            return a + b
    3 启动worker(worker监听消息队列,等待别人提交任务,如果没有就卡再这)
    	 celery -A demo worker -l info -P eventlet
        
    4 别人 提交任务,提交完成会返回一个id号,后期使用id号查询,至于这个任务有没有被执行,取决于worker有没有启动
    	from demo import add
		res=add.delay(77,66)
        
    5 提交任务的人,再查看结果
    	from demo import app
        # celery的包下
        from celery.result import AsyncResult

        id = '042a8fc1-6b0f-4ad6-bf72-edefa657a52f'
        if __name__ == '__main__':
            a = AsyncResult(id=id, app=app)
            if a.successful():  # 正常执行完成
                result = a.get()  # 任务返回的结果
                print(result)
            elif a.failed():
                print('任务失败')
            elif a.status == 'PENDING':
                print('任务等待中被执行')
            elif a.status == 'RETRY':
                print('任务异常后正在重试')
            elif a.status == 'STARTED':
                print('任务已经开始被执行')

包架构封装

project
    ├── celery_task  	# celery包
    │   ├── __init__.py # 包文件
    │   ├── celery.py   # celery连接和配置相关文件,且名字必须交celery.py
    │   └── tasks.py    # 所有任务函数
    ├── add_task.py  	# 添加任务
    └── get_result.py   # 获取结果
1. 新建包:celery_task
2. 在包下新建celery.py必须叫这个
    from celery import Celery
    broker = 'redis://127.0.0.1:6379/1'  # 消息中间件 redis
    backend = 'redis://127.0.0.1:6379/2'  # 结果存储 redis
app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.celery_add'])
    # include里写任务的路劲
    
    
3.  新建任务py文件:celery_add
    from .celery import app
    @app.task
    def add(a, b):
        print('运算结果:', a + b)
        return True

4 启动worker,以包启动,来到包所在路径下
     celery -A 包名 worker -l info -P eventlet
     celery -A celery_task worker -l info -P eventlet
        
5 其它程序,导入任务,提交任务即可
        from celery_task.user_task import send_sms
        res = celery_add.delay(1, 2)
        print(res)  # f33ba3c5-9b78-467a-94d6-17b9074e8533
        
        
        
6 其它程序,查询结果
    from celery_task.celery import app
    # celery的包下
    from celery.result import AsyncResult

    id = '51a669a3-c96c-4f8c-a5fc-e1b8e2189ed0'
    if __name__ == '__main__':
        a = AsyncResult(id=id, app=app)
        if a.successful():  # 正常执行完成
            result = a.get()  # 任务返回的结果
            print(result)
        elif a.failed():
            print('任务失败')
        elif a.status == 'PENDING':
            print('任务等待中被执行')
        elif a.status == 'RETRY':
            print('任务异常后正在重试')
        elif a.status == 'STARTED':
            print('任务已经开始被执行')

celery执行延迟任务和定时任务

# celery 可以做
	-异步任务
    -延迟任务---》延迟多长时间干任务
    -定时任务:每天12点钟,每隔几秒。。。
   		-如果只做定时任务,不需要使用celery这么重,apscheduler(自己去研究)
        

        
 # 异步任务
	-导入异步任务的函数
    -函数.delay(参数)
 # 延迟任务
	-导入异步任务的函数
    -函数.apply_async(kwargs={'mobile':'1896334234','code':8888},eta=时间对象)

   '''
   
   from datetime import datetime, timedelta
   eta = datetime.utcnow() + timedelta(seconds=10)
   
   datetime.utcnow():当前时间对象(utc时间)
   timedelta(seconds=10):10秒,使用他就可以和datetime相加
   '''
    
# 定时任务:在app所在的文件下配置
	- 1 配置
	app.conf.beat_schedule = {
        'send_sms': {
            'task': 'celery_task.user_task.send_sms',
            'schedule': timedelta(seconds=5),
            'args': ('1822344343', 8888),
        },
        'add_course': {
            'task': 'celery_task.course_task.add_course',
            # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
            'schedule': crontab(hour=11, minute=38),  # 每天11点35,执行
            'args': (),
        }
    }
	-2 启动beat,启动worker
	celery  -A celery_task beat -l info 
    -3 到了时间,beat进程负责提交任务到消息队列---》worker执行

django中使用celery

# 使用步骤
	1 把之前写好的包,copy到项目根路径下
    2 在xx_task.py 中写任务
    	from .celery import app
        @app.task
        def add_banner():
            from home.models import Banner
            Banner.objects.create(title='测试', image='/1.png', link='/banner', info='xxx',orders=99)
            return 'banner增加成功'
        
   3 在celery.py 中加载django配置
	# 一、加载django配置环境
    import os
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")

    4 视图函数中,导入任务,提交即可
    	class CeleryView(APIView):
            def get(self, request):
                res = add_banner.delay()
                return APIResponse(msg='新增banner的任务已经提交了')
    5 启动worker,等待运行即可
    
    
'''
celery中要使用djagno的东西,才要加这句话    
	import os
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")

'''

接口缓存

# 所有接口都可以改造,尤其是查询所有的这种接口,如果加入缓存,会极大的提高查询速度

# 首页轮播图接口:获取轮播图数据,加缓存---》咱们只是以它为例


class BannerView(GenericViewSet, ListModelMixin):
    queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    serializer_class = BannerSerializer

    def list(self, request, *args, **kwargs):
        '''
          1 先去缓存中查一下有没有数据
          2 如果有,直接返回,不走父类的list了(list在走数据库)
          3 如果没有,走父类list,查询数据库
          4 把返回的数据,放到缓存中
        '''

        data = cache.get('home_banner_list')
        if not data:  # 缓存中没有
            print('走了数据库')
            res = super().list(request, *args, **kwargs)  # 查询数据库
            # 返回的数据,放到缓存中
            data = res.data.get('data')  # {code:100,msg:成功,data:[{},{}]}
            cache.set('home_banner_list', data)
        return APIResponse(data=data)


    
# 公司里可能会这么写
	-写一个查询所有带缓存的基类
    -写个装饰器,只要一配置,就自动带缓存
    
    
# 双写一致性问题:缓存数据和数据库数据不一致了
	-写入数据库,删除缓存
    -写入数据库,更新缓存
    -定时更新缓存

双写一致性

# 只要将数据放入缓存就,就会出现数据不一致的情况

# 解决方案:
	- 修改数据时,删除缓存
	- 修改数据时,跟新缓存
	- 定时跟新缓存
    
# 首页轮播图存在双写一致性问题这个问题
解决方案:定时任务,定时跟新


from celery import Celery

# 使用django的配置文件先导入
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")

broker = 'redis://127.0.0.1:6379/1'
backend = 'redis://127.0.0.1:6379/2'
app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.home_task'])

### 3 定时任务要写在这里
# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False

# 任务的定时配置
from datetime import timedelta

app.conf.beat_schedule = {
    'update_banner': {
        'task': 'celery_task.home_task.update_banner',
        'schedule': timedelta(seconds=10),  # 每10秒执行一次任务
        'args': (),
    },
}


# 任务
from .celery import app
from home.serialzier import BannerSerializer
from django.core.cache import cache
from home.models import Banner
from django.conf import settings


@app.task
def update_banner():
    banner_list = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
    ser = BannerSerializer(instance=banner_list, many=True)
    # 如果在视图类中,做序列化 ,因为视图类中有request对象,所以像图片这种,会自动加前面地址,
    # 在这里没有request对象,需要手动拼
    for i in ser.data:
        i['image'] = settings.BACKEND_URL + i['image']
    cache.set('home_banner_list', ser.data)
    return True



# 视图
    def list(self, request, *args, **kwargs):
        data = cache.get('home_banner_list')
        if not data:
            res = super().list(request, *args, **kwargs)
            data = res.data.get('data')
            cache.set('home_banner_list', data)
        return APIResponse(data=data)

异步发送短信

# 视图函数
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):
    mobile = request.query_params.get('mobile', None)
    code = get_code()  # 把code存起来,放到缓存中,目前在内存,后期换别的
    cache.set('send_sms_code_%s' % mobile, code)
    if mobile:
        res = send_sms.delay(mobile,code)
        print(res)
        return APIResponse(msg='短信已发送')
    raise APIException('手机号没有携带')

# 任务
from libs.send_tx_sms import send_sms_by_mobile
@app.task
def send_sms(mobile, code):
    res=send_sms_by_mobile(mobile,code)
    if res:
        return '%s的短信发送成功,验证码是%s'%(mobile,code)
    else:
        return '%s的短信发送失败'%mobile

异步秒杀逻辑前后端

前端

<template>
  <div>
    <h2>go语言从入门到放弃</h2>
    <el-button type="danger" @click="handleSckill">秒杀</el-button>

  </div>
</template>

<script>
export default {
  name: "Sckill",
  data() {
    return {
      t: null,
      task_id: '',
    }
  },
  methods: {
    handleSckill() {
      this.$axios.post(`${this.$settings.BASE_URL}user/sckill/`, {
        name: "性感帽子",
      }).then(res => {
        if (res.data.code == 100) {
          alert('您正在排队')
          this.task_id = res.data.task_id    # task_id是任务的id,让定时任务定时发axios请求去后端拿数据
          // 起个转圈的东西
          // 起定时任务
          this.t = setInterval(() => {
            this.$axios.get(`${this.$settings.BASE_URL}user/sckill/?task_id=${this.task_id}`).then(res => {
              if (res.data.code == 100 || res.data.code == 101) {
                this.$message(res.data.msg)
                //销毁定时器
                clearInterval(this.t)
                this.t = null
              } else {
                console.log('过会再查')
              }
            })
          }, 3000)
        }
      })
    }
  }
}
</script>

<style scoped>

</style>

后端

视图
from celery_task.user_task import sckill_goods
from celery_task.celery import app
from celery.result import AsyncResult


class SckillView(APIView):
    def post(self, request, *args, **kwargs):
        name = request.data.get('name')
        # 提交秒杀异步任务
        res = sckill_goods.delay(name)
        return APIResponse(task_id=str(res))

    def get(self, request, *args, **kwargs):
        task_id = request.GET.get('task_id')
        a = AsyncResult(id=task_id, app=app)
        if a.successful():  # 正常执行完成
            result = a.get()  # 任务返回的结果
            if result:
                return APIResponse(code=100, msg='秒杀成功')
            else:
                return APIResponse(code=101, msg='秒杀失败')
        elif a.status == 'STARTED':
            print('任务已经开始被执行')
            return APIResponse(code=103, msg='还在排队')
        else:
            return APIResponse(code=102, msg='没成功')

路由
urlpatterns = [
    path('sckill/', SckillView.as_view()),
]
任务
@app.task
def sckill_goods(name):
    # 逻辑是:开启事务---》扣减库存---》生成订单
    import time
    time.sleep(6)
    res = random.choice([100, 102])
    if res == 100:
        print('%s被秒杀成功了' % name)
        return True
    else:
        print('%s被秒杀失败了' % name)
        return False

课程页页面前端

#1 前端  新建三个组件
	LightCourse.vue
    FreeCourse.vue
    ActualCourse.vue
    
# 2 配置路由
	import FreeCourse from "@/views/FreeCourse";
    import ActualCourse from "@/views/ActualCourse";
    import LightCourse from "@/views/LightCourse";
    
     {
        path: '/free-course',
        name: 'free-course',
        component: FreeCourse
    },
    {
        path: '/actual-course',
        name: 'actual-course',
        component: ActualCourse
    },
    {
        path: '/light-course',
        name: 'light-course',
        component: LightCourse
    },

FreeCourse.vue


<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">¥6.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>

建立表

from django.db import models

from utils.common_models import BaseModel


# 课程分类
class CourseCategory(BaseModel):
    """分类   id   name """
    name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")

    class Meta:
        db_table = "luffy_course_category"
        verbose_name = "课程分类"
        verbose_name_plural = verbose_name

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


# 课程表
class Course(BaseModel):
    """课程"""
    course_type = (
        (0, '付费'),
        (1, 'VIP专享'),
    )
    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="课程状态")
    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)

    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
                                        blank=True, verbose_name="课程分类")

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

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


# 章节
class CourseChapter(BaseModel):
    """章节"""
    '''
    基于对象的跨表查询:正:字段        反:表名小写(一对一是  表名小写,一对多   表名小写_set.all())
        -related_name='coursechapters'   基于对象跨表查反向查询,替换表名小写
    基于双下划线的连表查询:正:字段    反:表名小写
        -related_query_name='xxx'     基于连表查反向查询,替换表名小写

    '''
    # related_name='coursechapters'
    course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE,
                               verbose_name="课程名称")
    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)

    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)


# 课时
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="课时标题")
    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)


# 老师表
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

课程表数据录入


INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2022-07-14 13:44:19.661327', '2022-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教学总监', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 国内知名PYTHON语言推广者 51CTO学院2016\2017年度最受学员喜爱10大讲师之一 多款开源软件作者 曾任职公安部、飞信、中金公司、NOKIA中国研究院、华尔街英语、ADVENT、汽车之家等公司');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2022-07-14 13:45:25.092902', '2022-07-14 13:45:25.092936', 'Mjj', 0, '前美团前端项目组架构师', NULL, 'teacher/mjj_icon.png', '是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2022-07-14 13:46:21.997846', '2022-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux学科带头人', NULL, 'teacher/lyy_icon.png', 'Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸');


-- 分类表
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2022-07-14 13:40:58.690413', '2022-07-14 13:40:58.690477', 'Python');

INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2022-07-14 13:41:08.249735', '2022-07-14 13:41:08.249817', 'Linux');


-- 课程表
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2022-07-14 13:54:33.095201', '2022-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/alex_python.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2022-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2022-07-14 13:56:05.051103', '2022-07-14 13:56:05.051142', 'Python项目实战', 'courses/mjj_python.png', 0, '', 1, '2022-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2022-07-14 13:57:21.190053', '2022-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/lyy_linux.png', 0, '', 0, '2022-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);


-- 章节表
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2022-07-14 13:58:34.867005', '2022-07-14 14:00:58.276541', 1, '计算机原理', '', '2022-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2022-07-14 13:58:48.051543', '2022-07-14 14:01:22.024206', 2, '环境搭建', '', '2022-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2022-07-14 13:59:09.878183', '2022-07-14 14:01:40.048608', 1, '项目创建', '', '2022-07-14', 2);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2022-07-14 13:59:37.448626', '2022-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2022-07-14', 3);


-- 课时表
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2022-07-14 14:02:33.779098', '2022-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2022-07-14 14:02:33.779193', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2022-07-14 14:02:56.657134', '2022-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2022-07-14 14:02:56.657227', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2022-07-14 14:03:20.493324', '2022-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2022-07-14 14:03:20.493420', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2022-07-14 14:03:36.472742', '2022-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2022-07-14 14:03:36.472831', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2022-07-14 14:04:19.338153', '2022-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2022-07-14 14:04:19.338252', 1, 3);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2022-07-14 14:04:52.895855', '2022-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2022-07-14 14:04:52.895942', 1, 4);

课程主页接口

# 课程分类接口

# 查询所有课程接口
	-带过滤
    -带排序
    -带分页

视图类

from .models import CourseCategory
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from .serializer import CourseCategorySerializer
from utils.common_mixin import CommonListModelMixin as ListModelMixin

class CourseCategoryView(GenericViewSet, ListModelMixin):
    # 查询所有
    queryset = CourseCategory.objects.filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = CourseCategorySerializer

序列化类

class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseCategory
        fields = ['id','name']

路由

from django.contrib import admin
from django.urls import path

from rest_framework.routers import SimpleRouter

router = SimpleRouter()
from .views import CourseCategoryView

router.register('category', CourseCategoryView, 'category')
urlpatterns = [

]
urlpatterns += router.urls

查询所有课程接口

# 查询所有课程接口
	-带过滤:按分类过滤
    -带排序:价格,学习人数
    -带分页:简单分页

视图类

class CourseView(GenericViewSet, ListModelMixin):
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = CourseSerializer
    pagination_class = CommonPagination
    # 排序,过滤课程分类(course_category=2)
    filter_backends = [OrderingFilter, DjangoFilterBackend]
    ordering_fields = ['students', 'price']
    filterset_fields = ['course_category']

序列化类

class TeacherSerializer(serializers.ModelSerializer):
    class Meta:
        model = Teacher
        fields = ['id', 'name', 'role_name', 'title', 'signature', 'image', 'brief']


# 只用来做序列化
class CourseSerializer(serializers.ModelSerializer):
    teacher = TeacherSerializer() # 子序列化

    class Meta:
        model = Course
        fields = [
            'id',
            'name',
            'course_img',
            'brief',
            'attachment_path',  # 课件地址
            'pub_sections',  # 发布了多少课时
            'sections',  # 总共有多少
            'price',
            'students',
            'period',  # 建议学习周期

            'course_type_name',  # 课程类型 中文
            'level_name',  # 课程难度等级中文
            'status_name',  # 课程状态中文

            'teacher',  # 重写 返回老师的详情
            'section_list',  # 重写的字段,拿最多四个课时,超过4个就拿4个,不足4个,有几个拿几个
        ]

表模型

class Course(BaseModel):
    """课程"""
    course_type = (
        (0, '付费'),
        (1, 'VIP专享'),
    )
    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="课程状态")
    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)

    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
                                        blank=True, verbose_name="课程分类")

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

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

    def course_type_name(self):
        return self.get_course_type_display()  # 拿到course_type对应的中文

    def level_name(self):
        return self.get_level_display()

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

    def section_list(self):
        l = []
        # 超过4个就拿4个,不足4个,有几个拿几个
        # 根据课程,获取所有章节

        course_chapter_list = self.coursechapters.all()  # 咱们写了related_name,直接按字段名
        # 循环所有章节,拿出所有课时,追加到l中
        for course_chapter in course_chapter_list:
            chapter_section_list = course_chapter.coursesections.all()
            for chapter_section in chapter_section_list:
                l.append({'id': chapter_section.pk,
                          'name': chapter_section.name,
                          'section_link': chapter_section.section_link,
                          'duration': chapter_section.duration,
                          'free_trail': chapter_section.free_trail,
                          })
                # 当l长度大于等于4,就直接返回
                if len(l) >= 4:
                    return l

        return l

课程详情接口

# 课间,增加了些数据


# 多种写法
	-方式一:直接在原来查询所有的视图类上加入继承:RetrieveModelMixin
    	-继续使用查询所有的序列化类(没有章节及课时)
        -配合一个通过课程id,查询所有课程章节及课时的接口
        
     -方式二:直接在原来查询所有的视图类上加入继承:RetrieveModelMixin
    	-重写序列化类:返回课程详情,返回课程章节及课时

        
        
# 我们采用方式一:只要在CourseView加继承RetrieveModelMixin类即可,就有查询单条的接口了
class CourseView(GenericViewSet, ListModelMixin,RetrieveModelMixin):
    
# 方式二逻辑:
视图类中判断是使用哪个序列化类(self.action='Retrieve'),序列化类的字段

class CourseSerializer(serializers.ModelSerializer):
    teacher = TeacherSerializer()

    class Meta:
        model = Course
        fields = [
            'id',
            'name',  # 课程名称
            'course_img',  # 封面图片
            'course_type_name',  # 付费类型
            'price',  # 价格
            'attachment_path',  # 课件路径
            'status_name',  # 课程状态
            'students',  # 学习人数
            'sections',  # 总课时数量
            'teacher',  #
            'pub_sections',  # 课时更新数量
            'period',  # 建议学习周期
            'brief',
            'level_name',
            'chapter_list',
            # chapter_list字段是课程套课时
        ]

课程详情后台之所有章节接口

# 该接口是为了配合 :课程详情页面写
# 写的接口是查询所有章节及课时
	-需要安装课程id过滤
# 写查询所有章节接口(带过滤)

视图类

class CourseChapterView(GenericViewSet, ListModelMixin):
    serializer_class = CourseChapterSerializer
    queryset = CourseChapter.objects.filter(is_delete=False, is_show=True).order_by('orders')
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['course']  # 按课程过滤

序列化类

class CourseSectionSerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseSection
        fields = ['id', 'name', 'section_type_name',
                  'section_link',
                  'duration',
                  'free_trail',]



class CourseChapterSerializer(serializers.ModelSerializer):
    '''
        拿出章节下所有课时,方案有三种
         -表模型中写
         -序列化类中写
         -方式三子序列化
    coursesections = CourseSectionSerializer(many=True)  # 章节下的课时,是多条,使用子序列化,instance不用传,但是many=True必须传

    class Meta:
        model = CourseChapter
        fields = ['id', 'name', 'chapter', 'summary', 'coursesections']

所有课程前台,课程详情前台

# 0 查询所有课程分类接口
# 1 查询所有课程(过滤,排序,分页)
# 2 查询课程详情
# 3 查询所有章节及其下的课时,带按课程id过滤功能
	-配合查询课程详情页面使用的

课程列表页

<template>
  <div class="course">
    <Header></Header>
    <div class="main">
      <!-- 筛选条件 -->
      <div class="condition">
        <ul class="cate-list">
          <li class="title">课程分类:</li>
          <li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li>
          <li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"
              @click="filter.course_category=category.id" :key="category.name">{{ category.name }}
          </li>
        </ul>

        <div class="ordering">
          <ul>
            <li class="title">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
            <li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''"
                @click="filter.ordering='-id'">默认
            </li>
            <li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''"
                @click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人气
            </li>
            <li class="price"
                :class="filter.ordering=='price'?'price_up this':(filter.ordering=='-price'?'price_down this':'')"
                @click="filter.ordering=(filter.ordering=='-price'?'price':'-price')">价格
            </li>
          </ul>
          <p class="condition-result">共{{ course_total }}个课程</p>
        </div>

      </div>
      <!-- 课程列表 -->
      <div class="course-list">
        <div class="course-item" v-for="course in course_list" :key="course.name">
          <div class="course-image">
            <img :src="course.course_img" alt="">
          </div>
          <div class="course-info">
            <h3>
              <router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link>
              <span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3>
            <p class="teather-info">
              {{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}
              <span v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{ course.pub_sections }}课时</span>
              <span v-else>共{{ course.sections }}课时/更新完成</span>
            </p>
            <ul class="section-list">
              <li v-for="(section, key) in course.section_list" :key="section.name"><span
                  class="section-title">0{{ key + 1 }}  |  {{ section.name }}</span>
                <span class="free" v-if="section.free_trail">免费</span></li>
            </ul>
            <div class="pay-box">
              <div v-if="course.discount_type">
                <span class="discount-type">{{ course.discount_type }}</span>
                <span class="discount-price">¥{{ course.real_price }}元</span>
                <span class="original-price">原价:{{ course.price }}元</span>
              </div>
              <span v-else class="discount-price">¥{{ course.price }}元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
      </div>
      <div class="course_pagination block">
        <el-pagination
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :current-page.sync="filter.page"
            :page-sizes="[3, 5]"
            :page-size="filter.page_size"
            layout="sizes, prev, pager, next"
            :total="course_total">
        </el-pagination>
      </div>
    </div>
    <Footer></Footer>
  </div>
</template>

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

export default {
  name: "Course",
  data() {
    return {
      category_list: [], // 课程分类列表
      course_list: [],   // 课程列表
      course_total: 0,   // 当前课程的总数量
      filter: {
        course_category: 0, // 当前用户选择的课程分类,刚进入页面默认为全部,值为0
        ordering: "-id",    // 数据的排序方式,默认值是-id,表示对于id进行降序排列
        page_size: 3,       // 单页数据量
        page: 1,
      }
    }
  },
  created() {
    this.get_category();
    this.get_course();
  },
  components: {
    Header,
    Footer,
  },
  watch: {
    "filter.course_category": function () {
      this.filter.page = 1;
      this.get_course();
    },
    "filter.ordering": function () {
      this.get_course();
    },
    "filter.page_size": function () {
      this.get_course();
    },
    "filter.page": function () {
      this.get_course();
    }
  },
  methods: {

    handleSizeChange(val) {
      // 每页数据量发生变化时执行的方法
      this.filter.page = 1;
      this.filter.page_size = val;
    },
    handleCurrentChange(val) {
      // 页码发生变化时执行的方法
      this.filter.page = val;
    },
    get_category() {
      // 获取课程分类信息
      this.$axios.get(`${this.$settings.BASE_URL}course/category/`).then(response => {
        if (response.data.code == 100) {
          this.category_list = response.data.data;
          console.log(this.category_list)
        }

      }).catch(() => {
        this.$message({
          message: "获取课程分类信息有误,请联系客服工作人员",
        })
      })
    },
    get_course() {
      // 排序
      let filters = {
        ordering: this.filter.ordering, // 排序
      };
      // 判决是否进行分类课程的展示
      if (this.filter.course_category > 0) {
        filters.course_category = this.filter.course_category;
      }

      // 设置单页数据量
      if (this.filter.page_size > 0) {
        filters.page_size = this.filter.page_size;
      } else {
        filters.page_size = 5;
      }

      // 设置当前页码
      if (this.filter.page > 1) {
        filters.page = this.filter.page;
      } else {
        filters.page = 1;
      }


      // 获取课程列表信息
      this.$axios.get(`${this.$settings.BASE_URL}course/course/`, {
        params: filters
      }).then(response => {
        if (response.data.code == 100) {
          this.course_list = response.data.data.results;
          this.course_total = response.data.data.count;
        }

      }).catch(() => {
        this.$message({
          message: "获取课程信息有误,请联系客服工作人员"
        })
      })
    }
  }
}
</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 .ordering .price_up::before {
  border-bottom-color: #ffc210;
}

.course .ordering .price_down::after {
  border-top-color: #ffc210;
}

.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 {
  max-width: 100%;
  max-height: 210px;
}

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

.course-item .course-info h3 a {
  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 .section-list::after {
  content: "";
  display: block;
  clear: both;
}

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

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

.course-item .section-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 .section-list li:hover .free {
  color: #ffc210;
  border-color: #ffc210;
}

.course-item {
  position: relative;
}

.course-item .pay-box {
  position: absolute;
  bottom: 20px;
  width: 600px;
}

.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;
  position: absolute;
  right: 0;
  bottom: 5px;
}

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

.course .course_pagination {
  margin-bottom: 60px;
  text-align: center;
}
</style>

课程详情页面

# 视频播放器,可以使用h5自带的video标签
	-画面比例,清晰度
    -倍速播放
    -静音
    -暂停可以插广告。。。
# 使用第三方:vue-video-player
cnpm install -S vue-core-video-player 

# mian.js
import VueCoreVideoPlayer from 'vue-core-video-player'
//或者
Vue.use(VueCoreVideoPlayer, {
  lang: 'zh-CN'
})
<template>
  <div class="detail">
    <Header/>
    <div class="main">
      <div class="course-info">
        <div class="wrap-left">
          <vue-core-video-player :src="mp4_url"
                                 :muted="true"
                                 :autoplay="false"
                                 title="致命诱惑"
                                 preload="nona"
                                 :loop="true"
                                 controls="auto"
                                 cover='https://img1.wxzxzj.com/vpc-example-cover-your-name-c.png'
                                 @loadedmetadata="onLoaded"
                                 @play="onPlayerPlay"
                                 @pause="onPlayerPause"
          ></vue-core-video-player>
        </div>
        <div class="wrap-right">
          <h3 class="course-name">{{ course_info.name }}</h3>
          <p class="data">
            {{ course_info.students }}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{
              course_info.sections
            }}课时/{{ course_info.pub_sections }}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{ course_info.level_name }}</p>
          <div class="sale-time">
            <p class="sale-type">价格 <span class="original_price">¥{{ course_info.price }}</span></p>
            <p class="expire"></p>
          </div>
          <div class="buy">
            <div class="buy-btn">
              <button class="buy-now">立即购买</button>
              <button class="free">免费试学</button>
            </div>
            <!--<div class="add-cart" @click="add_cart(course_info.id)">-->
            <!--<img src="@/assets/img/cart-yellow.svg" alt="">加入购物车-->
            <!--</div>-->
          </div>
        </div>
      </div>
      <div class="course-tab">
        <ul class="tab-list">
          <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
          <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
          </li>
          <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
          <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
        </ul>
      </div>
      <div class="course-content">
        <div class="course-tab-list">
          <div class="tab-item" v-if="tabIndex==1">
            <div class="course-brief" v-html="course_info.brief_text"></div>
          </div>
          <div class="tab-item" v-if="tabIndex==2">
            <div class="tab-item-title">
              <p class="chapter">课程章节</p>
              <p class="chapter-length">共{{ course_chapters.length }}章 {{ course_info.sections }}个课时</p>
            </div>
            <div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
              <p class="chapter-title"><img src="@/assets/img/enum.svg"
                                            alt="">第{{ chapter.chapter }}章·{{ chapter.name }}
              </p>
              <ul class="section-list">
                <li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
                  <p class="name"><span class="index">{{ chapter.chapter }}-{{ section.orders }}</span>
                    {{ section.name }}<span class="free" v-if="section.free_trail">免费</span></p>
                  <p class="time">{{ section.duration }} <img src="@/assets/img/chapter-player.svg"></p>
                  <button class="try" v-if="section.free_trail">立即试学</button>
                  <button class="try" v-else>立即购买</button>
                </li>
              </ul>
            </div>
          </div>
          <div class="tab-item" v-if="tabIndex==3">
            用户评论
          </div>
          <div class="tab-item" v-if="tabIndex==4">
            常见问题
          </div>
        </div>
        <div class="course-side">
          <div class="teacher-info">
            <h4 class="side-title"><span>授课老师</span></h4>
            <div class="teacher-content">
              <div class="cont1">
                <img :src="course_info.teacher.image">
                <div class="name">
                  <p class="teacher-name">{{ course_info.teacher.name }}
                    {{ course_info.teacher.title }}</p>
                  <p class="teacher-title">{{ course_info.teacher.signature }}</p>
                </div>
              </div>
              <p class="narrative">{{ course_info.teacher.brief }}</p>
            </div>
          </div>
        </div>
      </div>
    </div>
    <Footer/>
  </div>
</template>

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


export default {
  name: "Detail",
  data() {
    return {
      tabIndex: 2,   // 当前选项卡显示的下标
      course_id: 0, // 当前课程信息的ID
      course_info: {
        teacher: {},
      }, // 课程信息
      course_chapters: [], // 课程的章节课时列表
      mp4_url: 'https://video.pearvideo.com/mp4/short/20230703/cont-1784316-71095491-hd.mp4',
    }
  },
  created() {
    this.get_course_id();
    this.get_course_data();
    this.get_chapter();
  },
  methods: {
    onLoaded() {
      console.log('视频播放第一帧')
    },
    onPlayerPlay() {
      // 当视频播放时,执行的方法
      console.log('视频开始播放')
    },
    onPlayerPause() {
      // 当视频暂停播放时,执行的方法
      console.log('视频暂停,可以打开广告了')
    },
    get_course_id() {
      // 获取地址栏上面的课程ID
      this.course_id = this.$route.params.id
      if (this.course_id < 1) {
        let _this = this;
        _this.$alert("对不起,当前视频不存在!", "警告", {
          callback() {
            _this.$router.go(-1);
          }
        });
      }
    },
    get_course_data() {
      // ajax请求课程信息
      this.$axios.get(`${this.$settings.BASE_URL}course/course/${this.course_id}/`).then(response => {
        this.course_info = response.data.data;
        console.log(this.course_info)
      }).catch(() => {
        this.$message({
          message: "对不起,访问页面出错!请联系客服工作人员!"
        });
      })
    },

    get_chapter() {
      // 获取当前课程对应的章节课时信息
      this.$axios.get(`${this.$settings.BASE_URL}course/course_chapter/`, {
        params: {
          "course": this.course_id,
        }
      }).then(response => {
        this.course_chapters = response.data.data;
      }).catch(error => {
        window.console.log(error.response);
      })
    },
  },
  components: {
    Header,
    Footer,
  }
}
</script>

<style scoped>
.main {
  background: #fff;
  padding-top: 30px;
}

.course-info {
  width: 1200px;
  margin: 0 auto;
  overflow: hidden;
}

.wrap-left {
  float: left;
  width: 690px;
  height: 388px;
  background-color: #000;
}

.wrap-right {
  float: left;
  position: relative;
  height: 388px;
}

.course-name {
  font-size: 20px;
  color: #333;
  padding: 10px 23px;
  letter-spacing: .45px;
}

.data {
  padding-left: 23px;
  padding-right: 23px;
  padding-bottom: 16px;
  font-size: 14px;
  color: #9b9b9b;
}

.sale-time {
  width: 464px;
  background: #fa6240;
  font-size: 14px;
  color: #4a4a4a;
  padding: 10px 23px;
  overflow: hidden;
}

.sale-type {
  font-size: 16px;
  color: #fff;
  letter-spacing: .36px;
  float: left;
}

.sale-time .expire {
  font-size: 14px;
  color: #fff;
  float: right;
}

.sale-time .expire .second {
  width: 24px;
  display: inline-block;
  background: #fafafa;
  color: #5e5e5e;
  padding: 6px 0;
  text-align: center;
}

.course-price {
  background: #fff;
  font-size: 14px;
  color: #4a4a4a;
  padding: 5px 23px;
}

.discount {
  font-size: 26px;
  color: #fa6240;
  margin-left: 10px;
  display: inline-block;
  margin-bottom: -5px;
}

.original {
  font-size: 14px;
  color: #9b9b9b;
  margin-left: 10px;
  text-decoration: line-through;
}

.buy {
  width: 464px;
  padding: 0px 23px;
  position: absolute;
  left: 0;
  bottom: 20px;
  overflow: hidden;
}

.buy .buy-btn {
  float: left;
}

.buy .buy-now {
  width: 125px;
  height: 40px;
  border: 0;
  background: #ffc210;
  border-radius: 4px;
  color: #fff;
  cursor: pointer;
  margin-right: 15px;
  outline: none;
}

.buy .free {
  width: 125px;
  height: 40px;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 15px;
  background: #fff;
  color: #ffc210;
  border: 1px solid #ffc210;
}

.add-cart {
  float: right;
  font-size: 14px;
  color: #ffc210;
  text-align: center;
  cursor: pointer;
  margin-top: 10px;
}

.add-cart img {
  width: 20px;
  height: 18px;
  margin-right: 7px;
  vertical-align: middle;
}

.course-tab {
  width: 100%;
  background: #fff;
  margin-bottom: 30px;
  box-shadow: 0 2px 4px 0 #f0f0f0;

}

.course-tab .tab-list {
  width: 1200px;
  margin: auto;
  color: #4a4a4a;
  overflow: hidden;
}

.tab-list li {
  float: left;
  margin-right: 15px;
  padding: 26px 20px 16px;
  font-size: 17px;
  cursor: pointer;
}

.tab-list .active {
  color: #ffc210;
  border-bottom: 2px solid #ffc210;
}

.tab-list .free {
  color: #fb7c55;
}

.course-content {
  width: 1200px;
  margin: 0 auto;
  background: #FAFAFA;
  overflow: hidden;
  padding-bottom: 40px;
}

.course-tab-list {
  width: 880px;
  height: auto;
  padding: 20px;
  background: #fff;
  float: left;
  box-sizing: border-box;
  overflow: hidden;
  position: relative;
  box-shadow: 0 2px 4px 0 #f0f0f0;
}

.tab-item {
  width: 880px;
  background: #fff;
  padding-bottom: 20px;
  box-shadow: 0 2px 4px 0 #f0f0f0;
}

.tab-item-title {
  justify-content: space-between;
  padding: 25px 20px 11px;
  border-radius: 4px;
  margin-bottom: 20px;
  border-bottom: 1px solid #333;
  border-bottom-color: rgba(51, 51, 51, .05);
  overflow: hidden;
}

.chapter {
  font-size: 17px;
  color: #4a4a4a;
  float: left;
}

.chapter-length {
  float: right;
  font-size: 14px;
  color: #9b9b9b;
  letter-spacing: .19px;
}

.chapter-title {
  font-size: 16px;
  color: #4a4a4a;
  letter-spacing: .26px;
  padding: 12px;
  background: #eee;
  border-radius: 2px;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
}

.chapter-title img {
  width: 18px;
  height: 18px;
  margin-right: 7px;
  vertical-align: middle;
}

.section-list {
  padding: 0 20px;
}

.section-list .section-item {
  padding: 15px 20px 15px 36px;
  cursor: pointer;
  justify-content: space-between;
  position: relative;
  overflow: hidden;
}

.section-item .name {
  font-size: 14px;
  color: #666;
  float: left;
}

.section-item .index {
  margin-right: 5px;
}

.section-item .free {
  font-size: 12px;
  color: #fff;
  letter-spacing: .19px;
  background: #ffc210;
  border-radius: 100px;
  padding: 1px 9px;
  margin-left: 10px;
}

.section-item .time {
  font-size: 14px;
  color: #666;
  letter-spacing: .23px;
  opacity: 1;
  transition: all .15s ease-in-out;
  float: right;
}

.section-item .time img {
  width: 18px;
  height: 18px;
  margin-left: 15px;
  vertical-align: text-bottom;
}

.section-item .try {
  width: 86px;
  height: 28px;
  background: #ffc210;
  border-radius: 4px;
  font-size: 14px;
  color: #fff;
  position: absolute;
  right: 20px;
  top: 10px;
  opacity: 0;
  transition: all .2s ease-in-out;
  cursor: pointer;
  outline: none;
  border: none;
}

.section-item:hover {
  background: #fcf7ef;
  box-shadow: 0 0 0 0 #f3f3f3;
}

.section-item:hover .name {
  color: #333;
}

.section-item:hover .try {
  opacity: 1;
}

.course-side {
  width: 300px;
  height: auto;
  margin-left: 20px;
  float: right;
}

.teacher-info {
  background: #fff;
  margin-bottom: 20px;
  box-shadow: 0 2px 4px 0 #f0f0f0;
}

.side-title {
  font-weight: normal;
  font-size: 17px;
  color: #4a4a4a;
  padding: 18px 14px;
  border-bottom: 1px solid #333;
  border-bottom-color: rgba(51, 51, 51, .05);
}

.side-title span {
  display: inline-block;
  border-left: 2px solid #ffc210;
  padding-left: 12px;
}

.teacher-content {
  padding: 30px 20px;
  box-sizing: border-box;
}

.teacher-content .cont1 {
  margin-bottom: 12px;
  overflow: hidden;
}

.teacher-content .cont1 img {
  width: 54px;
  height: 54px;
  margin-right: 12px;
  float: left;
}

.teacher-content .cont1 .name {
  float: right;
}

.teacher-content .cont1 .teacher-name {
  width: 188px;
  font-size: 16px;
  color: #4a4a4a;
  padding-bottom: 4px;
}

.teacher-content .cont1 .teacher-title {
  width: 188px;
  font-size: 13px;
  color: #9b9b9b;
  white-space: nowrap;
}

.teacher-content .narrative {
  font-size: 14px;
  color: #666;
  line-height: 24px;
}
</style>

视频托管

# 以后 静态文件(视频,图片,zip。。。),因为会越来越多,不会放在项目的media中---》使用第三方的托管平台---》第三方文件存储

# 存储视频,图片,文件
	-1 meida
    -2 第三方存储(花钱)
    	-七牛云
        -阿里 oss存储
        -其他。。。
    -3 自己公司内部搭建文件存储
    	-Ceph(重)、Minio(用的多一些)、FastDFS(老牌)
        -知乎:
        	-FastDFS:https://zhuanlan.zhihu.com/p/372286804
     	-Minio:之前学长写的博客
        	-参照一下:下午我搭建一下
            
        
# 使用七牛云托管
	-空间:存储文件的地方,不同空间相互隔离
    -注册七牛云账号
    -选择文件存储kodo
    -创建空间,手动上传视频
    
    
# 正常项目上传流程
	-后端:需要配合文件上传接口
    	-前端选择视频----》传到咱们后端----》后端把视频传到七牛云上----》返回地址--》存到咱们数据库
        -前端直接使用js---》传到七牛云---》七牛云返回地址---》向咱们后端发送ajax请求---》我们直接把地址存到库中即可
        
# 如何使用python把文件传到七牛云
	
from qiniu import Auth, put_file

q = Auth('', '')
#要上传的空间
bucket_name = 'lqz-luffy'
#上传后保存的文件名
key = '致命诱惑.mp4'
#生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
#要上传文件的本地路径
localfile = './1.mp4'
ret, info = put_file(token, key, localfile, version='v2')
print(info)
print(ret)
print('http://rx7c1tgq2.hd-bkt.clouddn.com/'+key)
        
        
        
        
 # 域名备案相关
	1 以后写了项目,放到互联网上给别人用
    2 购买域名---》www.lqz.com
    3 备案---》工信部---》流程比较繁琐
    4 项目在  189.28.11.11  
    5 域名解析:只要www.lqz.com  ---》189.28.11.11  

搜索后台接口

视图类

from utils.common_response import APIResponse
from rest_framework.filters import SearchFilter
# 搜索就是查询所有,带过滤   按课程名搜索
class SearchCourseView(GenericViewSet, ListModelMixin):
    serializer_class = CourseSerializer
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
    filter_backends = [SearchFilter]   # 使用模糊查询
    search_fields = ['name', 'brief']  # 根据名字和简介查询

搜索前台

搜索导航栏Header.vue div下面

 
<form class="search">
        <div class="tips" v-if="is_search_tip">
          <span @click="search_action('Python')">Python</span>
          <span @click="search_action('Linux')">Linux</span>
        </div>
        <input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search"
               v-model="search_word">
        <el-button icon="el-icon-search" circle @click="search_action(search_word)"></el-button>
      </form>


搜索结果页面Search.vue

<template>
    <div class="search-course course">
        <Header/>

        <!-- 课程列表 -->
        <div class="main">
            <div v-if="course_list.length > 0" class="course-list">
                <div class="course-item" v-for="course in course_list" :key="course.name">
                    <div class="course-image">
                        <img :src="course.course_img" alt="">
                    </div>
                    <div class="course-info">
                        <h3>
                            <router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
                            <span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
                        <p class="teather-info">
                            {{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
                            <span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
                            <span v-else>共{{course.sections}}课时/更新完成</span>
                        </p>
                        <ul class="section-list">
                            <li v-for="(section, key) in course.section_list" :key="section.name"><span
                                    class="section-title">0{{key+1}}  |  {{section.name}}</span>
                                <span class="free" v-if="section.free_trail">免费</span></li>
                        </ul>
                        <div class="pay-box">
                            <div v-if="course.discount_type">
                                <span class="discount-type">{{course.discount_type}}</span>
                                <span class="discount-price">¥{{course.real_price}}元</span>
                                <span class="original-price">原价:{{course.price}}元</span>
                            </div>
                            <span v-else class="discount-price">¥{{course.price}}元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
            </div>
            <div v-else style="text-align: center; line-height: 60px">
                没有搜索结果
            </div>
            <div class="course_pagination block">
                <el-pagination
                        @size-change="handleSizeChange"
                        @current-change="handleCurrentChange"
                        :current-page.sync="filter.page"
                        :page-sizes="[3, 5]"
                        :page-size="filter.page_size"
                        layout="sizes, prev, pager, next"
                        :total="course_total">
                </el-pagination>
            </div>
        </div>
    </div>
</template>

<script>
    import Header from '../components/Header'

    export default {
        name: "SearchCourse",
        components: {
            Header,
        },
        data() {
            return {
                course_list: [],
                course_total: 0,
                filter: {
                    page_size: 10,
                    page: 1,
                    search: '',
                }
            }
        },
        created() {
            this.get_course()
        },
        watch: {
            '$route.query' () {
                this.get_course()
            }
        },
        methods: {
            handleSizeChange(val) {
                // 每页数据量发生变化时执行的方法
                this.filter.page = 1;
                this.filter.page_size = val;
            },
            handleCurrentChange(val) {
                // 页码发生变化时执行的方法
                this.filter.page = val;
            },
            get_course() {
                // 获取搜索的关键字
                this.filter.search = this.$route.query.word

                // 获取课程列表信息
                this.$axios.get(`${this.$settings.BASE_URL}course/search/`, {
                    params: this.filter
                }).then(response => {
                    // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
                    this.course_list = response.data.data.results;
                    this.course_total = response.data.data.count;
                }).catch(() => {
                    this.$message({
                        message: "获取课程信息有误,请联系客服工作人员"
                    })
                })
            }
        }
    }
</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 .ordering .price_up::before {
        border-bottom-color: #ffc210;
    }

    .course .ordering .price_down::after {
        border-top-color: #ffc210;
    }

    .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 {
        max-width: 100%;
        max-height: 210px;
    }

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

    .course-item .course-info h3 a {
        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 .section-list::after {
        content: "";
        display: block;
        clear: both;
    }

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

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

    .course-item .section-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 .section-list li:hover .free {
        color: #ffc210;
        border-color: #ffc210;
    }

    .course-item {
        position: relative;
    }

    .course-item .pay-box {
        position: absolute;
        bottom: 20px;
        width: 600px;
    }

    .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;
        position: absolute;
        right: 0;
        bottom: 5px;
    }

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

    .course .course_pagination {
        margin-bottom: 60px;
        text-align: center;
    }
</style>
<template>
    <div class="search-course course">
        <Header/>

        <!-- 课程列表 -->
        <div class="main">
            <div v-if="course_list.length > 0" class="course-list">
                <div class="course-item" v-for="course in course_list" :key="course.name">
                    <div class="course-image">
                        <img :src="course.course_img" alt="">
                    </div>
                    <div class="course-info">
                        <h3>
                            <router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
                            <span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
                        <p class="teather-info">
                            {{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
                            <span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
                            <span v-else>共{{course.sections}}课时/更新完成</span>
                        </p>
                        <ul class="section-list">
                            <li v-for="(section, key) in course.section_list" :key="section.name"><span
                                    class="section-title">0{{key+1}}  |  {{section.name}}</span>
                                <span class="free" v-if="section.free_trail">免费</span></li>
                        </ul>
                        <div class="pay-box">
                            <div v-if="course.discount_type">
                                <span class="discount-type">{{course.discount_type}}</span>
                                <span class="discount-price">¥{{course.real_price}}元</span>
                                <span class="original-price">原价:{{course.price}}元</span>
                            </div>
                            <span v-else class="discount-price">¥{{course.price}}元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
            </div>
            <div v-else style="text-align: center; line-height: 60px">
                没有搜索结果
            </div>
            <div class="course_pagination block">
                <el-pagination
                        @size-change="handleSizeChange"
                        @current-change="handleCurrentChange"
                        :current-page.sync="filter.page"
                        :page-sizes="[3, 5]"
                        :page-size="filter.page_size"
                        layout="sizes, prev, pager, next"
                        :total="course_total">
                </el-pagination>
            </div>
        </div>
    </div>
</template>

<script>
    import Header from '../components/Header'

    export default {
        name: "SearchCourse",
        components: {
            Header,
        },
        data() {
            return {
                course_list: [],
                course_total: 0,
                filter: {
                    page_size: 10,
                    page: 1,
                    search: '',
                }
            }
        },
        created() {
            this.get_course()
        },
        watch: {
            '$route.query' () {
                this.get_course()
            }
        },
        methods: {
            handleSizeChange(val) {
                // 每页数据量发生变化时执行的方法
                this.filter.page = 1;
                this.filter.page_size = val;
            },
            handleCurrentChange(val) {
                // 页码发生变化时执行的方法
                this.filter.page = val;
            },
            get_course() {
                // 获取搜索的关键字
                this.filter.search = this.$route.query.word

                // 获取课程列表信息
                this.$axios.get(`${this.$settings.BASE_URL}course/search/`, {
                    params: this.filter
                }).then(response => {
                    // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
                    this.course_list = response.data.data.results;
                    this.course_total = response.data.data.count;
                }).catch(() => {
                    this.$message({
                        message: "获取课程信息有误,请联系客服工作人员"
                    })
                })
            }
        }
    }
</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 .ordering .price_up::before {
        border-bottom-color: #ffc210;
    }

    .course .ordering .price_down::after {
        border-top-color: #ffc210;
    }

    .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 {
        max-width: 100%;
        max-height: 210px;
    }

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

    .course-item .course-info h3 a {
        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 .section-list::after {
        content: "";
        display: block;
        clear: both;
    }

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

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

    .course-item .section-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 .section-list li:hover .free {
        color: #ffc210;
        border-color: #ffc210;
    }

    .course-item {
        position: relative;
    }

    .course-item .pay-box {
        position: absolute;
        bottom: 20px;
        width: 600px;
    }

    .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;
        position: absolute;
        right: 0;
        bottom: 5px;
    }

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

    .course .course_pagination {
        margin-bottom: 60px;
        text-align: center;
    }
</style>

支付宝支付介绍

"""
1)支付宝API:六大接口
https://docs.open.alipay.com/270/105900/

2)支付宝工作流程(见下图):
https://docs.open.alipay.com/270/105898/

3)支付宝8次异步通知机制(支付宝对我们服务器发送POST请求,索要 success 7个字符)
https://docs.open.alipay.com/270/105902/
"""

# 1、在沙箱环境下实名认证:https://openhome.alipay.com/platform/appDaily.htm?tab=info

# 2、电脑网站支付API:https://docs.open.alipay.com/270/105900/

# 3、完成RSA密钥生成:https://docs.open.alipay.com/291/105971

# 4、在开发中心的沙箱应用下设置应用公钥:填入生成的公钥文件中的内容

# 5、Python支付宝开源框架:https://github.com/fzlee/alipay
# >: pip install python-alipay-sdk --upgrade

# 7、公钥私钥设置
"""
# alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
支付宝公钥
-----END PUBLIC KEY-----

# app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
用户私钥
-----END RSA PRIVATE KEY-----
"""

# 8、支付宝链接
"""
开发:https://openapi.alipay.com/gateway.do
沙箱:https://openapi.alipaydev.com/gateway.do
"""

aliapy二次封装包

# GitHub开源框架
https://github.com/fzlee/alipay

# 安装
pip install python-alipay-sdk --upgrade
# 如果抛ssl相关错误,代表缺失该包
pip install pyopenssl

包结构

libs
    ├── iPay  							# aliapy二次封装包
    │   ├── __init__.py 				# 包文件
    │   ├── pem							# 公钥私钥文件夹
    │   │   ├── alipay_public_key.pem	# 支付宝公钥文件
    │   │   ├── app_private_key.pem		# 应用私钥文件
    │   ├── pay.py						# 支付文件
    └── └── settings.py  				# 应用配置  
alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
拿应用公钥跟支付宝换来的支付宝公钥
-----END PUBLIC KEY-----
app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
通过支付宝公钥私钥签发软件签发的应用私钥
-----END RSA PRIVATE KEY-----
setting.py
import os
# 应用私钥
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()

# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()

# 应用ID
APP_ID = '2016093000631831'

# 加密方式
SIGN = 'RSA2'

# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True

# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'

pay.py
from alipay import AliPay
from . import settings

# 支付对象
alipay = AliPay(
    appid=settings.APP_ID,
    app_notify_url=None,
    app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
    alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
    sign_type=settings.SIGN,
    debug=settings.DEBUG
)

# 支付网关
gateway = settings.GATEWAY
init.py
# 包对外提供的变量
from .pay import gateway, alipay

后台 - 支付接口

from django.urls import path, include
from utils.router import router
from . import views

"""
1)支付接口(需要登录认证:是谁):前台提交商品等信息,得到支付链接
    post方法

分析:支付宝回调
    同步:get给前台 => 前台可以在收到支付宝同步get回调时,ajax异步在给消息同步给后台,也采用get,后台处理前台的get请求
    异步:post给后台 => 后台直接处理支付宝的post请求
2)支付回调接口(不需要登录认证:哪个订单(订单信息中有非对称加密)、支付宝压根不可能有你的token):
    get方法:处理前台来的同步回调(不一定能收得到,所有不能在该方法完成后台订单状态等信息操作)
    post方法:处理支付宝来的异步回调
    
3)订单状态确认接口:随你前台任何时候来校验订单状态的接口
"""
# 支付接口(生成订单)
router.register('pay', views.PayViewSet, 'pay')


urlpatterns = [
    path('', include(router.urls)),

    path('success/', views.SuccessViewSet.as_view({'get': 'get', 'post': 'post'}))
]



class AliPayView(APIView):
    def get(self, *args, **kwargs):
        res = alipay.api_alipay_trade_page_pay(
            out_trade_no="2016111211111111",
            total_amount=1000,
            subject='手机',
            return_url="https://example.com",
            notify_url="https://example.com/notify"  # 可选,不填则使用默认 notify url
        )
        pay_url = gateway + res
        return APIResponse(pay_url=pay_url)
	

订单相关表设计

"""
class Order(models.Model):
    # 主键、总金额、订单名、订单号、订单状态、创建时间、支付时间、流水号、支付方式、支付人(外键) - 优惠劵(外键,可为空)
    pass

class OrderDetail(models.Model):
    # 订单号(外键)、商品(外键)、实价、成交价 - 商品数量
    pass
"""


from django.db import models
from user.models import User
from course.models import Course


class Order(models.Model):
    """订单模型"""
    status_choices = (
        (0, '未支付'),
        (1, '已支付'),
        (2, '已取消'),
        (3, '超时取消'),
    )
    pay_choices = (
        (1, '支付宝'),
        (2, '微信支付'),
    )
    subject = models.CharField(max_length=150, verbose_name="订单标题")
    total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
    out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True)
    trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")
    order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
    pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
    pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
    user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户")
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        db_table = "luffy_order"
        verbose_name = "订单记录"
        verbose_name_plural = "订单记录"

    def __str__(self):
        return "%s - ¥%s" % (self.subject, self.total_amount)

    @property
    def courses(self):
        data_list = []
        for item in self.order_courses.all():
            data_list.append({
                "id": item.id,
                "course_name": item.course.name,
                "real_price": item.real_price,
            })
        return data_list


class OrderDetail(models.Model):
    """订单详情"""
    order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="订单")
    course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程")
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
    real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")

    class Meta:
        db_table = "luffy_order_detail"
        verbose_name = "订单详情"
        verbose_name_plural = "订单详情"

    def __str__(self):
        try:
            return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
        except:
            return super().__str__()

扫描二维码登录

# 1 网站上,点击扫码登录----》弹出二维码
	-向后端发送请求----》后端生成二维码---》返回---》前端显示了---》放了个链接地址
    
# 2 掏出手机,打开对应的app-----》扫描二维码----》app能解析出这个地址----》取出你当前app登录的token----》向这个地址发送请求,携带token
	-后端有个 携带token请求登录的接口
    -后端接口:拿到请求传入的token,解析出当前用户----》签发token----》找个地方存着
    
# 3 网站上,开启了定时任务,不停的向后端发送请求----》查询有没有我让它签发token
	-一旦发现没有,过一会在发,3分钟内不停的发
    -一旦发现有,携带回来---》token---》前端就登录成功了
    
    
    
 # 后端接口
	-1 生成二维码接口
    -2 手机携带token,得到用户,再签发token的接口
    -3 网站获取token接口
    
    
# 前端
	-二维码页面
    -定时任务
    
    
# 后端
import qrcode
from io import BytesIO

from qrcode.image.pil import PilImage

import base64

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
from django.core.cache import cache


class QRlogin(ViewSet):
    @action(methods=['GET'], detail=False)
    def scan(self, request, *args, **kwargs):
        img = qrcode.make("http://:8000/api/v1/user/qrlogin/login/?user_id=1")
        img = img.get_image()
        b = BytesIO()
        img.save(b, format='JPEG')
        res = base64.b64encode(b.getvalue())
        # return APIResponse(url='data:image/jpg;base64,'+res)
        return APIResponse(url=res, user_id=1)

    @action(methods=['GET'], detail=False)
    def login(self, request, *args, **kwargs):
        user_id = request.GET.get('user_id')
        user = User.objects.get(pk=user_id)
        # 签发token,放到redis
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        user_info = {'username': user.username, 'token': token}
        import time
        time.sleep(5)
        cache.set('user_id_%s' % user_id, user_info)
        return APIResponse()

    @action(methods=['GET'], detail=False)
    def check_login(self, request, *args, **kwargs):
        user_id = request.GET.get('user_id')
        user_info = cache.get('user_id_%s' % user_id)
        if user_info:
            cache.set('user_id_%s' % user_id, None)
            return APIResponse(token=user_info.get('token'), username=user_info.get('username'))
        else:
            return APIResponse(code=101, msg='手机端尚未确认登录')
        
        
 # 前端
<template>
  <div>
    <img :src="url" alt="">
  </div>
</template>

<script>
export default {
  name: "QRCodeLogin",
  data() {
    return {
      url: '',
      user_id: '',
      t: null
    }
  },
  created() {
    this.$axios.get(`${this.$settings.BASE_URL}user/qrlogin/scan/`).then(res => {
      this.url = 'data:image/jpg;base64,' + res.data.url
      this.user_id = res.data.user_id
      // 开启定时任务,向后端发送请求
      this.t = setInterval(() => {
        this.$axios.get(`${this.$settings.BASE_URL}user/qrlogin/check_login/?user_id=` + this.user_id).then(res => {
          if (res.data.code == 100) {
            this.$cookies.set('token', res.data.token, '7d')
            this.$cookies.set('username', res.data.username, '7d')
            clearInterval(this.t)
            this.t = null
            this.$router.push('/')
          } else {
            this.$message({
              message: res.data.msg,
              type: 'warning'
            });
          }


        })
      }, 3000)

    })
  }
}
</script>

<style scoped>

</style>

上线

准备

# 后端项目,修改好线上配置文件,把代码推送到git仓库

# 前端项目
	-修改前端访问的后端地址 服务器公网IP
        export default {
            BASE_URL: 'http://8.130.18.221:8000/api/v1/'
        }
        
    -把vue项目编译成 html,css,js
    	npm run build  # 在项目目录下生成dist文件夹,内部就是咱们上线要用的
    -把dist文件夹压缩,待命
    

安装git

# 方式一:
	yum install git -y
    
# 方式二:(开发会用的软件)
	yum -y groupinstall "Development tools"
	
    
    
# 执行下面这条
	yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel -y

安装mysql

# mysql 5.7 

# 下载mysql57
wget http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm


# 安装mysql57
 yum -y install mysql57-community-release-el7-10.noarch.rpm
 yum install mysql-community-server --nogpgcheck -y

# 启动mysql57并查看启动状态
systemctl start mysqld
systemctl status mysqld

# 查看默认密码并登录
grep "password" /var/log/mysqld.log   # U>bjULStm1Q<
mysql -uroot -p

# 修改密码
 ALTER USER 'root'@'localhost' IDENTIFIED BY 'Lqz12345?';

安装redis

###### 官方下载编译好的reids
wget https://packages.redis.io/redis-stack/redis-stack-server-6.2.6-v7.rhel7.x86_64.tar.gz
tar -xf redis-stack-server-6.2.6-v7.rhel7.x86_64.tar.gz
mv redis-stack-server-6.2.6-v7/ redis
cd redis/bin
./redis-server   # 启动redis,使用默认配置启动的
#在任意路径下敲redis-server都能把服务运行,
	-方式一:把bin路径加入到环境变量
    -方式二:使用软连接, /usr/bin/    本身在环境变量 ,在任意路径敲redis-server redis-cli都能找到 
    ln -s /root/redis/bin/redis-server /usr/bin/redis-server
    ln -s /root/redis/bin/redis-cli /usr/bin/redis-cli
#查看是否创建软连接成功
ls |grep redis

# 启动redis服务,后台运行
redis-server &


###### 源码安装----》

# 下载redis-6.2.6
wget http://download.redis.io/releases/redis-6.2.6.tar.gz

# 解压安装包
tar -xf redis-6.2.6.tar.gz

# 进入目标文件
cd redis-6.2.6

# 编译环境 gcc   在src路径下把源码编译出  redis-cli  reidis-server
 make   

# 复制环境到指定路径完成安装
cp -r ~/redis-6.2.6 /usr/local/redis

# 配置redis可以后台启动:修改下方内容
vim /usr/local/redis/redis.conf
daemonize yes

# 完成配置修改
>: esc
>: :wq

# 建立软连接
ln -s /usr/local/redis/src/redis-server /usr/bin/redis-server1
ln -s /usr/local/redis/src/redis-cli /usr/bin/redis-cli1

# 后台运行redis
>: cd /usr/local/redis
>:  redis-server1 ./redis.conf

# 测试redis环境
redis-cli


# 关闭redis服务
pkill -f redis -9
redis-cli  shutdown

安装python

# 可以使用yum 安装,不能指定版本
# 源码安装,下载指定版本的源码,编译安装

# 所有linxu和mac,都自带python2:系统服务,是用python写的
# 阿里云的centos默认装了python3.6.8
# python2.7     python3.6.8     python3.9


#  源码安装python,依赖一些第三方zlib* libffi-devel
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel zlib* libffi-devel  -y


wget https://registry.npmmirror.com/-/binary/python/3.9.10/Python-3.9.10.tgz


#  解压安装包
tar -xf Python-3.9.10.tgz 

#4进入目标文件
cd Python-3.9.10

#  配置安装路径:/usr/local/python39
# 把python3.9.10 编译安装到/usr/local/python39路径下
./configure --prefix=/usr/local/python39

#  编译并安装,如果报错,说明缺依赖
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel zlib* libffi-devel  -y

make &&  make install

#  建立软连接:/usr/local/python38路径不在环境变量,终端命令 python3,pip3
ln -s /usr/local/python39/bin/python3 /usr/bin/python3.9
ln -s /usr/local/python39/bin/pip3 /usr/bin/pip3.9

# 机器上有多个python和pip命令,对应关系如下
python       2.x      pip 
python3      3.6      pip3
python3.9    3.9      pip3.9

#  删除安装包与文件:
rm -rf Python-3.9.10
rm -rf Python-3.9.10.tar.xz

安装虚拟环境


# 安装依赖
 pip3.9 install virtualenv     # 如果报错执行如下命令
# python3.9 -m pip install --upgrade pip
# python3.9 -m pip install --upgrade setuptools
# pip3.9 install pbr
 pip3.9 install virtualenvwrapper

# 建立虚拟环境软连接
>: ln -s /usr/local/python39/bin/virtualenv /usr/bin/virtualenv

# 配置虚拟环境:填入下方内容
# ~/ 表示用户家路径:root用户,就是在/root/.bash_profile
>: vi ~/.bash_profile  
# 按 a
# 光标上下控制,粘贴上下面内容

VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3.9
source /usr/local/python39/bin/virtualenvwrapper.sh
# 按 esc
# 输入  :wq   敲回车


# 退出编辑状态
esc

# 保存修改并退出
>: :wq

# 更新配置文件内容
source ~/.bash_profile

# 虚拟环境默认根目录:
/root/.virtualenvs


#创建虚拟环境
mkvirtualenv -p python3.9 luffy

安装uwsgi

# django 项目上线需要使用uwsgi这个web服务器运行django项目,安装这个web服务器
# 使用uwsgi运行django,不再使用测试阶段的wsgiref来运行django了

# uwsgi是符合wsgi协议的web服务器,使用c写的性能高,上线要使用uwsgi

# 安装步骤
# 在真实环境下安装
pip3.9 install uwsgi
# 安装到了python38的安装路径的bin路径下了
# 建立软连接
ln -s /usr/local/python39/bin/uwsgi /usr/bin/uwsgi

安装nginx

# 反向代理服务器
	- 做请求转发 
    - 静态资源代理
    - 负载均衡
    
    
    
# 前往用户根目录
cd ~

#下载nginx1.13.7
 wget http://nginx.org/download/nginx-1.13.7.tar.gz

#解压安装包
tar -xf nginx-1.13.7.tar.gz

#进入目标文件
cd nginx-1.13.7

# 配置安装路径:/usr/local/nginx
./configure --prefix=/usr/local/nginx

#编译并安装
 make &&  make install

# 建立软连接:终端命令 nginx
ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx

#删除安装包与文件:
cd ~
 rm -rf nginx-1.13.7
rm -rf nginx-1.13.7.tar.xz

# 测试Nginx环境,服务器运行nginx,本地访问服务器ip
nginx   # 启动nginx服务,监听80端口----》公网ip 80 端口就能看到页面了
服务器绑定的域名 或 ip:80

# 静态文件放的路径
/usr/local/nginx/html

# 查看进程 
ps aux | grep nginx

部署前端项目

# dist.zip 文件,在本地,上传到远程服务器

#  在远程服务器上
	yum install lrzsz -y   # 跟本地机器上传下载的软件
    yum install unzip -y    #解压zip软件
# 在远程服务器上
	rz  # 打开你本地的目录,选中dist.zip  上传到远端
# 解压
	unzip dist.zip
# 移动文件  /home/html  下面有咱们的前端静态文件
mv /root/dist /home/html
    
    
# 配置nginx 静态代理
cd  /usr/local/nginx/conf   # nginx.conf   配置文件
mv nginx.conf nginx.conf.bak
vi nginx.conf
# 按 a  粘贴下面代码
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    server {
        listen 80;
        server_name  127.0.0.1;
        charset utf-8;
        location / {
            root /home/html;   # 指定前端文件的路径
            index index.html;
            try_files $uri $uri/ /index.html;
        }
    }
} 
# 按 esc  :wq  回车

# 重启nginx
nginx -s reload

部署后台项目


    # 修改项目中wsgi.py,asgi.py  用uwsgi运行wsgi.py
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.prod')

    #  导出项目依赖
    pip3 freeze > requirements.txt

    # 推到远端

### 远程服务器
# 1 拉取项目,安装模块
    mkdir /home/project
    cd  /home/project
    git clone 自己的仓库地址
    进入到虚拟环境
    workon luffy 
    cd /home/project/luffy_api
    pip install -r requirements.txt  # 可能会出错 mysqlclient装不上,先注释,装上能装上的,再单独装mysqlclient

    yum install python3-devel -y
    yum install mysql-devel --nogpgcheck -y
    pip install mysqlclient


    # 2 虚拟环境中也要安装uwsgi
    pip install uwsgi
    
    # 3 配置数据库
        mysql -uroot -p
        #创建数据库
        create database luffy default charset=utf8;

        #设置权限账号密码:账号密码要与项目中配置的一致
        grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
        grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123?';
        flush privileges;
        #退出mysql
        quit;
        # 使用本地navicate链接阿里云的luffy库,使用luffy用户
   # 4 迁移表
		# 把项目中得迁移文件删除,提交,远程
		 python manage.py makemigrations
    	 python manage.py migrate
            
   # 5 uwsgi 运行django
		-写一个uwsgi的配置文件,在项目路径下,新建一个  luffyapi.xml
        <uwsgi>    
           <socket>127.0.0.1:8888</socket>
           <chdir>/home/project/luffy-api</chdir>    # 自己项目的路径   
           <module>luffy_api.wsgi</module> # wsgi文件的路径
           <processes>4</processes>
           <daemonize>uwsgi.log</daemonize>
    	</uwsgi>
    	-使用uwsgi启动
        	uwsgi -x luffyapi.xml
         -查看是否正常运行
        	ps aux |grep uwsgi
            
            
    # 6 配置nginx转发
    	cd /usr/local/nginx/conf
    	vi nginx.conf
    	# 新增的server
    server {
        listen 8000;
        server_name  127.0.0.1;
        charset utf-8;
        location / {
            include uwsgi_params;
            uwsgi_pass 127.0.0.1:8888;
            uwsgi_param UWSGI_SCRIPT luffy_api.wsgi;  # wsgi文件的路径
            uwsgi_param UWSGI_CHDIR /home/project/luffy-api/;  # 自己项目的路径   
        }
        location /static {
            alias /home/project/luffy_api/luffy_api/static;  # 静态文件的收集路径
        }
    }

        
        # 重启nginx 
        nginx -s reload
   # 7 导入数据
	
   # 8 配置域名解析

配置后台admin访问

# 1 浏览器访问http://8.130.18.221:8000/admin  发现没有样式
# 因为uwsgi不能给我们代理静态资源---》debug =False---》不能转发静态资源了

# 收集静态资源,使用nginx代理
# prod.py中加入
STATIC_ROOT = '/home/project/luffy_api/luffy_api/static/'

# 执行命令
# 进入虚拟环境
mkdir /home/project/luffy_api/luffy_api/static
python manage_prod.py collectstatic

# 修改nginx配置文件
# 新增的配置静态文件

server {
            listen 8000;
            server_name  127.0.0.1;
            charset utf-8;
            location / {
               include uwsgi_params;
               uwsgi_pass 127.0.0.1:8888;
               uwsgi_param UWSGI_SCRIPT luffy_api.wsgi; 
               uwsgi_param UWSGI_CHDIR /home/project/luffy_api/;
            }
           location /static {
            alias /home/project/luffy_api/luffy_api/static;
        	}
        }
 
posted @ 2023-09-05 14:53  秋洛尘  阅读(178)  评论(0)    收藏  举报