luffy项目

目录

luffy项目

技术类博客:博客园、CSDN、掘金、思否

找工作:boss之聘、拉钩、智联、猎聘、脉脉

企业项目类型

# 1.面向互联网用户:商城类项目
    -微信小程序商城
# 2.面向互联网用户:
   # 二手交易类的
    -咸鱼
    -转转
   # 交友类app
    -陌陌
    -探探
    -soul
# 3.公司内部项目:是python写的重点
    -oa系统
    -打卡系统工资核算系统
    -第三方公司做的:
    	-给医院 互联网,内部的项目
        -银行 内部系统
        -政府 
        -钢材市场,商户
    -微信小程序订餐
    	-二维火 餐饮行业
    -零售行业
    -问卷网
    -考试系统
    -django+simpleui:二次定制
# 4.个人博客
# 5.内容收费站
    -掘金
# 6.房屋租赁
    -青客
    -蛋壳
    -自如

企业项目开发流程

软件从立项到交付的整个过程

-立项:公司高层确定公司要开发该软件

-需求分析
    #互联网公司项目>>
         需求调研和分析:产品经理设计
    #传统软件公司>>(专门给用户[医院、政府、企业..]做东西)
         需求调研和分析:市场人员、开发高层和客户对接后,写一个需求文档
    
-原型图设计:[墨刀]
     产品经理懂业务并要求画某app基本样子
    
-分任务开发
     前端团队
         UI设计:根据原型图设计切图
         前端:
            拿着切图写代码(pc,小程序,移动端)
            mock数据:假数据,先把功能开发出来
     后端团队
         组里开会,需求文档和原型图,设计后台
         架构,数据库设计
         分任务开发:用户,商品板块
     测试(质量控制部)
         普通功能测试
         接口测试
         压力测试:jmeter
         自动化测试
     联调测试
         因为之前是mock假数据,现在是真正对接口

-项目上线

-持续维护项目
     一般软件开始后没有结束,都是在每天维护修复bug

作为后端开发,公司里的开发流程

# 开新项目,先开会,设计技术选型,数据库设计
    -老大在项目管理平台(禅道)分配任务
    -进入到管理平台就能看到任务,相关功能的原型图
            -需求,原型图,实现的效果
    -开发人员有不明白的需求就找产品经理对接,然后自测
    -提交到版本仓库(git,svn)
        
    -所有都开发完了,分支合并
    -跟前端联调
    -发版
    
    
# 如果是老项目
    -老大在项目管理平台(禅道)分配任务
    -进入到管理平台就能看到任务,相关功能的原型图
          -需求,原型图,实现的效果
    -开发人员有不明白的需求就找产品经理对接,然后自测
    -提交到版本仓库(git,svn)

    -所有都开发完了,分支合并
    -跟前端联调
    -发版

image

image

一.luffy项目需求分析

# luffy项目
    -商城
    -知识付费项目
    -前后端分离
        主站:vue
        后台管理:simpleui
        
# 需求
    -首页功能
        -首页轮播图接口
        -推荐课程接口(后期自行实现)
        
    -用户功能
        -用户名密码登录
        -手机号验证码登录
        -发送手机验证码
        -验证手机号是否注册过
        -注册接口
        -查看用户信息(后期自行实现)
        -修改用户信息(后期自行实现)
        
    -课程列表功能
        -课程列表接口,课程列表展示
        -排序,过滤,分页
        
    -课程详情
        -课程详情接口
        -视频播放功能,视频托管(放在第三方或自己平台)文件托管
        
    -下单功能
        -支付宝支付:生成支付链接、付款、回调修改订单状态
        -购买成功功能

https://www.luffycity.com/

e9fd70b6efa8e27d6c2912b64630dd0

77c1edfba32dd1146b37be04abe5901

c58bd02eae3ce1c9076de3cce8b6536

7cd3fc644cd583060cc5d2987fbab36

381f340dc68895146328d71dfc7def2

da3905e0037a460c1f6252fa2574ad5

0e2c2f33cfb4190712a9765977c32fc

二.更换pip源

1)常用pip源

第三方开发者开发了第三方模块,把模块打包上传到了pipi上,可以根据名字把这个包下载下来。同理我们也可以注册一个pypi账号去上传自己的第三方模块(不建议随意下载不认识的 里面容易有恶意代码)。上传包自行百度

在公司中如果遇到下载问题包问题可能有以下两点

# 公司不能连外网,只能连内部网
    xx.whl 文件提前下好再安装>>pip install 路径/xx.whl
    
# 如果可以上外网但是下载慢
    更换pip镜像源

2)永久换源

windows配置

①文件管理器文件路径地址栏敲:%APPDATA% 回车,可快速进入:C:\Users\用户\AppData\Roaming 文件夹中
②新建 pip 文件夹并在文件夹中新建 pip.ini配置文件
③在配置文件中写配置文件内容


MacOS/Linux配置

①在用户根目录下 ~ 下创建 .pip 隐藏文件夹,如果已经有了可以跳过
-- mkdir ~/.pip
②进入 .pip 隐藏文件夹并创建 pip.conf 配置文件
-- cd ~/.pip && touch pip.conf
③启动 Finder(访达) 按 cmd+shift+g 来的进入,输入 ~/.pip 回车进入
④新增 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

一但配置了永久换源以后在cmd中下载模块就不用再切换镜像源了

与pycharm中下载模块的源不冲突,以上修改仅限于用cmd去下载

image

三.虚拟环境搭建准备

虚拟环境是什么 有什么优点?

假如我现在系统上装了python3.8解释器,现在我有两个项目,分别基于django2和django3写的。现我想把两个项目同时打开下开发,每个项目都要又自己的独立环境且用的模块互不影响,这个时候就需要用到虚拟环境。简单说就是一个版本解释器下如何实现不同版本模块进行开发项目

# 两种解决方案:
        Virtualenv    '下面我们用该方案去创建虚拟环境解决'
    
        pipenv
# 虚拟环境的优点
1、使不同应用开发环境相互独立
2、环境升级不影响其他应用,也不会影响全局的python环境
3、防止出现包管理混乱及包版本冲突

1)虚拟环境的安装和配置

1.需cmd中先下载两个模块

# windows系统下载
pip3.8 install virtualenv  # 虚拟环境就是它
pip3.8 install virtualenvwrapper-win  # 对虚拟环境的加强,以后只需要简单的命令就可以用虚拟环境和创建虚拟环境
# MacOS、Linux系统下载
pip3.8 install virtualenv
pip3.8 install virtualenvwrapper

2.配置虚拟环境管理器工作目录

①在环境变量中新建:WORKON_HOME: D:\Virtualenvs
②在D盘创建文件夹,以后新建的虚拟环境,都会在这个文件夹下

image

3.去Python38解释器的安装目录>>Scripts文件夹>>virtualenvwrapper.bat 双击一下

由于第一步下载了virtualenvwrapper-win,所以在Scripts中会自动生成一个virtualenvwrapper.bat文件,双击一下即可

MacOS和Linux中的文件是virtualenvwrapper.sh

image

2)虚拟环境的创建和使用

创建并进入虚拟环境

创建一个名叫luffy的虚拟环境,会放在D:\Virtualenvs 中,且必须重新开一个cmd窗口执行命令。

mkvirtualenv -p python38 luffy

image

image

只要cmd窗口中带着虚拟环境名字,就表示在虚拟环境中。用pip安装模块都是给虚拟环境安装的

安装模块

以django为例

pip install django==3.2.2

查看安装的模块

pip list

退出虚拟环境

deactivate

查看有哪些虚拟环境

workon

进入虚拟环境

workon 虚拟环境名称

删除虚拟环境(要先退出才能删除)

rmvirtualenv 虚拟环境名称

3)pycharm创建虚拟环境(不用,仅作了解)

image

4)pycharm使用虚拟环境

image

如上图 这样我们下载的模块就是在给虚拟环境中下载了。今后下载模块一定要确认清楚在给谁下载

四.luffy项目前后端创建

pycharm创建路飞后端:luffy_api

但是一定要提前在虚拟环境中装好django3.2.2 否则会自动给我们下载最新版的django

image

pycharm创建路飞前端:luffy_city

打开cmd命令行终端,切到和上面后端项目路径一样的路径下创建vue2前端项目

image

image

image

image

这样我们就创建好luffy项目的前后端了

扩展:包导入

模块和包的区别

模块就是一个py文件
包是一个文件夹下有__init__.py,里面有很多py文件

今后下载第三方包、自己写包的用法较频繁

# python导入包需明白:
	1.pycharm会自动把'项目路径加入到环境变量'(命令行中执行的不会加入)

	2.from 的路径需要【'从环境变量开始'】
         安装的第三方模块都在环境变量中
         内置模块 os、sys..也都在环境变量中
        
    3.在包内部,推荐使用相对导入: 'form .s1 import a'
         一旦使用相对导入该文就'不能以脚本形式运行了'(不能右键运行)
         #【在同一个目录下的推荐使用相对导入 'from .si import a'】
         #【不同目录下的推荐使用绝对导入 'from zz.s1 import a'】

五.后端项目调整目录

1)后端项目调整目录

(1)调整后成以下样式
"""
├── luffy_api                   # 项目根目录
      ├── (包) logs                       # 项目运行时、开发时的【日志目录】>>>新建
      ├── manage.py                  #【启动文件】
      ├── (包) luffy_api                #【项目主应用】,今后开发时只在这里面写代码 
            ├── (包) apps                         #【存放APP的】 以后一个一个的APP都放在这里>>>新建
            ├── (包) libs                           # 放公共的模块。类库【第三方类库】>>>新建
            ├── (包) settings                    #存放【多个配置的目录】>>>新建
                  ├── dev.py                               # 项目【开发时的本地配置】>>>以前的settings.py改名为dev.py
                  └── prod.py                             # 项目【上线时的运行配置】>>>新建
            ├── asgi.py                            # 不用动
            ├── urls.py                             # 总路由
            ├── (包) utils                          #【公共类、方法..】>>>新建
            └── wsgi.py                           # 不用动
      └── scripts(文件)               # 项目运行或测试时写【测试脚本文件】后期不提交到git上
"""

image

(2)解决调整完项目启动不了

原因是我们把本身的配置文件改名为dev.py,并放在了新建的settings包内,django项目运行时第一时间就会加载配置文件但是找不到配置文件!所以才会报错!

🐑开发阶段要改

Terminal中输入python manage.py runserver运行项目发现报错

image

进入manage.py中给配置文件中的【开发本地配置dev】添加进去

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

—————————————————————————————

如果还不行就把这里删掉重新点一下

image

—————————————————————————————

🐑上线阶段也要改

要用wsgi来上线运行

进入wsgi.pyasgi.py中给配置文件中的【上线运行配置prod】添加进去

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.prod')

2)创建app

django用命令创建app

在luffy_api>>luffy_api>>apps>>创建一个home app
1.需注意创建app命令python manage.py startapp home在哪里执行,app就会创建在哪里
2.所以需要先cd到luffy_api>>apps中创建app:cd luffy_api/apps
3.此时执行创建app命令会发现找不到manage.py文件,所以需要用../../manage.py去找
image

3)注册app

创建完app去配置文件dev.py中注册app时运行还会报错,提示找不到该app,原因是根路径在环境变量中,但是现在dev.py被我们移到luffy_api>>settings>>中了。

解决:
方案一:先去homeapp>>apps.py中把name='home'改为luffy_api.apps.home
再去配置文件settings>>dev.py>>中注册app luffy_api.apps.home

方案二:如果路径过多会显得较为长,如果想像以前一样注册app只写一个单词:【推荐】

让小路飞作为根路径添加到环境变量中,还要把apps添加到环境变量中

image

这样以后在dev.py中注册app时就不用加那么长的路径了直接写名字即可,homeapp>>apps里也不用改名了!!!

添加环境变量的好处:

一但把apps加入到环境变量中,以后导包的时候就可以直接用from home import views来导了!

一但把小路飞加入导环境变量中,以后导包的时候就可以直接用from libs import xxx 来导了!

把小路飞加入到环境变量中还有一点是因为今后我们只在小路飞中写代码,所以给他也添加到环境变量中,今后直接把它当作根路径使用

问题:导入模块时编辑器爆红

image

虽然我们把路径添加到环境变量中了,但是:编辑器有时候会反应过不来以为没添加所以爆红

我们只需要把加入到环境变量的文件右键>>Mark Directory as>>source root 即可解决

image

六.luffy后台配置

1)封装日志

日志:程序运行中出的错误、警告都需要做记录

1.日志可以打印到控制台,但是一般都写在【文件中】
2.有的公司会把日志存到某个库中
3.所有项目日志统一管理:【sentry】是django写的服务,用来收集日志的然后展示的。主要开源,只是用的少

以后在项目中就不要出现print了,以后都用日志logger.info(),项目上线后只要调整日志级别即可,低级别的日志不显示打印。不然用print看起来太low

(1)使用步骤

第一步:在dev.py中添加日志配置

里面的级别注意:console实际上线建议用【WARNING】,开发测试阶段可以用INFOfile实际上线建议用【ERROR】

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': {
            # 实际开发建议使用WARNING 【这里暂用INFO,这样Logger.info()就能看到日志】
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {
            # 实际开发建议使用ERROR
            'level': 'WARNING',
            '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'
        },
    },
    # django日志对象
    'loggers': {
        'django': {
            'handlers': ['console', 'file'],
            'propagate': True,  # 是否让日志信息继续冒泡给其他的日志处理系统
        },
    }
}

第二步:在utils>>新建 【common_logger.py】 ,拿到配置文件django日志对象并赋值给一个对象

import logging
logger = logging.getLogger('django')

第三步:在任意想用日志的地方,导入使用即可

from utils.common_logger import logger
logger.info('info级别的日志')
logger.error('error级别的日志')
(2)测试代码

【views.py】

# 测试日志是否可以正常使用
from django.http import JsonResponse
from utils.common_logger import logger


def test_logger(request):
    logger.info('只在控制台打印,文件中不会有')
    logger.error('不仅在控制台打印,还在文件中存储')
    return JsonResponse({'name': 'zy'})

【urls.py】

urlpatterns = [
    path('test/',views.test_logger)
]

image

2)封装全局异常处理

无论后端是否出错,前端收到的数据格式要统一

无论三大认证还是视图类的方法中,只要出了异常就会执行一个函数,但是这个函数只能处理drf的异常,所以需要自己写个函数:既能处理drf异常,还能处理django异常,这样就统一了返回格式,前端看到的格式也就一样了。

(1)使用步骤

第一步:在utils>>新建【common_exceptions.py】写函数

注:虚拟环境中还没有下载drf模块pip install djangorestframework

from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
from utils.common_logger import logger

"""只要走了该函数中 就一定报错了(不能是INFO级别了),所以要记录日志"""
def exception_handler(exc, context):
    """记录日志:哪个id用户?哪个ip?访问哪个地址?执行哪个视图函数?出了什么错?"""
    # request可以从context中获取
    request = context.get('request')
    # id、ip、地址、视图函数、exc就是报错信息
    user_id = request.user.pk
    ip = request.META.get('REMOTE_ADDR')
    path = request.get_full_path()
    view = context.get('view')
    """如果有值说明是drf已处理了的异常,没值说明是django没处理的异常"""
    response = drf_exception_handler(exc, context)
    if response:
        """如果有值说明drf的异常已处理了,直接取detail,【这里可能会有问题】"""
        logger.warning('drf出了异常,异常是:%s' % str(exc))
        res = Response({'code': 999, 'msg': response.data.get('detail', '服务器异常,请联系管理员')})
    else:
        """如果没值说明是django的异常,需要自行处理"""
        logger.error('用户:【%s】,ip:【%s】,访问地址:【%s】,执行视图函数:【%s】,出的异常:【%s】' % (user_id, ip, path, str(view), str(exc)))
        res = Response({'code': 888, 'msg': str(exc)})
    return res

第二步:【dev.py】配置配置文件,以后只要出了异常,都会走上面的函数

REST_FRAMEWORK = {
            'EXCEPTION_HANDLER': 'utils.common_exceptions.exception_handler',
        }

第三步:【dev.py】中注册drf

'rest_framework',

今后尽管写代码,即便报错程序都不会崩掉且会记录日志并处理成统一格式,如果程序崩掉可能是【common_exceptions.py】中detail、或没有data的错误

(2)测试代码

【views.py】

# 测试异常是否正常处理
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import APIException


class TestException(APIView):
    def get(self, request):
        # drf异常
        # raise APIException('drf抛出了异常')
        # django的异常
        # l=[1,2,3]
        # print(l[9])
        # django主动抛的异常
        raise Exception('django主动抛异常')
        return Response('ok')

【urls.py】

urlpatterns = [
    path('text_exception/', views.TestException.as_view())
]

image

image

image

3)封装Response

本身drf是有Response的,但是公司会规定前端收到的格式必须固定

"""如:"""
{code: 100, msg: 提示, data:{}/[]}
{code: 100, msg: 提示, token: xxxxxxx, user: zy}

这样我们每次都要写前面的code和msg,所以想对Response进行封装,封装后code、msg不传的时候就会用默认的

封装后的效果:
return APIResponse()
前端收到:{code:100,msg:成功}
return APIResponse(code=101,msg=失败)
前端收到:{code:101,msg:失败}
return APIResponse(token=xxx,username=zy)
前端收到:{code:100,msg:成功,token:xxx,username:zy}
return APIResponse(data=[{},{},{}])
前端收到:{code:100,msg:成功,data:[{},{},{}]}
return APIResponse(headers={'xx':'yy'})
前端收到:{code:100,msg:成功} 响应头:xx=yy
return APIResponse(status=201)
前端收到:{code:100,msg:成功} 响应状态码:201

(1)使用步骤

在utils>>新建【common_response.py】里面写封装的Response

from rest_framework.response import Response


class APIResponse(Response):
    # 触发Response里的__init__,除了下面几个其他传的参数都让kwargs接收
    def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs):
        data = {'code': code, 'msg': msg}
        """如果有值说明传了其他参数,都要返回给前端并放到data中"""
        if kwargs:
            """update:用括号里的字典对data字典进行更新(数据加进去,变量名一样就会替换)"""
            data.update(kwargs)
        # 由于重写了__init__所以用super调用父类的方法完成初始化
        super().__init__(data=data, status=status, headers=headers)
(2)测试代码

【views.py】

# 测试自己写的Response
from rest_framework.views import APIView
from utils.common_response import APIResponse


class TestAPIResponse(APIView):
    def get(self, request):
        # return APIResponse(token='xxx',username='zy')
        # return APIResponse(code=101, msg='错误')
        return APIResponse(data=[{'name': '水浒传', 'price': 20}, {'name': '红楼梦', 'price': 30}])

【urls.py】

urlpatterns = [
    path('test_APIResponse/', views.TestAPIResponse.as_view()),
]

17

七.luffy数据库配置

1)创建用户创建库

之前项目操作数据库都是用root用户,但是在实际工作中root权限太高不会给你,如果开发人员有root权限数据安全性就会很差!所以会给开发人员专门创建一个用户,只对该项目的库有操作权限

所以我们要创建luffy数据库创建一个luffy用户luffy用户只对luffy库有操作权限

(1)创建luffy数据库
1.用root用户进入mysql
>:mysql -uroot -p

2.创建数据库
>: create database luffy default charset=utf8;
    
3.查看用户
>: select user,host from mysql.user; 
只有root用户,要创建luffy用户
——————————————————
"""5.7往后的版本查看用户"""
>: select user,host,authentication_string from mysql.user;

image

(2)创建luffy用户,并给他只有luffy库所有的权限

注意!!!这里密码建议用Luffy123? 否则后面服务器中创建用户密码不规范没办法和这里密码一样,改起来要动很多地方!!!!

"""
设置权限账号密码:
授权账号命令:grant 权限(create, update) on 库.表 to '账号'@'host' identified by '密码'
"""

1.配置任意ip都可以连入数据库的账户'%:允许远程访问'
"创建一个luffy用户密码是Luffy123,可以远程连接到数据库,对luffy库的所有表有所有权限"
>: grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123';  # 建议用Luffy123?

2.由于数据库版本的问题,可能本地还连接不上,所以还要给本地用户单独配置
>: grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123';  # 建议用Luffy123?

3.不要忘记刷新权限:把硬盘上的权限刷到内存中!
>: flush privileges;
    
———'只能操作luffy数据库的账户'————
quit退出后就可以用luffy用户重新登录
账号:luffy
密码:Luffy123  # 建议用Luffy123?

image

补充:mysql用localhost和ip地址连接有什么区别

"localhost本地连接"没有经过网卡 有一个socket连接
>:mysql -uluffy -p


"远程ip地址连接"经过了网卡 又回来经过网卡连接
>:mysql -uluffy -h 127.0.0.1 -p

image

2)使用项目连接库

项目连接mysql需要安装模块

pymysql
mysqlDB
mysqlclient

pymysql、mysqlDB、mysqlclient历史:

1.原来python2上有个操作mysql的模块叫mysqlDB
但到python3上时该模块已经不支持python3了,可是django默认用mysqlDB去连接mysql,运行时会导致报错

2.如果我们要用Pymysql作为连接mysql数据库的模块,就需要加代码:
猴子补丁:程序运行过程中动态的替换对象的一种方式

imprort pymysql
""" 猴子补丁"""
pymysql.install_as_mysqldb()  

3.如果django2.2.2版本以后想使用pymysql,就需要改django源码。

4.但是一直改源码很麻烦所以统一使用mysqlclient作为操作mysql的底层库(是基于python2的mysqlDB 在python3上重新装了一下 只是改名叫mysqlclient)

4.使用mysqlclient只需要安装该模块 不需要再写任何代码直接使用即可

5.但是mysqlclient这个模块特别难安装:

win:pip install mysqlclient,如果装不了就等centos部署项目,后面会讲centos上如何装

(1)安装mysqlclient模块
pip install mysqlclient

image

(2)连接数据库

用luffy用户在配置文件中连接mysql

如果用户名密码写死在DATABASES中一但代码泄露会很没安全保证安全
所以要用其他方式保证数据的安全把用户名和密码写在环境变量中

print(os.environ.get('XXX'))
"""可以获取到环境变量中XXX的值"""

【dev.py】:

"""能拿到环境变量中的值就用,拿不到就用后面的"""
name = os.environ.get('LUFFY_NAME', 'luffy')
password = os.environ.get('LUFFY_PASSWORD', 'Luffy123')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',
        'USER': name,
        'PASSWORD': password,
        'HOST': '127.0.0.1',
        'PORT': 3306
    }
}

软件开发模式

瀑布模式:bbs项目就是按照这种模式

需求分析>>设计>>创建数据库(所有都创建)>>开发(3个月)>>交给测试>>测试通过>>上线

敏捷开发:

第一周期:
需求分析>>设计(只设计一个板块)>>开发(2周)>>交给测试>>运维上线(测试环境)

第二周期:
设计(只设计一个板块)>>开发(2周)>>交给测试>>运维上线(测试环境)

面试提问:你们一个sprint周期有多久?一周

img

八.User模块用户表

1)user模块

注意:当决定使用auth表扩写后,项目一定不要先迁移,先建好用户表再迁移!!

如果迁移完了再想用auth的user表,只能删库、删迁移文件所有app、删admin和auth的迁移文件

(1)用户表使用auth表扩写

user>>【models.py】

from django.db import models
from django.contrib.auth.models import AbstractUser


"""用户表使用auth表扩写 【 pip install Pillow】"""
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

【dev.py】

"""自定义用户配置"""
AUTH_USER_MODEL = 'user.User'
(2)执行数据库迁移命令
>:python manage.py makemigrations
>:python manage.py migrate

image

可能遇到的问题
# 遇到的问题,明明小luffyapi已经加入到环境变量,程序运行没问题,但是表迁移,就报错,找不到模块
	-打印了看一下,确实环境变量有,但是不是个字符串,是个对象
    -程序运行,是没问题
    -迁移有问题:配置文件中转成字符串,就解决了

2)django的dev.py配置文件

(1)pathlib模块

pathlib模块是django3.6版本以后用来处理文件路径的模块,原来是用os处理

from pathlib import Path

1."""当前文件路径"""
Path(__file__).resolve()
D:\笔记\luffy_api\luffy_api\settings\dev.py
    
2."""当前文件上一个父路径"""
Path(__file__).resolve().parent
D:\笔记\luffy_api\luffy_api\settings
    
3."""当前文件上一个父路径的上一个父路径"""
Path(__file__).resolve().parent.parent
D:\笔记\luffy_api\luffy_api
(2)配置文件讲解
from pathlib import Path
import os
import sys

//项目根路径
BASE_DIR = Path(__file__).resolve().parent.parent
# print(BASE_DIR)  # D:\pythonProject1\luffy_api\luffy_api
# 把小路飞中的apps添加到环境变量中
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
# 把小路飞添加到环境变量中
sys.path.insert(0, str(BASE_DIR))


//秘钥,涉及到加密的django中,都会用它
SECRET_KEY = 'django-insecure-!g(8l%fw_#t$pz$x4jdf#e3$b4+c%xzqyq@3zki08vj&i)z4k-'


//项目是以debug模式运行还是非debug模式运行
//当项目上线时必须改成False
//当等于True时,代码可以热更新(自动重启)
//当项目调试时对开发者更友好,访问路径不存在可以列出所有路径,报错了前端能看到
DEBUG = True
//搭配DEBUG=False使用:允许我的项目部署在哪个ip地址上,'*'表示所有
ALLOWED_HOSTS = ['*']



