luffycity项目
详见:项目开发流程
1 pip3 install pymysql 国外很慢 2 pip3 install pymysql -i 地址 3 配置,以后pip3 install全走配好的源 -来到C:\Users\oldboy\AppData\Roaming %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环境中安装 -下载 pip3 install virtualenv pip3 install virtualenvwrapper-win -修改环境变量 WORKON_HOME: D:\Virtualenvs -python安装路径,scripts文件下执行virtualenvwrapper.bat 3 使用: -workon #列出有的虚拟环境(aaa) -workon aaa #使用这个虚拟环境 -mkvirtualenv -p python3 luffy # 创建虚拟环境 -rmvirtualenv 虚拟环境名字 # 删除虚拟环境 4 安装django环境 pip install django==2.2.2 5 基于虚拟环境创建项目(不要再pycharm中选择虚拟环境了)
# 在控制台直接指向项目 python manage.py runserver ---》manage.py的内容要改 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev') # 项目上线,走的不是manage.py---》uwsgi.py,需要修改 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev') # 国际化 # 创建app:startapp在哪执行,就把app创建在哪 python ../../manage.py startapp home # 注册app
详见:luffy数据库配置
# 项目依赖的数据库,luffyapi -create database luffyapi; # 创建数据库用户,并且授予luffyapi这个库的权限 -grant all privileges on luffyapi.* to 'luffyapi'@'%' identified by 'Luffy123?'; -grant all privileges on luffyapi.* to 'luffyapi'@'localhost' identified by 'Luffy123?'; -flush privileges; # 项目连接 -setting中配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'luffyapi', 'USER':'luffyapi', 'PASSWORD':'Luffy123?', 'HOST':'127.0.0.1', 'PORT':3306 } } import pymysql pymysql.install_as_MySQLdb() -用pymysql连接数据库 -django超过:2.0.7 -需要改源码,两个地方
详见:User表配置
# 用户要基于auth的user表,必须在数据库迁移命令之前操作好,后期如果再做,会出错 -把所有app下的迁移文件,全删除 from django.contrib import auth -admin,auth app下的迁移文件删除 -删除数据库(数据一定要导出来),重新迁移 (自己尝试一下)
- 卸载django 然后重新安装django # 继承AbstractUser,增加两个字段,telephone,icon(依附于pillow) # 在setting中配置 MEDIA_URL='/media/' MEDIA_ROOT=os.path.join(BASE_DIR,'media') #现在的BASEDIR是luffyapi下的luffyapi AUTH_USER_MODEL='user.user' # 路由,打开media文件夹 from django.views.static import serve from django.conf import settings re_path('media/(?P<path>.*)', serve,{'document_root':settings.MEDIA_ROOT}),
详见:路飞前台搭建
#1 安装node,官网下载,一路下一步 node -v v10.16.3 #2 安装模块 npm install 模块名 #npm比较慢,用淘宝的cnpm来替换npm #3 npm install -g cnpm --registry=https://registry.npm.taobao.org #4 创建vue的工程(需要一个vue脚手架) cnpm install -g @vue/cli #5 如果出问题执行如下,重新走3,4 npm cache clean --force #5 命令行下敲 vue 就会有提示 # 6 创建vue项目 vue create luffycity 选Manually #7 用pycharm打开 在terminal下敲:npm run serve #8 配置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>
总结
# 1 pip换源---》提高下载模块的速度() # 2 每个python项目,都应该有个requirement.txt--->项目依赖的模块 pip freeze >requirement.txt #生成 pip install -r requirement.txt -i 豆瓣源 # 下载 使用whl文件装,windows上你装的模块,都是 xxx.whl文件 -直接去网站搜:https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted -指定版本和平台下载Twisted‑20.3.0‑cp36‑cp36m‑win_amd64.whl -cmd下cd到指定的虚拟环境路径:pip3 install 文件的路径 # 离线安装 pip download -r requirement.txt -d "D:\pipDownloadTest" 切换终端在待安装文件目录下(D:\pipDownloadTest) pip install PyMySQL-0.9.3-py2.py3-none-any.whl # 3 虚拟环境 -因为机器上多个程序依赖的第三方模块不一样,版本不一样 -pip3 install virtualenv -pip3 install virtualenvwrapper-win -# 解释:windows下的批处理文件xx.bat,linux和mac上是xx.sh -配置环境变量,执行一下bat,让你配置的生效 -workon 虚拟环境 -mkvirtualenv -p python3.6 虚拟环境名称 -deactivate -rmvirtualenv 虚拟环境名称 # 4 目录结构调整 ├── luffyapi ├── logs/ # 项目运行时/开发时日志目录 - 包 ├── manage.py # 脚本文件 ├── luffyapi/ # 项目主应用,开发时的代码保存 - 包 ├── apps/ # 开发者的代码保存目录,以模块[子应用]为目录保存 - 包 ├── libs/ # 第三方类库的保存目录[第三方组件、模块] - 包 ├── settings/ # 配置目录 - 包 ├── dev.py # 项目开发时的本地配置 └── prod.py # 项目上线时的运行配置 ├── urls.py # 总路由 └── utils/ # 多个模块[子应用]的公共函数类库[自己开发的组件] └── scripts/ # 保存项目运营时的脚本文件 - 文件夹 -创建app,需要到apps目录下创建 python ../../manage.py startapp user -luffyapi--》下的luffyapi也加入到了环境变量,以后导入模块,从luffyapi--》下的luffyapi开始导入即可,pycharm会飘红,右键变成source -在setting中注册前,要把apps的路径加入到环境变量 #5 数据库配置 -创建了一个用户,授予了某个数据库的权限,给他设了一个密码 grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?'; grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123?'; -在setting中配置 #6 User表配置 -继承AbstractUser,要在执行迁移命令之前完成 # 7 路飞前台 -mvvm架构:M:script里写的东西 vm:viewmodel v:template, 数据驱动页面 -单页面开发:以后只需要写xx.vue组件:小组件和页面组件
# utils/response.py from rest_framework.response import Response class APIResponse(Response): def __init__(self,code=1,msg='成功',result=None,status=None,headers=None,content_type=None,**kwargs): dic={ 'code':code, 'msg':msg } if result: dic['result']=result dic.update(kwargs) #对象来调用对象的绑定方法,会自动传值 super().__init__(data=dic,status=status,headers=headers,content_type=content_type) # 类来调用对象的绑定方法,这个方法就是一个普通函数,有几个参数就要传几个参数 # Response.__init__(data=dic,status=status,headers=headers,content_type=content_type)
#utils/exceptions.py #方法,加日志 from rest_framework.views import exception_handler # from luffyapi.utils import response 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)
# setting/dev.py # rest_framework配置 REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'luffyapi.utils.exceptions.common_exception', } #日志的配置 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': 10, # 日志格式:详细格式 'formatter': 'verbose', # 文件内容编码 'encoding': 'utf-8' }, }, # 日志对象 'loggers': { 'django': { 'handlers': ['console', 'file'], 'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统 }, } }
#utils/logger.py import logging # log=logging.getLogger('名字') # 跟配置文件中loggers日志对象下的名字对应 log=logging.getLogger('django') # 以后其他地方需要获取日志信息,导入直接用 from luffyapi.utils.logger import log log.info('xxxxxx') # 不要写print
# xss(又叫css攻击:Cross Site Scripting):跨站脚本攻击, # cors(Cross-Origin Resource Sharing):跨域资源共享, # csrf(Cross-site request forgery):跨站请求伪造 # 1 同源策略:请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名,端口,协议相同. # 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请求,一次真正的请求 # 4 后端处理,开启cors,跨域资源共享(在中间中写) 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 #5 在setting的中间件中配置 #6使用第三方,django-cors-headers -pip install django-cors-headers -注册app:'corsheaders', -配置中间件:corsheaders.middleware.CorsMiddleware -setting中配置: CORS_ORIGIN_ALLOW_ALL = True CORS_ALLOW_METHODS = ( 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW', ) CORS_ALLOW_HEADERS = ( 'authorization', 'content-type', )
#1 前台可以发送ajax的请求,axios #2 cnpm install axios #3 配置在main.js中 import axios from 'axios' # 导入安装的axios Vue.prototype.$axios = axios; # 相当于把axios这个对象放到了vue对象中,以后用 vue对象.$axios #4 使用(某个函数中) this.$axios.get('http://127.0.0.1:8000/home/home/'). 向某个地址发送get请求 then(function (response) { 如果请求成功,返回的数据再response中 console.log(response) }).catch(function (error) { console.log(error) }) #5 es6的箭头函数 function (response) { console.log(response)} response=>{ console.log(response)}
simpleui后台管理
详见:Django后台管理之simpleui(推荐)
总结
# 1 同源策略:浏览器的安全策略,不允许不同域(ip+端口+协议) # 2 跨域资源共享,cors,本质就是在响应头部加入允许,允许某些域,允许某些头 # 3 咱的项目要处理跨域 -两种方式: -django-cors-headers:下载--》注册app——》配置中间件--》setting配置 -自己处理,就在中间件中允许域和头 # 4 跨域:简单请求和非简单请求,非简单请求发送两次,一次OPTIONS预检请求 -(1) 请求方法是以下三种方法之一: HEAD GET POST -(2)HTTP的头信息不超出以下几种字段: Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain # 5 单页面开发:小组件,页面组件 -组件:三部分,template,script,style -vue-router: const routes = [ { path: '/', name: 'Home', component: Home }, ] #6 vue跟后端交互(ajax) -axios -npm install axios -在main.js中配置 import axios from 'axios' //导入安装的axios //相当于把axios这个对象放到了vue对象中,以后用 vue对象.$axios Vue.prototype.$axios = axios; -使用:在组件中 -this.$axios.get(url).then(function(response){}).catch(function(error){}) -this.$axios.get(url).then(response=>{}).catch(error=>{})

