luffy
企业项目类型
# 1 面向互联网用户:商城类项目
-微信小程序商城
-app商城
-得物
-饿了么
-问卷网
# 2 面向互联网用户:二手交易类的
-咸鱼
-转转
# 3 公司内部项目:python写的重点
# 传统软件行业,互联网
-给客户做软件:国家电网,社保局,银行 ,医院,大客户
-互联网:饿了么,美团,抖音,阿里
-oa系统
-故障上报系统
-打卡系统工资核算系统
-第三方公司做的:
-给医院 互联网,内部的项目
-银行 内部系统
-政府
-钢材市场,商户
- 微信小程序订餐
-二维火 餐饮行业
-零售行业
-问卷网
-考试系统
-django+simpleui:二次定制
# 4 个人博客
# 5 内容收费站
-掘金
-思否
# 6 房屋租赁
-青客
-蛋壳
-自如
# 7 软硬件结合
-工业互联网
-智慧农业
企业项目开发流程
# 开发流程
-立项:高层
-需求分析
# 互联网项目
-需求调研和分析:产品经理设计出来的
# 传统软件
-需求调研和分析:市场人员,开发 跟客户对接
# 需求说明书
-原型设计:产品经理 根据需求说明书
-墨刀----》画原型图
-懂业务
-分任务开发
-ui团队
-根据原型图设计切图
-前端团队
-UI设计
-前端写代码(pc,小程序,移动端)
-后端团队
-架构,数据库设计
-分任务开发:用户,商品板块
-联调测试
-测试
-项目上线
# 你平时工作的流程是什么样的
-到了公司打开电脑---》登录公司的协作平台(禅道)---》看自己的任务和bug----》开始开发---》开发完提交到git----》如果有需求不明确,找产品经理讨论,bug找测试讨论
路飞项目需求
# 线上销售课程的
-商城
-知识付费类
# 需求
-首页功能
-轮播图接口
-推荐课程接口
-用户功能
-用户名密码登录
-手机号验证码登录
-发送手机验证码
-验证手机号是否注册过
-注册接口
-课程列表功能
-课程列表接口
-排序,过滤,分页
-课程详情
-课程详情接口
-视频播放功能
-视频托管(第三方,自己平台)
-下单功能
-支付宝支付:生成支付链接,付款,回调修改订单状态
-购买成功功能
# 原型图参照博客的具体图片
pip 永久换源
# pip install 下载比较慢----》第三方包都在 pypi 上,国外的,下载起来比较慢
# 临时换源:pip install -i 源地址(清华,阿里,豆瓣)
# 永久换源
> windows
-1 在文件地址栏输入:%APPDATA% 回车,快速进入 C:\Users\电脑用户\AppData\Roaming 文件夹中
-2 新建 pip 文件夹并
-3 在文件夹中新建 pip.ini 配置文件
-4 配置文件写入:
[global]
index-url = https://mirrors.aliyun.com/pypi/simple
[install]
use-mirrors =true
mirrors =https://mirrors.aliyun.com/pypi/simple
trusted-host =mirrors.aliyun.com
> mac配置或linux
1、在用户根目录下 ~ 下创建 .pip 隐藏文件夹,如果已经有了可以跳过
-- 打开terminal,敲 cd
-- mkdir ./.pip
2、进入 .pip 隐藏文件夹并创建 pip.conf 配置文件
-- cd ~/.pip && touch pip.conf
3、新增 pip.conf 配置文件内容
[global]
index-url = https://mirrors.aliyun.com/pypi/simple
[install]
use-mirrors =true
mirrors =https://mirrors.aliyun.com/pypi/simple
trusted-host =mirrors.aliyun.com
虚拟环境和虚拟环境搭建
# 场景
-写了个项目,使用djagno2.x版本---》django2.x装在了解释器上
-后来又有个项目,使用使用djagno3.x版本---》django3.x装在解释器上
-以后要打开第一个项目运行,需要卸载django3,安装django2
-有种方式解决这个问:
-每个项目自己有个环境,装的模块,都是这个项目自己的
# 使用虚拟环境解决上述问题
-Virtualenv 第三方的,用的多
-pipenv 官方的
# Virtualenv使用步骤 win 平台
1 安装两个模块
pip3.8 install virtualenv # 第三方虚拟环境
pip3.8 install virtualenvwrapper-win # 增加模块,使虚拟环境在win上更好用
2 配置环境变量:
# 控制面板 => 系统和安全 => 系统 => 高级系统设置 => 环境变量 => 系统变量 => 点击新建 => 填入变量名与值
变量名:WORKON_HOME 变量值:自定义存放虚拟环境的绝对路径
WORKON_HOME: D:\Virtualenvs
3 同步配置信息
# 去向Python3的安装目录 => Scripts文件夹 => virtualenvwrapper.bat => 双击
4 重新打开命令窗口,可以执行下面的命令
# 1、创建虚拟环境到配置的WORKON_HOME路径下,一旦进入到虚拟环境,所有安装模块操作,都是操作虚拟环境
# 1 选取默认Python环境创建虚拟环境:
-- mkvirtualenv 虚拟环境名称 # 默认以 python 这个解释器来创建虚拟环境
# 基于某Python环境创建虚拟环境:创建虚拟环境并进入虚拟环境
-- mkvirtualenv -p python3.8 虚拟环境名称
# 2、查看已有的虚拟环境
-- workon
# 3、使用某个虚拟环境
-- workon 虚拟环境名称
# 4、进入|退出 该虚拟环境的Python环境
-- python | exit()
# 5、为虚拟环境安装模块
-- pip或pip3 install 模块名
# 6、退出当前虚拟环境
-- deactivate
# 7、删除虚拟环境(删除当前虚拟环境要先退出)
-- rmvirtualenv 虚拟环境名称
-- 直接删文件夹
# mac或linux下安装虚拟环境
1 安装模块
pip3 install -i https://pypi.douban.com/simple virtualenv
pip3 install -i https://pypi.douban.com/simple virtualenvwrapper
2 复制virtualenvwrapper.sh到/usr/local/bin路径下
# 先找到virtualenvwrapper的工作文件 virtualenvwrapper.sh,该文件可以刷新自定义配置,但需要找到它
# MacOS可能存在的位置 /Library/Frameworks/Python.framework/Versions/版本号文件夹/bin
# Linux可能所在的位置 /usr/local/bin | ~/.local/bin | /usr/bin
# 建议不管virtualenvwrapper.sh在哪个目录,保证在 /usr/local/bin 目录下有一份
# 如果不在 /usr/local/bin 目录,如在 ~/.local/bin 目录,则复制一份到 /usr/local/bin 目录
-- sudo cp -rf /路径/virtualenvwrapper.sh /usr/local/bin
3 配置环境变量
# 在 ~/.bash_profile 完成配置,virtualenvwrapper的默认默认存放虚拟环境路径是 ~/.virtualenvs
# WORKON_HOME=自定义存放虚拟环境的绝对路径,需要自定义就解注
VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3 # 指定的是那个解释器
source /usr/local/bin/virtualenvwrapper.sh # 指定的是virtualenvwrapper.sh
4 在终端让配置生效:
source ~/.bash_profile
# 什么是环境变量
-我们在命令行中,执行一个命令,写命令名字执行
-1 当前路径下有这个可执行文件
-2 这个可执行文件,在环境变量中
-加环境变量的目的
-在任意路径下敲 可执行文件都可以,原因是当前敲的可执行文件路径在环境变量中
-两层
-用户环境变量:只有当前用户生效
-系统环境变量:所有用户都生效
# mac平台,需要用命令行操作,文件
用户环境变量:.bash_profile
luffy后台创建目录调整
# 创建django项目 两种方式
-命令行
django-admin startproject 项目名
-pycharm创建
# 命令行
1 workon luffy
2 pip install django==3.2.12
3 django-admin startproject luffy_api
# pycharm 创建项目
-要选中咱们的虚拟环境---》如果虚拟环境不在,要新增进去
# 打开的项目没有使用虚拟环境,在pycharm中如何配置

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

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

项目开始
数据库
# 软件开发模式
-学bbs,设计创建出所有表,直接迁移,后期没改过 --》瀑布开发模式
-学路飞,先设计,开发一点,测试上线一点-----》敏捷开发
# 如果用户表想用 auth的user表扩写,在一开始就要定好
#1 使用mysql 数据库,创建一个库,navicate创建即可
#2 新建一个user app,基于auth的user表扩写用户表
class User(AbstractUser):
mobile = models.CharField(max_length=11, unique=True)
# 需要pillow包的支持
icon = models.ImageField(upload_to='icon', default='icon/default.png')
class Meta:
db_table = 'luffy_user' # 指定表名
verbose_name = '用户表' # 后台管理看到的中文
verbose_name_plural = verbose_name
def __str__(self): # 打印对象,显示的
return self.username
# 3 迁移数据(mysql-->配置文件配置)
-项目的数据库用户,不使用root用,新建一个mysql用户给项目用
-因为root用户权限太大了,新建用户权限小一些
-创建一个luffy用户,授权,只授予luffy库的权限
# 查看当前数据库有哪些用户
select user,host,password from mysql.user;
# 创建luffy用户
# 创建一个用户叫luffy,密码是:Luffy123?,可以本地链接,对luffy库所有表有权限
grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123?';
# 创建一个用户叫luffy,密码是:Luffy123?,可以远程地链接,对luffy库所有表有权限
grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
# 删除一个用户
DROP USER '用户'@'连接方式';(localhost, %)
# django项目如果使用pymsql链接mysql,需要加两句话,加在哪不重要,重要的是它一定要执行
import pymysql
pymysql.install_as_MySQLdb()
django 2 高一点的版本就会报错,需要改源码,麻烦
# 咱们以后,使用mysqlclient 操作mysql不需要任何配置,就可以操作mysql
- pip install mysqlclient
- pip install pillow # 下载这个是因为数据库里面要用
# 4 执行迁移文件
python.exe manage.py makemigrations
python.exe manage.py migrate
数据库之隐藏密码
# 我们直接把mysql的用户名和密码 写死在了代码中----》后期可能会存在风险----》代码如果泄露----》mysql的用户密码泄露----》可以远程登录----》脱裤(拖库)----》所有数据会被黑客获取到
# 我们不把敏感信息写死在代码中
-使用从环境变量中获取
-PATH: 任意路径下敲可执行文件能找到
-其它key value 是该机器的全局配置,可以直接拿到
-使用python代码拿到
##### python代码获取环境变量的值#####
# 配置完环境变量,重启一下pycharm
# 如果环境变量没配置,就用默认的
# 如果配置了,就用配置的(项目上线时候,运维配置环境变量---》运维配置的环境变量的值----》开发根本不知道)
import os
res=os.environ.get('PWD','123?')
print(res)
res=os.environ.get('USER','gjl')
print(res)
###### 具体操作#####
# 使用get获取值,后面给上默认值,当在本地时,可以通过环境变量获取到用户名和密码,当在别的机器上是使用默认值登录
user = os.environ.get('USER', 'gjl')
pwd = os.environ.get('PWD', '123')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'luffy', # 数据库名字
'USER': user, # luffy用户
'PASSWORD': pwd,
'HOST': 'localhost',
'PORT': 3306
}
}

