1. 程式人生 > 實用技巧 >Django---django-rest-framework(drf)-luffycity projects

Django---django-rest-framework(drf)-luffycity projects

目錄

http://liuqingzheng.top/python/Python%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8B/

pip換源

永久配置安裝源

windows:

檔案管理器敲%APPDATA%,進去之後新建pip資料夾,建立pip.ini配置檔案,檔案內容:

[global]
index-url = http://pypi.douban.com/simple
[install]
use-mirrors =true
mirrors =http://pypi.douban.com/simple/
trusted-host =pypi.douban.com

虛擬環境搭建

#優點
1 使用不同應用開發環境相互獨立
2 環境升級不影響其他應用、不會衝突全域性的python環境
3 防止出現包管理混亂及版本衝突
windown:
#建議使用pip3安裝到python環境下
pip3 install virtualenv
pip3 install virtualenvwrapper-win
#建立資料夾D:\Virtualenvs
配置虛擬環境管理器工作目錄
#配置環境變數
# 控制面板 => 系統和安全 => 系統 => 高階系統設定 => 環境變數 => 系統變數 => 點選新建 => 填入變數名與值
變數名:WORKON_HOME  變數值:自定義存放虛擬環境的絕對路徑
eg: WORKON_HOME: D:\Virtualenvs
#同步配置資訊
# 去向Python3的安裝目錄 => Scripts資料夾 => virtualenvwrapper.bat => 雙擊
使用
#終端命令
1 建立虛擬環境到配置的WORKON_HOME路徑下
	mkvirtualenv -p python3 自定義的虛擬環境名稱(比如:luffy)	使用的是python3
	mkvirtualenv -p python2 自定義的虛擬環境名稱(比如:luffy)	使用的是python2
2 檢視已有的虛擬環境
	workon
3 使用虛擬環境
	workon 已有的虛擬環境名稱(比如:luffy)
4 進入、退出該虛擬環境的Python環境
	python、exit()
5 為虛擬環境安裝模組
	pip install 模組名稱(比如:django==2.2)
6 退出當前虛擬環境
	deactivate
7 刪除虛擬環境(刪除當前虛擬環境要先退出)
	rmvirtualenv
PyCharm的使用(使用虛擬環境)(我們使用這種方式,不建app)

新建專案

使用虛擬環境

環境使用

luffy後臺搭建

環境
#給虛擬環境luffy安裝依賴環境
    pip install django==2.2
	pip install djangorestframework
	pip install pymysql
建立專案(我們前面已經用PyCharm建立好了)
在目標目錄下建立luffy資料夾
cd 進入luffy專案
django-admin startproject luffyapi
用PyCharm開啟專案,使用已經建好的虛擬環境
重構專案目錄
luffyapi
	-logs	#專案執行時/開發時的日誌目錄
	-manage.py	#指令碼檔案
	-scripts	#儲存專案運營時的指令碼檔案
	-luffyapi	#專案的主應用,開發時的程式碼儲存
		-utils(包帶__init__.py)公共函式庫
		-urls.py	#總路由
		-apps(包帶__init__.py)	#子應用儲存目錄
		-libs	#第三方類庫的儲存目錄
		-settings(包帶__init__.py)	#配置目錄
			-dev.py	#專案開發時的本地配置
			-prod.py	#專案上線時的執行配置
建立doc資料夾、建立readme.md檔案
終端下建立兩個應用到apps
cd 到apps目錄下
python ../../manage.py startapp home
python ../../manage.py startapp user
因為manage.py在根目錄,所以需要../../到根目錄去用該檔案
配置開發環境
1 把settings.py內容複製到dev.py,原來的settings.py刪除
2 #修改下面兩檔案
# manage.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE','luffyapi.settings.dev')
# wsgi.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE','luffyapi.settings.dev')#本地使用.dev,後面修改為.prod即可
3 修改dev.py檔案內容(錯誤資訊改成中文)
	LANGUAGE_CODE = 'zh-hans'
    TIME_ZONE = 'Asia/Shanghai'
    USE_TZ = False
4 修改啟動配置
	看下圖
5 把apps做成Sources Root看圖2

資料庫配置

建立專案依賴資料庫,luffyapi
	create database luffyapi;
建立資料庫使用者,授予使用luffyapi資料庫的權利	
	grant all privileges on luffyapi.* to'luffyapi'@'localhost' identified by '123';
	---上面命令指:建立使用者luffyapi授予使用luffyapi資料庫使用權,使用者密碼是123
重新整理許可權
    flush privileges;
#配置檔案配置
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',
        'USER': 'luffy',
        'PASSWORD': 'Luffy123',
        'HOST': 'localhost',
        'PORT': 3306
    }
}
import pymysql
pymysql.install_as_MySQLdb()

django2.x版本可能會出現一些pymysql相容問題,按報錯註釋、修改原始碼即可

User表配置

匯入from django.contrib.auth.models import AbstractUser
繼承user之前不要資料遷移命令,會出錯,需要:
		-把所有app下的遷移檔案,全刪除
    	-admin,auth app下的遷移檔案刪除
   	    -刪庫(資料一定要匯出來),重新遷移  (自己嘗試一下)
繼承AbstractUser,增加兩個欄位,mobilephone、icon,
	    mobilephone = models.CharField(max_length=11,unique=True)
    	icon = models.ImageField(upload_to='icon',default='media/default.png')
在settings.py中配置
	MEDIA_URL='/media/'
	MEDIA_ROOT=os.path.join(BASE_DIR,'media')
	#此時的BASE_DIR是luffyapi下的luffyapi
路由開放media資料夾,訪問頭像,需要匯入
    from django.views.static import serve
    from django.conf import settings
    
	re_path('media/(?P<path>.*)', serve{'document_root':settings.MEDIA_ROOT}),	

luffy前臺搭建

1 安裝node.js,官網下載,一路下一步
	檢視版本node -v
2 安裝模組,npm比較慢,用淘寶的cnpm來替換npm
	npm install -g cnpm --registry=https://registry.npm.taobao.org   
3 建立vue的工程,需要一個vue腳手架
	cnpm install -g @vue/cli
4 如果有問題執行如下,重走2、3步
	npm cache clean --force
5 建立vue專案
	vue create 自定義專案名(我們用luffycity)
	選Manually
6 用pycharm開啟,配置快鍵啟動
	看下圖

目錄介紹

public
    -favicon.ico   # 
    -index.html    #整個專案的單頁面
src
    -assets  #靜態檔案,js,css,img
    -components # 小元件,頭部元件,尾部元件
    -router     # 路由相關
    -store      # vuex相關,狀態管理器,臨時儲存資料的地方
    -views      # 頁面元件
    -App.vue    # 根元件
    -main.js    # 配置檔案(跟django的setting一樣
#任何一個元件都有三部分,裡面書寫相關程式碼
	<template>
    	#html相關
	</template>
    <style>
		# css相關
	</style>

    <script>
		# js相關
    </script>

luffy後臺配置

封裝Response物件、異常處理

#封裝Response物件
	#luffyapi\utils\response.py
		from rest_framework.response import Response
        class APIResponse(Response):
            def __init__(self, code=100, msg='成功', data=None, status=None, headerrs=None, **kwargs):
                dic = {'code': code, msg: msg}
                if data:
                    dic = dic['data'] = data
                dic.update(kwargs)
                super().__init__(data=dic, status=status, headers=headerrs)
    #檢視方法中使用       
    	return APIResponse(data={"name":'lqz'},token='dsafsdfa',aa='dsafdsafasfdee')
#封裝異常處理
	#luffyapi\utils\exceptions.py
        from rest_framework.views import exception_handler
        from .response import APIResponse
        from .logger import log
        def common_exception_handler(exc, context):
            log.error('view是:%s ,錯誤是%s'%(context['view'].__class__.__name__,str(exc)))
            #context['view']  是TextView的物件,想拿出這個物件對應的類名
            print(context['view'].__class__.__name__)
            ret=exception_handler(exc, context)  # 是Response物件,它內部有個data

            if not ret:  #drf內建處理不了,丟給django 的,我們自己來處理
                # 好多邏輯,更具體的捕獲異常
                if isinstance(exc,KeyError):
                    return APIResponse(code=0, msg='key error')

                return APIResponse(code=0,msg='error',result=str(exc))
            else:
                return APIResponse(code=0,msg='error',result=ret.data)
                

封裝日誌

#luffyapi\utils\logger.py
	import logging
    log=logging.getLogger('django')
#luffyapi\settings\dev.py中配置
	#日誌配置
    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
                'level': 'DEBUG',
                'filters': ['require_debug_true'],
                'class': 'logging.StreamHandler',
                'formatter': 'simple'
            },
            'file': {
                # 實際開發建議使用ERROR
                'level': 'INFO',
                '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': 100,
                # 日誌格式:詳細格式
                'formatter': 'verbose',
                # 檔案內容編碼
                'encoding': 'utf-8'
            },
        },
        # 日誌物件
        'loggers': {
            'django': {
                'handlers': ['console', 'file'],
                'propagate': True, # 是否讓日誌資訊繼續冒泡給其他的日誌處理系統
            },
        }
    }
# 以後使用直接匯入
	from luffyapi.utils.logger import log

解決跨域問題(xss、cors、csrf)

#XSS指跨站指令碼攻擊,CORS指跨域資源共享、CSRF指跨站請求偽造
1 同源策略指協議,域名,埠一致為同源
2 CORS跨域資源共享,允許不同的域來自己的伺服器拿資料
3 CORS請求分簡單請求(simple request),非簡單請求(not-so-simple request)
	#簡單請求是指滿足以下兩大條件就是簡單請求
		1 三種請求方法之一
			HEAD
			GET
			POST
		2 HTTP的頭資訊不超出以下幾種欄位
			Accept
            Accept-Language
            Content-Language
            Last-Event-ID
            Content-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain#只能是這三種類型,否則就算不滿足該條件
	#非簡單請求是指post請求,資料格式是json, 非簡單請求需要發兩次,OPTIONS請求,一次POST請求
4 自己在後端解決跨域問題,開啟CORS,跨域資源共享(寫中介軟體)
	#luffyapi\utils\middle.py
        from django.utils.deprecation import MiddlewareMixin
        class MyMiddle(MiddlewareMixin):
            def process_response(self, request, response):
                response['Access-Control-Allow-Origin'] = '*'
                if request.method == "OPTIONS":
                    # 可以加*
                    response["Access-Control-Allow-Headers"] = "Content-Type"
                    response["Access-Control-Allow-Headers"] = "authorization"
                return response
	#dev.py配置
        MIDDLEWARE = [
        #自定義的跨域資源共享
        'luffyapi.utils.middle.MyMiddle',]
5 使用第三方的django-cors-headers
	#安裝pip install django-core-headers
	#配置註冊app 'corsheaders'
	#配置中介軟體
		MIDDLEWARE = [
            #第三方的解決跨域資源共享
            'corsheaders.middleware.CorsMiddleware',]
	#配置其他引數
        CORS_ORIGIN_ALLOW_ALL = True
        CORS_ALLOW_METHODS = (
            'DELETE',
            'GET',
            'OPTIONS',
            'PATCH',
            'POST',
            'PUT',
            'VIEW',
        )
        CORS_ALLOW_HEADERS = (
            'authorization',
            'content-type',
        )

前後臺打通

1 前臺傳送jQuery封裝的ajax請求,或通過promise封裝axios請求
2 安裝axios,cnpm install axios
3 配置在VUE前端的main.js
	#main.js
		import Vue from 'vue'
        import App from './App.vue'
        import router from './router'
        import store from './store'

        Vue.config.productionTip = false

        import axios from 'axios'

        Vue.prototype.$axios = axios


        new Vue({
          router,
          store,
          render: h => h(App)
        }).$mount('#app')
	#Home.Vue
    	<template>
        <div class="home">
            <h1>主頁</h1>
        </div>
    	</template>
    	<script>

        export default {
            name: 'Home',
            created() {
                this.$axios.get('http://127.0.0.1:8000/home/home/').then(response => {
                    console.log(response.data)  //response.data才是真正後臺返回的資料
                }).catch(errors => {
                    console.log(errors)
                })

            },
            components: {}
        }
    	</script>
#瞭解es6的箭頭函式
    function (response) { console.log(response)}
    response=>{ console.log(response)}    	

xadmin的使用

1 安裝pip install https://codeload.github.com/sshwsfc/xadmin/zip/django2
2 app中註冊
	#luffyapi\settings\dev.py
        INSTALLED_APPS = [
        # xadmin主體模組
        'xadmin',
        # 渲染表格模組
        'crispy_forms',
        # 為模型通過版本控制,可以回滾資料
        'reversion',]
        
3 資料庫遷移
	python manage.py makemigrations
	python manage.py migrate
4 主路由替換admin
	#urls.py
        # xadmin的依賴
        import xadmin
        xadmin.autodiscover()
        # xversion模組自動註冊需要版本控制的 Model
        from xadmin.plugins import xversion
        xversion.register_models()
        urlpatterns = [
            # path('admin/', admin.site.urls),
            path('xadmin/', xadmin.site.urls),
        ]
5 建立超級使用者,PyCharm終端執行
	python manage.py createsuperuser

luffy前臺元件搭建

頭部元件