#html中路由跳转 <router-link to="/"> <img src="../assets/img/head-logo.svg" alt=""> </router-link> #js中控制路由跳转 this.$router.push('/');

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

# 在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"] }) ] } };
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>
####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): created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间') is_delete = models.BooleanField(default=False, verbose_name='是否删除') is_show = models.BooleanField(default=True, verbose_name='是否展示') orders = models.IntegerField() class Meta: abstract=True # 一定不要忘了 ### 6 settings/const.py # 首页轮播图个数 BANNER_COUNTER=3 #### 7 settings/dev.py from .const import *

<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>
前端路由配置

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 }, ]
#1 协同开发,版本管理 #2 svn(集中式管理),git(分布式管理) #3 git装完,既有客户端,又有服务端 #4 git工作流程 -工作区,暂存区,版本库 #5 远程仓库:github,码云,公司内部(gitlab) # 6 安装:一路下一步 # 7 右键--git bash here # 8 git 命令 -初始化:git init 文件夹名 -初始化:git init #当前路径全被管理 -git status # 查看状态 -git add a.txt # 把a提交到暂存区 -git add . # 批量提交到暂存区 -git commit -m '注释,我新增了a' # 把暂存区的所有数据都提交到版本库 需先添加作者信息 -需要增加作者信息 git config --global user.email "lqz@qq.com" git config --global user.name "lqz" # 如果在全局添加则会保存到C:\>用户>xxx>.gitconfig配置文件中 git config user.email "egon@qq.com" git config user.name "egon" # 如果在局部添加则会保存到当前目录的.git文件夹下的config文件中 -把a的新增提交到版本管理 -新建b,在a中新增一行 -git checkout . # 回复到提交版本的位置,a是空的,b没有被git管理,所以,是什么样,还是什么样 -git log # 查看版本管理的日志 -git reflog # 查看日志,条数更多,内容更少 -git reset --hard 版本号 # 指定恢复到对应的版本 # 红色表示未被管理 # 绿色表示提交到暂存区了 # 忽略文件 -空文件夹不被管理 -指定某些文件或者文件夹不被git管理 -在项目根路径,跟.git文件夹一个路径,新建.gitignore.,在里面配置 - 语法: # 号是注释,没有用 文件夹名字,表示文件夹忽略,不被管理 /dist 表示根路径下的dist文件夹,不被管理 *.py 表示后缀名为py的文件,都被忽略 *.log* 表示后缀名中带log的都被忽略 # 分支操作 -查看分支 git branch 查看所有分支,分支是绿的,表示在当前分支上 -创建分支 git branch dev -创建并切换到 git checkout -b dev -删除分支 git branch -d dev -切换分支 git checkout dev -合并分支 git merge 分支名 # 把dev分支合并到master分支:切换到master分支,执行合并dev分支的命令
总结
# 1 轮播图接口 -封装一个BaseModel基类,utils/model.py下 -home的app下的models.py创建Banner表 -序列化类 -视图(继承哪个视图类) queryset对象[:3] -路由自动生成 -返回多少张,通过setting的配置实现 -在setting包下新建const.py 变量名=变量值 -在dev.py中 from .const import * # 2 前端轮播图 -elementui的跑马灯 v-for :src -axios发送请求 # 3 git -工作区,暂存区,版本库 -git init # 生成一个.git 的文件夹 -git stauts # 查看状态(红色和绿色),必须全绿再提交到版本库 -git add . # 提交到暂存区 -git commit -m '文字' # 从暂存区提交到版本库 -git checkout . # 把工作区的代码恢复到版本库中最先的代码 -git reset . # 把暂存区的东西拉回到工作区(原来是绿的变红了) -git reset --hard 版本号 # 回退到某个版本 分支 -git branch # 查看所有分支,如果分支是绿色,是再当前分支是 -git branch 分支名 # 创建分支 -git chekout dev # 切换到dev分支 -git chekout -b dev2 # 创建并切换到dev2分支 -git branch -d dev2 # 删除dev2分支,不能再这个分支上 -git merge dev # 你必须再其他分支(master),合并dev # 4 重现冲突就两种情况 -分支合并 -你和同时再同一条分支上改了相同的代码
# 1 码云(国内,快) # 2 新建仓库的时候,不要勾选Readme初始化这个仓库 否则就不是空仓库 # 3 现在什么都没有,新建仓库 mkdir lqz_test cd lqz_test git init touch a.txt git add a.txt git commit -m "first commit" git remote add origin https://gitee.com/liuqingzheng/lqz_test.git # 连接远程 git push origin master # 4 已经有了仓库 cd b git remote add origin https://gitee.com/liuqingzheng/lqz_test.git git push origin master # 5 git 远程操作命令 -git remote # 查看远程仓库(没有就看不到) -git remote add origin https://gitee.com/liuqingzheng/lqz_test.git # 跟远程仓库建立连接 -git push origin master # 把本地的master分支提交到远程的origin,需要输入用户名和密码(之前存的需要删掉)
# 1 项目创始者,如上 # 2 项目开发者,参与者(换了一台电脑),把代码拉下来继续开发 -git clone https://gitee.com/liuqingzheng/lqz_test.git -能看到完整的版本和日志控制 -可以回复到任意版本 -git rest --hard 版本号 -git checkout . # 表示回到指针指向的版本,因为已经用git rest --hard把指针移动了,所以checkout .就是当前再的版本上的东西 # 3 本地新增c.txt文件 -提交到暂存区 -提交到版本库(没有提交到远程,远程看不到) -提交到远程:git push origin master # 4 让a文件夹中的代码成为最新的 -git pull origin master # 5 重点:每次再提交代码之前,一定要先更新代码(拉),如果不拉提不上去
# 1 公司内部大部分用ssh连接 -领导给你一个git地址(项目地址) -你 git clone 地址 到本地 -改代码,改完了---》提交(禁止) -ssh配置,以后都不用输密码了 # 2 配置如何做 -对称加密(加密和解密用同一套秘密) -非对称加密(公钥和私钥),公钥加密,私钥解密 -生成一对公钥和私钥(用命令) -https://gitee.com/help/articles/4181 -ssh-keygen -t rsa -C "lqz@qq.com" 生成到用户家目录的.ssh文件夹下(一个公钥,一个私钥) -把公钥复制出来,再码云上配置 # 跟远程操作有三个命令 git pull git push -git fecth(一般不用,他跟pull是一个东西,当作不知道)
# 1 安装git # 2 再pycharm中配置,setting---》git--->git.exe的地址 # 3 git clone --->等同于下图 # 4 使用pycharm创建本地分支(见下图) # 5 拉取代码,如下图 # 6 push代码,如下图
# 1 协同开发出现冲突,如何解决 # 2 git pull origin master 拉下远程代码,同事和你修改了同一个位置,会冲突,如下 <<<<<<< HEAD 你的代码 ======= 别人代码 >>>>>>> origin/master # 3 处理方案 -删除你的代码 -删除同时代码 -合并你们的代码
# 1 分支:本地分支,远程分支 # 2 创建本地分支 -git checkout -b dev # 3 把本地分支提交到远程 -git push origin dev
# 如果看到可自动合并,表示合并后不会有冲突,正常操作即可 # 测试人员点通过,直接合并即可
# 1 切换到master分支 # 2 回滚打某个版本 git reset --hard 版本号 # 3 强制提交代码 git push origin master -f
# 1 远端创建一个dev分支 -在远端直接创建 -本地创建,提交到远端 git checkout -b dev git push origin dev # 2 本地新建一个dev_bug分支 -dev_bug分支改了文件 -dev分支改了同样的文件 -合并就出冲突 -git merge dev_bug (在dev分支上操作) -解决冲突(删你的,同事的,合并起来) -git add . git commit -正常了,冲突解决 # 3 手动线下合并代码并提交到远程 -git checkout master -git merge dev -如果出冲突,解决 -git add . -git commit -git push..
# 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 },
git rebase
总结
# 1 git远程 git remote 远程地址(https/ssh) # 2 ssh 的公钥和私钥,配在公司的git服务器上(gitlab,gitlab) # 3 git status 没有红色和绿的才能提交到远程 # 4 提交代码之前,一定要先拉代码(如果本地不是最新的,提交不上去) # 5 每天一到公司,先拉一下代码,你要提交之前,一定要拉 # 6 冲突出现的原因:1 多人同时操作同一个分支 2 分支合并的冲突 # 7 线上分支合并(了解) # 8 线下分支合并,可能会出冲突,解决,直接提交 # 9 创建远程分支:本地建完,push上去 远端直接创建 # 10 git clone 远程已经有了仓库和代码了 # 11 vue 登录页面分析

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

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