//django 是多个app组成的,在里面配置app,默认带的app就是django内置的app
INSTALLED_APPS = [
    'django.contrib.admin', //后台管理
    'django.contrib.auth',  //权限管理
    'django.contrib.contenttypes', //contenttypes表中存app与表的关系
    'django.contrib.sessions',//session表 django的session相关
    'django.contrib.messages',//消息框架 flask也叫闪现。
    'django.contrib.staticfiles',//静态资源
    'rest_framework',
    'home',
    'user'
]


//中间件 
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',  //用户安全认证相关中间件
    'django.contrib.sessions.middleware.SessionMiddleware', // session相关中间件
    'django.middleware.common.CommonMiddleware',            // 带不带 / 问题
    'django.middleware.csrf.CsrfViewMiddleware',            // csrf 认证,生成csrf串
    'django.contrib.auth.middleware.AuthenticationMiddleware', // 用户认证
    'django.contrib.messages.middleware.MessageMiddleware',  //消息框架相关
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


//根路由
ROOT_URLCONF = 'luffy_api.urls'

//模板文件 
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  // 坑,模板路径用列表,可以有多个
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]


//项目运行的配置---》【项目上线】运行使用uwsgi 运行  application()
WSGI_APPLICATION = 'luffy_api.wsgi.application'

//数据库配置,mysql 主从搭建完,读写分离
name = os.environ.get('LUFFY_NAME', 'luffy')
password = os.environ.get('LUFFY_PASSWORD', 'Luffy123?')
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',
        'USER': name,
        'PASSWORD': password,
        'HOST': '127.0.0.1',
        'PORT': 3306
    },
}

//忽略掉
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


//国际化
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False


//静态资源
STATIC_URL = '/static/'


//大整形
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

3)开启media访问

(1)使用

步骤一:【dev.py】配置文件加入:

"""开启media访问"""
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

步骤二:小路飞下新建【media】文件夹>>新建【icon】文件夹,并把models.py中的默认头像放在文件夹里

image

步骤三:【urls.py】路由中设置路由

from django.urls import path, re_path
from django.views.static import serve
from django.conf import settings

"""不建议用re_path"""
# re_path('^media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT})
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT})

关于导配置文件:

以后使用djagno的配置文件都用from django.conf import settings,好处是:如果你自己配了就用你的dev.py配置文件,如果你没配就用内置的配置文件

(2)测试访问

http://127.0.0.1:8000/media/icon/default.jpg

扩展

1)python运算符之位运算

https://zhuanlan.zhihu.com/p/370167569

2)windows安装 mysql 5.7

https://zhuanlan.zhihu.com/p/571585588

3)md5是对称加密还是非对称加密

-对称加密:加密的秘钥和解密的秘钥是同一个
-非对称加密:加密使用公钥加密,解密使用私钥解密,使用公钥是不能解密的
-md5属于摘要算法:没有解密这一说

4)局域网内访问django

跑在0.0.0.0上,局域网内所有人都能访问

image

九.路飞前台scc、js配置、下载模块

1)整理纯净项目

src>>【App.vue】修改

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

src>>views>>【HomeView.vue】修改,删除【AboutView.vue】暂时用不到

<template>
  <div class="home">
    <h1>首页</h1>
  </div>
</template>

<script>

export default {
  name: 'HomeView',
}
</script>

src>>router>>【index.js】把AboutView的路由删掉

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
]

src>>components里的小组件都删掉

现在就是纯净版的vue前端项目

2)配置全局css

正常写前端项目,需要去掉所有标签的默认样式的css

第一步:src>>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; /* 合并边框 */
}

第二步:css>>main.js全局生效配置

// 使用全局css样式,只需要导入就会生效
import '@/assets/css/global.css'

3)配置全局js

如果向后端发送请求:测试阶段时输入 127.0.0.1:8000
​ 后期上线时地址要变,如果在组件中把地址写死以后每个都要改

所以需要写一个全局js,里面有个url地址,以后所有组件中发送请求时都是用这个url地址

第一步:src>>assets>>新建【js】>>新建【settings.js】

    export default {
        BASE_URL:'http://127.0.0.1:8000/api/v1'
    }

第二步:src>>main.js中引入settings.js并把对象加入到vue原型中

// 引入settings.js,把settings对象放入到vue的原型中
import settings from "@/assets/js/settings";
Vue.prototype.$settings = settings

以后在任意组件中只需要 this.$settings.BASE_URL

第三步:以后在任意组件中使用this.$settings.BASE_URL即可使用

this.$settings.BASE_URL

//如使用axios需要拼路径用+/users即可
this.$axios.get(this.$settings.BASE_URL+'/users').then(res=>{
})

安装axios

跟后端交互

使用步骤

第一步:安装axios

cnpm install axios -S

第二步:src>>main.js中把axios放到vue原型中

import axios from "axios";
Vue.prototype.$axios=axios

第三步:在任意组件中使用

this.$axios.get()

安装vue-cookies

后期登录成功token存在cookie中

使用步骤

第一步:安装vue-cookies

cnpm install vue-cookies -S

第二步:src>>main.js中把vue-cookies放到vue原型中

import cookies from "vue-cookies";
Vue.prototype.$cookies=cookies

第三步:在任意组件中使用

this.$cookies.get()
this.$cookies.set()

安装elementui

样式使用elementui

使用步骤

第一步:安装element-ui

cnpm install element-ui -S

第二步:src>>main.js中把element-ui放到vue原型中

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

第三步:复制粘贴使用即可,如:

<el-button type="warning">按钮</el-button>

安装bootstrap和jquery

路飞项目中并不需要用bootstrap,但是有些项目可能会用到所以这里讲解下

注:bootstrap引用要想使用必须也要引用jquery(最新版的不用 3版本需要)

使用步骤

第一步:安装jquery和bootstrap

cnpm install jquery -S
cnpm install bootstrap@3 -S

第二步:引用bootstrap 配置jquery

src>>main.js中引入bootstrap

import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'

【vue.config.js】中配置jquery(覆盖里面的内容)

const webpack = require("webpack");
module.exports = {
    configureWebpack: {
        plugins: [
            new webpack.ProvidePlugin({
                $: "jquery",
                jQuery: "jquery",
                "window.jQuery": "jquery",
                "window.$": "jquery",
                Popper: ["popper.js", "default"]
            })
        ]
    }
};

第三步:在页面中使用bootstrap即可,如:

<button type="button" class="btn btn-danger">按钮</button>

以上就完成了前端后端基础配置

十.后端主页

1)后台主页模块轮播图表设计

分析原型图:首页要写的接口

  • 轮播图接口
  • 推荐课程接口(暂时没写)
  • 推荐老师(暂时没有)
  • 学员评论(暂时没有)
(1)后端创建首页【home】app

在apps文件夹下执行:(已经提前创好了)

python ../../manage.py  startapp home
(2)创建表

第一步:luffy_api>>utils>>新建【common_model.py】创建一个基表(因为该基表中字段其他表也要用所以放在utils中)

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

第二步:luffy_api>>apps>>home>>【models.py】中写轮播图表:Banner

表中有的字段:真正图片地址,标题,跳转链接,图片介绍,公共字段(创建时间、最后更新时间、是否删除(软删除)、是否显示、轮播图优先级)

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


# 创建一个基表,后期其他表用来继承该基表
class Banner(BaseModel):
        # 表中有的字段:图片名称,图片地址,跳转链接,图片详情,公共字段(创建时间、最后更新时间、是否删除(软删除)、是否显示、轮播图优先级)
        title = models.CharField(max_length=32, unique=True, verbose_name='图片名称')
        # 图片会传到media下的banner下
        image = models.ImageField(upload_to='banner', verbose_name='图片地址')
        link = models.CharField(max_length=64, verbose_name='跳转链接')
        info = models.TextField(verbose_name='图片详情')

        class Meta:
            # 指定表名 今后无论哪个APP创建表都叫luffy_xxx
            db_table = 'luffy_banner'
            # 后台管理中的名字
            verbose_name_plural = '轮播图表'

        def __str__(self):
            return self.title

第三步:数据库迁移

python manage.py makemigrations
python manage.py migrate

image

2)后台主页模块写轮播图接口

轮播图接口给首页用的

(1)luffy_api>>apps>>home>>新建序列化类文件【serializer.py】
from rest_framework import serializers
from .models import Banner


class BannerSerializer(serializers.ModelSerializer):
    class Meta:
        # 与Banner表建立关联
        model = Banner
        # 序列化Banner表中的哪些字段
        fields = ['id', 'image', 'title', 'link']
(2)luffy_api>>apps>>home>>views.py视图
from rest_framework.viewsets import GenericViewSet
from .models import Banner
from .serializer import BannerSerializer
from rest_framework.mixins import ListModelMixin
from utils.common_response import APIResponse

# 查所有轮播图接口
class BannerView(GenericViewSet, ListModelMixin):
    # 继承GenericViewSet可以写下面两句话,ViewSet是ApiView不能写下面两句话
    # 继承ListModelMixin可以查所有

    # 要序列化或反序列化的表模型数据
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = BannerSerializer

    def list(self, request, *args, **kwargs):
        res = super().list(request, *args, **kwargs)
        # {code:100,msg:成功,data=[{},{}]}
        return APIResponse(data=res.data)
(3)路由分发(各个app有自己路由)

luffy_api>>urls.py主路由

from django.urls import path, include

urlpatterns = [
        # 路由分发>>分发到各app下的子路由中
        path('api/v1/home/', include('home.urls'))
]

luffy_api>>apps>>home>>新建【urls.py】子路由

from rest_framework.routers import SimpleRouter
from . import views

router = SimpleRouter()
router.register('banner', views.BannerView, 'banner')
urlpatterns = [

]
urlpatterns += router.urls

(4)验证

访问 http://127.0.0.1:8000/api/v1/home/banner get请求就可以查看所有轮播图

image

3)录入数据

主站前后端分离

后台管理使用django的admin做>>>要用simpleui

使用步骤

第一步:luffy_api>>apps>>home>>admin.py中注册一下要在admin后台录数据的表

from .models import Banner

admin.site.register(Banner)

第二步:下载安装simpleui

pip install django-simpleui

第三步:luffy_api>>settings>>dev.py中注册一下simpleui(注册app中的第一个)

INSTALLED_APPS=[
        'simpleui',
             ......
]

第四步:创建一个admin后台用户,用该用户去admin后台中录入数据

python manage.py createsuperuser

image

image

第五步:录入4条轮播图数据

http://127.0.0.1:8000/admin

image

测试

image

4)跨域问题详解

跨域问题:前端发送ajax请求到后端会有跨域的拦截

image

出现的原因:
​ 同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
简单说就是请求的url地址(发送ajax请求的地址)必须与浏览器上的url地址处于同一个域中(域名、地址、端口、协议相同)。如果有跨域当请求成功让数据库返回,但浏览器会拦截下来。


扩展知识:
我们在浏览器上输入域名:www.baidu.com,这个域名会被dns解析成地址:192.168.2.3 然后去访问,如果我们没加端口会自动默认用80端口所以!我们如果把项目端口设置成80,那么当我们访问项目地址时就可以不用加端口了!!
dns域名解析会优先找本地的host文件(我们可以修改本地host做映射)


(1)自行解决跨域

方式一:CORS后端代码控制(我们用的方法)
方式二:Nginx反向代理(公司可能常用)
方式三:JSONP现在不常用了(只能发get请求)
方式四:搭建Node代理服务器

这里我们用第一种CORS后端代码控制解决跨域

CORS:【 跨域资源共享】是后端技术,在响应头中加入固定的头就可以允许前端访问

CORS基本流程

​ 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

​ 浏览器发出CORS简单请求,只需要在头信息之中增加一个Origin字段

​ 浏览器发出CORS非简单请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

什么是简单请求 和 非简单请求

符合以下l两个条件的都是简单请求:
​ (1)请求方式是HEAD/GET/POST三者之一的
​ (2)HTTP的头信息步超出以下字段的:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

如果我发一个POST的JSON格式数据,那它就是非简单请求

总结:
如果是简单请求,直接发送真正的请求
如果是非简单请求,先发送options,如果运行再发真正的请求

允许简单请求的跨域(不是最终)

image

只需要在后端views.py中添加允许该地址访问跨域即可【如果地址是*则允许所有人

return APIResponse(data=res.data, headers={'Access-Control-Allow-Origin': 'http://192.168.1.231:8080'})
——————————————————
return APIResponse(data=res.data, headers={'Access-Control-Allow-Origin': '*'})

image

允许非简单请求的跨域(解决了简单与非简单)

以上简单请求虽然可以解决跨域,但是非简单请求则会出错

image

解决该问题则需要自己写中间件,如果发过来了OPTIONS请求则允许,不是则添加简单请求的允许访问

luffy_api>>utils>>新建【common_middle.py】

from django.utils.deprecation import MiddlewareMixin

class CorsMiddleWare(MiddlewareMixin):
    def process_response(self, request, response):
        if request.method == "OPTIONS":
            # 允许非简单请求的请求头(*表示所有)
            response["Access-Control-Allow-Headers"] = "*"
        # 允许简单请求的请求头(*表示所有)
        response["Access-Control-Allow-Origin"] = "*"
        return response

luffy_api>>settings>>dev.py中配置自己的中间件

MIDDLEWARE = [
    'utils.common_middle.CorsMiddleWare'
    ......
]

image

(2)第三方模块解决跨域

上面是自己去写中间件解决简单与非简单请求的跨域,其实有一个第三方模块帮我们解决了该问题,只需要安装配置导入即可

第一步:下载安装模块

pip install django-cors-headers

第二步:配置

luffy_api>>settings>>dev.py

注册app:

INSTALLED_APPS = [
        ......
        'corsheaders',
    ]

配置中间件:

MIDDLEWARE = [
        ......
        'corsheaders.middleware.CorsMiddleware',
    ]

允许所有域、请求方式、请求头加的东西:

# 允许所有域
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',
)

可能还会有跨域问题:请求头中只允许加这些,如果是其他字段则还是有跨域,只需要自行添加即可

补充:mac无法下载mysqlclient

本地开发环境的依赖和上线环境依赖是不一样的

我们的项目一般本地都是用pymysql 上线用mysqlclient

每个项目都要有一个requirements.txt文件,里面有需要的所有依赖和版本,需要我们用命令自己创

>:pip freeze   # 显示当前环境装的所有依赖
>:pip freeze > requirements.txt  # 把当前环境所有依赖放在新建的文件中

image

以上我们可以把requirements.txt里的mysqlclient删掉,再建一个dev.txt把mysql删掉,这样就有两个依赖文件 可以针对不同环境下载不同依赖

当打开一个新项目时需要先安装依赖:

pip install -r dev.txt   # 开发环境用mysqlclient
pip install -r requirements.txt   # 上线环境环境用mysql

十一.前端主页

1)前端功能分析

image

首页页面组件
头部组件(小组件)Header可能多个页面会使用
轮播图组件(小组件)Banner可能多个页面会使用
尾部组件(小组件)Footer可能多个页面会使用

src>>components下创建三个组件写代码:

(1)Header组件
<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 {
      // 当前所在路径,去sessionStorage取的,如果取不到,就是 /
      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  this.$route.path
    sessionStorage.url_path = this.$route.path;
    // 把url_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>
(2)Banner组件
<template>
  <div class="banner">
    <el-carousel height="400px">
      <el-carousel-item v-for="item in 4" :key="item">
        <img src="../assets/img/banner1.png" alt="">
      </el-carousel-item>
    </el-carousel>
  </div>
</template>


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


<style scoped>
.el-carousel__item {
  height: 400px;
  min-width: 1200px;
}

.el-carousel__item img {
  height: 400px;
  margin-left: calc(50% - 1920px / 2);
}
</style>
(3)Footer组件
<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>
(4)HomeView中引入组件并使用
<template>
  <div class="home">
    <Header></Header>
    <Banner></Banner>
    <!--此处多写几个<br>标签隔开-->
    <Footer></Footer>
  </div>
</template>

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

export default {
  name: 'HomeView',
  components:{
    Header,
    Banner,
    Footer
  }
}
</script>

image

此时轮播图的图片是写死的,我们应该让它动起来(后端接口接好了,里面有4张图片,从后端拿到四张图片加载到前端页面)

2)前端轮播图功能完善

【Banner】组件中:向后端接口发送ajax请求拿到4张图片

<script>
export default {
  name: "Banner",
  data() {
    return {
      img_list: []
    }
  },
  created() {
    this.$axios.get(this.$settings.BASE_URL + '/home/banner/').then(res => {
      console.log(res.data)
      if (res.data.code == 100) {
        this.img_list = res.data.data
      } else {
        this.$message(res.data.msg)
      }
    }).catch(res => {
      this.$message('轮播图服务器异常,请稍后再试')
    })
  }
}
</script>

点击轮播图需要跳转页面(由于我们admin中有一张图片添加了外部链接,而router-link只能跳内部,所以还需判断是否为内部或外部链接)

<template>
  <div class="banner">
    <el-carousel height="400px" :interval="5000" arrow="always">
      <el-carousel-item v-for="item in img_list" :key="item.id">
        <!--判断link中的链接是不是以http开头-->
        <div v-if="item.link.indexOf('http')>-1">
          <a :href="item.link">
            <img :src="item.image" :alt="item.title">
          </a>
        </div>
        <div v-else>
          <!--router-link 只能跳内部链接 不能跳外部-->
          <router-link :to="item.link">
            <img :src="item.image" :alt="item.title">
          </router-link>
        </div>
      </el-carousel-item>
    </el-carousel>
  </div>
</template>

image

此时中间空着的不太好看,我们可以给里面添加点其他东西让页面看起来更丰富

【HomeView】中:

    <Header></Header>
    <Banner></Banner>

    <div class="course">
      <el-row>
        <el-col :span="6" v-for="(o, index) in 4" :key="o" class="course_detail">
          <el-card :body-style="{ padding: '0px' }">
            <img src="../assets/img/book.png"
                 class="image">
            <div style="padding: 14px;">
              <span>推荐课程</span>
              <div class="bottom clearfix">
                <time class="time">价格:999</time>
                <el-button type="text" class="button">查看详情</el-button>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>

    <Footer></Footer>

再给加点样式

<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>

image

十二.git

1)git介绍和安装

(1)git介绍

版本代码管理软件有两种:git、svn

主要是用来做版本代码管理的[文件管理]
1.帮助开发者合并开发代码(协同开发)
2.合并中如果出现代码冲突,会提示后提交给合并代码的开发者,让其解决冲突
3.代码版本管理(可以回退到之前版本)

//两者的比较:

svn:【集成式管理】,服务端挂掉就做不了版本管理,代码合并

git:【分布式管理】,服务端挂掉,本地还可以继续做版本管理,代码合并

image

image

(2)git、gitee、github、gitlab的区别
  • git:版本管理软件,装在操作系统上,有很多命令

  • gitee:远程仓库[ 开源代码、私有代码 ],网站里可以看到有哪些开源代码,通过网站做一些配置

    国内最大的开源远程仓库,小公司没实力搭gitlab所以会使用gitee的私有仓库

  • github:远程仓库[ 开源代码,私有代码 ],网站里可以看到有哪些开源代码,通过网站做一些配置

    国际上最大的开源远程仓库

  • gitlab:公司内部的远程仓库(自己公司搭建)公司中用的多

  • bibucket:只有私有仓库(远程代码仓库),没有开源代码

(3)git下载安装

官网https://git-scm.com/download/win

image

下载好后安装一直下一步即可(注意存储路径不要放c盘)

验证是否下载成功:
1)桌面鼠标右键会有两个git的东西
2)cmd中输git会有反应

2)git工作流程

image

"""有三个区"""
工作区    存放文件的地方,新增修改删除
暂存区    把工作区的更改提交到暂存区
版本库    把暂存区的内容提交到版本库,这样就被版本管理了
                     -可回退、查看之前版本   
    
    
"""工作流程"""
1 只要被git管理了、只要文件发生变化(新增,删除,修改),使用git就能看到它变成红色
2 工作区的变更,只要提交到暂存区就会变绿色
3 从暂存区提交到版本库才会被版本管理,一旦被版本管理了后期就可以回退到某个版本

4 可以把版本库的内容提交到远程仓库统一管理起来【所有开发者都可以提交】
5 本地可以拉去远程的代码
6 本地可以把本地代码提交到远程

3)git常用命令

//必须记住的:
    	git add  
        git commit -m
        git reset --hard 版本号
//需要会用:
		git init
    	git status
        git log
        git reflog
        
//了解:
		git checkout .
    	git reset HEAD
    	git config --global user.email "913650502@qq.com"
        git config --global user.name "zy"

"""核心总结"""
1.有红色信息(工作区有内容),就执行 add 
2.全绿信息(内容全部在暂存区),才执行 commit
3.只有被版本库控制的代码才能被监听,才可以回滚到任何一个版本
4.空文件夹不会被git记录
(1)文件夹作为仓库被git管理

被管理的文件夹中会出现一个.git的隐藏文件

进入某文件夹中右键git Bash Here

git init                 //让当前文件变成仓库
git init  xxx         //在当前路径中再创建一个xxx文件夹作为仓库

image

(2)查看状态[只看暂存区和工作区]
git status  //看文件颜色判断在哪个区

红色:工作区变化了,但是没有提交到暂存区
绿色:已提交到暂存区,没有提交到版本库
没有:所有东西都在版本库中了[所有文件都被git管理起来了]

image

(3)把工作区的变更提交到暂存区
git add .    //当前路径下所有变更的都提交到暂存区
git add xx  //当前路径下的xx文件的变更提交到暂存区

image

(4)把暂存区所有内容提交到版本库[作为一个版本]
git commit -m '需写个注释内容'
git commit -m '提交了1.txt'

此时直接输入会报错,会问我们是谁??原因是没设置作者。(这样做是为了知道是谁提交了该文件)如果想顺利执行,则需要先配置作者信息。

image

看不到的原因是git status只能查看暂存区和工作区的东西

(5)配置作者信息(全局、局部)

--global就是设置全局,没有就是局部

全局的意思是:当前操作系统下所有仓库提交到版本库时,都用这个作者
局部的意思是:只针对当前仓库用这个作者

如果局部没有就使用全局,全局没有就报错

git config --global user.email "913650502@qq.com"
git config --global user.name "zy"

image

(6)查看版本库版本信息
"查看当前版本及当前版本之前的版本
git log

"查看所有版本,不管是否回退"
git reflog  //比上面的看起来更精简一点

——————————————————————

'''git log 和git reflog的区别:'''
    git log 命令可以显示所有提交过的版本信息如果感觉太繁琐,可以加上参数  --pretty=oneline,只会显示版本号和提交时的备注信息git reflog 可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作)# git reset --hard,--mix,--soft的区别hard (硬)-> 全部删除,会彻底返回到回退前的版本状态,了无痕迹mixed (中)-> 保留工作目录,文件回退到未commit的状态soft (软)-> 保留工作目录、暂存区 ,文件会回退到未 add(未到暂存)的状态总结:soft是撤销commit的提交,但工作区未提交的更改还是保留;mixed是撤销暂存区的提交,工作区的更改同样也保留;而hard是把工作区、暂存区、commit到仓库的三个版本都回滚了

image

(7)把工作区的变更回退(还没放到暂存区)

不包含新增包含修改和删除

git checkout .  //当前所有的更改都改回退,不包含新增的文件
git checkout xxx  //当前xxx文件的更改回退

举例:在zy文件中给1.txt里写入新的内容,然后用git status查看状态发现1.txt变红了,当执行了git checkout会把工作区的变更回退。此时1.xtx里的内容也没了

(8)把暂存区拉回到工作区(绿变红)
git reset HEAD

举例:当工作区的变更放到了暂存区后,如果想回到工作区则需要用git reset HEAD即可拉回工作区绿变红

(9)把工作区回退到某个版本

只要被版本管理了就可以回退到任意版本,包括删除了某文件(前提是记录要提交到版本库),可以回退到某文件还在的时候。

git reset --hard 版本号

image

还可以继续执行git reset --hard 版本号继续切换

4)git忽略文件

如果项目中某些文件或文件夹必须要有,但是又不能提交它们,也就是不想被git管理,就可以设置忽略文件

使用步骤

第一步:在仓库目录下 (.git所在的目录下)新建【.gitignore】文件注意不要有后缀名

第二步:里面写要忽略的文件

node_models
.idea
*.log
scripts
__pycache__
*.pyc

注:已经被管理过的不会被忽略

image

忽略文件的写法

文件或文件夹名:代表所有目录下同名文件或文件夹都被忽略

/文件或/文件夹名:代表仓库根目录下的文件或文件夹被忽略

//例如:
写a.txt:项目中所有a.txt文件和文件夹都会被过滤
写/a.txt:项目中只有根目录下d的a.txt文件或文件夹会被过滤
写/b/a.txt:项目中只有根目录下的b文件夹下的a.txt文件或文件夹会被过滤
写*x*:名字中有一个x的都会被过滤(*代表0~n个任意字符)
空文件夹不会被提交,空包会被提交,包可以被提交(包中有一个init空文件)

5)git多分支管理

分支可以更方便协同开发、管理版本

image

master主分支给用户看,我们对dev分支做开发,做完一部分后再和master主分支合并,这样就避免了开发过程中用户看不了

分支操作
//查看分支(绿色的表示当前所在分支)
git branch

//创建dev开发分支
git branch dev

//切换分支
git checkout dev  