#在components新建Head.vue
    <template>
        <div class="header">
            <div class="slogan">
                <p>老男孩IT教育 | 幫助有志向的年輕人通過努力學習獲得體面的工作和生活</p>
            </div>
            <div class="nav">
                <ul class="left-part">
                    <li class="logo">
                        <router-link to="/">
                            <img src="../assets/img/head-logo.svg" alt="">
                        </router-link>
                    </li>
                    <li class="ele">
                        <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免費課</span>
                    </li>
                    <li class="ele">
                        <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">實戰課</span>
                    </li>
                    <li class="ele">
                        <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">輕課</span>
                    </li>
                </ul>

                <div class="right-part">
                    <div>
                        <span>登入</span>
                        <span class="line">|</span>
                        <span>註冊</span>
                    </div>
                </div>
            </div>
        </div>

    </template>

    <script>

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

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

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

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

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

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

        }

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

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

        .nav ul li {
            float: left;
        }

        .logo {
            margin-right: 20px;
        }

        .ele {
            margin: 0 20px;
        }

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

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

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

        .right-part {
            float: right;
        }

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

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

配置全域性css和setting

#在main.js中配置
    // 配置全域性樣式 @ 符號,代指src路徑
    import '@/assets/css/global.css'
    // 配置全域性自定義設定
    import settings from '@/assets/js/settings'
    Vue.prototype.$settings = settings;
    // 在所有需要與後臺互動的元件中:this.$settings.base_url + '再拼接具體後臺路由'

# 在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; /* 合併邊框 */
    }


    # 在assets下的js中加入settings.js
    export default {
        base_url: 'http://127.0.0.1:8000'
    }

前臺配置

#安裝
    cnpm install axios
    cnpm install vue-cookies
    cnpm install element-ui
    cnpm install jquery
    cnpm install bootstrap@3
#在main.js中配置
    //axios配置
    import axios from 'axios'
    Vue.prototype.$axios = axios;
    // vue-cookies配置
    import cookies from 'vue-cookies'
    Vue.prototype.$cookies = cookies;
    // ElementUI的配置
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    Vue.use(ElementUI);
    // bootstrap配置
    import 'bootstrap'
    import 'bootstrap/dist/css/bootstrap.min.css'
#jquery配置,專案根目錄下建立vue.config.js
    const webpack = require("webpack");
    module.exports = {
        configureWebpack: {
            plugins: [
                new webpack.ProvidePlugin({
                    $: "jquery",
                    jQuery: "jquery",
                    "window.jQuery": "jquery",
                    "window.$": "jquery",
                    Popper: ["popper.js", "default"]
                })
            ]
        }
    };

luffy尾部元件

#footer.vue
	<template>
        <div class="footer">
            <ul>
                <li>關於我們</li>
                <li>聯絡我們</li>
                <li>商務合作</li>
                <li>幫助中心</li>
                <li>意見反饋</li>
                <li>新手指南</li>
            </ul>
            <p>Copyright © luffycity.com版權所有 | 京ICP備17072161號-1</p>
        </div>
    </template>

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

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

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

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

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

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

輪播圖介面

#urls.py
####1  urls.py 
from rest_framework.routers import  SimpleRouter
router=SimpleRouter()
router.register('banner',views.BannerView,'banner')
urlpatterns = [
    # path('banner/', views.BannerView.as_view()),
    path('', include(router.urls)),
]

####2 views.py
from django.conf import settings
class BannerView(GenericViewSet,ListModelMixin):
    # 無論有多少條待展示的資料,最多就展示3條
    queryset = models.Banner.objects.filter(is_delete=False,is_show=True).order_by('display_order')[:settings.BANNER_COUNTER]
    serializer_class = serializer.BannerModelSerilaizer
    
### 3 serializer.py
from rest_framework import serializers
from . import models
class BannerModelSerilaizer(serializers.ModelSerializer):
    class Meta:
        model=models.Banner
        fields=['name','link','img']
        
### 4 models.py
from utils.models import BaseModel


class Banner(BaseModel):
    name=models.CharField(max_length=32,verbose_name='圖片名字')
    img=models.ImageField(upload_to='banner',verbose_name='輪播圖',help_text='圖片尺寸必須是:3840*800',null=True)
    link=models.CharField(max_length=32,verbose_name='跳轉連線')
    info=models.TextField(verbose_name='圖片簡介')
    # type=models.IntegerField(choices=)

    def __str__(self):
        return self.name
####5 utils/models.py

class BaseModel(models.Model):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
    update_time = models.DateTimeField(auto_now=True, verbose_name='最後更新時間')
    is_delete = models.BooleanField(default=False, verbose_name='是否刪除')
    is_show = models.BooleanField(default=True, verbose_name='是否展示')
    display_order = models.IntegerField()
    class Meta:
        abstract=True  # 一定不要忘了
        
### 6 settings/const.py
# 首頁輪播圖個數
BANNER_COUNTER=3

#### 7 settings/dev.py
from .const import *

輪播圖元件

#Banner.vue
    <template>

        <div id="banner">

            <el-carousel height="400px">
                <el-carousel-item v-for="item in banner_list">
                    <!--<img src="../assets/img/banner1.png" alt="">-->
                    <router-link :to="item.link">
                        <img :src="item.img" :alt="item.name">
                    </router-link>

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

    </template>

    <script>
        export default {
            name: "Banner",
            // data:function(){},
            data() {
                return {
                    banner_list: []
                }
            },
            created() {
                //當banner元件一建立,就向後臺發請求,拿回輪播圖資料
                this.$axios.get(this.$settings.base_url + '/home/banner/').then(response => {
                    console.log(response.data)
                    this.banner_list=response.data
                }).catch(error => {
                })
            },

        }
    </script>

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

        .el-carousel__item img {
            height: 400px;
            /*margin-left: 20px;*/
            /*margin-left: calc(50% - 1920px / 2);*/
        }
    </style>
#src\router\index.js
	#路由設定
	const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/free-course',
        name: 'FreeCourse',
        component: FreeCourse
    }, {
        path: '/light-course',
        name: 'LightCourse',
        component: LightCourse
    }, {
        path: '/actual-course',
        name: 'ActualCourse',
        component: ActualCourse
    },

]

Git入門

簡介
#生成.ssh命令(命令列)ssh-keygen -t rsa -C "[email protected]"
#檢視公鑰命令(命令列)cat ~/.ssh/id_rsa.pub
#git是版本控制器
	#完成協同專案開發,幫助程式設計師整合程式碼
	#合併程式碼衝突,會提示合併程式碼的開發者,讓其解決
	#git分散式管理,svn集中式管理
#git工作流程
	工作區-暫存區-版本區
#遠端倉庫github,gitee(國內),gitlab(公司內部)
#安裝,下載https://npm.taobao.org/mirrors/git-for-windows/v2.27.0.windows.1/一路下一步
#安裝完成,在需要管理檔案的目錄右鍵 git bash here進入命令列
git命令
git init 資料夾名 或 git init #表示當前路徑全部管理
git status #檢視檔案狀態,紅色的檔案表示未被管理,綠色表示在暫存區了
git add a.txt #新增a.txt到暫存區
git add . #添加當前所有檔案到暫存區
git commit -m '我新增了a.txt' #把暫存區所有的提交到本地版本區,必須要寫註釋
#需要新增作者資訊
	git config --global user.email "[email protected]"
	git config --global user.name "joab"
git log #檢視版本管理的日誌
git reflog #檢視日誌,條數更少,內容更多
#忽略檔案
	1 空資料夾不會被管理
	2 指定檔案或資料夾不被管理,在根目錄建一個.gitignore.檔案,注意前後都有一個點,檔案內寫忽略檔案,
		a.txt	#忽略a.txt
		 /dist	#忽略根路徑下的dist資料夾,不被管理
        *.py   #表示忽略字尾名為py的檔案,都被忽略,不止當前路徑,子目錄也生效
        *.log*	#表示忽略檔名中間為.log的所有檔案,不止當前路徑,子目錄也生效
#分支操作
	git branch 分支名(如dev)	#建立分支
	git checkout dev	#切換分支
	git checkout -b dev		#建立並切換到dev
	git branch -d dev	#刪除分支dev
	git merge 分支名	#合併分支,需要先切換到主分支master,再執行合併dev分支

Git遠端連線

1 連線Gitee,國內的比較快
2 Gitee新建倉庫,不要勾選Readme初始化倉庫
3 本地新建一個檔案,去連線Gitee
	mkdir test
	cd test
	git init	#管理當前目錄
	git add a.txt	#新增a.txt到暫存區
	git commit -m "提交了a.txt"
	git remote add origin 你的Gitee倉庫HTTPS地址,建立倉庫的時候能看到,看圖1(我的用https://gitee.com/lin_qiao_bao/test.git),或者是SSH地址也可以	
    git remote remove origin	刪除連線倉庫
	git push  origin master	#提交到Gitee倉庫,第一次提交需要Gitee的使用者名稱,密碼
#已有本地檔案
	cd 你的檔案路徑
	git remote add origin 你的Gitee倉庫HTTPS地址或SSH地址,建立倉庫的時候能看到,看圖1(我的用https://gitee.com/lin_qiao_bao/test.git)
	git push  origin master	#提交到Gitee倉庫,第一次提交需要Gitee的使用者名稱,密碼
4 git 遠端操作命令
	git remote	#檢視遠端倉庫,沒繫結就看不到
	git remote add origin 你的Gitee倉庫HTTPS地址,建立倉庫的時候能看到,看圖1(我的用https://gitee.com/lin_qiao_bao/test.git)	#跟遠端倉庫建立連線
	git push origin master	##提交到Gitee倉庫,第一次提交需要Gitee的使用者名稱,密碼

圖1

Gitee倉庫創始者和開發者

1 創始者指建立倉庫者
2 專案開發者,被創始者授權了的使用者,可以把程式碼拉下來開發,還有提交程式碼
	git clone 需要拉程式碼的Gitee倉庫HTTPS地址	#程式碼拉到本地,能夠看到完整的版本和日誌記錄,能恢復到任意版本
	git rset --hard 版本號		#恢復到指定版本號
	git checkout .		#表示回到指標指向的版本,因為git rest --hard把指標移動了,
#新增c.txt
	1 git add c.txt		#提交到暫存區
	2 git commit -m '提交了c.txt'	#提交到本地版本區
	3 git push origin master	#提交到gitee倉庫
3 重點,每次提交交代前,先拉去程式碼,再提交,不然有可能在你之前別人提交了過程式碼,會衝突

SSH連結和HTTPS連結

老劉的地址https://gitee.com/liuqingzheng/luffyapi.git
1 公司大部分用SSH連線
	-leader會給你一個git地址(專案倉庫地址)
	-你從倉庫拉程式碼下來
	-寫程式碼,提交到leader測試,通過就上線
	-ssh配置,不需要使用者名稱,密碼
2 ssh配置,採用非對稱加密
	生成一對一公鑰和私鑰看圖1	#公鑰在本地使用者名稱目錄下的.ssh下的id_rsa.pub,id_rsa私鑰
#跟遠端操作有關的命令
    git clone 需要拉程式碼的Gitee倉庫HTTPS地址	#拉gitee程式碼
    git push origin master	#提交到gitee倉庫

PyCharm操作Git

1 安裝Git
2 在PyCharm中配置,setting---git---git.exe
3 git clone 看圖3
4 建立本地分支,看圖4
5 拉取遠端倉庫程式碼,看圖5
6 提交程式碼到遠端倉庫,看圖6
7 原生代碼與倉庫比較,看圖7

圖3

圖4

圖5

圖6

圖7

協同開發

#協同開發,從倉庫拉取程式碼,提交程式碼可能會出現衝突
    <<<<<<< HEAD
    你的程式碼
    =======
    別人程式碼
#處理方案
	-刪除自己的程式碼
	-刪除衝突的程式碼
	-看情況優化程式碼

線上分支合併

#看到可自動合併,表示合併後不會衝突
#測試人員通過,直接合並即可

線上回滾

1 先檢視要恢復的版本git log 或 git reflog複製版本號,然後切換到master主分支
2 回滾到某個版本, git reset --hard 版本號
3 強制提交程式碼 git push origin master -f

vue登入介面

# Login.vue
    <template>
        <div class="login">
        <span @click="close_login">X</span>
        </div>
    </template>

    <script>
        export default {
            name: "Login",
            methods:{
                close_login(){
                    //子傳父元件 this.$emit,給父元件傳遞一個事件
                    this.$emit("close")
                },

            },

        }
    </script>

    <style scoped>
    .login{
        width: 100vw;
        height: 100vw;
        position: fixed;
        left: 0;
        top: 0;
        z-index: 666;
        background-color: rgba(0,0,0,0.3);
    }
        span{
            font-size: 30px;
            cursor: pointer;
        }
    </style>

    # Head.vue
    #template中
    <div class="right-part">
    <div>
    <span @click="pull_login">登入</span>
    <span class="line">|</span>
    <span>註冊</span>
    </div>
    </div>
    </div>

    <Login v-if="is_login" @close="close"/>
    <!--is_login是True就顯示,false就不顯示-->
    </div>
    # js中
    pull_login(){
        this.is_login=true
    },
    close(){
        this.is_login=false
    },

登入註冊模態框

#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">登入</el-button>
                    </el-form>
                    <el-form v-if="login_method === 'is_sms'">
                        <el-input
                                placeholder="手機號"
                                prefix-icon="el-icon-phone-outline"
                                v-model="mobile"
                                clearable
                                @blur="check_mobile">
                        </el-input>
                        <el-input
                                placeholder="驗證碼"
                                prefix-icon="el-icon-chat-line-round"
                                v-model="sms"
                                clearable>
                            <template slot="append">
                                <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                            </template>
                        </el-input>
                        <el-button type="primary">登入</el-button>
                    </el-form>
                    <div class="foot">
                        <span @click="go_register">立即註冊</span>
                    </div>
                </div>
            </div>
        </div>
    </template>

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

                    if (!this.is_send) return;
                    this.is_send = false;
                    let sms_interval_time = 60;
                    this.sms_interval = "傳送中...";
                    let timer = setInterval(() => {
                        if (sms_interval_time <= 1) {
                            clearInterval(timer);
                            this.sms_interval = "獲取驗證碼";
                            this.is_send = true; // 重新回覆點擊發送功能的條件
                        } else {
                            sms_interval_time -= 1;
                            this.sms_interval = `${sms_interval_time}秒後再發`;
                        }
                    }, 1000);
                }
            }
        }
    </script>

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

        .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>