#Head.vue #template底部 <div class="right-part"> <div> <span @click="put_login">登录</span> <span class="line">|</span> <span @click="put_register">注册</span> </div> </div> <Login v-if="is_login" @close="close_login" @go="put_register"/> <Register v-if="is_register" @close="close_register" @go="put_login"/> # js <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, } }, 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; } }, created() { sessionStorage.url_path = this.$route.path; this.url_path = this.$route.path; }, components: { Login, Register } } </script>
# 1 多方式登录接口 # 2 手机号登录接口 # 3 发送验证码接口 # 4 注册接口 # 5 校验手机号是否存在接口
# 路由 from django.urls import path,include from . import views from rest_framework.routers import SimpleRouter router=SimpleRouter() router.register('',views.LoginView,'login') urlpatterns = [ path('',include(router.urls) ), ] # 视图类 from rest_framework.viewsets import ViewSet from . import serializer from luffyapi.utils.response import APIResponse from rest_framework.decorators import action class LoginView(ViewSet): @action(methods=['POST'],detail=False) def login(self,request,*args,**kwargs): ser=serializer.UserSerilaizer(data=request.data) if ser.is_valid(): token=ser.context['token'] username=ser.context['user'].username return APIResponse(token=token,username=username) else: return APIResponse(code=0,msg=ser.errors) # 序列化类 from rest_framework import serializers from . import models from rest_framework.exceptions import ValidationError class UserSerilaizer(serializers.ModelSerializer): username=serializers.CharField() class Meta: model=models.User fields=['username','password','id'] extra_kwargs={ 'id':{'read_only':True}, 'password':{'write_only':True} } def validate(self, attrs): # 多种登录方式 user=self._get_user(attrs) # 签发token token=self._get_token(user) # 放到context中,我在视图类中可以取出来 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): user=models.User.objects.filter(telephone=username).first() elif re.match('^.+@.+$',username): #邮箱 user=models.User.objects.filter(email=username).first() else: user=models.User.objects.filter(username=username).first() if user: ret=user.check_password(password) if ret: return user else: raise ValidationError('密码错误') else: raise ValidationError('用户不存在') def _get_token(self,user): from rest_framework_jwt.serializers import jwt_payload_handler,jwt_encode_handler payload=jwt_payload_handler(user) # 通过user对象获得payload token=jwt_encode_handler(payload) # 通过payload获得token return token