//删除分支(不能在当前分支删当前分支)
git branch -d dev
//合并分支(重要)
1.在dev开发分支中做完操作后要想跟主分支合并
2.需先进入到master主分支中:git checkout master
3.执行合并命令:git merge dev   # 把dev合并到我(master)身上
________________________________________________________________
"""分支合并可能会出冲突(会面会详讲)"""
# 公司里分支方案:小公司
    1.有master分支、dev分支、bug分支 :所有人在dev分支开发,bug分支用来修复master出现bug的,开发完成合并到主分支
    2.有master分支、dev分支、bug分支 、个人分支:其他分支用法一样,个人在个人分支上开发,开发完成合并到dev分支(最后老大再合并到主分支)

6)git连接远程仓库

把本地的版本提交到远程仓库

远程仓库有:gitee、github、gitlab...

gitlab的搭建过程:https://zhuanlan.zhihu.com/p/413217715

我们这里主要以gitee作为远程仓库

luffy_api 已经写好了部分功能,现在需要把代码提交到远程仓库中

(1)https连接远程仓库

操作本地

第一步:把luffy_api变成本地仓库git init

第二步:新建忽略文件.gitignore

.idea
*.log
scripts
__pycache__
*.pyc

第三步:把里面东西提交到版本库

git add . git commit -m '第一次提交'

--------这样就会被版本管理----------

操作远程:

第一步:远程要创建一个仓库存放:gitee上创建一个仓库(右上角+)【仓库必须是空的

image

image

第二步:在本地中重新配置一下作者信息(如果不按照上面的配置,在右侧贡献者会有别人的头像)

git config --global user.name "oreox"
git config --global user.email "913650502@qq.com"

第三步:我们是已有仓库,所以输入下面两条

#添加一个远程仓库地址 名字叫origin,https地址是https://gitee.com/oreoxx/luffy_api.git
git remote add origin https://gitee.com/oreoxx/luffy_api.git

#把本地当前所在分支推送到origin远程仓库的master主分支上
git push origin master

git remote查看远程仓库

git remote remove xxx删除远程仓库xxx

注意:第一次推送需要输入gitee的用户名和密码,以后不用再输了(本地保存在了:凭据管理)

先提交再忽略的问题
# 如果luffy.log已经进了版本库:
    luffy.log>>去了暂存区>>又去了版本库
# 然后再写忽略文件会发现忽略不成功,只要luffy.log有变化还是会被监控到
    .gitignore   
——————————————————————————————
#【解决:】
    删除luffy.log>>git add .>>git commit -m '删除文件'  # 此时版本库中已经没有luffy.log了,再出的luffy.log,就不会被管理了
(2)ssh方式连接远程仓库

以上提交代码还要输入用户名密码才可以提交上去,使用的是https的协议提交的代码

现在学习用ssh方案:需要公钥和私钥【公司中用该方法的多

https与ssh的区别

https:输入用户名密码,保存在凭据管理中
ssh:配置公钥私钥,私钥在本地 公钥在gitee,配置好远程仓库以后直接提交不需要输任何认证

操作

首先:先把刚刚https中在凭据管理里的用户名和密码删除

image

第一步:生成公钥私钥https://gitee.com/help/articles/4181

# 执行命令(一直按回车)
ssh-keygen -t ed25519 -C "12537932+oreoxx@user.noreply.gitee.com"  

然后就会在C:/Users/ovo/.ssh 下生成两个文件,一个是公钥一个是私钥

image

第二步:把公钥文件打开复制里面的公钥配置在gitee上

image

第三步:之前我们的远程仓库origin对应的地址是https协议的,现在用ssh提交代码需要把它删了然后增加成ssh协议的。

image

//删除远程仓库origin
git remote remove origin

//添加一个远程仓库地址 名字叫origin,ssh地址是git@gitee.com:oreoxx/luffy_api.git
git remote add origin git@gitee.com:oreoxx/luffy_api.git

这样以后我们再把本地分支推送到远程仓库则不需要任何验证(第一次还是要输入yes,但是ssh比https更安全,公司中常用该方法)

7)协同开发

虽然我们[仓库管理员]创建了仓库,且对仓库又所有权限,但是后期我们要做多人开发,张三李四在自己的仓库中是不能看到我们仓库中的内容的,只有把他们设置为该仓库的开发者才能在他们的仓库里看到我们仓库中的项目

开发者:拥有读写权限
观察者:拥有读权限

image

(1)作为开发者拉远程仓库

第一步:我们本地是没有代码的,需要把代码从远程仓库clone(克隆)下来

image

//如果用的是ssh 一定要在本地配好ssh
//找个地方新建一个文件夹>>右键Git Bash Here 输入命令即可把远程仓库中的代码拉下来
git clone git@gitee.com:lmmxka/luffy_api.git

image

第二步:用pycharm打开,配置好解释器,安装依赖

【配置解释器:】

image

image

【安装依赖:】

pip install -r requirements.txt 

第三步:开发代码>>开发完

# 开发完代码后执行:

git add . //当前路径下所有变更的都提交到暂存区

# 此时可能还需要设置全局本地的用户名和邮箱 可能不需要:局部没有就会找全局
git config user.name "oreox"
git config user.email "913650502@qq.com"


git commit -m '需写个注释内容'  //把暂存区所有内容提交到版本库

git push origin master  //把本地当前所在分支推送到origin远程仓库的master主分支上
(2)刚去公司的操作步骤
1 自己注册或hr给'gitlab'账号和密码
         需:自己去你的账号里,配置ssh
2 老大会把一个仓库地址给你 【他已经把你加成开发者了】  https  ssh
3 去把仓库中的项目代码clone下来
4 使用pycharnm打开>>配置环境>>安装依赖>>代码运行起来
5 开发代码>>本地提交>>push到远端
    	add
        commit
        push

8)冲突解决

出现冲突的原因

多人在同一分支下开发:张三和李四一起开发,张三先推送到远程仓库,李四则需要去远程仓库拉下来最新的数据,此时拉下来的数据可能和李四开发的地方一样,就造成了冲突
分支合并:假设master被bug分支更改了一个地方,而我们开发分支做完要和master合并时发现某一行被改过 这就造成了冲突

演示:多人在同一分支开发的冲突
张三:
       git pull origin master  //从远程仓库拉最新的数据下来
       在requirements.txt最后一行加入xxx
       git add .
       git commit -m '提交了数据'
       git push origin master  //从本地推送到远程仓库
        
我:
        在requirements.txt最后一行加入xxx
        git add .
        git commit -m '提交了数据'
        git pull origin master //从远程仓库拉最新的数据下来
        # 此时就冲突了
        # 解决冲突:箭头去掉,阅读代码,选择保留或删除别人或自己的代码,然后再提交
        git push origin master  //从本地推送到远程仓库
        
——————————————————————————————————
        
# 冲突出现的样子:如果要删除别人的代码,需要跟别人说一声
    <<<<<<< HEAD   //我的代码
    print('刘亦菲')
    =======              //他的代码
    print('彭于晏')
    >>>>>>> 6f720edbd84c8744b1c7c10767fb89a5d0fa98f5
    
——————————————————————————————————
# 如何避免出现冲突
 	-你如果想少出现冲突,勤拉取代码
演示:分支合并的冲突
先创建一个dev分支:git branch dev
————————————————
【dev分支】
1.切换到dev分支:git checkout dev
2.在dev分支的views.py 中第2行加入代码
3.提交到版本库:git add .>>>>git commit -m '提交'
        
————————————————
【master主分支】
1.切换到主分支:git checkout master
2.在主分支的views.py 中第2行加入代码
3.提交到版本库:git add .>>>>git commit -m '提交'
4. 在主分支合并代码:git merge dev
5.此时就冲突了,解决冲突后
6.再提交代码:git add .>>>>git commit -m '提交'

9)线上分支合并

一般公司都是本地合并最后推到远端上,线上合并仅作了解


#但是把当前分支推到远端可能会用
git branch zz //本地创建一个zz分支
git checkout zz //切换进zz分支
git origin zz //把当前zz分支推到远端

前面的多分枝管理合并是线下的git merge dev

除了线下还有线上分支合并

1.gitee上【远端新建】一个dev分支
2.本地同步远端的dev分支
        git pull origin dev  // 拉下来了还看不到,只有切过去才能看到
        git checkout dev  // 切到dev分支中
3.在dev上开发然后提交到本地dev中
        git add .  //当前路径下所有变更的都提交到暂存区
        git commit -m '提交'  //把暂存区的内容提交到版本库
        # 上述现在是提交到本地的dev分支中
4.推到远端dev分支中:
        git push origin dev

image

此时远端master主分支中没有代码,远端dev开发分支中有提交的代码,我们需要线上两个分支合并

1.把远端的dev合并进远端的master
2.提交pr/提交rr:需注意是把dev合并进master,所以要切到master中去点'Pull Requests'>>新建Pull Requests>>创建
3.提交的申请就会到老大那里>>老大负责审核、测试、合并
这样线上dev就合并进线上的master了

image

image

image

10)远程仓库回滚

不要用,因为用了后好几个人一个月辛苦白费

# 如果想远程的代码变成最初的状态

# 步骤:
	1 本地版本回退
     git reset --hard 版本号
        
    2 强行推到远端
    git push origin master -f

11)如何为开源项目贡献代码

1.看到开源项目点fork,你的仓库就有这个开源项目了
2.在pycharm就可以clone在本地(后面会讲pycharm操作克隆) 你仓库fork的代码
3.你继续写提交到自己远程仓库git add.git commit -m '补充'git push origin master推到自己的远端仓库
4.在远端仓库>>Pull Requests创建(把我的合并到他的)并提交pr合并如果作者同意你就可以合并进去了

image

image

12)pycharm操作git

图形化界面操作,以前使用的所有命令都可以在pycharm中用点的方式实现

需注意:只有没作为仓库被git管理的可以点开

还需配置一下git的安装路径(虽然有的机器可以自动识别)

image

(1)clone克隆

填写远端地址即可

image

(2)git add

根路径下的是git add .

某个文件右键是git add 某文件

image

(3)git commit

image

image

(4)git push

image

(5)git pull

image

(7)git branch

image

image

(8)git log

image

(9)本地代码跟版本库比较

image

image

(10)查看修改和远程仓库的对应

image

14)git面试题

1 你们公司分支方案是什么样的?
    -master,dev,bug 三条分支
    -master主要用来发布版本,写好了某个版本的代码合并进去,不直接在master上开发
    -dev:开发分支,项目的开发者,都在dev分支上开发
    -bug:bug分支,用来修改bug,发布小版本

2 使用git开发,遇到过冲突吗?
    -遇到过
    -多人在dev分支开发,出现的冲突
    -分支合并出现的冲突
    -把代码拉下来,直接解决冲突,保留我的代码,保留同事的代码
    
3 你知道git 变基?
    -分支合并:dev分支合并到master分支
    -merge或rebase 合并
    -把多次提交合并成一个
    
4 git pull 和git fetch的区别
    -pull 和 fetch都是拉取代码
    -pull=fetch+合并
    
5 你知道git flow吗?git 工作流,它是一个别人提出的分支方案
    -我们没有用,我们用的就是master+dev+bug分支方案
    
6 使用git 的操作流程
    -如果是普通开发者:git clone下来,写代码,git add ., git commit, git pull, git push
    
7 什么是gitee,github:pr,gitlab:mr?
    -不同叫法:提交分支合并的请求

十三.登录注册功能

image

# 接口分析
1.多方式登录接口:用户名/手机号/邮箱   +  密码都可以登录的接口
2.校验手机号是否存在接口
3.发送手机验证码接口(借助第三方短信平台:腾讯云)
4.短信登录接口
5.短信注册接口

1)校验手机号是否存在接口

luffy_api>>urls.py主路由中分发路由给user

path('api/v1/user/', include('user.urls')),

luffy_api>>apps>>user>>urls.py分路由

from rest_framework.routers import SimpleRouter
from . import views

router = SimpleRouter()
# http://127.0.0.1:8000/api/v1/user/userinfo/check_mobile/  验证手机号是否存在
router.register('userinfo',views.UserView,'userinfo')
urlpatterns = [ ]
urlpatterns += router.urls

luffy_api>>apps>>user>>views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from .models import User
from rest_framework.exceptions import APIException
from django.utils.datastructures import MultiValueDictKeyError
from django.core.exceptions import ObjectDoesNotExist


class UserView(GenericViewSet):
    # 给视图类中定义的函数自动生成路由(detail:带id的路径用True,否则用False)
    @action(methods=['GET'], detail=False)  # 保证这个接口的安全(短信轰炸机:可以破解很多发送短信的接口)
    # 验证手机号是否存在接口
    def check_mobile(self, request, *args, **kwargs):
        try:
            # 从地址栏中取出手机号(get请求的数据都要在request.query_params中获取)
            mobile = request.query_params['mobile']
            # 根据手机号获取当前登录用户
            User.objects.get(mobile=mobile)
        # 获取不到报异常 会被全局异常捕获
        except MultiValueDictKeyError as e:
            raise APIException('没传手机号')
        except ObjectDoesNotExist as e:
            raise APIException('手机号不存在')
        except Exception as e:
            raise e
        # 当mobile有值,且也有该手机号的用户,则返回手机号已存在(返回100)
        return APIResponse(msg='手机号已存在')

补充:今后写视图函数可以按照该模板写

    def check_mobile(self, request, *args, **kwargs):
        try:
            # 放心写代码,出了异常会被下面异常捕获
        except Exception as e:
            raise e # 把异常往外抛会被全局异常捕获
        # 如果成功则会走这条
        return APIResponse()

补充:

视图函数、视图类的方法返回值return一定是响应对象(django原生4个drf的1个自己封装的1个

raise的对象必须是错误对象(KeyError、APIException、自定义异常)

image

2)多方式登录接口

可以使用:用户名/手机号/邮箱 + 密码 去登录

发送post请求>>>username可能是用户名/手机号/邮箱 密码则是正常的用户密码

luffy_api>>apps>>user>>views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from .models import User
from .serialzier import UserLoginSerializer

class UserView(ViewSetMixin, GenericAPIView):
    # 验证手机号是否存在接口
    ......
    
    # 要使用的序列化类
    serializer_class = UserLoginSerializer
    # 要序列化或反序列化的表模型数据(筛选掉被锁定的用户)
    queryset = User.objects.all().filter(is_active=True)

    # 给视图类中定义的函数自动生成路由(detail带id的路径用True,否则用False)
    @action(methods=['POST'], detail=False)
    # 多方式登录接口
    def login_mul(self, request, *args, **kwargs):
        """
        把以下逻辑放在序列化类(全局钩子)中:
        1.取出前端传入的用户名和密码
        2.通过用户名和密码去数据库查询用户
        3.如果有该用户则签发token并返回给前端登录成功,没有就返回失败
        """
        # 获取要使用的序列化类(前端提交来的数据都在request.data中)
        # 实例化序列化类里的对象时可以传入context字典,有了序列化类对象后通过对象.context就可以拿到序列化类中全局钩子中放入的值
        # context就是视图类和序列化类沟通的桥梁(序列化类想给视图类值,可以放到context中)
        ser = self.get_serializer(data=request.data)
        # 判断数据是否全部符合自己的校验要求>>局部钩子>>全局钩子(raise_exception=True当不符合时捕获全局异常)
        ser.is_valid(raise_exception=True)
        token = ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token, username=username)  # 符合则返回{code:100,msg:成功,token:xxx,username:zy},不符合全局返回异常

luffy_api>>apps>>user>>serialzier.py

from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import APIException
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


# 这个序列化类用来校验字段:不序列化(返回给前端)[因为前端只需要token]和反序列化(保存到数据库)
class UserLoginSerializer(serializers.ModelSerializer):
    """
    username是映射过来的,在User表中是唯一的,会导致字段自己的校验过不去,所以需要重写username字段把原有校验规则去掉
    """
    username = serializers.CharField()

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

    # 全局钩子校验
    def validate(self, attrs):
        """
        把以下逻辑放在序列化类(全局钩子)中:
        1.取出前端传入的用户名和密码
        2.通过用户名和密码去数据库查询用户
        3.如果有该用户则签发token并返回给前端登录成功,没有就返回失败
        """
        # 获取登录用户和token
        # attrs:前端传入的所有数据,然后经过字段自己校验和局部钩子校验过后的数据{username:用户名/手机号/邮箱,password:123}
        user = self._get_user(attrs)
        token = self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token'] = token
        self.context['username'] = user.username
        return attrs

    """
    在类内部隐藏属性和方法是__开头,但是公司中不用__开头,用_开头 表示不想给外部用,如果实在想用也可以直接用
    """
    # 获取登录用户
    def _get_user(self, attrs):
        # 判断用户名和密码是否正确
        username = attrs.get('username')
        password = attrs.get('password')
        # 判断用户传来的用户名是手机号还是邮箱还是用户名
        if re.match(r'^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        # 如果用户名和密码一致则登录成功
        if user and user.check_password(password):
            return user
        else:
            # 全局钩子要求如果校验不过必须抛异常(抛的异常可被全局异常捕获到)
            raise APIException('用户名或密码错误')

    # 根据用户签发token
    def _get_token(self, user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

3)腾讯云短信申请

发送短信接口需借助于第三方平台,但都是收费的:腾讯云短信、阿里大于短信..
腾讯云:https://cloud.tencent.com/

申请步骤

第一步:在页面中搜索短信>>扫码登录>>微信完成审核

image

第二步:去微信公众平台注册订阅号

微信公众平台:https://mp.weixin.qq.com/

第三步:去腾讯云创建签名、模板等待审核即可

image

第四步:审核通过后我们用python代码发送短信

​ 需先在腾讯云中找短信文档:https://cloud.tencent.com/document/product/382

​ 发现里面有两个短信文档:API文档SDK文档

image

4)腾讯云短信开发

如何保证发送短信接口的安全(不被第三方盗用)
1.加频率限制(限制手机号一分钟发一次)
2.随机字符串验证码:当你来了发送短信页面就发给你一个随机字符串,等你发短信的时候会携带该字符串,我们在后端验证。
类似解决csrf跨站请求伪造:
原理】:同一个浏览器中,先在银行登录成功cookie会存在浏览器中;页面不关闭再打开一个恶意网站,该网站中有一个按钮点击向招商银行发送请求可以发成功,服务器也会正常响应,但浏览器会拦截(跨域)。
一但登录银行成功,只要向招商银行发请求就会自动携带cookie,这个时候如果我要转账且在恶意网站点击按钮后,就会转到恶意网站上。这就是csrf
解决】:如果发送post请求(增加、修改)需要携带随机字符串,如果没带就禁止(随机字符串是只要来到发送post请求的页面后端就会随机生成)

接口幂等性

API和SDK的区别

API: 前面学过的API接口,写起来【比较麻烦】,要自己分析接口

SDK:集成开发工具包,分语言使用:java、python、go..
使用python对API封装成包
以后我们只需要:安装包>>导入包>>包名.发送短信>>传入参数就可以发送
sdk有两个版本:
2.0版本:简单,但只有发送短信功能
3.0版本:云操作的sdk,不仅有发送短信,还有云功能的其他功能

(1)安装3.0版本sdk

image

#方式一:pip命令执行(推荐)
pip install tencentcloud-sdk-python

#方式二:下载源码安装(当pip安装不了就用该方法安装)
进入Gitee下载http zip>>下载后命令切到该文件位置处执行:
python steup.py install

image

(2)测试发送短信代码

luffy_api>>scripts>>新建【send_sms】

仅测试能否发送成功

from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
try:
    # 必要步骤:
    # 实例化一个认证对象,入参需要传入腾讯云账户密钥对【secretId】,【secretKey】。
    # 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
    # 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
    # 以免泄露密钥对危及你的财产安全。
    # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi
    cred = credential.Credential("AKIDtH1ZtxG", "ESdhgSMcdTA")
    # cred = credential.Credential(
    #     os.environ.get(""),
    #     os.environ.get("")
    # )

    # 实例化一个http选项,可选的,没有特殊需求可以跳过。
    httpProfile = HttpProfile()
    # 如果需要指定proxy访问接口,可以按照如下方式初始化hp(无需要直接忽略)
    # httpProfile = HttpProfile(proxy="http://用户名:密码@代理IP:代理端口")
    httpProfile.reqMethod = "POST"  # post请求(默认为post请求)
    httpProfile.reqTimeout = 30  # 请求超时时间,单位为秒(默认60秒)
    httpProfile.endpoint = "sms.tencentcloudapi.com"  # 指定接入地域域名(默认就近接入)

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

    # 实例化要请求产品(以sms为例)的client对象
    # 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
    client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)

    # 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
    # 你可以直接查询SDK源码确定SendSmsRequest有哪些属性可以设置
    # 属性可能是基本类型,也可能引用了另一个数据结构
    # 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明
    req = models.SendSmsRequest()

    # 基本类型的设置:
    # SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。
    # SDK提供对基本类型的指针引用封装函数
    # 帮助链接:
    # 短信控制台: https://console.cloud.tencent.com/smsv2
    # 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81

    # 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666
    # 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
    req.SmsSdkAppId = "14008097"
    # 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名
    # 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
    req.SignName = "Oreoxxx公众号"
    # 模板 ID: 必须填写已审核通过的模板 ID
    # 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
    req.TemplateId = "17265"
    # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空
    req.TemplateParamSet = ["8888"]
    # 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
    # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
    req.PhoneNumberSet = ["+8615222222222"]
    # 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回
    req.SessionContext = ""
    # 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手]
    req.ExtendCode = ""
    # 国际/港澳台短信 senderid(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手]
    req.SenderId = ""

    resp = client.SendSms(req)

    # 输出json格式的字符串回包
    print(resp.to_json_string(indent=2))
    # 当出现以下错误码时,快速解决方案参考
    # - [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
    # - [FailedOperation.TemplateIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.templateincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
    # - [UnauthorizedOperation.SmsSdkAppIdVerifyFail](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunauthorizedoperation.smssdkappidverifyfail-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
    # - [UnsupportedOperation.ContainDomesticAndInternationalPhoneNumber](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunsupportedoperation.containdomesticandinternationalphonenumber-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
    # - 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms)
except TencentCloudSDKException as err:
    print(err)

(3)发送短信验证码接口
#目录结构:
luffy_api>> libs下:
        send_sms_v3
            __init__.py
            settings.py
            sms.py

luffy_api>>libs>>新建【send_sms_v3包】>>新建【settings.py

# 查看:https://console.cloud.tencent.com/cam/capi
SECRET_ID = 'AKIDtH1Ztx56o6Ae
SECRET_KEY = 'ESdhgSdnxzmJcjbI'
# 查看:https://console.cloud.tencent.com/smsv2/app-manage
APP_ID = '14007'
# 查看:https://console.cloud.tencent.com/smsv2/csms-sign
SIGN_NAME = 'Oreoxxx公众号'
# 查看:https://console.cloud.tencent.com/smsv2/csms-template
TEMPLATE_ID = '17265'

luffy_api>>libs>>send_sms_v3>>新建【sms.py

import random
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
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 . import settings
import json


# 随机生成4位数字验证码函数
def get_code(n=4):
    code = ''
    for i in range(n):
        code += str(random.randint(0, 9))
    return code


# 发送短信函数
def send_sms(code, mobile):
    # 直接cv
    try:
        # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi
        cred = credential.Credential(settings.SECRET_ID, settings.SECRET_KEY)
        httpProfile = HttpProfile()
        httpProfile.reqMethod = "POST"  # post请求(默认为post请求)
        httpProfile.reqTimeout = 30  # 请求超时时间,单位为秒(默认60秒)
        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()
        # 短信应用ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
        req.SmsSdkAppId = settings.APP_ID
        # 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
        req.SignName = settings.SIGN_NAME
        # 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
        req.TemplateId = settings.TEMPLATE_ID
        # 发送的内容(模板有两个需要填的就写两个)
        req.TemplateParamSet = [code]
        # 手机号,可填多个
        req.PhoneNumberSet = ["+86" + mobile]
        # 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回
        req.SessionContext = ""
        # 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手]
        req.ExtendCode = ""
        # 国际/港澳台短信 senderid(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手]
        req.SenderId = ""
        resp = client.SendSms(req)
        # 输出json格式的字符串回包
        res = json.loads(resp.to_json_string(indent=2))
        # 看发送结果是True还是False
        if res.get('SendStatusSet')[0].get('Code') == 'Ok':
            return True
        else:
            return False
    except TencentCloudSDKException as err:
        print(err)
        return False

luffy_api>>libs>>send_sms_v3>>新建【__init__.py

# 导获取验证码、发送手机短信验证码 的时候可以简写 不用写那么长
from .sms import get_code, send_sms

luffy_api>>apps>>user>>views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from rest_framework.exceptions import APIException
# 由于在send_sms_v3下的__init__中导了 所以可以简写,且因为views中有send_sms会重名所以起别名
from libs.send_sms_v3 import get_code, send_sms as send_sms_ss
from django.core.cache import cache


class UserView(GenericViewSet):
    # 验证手机号是否存在接口
    ...
    # 多方式登录接口
    ...
    # 给视图类中定义的函数自动生成路由(detail带id的路径用True,否则用False)
    @action(methods=['POST'], detail=False)
    # 发送手机短信验证码接口
    def send_sms(self, request):
        try:
            # 获取前端传来的手机号
            mobile = request.data.get('mobile')
            # 生成验证码
            code = get_code()
            # 在缓存中存储验证码(用于短信登录接口的校验) K值必须唯一,V值可以是任意类型
            # 目前还没有讲redis,暂时先存到缓存中(项目部署时会出问题),后期还是要用redis
            cache.set('sms_code_%s' % mobile, code)
            # 发送短信(这里是同步发送,会一值等发送完再往下走。可以用多线程实现类似异步的操作。后期可以改成异步)
            res = send_sms_ss(code, mobile)
            if res:
                return APIResponse(msg='验证码短信发送成功')
            else:
                return APIResponse(code=101, msg='验证码短信发送失败')
        except Exception as e:
            raise APIException(str(e))