#src\components\Register.vue
    <template>
        <div class="register">
            <div class="box">
                <i class="el-icon-close" @click="close_register"></i>
                <div class="content">
                    <div class="nav">
                        <span class="active">新使用者註冊</span>
                    </div>
                    <el-form>
                        <el-input
                                placeholder="手機號"
                                prefix-icon="el-icon-phone-outline"
                                v-model="mobile"
                                clearable
                                @blur="check_mobile">
                        </el-input>
                        <el-input
                                placeholder="密碼"
                                prefix-icon="el-icon-key"
                                v-model="password"
                                clearable
                                show-password>
                        </el-input>
                        <el-input
                                placeholder="驗證碼"
                                prefix-icon="el-icon-chat-line-round"
                                v-model="sms"
                                clearable>
                            <template slot="append">
                                <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                            </template>
                        </el-input>
                        <el-button type="primary">註冊</el-button>
                    </el-form>
                    <div class="foot">
                        <span @click="go_login">立即登入</span>
                    </div>
                </div>
            </div>
        </div>
    </template>

    <script>
        export default {
            name: "Register",
            data() {
                return {
                    mobile: '',
                    password: '',
                    sms: '',
                    sms_interval: '獲取驗證碼',
                    is_send: false,
                }
            },
            methods: {
                close_register() {
                    this.$emit('close', false)
                },
                go_login() {
                    this.$emit('go')
                },
                check_mobile() {
                    if (!this.mobile) return;
                    if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                        this.$message({
                            message: '手機號有誤',
                            type: 'warning',
                            duration: 1000,
                            onClose: () => {
                                this.mobile = '';
                            }
                        });
                        return false;
                    }
                    this.is_send = true;
                },
                send_sms() {
                    if (!this.is_send) return;
                    this.is_send = false;
                    let sms_interval_time = 60;
                    this.sms_interval = "傳送中...";
                    let timer = setInterval(() => {
                        if (sms_interval_time <= 1) {
                            clearInterval(timer);
                            this.sms_interval = "獲取驗證碼";
                            this.is_send = true; // 重新回覆點擊發送功能的條件
                        } else {
                            sms_interval_time -= 1;
                            this.sms_interval = `${sms_interval_time}秒後再發`;
                        }
                    }, 1000);
                }
            }
        }
    </script>

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

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

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

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

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

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

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

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

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

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

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

        .sms {
            color: orange;
            cursor: pointer;
            display: inline-block;
            width: 70px;
            text-align: center;
            user-select: none;
        }
    </style>
#src\components\Head.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 @click="logout">登出</span>
                    </div>
                </div>
                <Login v-if="is_login" @close="close_login" @go="put_register" @loginsuccess="login_success"/>
                <Register v-if="is_register" @close="close_register" @go="put_login"/>
            </div>
        </div>

    </template>

    <script>
        import Login from './Login'
        import Register from './Register'

        export default {
            name: "Header",
            data() {
                return {
                    url_path: sessionStorage.url_path || '/',
                    is_login: false,
                    is_register: false,
                    token: '',
                    username: ''
                }
            },
            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;
                },
                login_success(){
                    this.username=this.$cookies.get('username')
                    this.token=this.$cookies.get('token')
                },
                logout(){
                    //清除cookie
                    this.$cookies.remove('token')
                    this.$cookies.remove('username')
                    //把兩個變數值為空
                    this.username=''
                    this.token=''
                },
            },
            created() {
                sessionStorage.url_path = this.$route.path;
                this.url_path = this.$route.path;

                //當頁面一建立,我就去cookie中取token和username
                this.username = this.$cookies.get('username')   //取到就有值,取不到就為空
                this.token = this.$cookies.get('token')   //取到就有值,取不到就為空
            },
            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>

登入註冊介面分析

1 是多方式使用者登入介面
2 手機號登入介面
3 傳送驗證碼介面
4 註冊介面
5 校檢手機號是否存在介面

多種方式登入

#luffyapi\apps\user\views.py
    from luffyapi.utils.response import APIResponse
    from rest_framework.viewsets import ViewSet
    from . import serializer
    from rest_framework.decorators import action
    class LoginViewSet(ViewSet):

        @action(methods=['POST'],detail=False)
        def login(self,request,*args,**kwargs):
            user_ser = serializer.UserModelSerializer(data=request.data)
            if user_ser.is_valid():#自定義了全域性異常,不需要使用raise_exception=True
                #從序列化物件user_ser.context中獲取token和使用者名稱
                token = user_ser.context['token']
                username = user_ser.context['user'].username
                return APIResponse(token=token,username=username)
            else:
                return APIResponse(code=0,msg=user_ser.errors)#序列化校檢失敗,返回錯誤資訊
#luffyapi\apps\user\serializer.py
    #使用者序列化器
    from rest_framework import serializers
    from . import models
    from rest_framework.exceptions import ValidationError
    class UserModelSerializer(serializers.ModelSerializer):
        username = serializers.CharField()#username為unique欄位,需要重定義
        class Meta:
            model = models.User
            fields = ('username','password','id')
            extra_kwargs = {
                'id':{'read_only':True},
                'password':{'write_only':True}
            }

        #全域性鉤子,判斷多方式登入
        def validate(self, attrs):
            #_get_user去獲取使用者物件
            user = self._get_user(attrs)
            #去獲取簽發的token
            token = self._get_token(user)
            self.context['token'] = token
            self.context['user'] = user
            return attrs

        #獲取使用者名稱
        def _get_user(self,attrs):
            username = attrs.get('username')
            password = attrs.get('password')
            import re
            #判斷使用者名稱型別
            if re.match('^1[3-9][0-9]{9}$',username):#匹配usernmae是不是手機號型別
                user = models.User.objects.filter(mobilephone=username)
            elif re.match('^.*@.*$',username):
                user = models.User.objects.filter(email=username)
            else:
                user = models.User.objects.filter(username=username)
            if user:
                ret = models.User.check_password(password=password)
                if ret:
                    return True
                else:
                    raise ValidationError('密碼錯誤')
            else:
                raise ValidationError('使用者不存在')

        #獲取token
        def _get_token(self,user):
            from rest_framework_jwt.serializers import jwt_payload_handler,jwt_decode_handler
            payload = jwt_payload_handler(user)#加入user得到payload
            token = jwt_decode_handler(payload)#加入patload得到token
            return token
#luffyapi\apps\user\urls.py
    # 匯入路由物件
    from rest_framework.routers import SimpleRouter
    from django.urls import path, include

    from . import views

    # 例項化路由物件
    router = SimpleRouter()
    # 註冊路由
    router.register('', views.LoginViewSet,'login')#不要用as_view(),需要加上方向解析,不然不會報錯
    urlpatterns = [
        # 加入路由
        path('', include(router.urls))

    ]

Cookies修改頁面登入狀態、前臺登出

1 重點,Cookies操作
	this.$cookies.set('key',value,過期時間)	#設定cookies,還有過期時間
	this.$cookies.get('key')	#獲取cookies
	this.$cookies.remove(;key)	#登出cookies
2 #src\components\Login.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 @click="logout">登出</span>
                </div>
            </div>
            <Login v-if="is_login" @close="close_login" @go="put_register" @loginsuccess="login_success"/>
            <Register v-if="is_register" @close="close_register" @go="put_login"/>
        </div>
    </div>

</template>

<script>
    import Login from './Login'
    import Register from './Register'

    export default {
        name: "Header",
        data() {
            return {
                url_path: sessionStorage.url_path || '/',
                is_login: false,
                is_register: false,
                token: '',
                username: ''
            }
        },
        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;
            },
            login_success(){
                this.username=this.$cookies.get('username')
                this.token=this.$cookies.get('token')
            },
            logout(){
                //清除cookie
                this.$cookies.remove('token')
                this.$cookies.remove('username')
                //把兩個變數值為空
                this.username=''
                this.token=''
            },
        },
        created() {
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;

            //當頁面一建立,我就去cookie中取token和username
            this.username = this.$cookies.get('username')   //取到就有值,取不到就為空
            this.token = this.$cookies.get('token')   //取到就有值,取不到就為空
        },
        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>

手機號是否存在

#luffyapi\apps\user\views.py
    #多方式使用者登入
    from luffyapi.utils.response import APIResponse
    from rest_framework.viewsets import ViewSet
    from . import serializer
    from . import models
    from rest_framework.decorators import action
    class LoginViewSet(ViewSet,):

        @action(methods=['POST'],detail=False)
        def login(self,request,*args,**kwargs):
            user_ser = serializer.UserModelSerializer(data=request.data)
            if user_ser.is_valid():#自定義了全域性異常,不需要使用raise_exception=True
                #從序列化物件user_ser.context中獲取token和使用者名稱
                token = user_ser.context['token']
                username = user_ser.context['user'].username
                return APIResponse(token=token,username=username)
            else:
                return APIResponse(code=0,msg=user_ser.errors)#序列化校檢失敗,返回錯誤資訊
		#判斷手機號是否存在
        def check_mobilephone(self,request,*args,**kwargs):
            import re
            mobilephone = request.query_params.get('mobilephone')
            if not re.match('^[1][3-9][0-9]{9}$',mobilephone):
                return APIResponse(code=0,msg='手機號不合法')
            try:
                models.User.objects.get(mobilephone=mobilephone)
                return APIResponse(code=1)
            except:
                return APIResponse(code=0, msg='手機號不存在')

申請騰訊雲簡訊服務

1 必需申請一個公眾號地址https://mp.weixin.qq.com/
	1 進去之後點右上角立即註冊,型別訂閱號就行,其他型別需要域名比較複雜,然後按提示操作即可
2 到騰訊雲登入https://cloud.tencent.com/,第一次使用需要實名認證,然後搜尋簡訊,點選免費領取
3 建立簽名,建立正文模板,看圖1
4 建立應用,記住SDK AppID,App Key後面要用到

圖1

圖2

5 扣程式碼,用的是簡訊2.0 https://cloud.tencent.com/document/product/382/11672
6 可以康康幫助文件,看圖1 
	API指很多web介面,基於APIView來寫
    SDK指軟體開發工具包,別人基於api介面,用不同語言封裝的工具包,我們可以直接呼叫方法完成某種事,不同語言需要不同的SDK

實操騰訊雲簡訊介面

1 配置SDK pip install qcloudsms_py

2 #luffyapi\libs\tx_sms\settings.py

    # 簡訊應用 SDK AppID
    appid = 1400403539  # SDK AppID 以1400開頭
    # 簡訊應用 SDK AppKey
    appkey = "ca57467043d91ce7dfc090e9cd913acb"
    # 需要傳送簡訊的手機號碼
    phone_numbers = ["21212313123", "12345678902", "12345678903"]
    # 簡訊模板ID,需要在簡訊控制檯中申請
    template_id = 669037  # NOTE: 這裡的模板 ID`7839`只是示例,真實的模板 ID 需要在簡訊控制檯中申請
    # 簽名
    sms_sign = "Joab搞IT"  # NOTE: 簽名引數使用的是`簽名內容`,而不是`簽名ID`。這裡的簽名"騰訊雲"只是示例,真實的簽名需要在簡訊控制檯中申請
    
#luffyapi\libs\tx_sms\send.py

    from qcloudsms_py import SmsSingleSender
    from luffyapi.utils.logger import log
    from . import settings


    # 生成4位隨機驗證碼
    def get_code():
        import random
        str_code = ''
        for i in range(4):
            str_code = str(random.randint(0, 9))  # int需要轉成str
        return str_code


    # 傳送驗證碼
    def send_str_code(phone, str_code):
        sender = SmsSingleSender(settings.appid, settings.appkey)
        params = [str_code, '3']  # 3指簡訊過期時間3分鐘
        try:
            result = sender.send_with_param(86, phone, settings.template_id, params, sign=settings.sms_sign, extend="",
                                            ext="")
            if result.get('result') == 0:
                return True
            else:
                return False
        except Exception as e:
            log.error(f'手機號{phone}簡訊傳送失敗,錯誤為{str(e)}')
            
#luffyapi\libs\tx_sms\__init__.py

    from .send import get_code,send_str_code
    