1 重点,cookie操作 this.$cookies.set('key',value,过期时间秒) this.$cookies.get('key') this.$cookies.remove('key') 2 # template <el-button type="primary" @click="login_password">登录</el-button> #js 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', }); } },
# Head.vue ### template <span @click="logout">注销</span> ### js logout(){ //清除cookie this.$cookies.remove('token') this.$cookies.remove('username') //把两个变量值为空 this.username='' this.token='' },
# urls.py 如果不重写视图类不需要动 # views.py class LoginView(ViewSet): @action(methods=['GET'], detail=False) def check_telephone(self,request,*args,**kwargs): import re telephone=request.query_params.get('telephone') if not re.match('^1[3-9][0-9]{9}',telephone): return APIResponse(code=0,msg='手机号不合法') try: models.User.objects.get(telephone=telephone) return APIResponse(code=1) except: return APIResponse(code=0,msg='手机号不存在')
#0 注册一个公众号() -https://mp.weixin.qq.com/ -注册订阅号,一路下一步,申请个人 -截一个图(首页) #1 腾讯云,---注册---实名:-https://console.cloud.tencent.com/smsv2 #2 https://console.cloud.tencent.com/smsv2/csms-sign/create -把图传上去,认证 #3 创建模板(审核) #4 应用管理(创建一个应用,记住appid和App Key) #5 扣代码(https://cloud.tencent.com/document/product/382/11672) # 7 帮助文档 -API:一堆web接口,基于API接口来写 -SDK:软件开发工具包软件,别人基于api接口,用不同语言封装的工具包,我们可以直接调用方法完成某些事
# lib/tx_sms #__init__.py from .send import get_code,send_message #send.py from qcloudsms_py import SmsSingleSender from luffyapi.utils.logger import log from . import settings # 生成一个四位随机验证码 def get_code(): import random s_code='' for i in range(4): s_code+=str(random.randint(0,9)) return s_code def send_message(phone,code): ssender = SmsSingleSender(settings.appid, settings.appkey) params = [code, '3'] # 当模板没有参数时,`params = []` try: result = ssender.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('手机号:%s,短信发送失败,错误为:%s'%(phone,str(e))) #settings.py # 短信的配置 # 短信应用 SDK AppID appid = 140039846 # SDK AppID 以1400开头 # 短信应用 SDK AppKey appkey = "fd972f6da15add4de47b50b8dbe930" # 短信模板ID,需要在短信控制台中申请 template_id = 66935 # NOTE: 这里的模板 ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 # 签名 sms_sign = "小猿取经" # NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台中申请 # views.py @action(methods=['GET'], detail=False) def send(self,request,*args,**kwargs): ''' 发送验证码接口 :return: ''' import re from luffyapi.libs.tx_sms import get_code,send_message from django.core.cache import cache from django.conf import settings telephone = request.query_params.get('telephone') if not re.match('^1[3-9][0-9]{9}', telephone): return APIResponse(code=0, msg='手机号不合法') code=get_code() result=send_message(telephone,code) # 验证码保存(保存到哪?) # sms_cache_%s cache.set(settings.PHONE_CACHE_KEY%telephone,code,180) if result: return APIResponse(code=1,msg='验证码发送成功') else: return APIResponse(code=0, msg='验证码发送失败')
总结
# 1 多方式登录接口,路由配置 # 2 前端登录,判断了用户名密码是否填写,弹窗 # 3 vue操作cookie -this.$cookie.set(key,value,过期时间) -this.$cookie.get(key) -this.$cookie.remove(key) # 4 子传父 //给父组件,Head传递一个事件,让它从cookie中取出token和username this.$emit('loginsuccess') //在父组件中写<Login @loginsuccess="login_success"/> # 5 腾讯短信接入 -API:接口,不同语言都可以操作 -SDK:需要有不同语言的sdk -封装成了工具包 # 6 发送短信验证码接口 # 7 校验手机号
# user/throttlings.py # 继承SimpleRateThrottle 重写get_cache_key方法 返回什么就以什么作为限制 from rest_framework.throttling import SimpleRateThrottle class SMSThrotting(SimpleRateThrottle): scope = 'sms' def get_cache_key(self, request, view): telephone = request.query_params.get('telephone') #'throttle_%(scope)s_%(ident)s'%{} return self.cache_format%{'scope':self.scope,'ident':telephone} # 配置在视图类上 from .throttlings import SMSThrotting class SendSmSView(ViewSet): throttle_classes = [SMSThrotting,] # 在settings/dev中配置 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES':{ 'sms':'1/m' # key要跟类中的scop对应 } }
# views.py @action(methods=['POST'],detail=False) def code_login(self,request,*args,**kwargs): ser = serializer.CodeUserSerilaizer(data=request.data) if ser.is_valid(): token = ser.context['token'] username = ser.context['user'].username return APIResponse(token=token, username=username) else: return APIResponse(code=0,msg=ser.errors) # serializer class CodeUserSerilaizer(serializers.ModelSerializer): code=serializers.CharField() class Meta: model = models.User fields = ['telephone', 'code'] def validate(self, attrs): 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 telephone = attrs.get('telephone') code = attrs.get('code') # 取出原来的code cache_code=cache.get(settings.PHONE_CACHE_KEY%telephone) if code ==cache_code: # 验证码通过 if re.match('^1[3-9][0-9]{9}$', telephone): user = models.User.objects.filter(telephone=telephone).first() if user: # 把使用过的验证码删除 cache.set(settings.PHONE_CACHE_KEY % telephone,'') return user else: raise ValidationError('用户不存在') else: raise ValidationError('手机号不合法') else: raise ValidationError('验证码错误') def _get_token(self, user): from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler payload = jwt_payload_handler(user) # 通过user对象获得payload token = jwt_encode_handler(payload) # 通过payload获得token return token

<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"> <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; //字符串.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>
#详细代码看上面一条,前台发送验证码
# urls.py router.register('register', views.RegisterView, 'register') # /user/register post请求就是新增 # views.py class RegisterView(GenericViewSet,CreateModelMixin): queryset = models.User.objects.all() serializer_class = serializer.UserRegisterSerilaizer def create(self, request, *args, **kwargs): response=super().create(request, *args, **kwargs) username=response.data.get('username') return APIResponse(code=1,msg='注册成功',username=username) # serializer.py class UserRegisterSerilaizer(serializers.ModelSerializer): code=serializers.CharField(max_length=4,min_length=4,write_only=True) class Meta: model = models.User fields = ['telephone', 'code','password','username'] extra_kwargs = { 'password': {'max_length': 18,'min_length':8}, 'username': {'read_only':True} } def validate(self, attrs): telephone = attrs.get('telephone') code = attrs.get('code') # 取出原来的code cache_code = cache.get(settings.PHONE_CACHE_KEY % telephone) if code == cache_code: # 验证码通过 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

