【pip永久换源】
# 1 以后用阿里云 # 2 pip install -i 镜像站 # 3 永久换源 """ 1、文件管理器文件路径地址栏敲:%APPDATA% 回车,快速进入 C:\Users\电脑用户\AppData\Roaming 文件夹中 2、新建 pip 文件夹并在文件夹中新建 pip.ini 配置文件(之前得pip文件要删除) 3、新增 pip.ini 配置文件内容 """ [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
【虚拟环境】
# 虚拟环境作用 1、使不同应用开发环境相互独立 2、环境升级不影响其他应用,也不会影响全局的python环境 3、防止出现包管理混乱及包版本冲突 # 什么是虚拟环境,为什么要有它?它解决了什么问题 -操作系统装了python3.10 -使用django 2.2.2开发了一个项目 -使用django 3.x 开发了一个项目 -把两个项目都打开,同时开发 -每个项目都用自己独立的环境,装的模块相互不影响 -两种解决方案: Virtualenv pipenv
=====================================================
-1 安装两个模块 cmd 命令行执行命令
pip3 install virtualenv # 虚拟环境是它
pip3 install virtualenvwrapper-win
# 该命令是对虚拟环境的加强,以后只需要简单的命令就可以使用和创建虚拟环境
-2 配置虚拟环境管理器工作目录
-在环境变量中新建:WORKON_HOME: C:\Virtualenvs
-在C盘创建Virtualenvs文件夹,以后新建的虚拟环境,都会在这个文件夹下
-3 去Python38的安装目录----Scripts文件夹下----
-----virtualenvwrapper.bat--------双击一下
。
。
如何使用命令 创建和使用虚拟环境
1 pycharm能直接创虚拟环境,命令创建虚拟环境的作用是? 2 后期项目在linux里上线,项目要跑在虚拟环境里,所以也要创建虚拟环境 3 linux里创建虚拟环境,都是用命令来执行的 4 5 而且pycharm能直接创虚拟环境不通用,但是命令方式创建是通用的 6 --------------------------------- 7 8 创建并进入到虚拟环境: 9 10 创建虚拟环境,会自动放在D:\Virtualenvs目录下 11 mkvirtualenv -p python38 虚拟环境名 12 # 此处的python38是要指定解释器的名字 13 14 15 cmd 窗口中带着虚拟环境名字,表示在虚拟环境中, 16 以后安装的所有模块,都是给虚拟环境安装的!!和系统的环境没有关系 17 18 退出虚拟环境 19 deactivate 20 21 查看有哪些虚拟环境 22 workon 23 24 进入虚拟环境 25 workon 虚拟环境名
查看装了哪些模块
pip list 26 27 删除虚拟环境 28 rmvirtualenv 虚拟环境名 # 或者直接去文件夹下删文件
//命令行创建项目
pycharm中创建虚拟环境
【项目创建后台目录调整】
1 # 推荐的目录结构 2 """ 3 4 ├── luffytest 5 ├── logs/ # 项目运行时/开发时日志目录 - 包 6 ├── manage.py # 脚本文件 7 ├── luffytest/ # 项目主应用,开发时的代码保存 - 包 8 ├── apps/ # 开发者的代码保存目录,以模块[子应用]为目录保存 - 包 9 ├── libs/ # 第三方类库的保存目录[第三方组件、模块] - 包 10 ├── settings/ # 配置目录 - 包 11 ├── dev.py # 项目开发时的本地配置 12 └── prod.py # 项目上线时的运行配置 13 ├── urls.py # 总路由 14 └── utils/ # 多个模块[子应用]的公共函数或类库[自己开发的组件]等 15 └── scripts/ # 保存项目运行时的脚本文件夹,小的测试脚本或文件夹,不提交到git上 16 17 """ 18 19 ---------------------------------------- 20 21 # 目录调整后运行不了,报错 22 CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False. 23 24 -原因是 找不到配置文件了---》django项目运行,第一步就要加载配置文件 25 26 # 1 开发阶段:python manage.py runserver ---->找不到配置文件就报错了---》 27 改manage.py文件里面的代码'luffytesr.settings' 变成'luffytesr.settings.dev' 28 因为原来settings配置文件是在项目的小目录下的, 29 现在被我们放到自己创的settings包下面去了,而且还把名字该成了dev.py 30 所以配置文件路径要改一下!!!变成如下 31 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffytest.settings.dev') 32 33 ------------------------------------------------- 34 ------------------------------------------------- 35 36 # 2 上线阶段:要用uwsgi 来运行 要改两个文件 37 asgi.py 与 wsgi.py 文件 38 39 40 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffytest.settings') 41 要改成 42 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffytest.settings.prod') 43 44 ------------------------------------------------------- 45 ------------------------------------------------------- 46 47 # 3 项目能运行阶段 48 49 - 创建app 50 -python manage.py startapp home 命令在哪个路径下执行,app就创建在哪个路径下 51 直接创app会创到根路径下,但是我们现在想要的是创的app文件放在apps包下 52 53 所以 cd 到apps路径下 cd luffytest/apps 54 因为已经切到了apps的路径下了,所以先要找到manage.py要用相对路径的方式找到manage.py的路径 55 -python ../../manage.py startapp home 56 57 或者直接在根目录下,直接创建app,然后直接拖到apps目录下去就行了,最简单!!! 58 59 ------ 60 - 注册app 61 -先到dev里面注册app的名字比如 'luffytest.apps.home', 62 -其次我们命令在apps目录下创的home,home包里面的apps.py里面的name='home' 63 -不对,需要在home包下的apps.py中修改成 name = 'luffytest.apps.home' 64 -所以就有点烦了,建议直接在根目录下创建app,然后手动拖到apps包里面去 65 -这样系统会自动修改成name = 'luffytest.apps.home' 就方便了, 66 -也不用先切apps目录下 python ../../manage.py startapp app名字 67 -直接python manage.py startapp app名字 创app了 68 69 70 但是这样注册还是太烦了,如果就想注册app的时候,就只写app名home怎么办 71 在配置文件的里面最上面把apps包的路径加入到环境变量, 72 这样加载配置文件的一开始就会执行添加环境变量的操作,注册的时候直接写项目名就没问题了 73 import os 74 import sys 75 apps_dir = os.path.join(BASE_DIR, 'apps') 76 sys.path.insert(apps_dir) 77 顺便把项目根目录下的小目录的路径也放到环境变量中, 78 这样以后导apps包下的文件或导项目小目录下的文件都可以直接import 文件名了!!! 79 sys.path.insert(BASE_DIR) 80 81 82 导入模块,导入句式会飘红,飘红不一定是真的有问题,运行一下看看就知道了 83 怎么解决导入句式飘红的问题? 84 把加入到环境变量的路径,右键,做为source root
asgi.py 与 wsgi.py文件里面要改一下,为了上线阶段用uwsgi来运行项目文件用的!!!
。
。
【后端数据库创建】
# 之前项目操作数据库,都是使用root用户,root用户权限太高了 # 在公司里,一般不会给你root用户权限 # 如果开发人员是root权限,数据安全性就很差 # 正常开发人员,专门创建一个用户,用户只对当前项目的库有操作权限 ---------------------------------------- ---------------------------------------- # 面试题:utf8 utf8mb4 有什么区别 ? ---------------------------------------- # 创建一个luffy库,创建luffy用户,luffy用户只对luffy库有操作权限 # 创建luffy数据库 ---------------------------------------------------------- ---------------------------------------------------------- ---------------------------------------------------------- 1.管理员连接数据库 >: mysql -uroot -p密码 2.创建数据库 >: create database luffy default charset=utf8; # charset=utf8mb4也行 3.查看用户 >: select user,host,password from mysql.user; 只有root用户,但是root用户权限太高,所以需要创建luffy用户 # (5.7往后的版本) 查看用户的命令 >: select user,host,authentication_string from mysql.user;
-----------------------------------------------------------
创建普通用户,给这个用户只授权 某个 库的权限
#1 创建luffy库,给项目使用
# 在mysql8中的utf8 就是utf8mb3
create database luffy default charset=utf8mb4;
#2 查看用户
SELECT User, Host FROM mysql.user;
#3 创建用户
CREATE USER 'luffy'@'localhost' IDENTIFIED BY 'Luffy123?';
CREATE USER 'luffy'@'%' IDENTIFIED BY 'Luffy123?';
GRANT ALL PRIVILEGES ON luffy.* TO 'luffy'@'localhost' WITH GRANT OPTION; # 只有本地权限
GRANT ALL PRIVILEGES ON luffy.* TO 'luffy'@'%' WITH GRANT OPTION; # 查看所有权限
使用 luffy 用户,密码是:Luffy123? 登录了,只能看到luffy库
。
。
【后端User表】
链接数据库
settings.dev # 用户名密码写死在代码中了,为了保证安全,可以把用户名与密码写到环境变量中 user = os.environ.get('MYSQL_USER', 'luffy') password = os.environ.get('MYSQL_PASSWORD', 'Luffy123?') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'luffy', 'HOST': '127.0.0.1', 'PORT': '3306', 'USER': user, 'PASSWORD': password, } }
User表
1 from django.db import models 2 from django.contrib.auth.models import AbstractUser 3 4 5 class User(AbstractUser): 6 mobile = models.CharField(max_length=11, unique=True) 7 # ImageField需要pillow包的支持 8 icon = models.ImageField(upload_to='icon', default='icon/default.png') 9 10 class Meta: 11 db_table = 'luffy_user' 12 verbose_name = '用户表' 13 verbose_name_plural = verbose_name 14 15 def __str__(self): 16 return self.username
-----------------------------------------
扩写user表,在setting.dev里配置
# 扩写auth的user表
AUTH_USER_MODEL = 'user.User'
。
。
*******************************************************************************
软件开发模式:
# 瀑布开发
# 敏捷开发
[开启media访问]
1 #1 settings.dev 2 MEDIA_URL = '/media/' 3 MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 4 #2 创建文件夹 5 6 # 3 配一个路由 7 from django.contrib import admin 8 from django.urls import path 9 from django.views.static import serve 10 11 from django.conf import settings 12 13 urlpatterns = [ 14 path('admin/', admin.site.urls), 15 # 127.0.0.1:8000/media/icon/default.png 16 path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}), 17 18 ] 19 20 ### 补充 21 auth的user表,密码加密方式 22 -使用sha256+使用SECRET_KEY作为密码+盐 23 24 自定义用户表,参照这种方式设置密码这个字段
2.
创建超级用户
。
。
。
【项目日志】
(这是我们平常写的记录日志方法)
# 1 配置文件中写 # 真实项目上线后,日志文件打印级别不能过低,因为一次日志记录就是一次文件io操作 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' }, 'simple': { 'format': '%(levelname)s %(module)s %(lineno)d %(message)s' }, }, 'filters': { 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { 'console': { # 实际开发建议使用WARNING 'level': 'DEBUG', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'file': { # 实际开发建议使用ERROR 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi 'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"), # 日志文件的最大值,这里我们设置300M 'maxBytes': 300 * 1024 * 1024, # 日志文件的数量,设置最大日志数量为10 'backupCount': 10, # 日志格式:详细格式 'formatter': 'verbose', # 文件内容编码 'encoding': 'utf-8' }, }, # 日志对象 'loggers': { 'django': { 'handlers': ['console', 'file'], 'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统 }, } } #2 写一个py文件common_logger.py import logging logger = logging.getLogger('django') # 3 使用 class LoggerView(APIView): def get(self,request): logger.info('来了,老弟') return Response('ok')
(第三方记录日志方法,样式好看,且功能比我们自己写的好:loguru)
1 1.安装模块 2 pip3 install loguru 3 4 项目测试配置文件中配置 5 6 from loguru import logger 7 8 LOG_DIR = os.path.join(os.path.dirname(BASE_DIR), "logss") 9 LOGGING_CONFIG = None # 禁用默认的 Django logging 配置 10 11 # logger.remove(handler_id=None) # 指定不在控制台输出(默认输出在控制台中) 12 logger.add(os.path.join(LOG_DIR, "debug.log"), level="DEBUG", rotation="1 day", retention="7 days") 13 logger.add(os.path.join(LOG_DIR, "info.log"), level="INFO", rotation="1 day", retention="7 days") 14 logger.add(os.path.join(LOG_DIR, "error.log"), level="ERROR", rotation="1 day", retention="7 days") 15 logger.add(os.path.join(LOG_DIR, "critical.log"), level="CRITICAL", rotation="1 day", retention="7 days") 16 17 ********************************* 18 测试 19 20 from loguru import logger 21 22 23 class LogruView(APIView): 24 def get(self, request): 25 logger.info('info级别') 26 logger.warning('warning级别') 27 logger.error('error级别') 28 return Response('测试日志') 29 30 ************************************** 31 测试路由 32 from luffytest.apps.home.views import LogruView 33 34 urlpatterns = [ 35 36 path('logs/', LogruView.as_view(), name='logs'), 37 38 ] 39 路由访问 40 41 # http://127.0.0.1:8000/api/v1/user/logs/
。
。
【封装全局异常】
1 from rest_framework.views import exception_handler as drf_exception_handler 2 from rest_framework.response import Response 3 4 from luffytest.utils.common_logger import logger 5 6 7 # 以后可能会自定义自定义异常类 8 class NoPermissionException(Exception): 9 pass 10 11 12 def exception_handler(exc, context): 13 # 记录日志 14 request = context.get('request') 15 view = context.get('view') 16 ip = request.META.get('REMOTE_ADDR') 17 path = request.get_full_path() 18 method = request.method 19 user_id = request.user.id or '【匿名用户】' 20 logger.error( 21 f'操作出错:{str(exc)},ip地址为:{ip},请求方式是:{method},请求地址是:{path},用户为:{user_id},视图类为:{str(view)}') 22 res = drf_exception_handler(exc, context) 23 if res: 24 # drf的异常 data=['错误1',错误2] data={detail:'sss'} 25 if isinstance(res.data, dict): 26 err = res.data.get('detail') 27 elif isinstance(res.data, list): 28 err = res.data[0] 29 else: 30 err = '服务异常,请稍后再尝试,-drf' 31 response = Response({'code': 999, 'msg': err}) 32 else: 33 # 非drf异常,更细力度的区分异常 34 if isinstance(exc, ZeroDivisionError): 35 err = '数据操作出错,除以0了' 36 code = 909 37 elif isinstance(exc, NoPermissionException): 38 err = f'您没有操作权限:{str(exc)}' 39 code = 906 40 else: 41 err = f'系统错误:{str(exc)}' 42 code = 909 43 response = Response({'code': code, 'msg': err}) 44 45 return response 46 47 ******************************* 48 配置文件里配置 49 REST_FRAMEWORK = { 50 'EXCEPTION_HANDLER': 'luffytest.utils.common_exception.exception_handler', 51 } 52 53 ******************************* 54 测试 55 56 # 异常出来测试 57 class ExceptionView(APIView): 58 def get(self, request): 59 # drf异常 60 # raise ValidationError('校验失败') 61 # 2.drf异常 AuthenticationFailed的data是字典带detail情况 62 # raise AuthenticationFailed('认证失败') 63 # 3.drf 的APIException 64 # raise APIException('api异常') 65 # 4. 66 5/0 67 return Response('ok') 68 69 *************** 70 路由 71 path('exception/', ExceptionView.as_view()) 72 http://127.0.0.1:8000/api/v1/user/exception/
-----------------------------------------------------------------------------------------------------------
【二次封装Response】
1 '''
2
3 要达到的效果,以后使用APIResponse的情况
4 -return APIResponse()----------前端收到-->{code:100,msg:成功}
5 -return APIResponse(token=xsdse,username=lqz)----------前端收到-->{code:100,msg:成功,token:xsdse,username:lqz}
6 -return APIResponse(data=[{},{},{}])----------前端收到-->{code:100,msg:成功,data:[{},{},{}]}
7 -return APIResponse(code=101,msg=失败)----------前端收到-->{code:101,msg:失败}
8 -return APIResponse(headers={'xx':'yy'})----------前端收到-->{code:100,msg:成功},但是响应头中有xx=yy
9 -return APIResponse(status=201)----------前端收到-->{code:100,msg:成功},但是响应状态码是201
10
11
12 效果就是:传code msg status headers 都能正常改,不传有默认值,
13 如果有多余的参数传过来了,都统一放到响应体的字典里面去
14 实际上需要达到的效果就是:传Response里面有的参数,会替换,传传Response里面没有的参数,
15 会给APIResponse添加属性!!!
16
17 '''
18 common_response.py
19
20 from rest_framework.response import Response
21
22
23 class APIResponse(Response):
24 # data=None, status=None,template_name=None, headers=None,exception=False, content_type=None
25 def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs):
26 data = {'code': code, 'msg': msg}
27 if kwargs: # 说明除了code,msg这些以外,还传了其他数据
28 data.update(kwargs)
29 super().__init__(data=data, status=status, headers=headers)
30 子类调用父类里面的方法,执行Response里面的双下init方法,实现真正的初始化
31 ---------------------------
32 测试代码
33 class ResponseView(APIView):
34 def get(self, request, *args, **kwargs):
35 return APIResponse(username='tank',token='abc.e.d')
。
。
【前端项目创建】
1 # 1 vue2 创建项目 2 vue create luffy_city 3 # 2 使用pycharm打开 4 5 # 3 文件调整 6 -删除Helloworld组件 7 -删除AboutView页面组件 8 -App.vue中只留 9 <router-view/>
。
【前端配置】
1 # 1 axios 2 -cnpm install axios -S 3 -main.js中 4 import axios from 'axios' 5 Vue.prototype.$axios=axios 6 # 2 elementui 7 -cnpm install element-ui -S 8 -main.js 9 import ElementUI from 'element-ui'; 10 import 'element-ui/lib/theme-chalk/index.css'; 11 Vue.use(ElementUI); 12 # 3 cookies 13 -cnpm install vue-cookies -S 14 -main.js 15 import cookies from 'vue-cookies' 16 Vue.prototype.$cookies = cookies; 17 18 19 # 4 写个settings.js 20 const BASE_URL='http://127.0.0.1:8000/api/v1/' 21 export default { 22 banner:BASE_URL+'/home/banner/', 23 } 24 -main.js注册 25 import settings from "@/assets/js/settings"; 26 Vue.prototype.$settings=settings 27 - 再组件中使用 28 methods:{ 29 getdata(){ 30 async function a(){ 31 let response=await this.$axios(this.$settings.banner) 32 33 } 34 } 35 } 36 37 38 # 5 去掉默认样式 39 -写个global.css 40 /* 声明全局样式和项目的初始化样式 */ 41 body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea { 42 margin: 0; 43 padding: 0; 44 font-size: 15px; 45 } 46 47 a { 48 text-decoration: none; 49 color: #333; 50 } 51 52 ul { 53 list-style: none; 54 } 55 56 table { 57 border-collapse: collapse; /* 合并边框 */ 58 } 59 -再main.js中引入 60 // 全局css 61 import '@/assets/css/global.css'
。
。
【前台主页功能】
(在后端写)
# 1 轮播图接口--写它
# 2 推荐课程接口(往后放)
# 3 创建home--app
# 4 创建Banner表 -写一个基本 BaseModel,写一些基本字段,以后 轮播图或课程继承这个表
Uitil-Model-BaseModel
1 from django.db import models 2 3 4 class BaseModel(models.Model): 5 created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') 6 updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间') 7 is_delete = models.BooleanField(default=False, verbose_name='是否删除') 8 is_show = models.BooleanField(default=True, verbose_name='是否上架') 9 orders = models.IntegerField(verbose_name='优先级') 10 11 class Meta: 12 abstract = True # 只用来继承,不在数据库中生成表
。
Home-Model-Banner
1 from django.db import models 2 3 from luffytest.utils.common_model import BaseModel 4 5 6 # Create your models here. 7 8 class Banner(BaseModel): 9 title = models.CharField(max_length=16, unique=True, verbose_name='名称') 10 image = models.ImageField(upload_to='banner', verbose_name='图片') 11 # web , 小程序 ,app --》轮播图--》可以点击--》点击--》跳转到某个地址 12 # 跳转有两种情况:1 外链 2 自己的页面 13 link = models.CharField(max_length=64, verbose_name='跳转链接') 14 info = models.TextField(verbose_name='详情') 15 16 class Meta: 17 db_table = 'luffy_banner' 18 verbose_name_plural = '轮播图' 19 20 def __str__(self): 21 return self.title
serializer
1 from rest_framework import serializers 2 3 from .models import Banner 4 5 6 class Bannerserializer(serializers.ModelSerializer): 7 class Meta: 8 model = Banner 9 fields = ['title', 'image', 'link']
home.view
1 from django.shortcuts import render 2 from rest_framework.viewsets import GenericViewSet 3 from rest_framework.mixins import ListModelMixin 4 from utils.common_mixin import APIListModelMixin 5 from .models import Banner 6 from .serializer import Bannerserializer 7 8 9 # 轮播图接口 新增,修改,删除---用admin做,不要再写接口 10 class BannerView(GenericViewSet, APIListModelMixin): 11 queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders') # 过滤排序 12 serializer_class = Bannerserializer
common_mixin
1 from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, \ 2 UpdateModelMixin 3 4 from utils.common_response import APIResponse 5 6 7 class APIListModelMixin(ListModelMixin): 8 def list(self, request, *args, **kwargs): 9 res = super().list(request, *args, **kwargs) 10 return APIResponse(results=res.data)
注意如果报错
RuntimeError: Model class apps.home.models.Banner doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.
原因是 有些地方的导入有问题,虽然apps加入到了环境变量中,但是还是不行,找到从apps下导入的
句式都改成从根目录下开始导入!!!就解决报错了!!!
。
。
自定义配置
##1 settings下新建 common_settings.py # 用户自己配置,以后开发或上线环境都会用 BANNER_COUNT = 3 # 2 以后再dev.py和pro.py都导入 # 导入公共配置 from .common_settings import * # 3 在任意位置,导入django配置文件 from django.conf import settings queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[0:settings.BANNER_COUNT]
。
。
admin使用simpleui
# 1 下载 pip install django-simpleui # 2 app注册 INSTALLED_APPS = [ 'simpleui', ] # 3 在admin中注册 from .models import Banner @admin.register(Banner) class BannerAdmin(admin.ModelAdmin): list_display = ('id', 'title', 'image', 'link')
#4在home.app中
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'
verbose_name = '首页'
轮播图增加
。
。
【跨域问题详解】
# 1 浏览器同源策略 -同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能 -请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名(www.xx.com),端口(80,8080),协议(ftp,https,http)相同. -浏览器上就会报错,个就是同源策略的保护 # 2 浏览器的安全策略 -访问一个跨域[域名,端口,协议不同]接口---》后端正常执行--》浏览器拦截了 -只有web端需要处理跨域,小程序,app都不需要 # 3 解决跨域问题 -cors :咱们使用的,后端配置 -JSONP:忽略 -nginx代理跨域(服务器代理) -vue 代理(开发阶段) # 4 CORS(跨域资源共享)简介 CORS需要浏览器和服务器【响应头加东西】同时支持。目前,所有浏览器都支持该功能 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信 # 5 cors 两种请求 -简单请求:请求一次--》直接发送--》服务的处理了cors--》顺利返回--》如果服务的没处理就报错 -非简单请求:先发一次options请求--》服务的如果处理了cors--》再发真正的请求-》如果服务端没处理,它就不发了 # 6 简单和非简单区别 # 符合下面两个条件就是简单请求 - 请求方法是以下三种方法之一: HEAD GET POST -HTTP的头信息不超出以下几种字段: Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
。
自定义中间件处理跨域
1 # 自定义中间件 2 from django.utils.deprecation import MiddlewareMixin 3 4 5 class CorsMiddleWare(MiddlewareMixin): 6 def process_response(self, request, response): 7 if request.method == "OPTIONS": 8 # 可以加* 9 response["Access-Control-Allow-Headers"] = "*" 10 response['Access-Control-Allow-Methods'] = '*' 11 response["Access-Control-Allow-Origin"] = "*" 12 return response 13 14 --------------------------------- 15 #dev 配置文件配置## 16 MIDDLEWARE = [ 17 'utils.common_middleware.CorsMiddleWare' 18 ] 19 20 --------------------- 21 # 测试跨域问题 22 def cors_view(request): 23 res = HttpResponse('ok') 24 # res['Access-Control-Allow-Origin'] = '*' 25 # if request.method == 'OPTIONS': 26 # res['Access-Control-Allow-Headers'] = '*' 27 return res
路由:
path('cors/', cors_view)
===========================================================================
后端homeview.vue
created(){
this.$axios({
url:'http://127.0.0.1:8000/api/v1/user/cors/',
method:'GET',
headers:{
token:'aa,bb,cc'
}
}).then(res=>{
console.log(res)
}).catch(err=>{
console.log('err:',err)
})
},
第三方app解决跨域问题
1 # 1、使用pip安装 2 pip install django-cors-headers 3 4 # 2、添加到setting的app中 5 INSTALLED_APPS = ( 6 ... 7 'corsheaders', 8 ... 9 ) 10 # 3、添加中间件 11 MIDDLEWARE = [ 12 ... 13 'corsheaders.middleware.CorsMiddleware', 14 ... 15 ] 16 17 ------------------------------ 18 在公共的配置文件了设置 19 CORS_ORIGIN_ALLOW_ALL = True 20 CORS_ALLOW_METHODS = ( 21 'DELETE', 22 'GET', 23 'OPTIONS', 24 'PATCH', 25 'POST', 26 'PUT', 27 'VIEW', 28 ) 29 30 CORS_ALLOW_HEADERS = ( 31 'XMLHttpRequest', 32 'X_FILENAME', 33 'accept-encoding', 34 'authorization', 35 'content-type', 36 'dnt', 37 'origin', 38 'user-agent', 39 'x-csrftoken', 40 'x-requested-with', 41 'Pragma', 42 'token', 43 )
。
。
【首页前后端打通】
Header.vue组件
1 <script> 2 export default { 3 name: "Header", 4 data() { 5 return { 6 url_path: sessionStorage.url_path || '/', 7 } 8 }, 9 methods: { 10 goPage(url_path) { 11 // 已经是当前路由就没有必要重新跳转 12 if (this.url_path !== url_path) { 13 this.$router.push(url_path); 14 } 15 sessionStorage.url_path = url_path; 16 }, 17 }, 18 created() { 19 // this.$route.path 当前路径 / /about /course 20 sessionStorage.url_path = this.$route.path; 21 this.url_path = this.$route.path; 22 } 23 } 24 </script> 25 26 <template> 27 <div class="header"> 28 <div class="slogan"> 29 <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p> 30 </div> 31 <div class="nav"> 32 <ul class="left-part"> 33 <li class="logo"> 34 <router-link to="/"> 35 <img src="../assets/img/head-logo.svg" alt=""> 36 </router-link> 37 </li> 38 <li class="ele"> 39 <span @click="goPage('/free')" :class="{active: url_path === '/free'}">免费课</span> 40 </li> 41 <li class="ele"> 42 <span @click="goPage('/actual')" :class="{active: url_path === '/actual'}">实战课</span> 43 </li> 44 <li class="ele"> 45 <span @click="goPage('/light')" :class="{active: url_path === '/light'}">轻课</span> 46 </li> 47 </ul> 48 49 <div class="right-part"> 50 <div> 51 <span>登录</span> 52 <span class="line">|</span> 53 <span>注册</span> 54 </div> 55 </div> 56 </div> 57 </div> 58 </template> 59 60 <style scoped> 61 .header { 62 background-color: white; 63 box-shadow: 0 0 5px 0 #aaa; 64 } 65 66 .header:after { 67 content: ""; 68 display: block; 69 clear: both; 70 } 71 72 .slogan { 73 background-color: #eee; 74 height: 40px; 75 } 76 77 .slogan p { 78 width: 1200px; 79 margin: 0 auto; 80 color: #aaa; 81 font-size: 13px; 82 line-height: 40px; 83 } 84 85 .nav { 86 background-color: white; 87 user-select: none; 88 width: 1200px; 89 margin: 0 auto; 90 91 } 92 93 .nav ul { 94 padding: 15px 0; 95 float: left; 96 } 97 98 .nav ul:after { 99 clear: both; 100 content: ''; 101 display: block; 102 } 103 104 .nav ul li { 105 float: left; 106 } 107 108 .logo { 109 margin-right: 20px; 110 } 111 112 .ele { 113 margin: 0 20px; 114 } 115 116 .ele span { 117 display: block; 118 font: 15px/36px '微软雅黑'; 119 border-bottom: 2px solid transparent; 120 cursor: pointer; 121 } 122 123 .ele span:hover { 124 border-bottom-color: orange; 125 } 126 127 .ele span.active { 128 color: orange; 129 border-bottom-color: orange; 130 } 131 132 .right-part { 133 float: right; 134 } 135 136 .right-part .line { 137 margin: 0 10px; 138 } 139 140 .right-part span { 141 line-height: 68px; 142 cursor: pointer; 143 } 144 </style>
Footer.vue组件
1 <script> 2 export default { 3 name: "Footer" 4 } 5 </script> 6 7 <template> 8 <div class="footer"> 9 <ul> 10 <li>关于我们</li> 11 <li>联系我们</li> 12 <li>商务合作</li> 13 <li>帮助中心</li> 14 <li>意见反馈</li> 15 <li>新手指南</li> 16 </ul> 17 <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p> 18 </div> 19 </template> 20 21 <style scoped> 22 .footer { 23 width: 100%; 24 height: 128px; 25 background: #25292e; 26 color: #fff; 27 } 28 29 .footer ul { 30 margin: 0 auto 16px; 31 padding-top: 38px; 32 width: 810px; 33 } 34 35 .footer ul li { 36 float: left; 37 width: 112px; 38 margin: 0 10px; 39 text-align: center; 40 font-size: 14px; 41 } 42 43 .footer ul::after { 44 content: ""; 45 display: block; 46 clear: both; 47 } 48 49 .footer p { 50 text-align: center; 51 font-size: 12px; 52 } 53 </style>
Banner.vue组件
1 <script> 2 import api from '../assets/js/settings' 3 4 export default { 5 name: "Banner", 6 data() { 7 return { 8 bannerList: [] 9 } 10 }, 11 created() { 12 this.$axios.get(api.banner).then(res => { 13 console.log(res.data) 14 if (res.data.code == 100) { 15 this.bannerList = res.data.results 16 } else { 17 this.$message({type: "error", message: res.data.msg}) 18 } 19 }).catch(err => { 20 this.$message({type: "error", message: '服务器异常,请联系系统管理员'}) 21 }) 22 } 23 } 24 </script> 25 26 <template> 27 <div class="banner"> 28 <el-carousel height="400px" :interval="3000"> 29 <el-carousel-item v-for="(item,index) in bannerList" :key="index"> 30 <!-- 跳内部,跳外链接--> 31 <div v-if="item.link.indexOf('http')>=0"> 32 <a :href="item.link"> 33 <img :src="item.image" :alt="item.title"> 34 </a> 35 </div> 36 37 <div v-else> 38 <router-link :to="item.link"> 39 <img :src="item.image" :alt="item.title"> 40 </router-link> 41 </div> 42 </el-carousel-item> 43 </el-carousel> 44 </div> 45 </template> 46 47 <style scoped> 48 .el-carousel__item { 49 height: 400px; 50 min-width: 1200px; 51 } 52 53 .el-carousel__item img { 54 height: 400px; 55 margin-left: calc(50% - 1920px / 2); 56 } 57 </style>
HomeView.vue
1 <template> 2 <div class="home"> 3 <Header></Header> 4 <Banner></Banner> 5 <div class="course"> 6 <el-row> 7 <el-col :span="6" v-for="(o, index) in 8" :key="o" class="course_detail"> 8 <el-card :body-style="{ padding: '0px' }"> 9 <img src="http://photo.liuqingzheng.top/2023%2002%2022%2021%2057%2011%20/image-20230222215707795.png" 10 class="image"> 11 <div style="padding: 14px;"> 12 <span>推荐课程</span> 13 <div class="bottom clearfix"> 14 <time class="time">价格:999</time> 15 <el-button type="text" class="button">查看详情</el-button> 16 </div> 17 </div> 18 </el-card> 19 </el-col> 20 </el-row> 21 </div> 22 <img src="http://photo.liuqingzheng.top/2023%2003%2001%2016%2010%2034%20/1.png" alt="" width="100%" height="500px"> 23 <Footer></Footer> 24 </div> 25 </template> 26 27 <script> 28 29 30 import Header from "@/components/Header.vue"; 31 import Footer from "@/components/Footer.vue"; 32 import Banner from "@/components/Banner.vue"; 33 34 export default { 35 name: 'HomeView', 36 methods: { 37 getdata() { 38 async function a() { 39 let response = await this.$axios(this.$settings.banner) 40 41 } 42 } 43 }, 44 created() { 45 this.$axios({ 46 url: 'http://127.0.0.1:8000/api/v1/user/cors/', 47 method: 'GET', 48 headers: { 49 token: 'aa,bb,cc' 50 } 51 }).then(res => { 52 console.log(res) 53 }).catch(err => { 54 console.log('err:', err) 55 }) 56 }, 57 components: { 58 Header, 59 Footer, 60 Banner, 61 } 62 } 63 </script> 64 65 <style scoped> 66 .time { 67 font-size: 13px; 68 color: #999; 69 } 70 71 .bottom { 72 margin-top: 13px; 73 line-height: 12px; 74 } 75 76 .button { 77 padding: 0; 78 float: right; 79 } 80 81 .image { 82 width: 100%; 83 display: block; 84 } 85 86 .clearfix:before, 87 .clearfix:after { 88 display: table; 89 content: ""; 90 } 91 92 .clearfix:after { 93 clear: both 94 } 95 96 .course_detail { 97 padding: 50px; 98 } 99 </style>
。
。
#1 登陆和注册的,要写的接口 -1 发送短信验证码接口 -2 多方式登陆接口(用户名/邮箱/手机号+密码) -3 手机号+验证码登陆 -4 手机号+验证码+密码 ---》短信注册接口 -5 校验手机号是否存在接口 # 2 发送短信: 阿里大于短信 腾讯云短信:讲课 容联云通信 # 3 注册腾讯云短信 -1 地址:https://cloud.tencent.com/product/sms -2 创建签名 -公众号:注册个公众号 -微信公众号 -3 创建模版 # 4 备案网站 -1 买域名 -2 服务器:包年包月 -3 部署项目:个人博客 -4 阿里云初审: -5 工信部备案 -30天左右 -6 备案过的域名
。
。
手机号是否存在接口
1 app.user.view 2 3 from rest_framework.viewsets import ViewSet 4 from rest_framework.decorators import action 5 from .models import User 6 from utils.common_response import APIResponse 7 8 9 class UserMobileView(ViewSet): 10 @action(methods=['get'], detail=False) 11 def check_mobile(self, request, *args, **kwargs): 12 try: 13 # 获取手机号 放在地址栏中 14 mobile = request.query_params.get('mobile') 15 # 数据库校验手机号是否存在 16 User.objects.get(mobile=mobile) 17 return APIResponse(msg='手机号已存在') 18 except Exception as e: 19 raise Exception('手机号不存在') 20 21 *********************************** 22 urls 23 24 from rest_framework.routers import SimpleRouter 25 26 router = SimpleRouter() 27 # http://127.0.0.1:8000/api/v1/user/mobile/check_mobile/ 28 router.register('mobile', UserMobileView, 'mobile') 29 30 urlpatterns += router.urls
。
。
多方式登录接口
1 view.py 2 3 from .serializer import LoginSerializer 4 5 6 class UserView(GenericViewSet): 7 serializer_class = LoginSerializer 8 9 @action(methods=['POST'], detail=False) 10 def mul_login(self, request, *args, **kwargs): 11 # 1 把校验逻辑写在序列化类中 12 serializer: LoginSerializer = self.get_serializer(data=request.data) 13 serializer.is_valid(raise_exception=True) 14 token = serializer.context.get('token') 15 username = serializer.context.get('username') 16 icon = serializer.context.get('icon') 17 return APIResponse(token=token, username=username, icon=icon) 18 # 另一种方案,但是 serializer.instance 必须是user对象 19 # serializer.data # {'username.icon} 20 # print(serializer.data) 21 # return APIResponse(token=token, username=serializer.data.get('username'), icon=serializer.data.get('icon')) 22 ----------------------------------- 23 序列化类 24 25 from rest_framework import serializers 26 from .models import User 27 import re 28 from rest_framework.exceptions import ValidationError 29 from rest_framework_simplejwt.tokens import RefreshToken 30 from django.conf import settings 31 32 33 class LoginSerializer(serializers.ModelSerializer): 34 # 这个序列化类用来: 1 校验 35 36 # 坑:必须重写username--》去除它的unique 37 username = serializers.CharField() 38 39 class Meta: 40 model = User 41 fields = ['username', 'password', 'icon'] 42 extra_kwargs = { 43 'password': {'write_only': True} # 它不做序列化 44 } 45 46 def _get_user(self, attrs): 47 username = attrs.get('username') 48 password = attrs.get('password') 49 # 2 去数据库校验:正则--》 50 if re.match(r'^1[3-9][0-9]{9}$', username): 51 user = User.objects.filter(mobile=username).first() 52 elif re.match('^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$', username): 53 user = User.objects.filter(email=username).first() 54 else: 55 user = User.objects.filter(username=username).first() 56 57 if user and user.check_password(password): 58 return user 59 else: 60 raise ValidationError('用户名或密码错误') 61 62 def _get_token(self, user): 63 refresh = RefreshToken.for_user(user) 64 return str(refresh.access_token) 65 66 def _pre_data(self, token, user): 67 self.context['token'] = token 68 self.context['username'] = user.username 69 # self.instance=user # 当前用户,放到instance中了 70 self.context['icon'] = settings.BACKEND_URL + "media/" + str(user.icon) # 不带 域名前缀的 71 72 def validate(self, attrs): 73 # 1 取出用户名(手机号,邮箱)和密码 74 user = self._get_user(attrs) 75 # 2 如果存在:签发token,返回 76 token = self._get_token(user) 77 # 3 把token,用户名和icon放入context 78 self._pre_data(token, user) 79 return attrs 80 81 82 ---------- 83 公共配置文件里配置 84 85 BACKEND_URL = 'http://127.0.0.1:8000/'
。
异常处理修改
1 from rest_framework.views import exception_handler as drf_exception_handler 2 from rest_framework.response import Response 3 4 from luffytest.utils.common_logger import logger 5 6 7 # 以后可能会自定义自定义异常类 8 class NoPermissionException(Exception): 9 pass 10 11 12 def exception_handler(exc, context): 13 # 记录日志 14 request = context.get('request') 15 view = context.get('view') 16 ip = request.META.get('REMOTE_ADDR') 17 path = request.get_full_path() 18 method = request.method 19 user_id = request.user.id or '【匿名用户】' 20 logger.error( 21 f'操作出错:{str(exc)},ip地址为:{ip},请求方式是:{method},请求地址是:{path},用户为:{user_id},视图类为:{str(view)}') 22 res = drf_exception_handler(exc, context) 23 if res: 24 # drf的异常 data=['错误1',错误2] data={detail:'sss'} 25 if isinstance(res.data, dict): 26 err = res.data.get('detail') or res.data.get('non_field_errors')[0] or '系统错误' 27 elif isinstance(res.data, list): 28 err = res.data[0] 29 else: 30 err = '服务异常,请稍后再尝试,-drf' 31 response = Response({'code': 999, 'msg': err}) 32 else: 33 # 非drf异常,更细力度的区分异常 34 if isinstance(exc, ZeroDivisionError): 35 err = '数据操作出错,除以0了' 36 code = 909 37 elif isinstance(exc, NoPermissionException): 38 err = f'您没有操作权限:{str(exc)}' 39 code = 906 40 else: 41 err = f'{str(exc)}' 42 code = 909 43 response = Response({'code': code, 'msg': err}) 44 45 return response
。
。
腾讯云短信封装
腾讯云短信申请
# 发送短信接口,借助于第三方短信平台,收费的 -腾讯云短信 -阿里 大于短信 -。。。。 --------------------------------------- # 使用腾讯短信 https://cloud.tencent.com,微信扫码登录 先点产品,再在搜索框里面搜索短信,回车调到短信的页面 https://console.cloud.tencent.com/smsv2 1 创建短信签名:公众号注册,提交等待审核通过 2 需要先申请微信公众号,用网站或app认证比较烦 3 创建短信正文模版 -等待审核 -发送短信 python代码发送昂短信
# API SDK
-API: 腾讯自己的API接口,我们要带着数据向对应的地址发送post请求,
然后腾讯会根据我们post携带的对应数据,发送对应的短信给对应的人
写起来比较麻烦,要自己分析腾讯的接口
------------------------------------------------
-SDK:集成开发工具包,分语言,java,python,go
-使用python 对api进行封装成包
-以后我们只需要,安装包,导入包,包名.发送短信,传入参数,就可以发送了
------------------------------------------------
-只要官方提供sdk,优先用sdk
先下载adk的模块
pip install tencentcloud-sdk-python
python sdk
安装python项目里面安装sdk模块
把发送短信的代码整体复制到,python文件里面去,对应参数改好,右键运行,就行了
需要修改5个参数
生成密钥
发送短信
1 # -*- coding: utf-8 -*- 2 from tencentcloud.common import credential 3 from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException 4 # 导入对应产品模块的client models。 5 from tencentcloud.sms.v20210111 import sms_client, models 6 7 # 导入可选配置类 8 from tencentcloud.common.profile.client_profile import ClientProfile 9 from tencentcloud.common.profile.http_profile import HttpProfile 10 11 try: 12 # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi 13 cred = credential.Credential("AKIDlT7aqG7wkZONVkzEY5lSEB39QqGdFUmz", "SwMkHnEmyIXkv4bKFAFe5c9ZAkT7lXEl") 14 httpProfile = HttpProfile() 15 # httpProfile = HttpProfile(proxy="http://用户名:密码@代理IP:代理端口") 16 httpProfile.reqMethod = "POST" # post请求(默认为post请求) 17 httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒) 18 httpProfile.endpoint = "sms.tencentcloudapi.com" # 指定接入地域域名(默认就近接入) 19 20 clientProfile = ClientProfile() 21 clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法 22 clientProfile.language = "en-US" 23 clientProfile.httpProfile = httpProfile 24 client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile) 25 req = models.SendSmsRequest() 26 # 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看 27 req.SmsSdkAppId = "1400909757" 28 req.SignName = "阿浩取经公众号" 29 # 模板 ID: 必须填写已审核通过的模板 ID 30 req.TemplateId = "2153916" 31 # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空 32 req.TemplateParamSet = ["123456"] 33 # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 34 req.PhoneNumberSet = ["+8618451110811"] 35 req.SessionContext = "" 36 req.ExtendCode = "" 37 req.SenderId = "" 38 resp = client.SendSms(req) 39 # 输出json格式的字符串回包 40 print(resp.to_json_string(indent=2)) 41 42 43 44 except TencentCloudSDKException as err: 45 print(err)
。
封装成包
# 1 包结构 libs
-tx_sms
-__init__.py #给外部使用的,在这注册
-settings.py # 配置
-sms.py # 核心
settings SECRET_ID = 'AKIDlT7aqG7wkZONVkzEY5lSEB39QqGdFUmz' SECRET_KEY = 'SwMkHnEmyIXkv4bKFAFe5c9ZAkT7lXEl' SmsSdkAppId = "1400909757" SIGN_NAME = '阿浩取经公众号' TemplateId = "2153916"
sms.py # -*- coding: utf-8 -*- from tencentcloud.common import credential from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.sms.v20210111 import sms_client, models from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile import random from .settings import * # 生成n位数字的随机验证码 def get_code(num=4): code = '' for i in range(num): r = random.randint(0, 9) code += str(r) return code # 发送短信函数 def send_sms(mobile, code): try: # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi cred = credential.Credential(SECRET_ID, SECRET_KEY) httpProfile = HttpProfile() # httpProfile = HttpProfile(proxy="http://用户名:密码@代理IP:代理端口") httpProfile.reqMethod = "POST" # post请求(默认为post请求) httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒) httpProfile.endpoint = "sms.tencentcloudapi.com" # 指定接入地域域名(默认就近接入) clientProfile = ClientProfile() clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法 clientProfile.language = "en-US" clientProfile.httpProfile = httpProfile client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile) req = models.SendSmsRequest() # 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看 req.SmsSdkAppId = SmsSdkAppId req.SignName = SIGN_NAME # 模板 ID: 必须填写已审核通过的模板 ID req.TemplateId = TemplateId # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空 req.TemplateParamSet = [code] # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 req.PhoneNumberSet = [f"+86{mobile}"] req.SessionContext = "" req.ExtendCode = "" req.SenderId = "" resp = client.SendSms(req) # 输出json格式的字符串回包 print(resp.to_json_string(indent=2)) res = resp.to_json_string(indent=2) if res.get('SendStatusSet')[0].get('Code') == 'Ok': return True else: return False except TencentCloudSDKException as err: print(err) return False if __name__ == '__main__': print(get_code(3))
init from .sms import get_code,send_sms
。
发送短信
class UserMobileView(ViewSet): # 发送短信接口 @action(methods=['GET'], detail=False) def send_sms(self, request, *args, **kwargs): # 后续要保证发送短信接口安全 # 1 取出手机号 mobile = request.query_params.get('mobile') # 2 生成验证码,保存到 缓存中--》后续要取出来校验 code = get_code() print(code) cache.set(f'cache_code_{mobile}', code) # key 要唯一,根据手机号唯一 # # 3 发送短信--》同步发送 # res=sms(mobile,code) # # 4 返回给前端 # if res: # return APIResponse(msg='短信发送成功') # else: # return APIResponse(code=101,msg='发送短信失败,请稍后再试') # 3 异步发送短信,使用多线程,后期会用别的 t = Thread(target=sms, args=[mobile, code]) t.start() return APIResponse(msg='短信已发送')
。
。
短信登陆
视图类
1 from .serializer import LoginSerializer,SMSLoginSerializer 2 3 4 class UserView(GenericViewSet): 5 serializer_class = LoginSerializer 6 @action(methods=['POST'], detail=False) 7 def mul_login(self, request, *args, **kwargs): 8 return self._login(request, *args, **kwargs) 9 10 @action(methods=['POST'], detail=False) 11 def sms_login(self, request, *args, **kwargs): 12 return self._login(request, *args, **kwargs) 13 14 def _login(self, request, *args, **kwargs): 15 serializer = self.get_serializer(data=request.data) 16 serializer.is_valid(raise_exception=True) 17 token = serializer.context.get('token') 18 username = serializer.context.get('username') 19 icon = serializer.context.get('icon') 20 return APIResponse(token=token, username=username, icon=icon) 21 22 def get_serializer_class(self): 23 if self.action == 'sms_login': 24 return SMSLoginSerializer 25 else: 26 return LoginSerializer
序列化类
from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import ValidationError
from rest_framework_simplejwt.tokens import RefreshToken
from django.conf import settings
from django.core.cache import cache
class CommonLoginSerializer:
def _get_user(self, attrs):
raise Exception('这个方法必须被重写')
def _get_token(self, user):
refresh = RefreshToken.for_user(user)
return str(refresh.access_token)
def _pre_data(self, token, user):
self.context['token'] = token
self.context['username'] = user.username
# self.instance=user # 当前用户,放到instance中了
self.context['icon'] = settings.BACKEND_URL + "media/" + str(user.icon) # 不带 域名前缀的
def validate(self, attrs):
# 1 取出用户名(手机号,邮箱)和密码
user = self._get_user(attrs)
# 2 如果存在:签发token,返回
token = self._get_token(user)
# 3 把token,用户名和icon放入context
self._pre_data(token, user)
return attrs
class SMSLoginSerializer(CommonLoginSerializer, serializers.Serializer):
# 重写
code = serializers.CharField()
mobile = serializers.CharField()
def _get_user(self, attrs):
# 1 取出手机号,取出验证码
mobile = attrs.get('mobile')
code = attrs.get('code')
# 2 校验验证码
old_code = cache.get(f'cache_code_{mobile}')
assert old_code == code or (settings.DEBUG and code == '8888'), ValidationError('验证码错误')
user = User.objects.filter(mobile=mobile).first()
assert user, ValidationError('该手机号用户没注册')
return user
class LoginSerializer(CommonLoginSerializer,serializers.ModelSerializer):
# 这个序列化类用来: 1 校验
# 坑:必须重写username--》去除它的unique
username = serializers.CharField()
class Meta:
model = User
fields = ['username', 'password', 'icon']
extra_kwargs = {
'password': {'write_only': True} # 它不做序列化
}
def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
# 2 去数据库校验:正则--》
if re.match(r'^1[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
elif re.match('^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()
if user and user.check_password(password):
return user
else:
raise ValidationError('用户名或密码错误')
# def _get_token(self, user):
# refresh = RefreshToken.for_user(user)
# return str(refresh.access_token)
# def _pre_data(self, token, user):
# self.context['token'] = token
# self.context['username'] = user.username
# # self.instance=user # 当前用户,放到instance中了
# self.context['icon'] = settings.BACKEND_URL + "media/" + str(user.icon) # 不带 域名前缀的
# def validate(self, attrs):
# # 1 取出用户名(手机号,邮箱)和密码
# user = self._get_user(attrs)
# # 2 如果存在:签发token,返回
# token = self._get_token(user)
# # 3 把token,用户名和icon放入context
# self._pre_data(token, user)
# return attrs
.
。
短信注册
前端携带数据 -{mobile:,code:8888,password:123456}
视图类
1 # 注册接口 2 from .serializer import RegisterSerializer 3 class UserRegisterView(GenericViewSet): 4 serializer_class =RegisterSerializer 5 6 def create(self,request,*args,**kwargs): 7 # 逻辑写在序列化类中 8 serializer=self.get_serializer(data=request.data) 9 serializer.is_valid(raise_exception=True) 10 serializer.save() 11 return APIResponse(msg='注册成功')
序列化类
1 class RegisterSerializer(serializers.ModelSerializer): 2 # 校验和保存(create) 3 code = serializers.CharField() 4 class Meta: 5 model = User 6 fields = ['mobile', 'password', 'code'] 7 8 9 def validate(self, attrs): 10 # 1 取出code,验证code 11 code = attrs.pop('code') 12 mobile = attrs.get('mobile') 13 # 2 校验验证码 14 old_code = cache.get(f'cache_code_{mobile}') 15 assert old_code == code or (settings.DEBUG and code == '8888'), APIException('验证码错误') 16 # 2 取出手机号和密码--》创建用户-->有些字段必填 17 # 用户名必填 18 attrs['username'] = mobile 19 # 3 返回 20 return attrs 21 22 def create(self, validated_data): 23 # validated_data:code,mobile,password,username 24 user = User.objects.create_user(**validated_data) 25 return user
多方式登陆前端
1 <template> 2 <div class="login"> 3 <div class="box"> 4 <i class="el-icon-close" @click="close_login"></i> 5 <div class="content"> 6 <div class="nav"> 7 <span :class="{active: login_method === 'is_pwd'}" @click="change_login_method('is_pwd')">密码登录</span> 8 <span :class="{active: login_method === 'is_sms'}" @click="change_login_method('is_sms')">短信登录</span> 9 </div> 10 <el-form v-if="login_method === 'is_pwd'"> 11 <el-input 12 placeholder="用户名/手机号/邮箱" 13 prefix-icon="el-icon-user" 14 v-model="username" 15 clearable> 16 </el-input> 17 <el-input 18 placeholder="密码" 19 prefix-icon="el-icon-key" 20 v-model="password" 21 clearable 22 show-password> 23 </el-input> 24 <el-button type="primary" @click="handleLogin">登录</el-button> 25 </el-form> 26 <el-form v-if="login_method === 'is_sms'"> 27 <el-input 28 placeholder="手机号" 29 prefix-icon="el-icon-phone-outline" 30 v-model="mobile" 31 clearable 32 @blur="check_mobile"> 33 </el-input> 34 <el-input 35 placeholder="验证码" 36 prefix-icon="el-icon-chat-line-round" 37 v-model="sms" 38 clearable> 39 <template slot="append"> 40 <span class="sms" @click="send_sms">{{ sms_interval }}</span> 41 </template> 42 </el-input> 43 <el-button type="primary" @click="handleSMSLogin">登录</el-button> 44 </el-form> 45 <div class="foot"> 46 <span @click="go_register">立即注册</span> 47 </div> 48 </div> 49 </div> 50 </div> 51 </template> 52 53 54 <script> 55 import api from '../assets/js/settings' 56 57 export default { 58 name: "Login", 59 data() { 60 return { 61 username: '', 62 password: '', 63 mobile: '', 64 sms: '', 65 login_method: 'is_pwd', 66 sms_interval: '获取验证码', 67 is_send: false, 68 } 69 }, 70 methods: { 71 close_login() { 72 this.$emit('close') 73 }, 74 go_register() { 75 this.$emit('go') 76 }, 77 change_login_method(method) { 78 this.login_method = method; 79 }, 80 check_mobile() { 81 if (!this.mobile) return; 82 if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) { 83 this.$message({ 84 message: '手机号有误', 85 type: 'warning', 86 duration: 1000, 87 onClose: () => { 88 this.mobile = ''; 89 } 90 }); 91 return false; 92 } 93 // 校验手机号是否能发送验证码:只有后台存在才能发 94 this.$axios({ 95 url: api.check_mobile, 96 params: {'mobile': this.mobile} 97 }).then(res => { 98 if (res.data.code == 100) { 99 this.$message({type: "success", message: '手机号正确,可以正常发送'}) 100 this.is_send = true; // 发送验证码可以点击 101 } else { 102 // 手机号,后端没有,让它注册 103 this.$message({type: "error", message: '手机号尚未注册,请先注册'}) 104 this.mobile = '' 105 } 106 }) 107 108 109 }, 110 send_sms() { 111 if (!this.is_send) return; 112 this.is_send = false; // 发送短信按钮不能点击了 113 let sms_interval_time = 60; 114 this.sms_interval = "发送中..."; 115 let timer = setInterval(() => { 116 if (sms_interval_time <= 1) { 117 clearInterval(timer); 118 this.sms_interval = "获取验证码"; 119 this.is_send = true; // 重新回复点击发送功能的条件 120 } else { 121 sms_interval_time -= 1; 122 this.sms_interval = `${sms_interval_time}秒后再发`; 123 } 124 }, 1000); 125 // 发送短信 126 this.$axios({ 127 url: api.send_sms, 128 params: {mobile: this.mobile} 129 }).then(res => { 130 if (res.data.code == 100) { 131 this.$message('短信发送成功') 132 } 133 }) 134 135 136 }, 137 // 多方式登陆 138 handleLogin() { 139 // 1 用户名密码框不能为空 140 if (this.username && this.password) { 141 this.$axios({ 142 url: api.mul_login, 143 method: 'POST', 144 data: { 145 username: this.username, 146 password: this.password 147 }, 148 }).then(res => { 149 if (res.data.code == 100) { 150 // 登陆成功 151 // 1 把token,username,icon,存到cookie中 152 this.$cookies.set('token', res.data.token, '7d') 153 this.$cookies.set('username', res.data.username, '7d') 154 this.$cookies.set('icon', res.data.icon, '7d') 155 // 2 关闭当前模态框 156 this.$emit('close') 157 } else { 158 // 登陆失败 159 this.$message({ 160 type: 'error', 161 text: res.data.msg 162 }) 163 } 164 }).catch(err => { 165 166 }) 167 } else { 168 this.$message('用户名和密码必须') 169 } 170 }, 171 172 // 短信登陆 173 handleSMSLogin() { 174 if (this.mobile && this.sms) { 175 this.$axios({ 176 url: api.sms_login, 177 method: 'POST', 178 data: { 179 mobile: this.mobile, 180 code: this.sms 181 }, 182 }).then(res => { 183 if (res.data.code == 100) { 184 // 登陆成功 185 // 1 把token,username,icon,存到cookie中 186 this.$cookies.set('token', res.data.token, '7d') 187 this.$cookies.set('username', res.data.username, '7d') 188 this.$cookies.set('icon', res.data.icon, '7d') 189 // 2 关闭当前模态框 190 this.$emit('close') 191 } else { 192 // 登陆失败 193 this.$message({ 194 type: 'error', 195 text: res.data.msg 196 }) 197 } 198 }).catch(err => { 199 200 }) 201 } else { 202 this.$message('手机号和验证码不能为空') 203 } 204 } 205 } 206 } 207 </script> 208 209 <style scoped> 210 .login { 211 width: 100vw; 212 height: 100vh; 213 position: fixed; 214 top: 0; 215 left: 0; 216 z-index: 10; 217 background-color: rgba(0, 0, 0, 0.3); 218 } 219 220 .box { 221 width: 400px; 222 height: 420px; 223 background-color: white; 224 border-radius: 10px; 225 position: relative; 226 top: calc(50vh - 210px); 227 left: calc(50vw - 200px); 228 } 229 230 .el-icon-close { 231 position: absolute; 232 font-weight: bold; 233 font-size: 20px; 234 top: 10px; 235 right: 10px; 236 cursor: pointer; 237 } 238 239 .el-icon-close:hover { 240 color: darkred; 241 } 242 243 .content { 244 position: absolute; 245 top: 40px; 246 width: 280px; 247 left: 60px; 248 } 249 250 .nav { 251 font-size: 20px; 252 height: 38px; 253 border-bottom: 2px solid darkgrey; 254 } 255 256 .nav > span { 257 margin: 0 20px 0 35px; 258 color: darkgrey; 259 user-select: none; 260 cursor: pointer; 261 padding-bottom: 10px; 262 border-bottom: 2px solid darkgrey; 263 } 264 265 .nav > span.active { 266 color: black; 267 border-bottom: 3px solid black; 268 padding-bottom: 9px; 269 } 270 271 .el-input, .el-button { 272 margin-top: 40px; 273 } 274 275 .el-button { 276 width: 100%; 277 font-size: 18px; 278 } 279 280 .foot > span { 281 float: right; 282 margin-top: 20px; 283 color: orange; 284 cursor: pointer; 285 } 286 287 .sms { 288 color: orange; 289 cursor: pointer; 290 display: inline-block; 291 width: 70px; 292 text-align: center; 293 user-select: none; 294 } 295 </style>
。
1 <template> 2 <div class="header"> 3 <div class="slogan"> 4 <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p> 5 </div> 6 <div class="nav"> 7 <ul class="left-part"> 8 <li class="logo"> 9 <router-link to="/"> 10 <img src="../assets/img/head-logo.svg" alt=""> 11 </router-link> 12 </li> 13 <li class="ele"> 14 <span @click="goPage('/free')" :class="{active: url_path === '/free'}">免费课</span> 15 </li> 16 <li class="ele"> 17 <span @click="goPage('/actual')" :class="{active: url_path === '/actual'}">实战课</span> 18 </li> 19 <li class="ele"> 20 <span @click="goPage('/light')" :class="{active: url_path === '/light'}">轻课</span> 21 </li> 22 </ul> 23 24 <div class="right-part"> 25 <div v-if="username && username.length>0"> 26 <span>{{ username }}</span> 27 <span class="line">|</span> 28 <span @click="handleLogout">注销</span> 29 </div> 30 <div v-else> 31 <span @click="put_login">登录</span> 32 <span class="line">|</span> 33 <span @click="put_register">注册</span> 34 </div> 35 36 </div> 37 38 <LoginView v-if="is_login" @close="close_login" @go="put_register"></LoginView> 39 <RegisterView v-if="is_register" @close="close_register" @go="put_login"></RegisterView> 40 41 </div> 42 </div> 43 </template> 44 <script> 45 import LoginView from "@/views/LoginView.vue"; 46 import RegisterView from "@/views/RegisterView.vue"; 47 48 export default { 49 name: "Header", 50 data() { 51 return { 52 url_path: sessionStorage.url_path || '/', 53 is_login: false, 54 is_register: false, 55 username: '' 56 } 57 }, 58 methods: { 59 handleLogout() { 60 this.username = '' 61 this.$cookies.remove('username') 62 this.$cookies.remove('token') 63 this.$cookies.remove('icon') 64 }, 65 put_login() { 66 this.is_login = true; 67 this.is_register = false; 68 }, 69 put_register() { 70 this.is_login = false; 71 this.is_register = true; 72 }, 73 close_login() { 74 // 子组件登陆成功或点击X,触发我 75 this.is_login = false; 76 // 如果是登陆成功,取出用户名 77 this.username = this.$cookies.get('username') 78 79 }, 80 close_register() { 81 this.is_register = false; 82 }, 83 84 85 goPage(url_path) { 86 // 已经是当前路由就没有必要重新跳转 87 if (this.url_path !== url_path) { 88 this.$router.push(url_path); 89 } 90 sessionStorage.url_path = url_path; 91 }, 92 }, 93 created() { 94 // this.$route.path 当前路径 / /about /course 95 sessionStorage.url_path = this.$route.path; 96 this.url_path = this.$route.path; 97 98 //当页面刷新,从cookie中取出用户名 99 this.username = this.$cookies.get('username') 100 }, 101 components: { 102 LoginView, RegisterView 103 } 104 } 105 </script> 106 <style scoped> 107 .header { 108 background-color: white; 109 box-shadow: 0 0 5px 0 #aaa; 110 } 111 112 .header:after { 113 content: ""; 114 display: block; 115 clear: both; 116 } 117 118 .slogan { 119 background-color: #eee; 120 height: 40px; 121 } 122 123 .slogan p { 124 width: 1200px; 125 margin: 0 auto; 126 color: #aaa; 127 font-size: 13px; 128 line-height: 40px; 129 } 130 131 .nav { 132 background-color: white; 133 user-select: none; 134 width: 1200px; 135 margin: 0 auto; 136 137 } 138 139 .nav ul { 140 padding: 15px 0; 141 float: left; 142 } 143 144 .nav ul:after { 145 clear: both; 146 content: ''; 147 display: block; 148 } 149 150 .nav ul li { 151 float: left; 152 } 153 154 .logo { 155 margin-right: 20px; 156 } 157 158 .ele { 159 margin: 0 20px; 160 } 161 162 .ele span { 163 display: block; 164 font: 15px/36px '微软雅黑'; 165 border-bottom: 2px solid transparent; 166 cursor: pointer; 167 } 168 169 .ele span:hover { 170 border-bottom-color: orange; 171 } 172 173 .ele span.active { 174 color: orange; 175 border-bottom-color: orange; 176 } 177 178 .right-part { 179 float: right; 180 } 181 182 .right-part .line { 183 margin: 0 10px; 184 } 185 186 .right-part span { 187 line-height: 68px; 188 cursor: pointer; 189 } 190 </style>
。
1 <template> 2 <div class="register"> 3 <div class="box"> 4 <i class="el-icon-close" @click="close_register"></i> 5 <div class="content"> 6 <div class="nav"> 7 <span class="active">新用户注册</span> 8 </div> 9 <el-form> 10 <el-input 11 placeholder="手机号" 12 prefix-icon="el-icon-phone-outline" 13 v-model="mobile" 14 clearable 15 @blur="check_mobile"> 16 </el-input> 17 <el-input 18 placeholder="密码" 19 prefix-icon="el-icon-key" 20 v-model="password" 21 clearable 22 show-password> 23 </el-input> 24 <el-input 25 placeholder="验证码" 26 prefix-icon="el-icon-chat-line-round" 27 v-model="sms" 28 clearable> 29 <template slot="append"> 30 <span class="sms" @click="send_sms">{{ sms_interval }}</span> 31 </template> 32 </el-input> 33 <el-button type="primary" @click="handleRegister">注册</el-button> 34 </el-form> 35 <div class="foot"> 36 <span @click="go_login">立即登录</span> 37 </div> 38 </div> 39 </div> 40 </div> 41 </template> 42 43 <script> 44 import api from "@/assets/js/settings"; 45 46 export default { 47 name: "Register", 48 data() { 49 return { 50 mobile: '', 51 password: '', 52 sms: '', 53 sms_interval: '获取验证码', 54 is_send: false, 55 } 56 }, 57 methods: { 58 close_register() { 59 this.$emit('close', false) 60 }, 61 go_login() { 62 this.$emit('go') 63 }, 64 check_mobile() { 65 if (!this.mobile) return; 66 if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) { 67 this.$message({ 68 message: '手机号有误', 69 type: 'warning', 70 duration: 1000, 71 onClose: () => { 72 this.mobile = ''; 73 } 74 }); 75 return false; 76 } 77 78 //跟后端交互,判断手机号是否注册过 79 this.$axios({ 80 url: api.check_mobile, 81 params: {'mobile': this.mobile} 82 }).then(res => { 83 if (res.data.code == 100) { 84 this.$message({type: "error", message: '您已经注册过,请直接登陆'}) 85 this.mobile = '' 86 } else { 87 // 手机号,后端没有注册 88 this.$message({type: "success", message: '手机号尚未注册'}) 89 this.is_send = true; // 发送验证码可以点击 90 91 } 92 }) 93 }, 94 send_sms() { 95 if (!this.is_send) return; 96 this.is_send = false; 97 let sms_interval_time = 60; 98 this.sms_interval = "发送中..."; 99 let timer = setInterval(() => { 100 if (sms_interval_time <= 1) { 101 clearInterval(timer); 102 this.sms_interval = "获取验证码"; 103 this.is_send = true; // 重新回复点击发送功能的条件 104 } else { 105 sms_interval_time -= 1; 106 this.sms_interval = `${sms_interval_time}秒后再发`; 107 } 108 }, 1000); 109 // 发送短信 110 this.$axios({ 111 url: api.send_sms, 112 params: {mobile: this.mobile} 113 }).then(res => { 114 if (res.data.code == 100) { 115 this.$message('短信发送成功') 116 } 117 }) 118 }, 119 handleRegister() { 120 if (this.mobile && this.sms && this.password) { 121 this.$axios({ 122 url: api.register, 123 method: 'POST', 124 data: { 125 mobile: this.mobile, 126 code: this.sms, 127 password: this.password 128 } 129 }).then(res => { 130 if (res.data.code == 100) { 131 this.$message({ 132 type: "success", message: '注册成功,请去登陆', onClose: () => { 133 this.$emit('go') 134 } 135 }) 136 } 137 138 }) 139 } 140 } 141 } 142 } 143 </script> 144 145 <style scoped> 146 .register { 147 width: 100vw; 148 height: 100vh; 149 position: fixed; 150 top: 0; 151 left: 0; 152 z-index: 10; 153 background-color: rgba(0, 0, 0, 0.3); 154 } 155 156 .box { 157 width: 400px; 158 height: 480px; 159 background-color: white; 160 border-radius: 10px; 161 position: relative; 162 top: calc(50vh - 240px); 163 left: calc(50vw - 200px); 164 } 165 166 .el-icon-close { 167 position: absolute; 168 font-weight: bold; 169 font-size: 20px; 170 top: 10px; 171 right: 10px; 172 cursor: pointer; 173 } 174 175 .el-icon-close:hover { 176 color: darkred; 177 } 178 179 .content { 180 position: absolute; 181 top: 40px; 182 width: 280px; 183 left: 60px; 184 } 185 186 .nav { 187 font-size: 20px; 188 height: 38px; 189 border-bottom: 2px solid darkgrey; 190 } 191 192 .nav > span { 193 margin-left: 90px; 194 color: darkgrey; 195 user-select: none; 196 cursor: pointer; 197 padding-bottom: 10px; 198 border-bottom: 2px solid darkgrey; 199 } 200 201 .nav > span.active { 202 color: black; 203 border-bottom: 3px solid black; 204 padding-bottom: 9px; 205 } 206 207 .el-input, .el-button { 208 margin-top: 40px; 209 } 210 211 .el-button { 212 width: 100%; 213 font-size: 18px; 214 } 215 216 .foot > span { 217 float: right; 218 margin-top: 20px; 219 color: orange; 220 cursor: pointer; 221 } 222 223 .sms { 224 color: orange; 225 cursor: pointer; 226 display: inline-block; 227 width: 70px; 228 text-align: center; 229 user-select: none; 230 } 231 </style>