#luffyapi\apps\user\views.py

    # 傳送簡訊介面
    @action(methods=['GET'],detail=False)
    def send(self,request,*args,**kwargs):
        """
        傳送驗證碼介面
        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        #先判斷是不是手機號
        import re
        from luffyapi.libs.tx_sms import get_code, send_str_code#因為tx_sms的init匯入了get_code,send_str_code所以可以不用.send直接導到這兩個方法
        from django.core.cache import cache#匯入django快取
        from django.conf import settings
        mobilephone = request.query_params.get('mobilephone')
        if not re.match('^1[3-9][0-9]{9}$',mobilephone):
            return APIResponse(code=0,msg='手機號不合法')#code=0表示不合法,code=1是0k
        code = get_code()
        result = send_str_code(mobilephone,code)
        #驗證碼存到django快取中
        cache.set(settings.PHONE_CACHE_KEY%mobilephone,code,180)#過期時間180秒,3分鐘
        if result:
            return APIResponse(code=1,msg='驗證碼傳送成功')
        else:
            return APIResponse(code=0,msg='驗證碼傳送失敗')

簡訊介面頻率限制

#luffyapi\utils\throttings.py

    from rest_framework.throttling import SimpleRateThrottle
    class SMSThrotting(SimpleRateThrottle):
        scope = 'sms'
        def get_cache_key(self, request, view):
            print('00')
            mobilephone = request.query_params.get('mobilephone')
            return self.cache_format%{'scope':self.scope,'ident':mobilephone}

#luffyapi\apps\user\views.py
    #簡訊頻率限制
    from luffyapi.utils.throttings import SMSThrotting
    class SendSmsView(ViewSet):
        throttle_classes = [SMSThrotting]
        # 傳送簡訊介面
        @action(methods=['GET'],detail=False)
        def send(self,request,*args,**kwargs):
            print('0')
            """
            傳送驗證碼介面
            :param request:
            :param args:
            :param kwargs:
            :return:
            """
            #先判斷是不是手機號
            import re
            from luffyapi.libs.tx_sms import get_code, send_str_code#因為tx_sms的init匯入了get_code,send_str_code所以可以不用.send直接導到這兩個方法
            from django.core.cache import cache#匯入django快取
            from django.conf import settings
            mobilephone = request.query_params.get('mobilephone')
            if not re.match('^1[3-9][0-9]{9}$',mobilephone):
                print('1')
                return APIResponse(code=0,msg='手機號不合法')#code=0表示不合法,code=1是0k
            code = get_code()
            result = send_str_code(mobilephone,code)
            #驗證碼存到django快取中
            cache.set(settings.PHONE_CACHE_KEY%mobilephone,code,180)#過期時間180秒,3分鐘
            print('2')
            if result:
                return APIResponse(code=1,msg='驗證碼傳送成功')
            else:
                return APIResponse(code=0,msg='驗證碼傳送失敗')
            
#luffyapi\settings\const.py

	# 手機驗證碼快取的key值
	PHONE_CACHE_KEY='sms_cache_%s'
    
#luffyapi\settings\dev.py配置簡訊傳送頻率的key

    REST_FRAMEWORK = {
        'EXCEPTION_HANDLER': 'luffyapi.utils.exceptions.common_exception_handler',
        # 簡訊傳送頻率限制的key
        'DEFAULT_THROTTLE_RATES': {'sms': '1/m'  }# key要跟類中的scop對應
    }

驗證碼登入介面

#luffyapi\apps\user\views.py,LoginViewSet檢視內

        #驗證碼登入介面
        @action(methods=['POST'],detail=False)
        def code_login(self,request,*args,**kwargs):
            ser = serializer.CodeUserSSerializer(data=request.data)
            if ser.is_valid():
                token = ser.context.get('token')
                username = ser.context.get('user').username
                return APIResponse(username=username,token=token)
            else:
                return APIResponse(code=0, msg=ser.errors)
                
#luffyapi\apps\user\serializer.py

	#驗證碼序列化器
    class CodeUserSSerializer(serializers.ModelSerializer):
        code = serializers.CharField()
        class Meta:
            model = models.User
            fields = ('mobilephone','code')

        #全域性鉤子,驗證碼登入成功簽發token
        def validate(self, attrs):
            #_get_user去獲取使用者物件
            user = self._get_user(attrs)
            #去獲取簽發的token
            token = self._get_token(user)
            self.context['token'] = token
            self.context['user'] = user
            return attrs

        #獲取使用者名稱
        def _get_user(self,attrs):
            from django.core.cache import cache
            from django.conf import settings
            import re
            mobilephone = attrs.get('mobilephone')
            code = attrs.get('code')
            #取出原來的code
            cache_code = cache.get(settings.PHONE_CACHE_KEY%mobilephone)
            if code == '0000':#萬能驗證碼#if code == cache_code:
                #驗證碼用過一次就更新為空
                cache.set(settings.PHONE_CACHE_KEY%mobilephone,'')
                user = models.User.objects.filter(mobilephone=mobilephone).first()
                #驗證成功返回使用者
                return user
            else:
                raise ValidationError('驗證碼錯誤,請重新輸入')

        #獲取token
        def _get_token(self,user):
            from rest_framework_jwt.serializers import jwt_payload_handler,jwt_decode_handler
            payload = jwt_payload_handler(user)#加入user得到payload
            token = jwt_decode_handler(payload)#加入patload得到token
            return token

前臺傳送驗證碼

#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_password">登入</el-button>
                    </el-form>
                    <el-form v-if="login_method === 'is_sms'">
                        <el-input
                                placeholder="手機號"
                                prefix-icon="el-icon-phone-outline"
                                v-model="mobile"
                                clearable
                                @blur="check_mobile">
                        </el-input>
                        <el-input
                                placeholder="驗證碼"
                                prefix-icon="el-icon-chat-line-round"
                                v-model="sms"
                                clearable>
                            <template slot="append">
                                <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                            </template>
                        </el-input>
                        <el-button type="primary" @click="code_login">登入</el-button>
                    </el-form>
                    <div class="foot">
                    </div>
                        <span @click="go_register">立即註冊</span>
                </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;
                    //字串.match(/正則表示式/)
                    if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                        this.$message({
                            message: '手機號有誤',
                            type: 'warning',
                            duration: 1000,
                            onClose: () => {
                                // this.mobile = '';
                            }
                        });
                        return false;
                    }
                    //傳送axios請求
                    // this.$axios.get(this.$settings.base_url+'/user/check_telephone/telephone='+this.mobile})
                    this.$axios.get(this.$settings.base_url + '/user/check_telephone/', {params: {telephone: this.mobile}}).then(response => {
                        if (response.data.code) {
                            //手機號存在,允許傳送驗證碼
                            this.is_send = true;
                        } else {
                            this.$message({
                                message: '手機號不存在',
                                type: 'warning',
                                duration: 1000,
                                onClose: () => {
                                    // this.mobile = '';
                                }
                            });
                        }
                    }).catch(error => {
                        console.log(error)
                    })

                },
                send_sms() {

                    if (!this.is_send) return;
                    this.is_send = false;
                    let sms_interval_time = 60;
                    this.sms_interval = "傳送中...";

                    this.$axios.get(this.$settings.base_url + '/user/send/', {params: {'telephone': this.mobile}})
                        .then(response => {
                            if (response.data.code) {
                                this.$message({
                                    message: '傳送驗證碼成功',
                                    type: 'success',
                                    duration: 1000,

                                });
                            }
                        })


                    // setInterval(()=>{},100)
                    //定時器:每隔一秒種,把數字減一,當減到小於1,按鈕又能點了,顯示獲取驗證碼
                    let timer = setInterval(() => {
                        if (sms_interval_time <= 1) {
                            clearInterval(timer);  //如果小於等於1,把定時器清除
                            this.sms_interval = "獲取驗證碼";
                            this.is_send = true; // 重新回覆點擊發送功能的條件
                        } else {
                            sms_interval_time -= 1;
                            this.sms_interval = `${sms_interval_time}秒後再發`;
                        }
                    }, 1000);
                },

                login_password() {
                    if (this.username && this.password) {
                        //傳送請求
                        this.$axios.post(this.$settings.base_url + '/user/login/', {
                            username: this.username,
                            password: this.password


                        }).then(response => {
                            console.log(response.data)
                            //把使用者資訊儲存到cookie中
                            // this.$cookies.set('key','value','過期時間,按s計')
                            this.$cookies.set('token', response.data.token, '7d')
                            this.$cookies.set('username', response.data.username, '7d')
                            //關閉登入視窗(子傳父)
                            this.$emit('close')
                            //給父元件,Head傳遞一個事件,讓它從cookie中取出token和username
                            this.$emit('loginsuccess')
                        }).catch(errors => {
                        })
                    } else {
                        this.$message({
                            message: '使用者名稱或密碼必須填哦',
                            type: 'warning',

                        });
                    }
                },
                code_login() {

                    if (this.mobile && this.sms) {
                        //傳送請求
                        this.$axios.post(this.$settings.base_url + '/user/code_login/', {
                            telephone: this.mobile,
                            code: this.sms

                        }).then(response => {
                            console.log(response.data)
                            //把使用者資訊儲存到cookie中
                            // this.$cookies.set('key','value','過期時間,按s計')
                            this.$cookies.set('token', response.data.token, '7d')
                            this.$cookies.set('username', response.data.username, '7d')
                            //關閉登入視窗(子傳父)
                            this.$emit('close')
                            //給父元件,Head傳遞一個事件,讓它從cookie中取出token和username
                            this.$emit('loginsuccess')
                        }).catch(errors => {
                        })
                    } else {
                        this.$message({
                            message: '手機號或驗證碼必填',
                            type: 'warning',

                        });
                    }

                },
            }
        }
    </script>

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

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

後臺註冊介面

# luffyapi\apps\user\urls.py

    # 匯入路由物件
    from rest_framework.routers import SimpleRouter
    from django.urls import path, include

    from . import views

    # 例項化路由物件
    router = SimpleRouter()
    # 註冊路由
    router.register('', views.LoginViewSet,'login')#不要用as_view(),需要加上反向解析,不然不會報錯
    router.register('', views.SendSmsView,'send')#不要用as_view(),需要加上反向解析,不然不會報錯
    router.register('register', views.RegisterGenericViewSet,'register')#使用者註冊介面
    urlpatterns = [
        # 加入路由
        path('', include(router.urls))
    ]

#luffyapi\apps\user\views.py

    #註冊介面
    from rest_framework.viewsets import GenericViewSet
    from rest_framework.mixins import CreateModelMixin
    class RegisterGenericViewSet(GenericViewSet,CreateModelMixin):
        queryset = models.User.objects.all()
        serializer_class = serializer.RegisterModelSerializer
        def create(self,request,*args,**kwargs):
            response = super().create(request,*args,**kwargs)
            username = response.data.get('username')
            return APIResponse(code=1,msg='註冊成功',username=username)

# luffyapi\apps\user\serializer.py

    #註冊序列化器
    from django.core.cache import cache
    from django.conf import settings
    class RegisterModelSerializer(serializers.ModelSerializer):
        code = serializers.CharField(max_length=4,min_length=4,write_only=True)
        class Meta:
            model = models.User
            fields = ('username', 'telephone', 'code', 'password')
            extra_kwargs = {
                'password': {'write_only': True},
                'username': {'read_obly': True},
            }

        def validate(self, attrs):
            telephone = attrs.get('telephone')
            code = attrs.get('code')
            cache_code = cache.get(settings.PHONE_CACHE_KEY % telephone)
            if code == cache_code:
                import re
                if re.match('^[1][3-9][0-9]{9}$',telephone):
                    attrs['username'] = telephone
                    attrs.pop('code')
                    return attrs
                else:
                    raise ValidationError('手機不合法')
            else:
                raise ValidationError('驗證碼有誤')

        #重新create方法
        def create(self, validated_data):
            user = models.User.objects.create_user(**validated_data)
            return user

前臺註冊介面

#src\components\Register.vue
    <template>
        <div class="register">
            <div class="box">
                <i class="el-icon-close" @click="close_register"></i>
                <div class="content">
                    <div class="nav">
                        <span class="active">新使用者註冊</span>
                    </div>
                    <el-form>
                        <el-input
                                placeholder="手機號"
                                prefix-icon="el-icon-phone-outline"
                                v-model="mobile"
                                clearable
                                @blur="check_mobile">
                        </el-input>
                        <el-input
                                placeholder="密碼"
                                prefix-icon="el-icon-key"
                                v-model="password"
                                clearable
                                show-password>
                        </el-input>
                        <el-input
                                placeholder="驗證碼"
                                prefix-icon="el-icon-chat-line-round"
                                v-model="sms"
                                clearable>
                            <template slot="append">
                                <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                            </template>
                        </el-input>
                        <el-button type="primary" @click="register">註冊</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;
                    //字串.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.get(this.$settings.base_url + '/user/check_telephone/', {params: {telephone: this.mobile}}).then(response => {
                        if (response.data.code) {
                            this.$message({
                                message: '您已經註冊過了,快去登入把',
                                type: 'warning',
                                duration: 1000,
                                onClose: () => {
                                    this.go_login()
                                }
                            });

                        } else {
                            this.is_send = true;
                            this.$message({
                                message: '該使用者沒有註冊過,歡迎註冊我們的平臺',
                                type: 'success',
                                duration: 1000,
                            });
                        }
                    }).catch(error => {
                        console.log(error)
                    })

                },
                send_sms() {

                    if (!this.is_send) return;
                    this.is_send = false;
                    let sms_interval_time = 60;
                    this.sms_interval = "傳送中...";

                    this.$axios.get(this.$settings.base_url + '/user/send/', {params: {'telephone': this.mobile}})
                        .then(response => {
                            if (response.data.code) {
                                this.$message({
                                    message: '傳送驗證碼成功',
                                    type: 'success',
                                    duration: 1000,

                                });
                            }
                        })


                    // setInterval(()=>{},100)
                    //定時器:每隔一秒種,把數字減一,當減到小於1,按鈕又能點了,顯示獲取驗證碼
                    let timer = setInterval(() => {
                        if (sms_interval_time <= 1) {
                            clearInterval(timer);  //如果小於等於1,把定時器清除
                            this.sms_interval = "獲取驗證碼";
                            this.is_send = true; // 重新回覆點擊發送功能的條件
                        } else {
                            sms_interval_time -= 1;
                            this.sms_interval = `${sms_interval_time}秒後再發`;
                        }
                    }, 1000);
                },
                register() {
                    if (this.mobile && this.sms && this.password) {
                        this.$axios.post(this.$settings.base_url + '/user/register/', {
                            telephone: this.mobile,
                            code: this.sms,
                            password: this.password
                        }).then(response => {
                            if (response.data.code) {
                                //註冊成功,來個提示,跳轉到登入
                                this.$message({
                                    message: '註冊成功',
                                    type: 'success',
                                    duration: 1000,
                                    onClose: () => {
                                        this.go_login()
                                    }
                                });
                            } else {
                                this.$message({
                                    message: '未知錯誤',
                                    type: 'error',
                                    duration: 1000,
                                    onClose: () => {
                                        this.mobile = ''
                                        this.sms = ''
                                        this.password = ''
                                    }
                                });
                            }
                        })
                    } else {
                        this.$message({
                            message: '你有沒填的資訊',
                            type: 'error',
                            duration: 1000,
                        });
                    }
                },
            }
        }
    </script>

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

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

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

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

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

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

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

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

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

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

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

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

Redis使用

Redis使用

安裝

Python操作Redis普通連線

1 pip install redis
#簡單使用
    from redis import Redis
    #獲取redis連線
    conn = Redis(host='127.0.0.1',port=6379)
    #key取值
    ret = conn.get('name')

    print(ret)#b'alex'

Python操作Redis連線池

#redis_pool\redis_pool.py

    import redis
    #連線池物件,最大連線數100
    POOL = redis.ConnectionPool(host='127.0.0.1',port=6379,max_connections=100)

#redis_pool\redis_pool.py  

    import redis
    from redis_pool import POOL
    #執行這句話就是從連線池拿出一個連線
    r = redis.Redis(connection_pool=POOL)

    ret = r.get('name')

    print(ret)#b'alex'

Redis資料型別

string(字串)
list(列表)
set(集合)
zset(有序集合)
hash(雜湊)
String(字串)操作
string在記憶體中按照一個name一個value來儲存,看圖1

#加*表示重點
set(name, value, ex=None, px=None, nx=False, xx=False)*
#設定值,不存在則建立,存在則修改,
引數:
	ex過期時間秒
    px過期時間毫秒
    nx為True時,key不存在時,才會執行,key存在則不執行操作
    xx為True時,key不存在時,不會執行操作,key存在才會執行
    
setnx(name, value)
#設定值,key不存在,才會執行,存在不會執行操作

setex(name, value, time)
#設定值,引數time過期時間數字秒,或timedelta物件

psetex(name, time_ms, value)
#設定值,引數time_ms過期時間數字毫秒,或timedelta物件

mset(*args,**kwargs)*
#批量設定值,如
	mset(k1='v1',k2='v2')
    mset({'k1':'v1','k2':'v2'})

get(key)*    
#獲取值

mget(keys,*args)*
#批量獲取,如
	mget('k1','k2')
    mget(['k1','k2'])

getset(name, value)*
#設定值並獲取原來的值

getrange(key, start, end)
#獲取子序列,根據位元組獲取,非字元,引數:
	start開始位置的索引(前閉後閉)#漢字佔3個位元組
    end結束位置的的索引(前閉後閉)

setrange(name, offset, value)
#修改字串內容,從指定索引向後修改,新值比原值長則向後新增,引數:
	0ffset開始修改字串的索引,漢字佔3個位元組
    value指要設定的指

setbit(name, offset, value)
#對key對應的值的二進位制(010101001)進行操作,引數:
	offset開始位的索引,需要將值轉成二進位制(1位元組等於8位二進位制)索引
    value值只能是0或1
    
getbit(name, offset)
# 獲取key對應值開始索引的二進位制的值0或1

bitcount(key, start=None, end=None)
#獲取key對應值的二進位制的值中的1的個數,引數:
	strat二進位制起始位置
    end二進位制結束位置
   
bitop(operation, dest, *keys)
#獲取多個值,並將值做位運算,將最後的值儲存至新的key對應的值,引數:
	operation可以是AND(並)、OR(或)、NOT(非)、XOR(異或)
	dest新的key名
    *keys要查詢的key
	如:
    conn.bitop('AND','name_1','name1','name2','name3')
    #獲取key,'name1','name2','name3'對應的值,做位運算(並集),然後將結果儲存到name_1對應的值中

strlen(name)
#返回key對應值的位元組長度(漢字三個位元組)

incr(self, name, amount=1)#incrby(self, name, amount=1)*
#自增key對應的值,當key不存在時,則建立key對應值是amount的值,否則則自增,引數:
	amount自增數,必須是整數

incrbyfloat(self, name, amount=1.0)
#自增key對應的值,key不存在時,則建立key=amount,否則自增,引數:
	amount自增數,必須是浮點型

decr(self, name, amount=1)*
#自減key對應的值,key不存在時,則建立key=amount,否則則自減,引數:
	amount自減數,必須是整數
    
append(key, value)*
#在key對應的值後面追加內容,引數:
	value要追加的字串

圖1

Hash(雜湊)操作
#Hash在記憶體中的儲存格式,value值必須是字串型別,看圖1

#加*表示重點
hset(name, key, value)*
#name對應的hash中設定一個鍵值對,不存在建立,存在則修改,引數:
	name指redis儲存hash的name
    key指name對應的hash的key
    value指name對應的hash的value
    
hmset(name, mapping)*
#在name對應的hash中批量設定鍵值對,引數:
	mapping是字典,比如{'k1':'v1','k2','v2'}
    
hget(name,key)*
#在name對應的hash中獲取根據key獲取value值

hmget(name, keys, *args)*
#在name對應的hash中獲取多個key對應的值。引數:
	keys要獲取key的集合,如['k1','k2','k3']
    *args要獲取的key,如k1,k2,k3
    例項:
        hmget('name',['k1','k2','k3'])
         hmget('name',k1,k2,k3)

hgetall(name)
#獲取name對應的hash的所有鍵值

hlen(name)
#獲取name對應的hash的鍵值對的個數

hkeys(name)
#獲取name對應的hash的所有的key的值

hvals(name)
#獲取name對應的hash的所有的value的值

hexists(name, key)
#檢查name對應的hash是否存在key

hdel(name,*keys)
#將name對應的hash指定的key的鍵值對刪除

hincrby(name, key, amount=1)*
#自增name對應的hash中指定的key的值,不存在則建立key=amount,引數:
	amount自增數,必須是整數
    
    
hincrbyfloat(name, key, amount=1.0)
#自增name對應的hash中指定key的值,不存在則建立key=amount,引數:
	amount自增數,必須是浮點數
    
    
hscan(name, cursor=0, match=None, count=None)
#增量式迭代獲取,對於資料大的非常有用,hscan可以實現分片式獲取資料,並非一次性將資料全部獲取,導致記憶體被撐爆,引數:
	cursor遊標,基於遊標分批獲取資料
    match匹配指定key,預設None,表示所有的key
    count每次分片最少獲取個數,預設None表示採用redis預設分片個數
    
hscan_iter(name, match=None, count=None)
#利用yield封裝hscan建立生成器,實現分批去redis中獲取資料,引數:
	match匹配指定key,預設Nnoe,表示所有的key
    count每次分片最少獲取個數,預設None表示採用redis的預設分片個數
    例項:for item in r.hscan_iter('xx'):
        	print item    
            
# 區分hgetall和hscan_iter            

List(列表)操作
#List在記憶體中按照一個name對應一個List來儲存,看圖1

#加*表示重點
lpush(name,values)*
#在name對應的list中新增元素,每個新的元素都新增到列表的最左邊,如:
	r.lpush('oo', 11,22,33)
    結果為: 33,22,11
    # 擴充套件:
        rpush(name, values) 表示從右向左操作

lpushx(name,value)
#在name對應的list中新增元素,只有name存在時,值新增到list最左邊
	擴充套件:rpushx(name, value) 表示從右向左操作
    
llen(name)*
#name對應的list元素的個數

linsert(name, where, refvalue, value))
#在name對應的list的某一個值前面或後面插入一個值,引數:
	where可以式BEFORE或AFTER(小寫也可以)
    refvalue標杆值,在它前後插入資料,如果存在多個標杆值,以找到的第一個為準
    value要插入的值
    
r.lset(name, index, value)
#對name對應的list的某一個索引位置重新賦值,引數:
	index指需要重新賦值的索引位置
    value要設定的值
    
r.lrem(name, value, num)
#在name對應的list中刪除指定的值,引數:
	value要刪除的值
    num:
    	num=0表示刪除該list中所有指定值
        num=2表示從前到後刪除兩個
        num=-2表示從後到前刪除兩個
        
lpop(name)*
#在name對應的list左側獲取第一個元素並在list中移除,返回值則是該元素
	擴充套件:rpop(name) 表示從右向左操作
    
lindex(name, index)
#在name對應的list中根據索引獲取list元素

lrange(name, start, end)*
#在name對應的list分片獲取資料,引數:
	start開始的索引位置
    end結束的索引位置
    
ltrim(name, start, end)
#在name對應的list中移除沒有在start-end索引之間的值,引數:
	start開始的索引位置
    end結束的索引位置
    
rpoplpush(src, dst)
#從一個列表取出最右邊的元素,提示新增到另一個list的最左邊,引數:
	src要獲取資料的list的name
    dst要新增資料的list的name
    
blpop(keys, timeout)*
#將多個list排列,按照從左到右去pop對應list的元素,引數:
	keys是個集合,要pop元素的集合
    timeout超時時間,當元素所有list的元素獲取玩之後,阻塞等待list內資料局的時間秒,0表示醫院阻塞,
    擴充套件:r.brpop(keys, timeout),從右向左獲取資料,應用場景:爬蟲實現簡單分散式:多個url放到列表裡,往裡不停放URL,程式迴圈取值,但是隻能一臺機器執行取值,可以把url放到redis中,多臺機器從redis中取值,爬取資料,實現簡單分散式

brpoplpush(src, dst, timeout=0)    
#從一個列表的右側移除一個元素,並將其新增到另一個list的左側,引數:
	src取出並要移除元素的list的name
    dst要插入元素的list的name
    timeout當src對應的list中沒有資料時,阻塞等待其有資料的超時時間秒,0表示醫院阻塞
 
自定義增量迭代
# 由於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)

Set(集合)操作
其他操作
delete(*names)
#刪除redis中任意資料型別

exists(name)
#檢查redis的name是否存在

keys(pattern='*')
#根據模型獲取redis的name,其他引數:
	KEYS * 匹配資料庫中所有 key 。
    KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
    KEYS h*llo 匹配 hllo 和 heeeeello 等。
    KEYS h[ae]llo 匹配 hello 和 hallo ,不匹配 hillo 
	
expire(name ,time)
#為某個redis的某個name設定超時時間

rename(src, dst)
#對redis的name重新命名

move(name, db)
#將redis的某個值移動到指定的db下

randomkey()
#隨機獲取一個redis的name,不刪除

type(name)
#獲取nmae對應值的型別

scan(cursor=0, match=None, count=None)
scan_iter(match=None, count=None)
#同字串操作,用於增量迭代獲取key

管道

如果想要在一次請求中指定多個命令,則可以使用pipline實現一次請求指定多個命令,並且預設情況下一次pipline 是原子性操作。#redis預設在執行每次請求都會建立(連線申請連線池)和斷開(歸還連線池)一次性操作,如果想要在一次請求中指定多個命令,則可以使用pipline實現一次請求指定多個命令,並且預設情況下一次pipline 是原子性操作(要麼全部成功,要麼全部失敗)。

#test.py
    import redis

    pool = redis.ConnectionPool(host='10.211.55.4', port=6379)

    r = redis.Redis(connection_pool=pool)

    # pipe = r.pipeline(transaction=False)
    pipe = r.pipeline(transaction=True)
    pipe.multi()
    pipe.set('name', 'alex')
    pipe.set('role', 'sb')

    pipe.execute()

Django使用Redis

#方式一,通用方式
    #utils\redis_pool.py
    
		import redis
        #POOL連線池,max_connections=100指最大連線數是100
        POOL = redis.ConnectionPool(host='127.0.0.1',port=6379,max_connections=100)
        
	#app01\views.py
    
        import redis
        from utils.redis_pool import POOL
        from django.shortcuts import render, HttpResponse
        def index(request):
            conn = redis.Redis(connection_pool=POOL)
            conn.set('hobby','sleep')
            return HttpResponse('設定成功')

        def order(request):
            conn = redis.Redis(connection_pool=POOL)
            ret = conn.get('hobby')
            print(ret)
            return HttpResponse('獲取成功')
        
#方式二,安裝django-redis模組
	pip install django-redis
    
    #settings.py配置
    
        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_redis import get_redis_connection
    conn = get_redis_connection('default')
    print(conn.hgetall('xxx'))

介面快取

#luffyapi\apps\home\views.py,BannerView類內    
    def banner_list(self,request,*args,**kwargs):
        banner_list = cache.get('banner_list')
        if not banner_list:
            #快取沒有去資料拿
            response = super().list(request,*args,**kwargs)
            #加到快取
            cache.set('banner_list',response.data,60*60*24)#banner_list一天過期
        return Response(data=banner_list)	

Celery(非同步任務框架)

#瞭解
    Celery 官網:http://www.celeryproject.org/
    Celery 官方文件英文版:http://docs.celeryproject.org/en/latest/index.html
    Celery 官方文件中文版:http://docs.jinkan.org/docs/celery/

    介紹: Celery是一個簡單、靈活、可靠處理大量訊息的分散式系統,專注於實時處理的非同步入任務佇列,同時支援任務排程   

    Celery is a project with minimal funding, so we don’t support Microsoft Windows. Please don’t open any issues related to that platform.

Celery使用

#可以執行非同步、延時、定時任務

#安裝 pip install celery

基本機構

#t_redis_pool\celery_1\celery_task.py

    from celery import Celery
    broker = 'redis://127.0.0.1:6379/1'#brokey任務佇列
    backend = 'redis://127.0.0.1:6379/2'#結構儲存,執行完的結果存在這

    app = Celery(__name__,broker=broker,backend=backend)

    #新增任務,使用裝飾器@app.task
    @app.task
    def add(x,y):
        print(x,y)
        return x*y

#t_redis_pool\celery_1\celery_add.py

    from celery_task import add
    ret = add.delay(3,8)#向add函式新增一個任務

#t_redis_pool\celery_1\result1.py

    from celery_task import app
    from celery.result import AsyncResult

    id = '3e397fd7-e0c1-4c5c-999c-2655a96793bb'
    if __name__ == '__main__':
        a = AsyncResult(id=id, app=app)
        if a.successful():
            result = a.get()
            print(result)
        elif a.failed():
            print('任務失敗')
        elif a.status == 'PENDING':
            print('任務等待中被執行')
        elif a.status == 'RETRY':
            print('任務異常後正在重試')
        elif a.status == 'STARTED':
            print('任務已經開始被執行')

包結構

#新建一個包celery_task,包裡面必須有個celery.py
#celery_task\celery.py
    from celery import Celery

    broker = 'redis://127.0.0.1:6379/1'#broker任務佇列
    backend = 'redis://127.0.0.1:6379/2'#結構儲存,執行完的結果在這

    app = Celery(__name__,broker=broker,backend=backend,include=['celery_task.task1','celery_task.task2'])

#celery_task\task1.py

    from .celery import app
    @app.task
    def mutile(x,y):
        print(x,y)
        return x*y
    
#celery_task\task2.py

    from .celery import app

    @app.task
    def add(x, y):
        print(x, y)
        return x + y
    
#celery_task\test.py提交任務

    from celery_task.task1 import mutile
    from celery_task.task2 import add

    ret = add.delay(6,9)
    print(ret)
    
    ret = mutile(9,8)#上面的ret不用註釋,可以提交多個
    print(ret)


    #提交延時任務
    from datetime import datetime, timedelta

    #需要utc時間
    eta = datetime.utcnow() + timedelta(seconds=10)
    ret = add.apply_async(args=(240,50),eta=eta)

    print(ret)
    
#檢視任務結果

	from celery_task import app
    from celery.result import AsyncResult
    id = '3e397fd7-e0c1-4c5c-999c-2655a96793bb'
    if __name__ == '__main__':
        async = AsyncResult(id=id, app=app)
        if async.successful():
            result = async.get()
            print(result)
        elif async.failed():
            print('任務失敗')
        elif async.status == 'PENDING':
            print('任務等待中被執行')
        elif async.status == 'RETRY':
            print('任務異常後正在重試')
        elif async.status == 'STARTED':
            print('任務已經開始被執行')    
    
#執行定時任務
#celery_task\celery.py

    from celery import Celery

    broker = 'redis://127.0.0.1:6379/1'  # broker任務佇列
    backend = 'redis://127.0.0.1:6379/2'  # 結構儲存,執行完的結果存在這
    app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.task1', 'celery_task.task2'])
    # 執行定時任務
    # 時區
    app.conf.timezone = 'Asia/Shanghai'
    # 是否使用UTC
    app.conf.enable_utc = False
    # 任務的定時配置
    from datetime import timedelta
    from celery.schedules import crontab

    app.conf.beat_schedule = {
        'add-task': {
            'task': 'celery_task.task1.add',
            # 'schedule': timedelta(seconds=3),
            'schedule': crontab(hour=8, day_of_week=1),  # 每週一早八點
            'args': (300, 150),
        }
    }
#pycharm終端執行

	#啟動worker,等待任務
	-celery worker -A celery_task -l info -P eventlet
    #啟動beat,提交任務,如果報錯刪除產生的檔案
	-celery beat -A celery_task -l info

課程表結構分析、優化

#  ForeignKey的欄位:
	-related_name 反向操作時,使用的欄位名,用於代替原反向查詢時的'表名_set'。
    -related_query_name :反向查詢操作時,使用的連線字首,用於替換表名。
    
################1   course/models.py
from luffyapi.utils.models import BaseModel
# 實際路飛課程相關表,以免費課為例
class CourseCategory(BaseModel):
    """分類
    python,linux,go, 網路安全
    跟課程是一對多的關係

    """
    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="課程狀態")
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程原價", default=0)

    # 優化欄位
    students = models.IntegerField(verbose_name="學習人數", default=0)
    sections = models.IntegerField(verbose_name="總課時數量", default=0)
    pub_sections = models.IntegerField(verbose_name="課時更新數量", default=0)

    # 關聯欄位
    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授課老師",db_constraint=False)
    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 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



class CourseChapter(BaseModel):
    """章節
    章節跟課程是一(課程)對多(章節多)
    """
    course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE, verbose_name="課程名稱",db_constraint=False)
    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="課程章節",db_constraint=False)
    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)
    
    
################2 utils/models/BaseModel
from django.db import models
# 後期課程表,輪播圖表,都會用到這些欄位

class BaseModel(models.Model):
    is_delete = models.BooleanField(default=False, verbose_name='是否刪除')
    is_show = models.BooleanField(default=True, verbose_name='是否展示')
    # 修改成這樣
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
    updated_time  = models.DateTimeField(auto_now=True, verbose_name='最後更新時間')
    orders = models.IntegerField()
    class Meta:
        abstract=True  # 一定不要忘了


建立應用python ../../manage.py startapp course



################3 資料遷移
python manage.py makemigrations
python manage.py migrate

課程測試資料準備


-- 老師表
-- 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, '2019-07-14 13:44:19.661327', '2019-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, '2019-07-14 13:45:25.092902', '2019-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, '2019-07-14 13:46:21.997846', '2019-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, '2019-07-14 13:40:58.690413', '2019-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, '2019-07-14 13:41:08.249735', '2019-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, '2019-07-14 13:54:33.095201', '2019-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, '2019-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, '2019-07-14 13:56:05.051103', '2019-07-14 13:56:05.051142', 'Python專案實戰', 'courses/mjj_python.png', 0, '', 1, '2019-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, '2019-07-14 13:57:21.190053', '2019-07-14 13:57:21.190095', 'Linux系統基礎5周入門精講', 'courses/lyy_linux.png', 0, '', 0, '2019-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, '2019-07-14 13:58:34.867005', '2019-07-14 14:00:58.276541', 1, '計算機原理', '', '2019-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, '2019-07-14 13:58:48.051543', '2019-07-14 14:01:22.024206', 2, '環境搭建', '', '2019-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, '2019-07-14 13:59:09.878183', '2019-07-14 14:01:40.048608', 1, '專案建立', '', '2019-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, '2019-07-14 13:59:37.448626', '2019-07-14 14:01:58.709652', 1, 'Linux環境建立', '', '2019-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, '2019-07-14 14:02:33.779098', '2019-07-14 14:02:33.779135', '計算機原理上', 1, 2, NULL, NULL, '2019-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, '2019-07-14 14:02:56.657134', '2019-07-14 14:02:56.657173', '計算機原理下', 2, 2, NULL, NULL, '2019-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, '2019-07-14 14:03:20.493324', '2019-07-14 14:03:52.329394', '環境搭建上', 1, 2, NULL, NULL, '2019-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, '2019-07-14 14:03:36.472742', '2019-07-14 14:03:36.472779', '環境搭建下', 2, 2, NULL, NULL, '2019-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, '2019-07-14 14:04:19.338153', '2019-07-14 14:04:19.338192', 'web專案的建立', 1, 2, NULL, NULL, '2019-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, '2019-07-14 14:04:52.895855', '2019-07-14 14:04:52.895890', 'Linux的環境搭建', 1, 2, NULL, NULL, '2019-07-14 14:04:52.895942', 1, 4);

課程主頁頁面

# 不要忘了註冊路由

# FreeCourse.vue
<template>
    <div class="course">
        <Header></Header>
        <div class="main">
            <!-- 篩選條件 -->
            <div class="condition">
                <ul class="cate-list">
                    <li class="title">課程分類:</li>
                    <li class="this">全部</li>
                    <li>Python</li>
                    <li>Linux運維</li>
                    <li>Python進階</li>
                    <li>開發工具</li>
                    <li>Go語言</li>
                    <li>機器學習</li>
                    <li>技術生涯</li>
                </ul>

                <div class="ordering">
                    <ul>
                        <li class="title">篩&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;選:</li>
                        <li class="default this">預設</li>
                        <li class="hot">人氣</li>
                        <li class="price">價格</li>
                    </ul>
                    <p class="condition-result">共21個課程</p>
                </div>

            </div>
            <!-- 課程列表 -->
            <div class="course-list">
                <div class="course-item">
                    <div class="course-image">
                        <img src="@/assets/img/course-cover.jpeg" alt="">
                    </div>
                    <div class="course-info">
                        <h3>Python開發21天入門 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入學習</span></h3>
                        <p class="teather-info">Alex 金角大王 老男孩Python教學總監 <span>共154課時/更新完成</span></p>
                        <ul class="lesson-list">
                            <li><span class="lesson-title">01 | 第1節:初識編碼</span> <span class="free">免費</span></li>
                            <li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span> <span class="free">免費</span></li>
                            <li><span class="lesson-title">01 | 第1節:初識編碼</span></li>
                            <li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span></li>
                        </ul>
                        <div class="pay-box">
                            <span class="discount-type">限時免費</span>
                            <span class="discount-price">¥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/Head"
    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>

課程分類群查介面

# 路由
router = SimpleRouter()
router.register('categories', views.CourseCategoryView, 'categories')
router.register('free', views.CouresView, 'free')
urlpatterns = [
    path('', include(router.urls)),
]

# 檢視views.py
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from . import models
from . import serializer
class CourseCategoryView(GenericViewSet,ListModelMixin):
    queryset = models.CourseCategory.objects.filter(is_delete=False,is_show=True).order_by('orders')
    serializer_class = serializer.CourseCategorySerializer

# 序列化類
from rest_framework import serializers
from . import models
class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model=models.CourseCategory
        fields=['id','name']

課程群查介面

# 路由
router = SimpleRouter()
router.register('categories', views.CourseCategoryView, 'categories')
router.register('free', views.CouresView, 'free')
urlpatterns = [
    path('', include(router.urls)),
]

# 檢視 views.py
from .paginations import PageNumberPagination
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter,SearchFilter
from .filters import MyFilter
class CouresView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
    queryset = models.Course.objects.filter(is_delete=False,is_show=True).order_by('orders')
    serializer_class = serializer.CourseModelSerializer
    pagination_class = PageNumberPagination
    filter_backends=[DjangoFilterBackend,OrderingFilter]
    ordering_fields=['id', 'price', 'students']

    filter_fields=['course_category','students']
    
 ###serializer
from rest_framework import serializers
from . import models


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


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


class CourseModelSerializer(serializers.ModelSerializer):
    # 子序列化的方式
    teacher = TeacherSerializer()

    class Meta:
        model = models.Course
        fields=[
            'id',
            'name',
            'course_img',
            'brief',
            'attachment_path',
            'pub_sections',
            'price',
            'students',
            'period',
            'sections',
            'course_type_name',
            'level_name',
            'status_name',
            'teacher',
            'section_list',
        ]
 # models.py
class Course(BaseModel):
	...
    @property
    def course_type_name(self):
        return self.get_course_type_display()
    @property
    def level_name(self):
        return self.get_level_display()
    @property
    def status_name(self):
        return self.get_status_display()

    @property
    def section_list(self):
        ll=[]
        # 根據課程取出所有章節(正向查詢,欄位名.all())
        course_chapter_list=self.coursechapters.all()
        for course_chapter in course_chapter_list:
            # 通過章節物件,取到章節下所有的課時(反向查詢)
            # course_chapter.表名小寫_set.all() 現在變成了course_chapter.coursesections.all()
            course_sections_list=course_chapter.coursesections.all()
            for course_section in course_sections_list:
                ll.append({
                    'name': course_section.name,
                    'section_link': course_section.section_link,
                    'duration': course_section.duration,
                    'free_trail': course_section.free_trail,
                })
                if len(ll)>=4:
                    return ll

        return ll


    
class Teacher(BaseModel):
	...
    def role_name(self):
        # 返回角色的中文
        return self.get_role_display()

搜尋元件和過濾

'''
排序:
按id正序倒敘排序,按price正序倒敘排列
使用:http://127.0.0.1:8000/course/free/?ordering=-id
配置類:
    filter_backends=[OrderingFilter]
配置欄位:
    ordering_fields=['id','price']
    
    
內建過濾:
使用:http://127.0.0.1:8000/course/free/?search=39
按照price過濾(表自有的欄位直接過濾)
配置類:
    filter_backends=[SearchFilter]
配置欄位:
    search_fields=['price']
    
擴充套件:django-filter
安裝:
支援自由欄位的過濾還支援外來鍵欄位的過濾
http://127.0.0.1:8000/course/free/?course_category=1   # 過濾分類為1 (python的所有課程)
配置類:
    filter_backends=[DjangoFilterBackend]
配置欄位:
    filter_fields=['course_category']


'''

自定義過濾類

# filters.py
# 自定義過濾規則
from rest_framework.filters import BaseFilterBackend

class MyFilter(BaseFilterBackend):
    def filter_queryset(self, request, queryset, view):
        # 真正的過濾規則
        # params=request.GET.get('teacher')
        # queryset.filter('''''')
        return queryset[:1]
 
# 使用:在檢視類中配置
filter_backends=[DjangoFilterBackend,OrderingFilter,MyFilter]

區間過濾

#使用django-filter實現過濾
#luffyapi\apps\course\filters.py

    from django_filters import FilterSet,filters
    from rest_framework.filters import BaseFilterBackend

    from . import models
    #區間過濾
    class CourseFilterSet(FilterSet):
        min_price = filters.NumberFilter(field_name='price',lookup_expr='gt')
        max_price = filters.NumberFilter(field_name='price',lookup_expr='lt')
        class Meta:
            model = models.Course
            fields = ('course_category')

    #自定義規則過濾
    class MyFilter(BaseFilterBackend):
        def filter_queryset(self, request, queryset, view):
            return queryset[:1]

#luffyapi\apps\course\views.py,檢視中配置filter_backends=[DjangoFilterBackend],filter_class = CourseFilterSet

    #課程群查介面
    # from .paginations import PageNumberPagination
    from django_filters.rest_framework import DjangoFilterBackend
    from rest_framework.filters import OrderingFilter,SearchFilter
    from .filters import MyFilter
    #匯入區間過濾
    from .filters import CourseFilterSet
    class CouresView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
        queryset = models.Course.objects.filter(is_delete=False,is_show=True).order_by('orders')
        serializer_class = serializer.CourseModelSerializer
        # pagination_class = PageNumberPagination

        filter_backends=[DjangoFilterBackend,OrderingFilter]
        ordering_fields=['id', 'price', 'students']


        filter_fields=['course_category','students']


前端主頁詳情頁

<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/Head"
    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/categories/`).then(response => {
                    this.category_list = response.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/free/`, {
                    params: filters
                }).then(response => {
                    // console.log(response.data);
                    this.course_list = response.data.results;
                    this.course_total = response.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>

前端課程詳情頁

#新建vue,src\views\FreeCourseDetail.vue

#配置路由,src\router\index.js

		{
            path: '/free/detail/:pk',
            name: 'FreeCourseDetail',
            component: FreeCourseDetail
        },
    
#安裝 vue-video播放器,cnpm install vue-video-player,在main.js配置#src\main.js

	        // vue-video播放器
            require('video.js/dist/video-js.css');
            require('vue-video-player/src/custom-theme.css');
            import VideoPlayer from 'vue-video-player'
            Vue.use(VideoPlayer);
            
#src\views\FreeCourseDetail.vue

    <template>
        <div class="detail">
            <Header/>
            <div class="main">
                <div class="course-info">
                    <div class="wrap-left">
                        <videoPlayer class="video-player vjs-custom-skin"
                                     ref="videoPlayer"
                                     :playsinline="true"
                                     :options="playerOptions"
                                     @play="onPlayerPlay($event)"
                                     @pause="onPlayerPause($event)">
                        </videoPlayer>
                    </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/Head"
        import Footer from "@/components/Footer"

        // 載入元件
        import {videoPlayer} from 'vue-video-player';

        export default {
            name: "Detail",
            data() {
                return {
                    tabIndex: 2,   // 當前選項卡顯示的下標
                    course_id: 0, // 當前課程資訊的ID
                    course_info: {
                        teacher: {},
                    }, // 課程資訊
                    course_chapters: [], // 課程的章節課時列表
                    playerOptions: {
                        aspectRatio: '16:9', // 將播放器置於流暢模式,並在計算播放器的動態大小時使用該值。值應該代表一個比例 - 用冒號分隔的兩個數字(例如"16:9"或"4:3")
                        sources: [{ // 播放資源和資源格式
                            type: "video/mp4",
                            src: "http://qe5smxg12.bkt.clouddn.com/3%20%E9%A6%96%E9%A1%B5%E8%BD%AE%E6%92%AD%E5%9B%BE%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0.mp4" //你的視訊地址(必填)
                        }],
                    }
                }
            },
            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.pk || this.$route.query.pk;
                    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/free/${this.course_id}/`).then(response => {
                        // window.console.log(response.data);
                        this.course_info = response.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/chapters/`, {
                        params: {
                            "course": this.course_id,
                        }
                    }).then(response => {
                        this.course_chapters = response.data;
                    }).catch(error => {
                        window.console.log(error.response);
                    })
                },
            },
            components: {
                Header,
                Footer,
                videoPlayer, // 註冊元件
            }
        }
    </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>

章節分類介面

#luffyapi\apps\course\views.py

    class CourseChapterView(GenericViewSet, ListModelMixin):
        queryset = models.CourseChapter.objects.filter(is_delete=False, is_show=True)
        serializer_class = serializer.CourseChapterSerializer

        # 可以按照課程id來查
        filter_backends = [DjangoFilterBackend]
        filter_fields = ['course']
 
#luffyapi\apps\course\serializer.py

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


    #課程章節序列化器
    class CourseChapterSerializer(serializers.ModelSerializer):
        # 子序列化的方式
        coursesections=CourseSectionSerializer(many=True)
        class Meta:
            model=models.CourseChapter
            fields=['name','summary','chapter','coursesections']
            
#luffyapi\apps\course\urls.py
    router.register('chapters', views.CourseChapterView, 'coursechapter')

七牛雲視訊託管

#進入位置註冊,實名認證
	https://www.qiniu.com/

#登入然後到管理控制檯建立物件儲存

#新建空間,上傳檔案,獲得檔案連結http://qe6j0hynu.bkt.clouddn.com/99f0eeb961b8cfae9cf8b017666c9bec.mp4

後臺搜尋介面

#luffyapi\apps\course\views.py

    class CouresSearchView(GenericViewSet,ListModelMixin):
        queryset = models.Course.objects.filter(is_delete=False,is_show=True)
        serializer_class = serializer.CourseModelSerializer
        # pagination_class = PageNumberPagination

        filter_backends=[SearchFilter]
        search_fields=['name']

#luffyapi\apps\course\urls.py

    router.register('search', views.CouresSearchView, 'search')

前臺搜尋頁面

# 0 在Head.vue中相應的配置


# 1  vue帶給另一個頁面值
    -路由:path: '/free/detail/:pk',this.$route.params.pk  從路徑中取值
	-search?word=dsa  去this.$route.query.word   從?後面的取
    
    
# 2 樣式
<template>
    <div class="search-course course">
        <Header/>
        <h1>總共搜尋到 {{course_total}}課程</h1>
        <!-- 課程列表 -->
        <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/Head'

    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 => {
                    // 如果後臺不分頁,資料在response.data中;如果後臺分頁,資料在response.data.results中
                    this.course_list = response.data.results;
                    this.course_total = response.data.count;
                }).catch(() => {
                    this.$message({
                        message: "獲取課程資訊有誤,請聯絡客服工作人員"
                    })
                })
            }
        }
    }
</script>

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

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

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

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

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

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

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

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

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

    .course .ordering ul {
        float: left;
    }

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

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

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

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

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

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

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

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

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

    .course .ordering .price_up::before {
        border-bottom-color: #ffc210;
    }

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

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

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

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

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

    .course .course-item .course-image img {
        max-width: 100%;
        max-height: 210px;
    }

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

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

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

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

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

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

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

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

    .course-item .section-list li .section-title {
        /* 以下3句,文字內容過多,會自動隱藏,並顯示省略符號 */
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        display: inline-block;
        max-width: 200px;
    }

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

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

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

    .course-item {
        position: relative;
    }

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

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

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

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

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

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

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

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

支付寶開放平臺(支付介面)

#進入沙箱環境,看圖1,https://openhome.alipay.com/platform/appDaily.htm?tab=info

#下載軟體,生成公鑰,祕鑰,看圖2,https://opendocs.alipay.com/open/291/106097/
#生成的公鑰,私鑰檔案在這C:\Users\Steady\Documents\支付寶開放平臺開發助手\RSA金鑰

#取出公鑰,配置到支付寶平臺,他會生成一個支付寶公鑰,看圖3

圖1

圖2

圖3

後端使用支付介面

#安裝pip install python-alipay-sdk

#新建一個包,本地測試用,scripts\alipay_test.py

    from alipay import AliPay


    # app_private_key_string = open("/path/to/your/private/key.pem").read()
    # alipay_public_key_string = open("/path/to/alipay/public/key.pem").read()
    #自己的私鑰
    app_private_key_string = """-----BEGIN RSA PRIVATE KEY-----
    MIIEowIBAAKCAQEAzH4gLah2+9reKHo9s/edqgytBn9tPzHBDNBca2OeUzWRsR0rJTa/3oQICddGttdwjrA8lV3rK0ByDGYKM3WZRuqDwdkRU17aav21sTw42JmeVPMxFzNczoN6N0IL8WcLwRhOeUUoOkrKkmeJFdZAkP2UuoRqh9qXCjDBlZUayEVMH3gRWdlvCdQnEqkJJTi94yHUgvVATQnU3FVLYLgMqCJCFHVabvEBRjBBZ6VzqOZic2WevlC2pjlZt/u1TWsCRIV4xHwbAtcfIW66WMV9q4tyN/9WTG437n7iokMbRZkBTQWFdh9wvSNgl/fCSBsmJPrtuj924xfLu/dvwBml9wIDAQABAoIBAAbiH/cpeofTwDKgmr8aEKaOsqxLUcQUyqjBwrqiqcunO/Y4QyGHjEHTM+oQ4wJ4kgNbj05/0NCt5KjiLmm5BaXLcw/tXJXv1W5Qh/MlUcyIrzmwzfA3/GG/Lzf48yn3DAA97zZJy6baqaLv9T9U7Ot0fbMhjexwwTDIn1uHn1UqYJOGAuGCB/a0GpK1n3Bk4Uut5LxEAa3F/Jyyx0YZaBcKwbp3Q7oVhhrYqFluDLoLzRRDRVMWSjfAd+IRK8KnnnBtd7hUydh9+lmnZ0JIE2OhFTEqhoNnShP1AxQkjfJVNj8LFm5N/hzgk6Uxu4WjqUvn0Cdl4CoI2mzhTSc5ZgECgYEA6X90QHWINcAntgBA4fcOAn5HmA6wP6xCiag+kXBPBx8oAsuRYeBywnw/8cJQ/XH1fz3hah3hFmly+5ckeAEF7WpyUISCEmhJr1sc4IAwBMX8KDYQUluvx2PfA/ZWpS+r9Xb1hz+2WRUWs1gACOjkMuHA0POhEYU8hhP08YyXI4ECgYEA4DMYMX+28UDYn6Dj15HuMQA3uNaXZwHVLLg4nYWRYlIa4hKygzFHN40KoZCgkg4H92TSs4pui87qJw9Pt2Z2ISW/lg3JCotBNkIN+Tcfm0fptecLItEztRjFlLtI0w1vperL0X0mbtKswtx4r3b7qFiu2VP18kIehqxzhN72pXcCgYAL1CEXgty7nFhmp+yhhdp31d3aniPfleJpBtTkmUPnoHRub9GVm8E0iQbUQHvJ2xQMth3NOmZEtGLWy/lU+vhJ5rKT0aKlPBM/huYXmWbVVxJHFzYMdgiZmT2a/ce15oxXiwc1Ks0aBi94NmUKDoJA8Z7K9wyZzp4aTLiD5BNkAQKBgQCh70XCOuUATD4qziLDSXwpmGam8xhYJlK7AjSiUh84TOVt9ONLLwSpcw4CtFvGIqxOXFxzocFG6iTmIYEKjs80EffXgg17uClZ70v4fYAuwDpmUIMSmtgmLcZILx3ikwnZZUXiAesIkNKHzIa+9cm/Ro7EGojCek9iJp0pk4HC8QKBgB8bAb7xqL6zCJQxRyDjfNUNiNicvuMLHWPJT55/sVNJixuxzB/+fOfZk+uSXaeI9lcgtb29SPQkUUeU+3ch0jnrnmt0juLT37HNYipbpAP4WkXMjL9fl7hnGlsUHluYHGuk+B0sNU8OQRhTKSEejejeErfCsfYO+s43ViXrg8Fb
    -----END RSA PRIVATE KEY-----
    """
    #支付寶公鑰
    alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAghJr50XjmyFrDu7t9rJhafOSgq5y/RANJ1IqferzYpzn9RJkV02nqe2gxXiXD52Ec4CRxYrQ43ghEEGcQtbWVg5u2PNVR8PFi/HVCW8fqwSYYIXQbhVN2doWS/cO8d6FdCg7+ni/XN9LYCk1SBJGLNrz1Mxz8LjLRNt7F34qa+DezaqG0jXt7wtk6Vd6amqVSvIMB80dJhdItc2WIOR/ml4rObiF/VHDhbe5dpD8iN8aCMQocVWDJq1Oe4Ihy9nUoXRWCKW3RM2Jfv/FIAHImHRT8CY12XnwjPcN/jUuQw9qB3ZDY2xBh0wbIVT6SQ802l6JWExBcb7gzAzgK5uQDQIDAQAB
    -----END PUBLIC KEY-----
    """

    alipay = AliPay(
        appid="2021000116660237",#沙箱應用appid
        app_notify_url='http://127.0.0.1:8000/home/',  # the default notify path
        app_private_key_string=app_private_key_string,
        # alipay public key, do not use your own public key!
        alipay_public_key_string=alipay_public_key_string,
        sign_type="RSA2", # RSA or RSA2
        debug=True  # False by default
    )


    alipay_url='https://openapi.alipaydev.com/gateway.do?'
    order_string = alipay.api_alipay_trade_page_pay    (
        out_trade_no="20161112www4334",#交易訂單號,每次不能一樣
        total_amount=9999,#訂單價格
        subject='韓紅版充氣娃娃',#商品名稱
        return_url="https://www.luffycity.com/free-course",#支付成功返回商戶的url
        notify_url="https://www.luffycity.com/free-course",#需要拿到返回結果的url
    )
    print(alipay_url+order_string)#打印出支付的地址
    
 #支付地址效果圖,看圖1   