# 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>
#1 key-value的存储方式,value有很多数据类型:5大:string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型 #2 内存数据库 #3 与Memcached比较: -1 Memcached只支持一种数据类型字符串 -2 Memcached不支持持久化(不支持存到硬盘上,只要一断电,数据就没了) # 4 使用Redis有哪些好处? (1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) (2) 支持丰富数据类型,支持string,list,set,sorted set,hash (3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 (4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除 # 5 单线程,单进程,不存在并发访问的问题(新版本已经不是了) -单线程为什么这么快 -数据在内存(最重要的) -io多路复用技术 -因为没有进程,线程间的切换 # 6 redis适合的场景 1 排行榜 2 网站访问量,文章访问量 3 缓存数据库(用的最多,就是做缓存) 4 发布订阅 5 去重 6 分布式(blpop) # 7 安装 -Redis-x64-3.2.100.msi 安装包 -redis-desktop-manager-0.9.3.817.exe 等同于navicate # 8 使用 -服务端和客户端 -安装完,服务端自动启动:redis-server 配置文件.conf -redis-cli :客户端连接服务端(redis-cli -h 127.0.0.1 -p 6379)
总结
# 1 频率限制类 (继承SimpleRateThrottle,scop,重写getcachekey方法:方法返回什么,就以什么作为key限制,setting中配置) # 2 手机号验证码登录,手机号和验证码 # 3 注册接口 # 4 redis:安装和启动 -5大数据类型:str:接口缓存,list:分布式,hash:缓存,set:去重,zset:排行榜 -redis-server 以某个配置文件启动 启动(windows上在服务中右键启动也可以) -redis-cli -h -p
# 1 pip3 install redis # 简单使用 from redis import Redis # conn=Redis() #连接对象 conn=Redis(host='127.0.0.1', port=6379) ret=conn.get('name') print(ret)
###### t_redis_pool.py #redis连接池 import redis #pool必须是单例的 POOL = redis.ConnectionPool(host='127.0.0.1', port=6379,max_connections=100) # 造一个池子,最多能放100个连接 #######t_redis_conn.py #redis连接池 # import redis # #pool必须是单例的 所以利用模块导入(单例) # pool = redis.ConnectionPool(host='127.0.0.1', port=6379,max_connections=100) # 造一个池子,最多能放100个连接 import redis #包内的py文件,如果想右键运行,导包的时候不能带点 from t_redis_pool import POOL # pycharm提示的错 r = redis.Redis(connection_pool=POOL) # 只要执行这一句话,就是从池中拿出一个连接 ret=r.get('name') print(ret)
#####字符串操作 ####1 set的用法 # conn.set('height',180) #基本使用 # conn.set('height','190',nx=True) # conn.set('height','190',xx=True) # conn.set('height1','190',xx=True) ''' ex,过期时间(秒) px,过期时间(毫秒) nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果 xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值 ''' ### 2 # setnx(name, value) # # 设置值,只有name不存在时,执行设置操作(添加),如果存在,不会修改 # setex(name, value, time) # # 设置值 # # 参数: # # time,过期时间(数字秒 或 timedelta对象) # psetex(name, time_ms, value) # # 设置值 # # 参数: # # time_ms,过期时间(数字毫秒 或 timedelta对象 # mset # conn.mset({'name1':'11','name3':'dasfd'}) # ret=conn.mget(['name1','name','name3']) # print(ret) # ret=conn.getset('name1', '999') # print(ret) # ret=conn.getrange('name1',0,0) # 前闭后闭区间 # print(ret) # conn.setrange('name1',1,88888) # ret=conn.getbit('name1',9) # print(ret) #incr :统计网站访问量,页面访问量,接口访问量 # conn.incr('name1') # 只要一执行,数字加1 # conn.incr('name1') # 只要一执行,数字加1 # conn.incr('name1') # 只要一执行,数字加1 # conn.incr('name1') # 只要一执行,数字加1 #decr # conn.incr('name1',-2) # conn.decr('name1',3) # conn.append('name1','oo') # conn.incr('name1') ##重点: #set :很多参数 #get #mset #mget #incr #decr #append
# hash操作 # conn.hset('hash1','name','lqz') # conn.hset('hash1','name2','lqz') # conn.hset('hash1','name','lqz444') # key不可以重复, # ret=conn.hget('hash1','name') #只能取一个 # print(ret) # conn.hmset('hash2',{'key1':'value1','key2':'value2'}) # ret=conn.hmget('hash1','name','name2') # ret=conn.hmget('hash1',['name','name2']) # print(ret) # ret=conn.hgetall('hash1') # 尽量少用 # print(ret) # ret=conn.hlen('hash1') # ret=conn.hkeys('hash1') # ret=conn.hexists('hash1','name1') # ret=conn.hdel('hash1','name') # conn.hset('hash1','name',12) # ret=conn.hincrby('hash1','name') # # print(ret) # 以后想取出hash类型内所有的数据,不建议用hgetall,建议用hscan_iter # 一次性先取一部分回来(假设有1w条,先取回100条,把这100条做成了生成器) # ret=conn.hscan_iter('hash1') # print(ret) # for i in ret: # print(i) ##重点掌握 # hset # hget #hmset #hmget # hincrby # 区分hgetall和hscan_iter
### 列表操作 # ret=conn.lpush('list1',1,2,3,4,5) # ret=conn.rpush('list1',999) # ret=conn.lpushx('list2',1) # ret=conn.lpushx('list1',888) # 必须有这个key才能放 # ret=conn.rpushx('list1',666) # 我们猜,返回总长度 # ret=conn.llen('list1') # ret=conn.linsert('list1','before','3','77777777') # ret=conn.linsert('list1','after','3','66666666') # ret=conn.lset('list1',3,'22222') #从0开始计数 # ret=conn.lset('list1',0,'11111') # ret=conn.lrem('list1',2,'5') # 从前往后删除两个5 # ret=conn.lrem('list1',-1,'5') # 从后往前删除1个5 # ret=conn.lrem('list1',0,'5') # 删除所有5 # ret=conn.lpop('list1') # ret=conn.rpop('list1') # ret=conn.lindex('list1',0) # ret=conn.lrange('list1',0,2) # 前闭后闭 # ret=conn.ltrim('list1',1,2) # 重点block,阻塞,可以写一个超时时间 # ret=conn.blpop('list1',timeout=10) # print(ret) # 自定制分批取列表的数据 # 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) # 重点 lpush lpop blpop lrange llen
# 其他操作 # conn.delete('list1') # ret=conn.delete('hash1') # ret=conn.exists('hash2') # ret=conn.keys('cache*') #查询以cache开头的所有key # ret=conn.expire('hash2',2) # ret=conn.type('name3') # ret=conn.type('test') # ret=conn.type('test') print(ret)
# redis支持事务 # 管道。实现事务 # import redis # pool = redis.ConnectionPool(host='127.0.0.1', port=6379) # # conn = redis.Redis(connection_pool=pool) # # # pipe = r.pipeline(transaction=False) # pipe = conn.pipeline(transaction=True) # pipe.multi() # pipe.set('name', 'alex') # # pipe.set('role', 'sb') # # pipe.execute() # 这句话,才真正的去执行
# 方式一(通用方式) # 方式二:django-redis -pip install django-redis -setting中配置 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", } } } -使用两种方式 1 使用cache from django.core.cache import cache cache.set('name',user) 2 直接使用conn对象 from django_redis import get_redis_connection conn = get_redis_connection('default') print(conn.hgetall('xxx'))
# 首页轮播图数据缓存到redis中 def list(self, request, *args, **kwargs): # response=super().list(request, *args, **kwargs) # 把data的数据加缓存 # 1 先去缓存拿数据 banner_list=cache.get('banner_list') if not banner_list: print('走数据库了') # 缓存中没有,去数据库拿 response = super().list(request, *args, **kwargs) # 加到缓存 cache.set('banner_list',response.data,60*60*24) return response return Response(data=banner_list)
# 1 异步任务框架,执行异步任务,执行延迟任务,执行定时任务 # 2 Celery is a project with minimal funding,
so we don’t support Microsoft Windows.
Please don’t open any issues related to that platform. # 3 使用 -pip install celery # 4 两种结构
#1 只写一个py文件,内容如下celery_task.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) #添加任务(使用这个装饰器装饰,@app.task) @app.task def add(x,y): print(x,y) return x+y
# 2启动worker # 用命令来执行 非windows 命令:celery worker -A celery_task -l info windows: pip3 install eventlet celery worker -A celery_task -l info -P eventlet # 注意:版本区别: # 命令:celery -A 模块名 worker -l info -P eventlet # 3.x及以前: # celery worker -A 模块名 -l info -P eventlet celery worker -A celery_task -l info -P eventlet # 需要切换到指定文件夹 # 4.x及以后: # celery -A 模块名 worker -l info -P eventlet celery -A celery_task worker -l info -P eventlet
# 3 添加任务 from celery_task import add # add(3,4) # 直接执行,不会被添加到broker中 ret=add.delay(5,4) #向broker中添加一个任务 print(ret)
# 4 查看任务执行结果 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('任务已经开始被执行')
#1 新建一个包,叫celery_task -celery_task -__init__.py -celery.py -task1.py -task2.py # 2 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']) # 3 task1.py from .celery import app @app.task def add(x,y): print(x,y) return x+y # 4 task2.py from .celery import app @app.task def mutile(x,y): print(x,y) return x*y # 5 添加任务(异步任务,延迟任务) from celery_task.task1 import add from celery_task.task2 import mutile # 提交异步 ret=add.delay(6,7) print(ret) # 2d4ad592-9548-4c7c-8df4-7f8583e8a1b1 # 提交延迟任务 from datetime import datetime, timedelta # 需要utc时间 eta=datetime.utcnow() + timedelta(seconds=10) ret=add.apply_async(args=(240, 50), eta=eta) print(ret) # 6获取结果同上
#1 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), } } # 2 启动worker,启动beat 这两个都必须cd到celery包所在的位置才能启动 # celery worker -A celery_task -l info -P eventlet # 3.x版本以前 celery -A celery_task worker -l info -P eventlet # 4.x版本以后 # celery beat -A celery_task -l info # 3.x版本以前 celery -A celery_task beat -l info # 4.x版本以后
celery框架django项目工作流程 1)加载django配置环境 2)创建Celery框架对象app,配置broker和backend,得到的app就是worker 3)给worker对应的app添加可处理的任务函数,用include配置给worker的app 4)完成提供的任务的定时配置app.conf.beat_schedule 5)启动celery服务,运行worker,执行任务 6)启动beat服务,运行beat,添加任务
# 重点:由于采用了django的反射机制,使用celery.py所在的celery_task包必须放置项目的根目录下
Celery配置
# 一、加载django配置环境 import os import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffyapi.settings.dev") django.setup() # 二、加载celery配置环境 from celery import Celery # broker broker = 'redis://127.0.0.1:6379/0' # backend backend = 'redis://127.0.0.1:6379/1' # worker app = Celery(broker=broker, backend=backend, include=['celery_task.tasks']) # 时区 app.conf.timezone = 'Asia/Shanghai' # 是否使用UTC app.conf.enable_utc = False # 任务的定时配置 from datetime import timedelta from celery.schedules import crontab app.conf.beat_schedule = { 'update-banner-list': { 'task': 'celery_task.tasks.update_banner_list', 'schedule': timedelta(seconds=10), 'args': (), } }
celery在django项目中的目录结构
必须放在更目录路径下(大luffyapi文件夹下)
1.加载django环境 # celery.py from celery import Celery # 加载django环境 import os import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffyapi.settings.dev") django.setup() 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.home_task',]) # 执行定时任务 # 时区 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.home_task.banner_update', 'schedule': timedelta(seconds=30), } } 2.cd到大luffyapi路径下:启动worker,启动beat 3.提交任务 # home_task.py from .celery import app @app.task def banner_update(): from home import serializer from home import models from django.conf import settings from django.core.cache import cache queryset_banner = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('display_order')[ :settings.BANNER_COUNTER] serializer_banner=serializer.BannerModelSerilaizer(instance=queryset_banner,many=True) # print(serializer_banner.data) for banner in serializer_banner.data: banner['img']='http://127.0.0.1:8000'+banner['img'] cache.set('banner_list',serializer_banner.data) # banner_list=cache.get('banner_list') # print(banner_list) return True
# 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 # 一定不要忘了 ################3 数据迁移 python manage.py makemigrations python manage.py migrate