image


多线程实现异步发送短信

与普通版本的区别是 :普通版本知道发没发送成功,该版本只管发不知道成功与否(大多APP都用该方法)
好处是:如果服务器在发送短信那里卡住了,要等好久,该接口不用等直接下一步

luffy_api>>apps>>user>>views.py

from threading import Thread  # 开启线程


class UserView(GenericViewSet):
    # 验证手机号是否存在接口
    ...
    # 多方式登录接口
    ...
    # 给视图类中定义的函数自动生成路由(detail带id的路径用True,否则用False)
    @action(methods=['POST'], detail=False)
    # 发送手机短信验证码接口
    def send_sms(self, request):
        try:
            # 获取前端传来的手机号
            mobile = request.data.get('mobile')
            # 生成验证码
            code = get_code()
            # 在缓存中存储验证码用于短信登录接口的校验 K值必须唯一
            # 目前还没有讲redis,暂时先存到缓存中(项目部署时会出问题),后期还是要用redis
            cache.set('sms_code_%s' % mobile, code)
            # 多线程实现异步发送短信
            t = Thread(target=send_sms_ss, args=[code, mobile])
            t.start()
            return APIResponse(msg='短信已发送,成没成功不管我事')
        except Exception as e:
            raise APIException(str(e))

image

6)短信登录接口

分析

1.前端发送{mobile:15222222222,code:8888}>>post>>后端
2.视图类的方法中的逻辑
    1)取出手机号和验证码
    2)校验验证码是否正确(发送短信验证码接口需要存储验证码这里才好校验)
        -session:以后根本不用
        -全局变量:不好用,可能会取不到 尤其在集群环境中
        -'缓存':django 自带缓存
            from django.core.cache import cache
            cache.set()   # 往缓存中放
            cache.get()  # 从缓存中取
    3)根据手机号查询用户
    4)如果能查到则签发token
    5)返回给前端
(1)第一版封装

luffy_api>>apps>>user>>views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from utils.common_response import APIResponse
from .serialzier import UserLoginSerializer, UserMobileSerializer

class UserView(GenericViewSet):
    # 验证手机号是否存在接口
    ......

    # 给视图类中定义的函数自动生成路由(detail带id的路径用True,否则用False)
    @action(methods=['POST'], detail=False)
    # 多方式登录接口
    def login_mul(self, request, *args, **kwargs):
        return self._login(request, *args, **kwargs)

    # 发送手机短信验证码接口
    ...

    # 重写get_serializer_class
    def get_serializer_class(self):
        # 如果访问login_sms方法,使用的序列化类就是UserMobileSerializer,否则还是之前的序列化类
        if self.action == 'login_sms':
            return UserMobileSerializer
        else:
            # get_serializer_class是GenericAPIView里的方法,查看源码发现返回了serializer_class
            return super().get_serializer_class()

    # 给视图类中定义的函数自动生成路由(detail带id的路径用True,否则用False)
    @action(methods=['POST'], detail=False)
    # 短信登录接口
    def login_sms(self, request, *args, **kwargs):
        return self._login(request, *args, **kwargs)

    # 多方式登录接口 和 短信登录接口封装
    def _login(self, request, *args, **kwargs):
        # 获取要使用的序列化类(前端提交来的数据都在request.data中)
        ser = self.get_serializer(data=request.data)
        # 判断数据是否全部符合自己的校验要求>>局部钩子>>全局钩子(raise_exception=True当不符合时抛异常被全局异常捕获)
        ser.is_valid(raise_exception=True)
        # 实例化序列化类里的对象时可以传入context字典,有了序列化类对象后通过对象.context就可以拿到序列化类中全局钩子中放入的值
        # 获取序列化类中context传来的token和用户名,context就是视图类和序列化类沟通的桥梁(序列化类想给视图类值,可以放到context中)
        token = ser.context.get('token')
        username = ser.context.get('username')
        return APIResponse(token=token, username=username)  # 符合则返回

luffy_api>>apps>>user>>serialzier.py

# 手机号短信登录序列化类
class UserMobileSerializer(serializers.ModelSerializer):
    # mobile是唯一约束,也需要重写校验规则
    # code不是表里的字段 需要重写校验规则
    mobile = serializers.CharField()
    code = serializers.CharField()

    class Meta:
        model = User
        # 要序列化的字段
        fields = ['mobile', 'code']

    # 全局钩子校验
    def validate(self, attrs):
        # 获取登录成功的用户和token
        # attrs:前端传入的所有数据,然后经过字段自己校验和局部钩子校验过后的数据
        user = self._get_user(attrs)
        token = self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token'] = token
        self.context['username'] = user.username
        return attrs

    # 获取登录用户
    def _get_user(self, attrs):
        # 校验手机号、验证码是否正确
        mobile = attrs.get('mobile')
        code = attrs.get('code')
        # 从缓存中取出验证码(用于短信登录接口的校验)
        old_code = cache.get('sms_code_%s' % mobile)
        if old_code and old_code == code:
            # 根据手机号获取用户
            user = User.objects.filter(mobile=mobile).first()
            if user:
                return user
            else:
                raise APIException('暂无该用户')
        else:
            raise APIException('验证码验证失败')

    # 根据用户签发token
    def _get_token(self, user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token
(2)第二版封装

第一版的序列化类中重复的代码还是有点多,还可以继续封装:写一个类来继承它

因为[多方式登录序列化类]和[手机号短信验证码登录序列化类]里的代码大致差不多,区别仅在于获取登录用户上,所以让他们继承一个父类,父类里有他们要的全局钩子校验获取登录用户(抛一个必须重写的异常)、根据用户签发token,后面序列化类只需要重写获取登录用户即可!

from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import APIException
from rest_framework_jwt.settings import api_settings
from django.core.cache import cache

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


# 写一个父类
class BaseUserSerializer:
    # 全局钩子校验
    def validate(self, attrs):
        # 获取登录成功的用户和token
        # attrs:前端传入的所有数据,然后经过字段自己校验和局部钩子校验过后的数据
        user = self._get_user(attrs)
        token = self._get_token(user)
        # 把用户名和token放到ser的context中
        self.context['token'] = token
        self.context['username'] = user.username
        return attrs

    # 获取登录用户
    def _get_user(self, attrs):
        raise Exception('必须重写')

    # 根据用户签发token
    def _get_token(self, user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token


# 多方式登录序列化类
class UserLoginSerializer(BaseUserSerializer, serializers.ModelSerializer):
    """
    username是映射过来的,在User表中是唯一的,会导致字段自己的校验过不去,所以需要重写username字段把原有校验规则去掉
    """
    username = serializers.CharField()

    class Meta:
        model = User
        # 要序列化的字段
        fields = ['username', 'password']

    # 获取登录用户
    def _get_user(self, attrs):
        # 判断用户名和密码是否正确
        username = attrs.get('username')
        password = attrs.get('password')
        # 判断用户传来的用户名是手机号还是邮箱还是用户名
        if re.match(r'^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        # 如果用户名和密码一致则登录成功(由于继承了auth_user表,里面的密码是密文所以用check_password)
        if user and user.check_password(password):
            return user
        else:
            # 全局钩子要求如果校验不过必须抛异常(抛的异常可被全局异常捕获到)
            raise APIException('用户名或密码错误')


# 手机号短信登录序列化类
class UserMobileSerializer(BaseUserSerializer, serializers.ModelSerializer):
    # mobile是唯一约束,也需要重写校验规则
    # code不是表里的字段 需要重写校验规则
    mobile = serializers.CharField()
    code = serializers.CharField()

    class Meta:
        model = User
        # 要序列化的字段
        fields = ['mobile', 'code']

    # 获取登录用户
    def _get_user(self, attrs):
        # 校验手机号、验证码是否正确
        mobile = attrs.get('mobile')
        code = attrs.get('code')
        # 从缓存中取出验证码(用于短信登录接口的校验)
        old_code = cache.get('sms_code_%s' % mobile)
        if old_code and old_code == code:
            # 根据手机号获取用户
            user = User.objects.filter(mobile=mobile).first()
            if user:
                return user
            else:
                raise APIException('暂无该用户')
        else:
            raise APIException('验证码验证失败')

7)短信注册接口

c58bd02eae3ce1c9076de3cce8b6536

分析

前端发送{mobile:1888344,code:8888,password:123}>>post>>后端>>因为之前视图类里代码太多所以再写一个

luffy_api>>apps>>user>>urls.py

# http://127.0.0.1:8000/api/v1/user/register/  发post请求注册
router.register('register', views.RegisterUserView, 'register')

luffy_api>>apps>>user>>views.py

from rest_framework.viewsets import GenericViewSet
from utils.common_response import APIResponse
from .models import User
from .serialzier import  RegisterSerializer
from rest_framework.mixins import CreateModelMixin

# 注册视图类
class RegisterUserView(GenericViewSet, CreateModelMixin):
    # 要使用的序列化类
    serializer_class = RegisterSerializer
    # 要序列化或反序列化的表模型数据(筛选掉被锁定的用户)
    queryset = User.objects.all()

    # 由于CreateModelMixin里的返回格式不是我们想要的,所以需要重写create该方法中.data了
    def create(self, request, *args, **kwargs):
        super().create(request, *args, **kwargs)
        
        # 另一种写法,不用序列化
        # serializer = self.get_serializer(data=request.data)
        # serializer.is_valid(raise_exception=True)
        # self.perform_create(serializer)
        return APIResponse(msg='注册成功')

luffy_api>>apps>>user>>serialzier.py

from rest_framework import serializers
from .models import User
from rest_framework.exceptions import APIException
from rest_framework_jwt.settings import api_settings
from django.core.cache import cache


# 注册序列化类
# 需要数据校验、反序列化、序列化
class RegisterSerializer(serializers.ModelSerializer):
    # code不是数据库字段需要重写
    # 只要调用ser.data就会触发序列化,而code不是表字段一序列化就会报错
    code = serializers.CharField(max_length=4, write_only=True)
    class Meta:
        model = User
        fields = ['mobile', 'code', 'password']
        extra_kwargs = {
            'password': {'write_only': True}
        }

    # 全局钩子校验
    def validate(self, attrs):
        """
        1.取出前端传入的code,校验code是否正确
        2.注册成功把用户名改为手机号
        3.code不是数据库的字段,所以在存数据库之前需从attrs中剔除
        """
        # attrs:前端传入的所有数据,然后经过字段自己校验和局部钩子校验过后的数据
        code = attrs.get('code')
        print(code)
        mobile = attrs.get('mobile')
        # 从缓存中取出验证码(用于短信登录接口的校验)
        old_code = cache.get('sms_code_%s' % mobile)
        print(old_code)
        if old_code and old_code == code:
            # 把attrs中的用户名改为手机号
            attrs['username'] = mobile
            attrs.pop('code')
        else:
            raise APIException('验证码验证失败')
        return attrs

    # 必须重写create(因为密码是明文,如果不重写存入数据库中的也是明文)
    def create(self, validated_data):
        # 创建用户
        # validated_data:前端传过来校验后的数据{"username":"110","mobile":"110","password":123}
        user = User.objects.create_user(**validated_data)
        # 必须返回,因为后面ser.data要使用当前返回的对象做序列化
        return user

十四.登录注册页面分析

image

1.当点击登录>>弹出登录组件,盖住整个屏幕(定位)
2.点击登录组件中的X,关闭登录组件(子传父)

src>>components>>新建【Login.vue】

<template>
  <div class="login">
    <span style="padding: 50px" @click="closeLogin">X</span>
  </div>
</template>

<script>
export default {
  name: "Login",
  methods:{
    closeLogin(){
      this.$emit('go_close')
    }
  }
}
</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>

src>>components>>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 @click="goLogin">登录</span>
          <span class="line">|</span>
          <span>注册</span>
        </div>
      </div>
    </div>
    <!--给登录页面添加自定义事件-->
    <Login v-if="login_show" @go_close="goClose"></Login>
  </div>
</template>

<script>
import Login from "@/components/Login";

export default {
  name: "Header",
  data() {
    return {
      // 当前所在路径,去sessionStorage取的,如果取不到,就是 /
      url_path: sessionStorage.url_path || '/',
      login_show: false
    }
  },
  methods: {
    goPage(url_path) {
      // 已经是当前路由就没有必要重新跳转
      if (this.url_path !== url_path) {
        this.$router.push(url_path);
      }
      sessionStorage.url_path = url_path;
    },
    //点击登录显示登录页面
    goLogin() {
      this.login_show = true
    },
    //点击X关闭登录页面
    goClose(){
      this.login_show = false
    },
  },
  created() {
    // 组件加载完成,就取出当前的路径,存到sessionStorage  this.$route.path
    sessionStorage.url_path = this.$route.path;
    // 把url_path = 当前路径
    this.url_path = this.$route.path;
  },
  components: {
    Login,
  }
}
</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>

18

以上是实现点击登录即进入登录页面,X即关闭登录页面,下面我们开始给登录页面加样式

1)登录页面

-手机验证码登录>>输入框输入手机号>>监听失去焦点事件>>手机号正则校验(JS),查询手机号是否存在>>点击发送验证码的按钮发送ajax请求>>起个定时任务>>手机收到验证码,填入验证码框>>点击登录按钮向后端发送ajax请求>>返回给前端token和username>>前端保存信息到cookie中>>子传父关闭登录模态框>>在Header.vue取token和username

-多方式登录>>输入用户名和密码>>点击登录..与上面一致

src>>components>>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" @click="login">登录</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 @click="mobile_login" 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;
      // js正则:/正则语法/
      // '字符串'.match(/正则语法/)
      if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
        this.$message({
          message: '手机号有误',
          type: 'warning',
          duration: 1000,
          onClose: () => {
            this.mobile = '';
          }
        });
        return false;
      }
      // 手机号前端校验通过>>开始后端手机号是否存在的校验
      // 后台校验手机号是否已存在
      this.$axios({
        //http://127.0.0.1:8000/api/v1/user/userinfo/send_sms/?mobile=前端写入的手机号
        url: this.$settings.BASE_URL + '/user/userinfo/check_mobile/?mobile=' + this.mobile,
        method: 'get',
      }).then(response => {
        // code 如果是100说明手机号存在, 登录功能,才能发送短信
        // ==   只比较值是否相等
        // ===  既比较值,又比较类型
        if (response.data.code == 100) {
          this.$message({
            message: '账号正常',
            type: 'success',
            duration: 1000,
          });
          // 手机号正确且手机号存在,发送验证码按钮才可以被点击
          this.is_send = true;
        } else {
          this.$message({
            message: '账号不存在',
            type: 'warning',
            duration: 1000,
            onClose: () => {
              this.mobile = '';
            }
          })
        }
      }).catch(() => {
      });
    },
    //发送短信验证码接口
    send_sms() {
      // this.is_send 如果是false,函数直接结束,就不能发送短信
      if (!this.is_send) return;
      // 按钮点一次立即禁用
      this.is_send = false;

      let sms_interval_time = 60;
      this.sms_interval = "发送中...";

      // 定时器: setInterval(fn, time, args)

      // 往后台发送验证码
      this.$axios({
        url: this.$settings.BASE_URL + '/user/userinfo/send_sms/',
        method: 'post',
        data: {
          mobile: this.mobile
        }
      }).then(response => {
        if (response.data.code == 100) { // 发送成功
          // 启动定时器
          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);
        } else {  // 发送失败
          this.sms_interval = "重新获取";
          this.is_send = true;
          this.$message({
            message: '短信发送失败',
            type: 'warning',
            duration: 3000
          });
        }
      }).catch(() => {
        this.sms_interval = "频率过快";
        this.is_send = true;
      })


    },
    //多方式登录接口
    login() {
      if (!(this.username && this.password)) {
        this.$message({
          message: '请填好账号密码',
          type: 'warning',
          duration: 1500
        });
        return false  // 直接结束逻辑
      }

      this.$axios({
        url: this.$settings.BASE_URL + '/user/userinfo/login_mul/',
        method: 'post',
        data: {
          username: this.username,
          password: this.password,
        }
      }).then(response => {
        if (response.data.code == 100) {
          let username = response.data.username;
          let token = response.data.token;
          this.$cookies.set('username', username, '7d');
          this.$cookies.set('token', token, '7d');
          this.$emit('success', response.data.result);
        } else {
          this.$message(response.data.msg)
        }
      }).catch(error => {
        console.log(error.response.data)
      })
    },
    //手机短信登录接口
    mobile_login() {
      if (!(this.mobile && this.sms)) {
        this.$message({
          message: '请填好手机与验证码',
          type: 'warning',
          duration: 1500
        });
        return false  // 直接结束逻辑
      }
      this.$axios({
        url: this.$settings.BASE_URL + '/user/userinfo/login_sms/',
        method: 'post',
        data: {
          mobile: this.mobile,
          code: this.sms,
        }
      }).then(response => {
        if (response.data.code == 100) {
          let username = response.data.username
          let token = response.data.token
          // 放到cookie中,7天过期
          this.$cookies.set('username', username, '7d')
          this.$cookies.set('token', token, '7d')
          // 关闭登录框
          this.$emit('success')
        } else {
          this.$message(response.data.msg)
        }
      }).catch(error => {
        console.log(error.response.data)
      })
    }
  }
}
</script>

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

.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>

2)注册页面

	-输入手机号>>监听失去焦点事件>>手机号正则校验(js)查询手机号是否存在>>如果不存在发送验证码的按钮才可以点击>>点击发送验证码按钮发送ajax验证码>>起一个定时任务>>手机收到验证码填入验证码框>>填入密码>>点击注册>>调用注册接口完成注册>>子传父>>Register.vue>>显示Login.vue
	```

	src>>components>>新建【Register.vue】

	```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 @click="register" 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;
      // js正则:/正则语法/
      // '字符串'.match(/正则语法/)
      if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
        this.$message({
          message: '手机号有误',
          type: 'warning',
          duration: 1000,
          onClose: () => {
            this.mobile = '';
          }
        });
        return false;
      }
      // 后台校验手机号是否已存在
      this.$axios({
        url: this.$settings.BASE_URL + '/user/userinfo/check_mobile/',
        method: 'get',
        params: {
          mobile: this.mobile
        }
      }).then(response => {
        // 手机号不存在才能发送短信注册
        if (response.data.code != 100) {
          this.$message({
            message: '欢迎注册我们的平台',
            type: 'success',
            duration: 1500,
          });
          // 当手机号不存在,发送验证码按钮才可以被点击
          this.is_send = true;
        } else {
          this.$message({
            message: '账号已存在,请直接登录',
            type: 'warning',
            duration: 1500,
          })
        }
      }).catch(() => {
      });
    },
    //发送短信验证码接口
    send_sms() {
      // this.is_send必须允许发送验证码,才可以往下执行逻辑
      if (!this.is_send) return;
      // 按钮点一次立即禁用
      this.is_send = false;

      let sms_interval_time = 60;
      this.sms_interval = "发送中...";

      // 往后台发送验证码
      this.$axios({
        url: this.$settings.BASE_URL + '/user/userinfo/send_sms/',
        method: 'post',
        data: {
          mobile: this.mobile
        }
      }).then(response => {
        if (response.data.code==100) { // 发送成功
          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);
        } else {  // 发送失败
          this.sms_interval = "重新获取";
          this.is_send = true;
          this.$message({
            message: '短信发送失败',
            type: 'warning',
            duration: 3000
          });
        }
      }).catch(() => {
        this.sms_interval = "频率过快";
        this.is_send = true;
      })


    },
    //注册接口
    register() {
      if (!(this.mobile && this.sms && this.password)) {
        this.$message({
          message: '请填好手机、密码与验证码',
          type: 'warning',
          duration: 1500
        });
        return false  // 直接结束逻辑
      }

      this.$axios({
        url: this.$settings.BASE_URL + '/user/register/',
        method: 'post',
        data: {
          mobile: this.mobile,
          code: this.sms,
          password: this.password
        }
      }).then(response => {
        this.$message({
          message: '注册成功,3秒跳转登录页面',
          type: 'success',
          duration: 3000,
          showClose: true,
          onClose: () => {
            // 成功则跳转到登录页面
            this.$emit('success')
          }
        });
      }).catch(error => {
        this.$message({
          message: '注册失败,请重新注册',
          type: 'warning',
          duration: 1500,
          showClose: true,
          onClose: () => {
            // 清空所有输入框
            this.mobile = '';
            this.password = '';
            this.sms = '';
          }
        });
      })
    }
  }
}
</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>

3)引入

src>>componeents>>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">
        <!--判断username是否有值-->
        <div v-if="!username">
          <!--给登录按钮添加点击事件-->
          <span @click="put_login">登录</span>
          <span class="line">|</span>
          <span @click="put_register">注册</span>
        </div>
        <div v-else>
          <span>{{ username }}</span>
          <span class="line">|</span>
          <span>注销</span>
        </div>
      </div>
    </div>
    <!--给登录页面添加自定义事件-->
    <Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login"/>
    <Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register"/>
  </div>
</template>

<script>
import Login from "@/components/Login";
import Register from "@/components/Register";

export default {
  name: "Header",
  data() {
    return {
      // 当前所在路径,去sessionStorage取的,如果取不到,就是 /
      url_path: sessionStorage.url_path || '/',
      is_login: false,
      is_register: false,
      username: this.$cookies.get('username'),
      token: this.$cookies.get('token'),
    }
  },
  methods: {
    goPage(url_path) {
      // 已经是当前路由就没有必要重新跳转
      if (this.url_path !== url_path) {
        this.$router.push(url_path);
      }
      sessionStorage.url_path = url_path;
    },
    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;
    },
    success_login() {
      this.is_login = false;
      this.username = this.$cookies.get('username');
      this.token = this.$cookies.get('token');
    },
    success_register() {
      this.is_login = true
      this.is_register = false
    },
  },
  created() {
    // 组件加载完成,就取出当前的路径,存到sessionStorage  this.$route.path
    sessionStorage.url_path = this.$route.path;
    // 把url_path = 当前路径
    this.url_path = this.$route.path;
  },
  components: {
    Login,
    Register,
  }
}
</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>

十五.Redis非关系型数据库

1)Redis介绍与安装

介绍

简介

​ redis(缓存数据库),大部分时间是用来做缓存,但不仅仅可以做缓存,也叫非关系型数据库,区别于mysql这种关系型数据库(nosql)。
​ c语言写的服务(监听端口),用来存储数据的,数据是存储在内存中,取值放值速度非常快,官方说有10万qbs

redis是key-value形式存储,没有表的概念

面试题

redis为什么这么快?
答:①纯内存操作,不需要去硬盘上 ,也不存在并发安全问题
②网络模型使用的是IO多路复用(并发量高、处理的请求更多)
​ ③6.x之前是单进程单线程架构(没有线程进程间切换,更少消耗资源)

安装

7.x:最新版本
5.x:公司用的最多,从该版本往后都是多进程、多线程架构
5.x之前:单进程、单线程架构

进程:资源分配的最小单位,一个程序运行可能有一个进程也可能有多个进程。
线程:CPU调度的最小单位,真正干活的是线程,遇到io操作就在操作系统层面切换。(跳到别的进程)
协程:单线程下的并发,程序层面控制,遇到io操作就在程序层面切换。(在同进程下跳到别的线程)

​ mac 源码编译安装
​ linux 源码编译安装
​ win 微软自己基于源码改动编译成安装包,但是仅维护到3.x版本
​ 最新3.x版本 https://github.com/microsoftarchive/redis/releases
​ 最新5.x版本 https://github.com/tporadowski/redis/releases/

image

安装完会把redis自动加入到服务中

目录中的重点文件

redis-server.exe
redis-cli.exe
redis.windows-service.conf   # 配置文件
      -bind 127.0.0.1   # 服务,跑在的地址(如果0.0.0.0即允许所有地址访问)
      -port 6379          # 监听的端口

image

启动redis

方式一:在服务中启动

image

方式二:使用命令启动

如果启动服务端命令打不开就直接把服务里改成自动启动,反正一般我们也不会主动去cmd中启动服务端

redis-server   # 启动服务端命令

image

客户端连接redis

方式一:简单命令连接

redis-cli  # 启动客户端命令(默认连接本地6379端口)

方式二:详细命令连接

如果redis没有跑在6379端口上,方式一则无效,且后期可以连接远程redis

redis-cli -h 地址 -p 端口
redis-cli -h 127.0.0.1 -p 6378

方式三:使用图形化客户端操作

①Redis Desktop Manager(也叫RESP):开源,现在收费,可破解【这里推荐用】(mac,win,linux 都有)
-Qt5 qt是个平台,专门用来做图形化界面的
-可以使用c++写
-可以使用python写 pyqt5 使用python写图形化界面 (少量公司在用)
②Redis Client:小众

这里下载破解版RESP进入图形化界面,连接redis 输入地址和端口,点击连接即可

image
redis默认有16个库,默认连进去就是第0个

image

简单使用KV形式的存取数据:

image

2)python操作Redis

python相当于是客户端来操作redis

安装模块
pip install redis
(1)普通连接使用
# 导入模块的Redis类
from redis import Redis