圖1

支付寶二次封裝

#luffyapi\libs\ali_pay\settings.py

    import os
    #支付寶沙箱應用appid
    APPID = 2021000116660237
    #自己生成的私鑰
    APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(__file__),'pem','private_key.pem')).read()
    #沙箱公鑰
    ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(__file__),'pem','al_public_key.pem')).read()
    #祕鑰長度
    SIGN_TYPE='RSA2'
    DEBUG=True
    GATEWAY='https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
    
#luffyapi\libs\ali_pay\pay.py

	from alipay import AliPay
	from . import settings
    alipay = AliPay(
        appid=settings.APPID,
        app_notify_url=None,  #支付結果回撥地址
        app_private_key_string=settings.APP_PRIVATE_KEY_STRING,#生成的私鑰
        alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,#沙箱公鑰
        sign_type=settings.SIGN_TYPE, # RSA or RSA2#祕鑰長度
        debug=settings.DEBUG  # False by default
    )
    gateway=settings.GATEWAY#閘道器
    
#luffyapi\libs\ali_pay\__init__.py
	
    from .pay import alipay,gateway

訂單模組表建立

#新建一個app python ../../manage.py startapp order ,建立表

from django.db import models
from django.db import models
from user.models import User
from course.models import Course
# 訂單表
class Order(models.Model):
    """訂單模型"""
    status_choices = (
        (0, '未支付'),
        (1, '已支付'),
        (2, '已取消'),
        (3, '超時取消'),
    )
    pay_choices = (
        (1, '支付寶'),
        (2, '微信支付'),
    )
    subject = models.CharField(max_length=150, verbose_name="訂單標題")
    total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="訂單總價", default=0)
    out_trade_no = models.CharField(max_length=64, verbose_name="訂單號", unique=True)
    trade_no = models.CharField(max_length=64, null=True, verbose_name="流水號")  # 支付寶生成回來的
    order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="訂單狀態")
    pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
    pay_time = models.DateTimeField(null=True, verbose_name="支付時間")
    # 一個使用者可以下多個訂單,一個訂單隻屬於一個使用者,一對多的關係,關聯欄位寫在多個一方,寫在order方
    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='建立時間')
    updated_time = models.DateTimeField(auto_now=True, verbose_name='最後更新時間')
    class Meta:
        db_table = "luffy_order"
        verbose_name = "訂單記錄"
        verbose_name_plural = "訂單記錄"

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

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