# 不要忘了注册路由 # 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">筛 选:</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>

-- 老师表 -- 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);
# 路由 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()
''' 排序: from rest_framework.filters import OrderingFilter,SearchFilter 按id正序倒叙排序,按price正序倒叙排列 使用:http://127.0.0.1:8000/course/free/?ordering=-id 配置类: filter_backends=[OrderingFilter] 配置字段: ordering_fields=['id','price'] 内置过滤: from rest_framework.filters import OrderingFilter,SearchFilter 使用:http://127.0.0.1:8000/course/free/?search=39 按照price过滤(表自有的字段直接过滤) 配置类: filter_backends=[SearchFilter] 配置字段: search_fields=['price'] 扩展:django-filter 安装: 支持自由字段的过滤还支持外键字段的过滤 from django_filters.rest_framework import DjangoFilterBackend http://127.0.0.1:8000/course/free/?course_category=1 # 过滤分类为1 (python的所有课程) 配置类: filter_backends=[DjangoFilterBackend] 配置字段: filter_fields=['course_category'] '''
# filters.py # 自定义过滤规则 # OrderingFilter,SearchFilter都继承BaseFilterBackend # 所以我们写的类也需要继承BaseFilterBackend 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]
总结
# 1 celery定时更新首页轮播图接口(只要涉及到加入缓存的接口,一定涉及到更新---(双写一致性)),执行异步任务,定时任务(apscheduler)和延时任务 # 2 课程群查接口 -继承哪个类: -如果想改路由(自动生成路由:ViewSetMixin), -如果想跟数据库和序列化类打交道:GenericAPIView -增删查改(5个视图扩展类) -序列化类怎么写 -子序列化:teacher = TeacherSerializer() -想序列化某个没有的字段,可以写在model类种或者序列化类中 -可能会重写validata或者create或者update方法 # 3 排序(继承:GenericAPIView,ListModelMixin) -在视图类种配置 filter_backends=[OrderingFilter] ordering_fields=['id', 'price', 'students'] -使用:http://127.0.0.1:8000/course/free/?ordering=-id # 4 过滤 (继承:GenericAPIView,ListModelMixin) -在视图类种配置 filter_backends=[SearchFilter] search_fields=['id','name'] -使用:http://127.0.0.1:8000/course/free/?search=39 # 5 自定义过滤 -写一个类,继承BaseFilterBackend,重写filter_queryset方法,返回queryset对象,返回的对象就是过滤完的 -使用跟过滤配置一样 # 6 django-filter -下载,配置在视图类中filter_backends=[DjangoFilterBackend] -配置字段:filter_fields=['course_category'],支持自由字段和连表字段
# 借助django-filter实现区间过滤 # 实现区间过滤 ##########1 filters.py from django_filters import filters from django_filters import FilterSet class CourseFilterSet(FilterSet): # 课程的价格范围要大于min_price,小于max_price 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'] #####2 视图类中配置 -filter_backends=[DjangoFilterBackend] # 配置类:(自己写的类) -filter_class = CourseFilterSet

<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">筛 选:</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>

# 1 新建FreeCourseDetail.vue # 2 配置路由 { path: '/free/detail/:pk', name: 'FreeCourseDetail', component: FreeCourseDetail }, #3 安装 vue-video播放器 -cnpm install vue-video-player -在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); #4 组件 <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}}人在学 课程总时长:{{course_info.sections}}课时/{{course_info.pub_sections}}小时 难度:{{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>
#1 urls.py router.register('chapters', views.CourseChapterView, 'coursechapter') # 2 views.py class CourseChapterView(GenericViewSet,ListModelMixin): queryset = models.CourseChapter.objects.filter(is_delete=False,is_show=True) serializer_class = serializer.CourseChapterSerializer # 可以按照课程id来查 因为course属于外键字段 所以查到的是课程的id filter_backends = [DjangoFilterBackend] filter_fields = ['course'] # 3 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'] # 4 models.py class CourseSection(BaseModel): ... def section_type_name(self): return self.get_section_type_display()
# 1 注册七牛云,实名认证 https://www.qiniu.com/ # 2 登录---》管理控制台---》对象存储 # 3 新建空间----》上传视频--》拿到视频连接
# urls.py router.register('search', views.CouresSearchView, 'search') # 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']