# 实例化得到对象
conn = Redis(host='127.0.0.1', port=6379)
# 使用得到的对象操作redis:

# 1.获取name的值
res = conn.get('name')
print(res)   # b'zy'  返回的是bytes格式数据

# 2.设置值(存在则修改,不存在则新增)
conn.set('name', 'zzy')
conn.set('name1', 'zy')

# 关闭连接
conn.close()

image

(2)连接池连接使用

image

redis服务可以用python程序操作,也可以用图形化界面操作。但是python程序可能开多线程去连接redis服务,此时我们在中间加上连接池限制只能两条来操作redis,这样哪怕我python程序开了100个线程也不会影响只会走连接池给的数量去操作redis。连接数太多会影响服务器性能

补充:使用django连接池

# djang 中操作mysql默认是没有连接池的,一个请求就是一个mysql连接
    -可能会有问题:并发数过高会导致mysql连接数过高影响mysql性能
    -用到的可能很少,因为:django并发很低,mysql完全顶得住。
    -使用django连接池:https://blog.51cto.com/liangdongchang/5140039

传统方案:开100个线程操作redis

from threading import Thread
from redis import Redis

def task():
    conn = Redis(host='127.0.0.1', port=6379)
    print(conn.get('name'))

# 启动100个线程执行task函数。
for i in range(100):
    t = Thread(target=task)
    t.start()

缺点:每次都是新的一个连接,会导致redis的连接数过多

连接池方案:连接池连接使用

需把连接池POOL做成单例模式,否则有可能会在每个线程中创建一个连接池,然后在每个线程中的连接池中取一个
使用模块导入的方式做单例>>新建一个pool文件当作模块
做成模块后导入,无论导入多少次导入的都那一个POOL对象

pool.py

import redis
# 创建一个大小为2的redis连接池
POOL = redis.ConnectionPool(max_connections=10, host='127.0.0.1', port=6379)

测试

import redis
from threading import Thread
from pool import POOL

def task():
    # 从池中拿一个连接
    conn = redis.Redis(connection_pool=POOL)  # 当线程开的超过连接池大小时会报错,但是还是可以取出连接池大小的数据,如果加了等待则不会报错
    print(conn.get('name'))

for i in range(2):
    t = Thread(target=task)
    t.start()

补充:单例模式

总共有23种设计模式,单例是其中最简单的一种

什么是单例:类加括号无论执行多少次永远只会产生一个对象,当类中有很多强大的方法 我们在程序中很多地方都需要使用,如果不做单例 会产生很多无用的对象浪费存储空间,所以用单例模式让整个程序就用一个对象。主要就是节省内存

使用单例模式的多种方式:https://www.cnblogs.com/oreoz/p/16852450.html#12设计模式简介及单例模式

3)Redis数据类型

redis是key-value形式存储
redis数据放在内存中,如果断电数据就会丢失。所以需要有持久化的方案,当ctrl+c程序正常关闭它会生成一个dump.rdb文件把数据持久化在硬盘上

Redis有5种数据类型(value)
string字符串:用的最多,做缓存、做计数器(访问量、阅读量..)
list列表: 简单的消息队列
hash(字典):缓存
set集合:去重
zset有序集合:排行榜

(1)string字符串类型

String操作,redis中的String在在内存中按照一个name对应一个value来存储。如图:

image

需要记住:set修改新增、get获取、strlen字节长度、incr自增

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

import redis
conn = redis.Redis()

"""
ex:过期时间(秒)(过期后自动销毁)
px:过期时间(毫秒)(过期后自动销毁) 一般不用
nx:如果为True,只有name不存在时才能新增,name存在就新增不了
xx:如果为False,只有name存在时才能修改,name不存在就修改不了
"""
conn.set('hobby', '篮球', ex=3)
conn.set('hobby', '篮球', nx=True)
conn.set('hobby', '足球', xx=True)

# 关闭连接
conn.close()

# redis实现分布式锁的底层就是用【nx】实现的

2.setnx(name, value)设置值

等同于:conn.set('name1', 'zy',nx=True)

"""
只有name不存在时才能新增,name存在就新增不了
"""
conn.setnx('name1', 'zy')

3.setex(name, value, time)设置值

等同于:conn.set('name2', 'zy',ex=3)

"""
time:过期时间(秒)
"""
conn.setex('name2', 3, 'zy')

4.psetex(name, time_ms, value)设置值

等同于:conn.set('name1', 'zy',px=3000)