# 訂單詳情表
# 訂單和詳情是一對多,關聯欄位寫在多個的一方,寫在訂單詳情表中
class OrderDetail(models.Model):
    """訂單詳情"""
    order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="訂單")
    course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.SET_NULL, db_constraint=False, verbose_name="課程",null=True)
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程原價")
    real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程實價")

    class Meta:
        db_table = "luffy_order_detail"
        verbose_name = "訂單詳情"
        verbose_name_plural = "訂單詳情"

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

支付介面

#支付介面分析
	1 生成訂單,生成支付連線,返回支付連線
    	order表和orderdetail表插入資料,重寫create方法
        生成訂單號uuid
        登入後才能做,使用jwt認證
        當前使用者是下單使用者,存到order表中
        效驗價格
	2 支付寶非同步回撥的post介面,用於驗證簽名,修改訂單狀態
    3 支付寶get回撥前端,vue元件收到,立馬向後端發一個get請求
    
    
#實際操作,規定前端資料格式
	-{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,}
    
#luffyapi\apps\order\views.py

    from rest_framework.viewsets import GenericViewSet
    from rest_framework.mixins import CreateModelMixin
    from rest_framework_jwt.authentication import JSONWebTokenAuthentication
    from rest_framework.permissions import IsAuthenticated
    from . import models,serializer
    from rest_framework.response import Response
    class PayView(GenericViewSet, CreateModelMixin):
        authentication_classes = [JSONWebTokenAuthentication, ]
        permission_classes = [IsAuthenticated, ]
        queryset = models.Order.objects.all()
        serializer_class = serializer.OrderSerializer

        # 重寫create方法
        def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data, context={'request': request})
            serializer.is_valid(raise_exception=True)
            self.perform_create(serializer)
            return Response(serializer.context.get('pay_url'))

        