# 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>
# 沙箱环境:https://openhome.alipay.com/platform/appDaily.htm # https://opendocs.alipay.com/open/291/106097/ 下载软件,生成秘钥(公钥私钥) # 取出公钥,配置再支付宝平台,它会生成一个支付宝公钥 # 安装alipay模块 pip install python-alipay-sdk # Alipay的sdk文档 https://github.com/fzlee/alipay/ 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----- MIIEpAIBAAKCAQEApkFnp38PSslhn4wvRarL40+WtVpan6tVT55FWzAzLfQ8PfqZE2qW7mCJy1/zM07+S5L+X0pC+F7BlT+0z4rlxraG5D23khWfHb4x+uDi0Wd94eaVB0PzQXCYYBChuORvNN5YLTspeP7rNQ2OcdpGx2KUzo9w59dVW+AweblFl7GqPkRSC0P5XbkJtwnEAd0Nc08HzJR0OWZT9HRFOA7coaude9uSwDsXmsdkSNGsGDcaGEY6aGk2Tj9Fyni9LJJcOXwGrUuGDpdcP2tYePrFUGMDml/feFR9QGUbExWh/ZNpb9lKLwOtXOhQj9slilp+YJek/rLlSgW9K1WrYpuuBwIDAQABAoIBADN1mRzKAjS2wmW84UDiDbus/cviTJyRTpWXOoZwE9dMen0AnPLakh70eJIff8pI0AMaW2upM7NmuOp2ToPSzS5FftkUlUY9NQPiw9uQUgRY0Sjj0wrtqFSAAlnxq+zrn9QwYgCWCE8wMCM6r/Vjh3bdd4u78EmCaCRI7xguFXFPB9NY9oCFiwsTFJtXPZo+DSIQjqIDnh8YtV3NzrA3Ln3DsKMAr+vnPMeljAL5US6Rt5tOQY0qa9bUi6RqQwTLADcANd49YOvlr7OLjwi5jkeJ16j1nKgpasAIg9V+rF085u+PEKkLL3WJHwmhiIxrA11bxCnm1fRdfHbyWZKY9yECgYEA0k3wS7A7rzhtpznc5ZfVGWIEQ5IahRkqVGbc6wFIXKXnypOtGKAFiRmEPwm9xRurlo8TJ1bjK4D4ljEANJhX2NnIZfK3YDdNEN6D4GnEvPFR641M8M47Z7ItvdRKre8P2/ThXhkMnq5/1y7e87xvE6/a1Tr44+hclHvQWPRrsPcCgYEAymFEy0/2xAX+si4S8pzrgLfBg3Ehjan8T6JZZNCKhR7b/IT+3/AWJimzEL6jYQMlhy7QzZbRb1ssIIzqPFcFQYjQE0n6fswJuN4+jayW9Jtjvar/zhHFjW3EODR5yEDTvS3CHqQLeG3ce+srMJSxVDJR2V9ortFeb+VPbWZ+t3ECgYEAxPYvvoNwcpuzvvGnW/RGpb4x5iL46Xz3MxMfho2t+u961jRW4oBEjvGx9OQnsmpG2vxm4Oo0WnMw3mFIIvonFDZrxGd8rQU+DTWJZ21Hz/lnUugEjmdoJacvxeEEjEAgp02CoQFu21Ls8li4gKgTk+mYVyojHjhqNLp9GELadWMCgYEAi0RSXgLCEnT5p03zdgcsPOC3ByfT6jO+0GItWCX2HNN2mRhAeIQ0CcEKW4yEy56ptZQu1jtiFlpMTH4MNse/czCd15hCC/2G9zPhIgdRvjQsd/nznLA4HTIbJH5gC8EotHeHrSRATHh1kMTtbLn2KbWTA54XYK3tad0IQoWUz9ECgYBBc+kCJsb0shaRIRP5nmeb5tZCH7Yq9DgriwcIlK74cxW9fvL94n91Y6M8vSZ6MC7vF5wmcXMj3K1of04qizKbbKxCCYeOIHhTh0dB+ualDxu5WBG/6mLWR3HVHazqRVri9qoZ/3zNDx5CRNi81VpzOPmVtuos5d9sZZaNR3lkRQ== -----END RSA PRIVATE KEY----- """ alipay_public_key_string = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwU9mMZHHlQPE9FcxVtXOXhbWCtuDZLJRVCiofdbTVmRXrx47yGniPehwcKIsqhzhEaXBG2QhpIZUL8YsCav0mkrppoRvWOytuGyxNRESo8I6DWRs0aCq6P3AuiD9kSXET4dpAuRYT/+JrMXIZTycEts6vYYNAT9QivXJoa2FmiCQBAL3HP7F36pby9VstObilxXQcoBBJwEYGf2TK6moFFZ1dkloRr5Cfk/G82DpuVfrt1gr4OuIDWtcE3MZTrvDgTqtkRuwGF76FY3+8xUCUbJs1dL5cXYN7/b3jPcXVcdKXFj4WrOQd42ofE1BJWMxBW7L3Qlxue1vy+NGx/CuKwIDAQAB -----END PUBLIC KEY----- """ alipay = AliPay( appid="2016092000554611", 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", notify_url="https://www.luffycity.com/free-course" ) print(alipay_url+order_string)
补充
# 1 全文检索引擎:elasticsearch,java封装的一个数据库,专注于大数据存储和搜索
总结
# 1 区间过滤: -自己写过滤类 -使用django-filter -在视图类中配置filter_backends=[DjangoFilterBackend] -写一个类,继承FilterSet min_price = filters.NumberFilter(field_name='price', lookup_expr='gt') max_price = filters.NumberFilter(field_name='price', lookup_expr='lt') -在视图类中配置:filter_class = CourseFilterSet # 2 章节群查 -群查接口+过滤 filter_backends = [DjangoFilterBackend] filter_fields = ['course'] # 3 为了解耦合,把搜索接口单独写的 # 4 支付宝支付
al_pay -pem -__init__.py -pay.py -setting.py #__init__.py from .pay import alipay,gateway #pay.py from alipay import AliPay from . import setting alipay = AliPay( appid=setting.APPID, app_notify_url=None, # the default notify path app_private_key_string=setting.APP_PRIVATE_KEY_STRING, # alipay public key, do not use your own public key! alipay_public_key_string=setting.ALIPAY_PUBLIC_KEY_STRING, sign_type=setting.SIGN_TYPE, # RSA or RSA2 debug=setting.DEBUG # False by default ) gateway=setting.GATEWAY # setting.py import os APPID="2016092000554611" 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?'
# 订单表分析 -订单表 -订单详情 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表中 -下了三个课程,总价格100,前端提交的价格是99,异常处理 ''' #1)订单总价校验 # 2)生成订单号 # 3)支付用户:request.user # 4)支付链接生成 # 5)入库(两个表)的信息准备 ''' # 2 支付宝异步回调的post接口(验证签名,修改订单状态) # 3 当支付宝get回调前端,vue组件一创建,立马向后端发一个请求(get)
# 1 前端传什么格式数据 -{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,} # 2 后端接到数据要校验 -course:[1,2,3]===》course:[obj1,obj2,obj3] -在序列化类中: course=serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True) # 3 在序列化类的validate中处理一堆逻辑 ''' # 1)订单总价校验: -一个个课程价格取出来累加看是否等于传入的总价格 # 2)生成订单号 -通过uuid生成 # 3)支付用户:request.user -通过视图和序列化类之间的桥梁context对象传递 -重新视图类中的create方法,把request对象放入context -self.context.get('request').user # 4)支付链接生成 -导入封装的支付宝支付,生成 order_string = alipay.api_alipay_trade_page_pay ( out_trade_no=out_trade_no, total_amount=total_amout, subject=subject, return_url=settings.RETURN_URL, # get回调,前台地址 notify_url=settings.NOTIFY_URL # post回调,后台地址 ) # 5)入库(两个表)的信息准备 -把user放入attrs中 -把订单号,放入attrs中 attrs['user']=user attrs['out_trade_no']=out_trade_no self.context['pay_url']=pay_url ''' # 4 重写序列化类的create方法 -把course_list弹出来 -order=models.Order.objects.create(**validated_data)
# urls.py router = SimpleRouter() router.register('pay', views.PayView, 'pay') urlpatterns = [ path('', include(router.urls)), ] # views.py 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')) # serializer.py 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.al_pay import alipay,gateway 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): # 将user和订单号存到attrs中 后面直接存入数据库 attrs['user']=user attrs['out_trade_no']=out_trade_no # 这里需要通过context将支付链接传给视图类 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
# 后端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"

# FreeCourse.vue <span class="buy-now" @click="buy_now(course)">立即购买</span> # script 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 => { }) },

