luffy项目
Luffy后台
环境
"""
为luffy项目创建一个虚拟环境
>: mkvirtualenv luffy
"""
"""
按照基础环境依赖
>: pip install django==2.2.2
>: pip install djangorestframework
>: pip install pymysql
"""
创建项目
"""
前提:在目标目录新建luffy文件夹
>: cd 建立的luffy文件夹
>: django-admin startproject luffyapi
开发:用pycharm打开项目,并选择提前备好的虚拟环境
"""
重构项目目录
"""
├── luffyapi
├── logs/ # 项目运行时/开发时日志目录 - 包
├── manage.py # 脚本文件
├── luffyapi/ # 项目主应用,开发时的代码保存 - 包
├── apps/ # 开发者的代码保存目录,以模块[子应用]为目录保存 - 包
├── libs/ # 第三方类库的保存目录[第三方组件、模块] - 包
├── settings/ # 配置目录 - 包
├── dev.py # 项目开发时的本地配置
└── prod.py # 项目上线时的运行配置
├── urls.py # 总路由
└── utils/ # 多个模块[子应用]的公共函数类库[自己开发的组件]
└── scripts/ # 保存项目运营时的脚本文件 - 文件夹
"""
配置开发环境
# 环境变量操作:小luffyapiBASE_DIR与apps文件夹都要添加到环境变量
import sys
sys.path.insert(0, BASE_DIR)
APPS_DIR = os.path.join(BASE_DIR, 'apps')
sys.path.insert(1, APPS_DIR)
"""
1.修改 wsgi.py 与 manage.py 两个文件:
# manage.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev')
# wsgi.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.prod')
# manage_prod.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.prod')
2.将settings.py删除或改名,内容拷贝到settings/dev.py中
3.修改dev.py文件内容
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
4.修改启动配置:见插图
5.在任何一个__init__.py文件中测试默认配置文件是否是dev.py文件
from django.conf import settings
print(settings)
"""
Luffy后台配置
在Django中使用日志
1 复制日志配置的字典到dev.py中
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
},
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
# 实际开发建议使用WARNING
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': {
# 实际开发建议使用ERROR
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
# 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"),
# 日志文件的最大值,这里我们设置300M
'maxBytes': 300 * 1024 * 1024,
# 日志文件的数量,设置最大日志数量为10
'backupCount': 10,
# 日志格式:详细格式
'formatter': 'verbose',
# 文件内容编码
'encoding': 'utf-8'
},
},
# 日志对象
'loggers': {
'django': {
'handlers': ['console', 'file'],
'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统
},
}
}
2 新建一个utils/loggins.py,写入
import logging
def get_logger():
logger = logging.getLogger('django')
return logger
封装项目异常处理
1 写一个函数 utils/excepitons.py
from rest_framework.views import exception_handler
from luffy_api.utils.response import Response
from luffy_api.utils.loggins import get_logger
logger = get_logger()
def common_exception_handler(exc, context):
# print(str(context.get('view')))
# print(context.get('request').Meta.get('REMOTE_ADDR'))
# 只要出异常,就要记录日志
logger.error('ip为%s的用户,访问%s视图类出错,错误信息是%s' % (
context.get('request').META.get('REMOTE_ADDR'), str(context.get('view')), str(exc)))
response = exception_handler(exc, context)
if response: # drf的异常
return Response({'code': 999, 'msg': response.data.get('detail', '服务器异常,请联系系统管理员')})
else: # django的异常
return Response({'code': 999, 'msg': '服务器异常,请联系系统管理员'})
2 配置文件中配置
REST_FRAMEWORK={
'EXCEPTION_HANDLER': 'luffy_api.utils.exceptions.common_exception_handler', # 只要出异常,触发这个函数执行
}
封装Response对象
1 在utils下新建response.py
2 写入
from rest_framework.response import Response
class APIResponse(Response):
def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs):
res_data = {
'code': code,
'msg': msg,
}
if kwargs:
res_data.update(kwargs)
super().__init__(data=res_data, status=status, headers=headers)
3 使用
from luffy_api.utils.response import APIResponse
class HomeView(APIView):
def get(self,request):
return APIResponse()
数据库配置
1 创建路飞数据库:luffy(使用命令,使用图形化界面,指定编码utf8)
2 创建一个luffy用户,授予权限,对luffy库的增删查改权限
-查看有哪些用户
select user,host,authentication_string from mysql.user;
-创建用户(lqz)
#授权账号命令:grant 权限(create, update) on 库.表 to '账号'@'host' identified by '密码'
grant all privileges on luffy.* to 'cjt'@'%' identified by 'jerry123';
grant all privileges on luffy.* to 'cjt'@'localhost' identified by 'jerry123?';
flush privileges;
# mysql -h 192.168.11.47 -P 3306 -ulqz -p
3 使用lqz用户登录(只能看到路飞库,操作路飞库)
mysql -ulqz -pLqz123?
4 django 项目操作mysql
-如果使用pymysql---》配置如下两句话---》自从2.0.7版本以后,需要改源码
import pymysql
pymysql.install_as_MySQLdb()
-如果使用mysqlclient----》不需要任何操作
-mysqlclient:有的情况下,安装不上
前台项目创建
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; /* 合并边框 */
}
settings.js
export default {
base_url: 'http://127.0.0.1:8000'
}
1 vue create luffycity # 把项目创建,打开
2 在assets新建css和js,分别放入
global.css
settings.js
3 全局使用css和js,在main.js中
import '@/assets/css/global.css'
import settings from './assets/js/settings'
Vue.prototype.$settings = settings
4 安装第三方依赖
-axios
-cnpm install axios -S
-配置main.js
import axios from 'axios'
Vue.prototype.$http = axios
-vue-cookies
-cnpm install vue-cookies -S
-配置main.js
import cookies from 'vue-cookies'
Vue.prototype.$cookies = cookies
-elementui
-npm i element-ui -S
-配置main.js
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
-bootstrap
-cnpm install jquery -S
- cnpm install bootstrap@3 -S
-配置main.js
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
-配置,在项目根路径新建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"]
})
]
}
};
扩写user表
1 切换到apps路径下,新建user app
python ../../manage.py startapp user
2 在models.py中写入
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
mobile = models.CharField(max_length=11, unique=True)
icon = models.ImageField(upload_to='icon', default='icon/default.png') # 必须安装pillow
class Meta:
db_table = 'luffy_user' # 表明
verbose_name = '用户表' # 后台admin中显示的名字
verbose_name_plural = verbose_name
def __str__(self):
return self.username
3 在配置文件中写入
AUTH_USER_MODEL='user.user'
4 media配置,在配置文件中写入
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
5 配置media路由
re_path('^media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}) # 配置media的访问路径
# path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}) # 配置media的访问路径
6 执行迁移命令(安装pillow模块)
#### 国际化相关,配置文件中设置
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
主页后台模块设计
- 轮播图表(建立基表)
- 轮播图接口
在utils下新建models.py
from django.db import models
class BaseModel(models.Model):
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
is_delete = models.BooleanField(default=False, verbose_name='是否删除')
is_show = models.BooleanField(default=True, verbose_name='是否上架')
orders = models.IntegerField(verbose_name='优先级')
class Meta:
abstract = True # 不在数据库生成该表,只用来做继承
在home app下models.py新建表
# 轮播图表,继承BaseModel
from luffy_api.utils.model import BaseModel
class Banner(BaseModel):
title = models.CharField(max_length=16, unique=True, verbose_name='名称')
image = models.ImageField(upload_to='banner', verbose_name='图片',help_text='必须传入图片大小为 100*200')
link = models.CharField(max_length=64, verbose_name='跳转链接')
info = models.TextField(verbose_name='详情') # 也可以用详情表,宽高出处
class Meta:
db_table='luffy_banner'
verbose_name = '轮播图' # 后台admin中显示的名字
verbose_name_plural = verbose_name
def __str__(self):
return self.title
视图
class BannerView(Common_list_view):
queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = BannerSerializer
class Common_list_view(ViewSetMixin,ListAPIView):
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
return APIResponse(result = response.data)
序列化器
class BannerSerializer(serializers.ModelSerializer):
class Meta:
model = Banner
fields = ['title', 'image', 'link']
路由
# 路由分发
# 总路由:
from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
from django.conf import settings
urlpatterns = [
path('admin/', admin.site.urls),
path('api/home/', include('home.urls')),
path('api/user/', include('user.urls')),
re_path('^media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}) # 配置media的访问路径
# path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}) # 配置media的访问路径
]
# home.urls.py
router = SimpleRouter()
router.register('banner', views.BannerView, 'banner')
urlpatterns = [
path('', include(router.urls))
]
后台管理
- simple-ui
# 使用simple-ui的步骤
-下载:pip3 install django-simpleui
-在配置文件中app注册
INSTALLED_APPS = [
'simpleui',
]
-使用,跟之前使用admin一样
-在admin.py中注册表
from .models import Banner
@admin.register(Banner)
class BannerAdmin(admin.ModelAdmin):
# 展示那些字段
list_display = ('id', 'title','link', 'created_time')
跨域问题详解
- 同源策略
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现
请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名,端口,协议相同.
比如:我在本地上的域名是127.0.0.1:8000,请求另外一个域名:127.0.0.1:8001一段数据
浏览器上就会报错,这个就是同源策略的保护,如果浏览器对javascript没有同源策略的保护,那么一些重要的机密网站将会很危险
- cors(跨域资源共享)
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能
实现CORS通信的关键是服务器。只要服务器实现了CORS,就可以跨源通信
cors:分两种
(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
# 就是简单请求,否则就是非简单请求
# 简单请求,发送一次请求;非简单请求,发送两次,第一次是options请求,如果允许跨越,再发送真实请求
# 后端解决方式:
1 自己处理跨域问题,需要在中间件里重写response方法
2 使用第三方插件(django-cors-headers)
-安装:pip install django-cors-headers
-在app中注册
INSTALLED_APPS = [
...
'corsheaders',
...
]
-在中间件里注册
MIDDLEWARE = [
...,
'corsheaders.middleware.CorsMiddleware',
...,
]
-配置文件中配置
# 允许所有的域
CORS_ALLOW_ALL_ORIGINS=True
# 允许的请求头
CORS_ALLOW_HEADERS = (
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
# 额外允许的请求头
)
# 允许的请求方式(默认就是这样)
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
前台首页设计
Header组件
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
</li>
</ul>
<div class="right-part">
<div>
<span>登录</span>
<span class="line">|</span>
<span>注册</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
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>
Footer组件
<template>
<div class="footer">
<ul>
<li>关于我们</li>
<li>联系我们</li>
<li>商务合作</li>
<li>帮助中心</li>
<li>意见反馈</li>
<li>新手指南</li>
</ul>
<p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p>
</div>
</template>
<script>
export default {
name: "Footer"
}
</script>
<style scoped>
.footer {
width: 100%;
height: 128px;
background: #25292e;
color: #fff;
}
.footer ul {
margin: 0 auto 16px;
padding-top: 38px;
width: 810px;
}
.footer ul li {
float: left;
width: 112px;
margin: 0 10px;
text-align: center;
font-size: 14px;
}
.footer ul::after {
content: "";
display: block;
clear: both;
}
.footer p {
text-align: center;
font-size: 12px;
}
</style>
Banner组件
<template>
<div class="banner">
<el-carousel height="400px">
<el-carousel-item v-for="item in banner_list" :key="item.id">
<img :src="item.image" alt="">
</el-carousel-item>
</el-carousel>
</div>
</template>
<script>
export default {
name: "Banner",
data(){
return {
banner_list:[]
}
},
mounted() {
this.$http.get(this.$settings.base_url+'/api/home/banner/').then(res=>{
if(res.data.code==100){
this.banner_list=res.data.result
}
})
}
}
</script>
<style scoped>
.el-carousel__item {
height: 400px;
min-width: 1200px;
}
.el-carousel__item img {
height: 400px;
margin-left: calc(50% - 1920px / 2);
}
</style>
Home组件
<template>
<div class="home">
<Header></Header>
<Banner></Banner>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<Footer/>
</div>
</template>
<script>
import Footer from '../components/Footer'
import Header from '../components/Header'
import Banner from '../components/Banner'
export default {
name: 'Home',
components: {
Footer, Header,Banner
},
data() {
return {}
},
mounted() {
},
methods: {},
}
</script>
自定义配置
# 额外创建一个配置文件(这里是banner_settings),后期修改该配置文件中的配置,可以直接在Django的配置文件中导入使用
# 在setting文件夹下新建common_setting
# 写入
BANNER_COUNT=1
# 在dev.py中,写入
from .common_settings import *
# 在views中用切片获取指定数量的xx
class BannerView(Comment_banner):
queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
serializer_class = BannerSerializer
补充
- 前端存储数据的地方
1 存到cookie中:到了过期时间,失效
2 存到sessionStorage中:在当前页面中有效,关闭浏览器无效
3 存到localStorage中:永久有效,除非我们删除
- vue-router路由跳转的方式
方式1(html中)
<router-link to="/about"></router-link>
方式2(js中)
this.$router.push('/about')
git
介绍与安装
0 git,github,gitee,gitlab
-git:版本管理软件
-github:网站,全球开源代码,放在上面,我们可以看到,并下载--》本质是一个git的远程仓库
-gitee:中国版的github,公有,私有(中小型公司的代码放在私有仓库中)
-gitlab:公司内部的(公司内部的github)
1 git:软件,实现 版本控制:协同开发(多人合作开发),合并代码
2 下载:https://git-scm.com/download/win
一路下一步
3 装完后,任意位置,鼠标右键,多出
git gui here:图形化界面
git bash here:命令行
4 git和svn
-git的版本管理不需要服务端(远程仓库)
-svn要进行版本管理,必须有服务端
常用命令
# 分为三个地方
1 工作区
2 暂存区
3 版本库
# 命令
git init # 初始化仓库,在当前路径下生成一个.git文件夹,隐藏的
git add . # 把工作区的更该(.表示当前路径下所有更改,都提交),提交到暂存区
git status # 查看文件状态(绿色表示,在暂存区,不在版本库,红色表示工作区更改了,没有提交到暂存区)
git reset . # 把提交到暂存区的,拉回到工作区(以后基本不用)
git commit -m '注释' # 把暂存区的所有内容,提交到版本库
#在提交到版本库之前,要先配置用户信息 (--global:表示全局,在用户家路径.gitconfig)
git config --global user.email "306334678@qq.com"
git config --global user.name "liuqingzheng"
# 局部的用户信息,是在仓库的.git文件夹下的config中
git config user.email "33@qq.com"
git config user.name "egon"
git log # 查看版本提交的日志(谁提交的,提交时间,提交的版本号)
git reflog # 查看版本提交的日志
git reset --hard 74c071f922 #工作区的内容回退到 74c071f922 版本
# 空文件夹,不会被git管理,python中包会:包有 __init__.py
git过滤文件
在仓库目录下新建.gitignore 文件,在文件中写入要忽略的文件,文件夹,模糊匹配
## 注意:
'''
文件或文件夹名:代表所有目录下的同名文件或文件夹都被过滤
/文件或文件夹名:代表仓库根目录下的文件或文件夹被过滤
eg:
a.txt:项目中所有a.txt文件和文件夹都会被过滤
/a.txt:项目中只有根目录下a.txt文件和文件夹会被过滤
/b/a.txt:项目中只有根目录下的b文件夹下的a.txt文件和文件夹会被过滤
*x*:名字中有一个x的都会被过滤(*代表0~n个任意字符)
空文件夹不会被提交,空包会被提交,包可以被提交(包中有一个init空文件)
eg:
.idea
scripts
*.pyc
__pycache__
'''
pycharm软件
-绿色:在暂存区,没有提交到版本库
-红色:在工作区新增,没有提交到暂存区
-蓝色:在工作区修改的,没有提交到暂存区
-黄色:忽略的
-白色:已经被git管理的
git分支操作
#创建分支
git branch 分支名
git checkout -b 分支名 #创建并切换到新分支
#切换分支
git checkout dev
#查看分支
git branch
#删除分支
git branch -d 分支名
#合并分支
git merge 分支名 #把dev分支合并到master分支:切换到master分支,执行合并dev分支的命令
# 分支合并可能会出冲突
git远程仓库
- GitHub
- gitee
- gitlab
1 在远程仓库中新建空仓库
2 git init # 初始化,让项目被git管理
git add . # 将所有项目提交到暂存区
git commit -m '注释' # 将暂存区的项目提交到本地版本库
git remote add 名字 远程仓库地址 # 增加一个远程仓库(指定目标远程仓库)
git push 远程仓库名字 分支名字 # 把本地版本库内容,提交到远程仓库(需要输入账户密码/用ssh连接)
3 远程仓库操作
增加
git remote add 名字 远程仓库地址
查看
git remote
删除
git remote remove origin
提交到远程仓库
git push origin master
采用ssh协议连接远程仓库
1 https方式:用户名+密码认证方式
2 ssh方式:公钥私钥方式
本地生成公钥私钥
ssh-keygen -t rsa -C "1965617308@qq.com"
生成的公钥私钥在用户家路径的.ssh文件夹下
id_rsa # 私钥
id_rsa.pub # 公钥
把'公钥'配置到远程仓库上
以后再提交代码就不用输入用户名密码
ps:重新配置远程仓库
git remote add origin git@gitee.com:chi-jintao/luffy_api.git
git push origin master # 选yes
项目创始者与项目开发者
- 开发者
需要把代码clone到本地
1 git clone https://gitee.com/liuqingzheng/luffy_api.git
2 修改代码
3 提交到远程
4 git push origin master
- 创始者
需要远程新建仓库,本地代码push上去
协同开发
1 管理员、开发者(10开发者以上需要充值~)
能提交代码(增删改查)
2 有一个开发者提交了代码
3 另一个开发者,在提交之前,必须先拉取代码(让本地保持最新) (有事没事pull一下)
4 冲突解决(多人在同一个分支开发,导致冲突)
"""
<<<<<<< HEAD # 冲突的开始
# 自己的代码
======= # 分割线
# 别人的代码
>>>>>>> b63c408abd05b87fc492d40523240561999dba50 # 冲突的结束(版本)
"""
# 解决
1 该同样的功能出现的冲突,保留好代码,删掉其中一个即可
2 改不同功能出现冲突,两者都留
# 有事没事pull一下代码
线上分支合并
1 本地和远端分支
本地建立分支,同步到远端
git branch dev
git push origin dev
远端建立分支,同步到本地
远端建立分支
git pull origin dev
git branch # 看不到
git checkout dev # 切到dev分支,本地就能看到了
2 提一个pr:pull request(分支合并的请求)
3 审核通过、测试通过后,合并分支/扁平化合并
git 变基(git rebase) ---> 扁平化合并(不保留分支的各个提交的小版本)(结果好看,但有冲突比较麻烦)
合并分支 合并分支(保留分支的各个提交的小版本)(结果乱,有冲突比较好解决)
4 远端:dev分支代码和master分支代码完全一致
本地分支合并推送到远程
1 由于dev分支和master分支合并时,有冲突,就不能自动合并了,我们需要本地合并完,解决冲突,再提交
2 把dev合并到master
git checkout master
git merge dev
有冲突,解决冲突
再提交
3 本地的分支提交到远端分支(如果名字不一样)
git push origin dev111:dev222 # 不要作死
ps:
1 git操作出没出冲突,如何解决
-第一:多人在同一分支开发
-第二:分支合并出冲突
2 git 变基
3 git fetch 和 git pull的区别
4 git add . 与git add xxx
前端登录、注册组件
模态框登录组件
<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>
模态框注册组件
<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>
导航条
<template>
<div class="nav">
<span @click="put_login">登录</span>
<span @click="put_register">注册</span>
<Login v-if="is_login" @close="close_login" @go="put_register" />
<Register v-if="is_register" @close="close_register" @go="put_login" />
</div>
</template>
<script>
import Login from "./Login";
import Register from "./Register";
export default {
name: "Nav",
data() {
return {
is_login: false,
is_register: false,
}
},
methods: {
put_login() {
this.is_login = true;
this.is_register = false;
},
put_register() {
this.is_login = false;
this.is_register = true;
},
close_login() {
this.is_login = false;
},
close_register() {
this.is_register = false;
}
},
components: {
Login,
Register
}
}
</script>
<style scoped>
</style>
多方式登录接口
序列化类
class UserModelSerializer(serializers.ModelSerializer):
username = serializers.CharField()
class Meta:
model = User
fields = ['username', 'password']
def validate(self, attrs):
user = self._get_user(attrs)
token = self._get_token(user)
self.context['token'] = token
return attrs
def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
if re.match('^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$', username):
user = User.objects.filter(mobile=username).first()
elif re.match('^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()
if user:
if user.check_password(password):
return user
else:
raise ValidationError({'detail':'密码错误'})
else:
raise ValidationError({'detail':'用户不存在'})
def _get_token(self, user):
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
视图类
class Userinfo(ViewSet):
@action(methods=['POST'], detail=False)
def login(self, request, *args, **kwargs):
ser = UserModelSerializer(data=request.data)
ser.is_valid(raise_exception=True)
token = ser.context.get('token')
return APIResponse(token=token, data=ser.data)
校验手机号是否存在接口
@action(methods=['GET'], detail=False)
def check_mobile(self,request):
mobile=request.query_params.get('mobile',None)
if mobile:
user=User.objects.filter(mobile=mobile).first()
if user:
return APIResponse(exisit=True)
else:
return APIResponse(exisit=False)
多方式登录功能前端和注销功能
Login.vue
<template>
<div class="login">
<div class="box">
<i class="el-icon-close" @click="close_login"></i>
<div class="content">
<div class="nav">
<span :class="{active: login_method === 'is_pwd'}"
@click="change_login_method('is_pwd')">密码登录</span>
<span :class="{active: login_method === 'is_sms'}"
@click="change_login_method('is_sms')">短信登录</span>
</div>
<el-form v-if="login_method === 'is_pwd'">
<el-input
placeholder="用户名/手机号/邮箱"
prefix-icon="el-icon-user"
v-model="username"
clearable>
</el-input>
<el-input
placeholder="密码"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-button type="primary" @click="login_pwd">登录</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);
},
login_pwd() {
if (this.username && this.password) {
this.$http.post(this.$settings.base_url + '/api/user/login/', {
username: this.username,
password: this.password
}).then(res => {
if (res.data.code == 100) {
// 把用户名和token放到cookie中(s:秒,d:天,m:月)
this.$cookie.set('token', res.data.token, '7d')
this.$cookie.set('username', res.data.username, '7d')
//关闭登录框
this.$emit('close')
} else {
this.$message({
message: res.data.msg[0],
type: 'error'
})
}
})
} 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>
Header.vue
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/course')" :class="{active: url_path === '/course'}">免费课</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
</li>
</ul>
<div class="right-part">
<div v-if="!username">
<span @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
</div>
<div v-else>
<span>{{username}}</span>
<span class="line">|</span>
<span @click="logout">注销</span>
</div>
</div>
</div>
<Login v-if="is_login" @close="close_login" @go="put_register"/>
<Register v-if="is_register" @close="close_register" @go="put_login"/>
</div>
</template>
<script>
import Login from "./Login";
import Register from "./Register";
export default {
name: "Header",
data() {
return {
username:'',
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;
// 只要关闭登录框,去cookie中取username,如果在,就放到头部,不在,就不做处理
this.username = this.$cookie.get('username')
},
close_register() {
this.is_register = false;
},
logout(){
// 退出登录不需要跟后端交互,本地的cookie删除即可
this.$cookie.remove('username')
this.$cookie.remove('token')
//username也要值为空
this.username=''
}
},
created() {
// localStorage.name='lqz'
// sessionStorage.name='lqz'
// this.$cookie.set('name','lqz',3)
//
// this.$router 全局vue-router的对象
// this.$route 当次请求地址的对象,有请求地址,有携带数据
// console.log(this.$router)
// console.log(this.$route)
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
this.username=this.$cookie.get('username')
},
components: {
Register,
Login
}
}
</script>
<style scoped>
.header {
background-color: white;
box-shadow: 0 0 5px 0 #aaa;
}
.header:after {
content: "";
display: block;
clear: both;
}
.slogan {
background-color: #eee;
height: 40px;
}
.slogan p {
width: 1200px;
margin: 0 auto;
color: #aaa;
font-size: 13px;
line-height: 40px;
}
.nav {
background-color: white;
user-select: none;
width: 1200px;
margin: 0 auto;
}
.nav ul {
padding: 15px 0;
float: left;
}
.nav ul:after {
clear: both;
content: '';
display: block;
}
.nav ul li {
float: left;
}
.logo {
margin-right: 20px;
}
.ele {
margin: 0 20px;
}
.ele span {
display: block;
font: 15px/36px '微软雅黑';
border-bottom: 2px solid transparent;
cursor: pointer;
}
.ele span:hover {
border-bottom-color: orange;
}
.ele span.active {
color: orange;
border-bottom-color: orange;
}
.right-part {
float: right;
}
.right-part .line {
margin: 0 10px;
}
.right-part span {
line-height: 68px;
cursor: pointer;
}
</style>
腾讯云短信功能使用、发送短信接口
API接口和SDK
1 api接口,第三方提供的一个个的http的接口:腾讯会提供api接口
2 sdk,第三方基于api接口,封装的代码,发送短信,腾讯云也提供了sdk (如果有sdk,优先用sdk)
-腾讯发送短信,python提供了两个版本的sdk
-3.x:不仅仅包含发送短信,还包含其他云服务业务
-2.x:只有发送短信业务(简洁)
短信功能封装
# 1 在libs目录下,新建sms包
sms #包名
sms_main.py # 核心逻辑
settings.py # 配置
__init__.py
####### __init__.py
from .sms_main import get_code,send_sms
####### settings.py
APPID = # SDK AppID 以1400开头
# 短信应用 SDK AppKey
APPKEY = ""
# 短信模板ID,需要在短信控制台中申请
TEMPLATE_ID = # NOTE: 这里的模板 ID`7839` 只是示例,真实的模板 ID 需要在短信控制台中申请
# 签名
SMS_SIGN = ""
#####sms_main.py
from qcloudsms_py import SmsSingleSender
from qcloudsms_py.httpclient import HTTPError
from . import settings
import random
from luffy_api.utils.logging import get_logger
logger=get_logger()
# 生成code函数,四位数字
def get_code():
s=''
for i in range(4):
num=random.randrange(0,9)
s+=str(num)
return s
def send_sms(mobile, code):
ssender = SmsSingleSender(settings.APPID, settings.APPKEY)
params = [code, ] # 当模板没有参数时,`params = []`
try:
result = ssender.send_with_param(86, mobile,
settings.TEMPLATE_ID, params, sign=settings.SMS_SIGN, extend="", ext="")
if result.get('result')==0:
return True
else:
# 发送失败,记录日志
logger.error('手机号为:%s,发送短信失败'%mobile)
return False
except Exception as e:
logger.error('发送短信出异常,异常手机号为:%s' % mobile)
return False
发送短信接口
@action(methods=['GET'], detail=False)
def send_sms(self, request):
mobile = request.query_params.get('mobile', None)
if mobile:
# 手机号存在,发送短信
code = sms.get_code()
print(code)
# 发送短信(同步发送:等着腾讯发送成功或失败的返回,可能等很长时间)
#异步发送短信(不用等腾讯是否成功,直接给用户返回)
res = sms.send_sms(mobile, code)
if res:
# 发送成功后,短信验证码,放到缓存中(默认放内存中,实际项目要放到redis中)
cache.set(settings.SMS_CACHE%mobile,code,60*10)
# # 取出验证码
# code=cache.get(settings.SMS_CACHE%mobile)
return APIResponse(msg='短信发送成功')
else:
raise APIException({'detail': '短信发送失败'})
else:
# return APIResponse(code=101,msg='手机号没有传')
raise APIException({'detail': '手机号没有传'})
# raise Exception('手机号不存在')
公共配置文件(common_setting.py)
SMS_CACHE = 'sms_cache_%s'
短信登录接口
视图类
@action(methods=['POST'], detail=False)
def mobile_login(self, request):
ser=UserMobileModelSerializer(data=request.data)
ser.is_valid(raise_exception=True)
token = ser.context.get('token')
username = ser.context.get('username')
icon = ser.context.get('icon')
return APIResponse(token=token, username=username, icon=icon)
序列化类
class UserMobileModelSerializer(serializers.ModelSerializer):
mobile=serializers.CharField()
# code不是User表的字段,所以要重写
code=serializers.CharField()
class Meta:
model=User
fields=['mobile','code']
def validate(self, attrs):
# 校验code是否正确
self._check_code(attrs)
# 获取用户
user=self._get_user(attrs)
# 签发token
token=self._get_token(user)
self.context['token']=token
self.context['username']=user.username
self.context['icon']=str(user.icon)
return attrs
def _check_code(self,attrs):
code=attrs.get('code')
mobile=attrs.get('mobile')
#获取放在缓存中的code
old_code=cache.get(settings.SMS_CACHE%mobile)
# 删除缓存中的code
if not(old_code and code==old_code):
raise ValidationError({'detail':'验证码校验失败'})
def _get_user(self,attrs):
mobile = attrs.get('mobile')
user = User.objects.filter(mobile=mobile).first()
if not user:
raise ValidationError({'detail':'用户不存在'})
return user
def _get_token(self,user):
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
短信注册接口
- 手机号
- 验证码
- 密码
序列化类
class RegisterModelSerializer(serializers.ModelSerializer):
# code = serializers.CharField(max_length=4, min_length=4,write_only=True)
code = serializers.CharField(max_length=4, min_length=4)
class Meta:
model = User
fields = ['mobile', 'code', 'password']
extra_kwargs = {
'password': {'max_length': 16, 'min_length': 3},
}
# def validate_mobile(self, mobile):
# # 手机号是否在库中了
# import re
# return mobile
def validate(self, attrs):
# 验证码是否正确
self._check_code(attrs)
# 直接返回attrs
# code 剔除
attrs.pop('code')
return attrs
def _check_code(self, attrs):
code = attrs.get('code')
mobile = attrs.get('mobile')
# 有可能有的公司,测试阶段,有万能验证码
# 获取放在缓存中的code
old_code = cache.get(settings.SMS_CACHE % mobile)
# 删除缓存中的code
if not (old_code and code == old_code):
raise ValidationError({'detail': '验证码校验失败'})
def create(self, validated_data):
# username创建出来,使用手机号作为用户名
validated_data['username'] = validated_data.get('mobile')
# 使用User表的create_user方法新增用户(密码是密文的)
user = User.objects.create_user(**validated_data)
return user
视图类
class UserRegisterView(GenericViewSet, CreateModelMixin):
queryset = User.objects.all()
serializer_class = RegisterModelSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return APIResponse(msg='注册成功')
Redis
简介
Redis是一个key - value存储系统,非关系型数据库(nosql数据库),缓存数据库,数据存在于内存中
Redis支持五大数据类型
- 字符串
- 列表
- hash(字典)
- 集合
- 有序集合
使用Redis的好处
- 速度快:缓存数据库
- 支持丰富数据类型(五大数据类型)
- 支持事务(通过管道支持)(单实例上有效)
- 丰富的特性:可用于缓存、消息队列、按key设置过期时间,过期后自动删除
Redis对比memcached
- 都是内存数据库
- Redis类型丰富(五大数据类型),memcached只支持字符串类型
- Redis支持持久化,memcached不支持持久化
- Redis速度比memcached块
Redis特点
- 可以持久化
- 单线程,单进程
- Redis最新版本是6.x
- 6.x版本之前是单线程,单进程
Redis单线程为什么这么快(官方:10w左右并发,实际:6w左右并发)
- 纯内存操作
- 使用了io多路复用模型(epoll、select)
- 因为是单线程,不存在线程间切换,节省资源
服务:客户端,服务端(跟MySQL一样,是存数据的地方)
安装
- Redis是使用C写的且开源,官方并不支持Windows平台
- 第三方基于Redis源码,修改编译成Windows平台可以运行的(老:维护到3.x,新:维护到5.x)
- https://github.com/microsoftarchive/redis(Windows平台(3.x))
- 图形化界面客户端(redis-desktop-manager)
- 监听默认端口:6379
启动Redis服务
- 在任意路径下敲(前提:已经添加环境变量):redis-server
- Windows平台已经做成服务,直接开启服务
客户端连接
- 任意路径下敲:redis-cli -h 127.0.0.1 -p 6379
- 使用客户端连接
- 使用python操作(pip install redis)
普通连接与连接池
普通连接
# redis-py提供两个类Redis和StrictRedis用于实现Redis的命令,StrictRedis用于实现大部分官方的命令,并使用官方的语法和命令,Redis是StrictRedis的子类,用于向后兼容旧版本的redis-py
import redis
conn = redis.Redis(host='127.0.0.1', port=6379)
conn.set('foo', 'Bar')
print(r.get('foo'))
连接池
# redis-py使用connection pool来管理对一个redis server的所有连接,避免每次建立、释放连接的开销
# 默认:每个Redis实例都会维护一个自己的连接池
# 可以直接建立一个连接池,然后作为参数Redis,这样就可以实现多个Redis实例共享一个连接池
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
conn = redis.Redis(connection_pool=pool)
conn.set('foo', 'Bar')
print(r.get('foo'))
把POOL以单例形式使用
# pool.py
import redis
# 拿到一个连接数为3的连接池
# 把POOL做成单例:把它以模块导入方式
POOL=redis.ConnectionPool(max_connections=3,host='127.0.0.1', port=6380)
========================================================================================
import redis
from pool import POOL # 以模块形式导入,是天然单例
conn=redis.Redis(connection_pool=POOL)
# conn2=redis.Redis(connection_pool=POOL)
# conn3=redis.Redis(connection_pool=POOL)
# conn4=redis.Redis(connection_pool=POOL)
# conn5=redis.Redis(connection_pool=POOL)
# conn6=redis.Redis(connection_pool=POOL)
conn.set('age',19)
conn.close() # 不是关闭连接,把该连接,放回到连接池
Redis五大数据类型之字符串
字符串操作,Redis中的字符串再内存中按照一个name对应一个value来存储
'''
set(name, value, ex=None, px=None, nx=False, xx=False) 重点
setnx(name, value)
setex(name, value, time)
psetex(name, time_ms, value)
mset(*args, **kwargs) 重点
get(name) 重点
mget(keys, *args) 重点
getset(name, value)
getrange(key, start, end)
setrange(name, offset, value)
setbit(name, offset, value)
getbit(name, offset)
bitcount(key, start=None, end=None)
strlen(name) 重点
incr(self, name, amount=1) 重点
incrbyfloat(self, name, amount=1.0)
decr(self, name, amount=1)
append(key, value)
'''
import redis
conn=redis.Redis()
# set:放字符串值
'''
ex,过期时间(秒)
px,过期时间(毫秒)
nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果
xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值
'''
# conn.set('name','lqz')
# res=conn.set('name','lqz',ex=3)
# res=conn.set('name','lqz',px=3000)
# res=conn.set('name','lqz')
# res=conn.set('name','egon')
# res=conn.set('name','lqz',nx=True)
# res=conn.set('name','19',xx=True)
# get 获取值
# res=conn.get('name')
# mset 批量设置值
# conn.mset({'name':'lqz','age':19,'sex':'男'})
# mget 批量获取值
# res=conn.mget(['name','age'])
# res=conn.mget('name','age')
# conn.setex(name, value, time)
# conn.psetex(name, time_ms, value)
# conn.set(name, valeu, px=time)
# res=conn.getset('name', '刘亦菲')
# res=conn.getrange('name',3,5) #前闭后闭区间 拿到 亦 这个字 拿字节
#
# print(str(res,encoding='utf-8'))
# res=conn.setrange('name',0,'lqzzzzzzz')
# res=conn.setrange('name',0,'qq')
##获取和设置比特位
# setbit(name, offset, value)
# getbit(name, offset)
# print(conn.getbit('name',3))
# conn.setbit('name',0,1)
# print(conn.getbit('name',0))
# print(conn.strlen('name')) # 指的是字节 获取字节长度
# conn.incr('age') # 让该键自增1
# conn.incrby('age',3) # 让该键自增3
# conn.incrbyfloat('age', amount=1.1)
conn.append('name', 'nb')
conn.close()
Redis五大数据类型之hash
'''
hset(name, key, value) 重点
hmset(name, mapping) 重点
hget(name,key) 重点
hmget(name, keys, *args) 重点
hgetall(name)
hlen(name) 重点
hkeys(name)
hvals(name)
hexists(name, key)
hdel(name,*keys)
hincrby(name, key, amount=1) 重点
hincrbyfloat(name, key, amount=1.0)
hscan(name, cursor=0, match=None, count=None)
hscan_iter(name, match=None, count=None)
'''
import redis
conn=redis.Redis()
# conn.hset('users','name','egon')
# conn.hset('users','age','19')
# res=conn.hget('users','age')
# res=conn.hget('users','name')
# conn.hmset('h1',{'name':'lqz','age':19})
# res=conn.hmget('h1',['name','age'])
# res=conn.hmget('h1','name','age')
# res=conn.hlen('users') # 统计键值对的个数
# res=conn.hkeys('users')
# res=conn.hvals('users')
# hvals(name)
# print(str(res[2],encoding='utf-8'))
# res=conn.hexists('users','age1')
# res=conn.hdel('users','age')
# conn.hincrby('h1', 'age',7)
# conn.hincrbyfloat('h1', 'age', amount=-1.01)
# for i in range(1000):
# conn.hset('htest',i,'第%s个鸡蛋'%i)
###hash(字典)类型是无序的
# res=conn.hgetall('htest')
# print(res)
# res=conn.hscan('htest', cursor=0,count=500) # 用的少
# print(res)
# res=conn.hscan('htest', cursor=254,count=499) # 用的少
# print(res)
# res=conn.hscan('htest', cursor=511,count=2) # 用的少
# print(res)
# print(res)
# print(len(res[1]))
res=conn.hscan_iter('htest', count=20) # 把hash类型的所有数据都打印出来,比hgetall节省内存
for item in res:
print(str(item[1],encoding='utf-8'))
# print(res)
conn.close()
Redis五大数据类型之列表
'''
lpush(name,values) #从左侧插入值
lpushx(name,value)
llen(name) # 重点
linsert(name, where, refvalue, value)) # 重点
lset(name, index, value)
lrem(name, value, num) # 删除值
lpop(name) #从左侧弹出
lindex(name, index)
lrange(name, start, end)
ltrim(name, start, end)
rpoplpush(src, dst)
blpop(keys, timeout) # 阻塞式弹出
brpoplpush(src, dst, timeout=0)
'''
import redis
conn=redis.Redis()
# conn.lpush('list1','1') #左侧插入,上部
# conn.lpush('list1','lqz')
# conn.rpush('list1','egon') #右侧插入,下部
# conn.rpushx('list1','刘亦菲') # 只有name存在才能插入
# conn.lpushx('list2','刘亦菲')
# print(conn.llen('list1'))
# conn.linsert('list1','before','egon','迪丽热巴')
# conn.linsert('list1','AFTER','egon','古力娜扎')
# conn.linsert('list1','AFTER','1','99999')
# conn.lset('list1',3,66666) # 修改某个位置的值
# 如果count=0,表示删除所有符合条件的,删除所有66666
# conn.lrem('list1',0,'66666')
# 如果count=1,表示从左侧删除1个符合条件的,删除1个99999
# conn.lrem('list1',1,'99999')
# 如果count=-2,表示从底部,从右侧,删除符合条件的2个
# conn.lrem('list1',-2,'99999')
# res=conn.lpop('list1')
# res=conn.rpop('list1')
# res=conn.lindex('list1',1) #返回某个索引的值 列表操作 l[0]
# res=conn.lrange('list1', 0, 4) # 前闭后闭
# res=conn.ltrim('list1', 1, 3) #修剪,不符合范围的都删除,前闭后闭
# conn.lpush('list2','李易峰','刘亦菲','迪丽热巴')
# conn.rpoplpush('list1', 'list2')
res=conn.blpop('list2',timeout=2) # 阻塞式弹出,通过它做一些分布式任务,可以用来做消息队列
print(res)
# brpoplpush(src, dst, timeout=0)
conn.close()
其他通用操作
'''
delete(*names)
# 根据删除redis中的任意数据类型
exists(name)
# 检测redis的name是否存在
keys(pattern='*')
# 根据模型获取redis的name
expire(name ,time)
# 为某个redis的某个name设置超时时间
rename(src, dst)
# 对redis的name重命名为
move(name, db))
# 将redis的某个值移动到指定的db下
randomkey()
# 随机获取一个redis的name(不删除)
type(name)
#查看类型
'''
##通用操作
import redis
conn=redis.Redis()
# conn.delete('h1')
# print(conn.exists('name'))
# res=conn.keys('list*')
# print(res)
# conn.expire('list1',3)
# conn.rename('sex','sex111')
# conn.move('sex111',2)
# res=conn.randomkey()
# res=conn.type('users')
res=conn.type('name')
print(res)
conn.close()
管道
# redis支持事务,借助于管道
# import redis
#
#
# conn=redis.Redis()
#
# conn.decr('zhangsan',100)
#
# raise Exception('程序废了,出异常了')
#
# conn.incrby('wo',100)
#
# conn.close()
# 借助于管道实现
import redis
conn=redis.Redis()
pip_line=conn.pipeline(transaction=True) #开启一个管道
pip_line.decr('zhangsan',100) #往管道中放一个命令
raise Exception('程序崩了')
pip_line.incrby('wo',100) #往管道中放一个命令
pip_line.execute() # 把管道中的命令一次性执行
conn.close()
Django使用Redis
# 第一种:通用方式(任何web框架都可以)
新建一个文件,其他文件导入使用
# 第二种:django提供的
-在配置文件中配置
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} # 连接池大小为100
# "PASSWORD": "123",
}
}
}
-安装django_redis模块
-使用
from django_redis import get_redis_connection
def index(request):
conn=get_redis_connection()
res=conn.get('name')
print(res)
return HttpResponse('okkkkk')
-一旦这样配置,django中的缓存,也用redis
cache.set('key',python的任意对象)
Celery
介绍与架构
- 介绍
# celery:分布式的异步任务框架
# celery能做什么:
1 异步任务
1.1 解决耗时任务,将耗时任务提交给celery去异步执行(比如发送短信、邮件、消息推送、音视频处理等等...)
2 延迟任务
2.1 解决需要延迟执行的任务
3 定时任务
3.1 解决周期任务(比如每日数据统计)
'''
总结:
1 可以不依赖任何服务器,通过自身命令,启动服务
2 celery服务是为其他项目服务提供异步解决任务需求的
(会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要的时候通过异步完成项目的需求)
'''
举栗:
人是一个独立运行的服务 | 医院也是一个独立运行的服务
正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求
# ps:
celery官方不支持在Windows平台上使用(也可以使用)
1 使用Mac
2 乌班图
3 远程连接Linux
- 架构
# 消息中间件(broker)
celery本身不提供消息服务,但是可以使用第三方提供的消息中间件集成(Redis...)
# 任务执行单元(worker)
worker是celery提供的任务执行的单元,worker并发的运行在分布式的系统节点中(本质:一个work就是一个进程)
# 任务结果存储(backend)
Task result store用来存储worker执行的任务的结果,celery支持以不同方式存储任务的结果(Redis...)
基本使用
# 安装:pip install celery
# win上启动work
# 4.x版本及之前
pip install eventlet
celery worker -A celery_task -l info -P eventlet
# 5.x版本
pip install eventlet
celery -A celery_task worker -l info -P eventlet
# linux中启动work
celery worker -A celery_task -l info
celery -A celery_task worker -l info
# 启动beat的命令(负责每隔几秒钟,向任务队列中提交任务)
celery beat -A celery_task -l info
普通使用
新建celery_task.py(任意名字)
from celery import Celery
backend='redis://127.0.0.1:6379/1' # 结果存储
broker='redis://127.0.0.1:6379/2' # 消息中间件
app=Celery('test',broker=broker,backend=backend) # 传一个字符串,相当于名字
@app.task
def add(a,b): # 很耗时的任务
import time
time.sleep(3)
return a+b
新建add_task.py添加任务
from celery_task import add
# res=add(1,2) # 同步调用
res=add.delay(3,4) # 把任务提交到redis,返回要给uuid:b20f827d-dcf5-4fdf-a646-5abb963dc1d3
print(res)
新建get_result.py查询任务
from celery_task import app # 自己写的app
from celery.result import AsyncResult # celery模块下的
id = 'b20f827d-dcf5-4fdf-a646-5abb963dc1d3'
if __name__ == '__main__':
a = AsyncResult(id=id, app=app)
if a.successful():
result = a.get() # task中return的数据:7
print(result)
elif a.failed():
print('任务失败')
elif a.status == 'PENDING':
print('任务等待中被执行')
elif a.status == 'RETRY':
print('任务异常后正在重试')
elif a.status == 'STARTED':
print('任务已经开始被执行')
启动worker
# win
pip install eventlet
celery -A celery_task worker -l info -P eventlet
# linux中启动work
celery -A celery_task worker -l info
包结构(推荐)
celery_task # 包名
__init__.py # 任务存放路径
user_task.py # 任务存放路径
order_task.py # 任务存放路径
home_task.py # 任务存放路径
celery.py # app所在文件,必须叫celery
project
├── celery_task # celery包
│ ├── __init__.py # 包文件
│ ├── celery.py # celery连接和配置相关文件,且名字必须叫celery.py
│ └── tasks.py # 所有任务函数
├── add_task.py # 添加任务
└── get_result.py # 获取结果
celery.py
### 有app的这个py文件,必须叫celery
from celery import Celery
backend = 'redis://127.0.0.1:6379/1' # 结果存储
broker = 'redis://127.0.0.1:6379/2' # 消息中间件
app = Celery('test', broker=broker, backend=backend, include=[
'celery_task.home_task',
'celery_task.order_task',
'celery_task.user_task'
])
# 定制定时任务
# 时区(修改时区)
print(app.conf)
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False
# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
'send_sms_5': {
'task': 'celery_task.user_task.send_sms', # 要执行的任务
'schedule': timedelta(seconds=5),
# 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点
'args': (189533333,),
},
'add_3': {
'task': 'celery_task.home_task.add', # 要执行的任务
'schedule': timedelta(seconds=3),
# 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点
'args': (6,8),
}
}
home_tesk.py
from .celery import app
@app.task # home模块的任务
def add(a,b):
return a+b
order_task.py
from .celery import app
@app.task # order模块的任务,写文件任务
def write_file(s):
with open(r'D:\lqz2\luffy_api\log\a.txt','w',encoding='utf-8') as f:
f.write(s)
return True
user_task.py
from .celery import app
@app.task # user模块的任务,发送带内心
def send_sms(s):
import time
time.sleep(3)
return '%s手机号,短信发送成功'%s
celery执行异步任务,延迟任务,定时任务(5星)
异步任务
# delay
from celery_task.user_task import send_sms
res=send_sms.delay('18953675221')
print(res)
延迟任务
# 延迟任务,延迟5s钟后,发送短信
from celery_task.user_task import send_sms
from datetime import datetime, timedelta
# print(datetime.utcnow()+timedelta(seconds=10)) # 打印出当前utc时间
eta=datetime.utcnow() + timedelta(seconds=10) # 当前utc时间,往后推10s,时间对象
# # args是列表,send_sms的参数,eta是延迟时间,时间对象
res=send_sms.apply_async(args=['1888888',],eta=eta)
print(res)
定时任务
# 写在celery.py中
# 定制定时任务
# 时区(修改时区)
print(app.conf)
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False
# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
'send_sms_5': {
'task': 'celery_task.user_task.send_sms', # 要执行的任务
'schedule': timedelta(seconds=5),
# 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点
'args': (189533333,),
},
'add_3': {
'task': 'celery_task.home_task.add', # 要执行的任务
'schedule': timedelta(seconds=3),
# 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点
'args': (6,8),
}
}
# 启动beat
celery -A celery_task beat -l info
Django使用celery
- django-celery模块(年久失修)
- 自己集成(和框架无关)
celery.py
"""
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包必须放置项目的根目录下
"""
# 一、加载django配置环境
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")
# 二、加载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': (),
}
}
tasks.py
from .celery import app
from django.core.cache import cache
from home import models, serializers
from django.conf import settings
@app.task
def update_banner_list():
queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('-orders')[:settings.BANNER_COUNT]
banner_list = serializers.BannerSerializer(queryset, many=True).data
# 拿不到request对象,所以头像的连接base_url要自己组装
for banner in banner_list:
banner['image'] = 'http://127.0.0.1:8000%s' % banner['image']
cache.set('banner_list', banner_list, 86400)
return True
异步发送短信
@action(methods=['GET'], detail=False, throttle_classes=[Mobile_throttle])
def send_sms(self, request, *args, **kwargs):
mobile = request.query_params.get('mobile', None)
if mobile:
# 手机号存在,发送短信
code = sms.get_code()
print(code)
# 发送短信(同步发送:等着腾讯发送成功或失败的返回,可能等很长时间)
# 异步发送短信(不用等腾讯是否成功,直接给用户返回)
from celery_task.user_task import send_sms_6
send_sms_6.delay(mobile, code)
cache.set(settings.SMS_CACHE % mobile, code, 600)
return APIResponse(msg='短信已经发送')
else:
raise APIException({'detail': '手机号没有传'})
轮播图接口加入缓存
1 如果mysql数据变化了,轮播图表数据变化了,由于缓存没有更新,就会出现问题
2 双写一致性问题(4星)
-因为使用了缓存,mysql和redis的数据不一致了
-如何解决:(缓存更新策略)
-加入过期时间? 60s? 过期时间内数据还有问题
-定时更新缓存(每隔60s,更新一下缓存)
-数据库一更改,立马改缓存
-先改数据库,再改缓存
-先改缓存,再改数据库
-数据库一改,删除缓存
-先删缓存,再改数据库
-先改数据库,再删缓存(稍微好一些)
3 首页轮播图接口:缓存更新策略是定时更新
-celery的定时任务
-使用celery的定时任务,完成首页轮播图缓存的更新
课程功能表分析
模型表部分
from django.db import models
# Create your models here.
from luffy_api.utils.model import BaseModel
#### 课程分类
class CourseCategory(BaseModel):
"""分类"""
name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")
class Meta:
db_table = "luffy_course_category"
verbose_name = "分类"
verbose_name_plural = verbose_name
def __str__(self):
return "%s" % self.name
####实战课表
class Course(BaseModel):
"""课程"""
course_type = (
(0, '付费'),
(1, 'VIP专享'),
(2, '学位课程')
)
level_choices = (
(0, '初级'),
(1, '中级'),
(2, '高级'),
)
status_choices = (
(0, '上线'),
(1, '下线'),
(2, '预上线'),
)
name = models.CharField(max_length=128, verbose_name="课程名称")
course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
# 使用这个字段的原因
brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True, null=True)
status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
students = models.IntegerField(verbose_name="学习人数", default=0) # 正常来讲,没有这个字段,应该跟User表关联
sections = models.IntegerField(verbose_name="总课时数量", default=0)
pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
###### 关联字段
teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
# 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="课程名称")
chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
name = models.CharField(max_length=128, verbose_name="章节标题")
summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
class Meta:
db_table = "luffy_course_chapter"
verbose_name = "章节"
verbose_name_plural = verbose_name
def __str__(self):
return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
###课时表
class CourseSection(BaseModel):
"""课时"""
section_type_choices = (
(0, '文档'),
(1, '练习'),
(2, '视频')
)
chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
verbose_name="课程章节")
name = models.CharField(max_length=128, verbose_name="课时标题")
orders = models.PositiveSmallIntegerField(verbose_name="课时排序")
section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接",
help_text="若是video,填vid,若是文档,填link")
duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32) # 仅在前端展示使用
pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
free_trail = models.BooleanField(verbose_name="是否可试看", default=False)
class Meta:
db_table = "luffy_course_Section"
verbose_name = "课时"
verbose_name_plural = verbose_name
def __str__(self):
return "%s-%s" % (self.chapter, self.name)
数据部分
-- 老师表
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);
查询所有课程分类接口
- 视图类
class CourseCategoryView(GenericViewSet,ListModelMixin):
queryset = CourseCategory.objects.all().filter(is_delete=False,is_show=True).order_by('orders')
serializer_class = CourseCategorySerializer
- 序列化类
class CourseCategorySerializer(serializers.ModelSerializer):
class Meta:
model=CourseCategory
fields=['id','name']
查询所有课程接口(排序,过滤)
- 视图类
class CourseView(GenericViewSet,ListModelMixin):
queryset = Course.objects.all().filter(is_delete=False,is_show=True).order_by('orders')
serializer_class = CourseSerializer
# 排序
filter_backends = [OrderingFilter,DjangoFilterBackend]
ordering_fields=['students','price']
# 过滤(使用第三方)
# 内置过滤:search='1'
# 第三方:course_category=2
filter_fields=['course_category']
# 分页
pagination_class = CommonPageNumberPagination
- 序列化类
class TeacherSerializer(serializers.ModelSerializer):
class Meta:
model=Teacher
fields = ('name', 'role_name', 'title', 'signature', 'image', 'brief')
class CourseSerializer(serializers.ModelSerializer):
class Meta:
model=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',] # 课时显示,超过4个,只显示4个,不超过都显示
## 第二种:
# course_type_name=serializers.CharField(source='get_course_type_display')
## 第三种
# course_type_name=serializers.SerializerMethodField()
# def get_course_type_name(self,obj):
# return obj.get_course_type_display()
###序列化老师的第二种方式
# teacher=serializers.SerializerMethodField()
# def get_teacher(self,obj):
# return {'name':obj.teacher.name}
### 序列化老师的第三种方式:子序列化
teacher=TeacherSerializer()
- 表模型
####实战课表
class Course(BaseModel):
"""课程"""
course_type = (
(0, '付费'),
(1, 'VIP专享'),
(2, '学位课程')
)
level_choices = (
(0, '初级'),
(1, '中级'),
(2, '高级'),
)
status_choices = (
(0, '上线'),
(1, '下线'),
(2, '预上线'),
)
name = models.CharField(max_length=128, verbose_name="课程名称")
course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
# 使用这个字段的原因
brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True,
null=True)
status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
students = models.IntegerField(verbose_name="学习人数", default=0) # 正常来讲,没有这个字段,应该跟User表关联
sections = models.IntegerField(verbose_name="总课时数量", default=0)
pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
###### 关联字段
teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
# 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
@property
def course_type_name(self):
# get_字段名_display() 取到该字段用choice映射的中文
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()
## 序列化teacher第一种方案
# def teacher_detail(self):
# return {'name':self.teacher.name}
@property
def section_list(self):
sections_all = []
# 通过课程拿到所有章节(反向操作:使用表名小写_set.all())
# 如果ForeignKey加了related_name,反向操作的时候,直接使用related_name对象的值:coursechapters
for coursechapter in self.coursechapters.all():
for course_section in coursechapter.coursesections.all():
sections_all.append(
{
'name': course_section.name,
'section_link': course_section.section_link,
'duration': course_section.duration,
'free_trail': course_section.free_trail,
}
)
if len(sections_all)>=4:
return sections_all
return sections_all
###老师表
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
def role_name(self):
return self.get_role_display()
补充
# related_name:反向操作时,使用的字段名,用于代替原反向查询时的’表名_set’
# related_query_name:反向查询操作时,使用的连接前缀,用于替换表名
自定义过滤器
1 写一个类CourseNameFilter,继承BaseFilterBackend
2 重写filter_queryset,在内部实现过滤逻辑,返回queryset对象
3 在视图类中配置
filter_backends = [CourseNameFilter]
借助django-filter实现过滤功能
#########方案一(弱点是只能使用课程(course表)中的字段)#######
### 1 在视图类中
filter_backends = [DjangoFilterBackend]
filter_fields=['course_category','name']
## 使用
http://127.0.0.1:8000/api/course/actual_course/?course_category=2
#########方案二:可以自定义字段,及规则实现过滤#######
filter_backends = [DjangoFilterBackend]
filter_class = CourseFilterSet #写一个类
#### 过滤类
class CourseFilterSet(FilterSet):
min_price = filters.NumberFilter(field_name='price', lookup_expr='gte')
max_price = filters.NumberFilter(field_name='price', lookup_expr='lte')
class Meta:
model = models.Course
fields = ['course_category','max_price','min_price']
课程详情接口
class CourseView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
获取所有章节接口
- 视图类
from .models import CourseChapter
class CourseChapterView(GenericViewSet,ListModelMixin):
queryset = CourseChapter.objects.filter(is_delete=False,is_show=True).order_by('orders')
serializer_class =CourseChapterSerializer
# 加一个通过课程id过滤功能
filter_backends = [ DjangoFilterBackend]
filter_fields = ['course']
- 序列化类
class CourseSectionSerializer(serializers.ModelSerializer):
class Meta:
model=CourseSection
fields=['name', 'orders', 'section_link', 'duration', 'free_trail']
class CourseChapterSerializer(serializers.ModelSerializer):
coursesections=CourseSectionSerializer(many=True) # 子序列化,多条
class Meta:
model=CourseChapter
fields=['course','chapter','name','summary','coursesections']
小结
1 课程分类
2 查询所有课程:支持分页,支持排序(价格,人数),支持过滤 ---》自定义过滤类,根据django-filter自定义区间过滤
3 课程详情接口
4 所有课程章节接口(每个章节下的课时:子序列化):支持按课程id号过滤功能
课程列表前台
<template>
<div class="course">
<Header></Header>
<div class="main">
<!-- 筛选条件 -->
<div class="condition">
<ul class="cate-list">
<li class="title">课程分类:</li>
<li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li>
<li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"
@click="filter.course_category=category.id" :key="category.name">{{category.name}}
</li>
</ul>
<div class="ordering">
<ul>
<li class="title">筛 选:</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="'/actual-course/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
<span v-else>共{{course.sections}}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原价:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<!--分页功能-->
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[2, 3, 5, 10]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
export default {
name: "Course",
data() {
return {
category_list: [], // 课程分类列表
course_list: [], // 课程列表
course_total: 0, // 当前课程的总数量
filter: {
course_category: 0, // 当前用户选择的课程分类,刚进入页面默认为全部,值为0
ordering: "-id", // 数据的排序方式,默认值是-id,表示对于id进行降序排列
page_size: 2, // 单页数据量
page: 1,
}
}
},
created() {
this.get_category();
this.get_course();
},
components: {
Header,
Footer,
},
watch: {
"filter.course_category": function(){
this.filter.page = 1;
this.get_course();
},
"filter.ordering": function () {
this.get_course();
},
"filter.page_size": function () {
this.get_course();
},
"filter.page": function () {
this.get_course();
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_category() {
// 获取课程分类信息
// this.$http.get(`${this.$settings.base_url}/course/categories/`).then(response => {
this.$http.get(this.$settings.base_url+'/api/course/category/').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.$http.get(`${this.$settings.base_url}/api/course/actual_course/`, {
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>
课程详情前台
<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/Header"
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: {
//https://www.jianshu.com/p/e8e747e33ef0
playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
sources: [{ // 播放资源和资源格式
type: "video/mp4",
src: "http://qx1wkw53q.hb-bkt.clouddn.com/01%20%E6%9C%AC%E5%91%A8%E5%86%85%E5%AE%B9%E5%AE%89%E6%8E%92%E4%B8%8E%E5%AD%A6%E4%B9%A0%E6%96%B9%E6%B3%95%E4%BB%8B%E7%BB%8D.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;
this.course_id = this.$route.params.pk
if (this.course_id < 1) {
let _this = this;
_this.$alert("对不起,当前视频不存在!", "警告", {
callback() {
_this.$router.go(-1); // 退到上一个地址
}
});
}
},
get_course_data() {
// ajax请求课程信息
this.$http.get(`${this.$settings.base_url}/api/course/actual_course/${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.$http.get(`${this.$settings.base_url}/api/course/course_chapter/`, {
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>
视频、图片...托管
第三方
- 七牛云
- 阿里oss
- ...
自己搭建文件服务器
- fastdfs
- 渐少、没有web界面操作
- go-fastdfs
- ceph
- 专业的文件服务器
- minio
- 有web操作界面
补充(不同接口使用不同序列化类)
# 需求是继承class CourseView(GenericViewSet,ListModelMixin,RetrieveModelMixin):实现不同接口,使用不同的序列化类
def get_serializer_class(self): # 返回哪个序列化类,当前接口就以该序列化类进行序列化
print(self.request)
if self.action=='list':
print('获取所有')
return self.serializer_class
else:
print('其他情况')
from .serializer import CourseDetailSerializer
return CourseDetailSerializer
前端搜索导航栏
- Head.vue
<form class="search">
<div class="tips" v-if="is_search_tip">
<span @click="search_action('Python')">Python</span>
<span @click="search_action('Linux')">Linux</span>
</div>
<input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search"
v-model="search_word">
<button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)"></button>
</form>
# js
#data:
// 搜索相关
is_search_tip: true,
search_placeholder: '',
search_word: ''
#methods
search_action(search_word) {
if (!search_word) {
this.$message('请输入要搜索的内容');
return
}
if (search_word !== this.$route.query.word) {
this.$router.push(`/course/search?word=${search_word}`);
// this.$router.push(`/course/search/${search_word}`);
}
this.search_word = '';
},
on_search() {
this.search_placeholder = '请输入想搜索的课程';
this.is_search_tip = false;
},
off_search() {
this.search_placeholder = '';
this.is_search_tip = true;
},
#### 样式
.search {
float: right;
position: relative;
margin-top: 22px;
margin-right: 10px;
}
.search input, .search button {
border: none;
outline: none;
background-color: white;
}
.search input {
border-bottom: 1px solid #eeeeee;
}
.search input:focus {
border-bottom-color: orange;
}
.search input:focus + button {
color: orange;
}
.search .tips {
position: absolute;
bottom: 3px;
left: 0;
}
.search .tips span {
border-radius: 11px;
background-color: #eee;
line-height: 22px;
display: inline-block;
padding: 0 7px;
margin-right: 3px;
cursor: pointer;
color: #aaa;
font-size: 14px;
}
.search .tips span:hover {
color: orange;
}
搜索接口
1 全文检索,尽量不要使用mysql,(mysql性能非常低),需要用到专业的搜索引擎Elasticsearch(分布式全文检索引擎)
from rest_framework.filters import SearchFilter
from luffy_api.utils.response import APIResponse
class CourseSearchView(GenericViewSet, ListModelMixin):
queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseSerializer
filter_backends = [SearchFilter]
search_fields = ['name',]
pagination_class = CommonPageNumberPagination
# 后期改接口,可能要查好几种课程
# 加入全文检索,使用es实现
# def list(self, request, *args, **kwargs):
# search=request.query_params.get('search')
# # 实战课
# response=super().list(request, *args, **kwargs)
# # 免费课
# course_list=CourseFree.objects.all().filter(name__contains=search)
# # 序列化
#
# return APIResponse(result={'actual_course':response.data,'free_course':ser.data})
搜索页面
- SearchCourse.vue
<template>
<div class="search-course course">
<Header/>
<!-- 课程列表 -->
<div class="main">
<div v-if="course_list.length > 0" class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
<span v-else>共{{course.sections}}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原价:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; line-height: 60px">
没有搜索结果
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[2, 3, 5, 10]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from '../components/Header'
export default {
name: "SearchCourse",
components: {
Header,
},
data() {
return {
course_list: [],
course_total: 0,
filter: {
page_size: 10,
page: 1,
search: '',
}
}
},
created() {
this.get_course()
},
watch: {
'$route.query' () {
this.get_course()
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_course() {
// 获取搜索的关键字
// this.filter.search = this.$route.query.word || this.$route.query.wd;
this.filter.search = this.$route.query.word
// 获取课程列表信息
this.$http.get(`${this.$settings.base_url}/api/course/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>
支付宝支付介绍
-
支付功能
- 支付宝
- 微信
- 自有支付系统
-
支付流程
- 申请支付宝支付
- 必须以企业申请(需要营业执照)
- 沙箱环境(测试环境)
- 沙箱app
- 使用第三方sdk实现支付
- 生成公钥私钥(非对称加密)
- 加密解密使用同一个密钥(对称加密)
- 使用第三方sdk
- pip install python-alipay-sdk
- pip install pyOpenSsl
支付宝二次封装
在libs文件夹下新建alipay包
alipay
__init__.py #包
settings.py #配置文件
pay.py # 核心文件
pem # 秘钥
alipay_public_key.pem #支付宝公钥(从支付宝网站获得)
app_private_key.pem #应用私钥(生成自己保存)
settings.py
import os
# 应用私钥
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
# 应用ID
APP_ID = '2016092000554611'
# 加密方式
SIGN = 'RSA2'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
pay.py
from alipay import AliPay
from . import settings
alipay = AliPay(
appid=settings.APP_ID,
app_notify_url=None,
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN, # RSA 或者 RSA2
debug=settings.DEBUG , # 默认 False
)
订单相关
#新建order app,新建表,迁移
from django.db import models
# Create your models here.
# 订单表
# 订单详情表
from user.models import User
from course.models import Course
class Order(models.Model):
"""订单模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超时取消'),
)
pay_choices = (
(1, '支付宝'),
(2, '微信支付'),
)
subject = models.CharField(max_length=150, verbose_name="订单标题")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True) #必须唯一
trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号") # 支付交易流水号
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户")
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = "luffy_order"
verbose_name = "订单记录"
verbose_name_plural = "订单记录"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
class OrderDetail(models.Model):
"""订单详情"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="订单")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
class Meta:
db_table = "luffy_order_detail"
verbose_name = "订单详情"
verbose_name_plural = "订单详情"
def __str__(self):
try:
return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
except:
return super().__str__()
支付接口
# 点击立即购买,向后端发送post请求,携带数据格式
-{'total_amount':99,'courses':[1,2,3]}
# 必须登录后才能调用
-request.user中取出当前登录用户
# 后端处理逻辑,生成订单
-1 校验课程总价格
-2 获取当前登录用户
-3 生成支付连接
-4 生成订单(存order和orderdetail表)
视图类
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from .models import Order
from .serializer import OrderModelSerializer
from luffy_api.utils.response import APIResponse
class PayView(GenericViewSet,CreateModelMixin):
queryset = Order.objects.all()
serializer_class =OrderModelSerializer
# 认证类(可以自己写)
authentication_classes = [JSONWebTokenAuthentication,]
permission_classes = [IsAuthenticated,]
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)
pay_url=serializer.context.get('pay_url')
return APIResponse(pay_url=pay_url)
序列化类
from rest_framework import serializers
from .models import Order, OrderDetail
from course.models import Course
import uuid
from rest_framework.exceptions import ValidationError
from luffy_api.libs.alipay import alipay,GATEWAY
from django.conf import settings
class OrderModelSerializer(serializers.ModelSerializer):
# courses=serializers.ListField() # 映射列表形式
# courses=serializers.DictField() # 映射字典形式
# 这个字段,会把前端传入的课程id列表,转换成课程对象列表
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(),many=True)
class Meta:
model = Order
fields = ['total_amount', 'courses']
def validate(self, attrs):
'''
-1 校验课程总价格
-2 获取当前登录用户
-3 生成支付连接
-4 存库之前数据处理
-5 生成订单(存order和orderdetail表)
'''
# 1 校验课程总价格
self._check_price(attrs)
# 2 获取当前登录用户
user = self._get_user()
#3 生成支付连接
trade_no=self._get_out_trade_no()
total_amount=float(attrs.get('total_amount'))
# 订单名:如果是一个课程,就使用课程名,如果是多个课程,就使用第一个课程名+等
if len(attrs.get('courses'))>1:
subject='合并支付'
else:
subject=attrs.get('courses')[0].name
pay_url = self._get_pay_url(total_amount,subject,trade_no)
self.context['pay_url'] = pay_url
# 4 支付前数据处理
self._perform_save(attrs,user,trade_no,subject)
return attrs
def create(self, validated_data):
courses=validated_data.pop('courses')
order=Order.objects.create(**validated_data)
# 存订单详情
for course in courses:
OrderDetail.objects.create(order=order,course=course,price=course.price,real_price=course.price)
return order
def _check_price(self, attrs):
total_amount=attrs.get('total_amount')
courses=attrs.get('courses')
price=0
for course in courses:
price+=course.price
if not total_amount==price:
raise ValidationError({'detail':'价格不匹配'})
def _get_user(self):
return self.context.get('request').user
def _get_pay_url(self,total_amount,subject,trade_no):
res=alipay.api_alipay_trade_page_pay(
total_amount=total_amount,
subject=subject,
out_trade_no=trade_no,
return_url=settings.RETURN_URL, # 前端回调
notify_url=settings.NOTIFY_URL #后端回调
)
return GATEWAY+res
def _get_out_trade_no(self):
# 使用uuid生成
# 分布式id生成方案(在分布式的节点中生成不重复的id号,性能高)
# -uuid
# -mysql 自增 当前时间+自增
# -redis自增 当前时间+incr
# -雪花算法 主流
# -美团的Leaf
return str(uuid.uuid4()).replace('-','')
def _perform_save(self,attrs,user,trade_no,subject):
# 把当前登录用户放到attrs中,把订单号,放入attrs中
attrs['user']=user
attrs['out_trade_no']=trade_no
attrs['subject']=subject
支付前台,回调页面
go_buy() {
let token = this.$cookie.get('token')
if (token) {
this.$http.post(`${this.$settings.base_url}/api/order/pay/`, {
'total_amount': this.course_info.price,
'courses': [this.course_info.id,]
}, {
headers: {
'Authorization': 'jwt ' + token
}
}).then(res => {
if (res.data.code == 100) {
//打开一个新连接,支付
open(res.data.pay_url, '_self')
} else {
this.$message({
message: res.data.msg
});
}
})
} else {
this.$message({
message: "您没有登录,请先登录"
});
}
}
支付成功回调接口(两个)
from rest_framework.views import APIView
from rest_framework.response import Response
class SuccessView(APIView):
def get(self,request):
# vue的前端来调用的
out_trade_no=request.query_params.get('out_trade_no')
order=Order.objects.filter(out_trade_no=out_trade_no).first()
if order.order_status==1:
# 订单支付成功,
return APIResponse(msg='支付成功')
else:
return APIResponse(msg='我们还没有收到该订单的付款,请稍后刷新页面再试')
def post(self,request):
# 支付宝来调用,用来修改订单状态
import json
try:
result_data = request.data.dict() # 把querydic转成字典对象,否则,不允许改值
logger.warning(json.dumps(result_data))
out_trade_no = result_data.get('out_trade_no')
signature = result_data.pop('sign')
###一定要验证签名,如果不验证签名,不能修改订单状态
from libs.alipay import alipay
result = alipay.verify(result_data, signature)
if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# 完成订单修改:订单状态、流水号、支付时间
Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
# 完成日志记录
logger.warning('%s订单支付成功' % out_trade_no)
return Response('success') # 支付宝要求的格式
else:
logger.error('%s订单支付失败' % out_trade_no)
except:
pass
return Response('failed')