#luffyapi\apps\order\serializer.py      

    from rest_framework import serializers
    from . import models
    from apps import Course
    from rest_framework.exceptions import ValidationError
    class OrderSerializer(serializers.ModelSerializer):
        # 前端傳什麼資料過來{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,}
        # user欄位需要,但是不是傳的,使用了jwt


        # 需要把course:[1,2,3] 處理成 course:[obj1,obj2,obj3]

        # 課時:[1,4,6,]===>課時:[obj1,obj4,obj6,]
        # course=serializers.CharField()
        course=serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True)

        class Meta:
            model = models.Order
            fields = ['total_amount','subject','pay_type','course']
            extra_kwargs={
                'total_amount':{'required':True},
                'pay_type': {'required': True},
            }


        def _check_price(self,attrs):
            total_amount=attrs.get('total_amount')
            course_list=attrs.get('course')
            total_price=0
            for course in course_list:
                total_price+=course.price
            if total_price!=total_amount:
                raise ValidationError('價格不合法')
            return total_amount

        def _gen_out_trade_no(self):
            import uuid
            return str(uuid.uuid4()).replace('-','')

        def _get_user(self):
            # 需要request物件(需要檢視通過context把reuqest物件傳入。重寫create方法)
            request=self.context.get('request')
            return request.user

        def _gen_pay_url(self,out_trade_no,total_amout,subject):
            # total_amout是Decimal型別,識別不了,需要轉換成float型別
            from luffyapi.libs.ali_pay import alipay,gateway
            from django.conf import settings
            order_string = alipay.api_alipay_trade_page_pay    (
                out_trade_no=out_trade_no,
                total_amount=float(total_amout),
                subject=subject,
                return_url=settings.RETURN_URL,  # get回撥,前臺地址
                notify_url=settings.NOTIFY_URL   # post回撥,後臺地址
            )
            return gateway+order_string

        def _before_create(self,attrs,user,pay_url,out_trade_no):
            attrs['user']=user
            attrs['out_trade_no']=out_trade_no

            self.context['pay_url']=pay_url
        def validate(self, attrs):
            '''
            # 1)訂單總價校驗
            # 2)生成訂單號
            # 3)支付使用者:request.user
            # 4)支付連結生成
            # 5)入庫(兩個表)的資訊準備
            '''
            # 1)訂單總價校驗
            total_amout = self._check_price(attrs)
            # 2)生成訂單號
            out_trade_no=self._gen_out_trade_no()
            # 3)支付使用者:request.user
            user=self._get_user()
            # 4)支付連結生成
            pay_url=self._gen_pay_url(out_trade_no,total_amout,attrs.get('subject'))
            # 5)入庫(兩個表)的資訊準備
            self._before_create(attrs,user,pay_url,out_trade_no)
            return attrs
        def create(self, validated_data):
            course_list=validated_data.pop('course')
            order=models.Order.objects.create(**validated_data)
            for course in course_list:
                models.OrderDetail.objects.create(order=order,course=course,price=course.price,real_price=course.price)

            return order
        