# 路由 { path: '/pay/success', name: 'PaySuccess', component: PaySuccess }, #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>
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
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.al_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')
# 前端执行 npm run build 把你写的vue代码编译成html,css,js # 后端代码 -修改setting中的pro.py -项目根路径新建一个manage_pro.py(把原来的manage.py复制改动上线的配置文件) -wsgi.py:改成线上的dev
总结:
# 1 支付宝二次封装 # 2 订单表设计 order orderdetail # 3 支付宝支付接口(前端传递什么数据,后端返回什么数据) # 4 前端get回调 # 5 支付宝post回调
上线
# 1 写好的项目,在本地跑,别问访问不到,没有公网ip # 2 远程连接阿里云的服务器47.103.156.13(xshell,finalShell) # 3 ssh连接 # 4 配置前后端项目 # 5 编译前端项目,要把前端项目传到服务器(可以用软件) cd 到 dist文件夹所在的路径 scp -r dist root@47.103.156.13:~ # 6
1)前往用户根目录 >: cd ~ 2)下载redis-5.0.5 >: wget http://download.redis.io/releases/redis-5.0.5.tar.gz >: scp -r C:\Users\dell\Desktop\pkg\redis-5.0.5.tar.gz root@39.99.192.127:~ 3)解压安装包 >: tar -xf redis-5.0.5.tar.gz 4)进入目标文件 >: cd redis-5.0.5 5)编译环境 >: make 6)复制环境到指定路径完成安装 >: cp -r ~/redis-5.0.5 /usr/local/redis 9)建立软连接 >: ln -s /usr/local/redis/src/redis-server /usr/bin/redis-server >: ln -s /usr/local/redis/src/redis-cli /usr/bin/redis-cli 10)后台运行redis >: cd /usr/local/redis >: redis-server & ctrl + c 停止 11)测试redis环境 >: redis-cli ctrl + c 12)关闭redis服务 >: pkill -f redis -9
1)前往用户根目录 >: cd ~ 2)下载mysql57 >: wget http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm 也可以本地上传,这条命令要在本地终端上执行 >: scp -r C:mysql57-community-release-el7-10.noarch.rpm root@39.99.192.127:~ 3)安装mysql57 >: yum -y install mysql57-community-release-el7-10.noarch.rpm >: yum -y install mysql-community-server 4)启动mysql57并查看启动状态 >: systemctl start mysqld.service >: systemctl status mysqld.service 5)查看默认密码并登录 >: grep "password" /var/log/mysqld.log >: mysql -uroot -p 6)修改密码 >: ALTER USER 'root'@'localhost' IDENTIFIED BY 'new password';
1)前往用户根目录 >: cd ~ 2)下载 或 上传 Python3.6.7 # 服务器终端 >: wget https://www.python.org/ftp/python/3.6.7/Python-3.6.7.tar.xz # 本地终端,给服务器上传 >: scp -r 本地Python-3.6.7.tar.xz ssh root@39.99.192.127:服务器路径 >: scp -r C:\Users\dell\Desktop\pkg\Python-3.6.7.tar.xz ssh root@39.99.192.127~ 3)解压安装包 >: tar -xf Python-3.6.7.tar.xz 4)进入目标文件 >: cd Python-3.6.7 5)配置安装路径:/usr/local/python3 >: ./configure --prefix=/usr/local/python3 6)编译并安装 >: make && sudo make install 7)建立软连接:终端命令 python3,pip3 >: ln -s /usr/local/python3/bin/python3.6 /usr/bin/python3 >: ln -s /usr/local/python3/bin/pip3.6 /usr/bin/pip3 8)删除安装包与文件: >: rm -rf Python-3.6.7 >: rm -rf Python-3.6.7.tar.xz
1)前往用户根目录 >: cd ~ 2)下载nginx1.13.7 >: wget http://nginx.org/download/nginx-1.13.7.tar.gz 3)解压安装包 >: tar -xf nginx-1.13.7.tar.gz 4)进入目标文件 >: cd nginx-1.13.7 5)配置安装路径:/usr/local/nginx >: ./configure --prefix=/usr/local/nginx 6)编译并安装 >: make && sudo make install 7)建立软连接:终端命令 nginx >: ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx 8)删除安装包与文件: >: cd ~ >: rm -rf nginx-1.13.7 >: rm -rf nginx-1.13.7.tar.xz 9)测试Nginx环境,服务器运行nginx,本地访问服务器ip >: nginx >: 服务器绑定的域名 或 ip:80 #Nginx命令 1)启动 >: nginx 2)关闭nginx >: nginx -s stop 3)重启nginx >: nginx -s reload 4)查看端口,强行关闭 >: ps -aux|grep nginx >: kill <pid:进程编号> # Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 Apache和Nginx最核心的区别在于 apache 是同步多进程模型,一个连接对应一个进程;而 nginx 是异步的,多个连接(万级别)可以对应一个进程 http请求转发 反向代理服务器 负载均衡 动静分离
# 1 mv ~/dist /home/html # 2 cd /usr/local/nginx/conf # 3 vim nginx.conf events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; server { listen 80; server_name 127.0.0.1; # 改为自己的域名,没域名修改为127.0.0.1:80 charset utf-8; location / { root /home/html; # html访问路径 index index.html; # html文件名称 try_files $uri $uri/ /index.html; # 解决单页面应用刷新404问题 } } } esc :wq # 保存并推出 # 重启nginx nginx -s reload # 前端项目就有了
# 1 mkdir /home/project # 2 cd /home/project # 3 git clone xxxx # 4 安装虚拟环境,创建虚拟环境,安装项目依赖 pip install -r requirements.txt (在指定的目录下) # 5 pip install uwsgi (虚拟和真实环境都需要安装) # 6 进行uwsgi服务配置,内容如下 vim /home/project/luffyapi/luffyapi.xml <uwsgi> <socket>127.0.0.1:8808</socket> <!-- 内部端口,自定义 --> <chdir>/home/project/luffyapi/</chdir> <!-- 项目路径 --> <module>luffyapi.wsgi</module> <!-- luffyapi为wsgi.py所在目录名--> <processes>4</processes> <!-- 进程数 --> <daemonize>uwsgi.log</daemonize> <!-- 日志文件 --> </uwsgi> # 7 去向Nginx配置目录,备份配置,完全更新配置:填入下方内容 >: vim /usr/local/nginx/conf/nginx.conf events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; server { listen 80; server_name 127.0.0.1; # 改为自己的域名,没域名修改为127.0.0.1:80 charset utf-8; location / { root /home/html; # html访问路径 index index.html; # html文件名称 try_files $uri $uri/ /index.html; # 解决单页面应用刷新404问题 } } # 新增的server server { listen 8000; server_name 127.0.0.1; # 改为自己的域名,没域名修改为127.0.0.1:80 charset utf-8; location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8808; # 端口要和uwsgi里配置的一样 uwsgi_param UWSGI_SCRIPT luffyapi.wsgi; #wsgi.py所在的目录名+.wsgi uwsgi_param UWSGI_CHDIR /home/project/luffyapi/; # 项目路径 } } } #9 mysql的root用户,登录,创建表luffyapi,创建用户,授权 1)管理员连接数据库 >: mysql -uroot -p 2)创建数据库 >: create database luffyapi default charset=utf8; 3)设置权限账号密码:账号密码要与项目中配置的一致 >: grant all privileges on luffyapi.* to 'luffyapi'@'%' identified by 'Luffy123?'; >: grant all privileges on luffyapi.* to 'luffyapi'@'localhost' identified by 'Luffy123?'; >: flush privileges; 4)退出mysql >: quit; # 10 数据迁移创建超级用户 python manage_pro.py makemigraions python manage_pro.py migrate python manage_pro.py createsuperuser #11 启动uwsgi和nginx(后端项目就可以了) 1)启动uwsgi >: uwsgi -x /home/project/luffyapi/luffyapi.xml 2)重启nginx >: nginx -s reload
1 gitlab,zabbix(监控,php写),jekins(java写的,持续集成工具),普罗尼修斯(go写的监控),
docker,k8s,ansiable(python写的自动化运维工具),salstack(Python,自动化运维工具)
2 devops
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具