"""
time_ms:过期时间(毫秒)
"""
conn.`psetex('name2', 3000, 'zy')

5.mset(*args, **kwargs)批量设置值

conn.mset({'k1': 'v1', 'k2': 'v2'})

6.get(name)获取值

# 获取到的是二进制数据格式
print(conn.get('name'))  # b'\xe5\xbc\xa0\xe4\xb8\x89'

# 把二进制数据用utf8编码
print(str(conn.get('name'), encoding='utf-8'))  # 张三

7.mget(keys, *args)批量获取值最后放在列表里

res = conn.mget('name', 'name1')
print(res)  # [二进制数, 二进制数]

8.getset(name, value)获取原来的值并修改为新值

res=str(conn.getset('name','李四'),encoding='utf-8')
print(res)   # 张三

9.getrange(key, start, end)获取某区间的字节(不是字符)

"""
start:开始位置
end:结束位置
"""
res = conn.get('name')
print(res)    # b'\xe6\x9d\x8e\xe5\x9b\x9b'
res1 = conn.getrange('name', 0, 2)
print(res1)  # b'\xe6\x9d\x8e'

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

"""
offset:字符串的索引,字节(一个汉字三字节)
"""
conn.setrange('name', 3, 'bbb')
# 此时name从【张三】变成了【张bbb】

vvvvvvvvvvvvvvv比特位的操作vvvvvvvvvvvvvvv

11.setbit(name, offset, value)对name对应值的二进制表示的位进行操作

12.getbit(name, offset)获取name对应的值的二进制表示中的某位的值 (0或1)

13.bitcount(key, start=None, end=None)获取name对应的值的二进制表示中 1 的个数

^^^^^^^^^^比特位的操作^^^^^^^^^^

14.bitop(operation, dest, *keys)获取多个值,并将值做位运算,将最后的结果保存至新的name对应的值

"""
operation:AND(并)、OR(或)、NOT(非)、XOR(异或)
dest:新的Redis的name
*keys:要查找的Redis的name
"""

15.strlen(name)返回name对应值的字节长度(一个汉字3个字节)

res = conn.strlen('name')
print(res)  # 6

16.incr(self, name, amount=1)自增name对应的值,当name不存在时则创建name=amount

不会出并发安全问题,单线程架构并发量高,后期建议不要直接操作mysql,而是让redis去修改

conn.incr('age', amount=1)

17.incrbyfloat(self, name, amount=1.0)自增name对应的值,当name不存在时,则创建name=amount

精度会有问题

conn.incrbyfloat('age', amount=1.2)  #  20.19999999999999929

18.decr(self, name, amount=1)自减name对应的值,当name不存在时,则创建name=amount

conn.decr('age', amount=1)

19.append(key, value)让name对应的值后面追加新内容

conn.append('name', '嘿嘿')  # zy嘿嘿

3)Redis数据类型

(2)list列表类型

List操作,redis中的List在在内存中按照一个name对应一个List来存储。如图:

image

需要记住:lpush每个新增数据在列表最左侧、llen列表元素个数、lpop列表左侧获取第一个值并移除,返回值就是该值、lrange切片获取数据、blpop可以做消息队列使用从左阻塞式弹出(有值就弹,没值就阻塞)

1.lpush(name, values) 每个新添加的数据在列表最左侧

import redis
conn = redis.Redis()

conn.lpush('user', 'z1', 'z2')
# [z2, z1]

conn.close()

2.rpush(name, values) 每个新添加的数据在列表最右侧

conn.rpush('user', 'z3')
# [z2, z1, z3]

3.lpushx(name, value)name存在时添加到列表最左侧

conn.lpushx('user', 'zzz')
# [zzz, z2, z1, z3]

4.rpushx(name, value) name存在时添加到列表最右侧

conn.rpushx('user', 'xxx')
# [zzz, z2, z1, z3, xxx]

5.llen(name) 统计列表元素个数

res = conn.llen('user')
print(res)  # 5
# [zzz, z2, z1, z3, xxx]

6.linsert(name, where, refvalue, value))某个值[前\后]插入新值

"""
where:哪里(before前,after后)
refvalue:以谁为参照物
"""
# 在z1前插入11
conn.linsert('user', 'before', 'z1', '11')
# [zzz, 11,  z1,  xxx]

# 在z1后插入22
conn.linsert('user', 'after', 'z1', '22')
# [zzz,  z1, 22,  xxx]

7.lset(name, index, value) 按索引改值

"""
index:索引值
"""
conn.lset('user', 1, '我改了')
# [zzz, 我改了,  z1,  22, xxx]

8.lrem(name, num, value)删除指定个数的指定值

"""
num:num=0,从左开始全删
           num=1,从左开始删1个
           num=-1,从右开始删1个
value:要删除的值(列表可重复)
"""
conn.lrem('user', 1, 'zz')

9.lpop(name)列表左侧获取第一个值并移除,返回值就是该值

# [11, 22, 33, 44]

res = conn.lpop('user')
print(res)# b'11'
# [22, 33, 44]

10.rpop(name) 列表右侧获取第一个值并移除,返回值就是该值

# [11, 22, 33, 44]

res = conn.rpop('user')
print(res)# b'44'
# [11, 22, 33]

11.lindex(name, index)根据索引获取对应列表的值

不用utf-8获取到的就是二进制

# [z1, z2, z3]

res = str(conn.lindex('user', 1), encoding = 'utf-8')
print(res)  # z2

12.lrange(name, start, end)切片获取数据

# [z1, z2, z3]

res = conn.lrange('user', 0, 1) 
print(res)  # [b'z1', b'z2']

13.ltrim(name, start, end)移除没有在start-end索引之间的值

# [z1, z2, z3]

res = conn.ltrim('user', 0, 1) 
print(res)  # True
# [z1, z2]

14.rpoplpush(src, dst)从列表1中移除最右边的值并添加到列表2的最左边

"""
src:要取数据的列表
dst:要添加数据的列表
"""
# user[z1, z2]
# user2[x1, x2]
conn.rpoplpush('user', 'user2')
# user[z1]
# user2[z2, x1, x2]

15.blpop(keys, timeout) 可以做消息队列使用从左阻塞式弹出(有值就弹,没值就阻塞)

"""
timeout:当列表的元素获取完之后,阻塞等待的时间(秒)【 0 表示永远阻塞】
"""

# user[z1, z2, z3]
res = conn.blpop('user')
print(res)  # (b'user', b'z2')
# 执行一次弹一次,直到没有值就阻塞住

16.brpop(keys, timeout),从右向左获取数据

爬虫实现简单分布式:多个url放到列表里,往里不停放URL,程序循环取值,但是只能一台机器运行取值,可以把url放到redis中,多台机器从redis中取值,爬取数据,实现简单分布式

"""
timeout:当列表的元素获取完之后,阻塞等待的时间(秒)【 0 表示永远阻塞】
"""
# user[z1, z2, z3]
res = conn.brpop('user')
print(res)  # (b'user', b'z3')
# 执行一次弹一次,直到没有值就阻塞住

17.brpoplpush(src, dst, timeout=0)阻塞式从列表1中移除最右边的值并添加到列表2的最左边

"""
src:要取数据的列表
dst:要添加数据的列表
timeout:当列表的元素获取完之后,阻塞等待的时间(秒)【 0 表示永远阻塞】
"""
# user[z1, z2]
# user2[x1, x2]
conn.rpoplpush('user', 'user2')
# user[z1]
# user2[z2, x1, x2]

18.自定义增量迭代

由于redis类库中没有提供对列表元素的增量迭代,如果想要循环name对应的列表的所有元素,那么就需要:
1.获取name对应的所有列表
2.循环列表

但是如果列表非常大,那么就有可能在第一步时就将程序的内容撑爆,所以有必要自定义一个增量迭代的功能:

import redis
conn=redis.Redis(host='127.0.0.1',port=6379)
# conn.lpush('test',*[1,2,3,4,45,5,6,7,7,8,43,5,6,768,89,9,65,4,23,54,6757,8,68])
# conn.flushall()
def scan_list(name,count=2):
    index=0
    while True:
        data_list=conn.lrange(name,index,count+index-1)
        if not data_list:
            return
        index+=count
        for item in data_list:
            yield item
print(conn.lrange('test',0,100))
for item in scan_list('test',5):
    print('---')
    print(item)
(3)hash(字典)类型

Hash操作,redis中Hash在内存中的存储格式如下图:kv数据形式

image

需要记住:hset设置键值对、hget根据K获取V值、hmget获取多个K的值并组成列表、hlen统计键值对的个数、hdel删除键值对、hscan_iter 获取所有值

1.hset(name, key, value) 设置键值对

conn.hset('info', 'name', '11')  # 当name和key不存在则新增
conn.hset('info', 'name', 'zy')  # 当key存在则修改
conn.hset('info', 'age', '18')  # 当key不存在则新增
# info {name: zy}, {age: 18}
conn.hset('info', mapping={'age': 20, 'hobby': 'run'})  # 批量操作
# info {name: 11}, {age: 18},{hobby: run}

2.hmset(name, mapping) 批量设置被弃用了,以后都使用hset

3.hget(name,key) 根据K获取V值

res = conn.hget('info', 'name')
print(res)  # b'zy'

4.hmget(name, keys, *args) 获取多个K的值并组成列表

res = conn.hmget('info', ['name', 'age'])
print(res)  # [b'zy', b'20']
res1 = conn.hmget('info', 'name', 'age')
print(res1)  # [b'zy', b'20']

5.hgetall(name) 获取所有KV键值对当数据多时慎用!并组成列表

res = conn.hgetall('info')
print(res)  # {b'name': b'zy', b'age': b'20', b'hobby': b'run'}

6.hlen(name) 统计键值对的个数

res = conn.hlen('info')
print(res)  # 3

7.hkeys(name) 获取所有的K键并组成列表

res = conn.hkeys('info')
print(res)  # [b'name', b'age', b'hobby']

8.hvals(name) 获取所有的V值并组成列表

res = conn.hvals('info')
print(res)  # [b'zy', b'20', b'run']

9.hexists(name, key) 判断K是否存在

res = conn.hexists('info', 'name')
print(res)  # True

10.hdel(name,*keys) 删除键值对

res = conn.hdel('info','hobby')
print(res)  # 1

# 如果没有该K键则返回0

11.hincrby(name, key, amount=1) 自增k的值,如果K不存在则创建K,值等于amount

"""
name,redis中的name
key, hash对应的key
amount,自增数(整数)
"""
conn.hincrby('info','age1',amount=1)

12.hincrbyfloat(name, key, amount=1.0)自增k的值,如果K不存在则创建K,值等于amount

"""
name,redis中的name
key, hash对应的key
amount,自增数(浮点数)
"""
conn.hincrbyfloat('info','age2',amount=1.2)

13.hscan(name, cursor=0, match=None, count=None) 分批获取所有键值对,hash类型是无序的增量式迭代获取,对于数据量大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而防止内存被撑爆

hgetall会一次性取出所有键值对,效率低且可以能占内存很多

它不单独使用,拿的数据,不是特别准备

咱们用这个,它内部用了hscan,等同于hgetall 所有数据都拿出来,count的作用是,生成器,每次拿count个个数

# 首先需要插入一批数据
for i in range(1000):
    conn.hset('info', 'id_%s' % i, '鸡蛋_%s号' % i)

image

"""
name:redis的name
cursor:游标(基于该游标位置获取数据)
match:匹配指定key,默认None 表示所有的key
count:每次分片最少获取个数,默认None表示采用Redis的默认分片个数
"""
res = conn.hscan('info', cursor=0, count=5)
print(res)  # (数字, {count条数据}) 这个数字就是游标,继续获取则要把游标改成该数字

# 发现count的数字不准确,有时候对有时候不对,所以它不应该这样用,而是应该用hscan_iter

image

14.hscan_iter(name, match=None, count=None)获取所有值

利用yield封装hscan创建生成器,实现分批去redis中获取数据,用for循环可一次性获取所有。count的作用就是生成器,每次拿count个个数

"""
match,匹配指定key,默认None 表示所有的key
count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
"""
res = conn.hscan_iter('info', count=5)
print(res)  # generator 只要函数中有yield关键字,这个函数执行的结果就是生成器 ,生成器就是迭代器,可以被for循环
for i in res:
    print(i)# 循环打印所有

image

(4)其他通用操作

不指定类型,所有类型都支持

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

conn.delete('name', 'info')
conn.delete(*['name', 'info'])

2.exists(name) 检测redis的name是否存在

res = conn.exists('info')
print(res) # 1
# 在就是1  不在就是0

3.keys(pattern='*') 根据模型获取redis的name

image

?表示一个字符, * 表示多个字符

# 把所有name拿出来
res = conn.keys('*')
print(res) # [b'info', b'info1']

# 把所用name是*o的拿出来
res = conn.keys('*o')
print(res) # [b'info']

4.expire(name ,time) 为某个redis的某个name设置超时时间(秒)

conn.expire('userinfo',3) # 到时间则删除

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

conn.rename('info','info111') #把info重命名为info111

6.move(name, db)) 将redis的某个值移动到指定的db库下

conn.move('info',8)

image

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

res=conn.randomkey()
print(res)

8.type(name) 获取name对应值的类型

print(conn.type('age'))  # b'string'

4)Redis管道

mysql的事务四大特性:
原子性:最小的单元不能再分割(我现在拿起杯子喝水,中途不能做其他事必须干完)
一致性:杯子里的水少了,我肚子里的水多了
隔离性:多个事务之间互不影响,所以才有隔离级别
持久性:一但完成就会永久存储在硬盘中

redis说支持事物也对,不支持事物也对。单实例才支持所谓的事物,因为支持事务是基于管道的

以前我们执行命令:一条一条执行
  举例:
     user1金额-100:      conn.decr('user1_money',100)
     如果这个时候程序断电,则会出现user1钱少了,user2钱没多
     user2金额+100:     conn.incr('user2_money',100)
        
        
pipline>>所以要把这两条命令放到一个管道中先不执行,执行excute时再一次性都执行完成。这就保证了【事物】
     conn.decr('user1_money',100)         conn.incr('user2_money',100)
使用管道

image

user1和user2各有200元,现在完成转账:

import redis

conn = redis.Redis()
# 生成一个管道事物
p = conn.pipeline(transaction=True)
# 开启管道事物
p.multi()

# user1减100
p.decr('user1', 100)
# raise Exception('程序突然崩了')
# user2加100
p.incr('user2', 100)

# 结束管道事物
p.execute()
conn.close()

假如中途程序突然崩了,则转账两边都不成功。这就是redis使用管道操作事物

5)django中使用redis

方式一:自定义包方案

所有框架通用

第一步:写一个pool.py里面创建一个连接池

import redis
# 创建一个大小为100的redis连接池
POOL = redis.ConnectionPool(max_connections=100, host='127.0.0.1', port=6379)

第二步:以后哪里需要直接导入即可

views.py

from django.http import JsonResponse
from utils.pool import POOL
import redis
import json


# 测试访问一次接口网站访问量+1
def test_redis(request):
    # 从连接池中拿一个连接出来
    conn = redis.Redis(connection_pool=POOL)
    # 给count的值自增1
    conn.incr('count')
    # 获取count值
    res = conn.get('count')
    return JsonResponse({'count': '该接口今天被访问了:%s次' % res}, json_dumps_params={'ensure_ascii': False})

image

方式二:django方案
方案1:django缓存使用

【推荐使用】

下载模块

pip install django-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",
        }
    }
}

使用的地方

views.py

from django.core.cache import cache
from django.http import JsonResponse

# 测试访问一次接口网站访问量+1
def test_redis(request):
    # 先往缓存中放一个count=1
    # cache.set('count', 1)
    # 然后从缓存中取数据
    res = cache.get('count')
    # 再往缓存中放数据
    cache.set('count', res + 1)
    return JsonResponse({'count': '该接口今天被访问了:%s次' % res}, json_dumps_params={'ensure_ascii': False})

image

以上需注意缓存中的count和redis中的count没关系

补充

缓存的底层:是pickle序列化后存入的,也就是说我可以传对象进缓存,最后再取出来。但是redis只支持存五种类型,不能存对象。

# 类只能定义在全局,如果在局部会找不到该类
class Person:
    name = 'zy'

def test_redis(request):
    # 产生一个对象
    p = Person()
    # 把对象存入缓存中
    cache.set('obj', p)
    # 从缓存中取出对象
    p = cache.get('obj')
    print(p.name)
    return JsonResponse({'count': '该接口今天被访问了:%s次'}, json_dumps_params={'ensure_ascii': False})
方案2:第三方模块使用

django-redis模块,最不推荐使用

from django_redis import get_redis_connection
from django.http import JsonResponse


# django第三方模块测试访问一次接口网站访问量+1
def test_redis(request):
    # 过去redis连接
    conn = get_redis_connection()
    # 从redis中取出count并转成整型
    res = int(conn.get('count'))
    # 写入redis
    conn.set('count', res + 1)
    return JsonResponse({'count': '该接口今天被访问了:%s次' % res}, json_dumps_params={'ensure_ascii': False})

十六.celery框架

1)celery介绍与安装

介绍

Celery:翻译过来是芹菜的意思,但是和芹菜可没关系。是分布式异步任务框架
它属于框架,运行起来就是个服务。是python的一个框架和django无关。
主要用来做:异步任务定时任务延迟任务

Celery 官网:http://www.celeryproject.org/
Celery 官方文档英文版:http://docs.celeryproject.org/en/latest/index.html
Celery 官方文档中文版:http://docs.jinkan.org/docs/celery/

Celery的运行原理

1)可以不依赖任何服务器,通过自身命令,启动服务
2)celery服务为为其他项目服务提供'异步解决任务需求'的
注:会有'两个服务'同时运行,一个是'项目服务',一个是'celery服务',项目服务将需要异步处理的任务交给celery服务,celery就会在需要时异步完成项目的需求

# 举例:
人是一个独立运行的服务 | 医院也是一个独立运行的服务
    正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
    人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求
    django中想做异步就交给celery,不想做异步就各干各的互补干扰(两个服务)

Celery架构(Broker,backend 都用redis)

image

1.【任务中间件 Broker】其他服务'提交的异步任务',放在这里面'排队'
          需要借助于第三方:redis、rabbitmq  
2.【任务执行单元 worker】   '真正执行异步任务的进程'
          celery提供的
3.【结果存储 backend】   函数的'返回结果',存到这里 
          需要借助于第三方:redis、mysql

使用场景

异步执行:解决耗时任务
延迟执行:解决延迟任务
定时执行:解决周期任务

安装

celery不支持win,通过eventlet可支持在win上运行

pip install celery
pip install eventlet

2)celery快速使用

以下第二步和第五步和celery没有必然联系,是在django中做的

第一步:新建main.py

from celery import Celery

# 提交的异步任务放在redis的1这里
broker = 'redis://127.0.0.1:6379/1'
# 执行完的结果放在redis的2这里
backend = 'redis://127.0.0.1:6379/2'
app = Celery('test', broker=broker, backend=backend)

@app.task
def add(a, b):
    import time
    time.sleep(3)
    print('-----', a + b)
    return a + b

第二步:其他程序提交任务(随便新建一个py文件)

# 异步调用
from main import add

# 执行异步任务
res = add.delay(5, 6)  # 原来add的参数,直接放在delay中传入即可
print(res)  # 9945df25-b3ea-4fec-9371-1a680bdd95c4

第三步:启动worker(真正执行异步任务的进程)

启动worker命令,需要安装eventlet,见安装

 win:
    -4.x之前版本
         celery worker -A main -l info -P eventlet
        
    -4.x之后版本
         # 写Celery代码的文件 用eventlet运行
         celery  -A main  worker -l info -P eventlet
————————————————————————
 """需注意执行命令是路径一定要在main.py文件那层""" 
————————————————————————
mac:
         celery  -A main  worker -l info

image

第四步:worker会执行消息中间件中的任务,把结果存起来放在redis中的2中

提交任务放在Broker中间件中,启动worker执行任务,结果放在backend中

image

第五步:查看异步任务的执行结果(再随便新建一个py文件)

from main import app
from celery.result import AsyncResult

id = '9945df25-b3ea-4fec-9371-1a680bdd95c4'
if __name__ == '__main__':
    # 把任务id和app传进去
    a = AsyncResult(id=id, app=app)
    # 如果是true说明执行完了
    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('任务已经开始被执行')

3)celery包结构使用

和快速使用相比更方便,把包放在最上面,今后那个视图函数想用直接调用对应任务.delay(参数) 即可

包结构

project项目
    ├── celery_task     # 新建celery包
    │   ├── __init__.py # 包文件
    │   ├── celery.py   # celery连接和配置相关文件,且【名字必须叫celery.py】
    │   └── tasks.py     # 所有任务函数可写多个py文件
    │
    ├── add_task.py    # 添加任务
    └── get_result.py   # 获取结果

image

第一步:新建包 celery_task

在包下新建必须叫celery.py文件

from celery import Celery

# 如果用django里的orm、缓存、表模型则必须加下列代码
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

# 提交的异步任务放在redis的1这里
broker = 'redis://127.0.0.1:6379/1'
# 执行完的结果放在redis的2这里
backend = 'redis://127.0.0.1:6379/2'

# 开了哪些任务就要在include中写出来
app = Celery('test', broker=broker, backend=backend, include=['celery_task.order_task', 'celery_task.user_task'])

第二步:在包内部写不同的task任务来执行异步任务

# order_task.py
from .celery import app
import time
# 模拟计算a+b任务
@app.task
def add(a, b):
    print('-----', a + b)
    time.sleep(2)
    return a + b
——————————————————
# user_task.py
from .celery import app
import time
# 模拟发送短信任务
@app.task
def send_sms(phone, code):
    print("给%s发送短信成功,验证码为:%s" % (phone, code))
    time.sleep(2)
    return True

第三步:其他程序提交任务该任务会被提交到中间件中等待worker执行

【add_task.py】

from celery_task.user_task import send_sms

# 执行异步任务 (没有执行直接返回)
res = send_sms.delay('1999999', 8888)
print(res)  # f392bb4b-5c0b-4907-8102-f966a780672c

第四步:启动worker(真正执行异步任务的进程)

启动worker命令,需要安装eventlet,见安装

 win:
    -4.x之前版本
         celery worker -A celery_task -l info -P eventlet
        
    -4.x之后版本
         # 写Celery代码的文件 用eventlet运行
         celery  -A celery_task  worker -l info -P eventlet
————————————————————————
 """需注意执行命令是路径一定要在包那层""" 
————————————————————————
mac:
         celery  -A main  worker -l info

image

第五步:worker执行完,结果存到backend中

第六步:查看结果

【get_result.py】

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

id = 'f392bb4b-5c0b-4907-8102-f966a780672c'
if __name__ == '__main__':
    # 把任务id和app传进去
    a = AsyncResult(id=id, app=app)
    # 如果是true说明执行完了
    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('任务已经开始被执行')

4)Celery执行任务

(1)执行异步任务

任务.delay(参数)

# 异步执行任务 (没有执行直接返回)
res = send_sms.delay('1999999', 8888)
print(res)  # 23422a83-ae30-494e-b3e9-77831ba61dea
————————————————
# 不要忘记启动worker
(2)执行延迟任务

任务.apply_async(args=[参数],eta=时间对象) 如果没有修改时区,需要使用utc时间

from datetime import datetime, timedelta

# 拿到当前utc的时间:datetime.utcnow()
# 可以和时间对象相加/相减:timedelta(参数=值)
"""
参数:days 天/minutes 分/seconds 秒 
"""
eta = datetime.utcnow() + timedelta(minutes=1)
# 立即执行异步任务
# res = send_sms.delay('13888888888','9999')
# print(res)
# 延迟一分钟后执行任务
res1 = send_sms.apply_async(args=['15222222222', '8888'], eta=eta)
print(res1)  # 7393e515-13bb-4bc5-8f73-8e902591caaf
————————————————
# 不要忘记启动worker
(3)执行定时任务

每隔多长时间执行某个任务

# 需要启动beat和启动worker
        -beat    定时提交任务的进程>>配置在app.conf.beat_schedule的任务
        -worker  执行任务的
使用步骤

第一步:在celery_task包下的celery的py文件中写

from celery import Celery

# 如果用django里的orm、缓存、表模型则必须加下列代码
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

# 提交的异步任务放在redis的1这里
broker = 'redis://127.0.0.1:6379/1'
# 执行完的结果放在redis的2这里
backend = 'redis://127.0.0.1:6379/2'
# 新加的任务.py不要忘记注册
app = Celery('test', broker=broker, backend=backend, include=['celery_task.order_task', 'celery_task.user_task'])
from datetime import timedelta
from celery.schedules import crontab
# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False
# 任务的定时配置
app.conf.beat_schedule = {
    'send_sms': {
        'task': 'celery_task.user_task.send_sms',  # 任务的名字
        # 'schedule': timedelta(seconds=3),  # 每隔三秒执行一次
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点执行一次
        'schedule': crontab(hour=9, minute=43),  # 每天9点43执行一次
        'args': ('18888888', '6666'),  # 参数
    },
}

第二步:启动beat、worker

# 启动的包不要错
celery -A celery_task beat -l info
# 启动的包不要错
celery -A celery_task worker -l info -P eventlet

image

注意

1 启动命令的执行路径!!如果是包结构一定在包这一层
2 include=['celery_task.order_task'],路径从包名下开始导入,因为我们在包这层执行的命令。且不需要加.py

5)django中使用celery包

补充:APSchedule框架

如果在公司中只做定时任务,可以用一个简单的框架APSchedule,只能做定时不能做异步。
如果公司又想做定时,又想做异步那么celery框架是首选

任务一:每隔三秒执行一次

from datetime import datetime
import os
# 1: 导入这个最简单的调度器
from apscheduler.schedulers.blocking import BlockingScheduler

# 2: 定义我们的job
def tick():
    print("Tick! The time is: %s" % datetime.now())


if __name__ == '__main__':
    # 3: 实例化BlockingScheduler调度器,没有参数表示存储器是:内存
    # 执行器是线程池,默认的线程并发数是10个。
    scheduler = BlockingScheduler()
    # 4:调度器绑定任务,并指定触发器
    # 触发器:‘interval’表示间隔执行, ‘date’, 表示指定时间触发, ‘cron’表示固定时间间隔触发。
    scheduler.add_job(tick, 'interval', seconds=3)

    try:
        # 5:执行任务
        scheduler.start()
    except Exception as e:
        print(e)

任务二:每天固定时间执行

from datetime import datetime
import os
# 1: 导入这个最简单的调度器
from apscheduler.schedulers.blocking import BlockingScheduler

# 2: 定义我们的job
def tick():
    print("Tick! The time is: %s" % datetime.now())


if __name__ == '__main__':
    # 3: 实例化BlockingScheduler调度器,没有参数表示存储器是:内存
    # 执行器是线程池,默认的线程并发数是10个。
    scheduler = BlockingScheduler()
    # 4:调度器绑定任务,并指定触发器
    # 每天10点10分执行
    # scheduler.add_job(tick, trigger='cron', hour=10, minute=10)
    # 每天 10点10分,11点10分,12点10分执行
    scheduler.add_job(tick, trigger='cron', hour='10-12', minute=10)

    try:
        # 5:执行任务
        scheduler.start()
    except Exception as e:
        print(e)
(1)django使用celery包步骤
1.把前面自己写的包复制到项目(大路飞)路径下
        -luffy_api
            -celery_task #celery的包路径
                  -celery.py # 如果用django里的orm、缓存、表模型则必须加下列代码
                        import os
                        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
            -luffy_api  #源代码路径
            
2.在使用提交异步任务的位置,导入使用即可
        -视图函数中使用,导入任务
        -任务.delay()  # 提交任务
        
3.启动worker,如果有定时任务也要启动beat
        # 启动的包不要错、执行命令时的路径一定要和包在同一个路径中
        celery -A celery_task beat -l info
        celery -A celery_task worker -l info -P eventlet

4.等待任务被worker执行
    
5.在视图函数中,查询任务执行的结果
————————————————————————

 #【重点】:celery中使用djagno,有时候任务中会使用django的orm、缓存、表模型,这个时候必须加这句话
	os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')
案例:秒杀功能

秒杀逻辑分析

1.当用户点击前端[秒杀按钮]>>发送ajax请求到后端
2.后端视图函数>>提交秒杀任务>>借助于celery异步提交到中间件中
3.用户当次秒杀的请求就会回去,携带着任务id号在前端(执行没执行不知道)
4.前端开启定时任务(每隔3s带着任务id向后端发送请求查看是否秒杀成功)
5.此时后端的情况
    (1)任务还在等待被执行>>返回给前端,前端继续每隔3s发送一次请求查看是否秒杀成功
    (2)任务执行完了 秒杀成功>>返回给前端秒杀成功>>关闭前端定时任务
    (3)任务执行完了 秒杀失败>>返回给前端秒杀失败>>关闭前端定时任务
    
# 以下代码仅伪代码 主要为了验证

luffy_api>>apps>>user>>views.py

# 模拟秒杀逻辑(选择用CBV写)
from rest_framework.viewsets import ViewSet
from celery_task.order_task import sckill_task
from celery_task.celery import app
from celery.result import AsyncResult

class SckillView(ViewSet):
    # 异步提交秒杀任务
    @action(methods=['GET'], detail=False)
    def sckill(self, request):
        # 获取商品的id号
        a = request.query_params.get('id')
        # 使用异步提交一个秒杀任务
        res = sckill_task.delay(a)
        # print(res, type(res))  # bd2b3265-4d43-4873-9c74-5c7087df42a1  <class 'celery.result.AsyncResult'>
        # print(res.id, type(res.id))  # bd2b3265-4d43-4873-9c74-5c7087df42a1 <class 'str'>
        # 查看结果需要传一个任务id必须传字符串 不能传对象,这里我们应该可以用str包起来也能变成字符串
        return APIResponse(task_id=res.id)

    # 查看结果
    @action(methods=['GET'], detail=False)
    def get_result(self, request):
        task_id = request.query_params.get('task_id')
        # 把任务id和app传进去
        a = AsyncResult(id=task_id, app=app)
        # 如果是true说明执行完了
        if a.successful():
            # 拿结果
            result = a.get()
            if result:
                return APIResponse(msg='秒杀成功')
            else:
                return APIResponse(code=101, msg='秒杀失败')
        elif a.status == 'PENDING':
            print('任务等待中被执行')
            return APIResponse(code=666, msg='还在秒杀中')
        else:
            return APIResponse()

luffy_api>>apps>>user>>urls.py

# http://127.0.0.1:8000/api/v1/user/sckill/
router.register('sckill',views.SckillView,'sckill')

celery_task包中新建任务order_task.py

# 模拟秒杀任务
from .celery import app
import random
import time

@app.task
def sckill_task(good_id):
    # 生成订单,减库存都在一个事物中
    print("商品%s:秒杀开始" % good_id)
    # 这个过程可能是6、7、8秒中的某一个时间
    time.sleep(random.choice([6, 7, 8]))
    print("商品%s秒杀结束" % good_id)
    # 结果可能成功也可能失败
    return random.choice([True, False])

前端:src>>views>>新建Sckill.vue

<template>
  <div>
    <button @click="handleSckill">秒杀</button>
  </div>
</template>

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

export default {
  name: 'Sckill',
  data() {
    return {
      task_id: '',
      t: null
    }
  },
  methods: {
    handleSckill() {
      this.$axios.get(this.$settings.BASE_URL + '/user/sckill/sckill/?id=999').then(res => {
        this.task_id = res.data.task_id
        this.t = setInterval(() => {
          this.$axios.get(this.$settings.BASE_URL + '/user/sckill/get_result/?task_id=' + this.task_id).then(res => {
            if (res.data.code == 666) {
              //如果秒杀任务还没执行,定时任务继续执行
              console.log(res.data.msg)
            } else {
              // 秒杀结束,无论成功失败这个定时任务都结束
              clearInterval(this.t)
              this.t = null
              this.$message(res.data.msg)
            }
          })
        }, 2000)
      }).catch(res => {
      })
    }
  }
}
</script>

前端:src>>router>>index.js写路由

import Sckill from "@/views/Sckill";

    {
        path: '/sckill',
        name: 'sckill',
        component: Sckill
    },

后端:启动worker(真正执行异步任务的进程)

celery -A celery_task worker -l info -P eventlet
验证

打开前端页面访问http://192.168.1.231:8080/sckill

image

十七.轮播图

1)轮播图接口加缓存

一般网站首页被访问的频率会很高,假如瞬间有一万人访问那首页的轮播图接口就会执行一万次,那sql就会执行一万次,mysql顶不住啊!!由于轮播图都不咋变动所以要想一种方法让这一万个的访问效率更高一些不去查数据库了,而是利用redis缓存。这样效率会更高

# 现在的逻辑就变成了:
1.轮播图接口请求来了先去缓存中查,如果有数据直接返回
2.如果没有就查数据库,然后把轮播图数据放到redis中缓存起来

——————————————————————————————
# 这样一万条访问可能就第一条去查了数据库,其他的都是在缓存中查看,查询效率会非常的高
这些就大型公司首页需要这样做,公司内部的就几个人查看不需要用缓存,用缓存仅针对频率很高需要效率时才用redis

更改轮播图接口

# 加了缓存的 查所有轮播图接口
class BannerView(GenericViewSet, ListModelMixin):
    # 继承GenericViewSet可以写下面两句话,ViewSet是ApiView不能写下面两句话
    # 继承ListModelMixin可以查所有

    # 要序列化或反序列化的表模型数据
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')
    serializer_class = BannerSerializer

    # 重写list方法
    def list(self, request, *args, **kwargs):
        # 查看缓存有没有数据,如果没有就去数据库中拿
        banner_list = cache.get('banner_list')
        # 如果缓存中有数据就在缓存中拿
        if banner_list:
            # 返回缓存中的数据
            return APIResponse(data=banner_list)
        # 否则就在数据库中拿
        else:
            res = super().list(request, *args, **kwargs)
            # 把序列化后的数据存到缓存中
            cache.get('banner_list', res.data)
            # {code:100,msg:成功,data=[{},{}]}
            # 返回数据库中的数据
            return APIResponse(data=res.data)

以上仅对轮播图接口做优化,其他接口只要访问频率高都应该考虑是否可以做到缓存中

2)双写一致性

​ 加入缓存后,缓存中有数据会先去缓存中拿,但是如果mysql中数据变了缓存中却不会自动变化,就出现了数据不一致的问题(写入mysql,redis没动,数据不一致),所以我们需要做到双写一致性

如何解决?1.修改数据时删除缓存 2.修改数据时更新缓存 3.定时更新缓存>>实时性较差(几张图片无所谓,重要数据就不行)

这里我们针对第三种定时更新缓存来完成:需要一个定时任务celery

给首页轮播图开定时更新任务

第一步:在celery包下新建定时任务home_task.py

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


# 开启定时任务:更新轮播图
@app.task
def update_banner():  # 只要该任务一执行就会更新轮播图的缓存
    # 获取轮播图数据对象
    banners = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
    # 给轮播图数据对象进行序列化(序列化后的数据都在ser.data中)
    # instance 要序列化的数据
    # 只要是queryset对象就要加many=True,如果是单个对象则不用
    ser = BannerSerializer(instance=banners, many=True)
    # 必须自己给各图片拼上前面的地址,否则图片加载不出来
    for i in ser.data:
        i['image'] = settings.BACKEND_URL + i['image']
    cache.set('banner_list', ser.data)
    return True
# 因为还要拼地址,所以在settings.py>>dev.py中配置地址。【项目上线需要做的,今后还要加前端地址】

# 后端地址
BACKEND_URL = 'http://127.0.0.1:8000'

第二步:在celery包中的celery里配置任务的定时配置

from celery import Celery

# 如果用django里的orm、缓存、表模型则必须加下列代码
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.dev')

# 提交的异步任务放在redis的1这里
broker = 'redis://127.0.0.1:6379/1'
# 执行完的结果放在redis的2这里
backend = 'redis://127.0.0.1:6379/2'

# 新加的任务.py不要忘记注册
app = Celery('test', broker=broker, backend=backend,
             include=['celery_task.home_task'])

from datetime import timedelta
from celery.schedules import crontab

# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False

# 任务的定时配置
app.conf.beat_schedule = {
    'update_banner': {
        'task': 'celery_task.home_task.update_banner',  # 任务的名字
        'schedule': timedelta(seconds=3),  # 每隔三秒执行一次
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点执行一次
        # 'schedule': crontab(hour=19, minute=50),  # 每天19点50执行一次
        # 'args': ('18888888', '6666'),  # 如果需要参数就写
    },
}

第二步:启动worker、启动beat

# 启动的包不要错
celery -A celery_task beat -l info
# 启动的包不要错
celery -A celery_task worker -l info -P eventlet
验证
访问主页时轮播图有4张,当把其中一张轮播图数据库的is_delete改为1,这样轮播图的数量就从4变成了3,页面等定时任务的时间结束刷新页面即可看到定时更新缓存。

# 一般也不会在数据库中更改 会在admin后台中改
# 定时更新对数据要求准确的不会用

十八.课程模块

1)课程种类前端页面

image

# 分析项目:
        线上卖课商场
# 种类有:
        免费课>>免费不要钱的课程
        实战课>>某节课99元
        轻课   >>完整课19999元
根据三种课程创建三个页面

第一步:新建三个页面 src>>views>>新建页面

FreeCourseView.vue  # 免费课
ActualCourseView.vue  # 实战课
LightCourseView.vue  # 轻课

第二步:配置路由 src>>router>>index.js

      {
        path: '/free-course',
        name: 'free',
        component: FreeCourserView
    },
    {
        path: '/actual-course',
        name: 'actual-course',
        component: ActualCourserView
    },
    {
        path: '/light-course',
        name: 'light-course',
        component: LightCourseView
    },
3.复制代码 

第三步:写样式代码免费课和实战课都可用下面样式 , 轻课暂时只要头和尾

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

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

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

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

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

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

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

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

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

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

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

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

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

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

.course .ordering ul {
  float: left;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


<template>
  <div>
    <Header></Header>
    <div>
      <br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
    </div>
    <Footer></Footer>
  </div>
</template>

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

export default {
  name: "LightCourseView",
  components: {
    Header,
    Footer,
  }
}
</script>

<style scoped>
</style>

image

2)课程功能表分析

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

【实战课程】分析有几张表

课程分类表【CourseCategory】: 一个分类下有多个课程,跟课程一对多
实战课表【Course】:一个实战课会有多个章节,跟章节一对多
章节表【CourseChapter】:一个章节下有多个课时,跟课时一对多
课时表【CourseSection】:
老师表【Teacher】:一个老师下有多个实战课比,跟实战课一对多

# 一般正常课程都要有评论,得建立评论表,不过我们这次评论板块不写。

3)课程功能表创建

第一步:首先需要创建课程app

# 命令切换到apps路径下 执行创建课程app命令
python ../../ manage.py startapp course

# 在dev.py配置文件中注册app

第二步:在课程app下的models中创建表

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


# 课程分类表
class CourseCategory(BaseModel):
    """分类"""
    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专享'),
        (2, '学位课程')
    )
    level_choices = (
        (0, '初级'),
        (1, '中级'),
        (2, '高级'),
    )
    status_choices = (
        (0, '上线'),
        (1, '下线'),
        (2, '预上线'),
    )
    name = models.CharField(max_length=128, verbose_name="课程名称")
    course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
    course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
    brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
    level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
    period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
    attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True,
                                       null=True)
    status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")

    students = models.IntegerField(verbose_name="学习人数", default=0)  # 跟用户表做关联,优化字段

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

    # 课程和老师一对多,外键字段在多的一边
    # on_delete 级联删除,除了级联删除外还有很多其他的,暂时先不讲   DO_NOTHING:把某个字段删了后什么也不干,SET_NULL:置为空
    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):
    """章节"""
    # 章节跟课程一对多,关联字段写在多的一方
    # related_name 暂时先不讲
    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="课时标题")
    orders = models.PositiveSmallIntegerField(verbose_name="课时排序")
    section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
    section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接",
                                    help_text="若是video,填vid,若是文档,填link")
    duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
    pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
    free_trail = models.BooleanField(verbose_name="是否可试看", default=False)

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

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


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

创建完执行数据库迁移命令

# 项目根路径下执行
python manage.py makemigrations
python manage.py migrate

4)课程表数据录入

录入数据
-- 老师表
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);
admin中显示5张表

luffy_api>>apps>>course>>admin.py

from .models import *

admin.site.register(CourseCategory)
admin.site.register(Course)
admin.site.register(CourseChapter)
admin.site.register(CourseSection)
admin.site.register(Teacher)

5)查询所有课程分类接口

luffy_api>>apps>>course>>views.py

from rest_framework.viewsets import GenericViewSet
from .models import CourseCategory
from .serializer import CourseCategorySerializer
from utils.common_view import CommonListModelMixin

# 课程分类接口
class CourseCategoryView(GenericViewSet, CommonListModelMixin):
    # 要序列化或反序列化的表模型数据(筛选掉被锁定的用户)
    queryset = CourseCategory.objects.filter(is_delete=False, is_show=True).order_by('orders')
    # 要使用的序列化类
    serializer_class = CourseCategorySerializer

luffy_api>>apps>>course>>serializer.py

from rest_framework import serializers
from .models import CourseCategory

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

luffy_api>>utils>>common_view.py

这里是给查所有接口封装了 和我们前面写的有缓存的查所有接口代码会有很多地方用,所以封装起来分别是:有缓存的查所有、没缓存的查所有。到时候随用随改继承即可

from rest_framework.mixins import ListModelMixin
from .common_response import APIResponse

# 没缓存的查所有
class CommonListModelMixin(ListModelMixin):
    # 重写list方法
    def list(self, request, *args, **kwargs):
        res = super().list(request, *args, **kwargs)
        return APIResponse(data=res.data)

luffy_api>>apps>>course>>urls.py

不要忘记总路由的路由分发

# 访问http://127.0.0.1:8000/api/v1/course/category/ get请求就可以查看所有课程分类
router.register('category', views.CourseCategoryView, 'category')

测试:

image

6)查询所有课程接口

image

继续分析前端页面,还需要对所有课程按照课程分类过滤,有课程分类后 还需要按照人气(学习人数)价格进行排序

# 查询所有课程需注意:
	-需要老师的数据
    -需要章节和课时:如果课时数大于4就返回4条,如果小于4有多少就返回多少

luffy_api>>apps>>course>>models.py中

# 实战课表
class Course(BaseModel):
	...
        def course_type_name(self):
        # 如果表模型中用了choice,就可以用get_字段名_display() 拿到choice对应的中文
        return self.get_course_type_display()

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

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

    def section_list(self):
        """
        逻辑:
        先根据课程拿到所有章节
        循环所有章节,拿出每个章节下的课时拼到一个列表中
        判断列表长度是否>=4,如果是直接return结束循环
        """
        l1 = []
        # 拿到所有章节(正向查询直接按字段,反向查询单条:表名小写   多条:表名小写_set)
        course_chapter_list = self.coursechapters.all()
        for course_chapter in course_chapter_list:
            # 根据章节拿所有课时(反向查询 写了related_name)
            course_section_list = course_chapter.coursesections.all()
            for course_section in course_section_list:
                l1.append({
                    'name': course_section.name,  # 课时名字
                    'section_link': course_section.section_link,  # 课时链接
                    'duration': course_section.duration,  # 课时时长
                    'free_trail': course_section.free_trail,  # 是否免费
                })
                if len(l1) >= 4:
                    return l1
        # 如果全部循环完不足4个直接return
        return l1
   
# 老师表
class Teacher(BaseModel):
      ...
      def role_name(self):
            # 如果表模型中用了choice,就可以用get_字段名_display() 拿到choice对应的中文
            return self.get_role_display()

luffy)api>>apps>>course>>serializer.py

# 老师表序列化类
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',  # 课程id
            'name',  # 课程名字
            'course_img',  # 课程图片
            'brief',  # 课程介绍(该页面用不到,但是课程详情里会用到)
            'attachment_path',  # 课程课件路径
            'pub_sections',  # 更新了多少课时
            'price',  # 价格
            'students',  # 学习人数
            'period',  # 建议学习周期
            'sections',  # 总课时数量
            # 在表模型中写下面的三个方法
            'course_type_name',  # 定制的返回格式,可以在序列化类中或表模型中写方法(这里我们在表模型中写方法)
            'level_name',  # 等级名字
            'status_name',  # 课程状态
            # 关联字段
            'teacher',  # 表模型中写 或 序列化类中写 或 子序列化中写(给老师信息写个序列化类)
            'section_list',  # 表模型中写(给实战课表写一个章节d的课时方法)  (DDD:领域驱动模型)
        ]

luffy_api>>utils>>新建分页common_page.py

from rest_framework.pagination import PageNumberPagination


# 分页
class CommonPageNumberPagination(PageNumberPagination):
    page_size = 2  # 每页显示条数
    page_query_param = 'page'  # 查询条件 page=3
    page_size_query_param = 'page_size'  # 查三页,每页显示5条 page=3&size=5
    max_page_size = 5  # 每页最大显示5条

luffy_api>>apps>>course>>views.py

# 查询所有课程接口
class CourseView(GenericViewSet, CommonListModelMixin):
    # 要序列化或反序列化的表模型数据(筛选掉被锁定的用户)
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
    # 要使用的序列化类
    serializer_class = CourseSerializer
    # 分页
    pagination_class = CommonPageNumberPagination
    # 要使用的过滤类
    filter_backends = [OrderingFilter, DjangoFilterBackend]
    # 按哪个字段排序
    ordering_fields = ['price', 'students']
    # 过滤(按课程分类过滤)过滤类有三种:内置(只能模糊)、第三方、自定义 
    # 需要使用第三方的django-filter过滤
    # http://127.0.0.1:8000/api/v1/course/course/?ordering=students&course_category=1
    filterset_fields = ['course_category']  # 按课程分类过滤

路由

# 访问http://127.0.0.1:8000/api/v1/course/course/ get请求就可以查看所有课程
router.register('course', views.CourseView, 'course')

7)实战课页面前端

src>>views>>新建实战课列表前端ActualCourseView.vue

<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="[2, 3, 5, 10]"
            :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: 2,       // 单页数据量
        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 => {
        this.category_list = response.data.data;
      }).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 => {
        // console.log(response.data);
        this.course_list = response.data.data.results;
        this.course_total = response.data.data.count;
        // console.log(this.course_list);
      }).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>

8)课程详情页前端

#课程详情页中由于有第三方【视频播放器组件】
1.安装
        cnpm install vue-core-video-player -S

2.在main.js配置
    	import VueCoreVideoPlayer from 'vue-core-video-player'
        Vue.use(VueCoreVideoPlayer, {
          lang: 'zh-CN'
        })
        
3.在使用的组件中,使用即可
    	<vue-core-video-player
              src="https://video.pearvideo.com/mp4/third/20230307/cont-1779481-10023871-222053-hd.mp4"
              @play="onPlayerPlay"
              @pause="onPlayerPause"
              title="课程名字"
              //视频底部控制栏 无操作时隐藏
              controls='auto'>
                
          </vue-core-video-player>
            
            
      onPlayerPlay() {
      // 当视频播放时,执行的方法
      console.log('视频开始播放')
    },
    onPlayerPause() {
      // 当视频暂停播放时,执行的方法
      console.log('视频暂停,可以打开广告了')
    },  
    
    
4.更详细:https://www.cnblogs.com/liuqingzheng/p/16204851.html

src>>views>>新建一个课程详情页 CourseDetail.vue

<template>
  <div class="detail">
    <Header/>
    <div class="main">
      <div class="course-info">
        <div class="wrap-left">
          <vue-core-video-player
              src="https://video.pearvideo.com/mp4/third/20230307/cont-1779481-10023871-222053-hd.mp4"
              @play="onPlayerPlay"
              @pause="onPlayerPause"
              title="课程名字"
              controls='auto'
          >
          </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: [], // 课程的章节课时列表
    }
  },
  created() {
    this.get_course_id();
    this.get_course_data();
    this.get_chapter();
  },
  methods: {
    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 => {
        // window.console.log(response.data);
        this.course_info = response.data.data;
        console.log(this.course_info)
      }).catch(() => {
        this.$message({
          message: "对不起,访问页面出错!请联系客服工作人员!"
        });
      })
    },

    get_chapter() {
      // 获取当前课程对应的章节课时信息
      // http://127.0.0.1:8000/course/chapters/?course=(pk)
      this.$axios.get(`${this.$settings.BASE_URL}/course/coursechapter/`, {
        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>

src>>router>>index.js配置路由

    {
        path:'/free/detail/:id',
        name:'detail',
        component: CourseDetail
    }

9)课程详情接口

由于没有章节和课时内容,所以还要再写一个所有章节接口,章节里带课时

# 根据课程详情页前端分析接口
    1.课程详情接口:#根据id查√
    2 课程章节接口:#单独写的课程章节接口,但它是可以放在课程详情接口中的√
    3 查询老师接口:不需要写,课程详情接口中有老师所有信息了×

image

from rest_framework.mixins import RetrieveModelMixin
from utils.common_response import APIResponse

# 查询所有课程接口
class CourseView(GenericViewSet, CommonListModelMixin, RetrieveModelMixin):
	...
    #重写retrieve查单条
    def retrieve(self, request, *args, **kwargs):
        res = super().retrieve(request, *args, **kwargs)
        return APIResponse(data=res.data)
验证

image

10)所有章节接口

按课程id号过滤>>查询到当前课程的所有章节http://127.0.0.1:8000/api/v1/course/coursechapter/?course=3

luffy_api>>apps>>course>>views.py

# 查询所有章节接口
class CourseChapterView(GenericViewSet, CommonListModelMixin):
    # 要序列化或反序列化的表模型数据(筛选掉被锁定的用户)
    queryset = CourseChapter.objects.filter(is_delete=False, is_show=True).order_by('orders')
    # 要使用的序列化类
    serializer_class = CourseChapterSerializer
    # 要使用的过滤类
    filter_backends = [DjangoFilterBackend]
    # 过滤(按课程过滤章节) 需要使用第三方的django-filter
    filterset_fields = ['course']

luffy_api>>apps>>course>>serializer.py

# 课时序列化类
class CourseSectionSerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseSection
        fields = (
            'id',
            'name',  # 课时名
            'orders',  # 课时排序
            'section_link',  # 课时链接
            'duration',  # 视频时长
            'free_trail'  # 是否免费
        )


# 章节序列化类
class CourseChapterSerializer(serializers.ModelSerializer):
    # 子序列化  多条
    coursesections = CourseSectionSerializer(many=True)

    class Meta:
        model = CourseChapter
        fields = (
            'id',
            'name',  # 章节名字
            'chapter',  # 第几章
            'summary',  # 章节介绍
            'coursesections'  # 章节下所有课时(子序列化)
        )

luffy_api>>apps>>course>>urls.py

# 访问 http://127.0.0.1:8000/api/v1/course/coursechapter/  get请求就可以查询所有章节(必须带过滤:就能查到某个课程的所有章节)
router.register('coursechapter', views.CourseChapterView, 'coursechapter')

image

十九.搜索功能

1)文件存储

思考:有个视频文件需要存储到某个位置,如果放在自己服务器上

①放在项目的media文件夹,但是服务器上线后用户既要访问接口又要看视频,会大大占用请求,不方便
②把文件单独放在文件服务器上,文件服务器带宽很高

今后的图片、视频等就不放在media中了,而是放在文件服务器中

文件服务器

# 第三方:
        -阿里云:对象存储oss
        -腾讯云:对象存储
        -七牛云存储:更便宜
        
# 自己搭建: 
        fastdfs:'中小文件'对象存储  https://zhuanlan.zhihu.com/p/372286804
        minio
        go-fastdfs:
七牛云上传文件

python如何把文件传到七牛云中?有对应的sdk

前端>>传到后端>>从后端用代码传到七牛云>>会有一个链接地址直接使用即可	
前端>>传到自己服务器>>从服务器再传到七牛云

下载模块

pip install qiniu
from qiniu import Auth, put_file

q = Auth('access_key', 'secret_key')
"""
access_key: 账号密钥对中的accessKey,详见 https://portal.qiniu.com/user/key
secret_key: 账号密钥对重的secretKey,详见 https://portal.qiniu.com/user/key
"""
# 要上传的空间
bucket_name = 'zy'
# 上传后保存的文件名
key = '致命诱惑.mp4'
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
# 要上传文件的本地路径
localfile = './致命诱惑.mp4'
ret, info = put_file(token, key, localfile, version='v2')
print(info)

2)搜索导航栏

前端Header组件上放搜索框>>输入内容搜索>>后端有个搜索接口

所有的商城类网站、app都会有搜索功能,且技术含量很高
​ 如:输入课程名字、价格就可以把实战课程搜索出来(查询多个表)

以后会有专门的搜索引擎:分布式全文检索引擎es,做专门的搜索

image

前端搜索结果要呈现在页面:
src>>components>>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 v-if="!username">
          <span @click="put_login">登录</span>
          <span class="line">|</span>
          <span @click="put_register">注册</span>
        </div>
        <div v-else>
          <span>{{ username }}</span>
          <span class="line">|</span>
          <span>注销</span>
        </div>

      </div>
    </div>

    <Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login"/>
    <Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register"/>
    <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">
      <button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)">搜索</button>
    </form>
  </div>

</template>

<script>
import Login from "@/components/Login";
import Register from "@/components/Register";

export default {
  name: "Header",
  data() {
    return {
      // 当前所在路径,去sessionStorage取的,如果取不到,就是 /
      url_path: sessionStorage.url_path || '/',
      is_login: false,
      is_register: false,
      username: this.$cookies.get('username'),
      token: this.$cookies.get('token'),
      is_search_tip: true,
      search_placeholder: '',
      search_word: ''
    }
  },
  methods: {
    search_action(search_word) {
      console.log(search_word)
      if (!search_word) {
        this.$message('请输入要搜索的内容');
        return
      }
      if (search_word !== this.$route.query.word) {
        this.$router.push(`/course/search?word=${search_word}`);
      }
      this.search_word = '';
    },
    on_search() {
      this.search_placeholder = '请输入想搜索的课程';
      this.is_search_tip = false;
    },
    off_search() {
      this.search_placeholder = '';
      this.is_search_tip = true;
    },
    goPage(url_path) {
      // 已经是当前路由就没有必要重新跳转
      if (this.url_path !== url_path) {
        this.$router.push(url_path);
      }
      sessionStorage.url_path = url_path;
    },
    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;
    },
    success_login() {
      this.is_login = false;
      this.username = this.$cookies.get('username')
      this.token = this.$cookies.get('token')
    },
    success_register() {
      this.is_login = true
      this.is_register = false

    }
  },
  created() {
    // 组件加载万成,就取出当前的路径,存到sessionStorage  this.$route.path
    sessionStorage.url_path = this.$route.path;
    // 把url_path = 当前路径
    this.url_path = this.$route.path;
  },
  components: {
    Login,
    Register
  }
}
</script>

<style scoped>
.search {
  float: right;
  position: relative;
  margin-top: 22px;
  margin-right: 10px;
}

.search input, .search button {
  border: none;
  outline: none;
  background-color: white;
}

.search input {
  border-bottom: 1px solid #eeeeee;
}

.search input:focus {
  border-bottom-color: orange;
}

.search input:focus + button {
  color: orange;
}

.search .tips {
  position: absolute;
  bottom: 3px;
  left: 0;
}

.search .tips span {
  border-radius: 11px;
  background-color: #eee;
  line-height: 22px;
  display: inline-block;
  padding: 0 7px;
  margin-right: 3px;
  cursor: pointer;
  color: #aaa;
  font-size: 14px;

}

.search .tips span:hover {
  color: orange;
}

.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>

src>>router>>index.js

    {
        path: '/course/search',
        name: 'search',
        component: SearchCourse
    },

3)搜索页面

ser>>views>>新建SearchCourse.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="[2, 3, 5, 10]"
            :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.$route.query.wd;

      // 获取课程列表信息
      this.$axios.get(`${this.$settings.BASE_URL}/course/search/`, {
        params: this.filter
      }).then(response => {
        console.log(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>

4)后端搜索接口

搜索接口:get请求>>..../course/search/?+要搜索的条件通过问号拼接在后面

luffy_api>>apps>>course>>urls.py

# 访问http://127.0.0.1:8000/api/v1/course/search?word=Python   get请求就可以搜索对应内容
router.register('search', views.SearchCourseView, 'search')

luffy_api>>apps>>course>>views.py

from rest_framework.filters import SearchFilter

# 搜索接口(因为也是查询所有所以继承CommonListModelMixin 继续序列化 所有课程 的)
class SearchCourseView(GenericViewSet, CommonListModelMixin):
    # 要序列化或反序列化的表模型数据(筛选掉被锁定的用户)
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
    # 要使用的序列化类
    serializer_class = CourseSerializer
    # 要使用的过滤类
    filter_backends = [SearchFilter]
    # 按什么字段去搜索
    search_fields = ['name', 'price']
    # 分页
    pagination_class = CommonPageNumberPagination
    
    # # 额外补充:如果有多个课程(实战课、免费课、轻课),该接口都要搜索返回需要重写list
    # def list(self, request, *args, **kwargs):
    #     res = super(SearchCourseView, self).list(request, *args, **kwargs)
    #     # 实战课
    #     actual_course = res.data
    #     # 免费课(需自己写)
    #     free_course = [{}, {}]
    #     # 轻课(需自己写)
    #     light_course = [{}, {}, {}]
    #     return APIResponse(actual_course=actual_course, free_course=free_course, light_course=light_course)
    #     # 前端接收到的数据应该是{code:100,msg:成功,actual_course:[],free_course:[],light_course:[]}

二十.支付

1)支付宝支付介绍

购买功能:点击购买按钮>>使用支付宝支付

# 支付宝支付
-测试环境:任何人都可以测试
        沙箱环境实名认证:https://openhome.alipay.com/platform/appDaily.htm?tab=info
-正式环境:需申请,要有营业执照
        
        
# 我们开发虽然用的沙箱环境,后期上线公司会自己注册,注册成功后有个商户id号,作为开发只要有商户id号其他步骤都是一样,所有无论开发还是测试,代码都一样 只是商户号不一样

支付流程:

用户在系统中点击购买>>后端生成支付链接并返回给前端>>打开支付链接(支付宝付款页面),用户扫码/输入账号密码付款>>支付宝收到用户的钱>>用户付到支付宝商户上>>支付宝会有前端回调(get回调)>>掉回到我们的页面(配置)>>自己做一个付款成功页面>>后台写两个支付回调接口(get自己用,post支付宝用:验证签名)

image

使用支付宝支付

-API接口
-SDK:【'如果有就优先使用'】,早期支付宝没有python的sdk,后来才有的
-使用第三方sdk
        -第三方通过api接口,使用python封装了sdk并开源出来了

沙箱环境

-安卓的测试支付宝app,买家付款用的
-扫码使用这个app付款,这个app的钱都是假的,付款进测试商户(卖家)
(1)生成支付链接简单使用

GitHub开源框架

https://github.com/fzlee/alipay

安装依赖

pip install python-alipay-sdk

生成公钥私钥(非对称加密)

使用支付宝提供的工具 下载:https://opendocs.alipay.com/common/02kipk

19

生成的公钥配置在支付宝的网站上(沙箱环境)>>自定义密钥>>公钥模式>>生成一个支付宝公钥(以后使用这个支付宝公钥)

image

后端代码

luffy_api>>scripts>>新建两个文件【pub.pem】【pri.pem】

# 把生成的支付宝公钥复制在【pub.pem】中:
-----BEGIN PUBLIC KEY-----
支付宝公钥
-----END PUBLIC KEY-----

——————————————————————————

# 把应用中的应用私钥复制在【pri.pem】中:
-----BEGIN RSA PRIVATE KEY-----
应用私钥
-----END RSA PRIVATE KEY-----

luffy_api>>scripts>>新建【alipay_t.py】

from alipay import AliPay
from alipay.utils import AliPayConfig

app_private_key_string = open("./pri.pem").read()
alipay_public_key_string = open("./pub.pem").read()
alipay = AliPay(
    # https://open.alipay.com/develop/sandbox/app
    appid="2021000122628526",
    app_notify_url=None,  # 默认回调 url
    app_private_key_string=app_private_key_string,
    # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
    alipay_public_key_string=alipay_public_key_string,
    sign_type="RSA2",  # RSA 或者 RSA2
    debug=False,  # 默认 False
    verbose=False,  # 输出调试数据
    config=AliPayConfig(timeout=15)  # 可选,请求超时时间
)
res = alipay.api_alipay_trade_page_pay(subject='性感内衣', out_trade_no='asdas23sddfsasf', total_amount='999')
print('https://openapi.alipaydev.com/gateway.do?' + res)

image

image

(2)对以上二次封装

目录结构

libs
    ├── alipay_v1                                    # aliapy二次封装包
    │  ├── pem                                        # 公钥私钥文件夹
    │       ├── alipay_public_key.pem      # 支付宝公钥文件
    │       └── app_private_key.pem	       # 应用私钥文件
    │  ├── __init__.py                               # 包文件
    │  ├── pay.py                                    # 支付文件
    └─└── settings.py                             # 应用配置  

pem文件夹下

# 把生成的支付宝公钥复制在【alipay_public_key.pem】中:
-----BEGIN PUBLIC KEY-----
支付宝公钥
-----END PUBLIC KEY-----

——————————————————————————

# 把应用中的应用私钥复制在【app_private_key.pem】中:
-----BEGIN RSA PRIVATE KEY-----
应用私钥
-----END RSA PRIVATE KEY-----

init.py

from .pay import alipay
from .settings import GATEWAY

pay.py

from alipay import AliPay
from alipay.utils import AliPayConfig
from . import settings

alipay = AliPay(
    appid=settings.APP_ID,
    app_notify_url=None,  # 默认回调 url
    app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
    # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
    alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
    sign_type=settings.SIGN,  # RSA 或者 RSA2
    debug=settings.DEBUG,  # 默认 False
    verbose=settings.DEBUG,  # 输出调试数据
    config=AliPayConfig(timeout=15)  # 可选,请求超时时间
)

settings.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 = '2021000122628526'

# 加密方式
SIGN = 'RSA2'

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

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

创建订单app

# 1.进入apps路径下执行创建订单app命令:
python ../../manage.py startapp order

# 2.配置文件dev.py中注册app

# 3.给订单app创建路由(不要忘记总路由的路由分发)

生成订单链接接口

luffy_api>>order>>views.py

# 只要向该接口发请求就可以生成一个订单链接

from rest_framework.viewsets import ViewSet
from libs.alipay_v1 import alipay, GATEWAY
from utils.common_response import APIResponse
from rest_framework.decorators import action

# 测试:生成订单链接接口
class PayView(ViewSet):
    @action(methods=['POST'], detail=False)
    def pay(self, request):
        # 商品名、订单号、商品价格
        res = alipay.api_alipay_trade_page_pay(subject='男士内衣', out_trade_no='1111', total_amount='888')
        pay_url = GATEWAY + res
        return APIResponse(pay_url=pay_url)

使用Postman访问接口

image

用浏览器打开链接>>使用沙箱账号登录付款https://openhome.alipay.com/develop/sandbox/account

image

image

3)订单表设计

# 订单板块需要写的接口
    -下单接口>>没有支付订单是待支付状态
    -支付宝post回调接口>>修改订单状态成已支付
    -前端get回调接口(暂时先不关注)

# 订单板块表设计
    -订单表
    -订单详情表
(1)创建表

luffy_api>>apps>>order>>models.py

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)
    # 订单号,后端生成唯一的:后期支付宝回调回来的数据会带着这个订单号,根据这个订单号修改订单状态
    # 生成订单号: uuid生成,可能重复,但概率较低    其他方法:【分布式id的生成】雪花算法
    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="支付时间")
    # 下单用户 跟用户一对多关系    models.DO_NOTHING:什么也不做(如果把用户删了订单不动)
    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)


# 订单详情表
class OrderDetail(models.Model):
    """订单详情"""
    # related_name:反向查询替换表名小写_set
    # on_delete:级联删除
    # db_constraint=False:默认是True,会在表中为Order和OrderDetail创建外键约束
    # 如果是False:则没有外键约束,插入数据速度快,但可能会产生脏数据 很不合理的数据,所以要用程序控制,以后公司惯用的方法
    # 其实对到数据库上它是【不建立外键】,基于对象的跨表查、基于连表的查询正常用和之前没有任何区别(因为orm是根据ForeignKey去做的 和它无关)
    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.DO_NOTHING, 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__()

补充

# ForeignKey 中on_delete  
 -.CASCADE  级联删除
 -.DO_NOTHING     数据删掉后该字段啥都不做,前提是没有外键约束才能用(db_constraint=False)
 -.SET_NULL       数据删掉后该字段置为空,前提是字段null=True
 -.SET_DEFAULT    数据删掉后该字段设置为默认值,前提是设置了默认值default='xx'
 -.PROTECT    受保护的(很少用)
 -.SET(值/函数内存地址)   数据删掉后该字段变成set内的值

数据库迁移命令

# 在项目根目录路径下执行:
python manage.py makemigrations
python manage.py migrate
(2)下单接口
# 登录后前端点击立即购买>>发送post携带数据{courses:[id号,id号],total_amount:总价格,subject:订单标题}到后端>>视图类中重写create方法>>主要逻辑写到序列化类中

#后端 主要逻辑:
    1.取出所有课程id号,根据id拿到课程
    2.统计总价格,跟传入的total_amount做比较,如果一样继续往后执行(如果不一样则说明)
    3.获取购买人信息:登录后才能访问的接口,(request.user中可以取出)
    4.生成订单号:支付链接需要、存订单表也需要,uuid
    5.生成支付链接:支付宝支付生成
    
    6.生成订单记录,订单是待支付状态  在两个表中写数据(order表  order_detail表)
    7.返回前端支付链接

luffy_api>>apps>>order>>serializer.py

from rest_framework import serializers
from .models import Order, OrderDetail
from course.models import Course
from rest_framework.exceptions import APIException
import uuid
from libs.alipay_v1 import alipay, GATEWAY
from django.conf import settings


# 只做校验字段、反序列化 不做系列化
# 支付序列化类
class PaySerializer(serializers.ModelSerializer):
    # courses不是表的字段,需要重写
    # courses = serializers.ListField() 可以 但是后面我们还要通过列表去获取课程对象,所以我们用新方法
    # 前端传入的courses=[1,2]>>根据queryset对应的queryset对象做映射,映射成courses=[课程对象1,课程对象2]
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)
    class Meta:
        model = Order
        # 前端传什么就写什么
        fields = ['courses', 'total_amount', 'subject']

    # 订单总价校验
    def _check_total_amount(self, attrs):
        courses = attrs.get('courses')  # 课程对象列表[课程对象1,课程对象2]
        total_amount = attrs.get('total_amount')
        new_total_amount = 0
        for i in courses:
            new_total_amount += i.price
        if total_amount == new_total_amount:
            return new_total_amount
        raise APIException('价格有误!')

    # 生成订单号(uuid生成)
    def _get_out_trade_no(self):
        return str(uuid.uuid4())

    # 获取支付用户
    def _get_user(self):
        # 从桥梁获取到request
        user = self.context.get('request').user
        return user

    # 生成支付链接(订单号、总价格、订单标题)
    def _get_pay_url(self, out_trade_no, total_amount, subject):
        res = alipay.api_alipay_trade_page_pay(
            out_trade_no=out_trade_no,
            total_amount=float(total_amount),
            subject=subject,
            notify_url=settings.NOTIFY_URL,  # 回调给后端的地址,写这个接口改订单状态
            return_url=settings.RETURN_URL,  # 回调给前端的地址
        )
        self.context['pay_url'] = GATEWAY + res

    # 入库(两个表)前的准备:courses不是两个表中的字段所以要剔除
    def _before_create(self, attrs, user, out_trade_no):
        # 剔除courses:不在这里剔除,因为订单详情还要用,在create中剔除
        # 把user用户加入到attrs中
        attrs['user'] = user
        # 把订单号加入到attrs中
        attrs['out_trade_no'] = out_trade_no

    # 全局钩子校验
    def validate(self, attrs):
        # attrs:前端传入的所有数据,然后经过字段自己校验和局部钩子校验过后的数据
        # 1.订单总价校验
        total_amount = self._check_total_amount(attrs)
        # 2.生成订单号
        out_trade_no = self._get_out_trade_no()
        # 3.支付用户:request.user
        user = self._get_user()
        # 4.支付链接生成
        self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
        # 5.入库(两个表)前的准备
        self._before_create(attrs, user, out_trade_no)
        return attrs

    # 生成订单存订单表都必须重写create存两个表(不然只会存一个表)
    def create(self, validated_data):
        # validated_data里的数据:{subject,total_amount,user,out_trade_no,courses}
        # 剔除course
        courses = validated_data.pop('courses')
        # 存订单表 打散数据变成k=v,k=v型式
        order = Order.objects.create(**validated_data)
        # 存订单详情表(存几条取决于课程对象有几个)
        for i in courses:
            OrderDetail.objects.create(order=order, course=i, price=i.price, real_price=i.price)
        return order

视图类

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from .models import Order
from .serializer import PaySerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from utils.common_response import APIResponse

class PayView(GenericViewSet, CreateModelMixin):
    # 要序列化或反序列化的表模型数据
    queryset = Order.objects.all()
    # 要使用的序列化类
    serializer_class = PaySerializer
    # 由于必须登录才能访问所以加入认证类和权限类
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

    # 下单接口
    def create(self, request, *args, **kwargs):
        # 获取要使用的序列化类 前端提交过来的数据都在request.data中,把request通过桥梁传到序列化类中
        res = self.get_serializer(data=request.data, context={'request': request})
        res.is_valid(raise_exception=True)
        self.perform_create(res)
        # 从序列化类中的桥梁中取出支付链接
        pay_url = res.context.get('pay_url')
        return APIResponse(pay_url=pay_url)

luffy_api>>settings>>dev.py

# 后端地址
BACKEND_URL = 'http://127.0.0.1:8000'
# 前端地址
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后端异步回调接口
NOTIFY_URL = BACKEND_URL + "/api/v1/order/success/"  # 如果这个接口不对,支付宝永远掉不回来,订单状态永远不会改
# 前端同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/pay/success"

路由

# http://127.0.0.1:8000/api/v1/order/pay/pay/
router.register('pay',views.PayView,'pay')

image

image

(3)前端支付页面

src>>views>>CourseDetail.vue

给立即购买按钮添加一个点击事件

go_pay() {
      // 判断是否登录
      let token = this.$cookies.get('token')
      if (token) {
        this.$axios.post(this.$settings.BASE_URL + '/order/pay/', {
          subject: this.course_info.name,
          total_amount: this.course_info.price,
          courses: [this.course_id]
        }, {
          headers: {
            Authorization: `jwt ${token}`
          }
        }).then(res => {
          if (res.data.code == 100) {
            // 打开支付连接地址
            open(res.data.pay_url, '_self');
          } else {
            this.$message(res.data.msg)
          }
        })
      } else {
        this.$message('您没有登录,请先登录')
      }
    }
<template>
  <div class="detail">
    <Header/>
    <div class="main">
      <div class="course-info">
        <div class="wrap-left">
          <vue-core-video-player
              src="http://rrfq1koh6.hd-bkt.clouddn.com/1%20%E4%B8%8A%E8%8A%82%E8%AF%BE%E5%9B%9E%E9%A1%BE%282%29%282%29.mp4"
              @play="onPlayerPlay"
              @pause="onPlayerPause"
              title="课程名字"
              controls='auto'
          >
          </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" @click="go_pay">立即购买</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 @click="go_pay">立即购买</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: [], // 课程的章节课时列表
    }
  },
  created() {
    this.get_course_id();
    this.get_course_data();
    this.get_chapter();
  },
  methods: {
    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 => {
        // window.console.log(response.data);
        this.course_info = response.data.data;
        console.log(this.course_info)
      }).catch(() => {
        this.$message({
          message: "对不起,访问页面出错!请联系客服工作人员!"
        });
      })
    },

    get_chapter() {
      // 获取当前课程对应的章节课时信息
      // http://127.0.0.1:8000/course/chapters/?course=(pk)
      this.$axios.get(`${this.$settings.BASE_URL}/course/coursechapter/`, {
        params: {
          "course": this.course_id,
        }
      }).then(response => {
        this.course_chapters = response.data.data;
      }).catch(error => {
        window.console.log(error.response);
      })
    },
    go_pay() {
      // 判断是否登录
      let token = this.$cookies.get('token')
      if (token) {
        this.$axios.post(this.$settings.BASE_URL + '/order/pay/', {
          subject: this.course_info.name,
          total_amount: this.course_info.price,
          courses: [this.course_id]
        }, {
          headers: {
            Authorization: `jwt ${token}`
          }
        }).then(res => {
          if (res.data.code == 100) {
            // 打开支付连接地址
            open(res.data.pay_url, '_self');
          } else {
            this.$message(res.data.msg)
          }
        })
      } else {
        this.$message('您没有登录,请先登录')
      }
    }
  },
  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>

当支付完还应该有个回调一个支付成功的页面

src>>views>>新建PaySuccess.vue

<template>
  <div class="pay-success">
    <!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
    <Header/>
    <div class="main">
      <div class="title">
        <div class="success-tips">
          <p class="tips">您已成功购买 1 门课程!</p>
        </div>
      </div>
      <div class="order-info">
        <p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
        <p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
        <p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
      </div>
      <div class="study">
        <span>立即学习</span>
      </div>
    </div>
  </div>
</template>

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

export default {
  name: "Success",
  data() {
    return {
      result: {},
    };
  },
  created() {
    // 解析支付宝回调的url参数
    let params = location.search.substring(1);  // 去除? => a=1&b=2
    let items = params.length ? params.split('&') : [];  // ['a=1', 'b=2']
    //逐个将每一项添加到args对象中
    for (let i = 0; i < items.length; i++) {  // 第一次循环a=1,第二次b=2
      let k_v = items[i].split('=');  // ['a', '1']
      //解码操作,因为查询字符串经过编码的
      if (k_v.length >= 2) {
        // url编码反解
        let k = decodeURIComponent(k_v[0]);
        this.result[k] = decodeURIComponent(k_v[1]);
        // 没有url编码反解
        // this.result[k_v[0]] = k_v[1];
      }

    }

    // 把地址栏上面的支付结果,再get请求转发给后端
    this.$axios({
      url: this.$settings.BASE_URL + '/order/success/' + location.search,
      method: 'get',
    }).then(response => {
      if (response.data.code != 100) {
        alert(response.data.msg)
      }
    }).catch(() => {
      console.log('支付结果同步失败');
    })
  },
  components: {
    Header,
  }
}
</script>

<style scoped>
.main {
  padding: 60px 0;
  margin: 0 auto;
  width: 1200px;
  background: #fff;
}

.main .title {
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  padding: 25px 40px;
  border-bottom: 1px solid #f2f2f2;
}

.main .title .success-tips {
  box-sizing: border-box;
}

.title img {
  vertical-align: middle;
  width: 60px;
  height: 60px;
  margin-right: 40px;
}

.title .success-tips {
  box-sizing: border-box;
}

.title .tips {
  font-size: 26px;
  color: #000;
}


.info span {
  color: #ec6730;
}

.order-info {
  padding: 25px 48px;
  padding-bottom: 15px;
  border-bottom: 1px solid #f2f2f2;
}

.order-info p {
  display: -ms-flexbox;
  display: flex;
  margin-bottom: 10px;
  font-size: 16px;
}

.order-info p b {
  font-weight: 400;
  color: #9d9d9d;
  white-space: nowrap;
}

.study {
  padding: 25px 40px;
}

.study span {
  display: block;
  width: 140px;
  height: 42px;
  text-align: center;
  line-height: 42px;
  cursor: pointer;
  background: #ffc210;
  border-radius: 6px;
  font-size: 16px;
  color: #fff;
}
</style>

src>>router>>index.js

添加路由

    {
        path: '/pay/success',
        name: 'success',
        component: SearchCourse
    },
(4)支付宝成功回调两个接口
# get 给自己用
# post 给支付宝用

# 该接口不要加任何认证和权限,支付宝根本没我们的token,加了就用不了了

luffy_api>>apps>>order>>views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from utils.common_logger import logger
from libs import alipay_v1


# 支付成功回调的接口
class PaySuccess(APIView):
    def get(self, request):  # 咱们用的
        out_trade_no = request.query_params.get('out_trade_no')
        order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
        if order:  # 支付宝回调完, 订单状态改了
            return APIResponse()
        else:
            return APIResponse(code=101, msg='暂未收到您的付款,请稍后刷新再试')

    def post(self, request):  # 给支付宝用的,项目需要上线后才能看到  内网中,无法回调成功【使用内网穿透】
        try:
            result_data = request.data.dict()  # requset.data 是post提交的数据,如果是urlencoded格式,requset.data是QueryDict对象,方法dict()---》转成真正的字典
            out_trade_no = result_data.get('out_trade_no')
            signature = result_data.pop('sign')
            # 验证签名的---》验签
            result = alipay_v1.alipay.verify(result_data, signature)
            if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
                # 完成订单修改:订单状态、流水号、支付时间
                Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
                # 完成日志记录
                logger.warning('%s订单支付成功' % out_trade_no)
                return Response('success')  # 都是支付宝要求的
            else:
                logger.error('%s订单支付失败' % out_trade_no)
        except:
            pass
        return Response('failed')  # 都是支付宝要求的

luffy_api>>apps>>order>>urls.py

urlpatterns = [
    # http://127.0.0.1:8000/api/v1/order/success/
    path('success/', views.PaySuccess.as_view()),
]

二十一.项目上线

1)上线架构图

上线需要服务器公网ip地址,我们采用云服务器:阿里云、腾讯云

image

2)阿里云购买

1.购买阿里云服务器ECS>>试用一个月>>2核4G>>系统用CentOS

给实例重置一个密码用户名root 密码Zy123456
给主机名、实例名改个名字:zy

image

2.远程连接服务器

1.xshell

2.finalshell:http://www.hostbuf.com/t/988.html
      只能在windows上使用,'推荐'
        
3.Git Bash命令

image

image

3)配置服务器

安装一些常用依赖

粘贴:ctrl shift v

# 更新软件包的源
>: yum update -y
    
# 装一堆开发用的工具:git等
>: yum -y groupinstall "Development tools"
>: yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel
(1)云服务器安装mysql
1)前往用户根目录
>: cd ~  # 回到你家路径  /root

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

3)安装mysql57
>: yum -y install mysql57-community-release-el7-10.noarch.rpm
>: yum install mysql-community-server --nogpgcheck -y

4)启动mysql57并查看启动状态
>: systemctl start mysqld.service  # 启动mysql57
>: systemctl status mysqld.service  # 查看启动状态

5)查看默认密码并登录
>: grep "password" /var/log/mysqld.log
      密码>>:;e2Pl+lSo7y
            
6)登录
>: mysql -uroot -p

7)修改密码(必须有一个大写字母、必须有一个特殊符号)
>: ALTER USER 'root'@'localhost' IDENTIFIED BY 'Zy123456?';
    
8)退出
>:quit
下载完可以删除安装包
# 查看当前路径下的文件
:>ls

发现mysql57的安装包还在

# 删除当前路径下所有文件
:>rm -rf *
(2)云服务器安装redis(源码安装)
1)前往用户根目录
>: cd ~

2)下载redis-5.0.5
>: wget http://download.redis.io/releases/redis-5.0.5.tar.gz

3)解压安装包
>: tar -xf redis-5.0.5.tar.gz

4)删除安装包
>: rm -rf redis-5.0.5.tar.gz
    
5)进入目标文件
>: cd redis-5.0.5

6)编译环境
>: make     # 编译后在src路径下就会有可执行文件:redis-server    redis-cli

7)复制环境到指定路径完成安装
>: cp -r ~/redis-5.0.5 /usr/local/redis
    
8)删除根路径下的解压文件(没用的)
>: cd ~
>: rm -rf redis-5.0.5/

9)配置redis可以后台启动:修改下方内容
>: vim /usr/local/redis/redis.conf
# i:插入  
daemonize yes
# esc+shift :>>wq:保存退出

10)建立软连接(因为/usr/local/redis/src没有加到环境变量)
# 因为/usr/bin/ 在环境变量中,以后直接敲redis-server就能找到了
>: ln -s /usr/local/redis/src/redis-server /usr/bin/redis-server
>: ln -s /usr/local/redis/src/redis-cli /usr/bin/redis-cli

11)后台运行redis
>: cd /usr/local/redis
>: redis-server ./redis.conf &
# 查看是否正常在运行:ps aux |grep redis

12)测试redis环境
>: redis-cli
ctrl + c 退出

———————以下暂时不用———————————————————

13)关闭redis服务
>: pkill -f redis -9
(3)云服务器安装python3.8(源码安装)

注意:

1.linux、mac系统服务是用python2写的,所以这两个系统中默认自带了python2不要卸载!
2.阿里云的centos 自动装了python3.6,所以我们的云服务器上有python2 和python3.6
3.python3.8需自己安装

方式一:centos安装  >: yum install python
              # 可以快速安装,但是不能指定版本不能指定安装目录 【不推荐】
        
方式二:源码安装 
              # 下载指定版本的源码 编译安装【推荐】
###### 源码安装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
#################################################

1)前往用户根目录
>: cd ~

2)下载 或 上传 Python3.8.6  服务器终端
>: wget https://registry.npmmirror.com/-/binary/python/3.8.6/Python-3.8.6.tgz

3) 解压安装包
>: tar -xf Python-3.8.6.tgz
# 删除压缩包 rm -rf Python-3.8.6.tgz

4)进入目标文件
>: cd Python-3.8.6

5) 配置安装路径:/usr/local/python3
# 把python3.8.6 编译安装到/usr/local/python38路径下
>: ./configure --prefix=/usr/local/python38

6)编译并安装,如果报错,说明缺依赖
# 报错则执行:
>: yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel zlib* libffi-devel  -y
# 没报错直接编译并安装
>: make &&  make install

7)建立软连接:/usr/local/python38路径不在环境变量,终端命令 python3,pip3就可以找到
>: ln -s /usr/local/python38/bin/python3 /usr/bin/python3.8
>: ln -s /usr/local/python38/bin/pip3 /usr/bin/pip3.8

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

8)删除安装包与文件:
>: rm -rf Python-3.8.8
>: rm -rf Python-3.8.8.tar.xz
(4)安装uwsgi

django、flask项目上线需要使用uwsgi部署,它性能高符合wsig协议的web服务器

使用uwsgi运行django不再使用测试阶段的wsgiref来运行django了
uwsgi是符合wsgi协议的web服务器,使用c写的性能高,上线要使用uwsgi

# 安装步骤
1)进入python3.8下安装
>: cd /usr/local/python38/
>: pip3.8 install uwsgi
#安装到了python38的安装路径下的bin路径里了

2)建立软连接
>: ln -s /usr/local/python38/bin/uwsgi /usr/bin/uwsgi
(5)安装虚拟环境

可能会在云服务器上跑很多django,这个时候就要安装虚拟环境

1)安装依赖
>: pip3.8 install virtualenv
>: pip3.8 install virtualenvwrapper
    # 可换源装(建议用下面的):pip3.8 install -U virtualenvwrapper -i https://pypi.douban.com/simple/ 
    
# 如果安装报错则执行:
# 1)python3.8 -m pip install --upgrade pip    # 升级pip版本
# 2)python3.8 -m pip install --upgrade setuptools #升级setuptools
# 3)pip3.8 install pbr   # 下载一个pbr
# 4)再重新安装一下pip3.8 install virtualenvwrapper


2)建立虚拟环境软连接
>: ln -s /usr/local/python38/bin/virtualenv /usr/bin/virtualenv

3)配置虚拟环境:加入下方内容
# ~/ 表示用户家路径:root用户,就是在/root/.bash_profile
>: vim ~/.bash_profile
# i:插入
VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3.8
source /usr/local/python38/bin/virtualenvwrapper.sh
# esc shift :wq   :保存退出

4)更新配置文件内容 ※
>: source ~/.bash_profile

5)虚拟环境默认存放位置:~/.virtualenvs

6)创建虚拟环境
>: mkvirtualenv -p python3.8 luffy
    
7)虚拟环境中安装django3.2.2
>: pip install django==3.2.2
    
8)退出虚拟环境
>: deactivate
(6)安装nginx(源码安装)

​ Nginx (engine x) 是一个高性能的HTTP反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点
​ 它运行在服务器上监听某个端口,可以向这个服务器发送http请求了。django可以并发100,Nginx可以上万。简单说它就是用来转发的一个软件

我们用它主要是:转发http请求代理静态文件负载均衡。想让项目并发量高,架构层面就是部署Nginx

# 安装nginx
1)前往用户根目录
>: cd ~

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

3)解压安装包
>: tar -xf nginx-1.13.7.tar.gz

4)进入目标文件
>: cd nginx-1.13.7

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

6)编译并安装
>: make && sudo make install

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

8)删除安装包与文件:
>: cd ~
>: rm -rf nginx-1.13.7
>: rm -rf nginx-1.13.7.tar.xz
或直接 rm -rf *

9)测试Nginx环境,服务器运行nginx,本地访问服务器ip
>: nginx
# 不要忘记去服务器安全组配置端口和ip 否则会出现下面问题

额外补充

1.浏览器访问服务器地址进不去nginx页面

image

解决:进入阿里云找到实例>>配置规则>>添加安全组规则>>配置一个80端口

image

2.当前页面是显示的哪里的?

image

这个index.html就是我们浏览器访问地址显示的页面,nginx最基础的功能就是代理静态文件
nginx把这个html目录代理了,以后访问该地址+/文件名 即可访问对应路径的文件。如:服务器ip/s1.html (前提是要有该文件)

修改里面index.html内容 页面也会发生变化

>: vim index.html

image

image

4)路飞前端部署

要把vue的项目编译成纯碎的静态文件

第一步:先把ajax远程连接的地址改成服务器的地址

luffy_city>>src>>assets>>js>>settings.js

export default {
    BASE_URL:'http://47.103.13.81/:8080/api/v1'
}

nginx会监听80、8080端口,当收到8080端口请求后会转发给运行django的uwsgi上

第二步:前端命令中>>编译前端vue

# 项目路径下:
npm run build

编译后项目路径下会生成一个dist文件夹,项目被编译过后所有的静态资源都会存储在这里

image

第三步:把编译后产生的dist文件夹压缩成.zip的压缩包传到云服务器中并解压

# 在云服务器上安装上传下载的软件:lrzsz
>: yum install -y lrzsz
>: rz       # 选择刚刚压缩的.zip压缩包上传
    
    
# 在云服务器上安装解压上传的软件:unzip
>: yum install unzip -y
>: unzip dist.zip

image

第四步:修改nginx配置文件,实现代理路飞前端

# 进入nginx安装目录的conf路径下
>: cd /usr/local/nginx/conf/

# 修改nginx的配置文件(直接修改不太好)
# >: vim nginx.conf 
# 我们采取把原配置文件重命名,再新建配置文件粘贴下面配置
>: mv nginx.conf nginx.conf.bak  # 把原nginx.conf重命名
>: vim nginx.conf  # 新建一个空白的nginx.conf并写入下方配置
# 注释记得删掉

events {
    worker_connections  1024;
    }
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        server {
            listen 80;    # 监听80端口
            server_name  127.0.0.1;   #项目上线时改为自己的域名,没域名则临时修改为127.0.0.1
            charset utf-8;
            location / {
                # 【访问80端口会进入该路径下拿下面的index.html文件】
                root /home/html;   # html访问路径
                index index.html;   # html文件名称
                try_files $uri $uri/ /index.html;   # 解决单页面应用刷新404问题
            }
        }
    } 

第五步:把dist下的所有内容,cp到 /home/html # 注意路径

>: cd ~
>: ls
# 有刚刚解压的dist文件
>: cp -r dist /home/html  # 把dist文件下的内容复制到home>>htiml文件下

image

第六步:重新加载nginx配置文件

>: nginx -s reload 

访问 http://47.103.13.81/ 前端页面部署成功,由于后端还没部署所以接口有问题

image

5)路飞后端部署

(1)往gitee上提交本地代码

第一步:先把后端dev.py中的配置复制到prod.py中然后在prod.py中修改个别配置

luffy_api>>settings>>prod.py

# 把配置文件修改
    DEBUG = False
    ALLOWED_HOSTS = ['*']  # 任意地址都可以访问
    
    # 后端地址
    BACKEND_URL = 'http://47.103.13.81/:8080'
    # 前端地址
    LUFFY_URL = 'http://47.103.13.81'
    # 再检查一下数据库mysql 和 redis 地址是不是127.0.0.1

第二步:把根路径的manage.py复制一份并起名为manage_pro.py并修改一个配置

image

今后在本地运行用manage.py
线上迁移数据库用manage_pro.py

第三步:生成项目依赖

>: pip freeze > requirements.txt  # 把当前环境所有依赖放在新建的文件中

image

第四步:提交到git

>: git add .    # 当前路径下所有变更的都提交到暂存区
>: git commit -m 'v1版本上线'    # 把暂存区所有内容提交到版本库
>: git push origin master  # 把本地当前所在分支推送到origin远程仓库的master主分支上

执行时可能会报错:

image

我们应先把远程仓库拉最新的数据下来

>: git pull origin master  # 从远程仓库拉最新的数据下来
# 此时可能会有冲突,删除阅读箭头中的代码 删除箭头和不需要的代码 然后重新执行上面的提交命令即可

image

image

第五步:部署服务器

# 从根目录中进入home文件中
>: cd /home/
# 创建project文件
>: mkdir project
# 进入project文件中
>: cd /home/project
# 把gitee仓库中的项目克隆下来(HTTPS)
>: git clone https://gitee.com/oreoxx/luffy_api.git

image

今后cd /home/project/luffy_api/就进入了luffy_api项目中了

(2)配置luffy数据库
1)管理员连接数据库
>: mysql -uroot -p
    # root   Zy123456?

2)创建luffy数据库
>: create database luffy default charset=utf8;

3)设置权限账号密码:账号密码要与项目中配置的一致
>: grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
>: grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123?';
# 刷新权限
>: flush privileges;

4)退出mysql
>: quit;
    
# 账号:luffy    密码:Luffy123?
#由于以前密码不带问号,这里要求密码一样但是创建密码不带问号说不可以,只好把项目中的luffy用户删掉重新创建了一个,导致修改了很多地方!!!


本地使用Navicate连接服务器mysql中的lufyy

image

(3)安装项目依赖,迁移数据库,

第一步:在远端进入luffy_api根路径下 进入虚拟环境

>: cd /home/project/luffy_api/
    
# 进入虚拟环境中
>: workon luffy

第二步:安装项目依赖

>: pip install -r requirements.txt

这个时候会发现有的可能安装不上mysqlclientpip list可查看安装的依赖

# 如果是mysqlclient装不上,就先把mysqlclient注释,把能安装上的先安装
>: pip install -r requirements.txt

image

# 现在再安装mysqlclient(如果中途某个出错 就再执行一次)
>: yum install mysql-devel -y
>: yum install python-devel -y
>: rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022
>: pip install mysqlclient

image

第三步:在虚拟环境中也要安装uwsgi

>: pip install uwsgi

第五步:迁移数据库

线上用的manage_pro.py

# 进入项目根路径下执行
>: python manage_pro.py makemigrations
>: python manage_pro.py migrate
#扩展知识:迁移的记录文件,要不要提交到git上
-官方推荐:提交
-我推荐:如果多人开发,不提交。容易出错乱

此时刷新一下表即可刷新各个表,但是没有数据需要自己录入

image

录入数据:我们由于是测试,可以把本地的sql导出,导入到远端。工作中不要这样做!!!应该手动录

本地路飞项目>>luffy  转储成SQL文件>>结构和数据>>保存到桌面
远程路飞项目>>luffy  运行SQL文件>>开始>>刷新表就可以发现数据和本地的一样了
(4)使用uwsgi启动django
# 运行django(个人经验:先用wsgiref运行,能起来再用uwsgi运行)
    -原来测试阶段,使用wsgiref运行
            >: python manage_pro.py runserver 0.0.0.0:8888
            浏览器输入:47.103.13.81:8888 发现访问不到
            原因是:安全组没开启 8888和8080端口,测试阶段可以把安全组打开允许全部端口
            '但是后期:只留【22,3306,6379,80,8080】端口,其他不开'
    -上线使用uwsgi

第一步:写一个uwsgi的配置文件(.ini或.xml),这里我们用.xml

# 在项目根路径下创建配置文件
>: vim luffyapi.xml
    
# 里面粘贴代码(注释删掉)
    <uwsgi>    
       <socket>127.0.0.1:8888</socket> <!-- 内部端口,自定义 --> 
       <chdir>/home/project/luffy_api/</chdir> <!-- 项目路径 -->            
       <module>luffy_api.wsgi</module>  <!-- luffyapi为wsgi.py所在目录名--> 
       <processes>4</processes> <!-- 进程数 -->     
       <daemonize>uwsgi.log</daemonize> <!-- 日志文件 -->
    </uwsgi>

第二步:使用uwsgi启动djagno

>: uwsgi -x ./luffyapi.xml
>: ps aux |grep uwsgi  # 检查一下是否有四个进程uwsgi,有说明启动成功

image

第三步:修改nginx配置文件

此时访问端口8888还是没有响应

原因:uWSGI配置文件是socket,说明它只能监听uwsgi协议浏览器发出去的是http协议,它不能响应,只有http请求发送到nginx上,再让nginx把http请求转发到uwsgi上才可以。【nginx支持把http协议转成uwsgi】

所以要修改nginx配置文件,完成对http请求的转发

# 进入ngins配置文件所在路径上修改
>: cd /usr/local/nginx/conf/
>: vim nginx.conf
    
# 配置文件下再加下列代码完成nginx的转发
    server {
        listen 8080;
        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/;
        }
    }

image

# 重启nginx 
>: nginx -s reload

image

6)路飞后台管理样式处理

后台地址访问地址:http://47.103.13.81:8080/admin 发现没有任何样式

image

出错原因:

测试阶段:'wgisref'既能访问静态资源,又能访问动态
上线阶段:'uwsgi'为了提高性能,只处理动态请求,不管静态【动静分离】
                 # 静态资源需要使用【nginx】代理
处理:收集静态资源
1.simpleui的静态资源
2.drf的静态资源
3.前后端混合项目,项目的静态资源

第一步:远端创建文件夹,用来存放收集的静态资源

# 在小luffy_api下创建静态资源文件夹
>: mkdir /home/project/luffy_api/luffy_api/static

第二步:修改配置文件 prod.py

# 进入settings文件夹下修改prod.py配置文件
>: cd /home/project/luffy_api/luffy_api/settings/
>: vim prod.py
——————————————————————
STATIC_URL = '/static/'  # 我们已经配置这句了可以不复制
STATIC_ROOT = '/home/project/luffy_api/luffy_api/static'  # 以后python manag_pro.py runserver  0.0.0.0:8888就运行不了了

image

第三步:执行收集静态文件命令

# 进入项目根路径下
>: cd /home/project/luffy_api/
# 把静态文件收集到STATIC_ROOT对应的文件夹下
>: python manage_pro.py collectstatic

image

第四步:修改nginx配置文件

    events {
    worker_connections  1024;
    }
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        server {
            listen 80;
            server_name  127.0.0.1; # 改为自己的域名,没域名修改为127.0.0.1:80
            charset utf-8;
            location / {
                root /home/html; # html访问路径
                index index.html; # html文件名称
                try_files $uri $uri/ /index.html; # 解决单页面应用刷新404问题
            }
        }
        server {
            listen 8000;
            server_name  127.0.0.1; # 改为自己的域名,没域名修改为127.0.0.1:80
            charset utf-8;
            location / {
               include uwsgi_params;
               uwsgi_pass 127.0.0.1:8808;  # 端口要和uwsgi里配置的一样
               uwsgi_param UWSGI_SCRIPT luffyapi.wsgi;  #wsgi.py所在的目录名+.wsgi
               uwsgi_param UWSGI_CHDIR /home/project/luffyapi/; # 项目路径
            }
            # 新增的配置静态文件
            location /static {
                alias /home/project/luffy_api/luffy_api/static;
            }
        }
    }

第五步:重启nginx

>: nginx -s reload

image

 # 访问80端口是前端 http://47.103.13.81

#  访问8080是后端 http://47.103.13.81:8080/admin

7)域名问题

我们现在访问的都是ip地址,一般都是访问的域名如:www.baidu.com

# 域名转发
    购买一个域名>>备案>>域名解析配置(www.>>地址改成服务器的地址 以后就可以用域名访问服务器)
    
# 以后访问购买的域名即可访问对应ip地址的网址

image

posted @ 2023-02-26 15:31  oreox  阅读(84)  评论(0编辑  收藏  举报