#luffyapi\apps\order\urls.py

	rom django.urls import path,re_path,include
    from . import views

    from rest_framework.routers import  SimpleRouter

    router = SimpleRouter()
    router.register('pay', views.PayView, 'pay')
    urlpatterns = [
        path('', include(router.urls)),
    ]

前後臺回撥埠配置

#luffyapi\settings\dev.py
    # 上線後要換成公網地址
    # 後臺基URL
    BASE_URL = 'http://127.0.0.1:8000'
    # 前臺基URL
    LUFFY_URL = 'http://127.0.0.1:8080'
    # 支付寶同步非同步回撥介面配置
    # 後臺非同步回撥介面
    NOTIFY_URL = BASE_URL + "/order/success/"
    # 前臺同步回撥介面,沒有 / 結尾
    RETURN_URL = LUFFY_URL + "/pay/success"

前臺生成訂單並跳轉

#src\views\FreeCourse.vue,template內修改下面這句

	<span class="buy-now" @click="buy_now(course)">立即購買</span>

#src\views\FreeCourse.vue,script的methods內新增buy_now方法
	            buy_now(course) {
                let token = this.$cookies.get('token')
                if (!token) {
                    this.$message({
                        message: "您還沒有登入,請先登入",
                    })
                    return false
                }
                this.$axios({
                        method: 'post',
                        url: this.$settings.base_url + '/order/pay/',
                        data: {
                            "total_amount": course.price,
                            "subject": course.name,
                            "pay_type": 1,
                            "course": [
                                course.id,
                            ]
                        },
                        headers: {Authorization: 'jwt ' + token}
                    }
                ).then(response => {
                    console.log(response.data)
                    let pay_url = response.data
                    //前端傳送get請求
                    open(pay_url, '_self')
                }).catch(error => {
                })

            },

前臺支付成功頁面

#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/Head"

        export default {
            name: "Success",
            data() {
                return {
                    result: {},
                };
            },
            created() {
                // url後拼接的引數:?及後面的所有引數 => ?a=1&b=2
                // console.log(location.search);

                // 解析支付寶回撥的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];
                    }

                }
                // 解析後的結果
                // console.log(this.result);


                // 把位址列上面的支付結果,再get請求轉發給後端
                this.$axios({
                    url: this.$settings.base_url + '/order/success/' + location.search,
                    method: 'get',
                }).then(response => {
                    console.log(response.data);
                }).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: 'PaySuccess',
            component: PaySuccess
    	},

支付寶回撥到前端引數

charset=utf-8&
out_trade_no=17fcf8cac17c442bbbc114df6004afff&
method=alipay.trade.page.pay.return&
total_amount=99.00&
sign=WfK2beWFKvVaTHXpREi8HqZtFRH3JbeIvkliReYvfuhAsqaxHguARKtW6jUqUdZinm7ZSaYE1NrBRQa3%2BLquMk6uMnxE0i%2FTXIu4%2FmNTCEqSUlG8fTRPwC2%2BuU4nN1Ym0eM4puzAc2TUnEJnXCGKP9UxMifN3cjqR5BP%2B3RRngZSS4IQeogjurpfdiIolLzed%2FHTWbc4HqvWlWn9JuLmFGTtKHvRRKFr1hqq8Pj%2Fe3Al8kieDN9Q7JhEdC6F5ROo9rLlmUJtevkjI22oRScrfJl5hb%2BeYosxNg3WktmYKlF5vsKeZKKnLayAvKGoySLvaWk90x0LijHzzf2%2F8a9s3w%3D%3D&
trade_no=2020072922001480160500851368&
auth_app_id=2016092000554611&
version=1.0&
app_id=2016092000554611&
sign_type=RSA2&
seller_id=2088102176466324&
timestamp=2020-07-29%2015%3A06%3A44

同步非同步回撥介面

#luffyapi\apps\order\views.py

	#支付寶回撥後端、前端get效驗訂單狀態
    from rest_framework.views import APIView
    class SuccessView(APIView):
        def get(self,request,*args,**kwargs):
            out_trade_no=request.query_params.get('out_trade_no')
            order=models.Order.objects.filter(out_trade_no=out_trade_no).first()
            if order.order_status==1:
                return Response(True)
            else:
                return Response(False)

        def post(self,request,*args,**kwargs):
            '''
            支付寶回撥介面
            '''
            from luffyapi.libs.ali_pay import alipay
            from luffyapi.utils.logger import log
            data = request.data
            out_trade_no=data.get('out_trade_no',None)
            gmt_payment=data.get('gmt_payment',None)
            signature = data.pop("sign")
            # 驗證簽名
            success = alipay.verify(data, signature)
            if success and data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
                models.Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1,pay_time=gmt_payment)
                log.info('%s訂單支付成功'%out_trade_no)
                return Response('success')
            else:
                log.info('%s訂單有問題' % out_trade_no)
                return Response('error')

#luffyapi\apps\order\urls.py,加上路由          
 
        path('success',views.SuccessView.as_view()),
            

上線操作