封装logger
# 项目运行,会产生很多日志
# 日志作用:记录程序运行过程中 错误,警告,程序员的输出,观察项目运行情况
# 每个项目都需要记录日志
# django中加入记录日志的功能
1 把如下代码,copy到配置文件中
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': { # 日志的输出格式
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
},
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': { # 日志输出的路径 console输出在终端
# 实际开发建议使用WARNING
'level': 'WARNING', # 日志级别
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': { # 日志输出到文件
# 实际开发建议使用ERROR
'level': 'ERROR', # 日志级别
'class': 'logging.handlers.RotatingFileHandler',
# 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"),
# 日志文件的最大值,这里我们设置300M
'maxBytes': 300 * 1024 * 1024,
# 日志文件的数量,设置最大日志数量为10
'backupCount': 10,
# 日志格式:详细格式
'formatter': 'verbose',
# 文件内容编码
'encoding': 'utf-8'
},
},
# 日志对象
'loggers': {
'django': {
'handlers': ['console', 'file'],
'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统
},
}
}
2 获取logger对象:utils/common_logger.py
# 日志对象获取,以后想用日志的地方,直接导入,使用 logger.info error....
import logging
logger=logging.getLogger('django')
3 以后想用日志的地方,直接导入使用即可
# DEBUG < INFO < WARNING < ERROR < CRITICAL
logger.debug('debug级别')
logger.info('info级别')
logger.warning('warning级别')
logger.error('error级别')
logger.critical('CRITICAL级别')
4 以后想用print的位置,都用logger.info,以后项目上线,调高日志输出级别,虽然代码中写了日志输出,实际上并不会输出
环境变量设置
# 1 为什么一定要设置默认值
user = os.environ.get('USER', 'gjl')
pwd = os.environ.get('PWD', '123')
# 2 这种环境变量,一般写死在系统,而是临时设置,系统重启就失效
win:图形化界面的设置
mac:
.bash_profile:只在当前会话生效,当前窗口中生效,关掉窗口,再打开,就失效了,source .bash_profile
.zshrc:当前用户永久生效
linxu: 文件可能名字不一样,配置方式跟mac完全一样
# 临时设置:运维上线时候,手动设置,当前会话生效
win:
set xx=lqz # 设置环境变量
echo %xx%
linux ,mac:
export xx=lqz
echo $xx
项目部署:打开一个命令窗口
设置一堆环境变量
python manage.py runserver
封装全局异常
# utils/common_exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
from utils.common_logger import logger
#### 加入日志记录,只要走到这,说明程序出error了,程序的error,咱们都要记录日志,方便后期排查
### 日志记录尽量详细:ip;如果用户登录了,记录用户;请求地址是;执行那个视图类出的错
def common_exception_handler(exc, context):
request = context.get('request')
view = context.get('view')
ip = request.META.get('REMOTE_ADDR')
try:
user_id = request.user.pk
except:
user_id = '【未登录用户】'
path = request.get_full_path()
view_str = str(view)
res = exception_handler(exc, context)
logger.error('用户地址为:%s,用户id号为:%s,请求地址为:%s,执行的视图函数为:%s' % (ip, user_id, path, view_str))
if res:
# drf的异常,一种是从res.data这个字典的detail中取,一种是 直接取data
if isinstance(res.data, dict):
data = {'code': 999, 'msg': res.data.get('detail', '系统错误,请联系系统管理员')}
else:
data = {'code': 998, 'msg': res.data}
else:
# django的异常
data = {'code': 888, 'msg': str(exc)}
return Response(data)
### 配置文件配置
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.common_exceptions.common_exception_handler',
}
封装Response
# 咱们之前使用drf提供的Response类,用它的时候,需要传很多参数,基于它做封装,方便我们的使用
data=None,
status=None,
headers=None,
# 封装后达成的效果是 APIResponse
return APIResponse()
----->前端收到的是 {code:100,msg:成功}
return APIResponse(token=12q4dfasdf,username=lqz)
----->前端收到的是 {code:100,msg:成功,token:12q4dfasdf,username:lqz}
return APIResponse(data=[{name:红楼梦,price:33},{name:西游记,price:33}])
----->前端收到的是 {code:100,msg:成功,data:[{name:红楼梦,price:33},{name:西游记,price:33}]}
return APIResponse(code=101,msg='失败')
----->前端收到的是 {code:101,msg:失败}
return APIResponse(headers={'xx':'xx'})
----->前端收到的是 {code:100,msg:成功},但是相应中有xx:xx
return APIResponse(name=lqz,status=201)
----->前端收到的是 {code:100,msg:成功,name:lqz},但是响应状态码是201
# utils/common_response.py
from rest_framework.response import Response
class APIResponse(Response):
def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs):
data = {'code': code, 'msg': msg}
# kwargs 有可能是 {token:asdfad,name:lqz}
if kwargs:
data.update(kwargs)
# 调用父类(Response)的__init__完成初始化
super().__init__(data=data, status=status, headers=headers)
开启media访问
# 头像,课程图片,放在项目的某个目录下 (media),后期需要能够访问
# 需要开启media的访问
urlpatterns = [
path('admin/', admin.site.urls),
path('media/<path:path>', serve,{'document_root':settings.MEDIA_ROOT}),
]
前端配置
# 使用vue2创建一个新的前端项目
vue create luffy_city
# 安装第三方
- axios : cnpm install -S axios
- elementui : cnpm install -S vue-cookies
- vue-cookies : cnpm i element-ui@2.6.2 -S
# 配置第三方
// elementui配置
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
// axios配置
import axios from "axios";
vue.prototype.$axios=axios
// vue-cookies配置
import cookies from 'vue-cookies'
Vue.prototype.$cookies=cookies
# vue.prototype
vue.prototype是注册全局变量的一种方式,使用vue.prototype的变量可以全局访问,如上axios和cookies以后使用就是this.$axios和this.$cookies使用,就不需要再导入使用,再定义变量时前面加上$是为了防止命名冲突
# 写一个全局配置settings.js 文件
# assets--》js--》settings.js
export default {
BASE_URL: 'http://127.0.0.1:8000/api/v1/'
}
# main.js中
import settings from '@/assets/js/settings'
Vue.prototype.$settings=settings
# 以后再任意组件中
this.$settings.BASE_URL
# 全局样式
html的标签a ul li ,都会有默认样式,正常的前端,都会去掉所有标签的默认样式, 自己写样式
# 1 assets--》css--》global.css
/* 声明全局样式和项目的初始化样式 */
body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea {
margin: 0;
padding: 0;
font-size: 15px;
}
a {
text-decoration: none;
color: #333;
}
ul {
list-style: none;
}
table {
border-collapse: collapse; /* 合并边框 */
}
# 2 main.js 配置,样式全局生效
// 使用全局样式,取出所有标签默认样式
import '@/assets/css/global.css'
后台主页模块设计
# 1 创建后台主页模块(一个模块一个app)
python ../../manage.py startapp home
# 2 在models中写轮播图表
-写一个基表BaseModel
-写轮播图表
# 3 迁移
'''考虑到轮播图与课程表有相同的字段,所以就将公共的字段提取成一个虚拟表'''
### BaseModel##########
from django.db import models
class BaseModel(models.Model):
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
is_delete = models.BooleanField(default=False, verbose_name='是否删除')
is_show = models.BooleanField(default=True, verbose_name='是否上架')
orders = models.IntegerField(verbose_name='优先级')
class Meta:
abstract = True # 这个表模型只用来继承,不用来在数据库中生成表
##########Banner########
from django.db import models
from utils.common_models import BaseModel
class Banner(BaseModel):
# 1 id img图片地址 长传时间 是否删除 是否显示 order
# 把公共字段抽取到某个基表中,以后要使用,直接继承基表,扩写自己的字段就可以了---》用过的AbstractUser就是这个原理
title = models.CharField(max_length=16, unique=True, verbose_name='名称')
image = models.ImageField(upload_to='banner', verbose_name='图片')
# 点击图片,调整到的路径
# 前端跳转的地址: 前端路由 完整的http链接
link = models.CharField(max_length=64, verbose_name='跳转链接')
info = models.TextField(verbose_name='详情') # 也可以用详情表,宽高出处
class Meta:
db_table = 'luffy_banner'
verbose_name_plural = '轮播图表'
def __str__(self):
return self.title
simpleui后台管理
# 官网
https://simpleui.72wo.com/docs/simpleui/QUICK.html#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AAdjango%E9%A1%B9%E7%9B%AE
# 下载与配置
pip install django-simpleui -i https://pypi.tuna.tsinghua.edu.cn/simple
INSTALLED_APPS = [
'simpleui',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
...
]
# 录入数据
## 再admin中注册
from .models import Banner
admin.site.register(Banner)
###创建admin用户添加数据
python.exe manage.py createsuperuser
# 正常在公司中,网站分主站和后台管理
# 后台管理,主要是运营录入数据,使用simpleui
轮播图接口
封装自己的mixin
from rest_framework.mixins import ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin, \
RetrieveModelMixin
from utils.common_response import APIResponse
class CommonListModelMixin(ListModelMixin):
def list(self, request, *args, **kwargs):
res = super().list(request, *args, **kwargs)
return APIResponse(data=res.data)
class CommonCreateModelMixin(CreateModelMixin):
def create(self, request, *args, **kwargs):
res = super().create(request, *args, **kwargs)
return APIResponse(data=res.data) # {code:100,msg:成功,data:{}}
class CommonUpdateModelMixin(UpdateModelMixin):
def update(self, request, *args, **kwargs):
res = super(CommonUpdateModelMixin, self).update(request, *args, **kwargs)
return APIResponse(data=res.data)
class CommonRetrieveModelMixin(RetrieveModelMixin):
def retrieve(self, request, *args, **kwargs):
res = super(CommonRetrieveModelMixin, self).retrieve(request, *args, **kwargs)
return APIResponse(data=res.data)
class CommonDestroyModelMixin(DestroyModelMixin):
def destroy(self, request, *args, **kwargs):
res = super().destroy(request, *args, **kwargs)
return APIResponse(msg='删除成功')
视图
# 查询所有 轮播图接口
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from .serialzier import BannerSerializer
from .models import Banner
from utils.common_mixin import CommonListModelMixin as ListModelMixin
class BannerView(GenericViewSet, ListModelMixin):
queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = BannerSerializer
路由
# 使用路由分发
## 总路由
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/home/', include('home.urls')),
# static 默认开启的,后期咱们会开启media文件夹,除此之外的其它文件夹,尽量不要开放,让外部访问
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
]
# 分路由
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
from .views import BannerView
router.register('banner', BannerView, 'banner')
urlpatterns = [
# path('admin/', admin.site.urls),
]
urlpatterns += router.urls
序列化类
from rest_framework import serializers
from .models import Banner
class BannerSerializer(serializers.ModelSerializer):
class Meta:
model = Banner
fields = ['id', 'image', 'title', 'link']
自定义配置文件
# 通过配置文件控制显示多少条轮播图
# 使用步骤
1 在settings下新建:common_settings.py
2 在dev.py中加入
from .common_settings import *
3 在轮播图接口上
class BannerView(GenericViewSet, ListModelMixin):
queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
serializer_class = BannerSerializer
跨域问题详解
# 以后只要前后端分离项目,都会出现跨域问题,咱们要解决
# 同源策略
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现
请求的url地址,必须与浏览器上的url地址处于同域上:也就是[域名],[端口],[协议]相同.
http://127.0.0.1:8080
ftf://127.0.0.1:8080
比如:我在本地上的域名是127.0.0.1:8000,请求另外一个域名:127.0.0.1:8001一段数据
浏览器上就会报错,个就是同源策略的保护,如果浏览器对javascript没有同源策略的保护,那么一些重要的机密网站将会很危险
请求发送,服务的执行,数据也正常返回,只是被浏览器拦截了
# 正因为同源策略的存在,咱们写前后端分离的项目,无法正常获取到数据
# 解决跨域问题:
1 jsonp 跨域(不了解)
2 跨域资源共享(CORS) 后端技术
3 Nginx代理
# CORS:跨域资源共享
CORS需要浏览器和服务器同时支持,所有浏览器都支持该功能
只需要服务的处理即可:只需要在在响应头中加入固定的头就实现cors---》比如在响应头中加入Access-Control-Allow-Origin='*'---->get请求就没有跨域了---》但是put请求还会有
# cors的请求分两种
-简单请求,浏览器直接发起
-非简单请求,浏览器先发送要给options预检请求,服务端允许,再发送真正的请求
# 什么是简单请求,什么是非简单请求
# 如果属于下面,就是简单请求
1 请求方法是以下三种方法之一:
HEAD
GET
POST
2 HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
# 使用cors解决跨域,就是再响应头中加入固定的一些东西,专门写个中间件
res['Access-Control-Allow-Headers'] = 'token'
res['Access-Control-Allow-Methods'] = 'DELETE'
res['Access-Control-Allow-Origin'] = 'http://192.168.1.252:8080'
### 补充:######
前端访问的后端地址,一定要准确
自定义中间件,解决跨域问题
##### common_mideleware.py
from django.utils.deprecation import MiddlewareMixin
### 自定义中间件解决跨域问题---》以后其它框架都是这个原理---》django上有人做了
class CorsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
if request.method == 'OPTIONS':
response['Access-Control-Allow-Headers'] = 'token'
response['Access-Control-Allow-Methods'] = 'DELETE'
response['Access-Control-Allow-Origin'] = '*'
return response
### 配置文件配置中间件
MIDDLEWARE = [
'utils.common_mideleware.CorsMiddleware'
]
使用第三方 django-cors-headers
####使用pip安装
pip install django-cors-headers
#####添加到setting的app中
INSTALLED_APPS = (
...
'corsheaders',
...
)
#### 添加中间件
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
...
]
#### 4、setting下面添加下面的配置
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_METHODS = (
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
'VIEW',
)
CORS_ALLOW_HEADERS = (
'XMLHttpRequest',
'X_FILENAME',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'Pragma',
'token'
)
django-cors-headers源码
# 核心代码再中间件的---》process_response 3.0.14 版本
class CorsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
if (
not conf.CORS_ALLOW_ALL_ORIGINS
and not self.origin_found_in_white_lists(origin, url)
and not self.check_signal(request)
):
return response
if conf.CORS_ALLOW_ALL_ORIGINS and not conf.CORS_ALLOW_CREDENTIALS:
response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
else:
response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
if request.method == "OPTIONS":
response[ACCESS_CONTROL_ALLOW_HEADERS] = ", ".join(conf.CORS_ALLOW_HEADERS)
response[ACCESS_CONTROL_ALLOW_METHODS] = ", ".join(conf.CORS_ALLOW_METHODS)
return response
前台主页功能
Banner.vue
<template>
<div>
<el-carousel height="400px">
<el-carousel-item v-for="item in banner_list" :key="item">
<!-- router-link只能跳内部,不能跳外部-->
<router-link :to="item.link" v-if="item.link.indexOf('http:')<0">
<img :src="item.image" :alt="item.title">
</router-link>
<a :href="item.link" v-else>
<img :src="item.image" :alt="item.title">
</a>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script>
export default {
name: "Banner",
data() {
return {banner_list: []}
},
created() {
this.$axios.get(`${this.$settings.BASE_URL}home/banner/`).then(res => {
this.banner_list = res.data.data
})
}
}
</script>
<style scoped>
.el-carousel__item h3 {
color: #475669;
font-size: 18px;
opacity: 0.75;
line-height: 300px;
margin: 0;
}
.el-carousel__item {
height: 400px;
min-width: 1200px;
}
.el-carousel__item img {
height: 400px;
margin-left: calc(50% - 1920px / 2);
}
</style>
Footer.vue
<template>
<div class="footer">
<ul>
<li>关于我们</li>
<li>联系我们</li>
<li>商务合作</li>
<li>帮助中心</li>
<li>意见反馈</li>
<li>新手指南</li>
</ul>
<p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p>
</div>
</template>
<script>
export default {
name: "Footer"
}
</script>
<style scoped>
.footer {
width: 100%;
height: 128px;
background: #25292e;
color: #fff;
}
.footer ul {
margin: 0 auto 16px;
padding-top: 38px;
width: 810px;
}
.footer ul li {
float: left;
width: 112px;
margin: 0 10px;
text-align: center;
font-size: 14px;
}
.footer ul::after {
content: "";
display: block;
clear: both;
}
.footer p {
text-align: center;
font-size: 12px;
}
</style>
Header.vue
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
</li>
</ul>
<div class="right-part">
<div>
<span>登录</span>
<span class="line">|</span>
<span>注册</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
url_path: sessionStorage.url_path || '/',
}
},
methods: {
goPage(url_path) {
// 已经是当前路由就没有必要重新跳转
if (this.url_path !== url_path) {
this.$router.push(url_path);
}
sessionStorage.url_path = url_path;
},
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
}
}
</script>
<style scoped>
.header {
background-color: white;
box-shadow: 0 0 5px 0 #aaa;
}
.header:after {
content: "";
display: block;
clear: both;
}
.slogan {
background-color: #eee;
height: 40px;
}
.slogan p {
width: 1200px;
margin: 0 auto;
color: #aaa;
font-size: 13px;
line-height: 40px;
}
.nav {
background-color: white;
user-select: none;
width: 1200px;
margin: 0 auto;
}
.nav ul {
padding: 15px 0;
float: left;
}
.nav ul:after {
clear: both;
content: '';
display: block;
}
.nav ul li {
float: left;
}
.logo {
margin-right: 20px;
}
.ele {
margin: 0 20px;
}
.ele span {
display: block;
font: 15px/36px '微软雅黑';
border-bottom: 2px solid transparent;
cursor: pointer;
}
.ele span:hover {
border-bottom-color: orange;
}
.ele span.active {
color: orange;
border-bottom-color: orange;
}
.right-part {
float: right;
}
.right-part .line {
margin: 0 10px;
}
.right-part span {
line-height: 68px;
cursor: pointer;
}
</style>
HomeView.vue
<template>
<div class="home">
<Header></Header>
<Banner></Banner>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<Footer></Footer>
</div>
</template>
<script>
import Banner from "@/components/Banner";
import Footer from '@/components/Footer'
import Header from "@/components/Header";
export default {
name: 'HomeView',
// created() {
//
// this.$axios.get('http://127.0.0.1:8000/api/v1/home/test/', {
// headers: {
// token: 'sdfafasdf'
// }
//
// }).then(res => {
// console.log(res.data.data)
// })
// }
components: {
Header, Footer, Banner
}
}
</script>
导出项目依赖
# 以后所有python项目的根路径下,都会有个 requirements.txt 【约定俗称的名字】,这里面记录了当前项目所有的依赖---》格式如下
Django==3.2.12
redis # 最新版
# 我们也要设置
-笨办法
直接手动建立
-一键导出(虚拟环境中:虚拟环境用的模块,就是当前项目的模块,一般不会少,不会多,如果多,可以手动删掉)
-pip list
-pip freeze >requirements.txt
首页推荐课程前端
HomeView.vue
<template>
<div class="home">
<Header></Header>
<Banner></Banner>
<div>
<el-row>
<el-col :span="6" v-for="(o, index) in 8" :key="o" class="course_detail">
<el-card :body-style="{ padding: '0px' }">
<img src="http://photo.liuqingzheng.top/2023%2002%2022%2021%2057%2011%20/image-20230222215707795.png"
class="image">
<div style="padding: 14px;">
<span>热门课程</span>
<div class="bottom clearfix">
<time class="time">价格:199</time>
<el-button type="text" class="button">查看详情</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<img src="http://photo.liuqingzheng.top/2023%2003%2001%2016%2010%2034%20/1.png" alt="" width="100%" height="500px">
<Footer></Footer>
</div>
</template>
<script>
import Banner from "@/components/Banner";
import Footer from '@/components/Footer'
import Header from "@/components/Header";
export default {
name: 'HomeView',
// created() {
//
// this.$axios.get('http://127.0.0.1:8000/api/v1/home/test/', {
// headers: {
// token: 'sdfafasdf'
// }
//
// }).then(res => {
// console.log(res.data.data)
// })
// }
components: {
Header, Footer, Banner
}
}
</script>
<style scoped>
.time {
font-size: 13px;
color: #999;
}
.bottom {
margin-top: 13px;
line-height: 12px;
}
.button {
padding: 0;
float: right;
}
.image {
width: 100%;
display: block;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.course_detail {
padding: 50px;
}
</style>
git
介绍和安装
# 版本管理软件
-1 对代码版本进行管理---》首页功能完成---》课程功能完成---》可以回退到某个版本
-2 协同开发--》多人开发--》合并代码---》可能会有冲突,解决冲突
# 版本管理软件:主流就两个
-git:现在用的最多(学git)
-svn:老
# git与svn比较
-svn:cs架构 一个服务端,多个客户端,如果服务端挂掉,整个代码合并,提交代码就做不了了,只能本地开发代码
-git:分布式管理,装git的客户端,既可以当客户端,又可以当服务端,如果git远程仓库挂掉,本地可以继续做代码版本的管理
# 安装git,就是个软件
-https://git-scm.com/download/win
-官网下载,一路下一步
-再命令行中:git version 如果有翻译,说明装好了
git,github,gitee,gitlab
# git :版本管理软件,可以做版本管理
# github:它是一个网站:https://github.com/ 全球最大的开源代码管理仓库,git远程仓库
-运营商不让访问
# gitee:中国最大的开源代码管理仓库(私有仓库)
-https://gitee.com/kitego/hashmart
# gitlab: 公司内部搭建自己的远程仓库,只在公司内部用,外网访问不到(到公司用这个多)
git使用流程

# https://www.cnblogs.com/liuqingzheng/p/15328319.html
# https://www.cnblogs.com/liuqingzheng/articles/17146214.html
# git 分3个区---》三个区的来回操作
-工作区:存放代码的文件夹,只要工作区文件发生变(修改,新增,删除)---》
-暂存区:工作的变更,提交到暂存区 git add . 把工作区变更提交到暂存区
-版本库:暂存区内容,放到版本库,被版本管理---》git commit -m ''
git常用命令
# 0 再某个位置,右键---》git bash here ----》打开命令窗口---》等同于cmd---》在这个命令窗口里可以执行 linux命令,来操作win
# 1 初始化仓库,在某个文件夹下执行
git init # 在当前文件夹下就会创建出 .git 文件夹,这个就会被git管理
git init xxx # 在当前路径下创建 xxx文件夹,并用git管理xxx文件夹
# 1.1 配置用户
#### 全局配置 以后所有的版本提交时,都用这个用户和邮箱--》C:\Users\oldboy\.gitconfig
git config --global user.name '用户名'
git config --global user.email '用户邮箱'
#### 局部配置 只在当前 仓库生效--》仓库路径下 .git 文件夹下 config文件中配置的
git config user.name '用户名'
git config user.email '用户邮箱'
# 查看用户
git config -l
# 2 查看仓库状态
git status # 红 绿
# 如果是红色,表明是在工作发生了变化,没有提交到暂存区
# 如果是绿色:表明,暂存区数据没有提交到版本库
# 如果没有东西,表示当前目录下所有文件被git管理了,被版本管理了
# 3 把工作区变更,提交到暂存区
git add . # 当前目录下所有变更都提交
git add 1.txt # 只提交当前目录下 1.txt这个文件的变更
# 4 把暂存区内容,提交到版本库(只要被版本管理的东西,你尽管操作,后期都能回退回来)
git commit -m '我的第一次,提交' # 如果不设置用户,提交不了,不知道是谁提交
# 5 查看版本信息(提交过哪些版本,注释是什么)【可以按作者,时间过滤】
git log
git relog
---------------了解-----------
# 6 把工作区变更回退
git checkout . # 当前路径下所有
# 7 把暂存区内容,拉回到工作区(由绿变红)
git reset HEAD
# 8 从版本库拉回到暂存区(版本库内容回退,变绿)---》需要写上一个版本
git reset --soft 1603edf06d7d302ba50c22373c963af15725eda5
# 9 把版本库退回到工作区(版本库内容回退,变红)
git reset --mix 1603edf06d7d302ba50c22373c963af15725eda5
# 10 把版本库直接完整回退会工作区(增加的也没了)
git reset --hard 1603edf06d7d302ba50c22373c963af15725eda5
# 11 回退到某个版本的样子(可能会用)
git reset --hard 19f5891
# 总结:
git add
git commit -m ''
# 只要被版本管理的文件,即便以后删除了,也能回来
git忽略文件
# 写项目,会有一些文件或文件夹,不希望被git管理,忽略掉它, 不被git管理
-.idea
-node_models
-xx
# 需要写个忽略文件 .gitignore 必须叫它,没有后缀名
在里面写忽略的文件或文件夹,写法如下
.idea # 忽略idea文件夹及其下面所有的文件
lqz.txt # 忽略仓库中所有的lqz.txt
/lqz.txt # 忽略当前路径下的lqz.txt
a/lqz.txt # 只忽略当前路径下a文件夹下lqz.txt
*x*:名字中有一个x的都会被过滤(*代表0~n个任意字符)
# 给路飞做忽略文件
.idea
*.log
__pycache__
*.pyc
scripts
git多分支
# 查看分枝
git branch # 查看本地分支
git branch -a # 查看所有分区本地与远程
# 创建分枝
git branch [分支名称]
# 删除分支
git branch -d [分支名称]
# 合并分支
git branch dev
git checkout dev
新增一个文件 xx.txt,加入一行
git add .
git commit -m 'dev分支增加了xx.txt'
修改lqz.txt 加入一样
git add .
git commit -m 'lqz.txt 加入内容'
# 切回到主分支(增的东西,都没有)
# 新增的文件,看不到
# 把dev合并到master上,要在master身上
git merge dev
git远程仓库
# 我们要协同开发,代码要提交到远程仓库,可以使用gitee,github,gitlab,本文以gitee为例
#1 以gitee为例,注册账号
#2 在账号中,新建一个仓库 [本地仓库,推送到远程仓库]
#3 如图所示 ----》[如果创建远程仓库不是空的,就会有问题]
#4 本地仓库,推到远端
-本地已经有了
# git remote 查看有哪些远程仓库
# git remote remove origin # 删除本地跟远程仓库的链接关系
cd lqz
# git remote add 远程仓库名字 远程仓库地址
git remote add origin https://gitee.com/liuqingzheng/lqz.git
git push origin master # 把本地仓库中所有的内容,提交到远程仓库
# 弹出框,要求输入用户名和密码(之前输入过,保存了)
# 本地仓库代码就会被推送到远端了

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

冲突解决
# 出现冲突的原因
-1 多人在同一分支,修改了同一个地方的代码,出现的冲突
-2 分支合并时出冲突
# 1 多人统一分支开发,修改了同样的代码
-某人修改了1.txt的第四行,提交了
-我操作:
-修改了1.txt第四行
-git add .
-git commit -m ' 注释'
-git pull origin master
-出冲突了
<<<<<<< HEAD
我的代码
=======
别人的代码
>>>>>>> af38b6ae4d9e126bd88b9b039e475e8ddbc23510
-处理冲突
-选择要保留的代码,要么删自己的,要么删别人的,要么都留着
-重复操作
git add .
git commot -m '解决冲突'
git push origin master
# 大原则,多人同一分支开发,如果尽量避免冲突,要不停的拉去代码
# 分支合并出冲突
# 新建dev分支,切换,增加代码
git branch dev
git checkout dev
在1.txt最后一行增加 lqz nb1
git add
git commit -m '注释'
# 切换回主分支操作
git checkout master
在1.txt最后一行增加 lqz nb2
git add
git commit -m '注释'
# 合并分支
-出冲突了
<<<<<<< master
我的代码
=======
别人的代码
>>>>>>> dev
# 解决冲突,提交
git add
git cmommit
线上分支合并
# 本地分支合并----》git merge dev
# 有主分支----》开发分支开发完了----》合并到主分支
# 远端创建dev分支---》本地没有,拉去一下就有了 git pull origin dev
# 本地创建dev分支----》远端没有,推送一下就有了 git push origin dev
# 远端创建dev分支,拉去到本地
-远端,在网页中点点点创建分支
-本地:git pull origin dev
-切换过去才能看到:git checkout dev
# 本地和远端现在都有了master和dev分支
-本地的dev分支,删除东西
-提交到本地版本库
-推送到远程 git push origin dev
-远程分支合并
-组员新建pull request---》pr---》(merge request)mr
-组长审核---》同意---》dev就被合并到master
远程仓库回滚(你不要去做)
# 本地
git reset --hard 最初状态
git reset --hard 88aa1e64fa288af495ab6c283b139b7f7f0a237a
git push origin master -f
# 本地代码要提交,本地版本库的内容必须是最新的,git pull 就是最新
为开源项目贡献代码
# 1 gitee 找一个开源项目
# 2 点 fork---》复制一份到你的仓库中
# 3 在咱们仓库中,clone---》修改代码---》提交代码---》自己仓库
# 4 在自己仓库中提交pr---》我们本地dev分支申请提交到作者的dev分支
#5 等作者审核过,同意,你就是贡献者了
git工作流,git pull和git fetch,变基
# git 工作流:git flow---》分支方案
-我们没有采用
# git pull和git fetch
-git pull 从远程仓库拉取代码:从远程获取最新版本并merge到本地
-git fetch 从远程仓库拉取代码:会将数据拉取到本地仓库 - 它并不会自动合并或修改当前的工作
-git pull =git fetch +merge
# 变基 rebase
-1 多个提交记录整合成一个
-2 解决多次合并分叉问题
pycharm操作git
# 实际开发中,可以完全一点命令都不敲,通过pycharm 点点点
# pycharm 配置好git
# clone 代码
# git add 命令
# git commit
# git push
# git 分支操作
# 实用的,代码对比
git reflog 。git log 命令





登录注册页面分析
# 根据原型图分析出:要写的功能
# 用户名密码登录接口
# 注册功能接口
# 手机号验证码登录接口
# 发送短信验证码接口
# 验证手机号是否存在接口
验证手机号是否存在接口
class UserView(ViewSet):
# 验证手机号是否存在接口---》get请求---》跟数据库有关系,但不需要序列化----》自动生成路由
@action(methods=['GET'], detail=False)
def check_mobile(self, request, *args, **kwargs):
try:
mobile = request.query_params.get('mobile', None)
User.objects.get(mobile=mobile) # 有且只有一条才不报错,如果没有或多余一条,就报错
return APIResponse(msg='手机号存在')
except Exception as e:
raise APIException('手机号不存在')
腾讯云短信申请
# 发送短信功能
-网上会有第三方短信平台,为我们提供api,花钱,向它的某个地址发送请求,携带手机号,内容---》它替我们发送短信
-腾讯云短信---》以这个为例
-阿里 大于短信
-容联云通信
# 申请一个公众号---》自行百度---》个人账号
# 如何申请腾讯云短信
-1 地址:https://cloud.tencent.com/act/pro/csms
-2 登录后,进入控制台,搜短信https://console.cloud.tencent.com/smsv2
-3 创建签名:使用公众号
-身份证,照片
-4 模板创建
-5 发送短信
-使用腾讯提供的sdk发送
-https://cloud.tencent.com/document/product/382/43196
# 1 今天讲的所有操作一遍
# 2 写完手机号是否存在接口
# 3 手机号(用户名,邮箱)密码登录接口
# 4 申请腾讯云短信
---------------
# 借助于redis:安装()
-https://github.com/tporadowski/redis/releases/
# 研究 websocket协议,集成都django项目,写一个聊天室
https://zhuanlan.zhihu.com/p/371500343
# websocket 实现不了,就用定时器+http
后端登录注册接口
# 登录注册相关功能
- 1 校验手机号是否存在(已经完成)
- 2 多方式登录接口(手机号,邮箱,用户名都可以登录成功)
- 3 发送短信接口(腾讯云短信)
- 4 短信登录接口
- 5 短信注册接口
视图类
from django.shortcuts import render, HttpResponse
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from django.core.cache import cache
# Create your views here.
from utils.common_logger import logger
from threading import Thread
from user.models import User
from utils.common_response import APIResponse
from rest_framework.exceptions import APIException
from libs.send_tx_sms import get_code, send_sms_by_phone
from .serialzier import LoginSerializer, LoginUserSMSSerializer, UserRegisterSerializer
from rest_framework.mixins import CreateModelMixin
class UserView(GenericViewSet):
serializer_class = LoginSerializer
"""判断使用哪个序列化类"""
def get_serializer_class(self):
if self.action == 'sms_login':
return LoginUserSMSSerializer
elif self.action == 'register':
return UserRegisterSerializer
return self.serializer_class
"""多方式登录"""
@action(methods=['POST'], detail=False)
def login(self, request, *args, **kwargs):
return self._common_login(request, *args, **kwargs)
"""短信登录"""
@action(methods=['POST'], detail=False)
def sms_login(self, request, *args, **kwargs):
return self._common_login(request, *args, **kwargs)
"""判断手机号是否注册"""
@action(methods=['GET'], detail=False)
def check_mobile(self, request, *args, **kwargs):
try:
mobile = request.GET.get('mobile', None)
User.objects.get(mobile=mobile)
return APIResponse(msg='手机号已存在')
except Exception as e:
raise APIException('手机号不存在')
"""发送短信"""
@action(methods=['GET'], detail=False)
def send_sms(self, request):
mobile = request.query_params.get('mobile', None)
code = get_code()
cache.set(f'send_sms_code_{mobile}', code)
if mobile: # 开启线程,提高并发
t = Thread(target=send_sms_by_phone, args=[mobile, code])
t.start()
return APIResponse(msg='信息已发送')
raise APIException('请输入手机号')
"""隐藏属性:因为多方式登录与手机验证码登录的视图类中的代码重复,所以将重复的部分写成一个类的隐藏属性,在类中调用"""
def _common_login(self, request, *args, **kwargs):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
username = ser.context.get('username')
token = ser.context.get('token')
icon = ser.context.get('icon')
return APIResponse(username=username, token=token, icon=icon)
"""注册"""
# 如果继承CreateModelMixin注册可以不用写,但是上面序列化类的判断地址要改成self.action == create
@action(methods=['POST'], detail=False)
def register(self, request, *args, **kwargs):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
return APIResponse(msg='注册成功')
序列化类
from rest_framework import serializers
from .models import User
from django.contrib.auth import authenticate
from faker import Faker
from rest_framework_jwt.settings import api_settings
from rest_framework.exceptions import APIException
from django.conf import settings
from django.core.cache import cache
from faker import Faker
from django.contrib.auth.hashers import make_password
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
class CommonLoginSerializer():
def _get_user(self, attrs):
raise APIException('你必须重写')
def _get_token(self, user):
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
def validate(self, attrs):
user = self._get_user(attrs)
token = self._get_token(user)
self.context['username'] = user.username
self.context['icon'] = settings.BACKEND_URL + user.icon.url
self.context['token'] = token
return attrs
# 因为ModelSerializer中有全局钩子,所以要将CommonLoginSerializer写在前面,不然前端self.context获取不到数据
class LoginSerializer(CommonLoginSerializer, serializers.ModelSerializer):
username = serializers.CharField()
class Meta:
model = User
fields = ['username', 'password']
def _get_user(self, attrs):
credentials = {
'username': attrs.get('username'),
'password': attrs.get('password')
}
user = authenticate(**credentials)
if not user:
raise APIException('用户名或密码错误')
return user
class LoginUserSMSSerializer(CommonLoginSerializer, serializers.Serializer):
mobile = serializers.CharField()
code = serializers.CharField()
def _get_user(self, attrs):
mobile = attrs.get('mobile')
code = attrs.get('code')
old_code = cache.get(f'send_sms_code_{mobile}')
if code != old_code:
raise APIException('验证码错误')
user = User.objects.filter(mobile=mobile).first()
if not user:
raise APIException('该手机没有注册')
return user
class UserRegisterSerializer(serializers.ModelSerializer):
"""{"mobile":xxxx, "code":8888, "password":xxx}"""
code = serializers.CharField(max_length=4, min_length=4, write_only=True)
class Meta:
model = User
fields = ['mobile', 'password', 'code']
"""校验验证码"""
def _check_code(self, attrs):
mobile = attrs.get('mobile')
code = attrs.get('code')
old_code = cache.get(f'send_sms_code_{mobile}')
if not (code == old_code or (settings.DEBUG or code == '8888')): # 测试的时候可以使用万能验证码测试
raise APIException('验证码错误')
"""生成随机用户名"""
def _generate_user(self):
fake = Faker()
return fake.first_name()
"""处里入前的数据"""
def _pre_save(self, attrs):
attrs.pop('code')
pwd = attrs.get('password')
attrs['password'] = make_password(pwd) # 对密码进行加密
attrs['username'] = self._generate_user() # 短信注册的话,自动生成一个随机用户名
def validate(self, attrs):
self._check_code(attrs)
self._pre_save(attrs)
return attrs
# 前端如果是继承CreateModelMixin,再处理数据的时候已经将密码加密的话就可以不用写create
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
return user
多方式登录重写authenticate
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
def get_account_by_user(username):
from user.models import User
try:
user = User.objects.get(Q(username=username) | Q(mobile=username) | Q(email=username))
except User.DoesNotExist:
user = None
return user
class UsernameMobileAuthBackend(ModelBackend):
"""实现用户多条件登录"""
def authenticate(self, request, username=None, password=None, **kwargs):
user = get_account_by_user(username)
if user is not None and user.check_password(password) and user.is_active:
return user
# 在配置文件中配置
AUTHENTICATION_BACKENDS = [
'utils.authenticates.UsernameMobileAuthBackend'
]
发送短信
# 获取n位的随机验证码
import random
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 导入对应产品模块的client models。
from tencentcloud.sms.v20210111 import sms_client, models
# 导入可选配置类
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from .settings import *
def get_code(num=4):
code = ''
for i in range(num):
random_num = random.randint(0, 9)
code += str(random_num)
return code
# 发送短信函数
def send_sms_by_phone(mobile, code):
try:
cred = credential.Credential(SECRET_Id, SECRET_KEY)
httpProfile = HttpProfile()
httpProfile.reqMethod = "POST"
httpProfile.reqTimeout = 30
httpProfile.endpoint = "sms.tencentcloudapi.com"
# 非必要步骤:
# 实例化一个客户端配置对象,可以指定超时时间等配置
clientProfile = ClientProfile()
clientProfile.signMethod = "TC3-HMAC-SHA256"
clientProfile.language = "en-US"
clientProfile.httpProfile = httpProfile
client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
req = models.SendSmsRequest()
req.SmsSdkAppId = APP_ID
req.SignName = SIGN_NAME
req.TemplateId = TEMPLATED_ID
req.TemplateParamSet = [code, '10']
req.PhoneNumberSet = ["+86" + mobile]
req.SessionContext = ""
req.ExtendCode = ""
req.SenderId = ""
resp = client.SendSms(req)
# 输出json格式的字符串回包
res_dict = resp._serialize(allow_none=True)
print(res_dict)
if res_dict['SendStatusSet'][0]['Code'] == "Ok":
return True
else:
return False
except TencentCloudSDKException as err:
return False
异步发送短信
# 原来的发送短信,是同步
-前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》调用腾讯发送短信---》腾讯去发短信---》发完后----》回复给我们后端发送成功---》我们后端收到发送成功---》给我们前端返回发送成功
# 把腾讯发送短信的过程,变成异步
-前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》开启线程,去调用腾讯短信发送(异步)---》我们后端继续往后走----》直接返回给前端,告诉前端短信已发送
-另一条发短信线程线程会去发送短信,至于是否成功,我们不管了
@action(methods=['GET'], detail=False)
def send_sms(self, request):
mobile = request.query_params.get('mobile', None)
code = get_code()
cache.set(f'send_sms_code_{mobile}', code)
if mobile: # 开启线程,提高并发
t = Thread(target=send_sms_by_phone, args=[mobile, code])
t.start()
return APIResponse(msg='信息已发送')
raise APIException('请输入手机号')
前端注册页面分析
# 登录,注册,都写成组件----》在任意页面中,都能点击显示登录模态框
# 写好的组件,应该放在那个组件中----》不是页面组件(小组件)
# 点击登录按钮,把Login.vue 通过定位,占满全屏,透明度设为 0.5 ,纯黑色悲剧,覆盖在组件上
# 在Login.vue点关闭,要把Login.vue隐藏起来,父子通信之子传父,自定义事件
Header.vue
## 页面中
<span @click="go_login">登录</span>
<Login v-if="login_show" @close_login="close_login"></Login>
## data
login_show: false
### methods
go_login() {
this.login_show = true
},
close_login() {
this.login_show = false
}
Login.vue
<template>
<div class="login">
<span @click="close">X</span>
</div>
</template>
<script>
export default {
name: "Login",
methods: {
close() {
this.$emit('close_login')
}
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
</style>
前端登录注册页面复制
login.vue
<template>
<div class="login">
<div class="box">
<i class="el-icon-close" @click="close_login"></i>
<div class="content">
<div class="nav">
<span :class="{active: login_method === 'is_pwd'}" @click="change_login_method('is_pwd')">密码登录</span>
<span :class="{active: login_method === 'is_sms'}" @click="change_login_method('is_sms')">短信登录</span>
</div>
<el-form v-if="login_method === 'is_pwd'">
<el-input
placeholder="用户名/手机号/邮箱"
prefix-icon="el-icon-user"
v-model="username"
clearable>
</el-input>
<el-input
placeholder="密码"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-button type="primary">登录</el-button>
</el-form>
<el-form v-if="login_method === 'is_sms'">
<el-input
placeholder="手机号"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="验证码"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary">登录</el-button>
</el-form>
<div class="foot">
<span @click="go_register">立即注册</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: '',
password: '',
mobile: '',
sms: '',
login_method: 'is_pwd',
sms_interval: '获取验证码',
is_send: false,
}
},
methods: {
close_login() {
this.$emit('close')
},
go_register() {
this.$emit('go')
},
change_login_method(method) {
this.login_method = method;
},
check_mobile() {
if (!this.mobile) return;
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
this.is_send = true;
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval = "获取验证码";
this.is_send = true; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒后再发`;
}
}, 1000);
}
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.box {
width: 400px;
height: 420px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 210px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin: 0 20px 0 35px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
Register.vue
<template>
<div class="register">
<div class="box">
<i class="el-icon-close" @click="close_register"></i>
<div class="content">
<div class="nav">
<span class="active">新用户注册</span>
</div>
<el-form>
<el-input
placeholder="手机号"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="密码"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-input
placeholder="验证码"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary">注册</el-button>
</el-form>
<div class="foot">
<span @click="go_login">立即登录</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data() {
return {
mobile: '',
password: '',
sms: '',
sms_interval: '获取验证码',
is_send: false,
}
},
methods: {
close_register() {
this.$emit('close', false)
},
go_login() {
this.$emit('go')
},
check_mobile() {
if (!this.mobile) return;
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
this.is_send = true;
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval = "获取验证码";
this.is_send = true; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒后再发`;
}
}, 1000);
}
}
}
</script>
<style scoped>
.register {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.box {
width: 400px;
height: 480px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 240px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin-left: 90px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
Header.vue
<div class="right-part">
<div>
<span @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
</div>
<Login v-if="is_login" @close="close_login" @go="put_register"/>
<Register v-if="is_register" @close="close_register" @go="put_login"/>
</div>
## data
is_login: false,
is_register: false,
# methods
put_login() {
this.is_login = true;
this.is_register = false;
},
put_register() {
this.is_login = false;
this.is_register = true;
},
close_login() {
this.is_login = false;
},
close_register() {
this.is_register = false;
}
前端登录功能
# 1 多方式登录的axios请求,保存cookie---》Header.vue中要写created 生命周期,取出当前登录用户--》close_login取出当前登录用户
# 2 手机号验证码登录,axios请求,保存cookie
# 3 发送短信验证码
<template>
<div class="login">
<div class="box">
<i class="el-icon-close" @click="close_login"></i>
<div class="content">
<div class="nav">
<span :class="{active: login_method === 'is_pwd'}" @click="change_login_method('is_pwd')">密码登录</span>
<span :class="{active: login_method === 'is_sms'}" @click="change_login_method('is_sms')">短信登录</span>
</div>
<el-form v-if="login_method === 'is_pwd'">
<el-input
placeholder="用户名/手机号/邮箱"
prefix-icon="el-icon-user"
v-model="username"
clearable>
</el-input>
<el-input
placeholder="密码"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form>
<el-form v-if="login_method === 'is_sms'">
<el-input
placeholder="手机号"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="验证码"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary" @click="handleSmSLogin">登录</el-button>
</el-form>
<div class="foot">
<span @click="go_register">立即注册</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: '',
password: '',
mobile: '',
sms: '',
login_method: 'is_pwd',
sms_interval: '获取验证码',
is_send: false, // 如果是true,就是能发送验证码,是false就不能发送验证码
}
},
methods: {
close_login() {
this.$emit('close')
},
go_register() {
this.$emit('go')
},
change_login_method(method) {
this.login_method = method;
},
check_mobile() {
if (!this.mobile) return;
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
// 跟后端交互,查询该手机号是否注册过,如果注册过,才能发送
this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/check_mobile/?mobile=${this.mobile}`).then(res => {
if (res.data.code != 100) {
// 提示该手机号未注册
this.$message({
message: '该手机号未注册,请先注册',
type: 'success'
});
// 把手机号清空
this.mobile = ''
} else {
this.is_send = true; // 可以发送验证码
}
})
},
// 发送手机验证码
send_sms() {
if (!this.is_send) return; // 如果是false,不能点击发送
this.is_send = false; // 点了一次,不能再点了,1 m,定时器执行完 后才能再点
let sms_interval_time = 60;
this.sms_interval = "发送中...";
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval = "获取验证码";
this.is_send = true; // 是true表示,能够点击发送验证码按钮
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒后再发`;
}
}, 1000);
// 调用接口发送验证码
this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/send_sms/?mobile=${this.mobile}`).then(res => {
this.$message({
message: res.data.msg,
type: 'success'
});
})
},
// 用户名密码登录
handleLogin() {
if (this.username && this.password) {
this.$axios.post(`${this.$settings.BASE_URL}user/userinfo/mul_login/`, {
username: this.username,
password: this.password
}).then(res => {
if (res.data.code == 100) {
// 登录成功 返回了token,用户名,头像--->把他们存到cookie中
this.$cookies.set('token', res.data.token, '7d')
this.$cookies.set('username', res.data.username, '7d')
this.$cookies.set('icon', res.data.icon, '7d')
// 页面关闭,子传父,调用 自定义事件close,就会触发父组件中的close_login方法
this.$emit('close')
} else {
this.$message({
message: res.data.msg,
type: 'warning'
});
}
})
} else {
this.$message({
message: '用户名或密码不能为空',
type: 'warning'
});
}
},
// 验证码登录
handleSmSLogin() {
if (this.mobile && this.sms) {
this.$axios.post(`${this.$settings.BASE_URL}user/userinfo/sms_login/`, {
mobile: this.mobile,
code: this.sms
}).then(res => {
if (res.data.code == 100) {
this.$cookies.set('token', res.data.token, '7d')
this.$cookies.set('username', res.data.username, '7d')
this.$cookies.set('icon', res.data.icon, '7d')
this.$emit('close')
} else {
this.$message({
message: res.data.msg,
type: 'warning'
});
}
})
} else {
this.$message({
message: '用户名或密码不能为空',
type: 'warning'
});
}
}
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.box {
width: 400px;
height: 420px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 210px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin: 0 20px 0 35px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
前端注册功能
<template>
<div class="register">
<div class="box">
<i class="el-icon-close" @click="close_register"></i>
<div class="content">
<div class="nav">
<span class="active">新用户注册</span>
</div>
<el-form>
<el-input
placeholder="手机号"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="密码"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-input
placeholder="验证码"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary" @click="handleRegister">注册</el-button>
</el-form>
<div class="foot">
<span @click="go_login">立即登录</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data() {
return {
mobile: '',
password: '',
sms: '',
sms_interval: '获取验证码',
is_send: false,
}
},
methods: {
close_register() {
this.$emit('close', false)
},
go_login() {
this.$emit('go')
},
check_mobile() {
if (!this.mobile) return;
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
// 校验手机号是否能发验证码---》只有没注册过的,才能发
this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/check_mobile/?mobile=${this.mobile}`).then(res => {
if (res.data.code != 100) {
// 提示该手机号未注册
this.$message({
message: '该手机号未注册,可以发送短信',
type: 'success'
});
this.is_send = true; // 可以发送验证码
} else {
// 手机号注册过了,不用注册了,直接登录即可
this.$message({
message: '该手机号已经注册过,可以直接登录',
type: 'success',
onClose: () => {
// 跳转到登录页面
this.$emit('go')
}
});
}
})
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval = "获取验证码";
this.is_send = true; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒后再发`;
}
}, 1000);
// 调用接口发送验证码
this.$axios.get(`${this.$settings.BASE_URL}user/userinfo/send_sms/?mobile=${this.mobile}`).then(res => {
this.$message({
message: res.data.msg,
type: 'success'
});
})
},
handleRegister() {
if (this.mobile && this.sms && this.password) {
this.$axios.post(`${this.$settings.BASE_URL}user/userinfo/register/`, {
mobile: this.mobile,
code: this.sms,
password: this.password
}).then(res => {
if (res.data.code == 100) {
// 注册成功
this.$message({
message: res.data.msg,
type: 'success'
});
// 跳转到登录--->写自定义事件的名字
this.$emit('go')
}
})
} else {
this.$message({
message: '手机号,验证码,密码不能为空',
type: 'success'
});
}
}
}
}
</script>
<style scoped>
.register {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.box {
width: 400px;
height: 480px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 240px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin-left: 90px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
# 2 写个短信登录注册接口(只需要传手机号和验证码,生成一个默认密码,以短信形式通知用户)
# 3 登录接口,如果是用默认密码,不允许登录
# 4 记录三次用户密码修改记录,每次改密码,不能跟之前用过的相同
Redis安装合介绍
# Redis :软件,存储数据的,速度非常快,redis是一个key-value存储系统(没有表的概念),cs架构的软件
-服务端 客户端(python作为客户端,java,go,图形化界面,命令窗口的命令)
# es:存数据的地方
# 关系型数据库和非关系型数据库
-关系型:mysql,PostgreSQL,oracle,sqlserver,db2
-PG
-去 IOE:国产化
-IBM---》浪潮信息,曙光,联想
-Oracle---》数据----》达梦。。。。
-EMC存储--》国产存储
-非关系型数据库(nosql):redis(缓存),mongodb(json文档数据存储),es(大数据量存储)。。。。
-nosql 指非关系型数据库: no only sql,对关系型数据库的补充
# redis特点:
-开源软件,存数据,cs架构
-key-value存储 ,5大数据类型 value的类型是5种:字符串,hash(字典),列表,集合,有序集合
-速度快:
-1 纯内存存储(核心)
-2 使用了IO多路复用的网络模型
-3 数据操作是单线程,避免了线程间切换,而且没有锁,也不会数据错乱
-支持持久化
-纯内存,可以存到硬盘上,防止数据丢失
-redis又被称之为 缓存数据库
安装redis
# redis 是用c语言编写的,需要在不同平台编译成可执行文件,才能在这个平台上运行
-redis 使用了io多路复用种的epoll模型,win不支持epoll
-redis官方,不支持win版本
-微软官方,就把redis改动,编译成可执行,能运行在win上,滞后 3.x版本
-第三方:5.x版本
# redis 官方网:https://redis.io/download/
# redis中文网:http://redis.cn
# win:3.x:https://github.com/microsoftarchive/redis/releases
# win:5.x:https://github.com/tporadowski/redis/releases/
# 安装:一路下一步
-安装完成后,在安装路径下有
-redis-cli.exe # mysql
-redis-server.exe # mysqld
-redis.windows-service.conf # my.ini
-并且会自动做成服务
-服务的命令:redis-server.exe redis.windows-service.conf
# 启动redis服务端
-1 命令行中 redis 就可以启动服务
-2 命令行中,启动服务,并指定配置文件
redis-server 配置文件路径
-3 使用服务启动
# 客户端链接
-1 命令行客户端:
-redis-cli # 默认连本地的6379端口
-redis-cli -p 6379 -h 127.0.0.1
-2 图形化客户端链接
-1 最新版的Navicate支持链接redis了(收费的)
-2 Redis Desktop Manager(https://resp.app/) 收费的 用的多 qt写图形化界面
-qt是个平台,做GUI[图形化界面]开发
-用c写,用python写 pyqt5
-3 python的模块
-pip install redis
redis的五大数据类型
# string:字符
# list:列表/数组
# set:集合
# zset:有序集合
# hash:哈希
redis普通链接和连接池
from redis import Redis
# conn = Redis() # 建立redis的链接
conn = Redis(host="127.0.0.1",
port=6379,
db=0,decode_responses=True) # 建立redis的链接 decode_responses=True,查询回来返回的结果是字符串类型,否则是byte格式
res = conn.get('name') # 获取key名为name的value值
print(res)
conn.close() # 关闭链接
连接池链接
# 一定要保证,池是单例的,以模块导入的形式做成了单例
# pool.py
import redis
POOL = redis.ConnectionPool(max_connections=1000,host='127.0.0.1',port=6379)
# 其它.py
import redis
from pool import POOL # 模块导入的方式, 天然单例
conn = redis.Redis(connection_pool=POOL) # 以后拿到链接,是从POOL种取,如果没有可用的了,默认不阻塞,可以通过某个参数配置,设置阻塞等待
res = conn.get('name') # 获取key名为name的value值
print(res)
conn.close() # 关闭链接
redis字符串类型
import redis
conn = redis.Redis(decode_responses=True)
对象操作Redis
conn.close()
1. set(name, value, ex=None, px=None, nx=False, xx=False)
在Redis中设置值,默认,不存在则创建,存在则修改
参数:
ex,过期时间(秒)
px,过期时间(毫秒)
nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果
xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值
2. setnx(name, value)
设置值,只有name不存在时,执行设置操作(添加),如果存在,不会修改
# 相当于: set('test', 'gjl', nx=True)
3. setex(name, value, time)
# 设置值
# 参数:
time,过期时间(数字秒 或 timedelta对象)
# 相当于: set('test', 'gjl', ex=3)
4. psetex(name, time_ms, value)
# 设置值
# 参数:
time_ms,过期时间(数字毫秒 或 timedelta对象
# 相当于: set('test', 'gjl', px=3000)
5. mset(*args, **kwargs)
# 批量设置值
mset({'name': 'ggg', 'age': 18})
6. get(name)
# 获取值
conn.get('name')
# 获取的值是需要解码的,或者配置参数decode_responses=True
7. mget(keys, *args)
# 批量获取
conn.mget('name', 'age')
8. getset(name, value)
# 设置新值并获取原来的值
conn.getset('name', 'test')
9. getrange(key, start, end)
# 获取子序列(根据字节获取,非字符)
conn.getrange('name', 0, 2)
# 获取的是字节,不是字符
10. setrange(name, offset, value)
# 修改字符串内容,从指定字符串索引开始向后替换(新值太长时,则向后添加)
conn.setrange('name', 2, 'gjl')
11. strlen(name)
# 返回name对应值的字节长度(一个汉字3个字节)
conn.strlen('name')
12. incr(name, amount=1)
# 自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
conn.incr('name', 2)
# 参数:
name,Redis的name
amount,自增数(必须是整数)
13. incrbyfloat(name, amount=1.0)
# 自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
# 参数:
name,Redis的name
amount,自增数(浮点型)
14. decr(self, name, amount=1)
# 自减 name对应的值,当name不存在时,则创建name=amount,否则,则自减。
conn.decr('name', 4)
# 参数:
name,Redis的name
amount,自减数(整数)
15. append(key, value)
# 在redis name对应的值后面追加内容
conn.append('name', 'xxx')
"""
重点掌握:
get、set、append、strlen
"""
redis hash类型
8import redis
conn = redis.Redis(decode_responses=True)
对象操作Redis
conn.close()
1. hset(name, key, value)
# name对应的hash中设置一个键值对(不存在,则创建;否则,修改)
conn.hset('userinfo', 'name', 'gg')
# 参数:
name,redis的name
key,name对应的hash中的key
value,name对应的hash中的value
# 注:
hsetnx(name, key, value),当name对应的hash中不存在当前key时则创建(相当于添加)
# 批量设置
conn.hset('userinfo', mapping={'name': 'ggg', 'age': 19})
2. hget(name,key)
# 在name对应的hash中获取根据key获取value
conn.hget('userinfo', 'name')
3. hmget(name, keys, *args)
# 在name对应的hash中获取多个key的值
conn.hmget('userinfo', ['name', 'age'])
4. hgetall(name)
# 获取name对应hash的所有键值
conn.hgetall('userinfo')
# 尽量不也要用,获取数据如果过大会撑爆内存
5. hlen(name)
# # 获取name对应的hash中键值对的个数
conn.hlen('userinfo')
6. hkeys(name)
# 获取name对应的hash中所有的key的值
conn.hkeys('userinfo')
7. hvals(name)
# 获取name对应的hash中所有的value的值
conn.hvals('userinfo')
8. hexists(name, key)
# 检查name对应的hash是否存在当前传入的key
conn.hexists('userinfo', 'name')
9. hdel(name,*keys)
# 将name对应的hash中指定key的键值对删除
conn.hdel('userinfo', 'name')
10. hincrby(name, key, amount=1)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
# 参数:
name,redis中的name
key, hash对应的key
amount,自增数(整数)
conn.hincrby('userinfo', 'name')
11. hincrbyfloat(name, key, amount=1.0)
# 自增name对应的hash中的指定key的值,不存在则创建key=amount
# 参数:
name,redis中的name
key, hash对应的key
amount,自增数(浮点数
12. hscan(name, cursor=0, match=None, count=None)
# 增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆
# 参数:
name,redis的name
cursor,游标(基于游标分批取获取数据)
match,匹配指定key,默认None 表示所有的key
count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
# 如:
# 第一次:cursor1, data1 = r.hscan('xx', cursor=0, match=None, count=None)
# 第二次:cursor2, data1 = r.hscan('xx', cursor=cursor1, match=None, count=None)
# ...
# 直到返回值cursor的值为0时,表示数据已经通过分片获取完毕
13. hscan_iter(name, match=None, count=None)
# 利用yield封装hscan创建生成器,实现分批去redis中获取数据
# 参数:
# match,匹配指定key,默认None 表示所有的key
# count,每次分片最少获取个数,默认None表示采用Redis的默认分片个数
# 如:
# for item in r.hscan_iter('xx'):
# print item
"""
重点掌握:
"""
Redis 列表类型List
'''添加到最左边相当于从索引为0的位置往里插入,添加到最右边相当于从尾部追加,在Redis可视化软件中,是将列表竖着显示'''
1. lpush(name,values)
# 在name对应的list中添加元素,每个新的元素都添加到列表的最左边
res = conn.lpush('hobby', '乒乓球', '台球')
# 保存顺序为: '台球','乒乓球'
2. rpush(name, values)
# 在name对应的list中添加元素,表示从右向左操作
res = conn.rpush('hobby', '冰球', '保龄球')
# 保存顺序为:'冰球', '保龄球'
3. lpushx(name,value)
# 在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边
conn.lpushx('hobby', '123')
4. rpushx(name,value)
# 在name对应的list中添加元素,只有name已经存在时,值添加到列表的最右边
conn.rpushx('hobby', '123')
5. llen(name)
# name对应的list元素的个数
conn.llen('hobby')
6. linsert(name, where, refvalue, value)
# 在name对应的列表的某一个值前或后插入一个新值
# 参数:
name,redis的name
where,before/after (小写也可以)
refvalue,标杆值,即:在它前后插入数据(如果存在多个标杆值,以找到的第一个为准)
value,要插入的数据
conn.linsert('hobby', 'after', '足球', '333')
# 在key值为hobby中,values值为足球的后面插入一个333
7. lset(name, index, value)
# 对name对应的list中的某一个索引位置重新赋值
# 参数:
name,redis的name
index,list的索引位置
value,要设置的值
conn.lset('hobby', 4, '排球')
# 将索引为4的值重新赋值为排球
8. lrem(name, value, num)
# 在name对应的list中删除指定的值
# 参数:
name,redis的name
value,要删除的值
num, num=0,删除列表中所有的指定值;
num=2,从前到后,删除2个;
num=-2,从后向前,删除2个
conn.lrem('hobby', 0, '排球') # 删除所有值为排球的
conn.lrem('hobby', 2, '冰球') # 在Redis可视化工具中,从上往下删除两个值为冰球的
conn.lrem('hobby', -2, '保龄球') # 在Redis可视化工具中,从下往上删除两个值为保龄球的
9. lpop(name)
# 在name对应的列表的左侧获取第一个元素并在列表中移除,返回值则是第一个元素
conn.lpop('hobby') # 从左侧弹出
conn.rpop('hobby') # 从右侧弹出
10. lindex(name, index)
# 在name对应的列表中根据索引获取列表元素
conn.lindex('hobby', 2)
11. lrange(name, start, end)
# # 在name对应的列表分片获取数据
# 参数:
name,redis的name
start,索引的起始位置
end,索引结束位置
# 搭配llen使用
conn.lrange('hobby', 0, coon.llen('aa'))
12. ltrim(name, start, end)
# 在name对应的列表中移除没有在start-end索引之间的值
# 参数:
name,redis的name
start,索引的起始位置
end,索引结束位置(大于列表长度,则代表不移除任何)
conn.ltrim('hobby', 0, 2)
13. rpoplpush(src, dst)
# 从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边
# 参数:
src,要取数据的列表的name
dst,要添加数据的列表的name
conn.rpoplpush('hobby', 'db1')
# 将key值为hobby中最右的一个值取出并添加到key值为db1的最左边
14. blpop(keys, timeout)
# 将多个列表排列,按照从左到右去pop对应列表的元素
# 参数:
keys,redis的name的集合
timeout,超时时间,当元素所有列表的元素获取完之后,阻塞等待列表内有数据的时间(秒), 0 表示永远阻塞
conn.blpop('hobby')
# 有值是会顺利执行,没有值时会一直阻塞,所以要设置timeout时间
15. brpoplpush(src, dst, timeout=0)
# 从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧
# 参数:
src,取出并要移除元素的列表对应的name
dst,要插入元素的列表对应的name
timeout,当src对应的列表中没有数据时,阻塞等待其有数据的超时时间(秒),0 表示永远阻塞
'''
重点掌握:
lpush
lpop
linsert
lset
llen
lrange
'''
############################################自定义增量迭代######################################################
# 使用lrange与llen可以将列表所有的数据取出,但是面临一个问题就是,如果数据量过大将会撑爆整个内存,所以才会自定义一个迭代器
import redis
conn = redis.Redis(decode_responses=True)
def test(key, count):
num = 0
while True:
print('=========================')
res = conn.lrange(key, num, num + count + 1)
num = num + count
if res:
for item in res:
yield item
else:
break
for item in test('eggs', 10):
print(item)
conn.close()
redis其它操作
1. delete(*names)
# 根据删除redis中的任意数据类型
conn.delete('eggs')
2. exists(name)
# 检测redis的name是否存在
conn.exists('name3')
# 返回值:如果存在返回1,不存在返回0
3. keys(pattern='*')
# 根据模型获取redis的key
# 更多:
KEYS * 匹配数据库中所有 key 。
KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
KEYS h*llo 匹配 hllo 和 heeeeello 等。
KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo
conn.keys('name?')
# 可以使用正则匹配
4. expire(name ,time)
# 为某个redis的某个name设置超时时间
conn.expire('name', 3)
5. rename(src, dst)
# 对redis的name重命名为
conn.rename('name1', 'name')
6. move(name, db)
# 将redis的某个值移动到指定的db下
conn.move('name', 1)
# 将key值为name移动到db1下
7. randomkey()
# 随机获取一个redis的name(不删除)
conn.randomkey()
8. type(name)
# 获取name对应值的类型
conn.type('nnn')
redis管道
# 事务四大特性
-原子性:要么都成功,要么都失败
-一致性:数据前后要一致
-隔离性:多个事务之间相互不影响
-持久性:事务一旦完成,数据永久改变
# redis 有没有事务?支持事务
-redis要支持事务,要完成事务的几大特性,需要使用管道来支持
-单实例redis是支持管道的
-集群模式下,不支持管道,就不支持事务
# redis通过管道实现事务
import redis
conn = redis.Redis()
pipline = conn.pipeline(transaction=True)
pipline.decrby('a1', 10) # 没有真正执行,把命令先放到管道中
raise Exception('出错了')
pipline.incrby('a2', 10)
pipline.execute() # 把管道中的命令,一次性执行
conn.close()
django中使用redis
# 两种方式
-方式一:自定义的通用方案(跟框架无关)
-写一个py文件:redis_pool.py
import redis
POOL=redis.ConnectionPool(max_connections=10)
-在用的位置,导入直接使用
conn = redis.Redis(connection_pool=POOL)
conn.incrby('a1')
-django中有个模块,django-redis,方便我们快速集成redis
-1 下载:pip install django-redis
-2 配置文件配置:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
# "PASSWORD": "123",
}
}
}
-3 在使用的地方,导入直接使用
from django_redis import get_redis_connection
class MyResponseView(APIView):
def get(self, request):
conn = get_redis_connection() # 从连接池中拿出一个链接
conn.incrby('a1')
conn.set('name','彭于晏')
return APIResponse()
django缓存
# django 是大而全的框架,内置了很多web开发需要的东西,缓存内置了
# 缓存:可以把django中的一个变量(数据),存放到某个位置,下次还可以取出来
# 之前用过:默认放在:内存中,其实可以放在文件中,数据库,redis。。。。
from django.core.cache import cache
cache.set('key','value',5) # 存放值
res=cache.get('key') # 取值
# 通过配置,控制存放在哪,只要如下写,就会放在redis中
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
# "PASSWORD": "123",
}
}
}
# django缓存最强大之处在于,只要是python的变量,任意类型都可以,尽管使用set设置值
l = [1, 'lqz', [1, 3, 4, 5, 6], '彭于晏']
cache.set('ll1', l)
# 以后再django中往redis放数据,就用cache即可
# redis的5大数据类型,只支持一层
# 看一下这篇文章:https://www.cnblogs.com/liuqingzheng/articles/9803351.html
# 超过5k就要交税
-公司正常报税,工资中扣
-6月份开始工作,按1w工资报税, 退税
补充
#序列化
-json序列化---》得到字符串
json不能序列化对象(自定义的类的对象)
-数据结构:数据的组织形式跟下面不一样
能序列化: 数字,字符串,布尔,列表,字典 时间对象
-pickle序列化
-python独有的,二进制形式
-python可以序列化所有对象---》二进制形式
-二进制---》返序列化回来---》对象:属性,有方法
celery介绍和安装
# celery :分布式的异步任务框架,主要用来做:
- 异步任务
- 延迟任务
- 定时任务---》如果只想做定时任务,可以不使用celery,有别的选择
# celery 框架,原理
1)可以不依赖任何服务器,通过自身命令,启动服务(内部支持socket)
2)celery服务为为其他项目服务提供异步解决任务需求的
注:会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要时异步完成项目的需求
人是一个独立运行的服务 | 医院也是一个独立运行的服务
正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求
# celery架构
消息中间件(broker):消息队列:可以使用redis,rabbitmq,咱们使用redis
任务执行单元(worker):真正的执行 提交的任务
任务执行结果存储(banckend):可以使用mysql,redis,咱们使用redis
# 安装celery
-pip install Celery
-释放出可执行文件:celery,由于 python解释器的script文件夹再环境变量,任意路径下执行celery都能找到
# celery不支持win,所以想再win上运行,需要额外安装eventlet
windows系统需要eventlet支持:pip3 install eventlet
Linux与MacOS直接执行:
3.x,4.x版本:celery worker -A demo -l info
5.x版本: celery -A demo worker -l info -P eventlet
celery架构

celery执行异步任务
# 基本使用
1 在啥都没,虚拟环境中装celery和eventlet
2 写个py文件,实例化得到app对象,注册任务
from celery import Celery
import time
broker = 'redis://127.0.0.1:6379/1' # 消息中间件 redis
backend = 'redis://127.0.0.1:6379/2' # 结果存储 redis
app = Celery(__name__, broker=broker, backend=backend) # 指定消息队列中间件的地址,指定存储结果的地址
@app.task # 变成celery的任务了
def add(a, b):
print('运算结果是',a + b)
time.sleep(1)
return a + b
3 启动worker(worker监听消息队列,等待别人提交任务,如果没有就卡再这)
celery -A demo worker -l info -P eventlet
4 别人 提交任务,提交完成会返回一个id号,后期使用id号查询,至于这个任务有没有被执行,取决于worker有没有启动
from demo import add
res=add.delay(77,66)
5 提交任务的人,再查看结果
from demo import app
# celery的包下
from celery.result import AsyncResult
id = '042a8fc1-6b0f-4ad6-bf72-edefa657a52f'
if __name__ == '__main__':
a = AsyncResult(id=id, app=app)
if a.successful(): # 正常执行完成
result = a.get() # 任务返回的结果
print(result)
elif a.failed():
print('任务失败')
elif a.status == 'PENDING':
print('任务等待中被执行')
elif a.status == 'RETRY':
print('任务异常后正在重试')
elif a.status == 'STARTED':
print('任务已经开始被执行')
包架构封装
project
├── celery_task # celery包
│ ├── __init__.py # 包文件
│ ├── celery.py # celery连接和配置相关文件,且名字必须交celery.py
│ └── tasks.py # 所有任务函数
├── add_task.py # 添加任务
└── get_result.py # 获取结果
1. 新建包:celery_task
2. 在包下新建celery.py必须叫这个
from celery import Celery
broker = 'redis://127.0.0.1:6379/1' # 消息中间件 redis
backend = 'redis://127.0.0.1:6379/2' # 结果存储 redis
app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.celery_add'])
# include里写任务的路劲
3. 新建任务py文件:celery_add
from .celery import app
@app.task
def add(a, b):
print('运算结果:', a + b)
return True
4 启动worker,以包启动,来到包所在路径下
celery -A 包名 worker -l info -P eventlet
celery -A celery_task worker -l info -P eventlet
5 其它程序,导入任务,提交任务即可
from celery_task.user_task import send_sms
res = celery_add.delay(1, 2)
print(res) # f33ba3c5-9b78-467a-94d6-17b9074e8533
6 其它程序,查询结果
from celery_task.celery import app
# celery的包下
from celery.result import AsyncResult
id = '51a669a3-c96c-4f8c-a5fc-e1b8e2189ed0'
if __name__ == '__main__':
a = AsyncResult(id=id, app=app)
if a.successful(): # 正常执行完成
result = a.get() # 任务返回的结果
print(result)
elif a.failed():
print('任务失败')
elif a.status == 'PENDING':
print('任务等待中被执行')
elif a.status == 'RETRY':
print('任务异常后正在重试')
elif a.status == 'STARTED':
print('任务已经开始被执行')
celery执行延迟任务和定时任务
# celery 可以做
-异步任务
-延迟任务---》延迟多长时间干任务
-定时任务:每天12点钟,每隔几秒。。。
-如果只做定时任务,不需要使用celery这么重,apscheduler(自己去研究)
# 异步任务
-导入异步任务的函数
-函数.delay(参数)
# 延迟任务
-导入异步任务的函数
-函数.apply_async(kwargs={'mobile':'1896334234','code':8888},eta=时间对象)
'''
from datetime import datetime, timedelta
eta = datetime.utcnow() + timedelta(seconds=10)
datetime.utcnow():当前时间对象(utc时间)
timedelta(seconds=10):10秒,使用他就可以和datetime相加
'''
# 定时任务:在app所在的文件下配置
- 1 配置
app.conf.beat_schedule = {
'send_sms': {
'task': 'celery_task.user_task.send_sms',
'schedule': timedelta(seconds=5),
'args': ('1822344343', 8888),
},
'add_course': {
'task': 'celery_task.course_task.add_course',
# 'schedule': crontab(hour=8, day_of_week=1), # 每周一早八点
'schedule': crontab(hour=11, minute=38), # 每天11点35,执行
'args': (),
}
}
-2 启动beat,启动worker
celery -A celery_task beat -l info
-3 到了时间,beat进程负责提交任务到消息队列---》worker执行
django中使用celery
# 使用步骤
1 把之前写好的包,copy到项目根路径下
2 在xx_task.py 中写任务
from .celery import app
@app.task
def add_banner():
from home.models import Banner
Banner.objects.create(title='测试', image='/1.png', link='/banner', info='xxx',orders=99)
return 'banner增加成功'
3 在celery.py 中加载django配置
# 一、加载django配置环境
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")
4 视图函数中,导入任务,提交即可
class CeleryView(APIView):
def get(self, request):
res = add_banner.delay()
return APIResponse(msg='新增banner的任务已经提交了')
5 启动worker,等待运行即可
'''
celery中要使用djagno的东西,才要加这句话
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")
'''
接口缓存
# 所有接口都可以改造,尤其是查询所有的这种接口,如果加入缓存,会极大的提高查询速度
# 首页轮播图接口:获取轮播图数据,加缓存---》咱们只是以它为例
class BannerView(GenericViewSet, ListModelMixin):
queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
serializer_class = BannerSerializer
def list(self, request, *args, **kwargs):
'''
1 先去缓存中查一下有没有数据
2 如果有,直接返回,不走父类的list了(list在走数据库)
3 如果没有,走父类list,查询数据库
4 把返回的数据,放到缓存中
'''
data = cache.get('home_banner_list')
if not data: # 缓存中没有
print('走了数据库')
res = super().list(request, *args, **kwargs) # 查询数据库
# 返回的数据,放到缓存中
data = res.data.get('data') # {code:100,msg:成功,data:[{},{}]}
cache.set('home_banner_list', data)
return APIResponse(data=data)
# 公司里可能会这么写
-写一个查询所有带缓存的基类
-写个装饰器,只要一配置,就自动带缓存
# 双写一致性问题:缓存数据和数据库数据不一致了
-写入数据库,删除缓存
-写入数据库,更新缓存
-定时更新缓存
双写一致性
# 只要将数据放入缓存就,就会出现数据不一致的情况
# 解决方案:
- 修改数据时,删除缓存
- 修改数据时,跟新缓存
- 定时跟新缓存
# 首页轮播图存在双写一致性问题这个问题
解决方案:定时任务,定时跟新
from celery import Celery
# 使用django的配置文件先导入
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")
broker = 'redis://127.0.0.1:6379/1'
backend = 'redis://127.0.0.1:6379/2'
app = Celery(__name__, broker=broker, backend=backend, include=['celery_task.home_task'])
### 3 定时任务要写在这里
# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False
# 任务的定时配置
from datetime import timedelta
app.conf.beat_schedule = {
'update_banner': {
'task': 'celery_task.home_task.update_banner',
'schedule': timedelta(seconds=10), # 每10秒执行一次任务
'args': (),
},
}
# 任务
from .celery import app
from home.serialzier import BannerSerializer
from django.core.cache import cache
from home.models import Banner
from django.conf import settings
@app.task
def update_banner():
banner_list = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[:settings.BANNER_COUNT]
ser = BannerSerializer(instance=banner_list, many=True)
# 如果在视图类中,做序列化 ,因为视图类中有request对象,所以像图片这种,会自动加前面地址,
# 在这里没有request对象,需要手动拼
for i in ser.data:
i['image'] = settings.BACKEND_URL + i['image']
cache.set('home_banner_list', ser.data)
return True
# 视图
def list(self, request, *args, **kwargs):
data = cache.get('home_banner_list')
if not data:
res = super().list(request, *args, **kwargs)
data = res.data.get('data')
cache.set('home_banner_list', data)
return APIResponse(data=data)
异步发送短信
# 视图函数
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):
mobile = request.query_params.get('mobile', None)
code = get_code() # 把code存起来,放到缓存中,目前在内存,后期换别的
cache.set('send_sms_code_%s' % mobile, code)
if mobile:
res = send_sms.delay(mobile,code)
print(res)
return APIResponse(msg='短信已发送')
raise APIException('手机号没有携带')
# 任务
from libs.send_tx_sms import send_sms_by_mobile
@app.task
def send_sms(mobile, code):
res=send_sms_by_mobile(mobile,code)
if res:
return '%s的短信发送成功,验证码是%s'%(mobile,code)
else:
return '%s的短信发送失败'%mobile
异步秒杀逻辑前后端
前端
<template>
<div>
<h2>go语言从入门到放弃</h2>
<el-button type="danger" @click="handleSckill">秒杀</el-button>
</div>
</template>
<script>
export default {
name: "Sckill",
data() {
return {
t: null,
task_id: '',
}
},
methods: {
handleSckill() {
this.$axios.post(`${this.$settings.BASE_URL}user/sckill/`, {
name: "性感帽子",
}).then(res => {
if (res.data.code == 100) {
alert('您正在排队')
this.task_id = res.data.task_id # task_id是任务的id,让定时任务定时发axios请求去后端拿数据
// 起个转圈的东西
// 起定时任务
this.t = setInterval(() => {
this.$axios.get(`${this.$settings.BASE_URL}user/sckill/?task_id=${this.task_id}`).then(res => {
if (res.data.code == 100 || res.data.code == 101) {
this.$message(res.data.msg)
//销毁定时器
clearInterval(this.t)
this.t = null
} else {
console.log('过会再查')
}
})
}, 3000)
}
})
}
}
}
</script>
<style scoped>
</style>
后端
视图
from celery_task.user_task import sckill_goods
from celery_task.celery import app
from celery.result import AsyncResult
class SckillView(APIView):
def post(self, request, *args, **kwargs):
name = request.data.get('name')
# 提交秒杀异步任务
res = sckill_goods.delay(name)
return APIResponse(task_id=str(res))
def get(self, request, *args, **kwargs):
task_id = request.GET.get('task_id')
a = AsyncResult(id=task_id, app=app)
if a.successful(): # 正常执行完成
result = a.get() # 任务返回的结果
if result:
return APIResponse(code=100, msg='秒杀成功')
else:
return APIResponse(code=101, msg='秒杀失败')
elif a.status == 'STARTED':
print('任务已经开始被执行')
return APIResponse(code=103, msg='还在排队')
else:
return APIResponse(code=102, msg='没成功')
路由
urlpatterns = [
path('sckill/', SckillView.as_view()),
]
任务
@app.task
def sckill_goods(name):
# 逻辑是:开启事务---》扣减库存---》生成订单
import time
time.sleep(6)
res = random.choice([100, 102])
if res == 100:
print('%s被秒杀成功了' % name)
return True
else:
print('%s被秒杀失败了' % name)
return False
课程页页面前端
#1 前端 新建三个组件
LightCourse.vue
FreeCourse.vue
ActualCourse.vue
# 2 配置路由
import FreeCourse from "@/views/FreeCourse";
import ActualCourse from "@/views/ActualCourse";
import LightCourse from "@/views/LightCourse";
{
path: '/free-course',
name: 'free-course',
component: FreeCourse
},
{
path: '/actual-course',
name: 'actual-course',
component: ActualCourse
},
{
path: '/light-course',
name: 'light-course',
component: LightCourse
},
FreeCourse.vue
<template>
<div class="course">
<Header></Header>
<div class="main">
<!-- 筛选条件 -->
<div class="condition">
<ul class="cate-list">
<li class="title">课程分类:</li>
<li class="this">全部</li>
<li>Python</li>
<li>Linux运维</li>
<li>Python进阶</li>
<li>开发工具</li>
<li>Go语言</li>
<li>机器学习</li>
<li>技术生涯</li>
</ul>
<div class="ordering">
<ul>
<li class="title">筛 选:</li>
<li class="default this">默认</li>
<li class="hot">人气</li>
<li class="price">价格</li>
</ul>
<p class="condition-result">共21个课程</p>
</div>
</div>
<!-- 课程列表 -->
<div class="course-list">
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限时免费</span>
<span class="discount-price">¥6.00元</span>
<span class="original-price">原价:9.00元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限时免费</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原价:9.00元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限时免费</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原价:9.00元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码</span></li>
<li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限时免费</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原价:9.00元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
export default {
name: "Course",
data() {
return {
category: 0,
}
},
components: {
Header,
Footer,
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 过渡动画 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 顶级元素 父级元素 当前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
width: 100%;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .lesson-list::after {
content: "";
display: block;
clear: both;
}
.course-item .lesson-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路径") 是否平铺 x轴位置 y轴位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .lesson-list li .lesson-title {
/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .lesson-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .lesson-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .lesson-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
</style>
建立表
from django.db import models
from utils.common_models import BaseModel
# 课程分类
class CourseCategory(BaseModel):
"""分类 id name """
name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")
class Meta:
db_table = "luffy_course_category"
verbose_name = "课程分类"
verbose_name_plural = verbose_name
def __str__(self):
return "%s" % self.name
# 课程表
class Course(BaseModel):
"""课程"""
course_type = (
(0, '付费'),
(1, 'VIP专享'),
)
level_choices = (
(0, '初级'),
(1, '中级'),
(2, '高级'),
)
status_choices = (
(0, '上线'),
(1, '下线'),
(2, '预上线'),
)
name = models.CharField(max_length=128, verbose_name="课程名称")
course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True,
null=True)
status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
students = models.IntegerField(verbose_name="学习人数", default=0) # 优化字段
sections = models.IntegerField(verbose_name="总课时数量", default=0)
pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
blank=True, verbose_name="课程分类")
class Meta:
db_table = "luffy_course"
verbose_name = "课程"
verbose_name_plural = "课程"
def __str__(self):
return "%s" % self.name
# 章节
class CourseChapter(BaseModel):
"""章节"""
'''
基于对象的跨表查询:正:字段 反:表名小写(一对一是 表名小写,一对多 表名小写_set.all())
-related_name='coursechapters' 基于对象跨表查反向查询,替换表名小写
基于双下划线的连表查询:正:字段 反:表名小写
-related_query_name='xxx' 基于连表查反向查询,替换表名小写
'''
# related_name='coursechapters'
course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE,
verbose_name="课程名称")
chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
name = models.CharField(max_length=128, verbose_name="章节标题")
summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
class Meta:
db_table = "luffy_course_chapter"
verbose_name = "章节"
verbose_name_plural = verbose_name
def __str__(self):
return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
# 课时
class CourseSection(BaseModel):
"""课时"""
section_type_choices = (
(0, '文档'),
(1, '练习'),
(2, '视频')
)
chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
verbose_name="课程章节")
name = models.CharField(max_length=128, verbose_name="课时标题")
section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接",
help_text="若是video,填vid,若是文档,填link")
duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32) # 仅在前端展示使用
pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
free_trail = models.BooleanField(verbose_name="是否可试看", default=False)
class Meta:
db_table = "luffy_course_section"
verbose_name = "课时"
verbose_name_plural = verbose_name
def __str__(self):
return "%s-%s" % (self.chapter, self.name)
# 老师表
class Teacher(BaseModel):
"""导师"""
role_choices = (
(0, '讲师'),
(1, '导师'),
(2, '班主任'),
)
name = models.CharField(max_length=32, verbose_name="导师名")
role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="导师身份")
title = models.CharField(max_length=64, verbose_name="职位、职称")
signature = models.CharField(max_length=255, verbose_name="导师签名", help_text="导师签名", blank=True, null=True)
image = models.ImageField(upload_to="teacher", null=True, verbose_name="导师封面")
brief = models.TextField(max_length=1024, verbose_name="导师描述")
class Meta:
db_table = "luffy_teacher"
verbose_name = "导师"
verbose_name_plural = verbose_name
def __str__(self):
return "%s" % self.name
课程表数据录入
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2022-07-14 13:44:19.661327', '2022-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教学总监', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 国内知名PYTHON语言推广者 51CTO学院2016\2017年度最受学员喜爱10大讲师之一 多款开源软件作者 曾任职公安部、飞信、中金公司、NOKIA中国研究院、华尔街英语、ADVENT、汽车之家等公司');
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2022-07-14 13:45:25.092902', '2022-07-14 13:45:25.092936', 'Mjj', 0, '前美团前端项目组架构师', NULL, 'teacher/mjj_icon.png', '是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。');
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2022-07-14 13:46:21.997846', '2022-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux学科带头人', NULL, 'teacher/lyy_icon.png', 'Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸');
-- 分类表
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2022-07-14 13:40:58.690413', '2022-07-14 13:40:58.690477', 'Python');
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2022-07-14 13:41:08.249735', '2022-07-14 13:41:08.249817', 'Linux');
-- 课程表
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2022-07-14 13:54:33.095201', '2022-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/alex_python.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2022-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2022-07-14 13:56:05.051103', '2022-07-14 13:56:05.051142', 'Python项目实战', 'courses/mjj_python.png', 0, '', 1, '2022-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2022-07-14 13:57:21.190053', '2022-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/lyy_linux.png', 0, '', 0, '2022-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);
-- 章节表
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2022-07-14 13:58:34.867005', '2022-07-14 14:00:58.276541', 1, '计算机原理', '', '2022-07-14', 1);
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2022-07-14 13:58:48.051543', '2022-07-14 14:01:22.024206', 2, '环境搭建', '', '2022-07-14', 1);
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2022-07-14 13:59:09.878183', '2022-07-14 14:01:40.048608', 1, '项目创建', '', '2022-07-14', 2);
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2022-07-14 13:59:37.448626', '2022-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2022-07-14', 3);
-- 课时表
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2022-07-14 14:02:33.779098', '2022-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2022-07-14 14:02:33.779193', 1, 1);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2022-07-14 14:02:56.657134', '2022-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2022-07-14 14:02:56.657227', 1, 1);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2022-07-14 14:03:20.493324', '2022-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2022-07-14 14:03:20.493420', 0, 2);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2022-07-14 14:03:36.472742', '2022-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2022-07-14 14:03:36.472831', 0, 2);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2022-07-14 14:04:19.338153', '2022-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2022-07-14 14:04:19.338252', 1, 3);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2022-07-14 14:04:52.895855', '2022-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2022-07-14 14:04:52.895942', 1, 4);
课程主页接口
# 课程分类接口
# 查询所有课程接口
-带过滤
-带排序
-带分页
视图类
from .models import CourseCategory
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from .serializer import CourseCategorySerializer
from utils.common_mixin import CommonListModelMixin as ListModelMixin
class CourseCategoryView(GenericViewSet, ListModelMixin):
# 查询所有
queryset = CourseCategory.objects.filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseCategorySerializer
序列化类
class CourseCategorySerializer(serializers.ModelSerializer):
class Meta:
model = CourseCategory
fields = ['id','name']
路由
from django.contrib import admin
from django.urls import path
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
from .views import CourseCategoryView
router.register('category', CourseCategoryView, 'category')
urlpatterns = [
]
urlpatterns += router.urls
查询所有课程接口
# 查询所有课程接口
-带过滤:按分类过滤
-带排序:价格,学习人数
-带分页:简单分页
视图类
class CourseView(GenericViewSet, ListModelMixin):
queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseSerializer
pagination_class = CommonPagination
# 排序,过滤课程分类(course_category=2)
filter_backends = [OrderingFilter, DjangoFilterBackend]
ordering_fields = ['students', 'price']
filterset_fields = ['course_category']
序列化类
class TeacherSerializer(serializers.ModelSerializer):
class Meta:
model = Teacher
fields = ['id', 'name', 'role_name', 'title', 'signature', 'image', 'brief']
# 只用来做序列化
class CourseSerializer(serializers.ModelSerializer):
teacher = TeacherSerializer() # 子序列化
class Meta:
model = Course
fields = [
'id',
'name',
'course_img',
'brief',
'attachment_path', # 课件地址
'pub_sections', # 发布了多少课时
'sections', # 总共有多少
'price',
'students',
'period', # 建议学习周期
'course_type_name', # 课程类型 中文
'level_name', # 课程难度等级中文
'status_name', # 课程状态中文
'teacher', # 重写 返回老师的详情
'section_list', # 重写的字段,拿最多四个课时,超过4个就拿4个,不足4个,有几个拿几个
]
表模型
class Course(BaseModel):
"""课程"""
course_type = (
(0, '付费'),
(1, 'VIP专享'),
)
level_choices = (
(0, '初级'),
(1, '中级'),
(2, '高级'),
)
status_choices = (
(0, '上线'),
(1, '下线'),
(2, '预上线'),
)
name = models.CharField(max_length=128, verbose_name="课程名称")
course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True,
null=True)
status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
students = models.IntegerField(verbose_name="学习人数", default=0) # 优化字段
sections = models.IntegerField(verbose_name="总课时数量", default=0)
pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
blank=True, verbose_name="课程分类")
class Meta:
db_table = "luffy_course"
verbose_name = "课程"
verbose_name_plural = "课程"
def __str__(self):
return "%s" % self.name
def course_type_name(self):
return self.get_course_type_display() # 拿到course_type对应的中文
def level_name(self):
return self.get_level_display()
def status_name(self):
return self.get_status_display()
def section_list(self):
l = []
# 超过4个就拿4个,不足4个,有几个拿几个
# 根据课程,获取所有章节
course_chapter_list = self.coursechapters.all() # 咱们写了related_name,直接按字段名
# 循环所有章节,拿出所有课时,追加到l中
for course_chapter in course_chapter_list:
chapter_section_list = course_chapter.coursesections.all()
for chapter_section in chapter_section_list:
l.append({'id': chapter_section.pk,
'name': chapter_section.name,
'section_link': chapter_section.section_link,
'duration': chapter_section.duration,
'free_trail': chapter_section.free_trail,
})
# 当l长度大于等于4,就直接返回
if len(l) >= 4:
return l
return l
课程详情接口
# 课间,增加了些数据
# 多种写法
-方式一:直接在原来查询所有的视图类上加入继承:RetrieveModelMixin
-继续使用查询所有的序列化类(没有章节及课时)
-配合一个通过课程id,查询所有课程章节及课时的接口
-方式二:直接在原来查询所有的视图类上加入继承:RetrieveModelMixin
-重写序列化类:返回课程详情,返回课程章节及课时
# 我们采用方式一:只要在CourseView加继承RetrieveModelMixin类即可,就有查询单条的接口了
class CourseView(GenericViewSet, ListModelMixin,RetrieveModelMixin):
# 方式二逻辑:
视图类中判断是使用哪个序列化类(self.action='Retrieve'),序列化类的字段
class CourseSerializer(serializers.ModelSerializer):
teacher = TeacherSerializer()
class Meta:
model = Course
fields = [
'id',
'name', # 课程名称
'course_img', # 封面图片
'course_type_name', # 付费类型
'price', # 价格
'attachment_path', # 课件路径
'status_name', # 课程状态
'students', # 学习人数
'sections', # 总课时数量
'teacher', #
'pub_sections', # 课时更新数量
'period', # 建议学习周期
'brief',
'level_name',
'chapter_list',
# chapter_list字段是课程套课时
]
课程详情后台之所有章节接口
# 该接口是为了配合 :课程详情页面写
# 写的接口是查询所有章节及课时
-需要安装课程id过滤
# 写查询所有章节接口(带过滤)
视图类
class CourseChapterView(GenericViewSet, ListModelMixin):
serializer_class = CourseChapterSerializer
queryset = CourseChapter.objects.filter(is_delete=False, is_show=True).order_by('orders')
filter_backends = [DjangoFilterBackend]
filterset_fields = ['course'] # 按课程过滤
序列化类
class CourseSectionSerializer(serializers.ModelSerializer):
class Meta:
model = CourseSection
fields = ['id', 'name', 'section_type_name',
'section_link',
'duration',
'free_trail',]
class CourseChapterSerializer(serializers.ModelSerializer):
'''
拿出章节下所有课时,方案有三种
-表模型中写
-序列化类中写
-方式三子序列化
coursesections = CourseSectionSerializer(many=True) # 章节下的课时,是多条,使用子序列化,instance不用传,但是many=True必须传
class Meta:
model = CourseChapter
fields = ['id', 'name', 'chapter', 'summary', 'coursesections']
所有课程前台,课程详情前台
# 0 查询所有课程分类接口
# 1 查询所有课程(过滤,排序,分页)
# 2 查询课程详情
# 3 查询所有章节及其下的课时,带按课程id过滤功能
-配合查询课程详情页面使用的
课程列表页
<template>
<div class="course">
<Header></Header>
<div class="main">
<!-- 筛选条件 -->
<div class="condition">
<ul class="cate-list">
<li class="title">课程分类:</li>
<li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li>
<li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"
@click="filter.course_category=category.id" :key="category.name">{{ category.name }}
</li>
</ul>
<div class="ordering">
<ul>
<li class="title">筛 选:</li>
<li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''"
@click="filter.ordering='-id'">默认
</li>
<li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''"
@click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人气
</li>
<li class="price"
:class="filter.ordering=='price'?'price_up this':(filter.ordering=='-price'?'price_down this':'')"
@click="filter.ordering=(filter.ordering=='-price'?'price':'-price')">价格
</li>
</ul>
<p class="condition-result">共{{ course_total }}个课程</p>
</div>
</div>
<!-- 课程列表 -->
<div class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3>
<p class="teather-info">
{{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}
<span v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{ course.pub_sections }}课时</span>
<span v-else>共{{ course.sections }}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{ key + 1 }} | {{ section.name }}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{ course.discount_type }}</span>
<span class="discount-price">¥{{ course.real_price }}元</span>
<span class="original-price">原价:{{ course.price }}元</span>
</div>
<span v-else class="discount-price">¥{{ course.price }}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[3, 5]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
export default {
name: "Course",
data() {
return {
category_list: [], // 课程分类列表
course_list: [], // 课程列表
course_total: 0, // 当前课程的总数量
filter: {
course_category: 0, // 当前用户选择的课程分类,刚进入页面默认为全部,值为0
ordering: "-id", // 数据的排序方式,默认值是-id,表示对于id进行降序排列
page_size: 3, // 单页数据量
page: 1,
}
}
},
created() {
this.get_category();
this.get_course();
},
components: {
Header,
Footer,
},
watch: {
"filter.course_category": function () {
this.filter.page = 1;
this.get_course();
},
"filter.ordering": function () {
this.get_course();
},
"filter.page_size": function () {
this.get_course();
},
"filter.page": function () {
this.get_course();
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_category() {
// 获取课程分类信息
this.$axios.get(`${this.$settings.BASE_URL}course/category/`).then(response => {
if (response.data.code == 100) {
this.category_list = response.data.data;
console.log(this.category_list)
}
}).catch(() => {
this.$message({
message: "获取课程分类信息有误,请联系客服工作人员",
})
})
},
get_course() {
// 排序
let filters = {
ordering: this.filter.ordering, // 排序
};
// 判决是否进行分类课程的展示
if (this.filter.course_category > 0) {
filters.course_category = this.filter.course_category;
}
// 设置单页数据量
if (this.filter.page_size > 0) {
filters.page_size = this.filter.page_size;
} else {
filters.page_size = 5;
}
// 设置当前页码
if (this.filter.page > 1) {
filters.page = this.filter.page;
} else {
filters.page = 1;
}
// 获取课程列表信息
this.$axios.get(`${this.$settings.BASE_URL}course/course/`, {
params: filters
}).then(response => {
if (response.data.code == 100) {
this.course_list = response.data.data.results;
this.course_total = response.data.data.count;
}
}).catch(() => {
this.$message({
message: "获取课程信息有误,请联系客服工作人员"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 过渡动画 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 顶级元素 父级元素 当前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路径") 是否平铺 x轴位置 y轴位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
课程详情页面
# 视频播放器,可以使用h5自带的video标签
-画面比例,清晰度
-倍速播放
-静音
-暂停可以插广告。。。
# 使用第三方:vue-video-player
cnpm install -S vue-core-video-player
# mian.js
import VueCoreVideoPlayer from 'vue-core-video-player'
//或者
Vue.use(VueCoreVideoPlayer, {
lang: 'zh-CN'
})
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<vue-core-video-player :src="mp4_url"
:muted="true"
:autoplay="false"
title="致命诱惑"
preload="nona"
:loop="true"
controls="auto"
cover='https://img1.wxzxzj.com/vpc-example-cover-your-name-c.png'
@loadedmetadata="onLoaded"
@play="onPlayerPlay"
@pause="onPlayerPause"
></vue-core-video-player>
</div>
<div class="wrap-right">
<h3 class="course-name">{{ course_info.name }}</h3>
<p class="data">
{{ course_info.students }}人在学 课程总时长:{{
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"
export default {
name: "Detail",
data() {
return {
tabIndex: 2, // 当前选项卡显示的下标
course_id: 0, // 当前课程信息的ID
course_info: {
teacher: {},
}, // 课程信息
course_chapters: [], // 课程的章节课时列表
mp4_url: 'https://video.pearvideo.com/mp4/short/20230703/cont-1784316-71095491-hd.mp4',
}
},
created() {
this.get_course_id();
this.get_course_data();
this.get_chapter();
},
methods: {
onLoaded() {
console.log('视频播放第一帧')
},
onPlayerPlay() {
// 当视频播放时,执行的方法
console.log('视频开始播放')
},
onPlayerPause() {
// 当视频暂停播放时,执行的方法
console.log('视频暂停,可以打开广告了')
},
get_course_id() {
// 获取地址栏上面的课程ID
this.course_id = this.$route.params.id
if (this.course_id < 1) {
let _this = this;
_this.$alert("对不起,当前视频不存在!", "警告", {
callback() {
_this.$router.go(-1);
}
});
}
},
get_course_data() {
// ajax请求课程信息
this.$axios.get(`${this.$settings.BASE_URL}course/course/${this.course_id}/`).then(response => {
this.course_info = response.data.data;
console.log(this.course_info)
}).catch(() => {
this.$message({
message: "对不起,访问页面出错!请联系客服工作人员!"
});
})
},
get_chapter() {
// 获取当前课程对应的章节课时信息
this.$axios.get(`${this.$settings.BASE_URL}course/course_chapter/`, {
params: {
"course": this.course_id,
}
}).then(response => {
this.course_chapters = response.data.data;
}).catch(error => {
window.console.log(error.response);
})
},
},
components: {
Header,
Footer,
}
}
</script>
<style scoped>
.main {
background: #fff;
padding-top: 30px;
}
.course-info {
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left {
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-right {
float: left;
position: relative;
height: 388px;
}
.course-name {
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data {
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time {
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire {
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second {
width: 24px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
padding: 6px 0;
text-align: center;
}
.course-price {
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount {
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original {
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy {
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn {
float: left;
}
.buy .buy-now {
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free {
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart {
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img {
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab {
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list {
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li {
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active {
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free {
color: #fb7c55;
}
.course-content {
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list {
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item {
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title {
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
overflow: hidden;
}
.chapter {
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length {
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title {
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img {
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.section-list {
padding: 0 20px;
}
.section-list .section-item {
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.section-item .name {
font-size: 14px;
color: #666;
float: left;
}
.section-item .index {
margin-right: 5px;
}
.section-item .free {
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.section-item .time {
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.section-item .time img {
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.section-item .try {
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.section-item:hover {
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.section-item:hover .name {
color: #333;
}
.section-item:hover .try {
opacity: 1;
}
.course-side {
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info {
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title {
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.side-title span {
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content {
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1 {
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img {
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name {
float: right;
}
.teacher-content .cont1 .teacher-name {
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title {
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative {
font-size: 14px;
color: #666;
line-height: 24px;
}
</style>
视频托管
# 以后 静态文件(视频,图片,zip。。。),因为会越来越多,不会放在项目的media中---》使用第三方的托管平台---》第三方文件存储
# 存储视频,图片,文件
-1 meida
-2 第三方存储(花钱)
-七牛云
-阿里 oss存储
-其他。。。
-3 自己公司内部搭建文件存储
-Ceph(重)、Minio(用的多一些)、FastDFS(老牌)
-知乎:
-FastDFS:https://zhuanlan.zhihu.com/p/372286804
-Minio:之前学长写的博客
-参照一下:下午我搭建一下
# 使用七牛云托管
-空间:存储文件的地方,不同空间相互隔离
-注册七牛云账号
-选择文件存储kodo
-创建空间,手动上传视频
# 正常项目上传流程
-后端:需要配合文件上传接口
-前端选择视频----》传到咱们后端----》后端把视频传到七牛云上----》返回地址--》存到咱们数据库
-前端直接使用js---》传到七牛云---》七牛云返回地址---》向咱们后端发送ajax请求---》我们直接把地址存到库中即可
# 如何使用python把文件传到七牛云
from qiniu import Auth, put_file
q = Auth('', '')
#要上传的空间
bucket_name = 'lqz-luffy'
#上传后保存的文件名
key = '致命诱惑.mp4'
#生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
#要上传文件的本地路径
localfile = './1.mp4'
ret, info = put_file(token, key, localfile, version='v2')
print(info)
print(ret)
print('http://rx7c1tgq2.hd-bkt.clouddn.com/'+key)
# 域名备案相关
1 以后写了项目,放到互联网上给别人用
2 购买域名---》www.lqz.com
3 备案---》工信部---》流程比较繁琐
4 项目在 189.28.11.11
5 域名解析:只要www.lqz.com ---》189.28.11.11
搜索后台接口
视图类
from utils.common_response import APIResponse
from rest_framework.filters import SearchFilter
# 搜索就是查询所有,带过滤 按课程名搜索
class SearchCourseView(GenericViewSet, ListModelMixin):
serializer_class = CourseSerializer
queryset = Course.objects.filter(is_delete=False, is_show=True).order_by('orders')
filter_backends = [SearchFilter] # 使用模糊查询
search_fields = ['name', 'brief'] # 根据名字和简介查询
搜索前台
搜索导航栏Header.vue div下面
<form class="search">
<div class="tips" v-if="is_search_tip">
<span @click="search_action('Python')">Python</span>
<span @click="search_action('Linux')">Linux</span>
</div>
<input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search"
v-model="search_word">
<el-button icon="el-icon-search" circle @click="search_action(search_word)"></el-button>
</form>
搜索结果页面Search.vue
<template>
<div class="search-course course">
<Header/>
<!-- 课程列表 -->
<div class="main">
<div v-if="course_list.length > 0" class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
<span v-else>共{{course.sections}}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原价:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; line-height: 60px">
没有搜索结果
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[3, 5]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from '../components/Header'
export default {
name: "SearchCourse",
components: {
Header,
},
data() {
return {
course_list: [],
course_total: 0,
filter: {
page_size: 10,
page: 1,
search: '',
}
}
},
created() {
this.get_course()
},
watch: {
'$route.query' () {
this.get_course()
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_course() {
// 获取搜索的关键字
this.filter.search = this.$route.query.word
// 获取课程列表信息
this.$axios.get(`${this.$settings.BASE_URL}course/search/`, {
params: this.filter
}).then(response => {
// 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
this.course_list = response.data.data.results;
this.course_total = response.data.data.count;
}).catch(() => {
this.$message({
message: "获取课程信息有误,请联系客服工作人员"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 过渡动画 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 顶级元素 父级元素 当前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路径") 是否平铺 x轴位置 y轴位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
<template>
<div class="search-course course">
<Header/>
<!-- 课程列表 -->
<div class="main">
<div v-if="course_list.length > 0" class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
<span v-else>共{{course.sections}}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原价:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; line-height: 60px">
没有搜索结果
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[3, 5]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from '../components/Header'
export default {
name: "SearchCourse",
components: {
Header,
},
data() {
return {
course_list: [],
course_total: 0,
filter: {
page_size: 10,
page: 1,
search: '',
}
}
},
created() {
this.get_course()
},
watch: {
'$route.query' () {
this.get_course()
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_course() {
// 获取搜索的关键字
this.filter.search = this.$route.query.word
// 获取课程列表信息
this.$axios.get(`${this.$settings.BASE_URL}course/search/`, {
params: this.filter
}).then(response => {
// 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
this.course_list = response.data.data.results;
this.course_total = response.data.data.count;
}).catch(() => {
this.$message({
message: "获取课程信息有误,请联系客服工作人员"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 过渡动画 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 顶级元素 父级元素 当前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路径") 是否平铺 x轴位置 y轴位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
支付宝支付介绍
"""
1)支付宝API:六大接口
https://docs.open.alipay.com/270/105900/
2)支付宝工作流程(见下图):
https://docs.open.alipay.com/270/105898/
3)支付宝8次异步通知机制(支付宝对我们服务器发送POST请求,索要 success 7个字符)
https://docs.open.alipay.com/270/105902/
"""
# 1、在沙箱环境下实名认证:https://openhome.alipay.com/platform/appDaily.htm?tab=info
# 2、电脑网站支付API:https://docs.open.alipay.com/270/105900/
# 3、完成RSA密钥生成:https://docs.open.alipay.com/291/105971
# 4、在开发中心的沙箱应用下设置应用公钥:填入生成的公钥文件中的内容
# 5、Python支付宝开源框架:https://github.com/fzlee/alipay
# >: pip install python-alipay-sdk --upgrade
# 7、公钥私钥设置
"""
# alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
支付宝公钥
-----END PUBLIC KEY-----
# app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
用户私钥
-----END RSA PRIVATE KEY-----
"""
# 8、支付宝链接
"""
开发:https://openapi.alipay.com/gateway.do
沙箱:https://openapi.alipaydev.com/gateway.do
"""
aliapy二次封装包
# GitHub开源框架
https://github.com/fzlee/alipay
# 安装
pip install python-alipay-sdk --upgrade
# 如果抛ssl相关错误,代表缺失该包
pip install pyopenssl
包结构
libs
├── iPay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件
│ │ ├── app_private_key.pem # 应用私钥文件
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置
alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
拿应用公钥跟支付宝换来的支付宝公钥
-----END PUBLIC KEY-----
app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
通过支付宝公钥私钥签发软件签发的应用私钥
-----END RSA PRIVATE KEY-----
setting.py
import os
# 应用私钥
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
# 应用ID
APP_ID = '2016093000631831'
# 加密方式
SIGN = 'RSA2'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关
GATEWAY = 'https://openapi.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
pay.py
from alipay import AliPay
from . import settings
# 支付对象
alipay = AliPay(
appid=settings.APP_ID,
app_notify_url=None,
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN,
debug=settings.DEBUG
)
# 支付网关
gateway = settings.GATEWAY
init.py
# 包对外提供的变量
from .pay import gateway, alipay
后台 - 支付接口
from django.urls import path, include
from utils.router import router
from . import views
"""
1)支付接口(需要登录认证:是谁):前台提交商品等信息,得到支付链接
post方法
分析:支付宝回调
同步:get给前台 => 前台可以在收到支付宝同步get回调时,ajax异步在给消息同步给后台,也采用get,后台处理前台的get请求
异步:post给后台 => 后台直接处理支付宝的post请求
2)支付回调接口(不需要登录认证:哪个订单(订单信息中有非对称加密)、支付宝压根不可能有你的token):
get方法:处理前台来的同步回调(不一定能收得到,所有不能在该方法完成后台订单状态等信息操作)
post方法:处理支付宝来的异步回调
3)订单状态确认接口:随你前台任何时候来校验订单状态的接口
"""
# 支付接口(生成订单)
router.register('pay', views.PayViewSet, 'pay')
urlpatterns = [
path('', include(router.urls)),
path('success/', views.SuccessViewSet.as_view({'get': 'get', 'post': 'post'}))
]
class AliPayView(APIView):
def get(self, *args, **kwargs):
res = alipay.api_alipay_trade_page_pay(
out_trade_no="2016111211111111",
total_amount=1000,
subject='手机',
return_url="https://example.com",
notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
pay_url = gateway + res
return APIResponse(pay_url=pay_url)
订单相关表设计
"""
class Order(models.Model):
# 主键、总金额、订单名、订单号、订单状态、创建时间、支付时间、流水号、支付方式、支付人(外键) - 优惠劵(外键,可为空)
pass
class OrderDetail(models.Model):
# 订单号(外键)、商品(外键)、实价、成交价 - 商品数量
pass
"""
from django.db import models
from user.models import User
from course.models import Course
class Order(models.Model):
"""订单模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超时取消'),
)
pay_choices = (
(1, '支付宝'),
(2, '微信支付'),
)
subject = models.CharField(max_length=150, verbose_name="订单标题")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True)
trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户")
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = "luffy_order"
verbose_name = "订单记录"
verbose_name_plural = "订单记录"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
@property
def courses(self):
data_list = []
for item in self.order_courses.all():
data_list.append({
"id": item.id,
"course_name": item.course.name,
"real_price": item.real_price,
})
return data_list
class OrderDetail(models.Model):
"""订单详情"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="订单")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
class Meta:
db_table = "luffy_order_detail"
verbose_name = "订单详情"
verbose_name_plural = "订单详情"
def __str__(self):
try:
return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
except:
return super().__str__()
扫描二维码登录
# 1 网站上,点击扫码登录----》弹出二维码
-向后端发送请求----》后端生成二维码---》返回---》前端显示了---》放了个链接地址
# 2 掏出手机,打开对应的app-----》扫描二维码----》app能解析出这个地址----》取出你当前app登录的token----》向这个地址发送请求,携带token
-后端有个 携带token请求登录的接口
-后端接口:拿到请求传入的token,解析出当前用户----》签发token----》找个地方存着
# 3 网站上,开启了定时任务,不停的向后端发送请求----》查询有没有我让它签发token
-一旦发现没有,过一会在发,3分钟内不停的发
-一旦发现有,携带回来---》token---》前端就登录成功了
# 后端接口
-1 生成二维码接口
-2 手机携带token,得到用户,再签发token的接口
-3 网站获取token接口
# 前端
-二维码页面
-定时任务
# 后端
import qrcode
from io import BytesIO
from qrcode.image.pil import PilImage
import base64
from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
from django.core.cache import cache
class QRlogin(ViewSet):
@action(methods=['GET'], detail=False)
def scan(self, request, *args, **kwargs):
img = qrcode.make("http://:8000/api/v1/user/qrlogin/login/?user_id=1")
img = img.get_image()
b = BytesIO()
img.save(b, format='JPEG')
res = base64.b64encode(b.getvalue())
# return APIResponse(url='data:image/jpg;base64,'+res)
return APIResponse(url=res, user_id=1)
@action(methods=['GET'], detail=False)
def login(self, request, *args, **kwargs):
user_id = request.GET.get('user_id')
user = User.objects.get(pk=user_id)
# 签发token,放到redis
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user_info = {'username': user.username, 'token': token}
import time
time.sleep(5)
cache.set('user_id_%s' % user_id, user_info)
return APIResponse()
@action(methods=['GET'], detail=False)
def check_login(self, request, *args, **kwargs):
user_id = request.GET.get('user_id')
user_info = cache.get('user_id_%s' % user_id)
if user_info:
cache.set('user_id_%s' % user_id, None)
return APIResponse(token=user_info.get('token'), username=user_info.get('username'))
else:
return APIResponse(code=101, msg='手机端尚未确认登录')
# 前端
<template>
<div>
<img :src="url" alt="">
</div>
</template>
<script>
export default {
name: "QRCodeLogin",
data() {
return {
url: '',
user_id: '',
t: null
}
},
created() {
this.$axios.get(`${this.$settings.BASE_URL}user/qrlogin/scan/`).then(res => {
this.url = 'data:image/jpg;base64,' + res.data.url
this.user_id = res.data.user_id
// 开启定时任务,向后端发送请求
this.t = setInterval(() => {
this.$axios.get(`${this.$settings.BASE_URL}user/qrlogin/check_login/?user_id=` + this.user_id).then(res => {
if (res.data.code == 100) {
this.$cookies.set('token', res.data.token, '7d')
this.$cookies.set('username', res.data.username, '7d')
clearInterval(this.t)
this.t = null
this.$router.push('/')
} else {
this.$message({
message: res.data.msg,
type: 'warning'
});
}
})
}, 3000)
})
}
}
</script>
<style scoped>
</style>
上线
准备
# 后端项目,修改好线上配置文件,把代码推送到git仓库
# 前端项目
-修改前端访问的后端地址 服务器公网IP
export default {
BASE_URL: 'http://8.130.18.221:8000/api/v1/'
}
-把vue项目编译成 html,css,js
npm run build # 在项目目录下生成dist文件夹,内部就是咱们上线要用的
-把dist文件夹压缩,待命
安装git
# 方式一:
yum install git -y
# 方式二:(开发会用的软件)
yum -y groupinstall "Development tools"
# 执行下面这条
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel -y
安装mysql
# mysql 5.7
# 下载mysql57
wget http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm
# 安装mysql57
yum -y install mysql57-community-release-el7-10.noarch.rpm
yum install mysql-community-server --nogpgcheck -y
# 启动mysql57并查看启动状态
systemctl start mysqld
systemctl status mysqld
# 查看默认密码并登录
grep "password" /var/log/mysqld.log # U>bjULStm1Q<
mysql -uroot -p
# 修改密码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Lqz12345?';
安装redis
###### 官方下载编译好的reids
wget https://packages.redis.io/redis-stack/redis-stack-server-6.2.6-v7.rhel7.x86_64.tar.gz
tar -xf redis-stack-server-6.2.6-v7.rhel7.x86_64.tar.gz
mv redis-stack-server-6.2.6-v7/ redis
cd redis/bin
./redis-server # 启动redis,使用默认配置启动的
#在任意路径下敲redis-server都能把服务运行,
-方式一:把bin路径加入到环境变量
-方式二:使用软连接, /usr/bin/ 本身在环境变量 ,在任意路径敲redis-server redis-cli都能找到
ln -s /root/redis/bin/redis-server /usr/bin/redis-server
ln -s /root/redis/bin/redis-cli /usr/bin/redis-cli
#查看是否创建软连接成功
ls |grep redis
# 启动redis服务,后台运行
redis-server &
###### 源码安装----》
# 下载redis-6.2.6
wget http://download.redis.io/releases/redis-6.2.6.tar.gz
# 解压安装包
tar -xf redis-6.2.6.tar.gz
# 进入目标文件
cd redis-6.2.6
# 编译环境 gcc 在src路径下把源码编译出 redis-cli reidis-server
make
# 复制环境到指定路径完成安装
cp -r ~/redis-6.2.6 /usr/local/redis
# 配置redis可以后台启动:修改下方内容
vim /usr/local/redis/redis.conf
daemonize yes
# 完成配置修改
>: esc
>: :wq
# 建立软连接
ln -s /usr/local/redis/src/redis-server /usr/bin/redis-server1
ln -s /usr/local/redis/src/redis-cli /usr/bin/redis-cli1
# 后台运行redis
>: cd /usr/local/redis
>: redis-server1 ./redis.conf
# 测试redis环境
redis-cli
# 关闭redis服务
pkill -f redis -9
redis-cli shutdown
安装python
# 可以使用yum 安装,不能指定版本
# 源码安装,下载指定版本的源码,编译安装
# 所有linxu和mac,都自带python2:系统服务,是用python写的
# 阿里云的centos默认装了python3.6.8
# python2.7 python3.6.8 python3.9
# 源码安装python,依赖一些第三方zlib* libffi-devel
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel zlib* libffi-devel -y
wget https://registry.npmmirror.com/-/binary/python/3.9.10/Python-3.9.10.tgz
# 解压安装包
tar -xf Python-3.9.10.tgz
#4进入目标文件
cd Python-3.9.10
# 配置安装路径:/usr/local/python39
# 把python3.9.10 编译安装到/usr/local/python39路径下
./configure --prefix=/usr/local/python39
# 编译并安装,如果报错,说明缺依赖
yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel zlib* libffi-devel -y
make && make install
# 建立软连接:/usr/local/python38路径不在环境变量,终端命令 python3,pip3
ln -s /usr/local/python39/bin/python3 /usr/bin/python3.9
ln -s /usr/local/python39/bin/pip3 /usr/bin/pip3.9
# 机器上有多个python和pip命令,对应关系如下
python 2.x pip
python3 3.6 pip3
python3.9 3.9 pip3.9
# 删除安装包与文件:
rm -rf Python-3.9.10
rm -rf Python-3.9.10.tar.xz
安装虚拟环境
# 安装依赖
pip3.9 install virtualenv # 如果报错执行如下命令
# python3.9 -m pip install --upgrade pip
# python3.9 -m pip install --upgrade setuptools
# pip3.9 install pbr
pip3.9 install virtualenvwrapper
# 建立虚拟环境软连接
>: ln -s /usr/local/python39/bin/virtualenv /usr/bin/virtualenv
# 配置虚拟环境:填入下方内容
# ~/ 表示用户家路径:root用户,就是在/root/.bash_profile
>: vi ~/.bash_profile
# 按 a
# 光标上下控制,粘贴上下面内容
VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3.9
source /usr/local/python39/bin/virtualenvwrapper.sh
# 按 esc
# 输入 :wq 敲回车
# 退出编辑状态
esc
# 保存修改并退出
>: :wq
# 更新配置文件内容
source ~/.bash_profile
# 虚拟环境默认根目录:
/root/.virtualenvs
#创建虚拟环境
mkvirtualenv -p python3.9 luffy
安装uwsgi
# django 项目上线需要使用uwsgi这个web服务器运行django项目,安装这个web服务器
# 使用uwsgi运行django,不再使用测试阶段的wsgiref来运行django了
# uwsgi是符合wsgi协议的web服务器,使用c写的性能高,上线要使用uwsgi
# 安装步骤
# 在真实环境下安装
pip3.9 install uwsgi
# 安装到了python38的安装路径的bin路径下了
# 建立软连接
ln -s /usr/local/python39/bin/uwsgi /usr/bin/uwsgi
安装nginx
# 反向代理服务器
- 做请求转发
- 静态资源代理
- 负载均衡
# 前往用户根目录
cd ~
#下载nginx1.13.7
wget http://nginx.org/download/nginx-1.13.7.tar.gz
#解压安装包
tar -xf nginx-1.13.7.tar.gz
#进入目标文件
cd nginx-1.13.7
# 配置安装路径:/usr/local/nginx
./configure --prefix=/usr/local/nginx
#编译并安装
make && make install
# 建立软连接:终端命令 nginx
ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx
#删除安装包与文件:
cd ~
rm -rf nginx-1.13.7
rm -rf nginx-1.13.7.tar.xz
# 测试Nginx环境,服务器运行nginx,本地访问服务器ip
nginx # 启动nginx服务,监听80端口----》公网ip 80 端口就能看到页面了
服务器绑定的域名 或 ip:80
# 静态文件放的路径
/usr/local/nginx/html
# 查看进程
ps aux | grep nginx
部署前端项目
# dist.zip 文件,在本地,上传到远程服务器
# 在远程服务器上
yum install lrzsz -y # 跟本地机器上传下载的软件
yum install unzip -y #解压zip软件
# 在远程服务器上
rz # 打开你本地的目录,选中dist.zip 上传到远端
# 解压
unzip dist.zip
# 移动文件 /home/html 下面有咱们的前端静态文件
mv /root/dist /home/html
# 配置nginx 静态代理
cd /usr/local/nginx/conf # nginx.conf 配置文件
mv nginx.conf nginx.conf.bak
vi nginx.conf
# 按 a 粘贴下面代码
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 80;
server_name 127.0.0.1;
charset utf-8;
location / {
root /home/html; # 指定前端文件的路径
index index.html;
try_files $uri $uri/ /index.html;
}
}
}
# 按 esc :wq 回车
# 重启nginx
nginx -s reload
部署后台项目
# 修改项目中wsgi.py,asgi.py 用uwsgi运行wsgi.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffy_api.settings.prod')
# 导出项目依赖
pip3 freeze > requirements.txt
# 推到远端
### 远程服务器
# 1 拉取项目,安装模块
mkdir /home/project
cd /home/project
git clone 自己的仓库地址
进入到虚拟环境
workon luffy
cd /home/project/luffy_api
pip install -r requirements.txt # 可能会出错 mysqlclient装不上,先注释,装上能装上的,再单独装mysqlclient
yum install python3-devel -y
yum install mysql-devel --nogpgcheck -y
pip install mysqlclient
# 2 虚拟环境中也要安装uwsgi
pip install uwsgi
# 3 配置数据库
mysql -uroot -p
#创建数据库
create database luffy default charset=utf8;
#设置权限账号密码:账号密码要与项目中配置的一致
grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
grant all privileges on luffy.* to 'luffy'@'localhost' identified by 'Luffy123?';
flush privileges;
#退出mysql
quit;
# 使用本地navicate链接阿里云的luffy库,使用luffy用户
# 4 迁移表
# 把项目中得迁移文件删除,提交,远程
python manage.py makemigrations
python manage.py migrate
# 5 uwsgi 运行django
-写一个uwsgi的配置文件,在项目路径下,新建一个 luffyapi.xml
<uwsgi>
<socket>127.0.0.1:8888</socket>
<chdir>/home/project/luffy-api</chdir> # 自己项目的路径
<module>luffy_api.wsgi</module> # wsgi文件的路径
<processes>4</processes>
<daemonize>uwsgi.log</daemonize>
</uwsgi>
-使用uwsgi启动
uwsgi -x luffyapi.xml
-查看是否正常运行
ps aux |grep uwsgi
# 6 配置nginx转发
cd /usr/local/nginx/conf
vi nginx.conf
# 新增的server
server {
listen 8000;
server_name 127.0.0.1;
charset utf-8;
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8888;
uwsgi_param UWSGI_SCRIPT luffy_api.wsgi; # wsgi文件的路径
uwsgi_param UWSGI_CHDIR /home/project/luffy-api/; # 自己项目的路径
}
location /static {
alias /home/project/luffy_api/luffy_api/static; # 静态文件的收集路径
}
}
# 重启nginx
nginx -s reload
# 7 导入数据
# 8 配置域名解析
配置后台admin访问
# 1 浏览器访问http://8.130.18.221:8000/admin 发现没有样式
# 因为uwsgi不能给我们代理静态资源---》debug =False---》不能转发静态资源了
# 收集静态资源,使用nginx代理
# prod.py中加入
STATIC_ROOT = '/home/project/luffy_api/luffy_api/static/'
# 执行命令
# 进入虚拟环境
mkdir /home/project/luffy_api/luffy_api/static
python manage_prod.py collectstatic
# 修改nginx配置文件
# 新增的配置静态文件
server {
listen 8000;
server_name 127.0.0.1;
charset utf-8;
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8888;
uwsgi_param UWSGI_SCRIPT luffy_api.wsgi;
uwsgi_param UWSGI_CHDIR /home/project/luffy_api/;
}
location /static {
alias /home/project/luffy_api/luffy_api/static;
}
}

浙公网安备 33010602011771号