Django一页通

官网:https://www.djangoproject.com/,中文官方教程:https://docs.djangoproject.com/zh-hans/3.1/

安装: pip install django 

查看版本: django-admin --version  或: 1 import django 2 django.get_version() 

创建工程: django-admin startproject 工程名 

启动服务: python manage.py runserver 

服务启动成功后浏览器访问:http://127.0.0.1:8000/

启动服务指定端口: python manage.py runserver 8001 

启动服务指定IP:端口: python manage.py runserver 127.0.0.1:8002 (不能只指定IP不指定端口)

让所有人都可访问: python manage.py runserver 0.0.0.0:8000 同时需要设置settings.py中的代码 ALLOWED_HOSTS = ['*'] 

创建应用: python manage.py startapp myapp 或 django-admin startapp appname 

访问后台管理:http://localhost:8000/admin

生成迁移: python manage.py makemigrations ,指定应用生成迁移 python manage.py makemigrations myapp 

执行迁移: python manage.py migrate 


第一个演示Demo:

myapp/views.py

from django.http import HttpResponse

def hello(request):
    return HttpResponse('Hello')

也能识别HTML代码: return HttpResponse('<h1>Hello</h1>') 

mypro/urls.py

from django.contrib import admin
from django.urls import path

from myapp import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('hello/',views.hello)
]

启动服务后浏览器访问:http://localhost:8000/hello/


 优化上面的Demo:

 myapp/views.py不变。

新增:myapp/urls.py:

from django.urls import path
from myapp import views

urlpatterns = [
    path('hello/',views.hello)
]

修改:mypro/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('myapp/',include('myapp.urls'))
]

根路由已在mypro/settings.py中默认配置好: ROOT_URLCONF = 'mypro.urls' 

浏览器访问:http://localhost:8000/myapp/hello/ 


 创建模型表名自动为应用名_模型名,数据库中生成的表名为 myapp_person

from django.db import models

class Person(models.Model):
    name=models.CharField(max_length=32) # CharField必须指定max_length,否则生成迁移文件时就会报错
    age=models.IntegerField(default=18) # IntegerField必须指定default,否则,新增模型正常,添加数据时报错;修改模型迁移时会阻塞并提示两种指定default值的方式

创建模型:指定表名,数据库中生成的表名为person

查询排序,方式一

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=32)
    age=models.IntegerField(default=18)

    class Meta:
        db_table = 'person' # 指定表名
        ordering='age','-name' # 指定查询时的排序规则,默认使用id排序,倒序在字段前加-,用元组或列表指定1到多个排序字段

生成迁移文件

(venv) D:桌面mypro>python manage.py makemigrations
No changes detected

原因:未注册应用,需要在mypro/mypro/settings.py中注册 INSTALLED_APPS = ['myapp'] 或 INSTALLED_APPS = ['myapp.apps.MyappConfig'] 

再次生成迁移文件:

(venv) D:桌面mypro>python manage.py makemigrations
Migrations for 'myapp':
  myappmigrations001_initial.py
    - Create model Person

执行迁移:完成后会在数据库迁移记录表中产品相应的迁移记录,并创建对应模型的表。

(venv) D:桌面mypro>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, myapp, sessions
Running migrations:
  Applying myapp.0001_initial... OK

 若对模型有修改,可再次执行生成迁移文件执行迁移。

若删除迁移文件,则一定要同时删除表中迁移记录,及删除对应模型的表。

migrations包勿删!


使用模板,在myapp目录中建立模板文件夹templates,注:不是template

myapp/views.py

渲染方式一

from django.shortcuts import render

def hello(request):
    return render(request,'hello.html')

渲染方式二

from django.http import HttpResponse
from django.template import loader

def hello(request):
    template=loader.get_template('hello.html')
    return HttpResponse(template.render())

myapp/templates/hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>标题一</h1>
</body>
</html>

使用模板,在工程目录mypro中建立template或templates文件夹,并在settings.py中注册。(推荐方式)

视图函数myapp/views.py同上不用改变。

mypro/template/hello.html或mypro/templates/hello.html文件内容同上。

mypro/mypro/settings.py中注册:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,'templates')], # 注册代码
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

数据库的增删改查

,方式一

mypro/myapp/views.py

from django.http import HttpResponse
from myapp.models import Person

def add(request):
    person=Person()
    person.name='李四'
    person.save()
    return HttpResponse(f'新增成功,{person.name}')

,方式二

def addperson(request):
    Person.objects.create(name='李四',age=22) # 关键代码
    return HttpResponse('添加成功')

mypro/myapp/views.py

from django.http import HttpResponse
from myapp.models import Person

def delete(request):
    person=Person.objects.first()
    person.delete()
    return HttpResponse(f'删除成功,{person.name}')

 改

mypro/myapp/views.py

from django.http import HttpResponse
from myapp.models import Person

def update(request):
    person=Person.objects.first()
    person.name='王五'
    person.save()
    return HttpResponse(f'更新成功,{person.name}')

mypro/myapp/views.py

from django.shortcuts import render
from myapp.models import Person

def query(request):
    persons=Person.objects.all()
    context={
        'persons':persons
    }
    return render(request,'myapp/query.html',context=context)

mypro/templates/myapp/query.html,用到了模板语法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    {% for person in persons %}
        <li>{{ person.name }}</li>
    {% endfor %}
</ul>
</body>
</html>

查询过滤:filter和exclude可任意多个级联

过滤条件:__gt=、__gte=、__lt=、__lte=、__year=,其他month、day、week_day、hour、minute、second

def getperson(request):
    persons=Person.objects.filter(age__gt=20).exclude(age__gt=30)
    context={
        'persons':persons
    }
    return render(request,'hello.html',context=context)

= 同 __exact= 大小写敏感 persons=Person.objects.filter(name='Tom') 同 persons=Person.objects.filter(name__exact='Tom') 

__iexact 忽略大小写  persons=Person.objects.filter(name__iexact='Tom')  i为ignore在sqlite3中以下3个有无i都一样:

__contains同 __icontains忽略大小写 persons=Person.objects.filter(name__contains='t') 同 persons=Person.objects.filter(name__icontains='Tom') 

__startswith同 __istartswith忽略大小写 persons=Person.objects.filter(name__startswith='t')同 persons=Person.objects.filter(name__istartswith='t') 

__endswith同__iendswith忽略大小写persons=Person.objects.filter(name__endswith='m') 同 persons=Person.objects.filter(name__iendswith='m') 

__isnull  persons=Person.objects.filter(name__isnull=False)  __isnull=True未试验出效果。无__isnotnull和__notisnull

__in大小写敏感 persons=Person.objects.filter(name__in=['无名氏','Tom']) 

__range persons=Person.objects.filter(age__range=(30,50)) 

查询排序,方式二

def getperson(request):
    persons=Person.objects.order_by('name')
    context={
        'persons':persons
    }
    return render(request,'hello.html',context=context)

默认管理页面为英文,改为中文:mypro/mypro/settings.py LANGUAGE_CODE = 'zh-hans' ,再访问:http://127.0.0.1:8000/admin就显示中文。

默认时区为  TIME_ZONE = 'UTC' 改为 TIME_ZONE = 'Asia/Shanghai' 


 自定义创建对象的类方法

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=32)
    age=models.IntegerField(default=18)

    @classmethod
    def create(cls,name='无名氏',age=28):
        person=cls()
        person.name=name
        person.age=age
        person.save()

    class Meta:
        db_table = 'person'
        ordering='age','-name'

创建对象

,方式三

def addperson(request):
    Person.create()
    Person.create(name='小猪')
    Person.create(age=29)
    Person.create(name='小狗',age=21)
    return HttpResponse('添加成功')

 将查询结果集中的对象转化为字典,values()

def getperson(request):
    persons=Person.objects.order_by('name')
    print('-----------------------')
    print(type(persons))
    print('-----------------------')
    print(persons)
    print('-----------------------')
    print(persons.values())
    print('-----------------------')
    print(list(persons.values())) # 关键代码
    print('-----------------------')
    context={
        'persons':persons
    }
    return render(request,'hello.html',context=context)

查询单个数据

get()

def getperson(request):
    # 多个会报MultipleObjectsReturned
    # 未查询到数据会报DoesNotExist
    person=Person.objects.get(name='李四')
    print('-----------------------')
    print(type(person)) # <class 'myapp.models.Person'>
    print('-----------------------')
    print(person) # Person object (301)
    print('-----------------------')
    return HttpResponse(f'查询成功,姓名={person.name}')

 first()

def getperson(request):
    person=Person.objects.first() # first()无参数
    print('-----------------------')
    print(type(person)) # <class 'myapp.models.Person'>
    print('-----------------------')
    print(person) # Person object (239)
    print('-----------------------')
    return HttpResponse(f'查询成功,姓名={person.name}')

 last()

def getperson(request):
    person=Person.objects.last()
    return HttpResponse(f'查询成功,姓名={person.name}')

查询计数,统计查询结果集,count()

def getperson(request):
    persons=Person.objects.all()
    return HttpResponse(f'查询成功,{persons.count()}个人。')

判断查询结果集是否有数据,exists()

def getperson(request):
    persons=Person.objects.filter(name='李四')
    if persons.exists():
        return HttpResponse('查询成功')
    return HttpResponse('查询失败')

通过主键查询,pk=xxx,id=xxx

 persons=Person.objects.filter(pk=308) 或 persons=Person.objects.filter(id=306) 


 约束:唯一约束,unique=True,默认False

class User(models.Model):
    name=models.CharField(max_length=16,unique=True)

添加重复记录会报IntegrityError完整性错误,UNIQUE constraint failed: myapp_user.name唯一约束失败


限制查询结果集,类似字符串或列表的切片,但下标不能为负

def getperson(request):
    persons=Person.objects.all()[2:4] # 跳过第0、1个,取第2、3个,左闭右开
    context={
        'persons':persons
    }
    return render(request,'hello.html',context=context)

 分页查看,get请求传参数,方式一

def getperson(request):
    page=int(request.GET.get('page')) # get请求传参,关键代码
    per_page=5
    persons=Person.objects.all()[(page-1)*per_page:page*per_page] # 分页,关键代码
    context={
        'persons':persons
    }
    return render(request,'hello.html',context=context)

浏览器访问:http://127.0.0.1:8000/myapp/getperson/?page=2


分页器

views.py

def getinfo(request):
    p=int(request.GET.get('p',1))
    notices=Notice.objects.all()
    paginator=Paginator(notices,10) # 每页10条数据
    print(f'总记录数={paginator.count}')
    print(f'总页数={paginator.num_pages}')
    print(f'页码列表={paginator.page_range}')
    page=paginator.page(p)
    print(f'当前页数据对象={page.object_list}')
    print(f'当前页码值={page.number}')
    print(f'当前页的Paginator对象={page.paginator}')
    print(f'是否有下一页:{page.has_next()}')
    print(f'是否有上一页:{page.has_previous()}')
    print(f'是否有上一页或下一页:{page.has_other_pages()}')
    print(f'下一页页码:{page.next_page_number()}')
    print(f'上一页页码:{page.previous_page_number()}')
    print(f'当前页数据数:{len(page)}')
    return render(request,'info.html',locals())

urls.py  path('getinfo/',views.getinfo), 

models.py

class Notice(models.Model):
    info=models.CharField(max_length=16)
models.py

info.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>分页信息</title>
</head>
<body>
    <ul>
        {% for notice in page %}
        <li>{{ notice.info }}</li>
        {% endfor %}
    </ul>
</body>
</html>

get请求传参数,方式二

 urls.py  path('hi/<int:num>/<str:name>/',views.hi) 

views.py

def hi(request,num,name):
    print(type(num)) # <class 'int'>
    print(num) # 666
    print(type(name)) # <class 'str'>
    print(name) # good
    return HttpResponse('hi')

浏览器访问:http://127.0.0.1:8000/myapp/hi/666/good/


 数据级联

一对多

models.py

class User(models.Model):
    name=models.CharField(max_length=16,unique=True)

class Good(models.Model):
    name=models.CharField(max_length=16)
    user=models.ForeignKey(User,on_delete=models.CASCADE) # 一定要设置on_delete,否则生成迁移文件时会报错

views.py

def adduser(request):
    user=User()
    user.name='黄牛'
    user.save()
    return HttpResponse(f'添加User成功,{user.name}')

def addgood(request):
    user=User.objects.last()
    good=Good()
    good.name='泉水'
    good.user=user
    good.save()
    return HttpResponse(f'添加货物成功:{good.name},{good.user.name}')

def getgood(request):
    user=User.objects.last()
    # goods=Good.objects.filter(user=user)
    goods=user.good_set.all()
    return render(request,'goods.html',context=locals())

goods.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>货物清单</title>
</head>
<body>
    <ul>
        {% for good in goods %}
            <li>{{ good.name }} - {{ good.user.name }}</li>
        {% endfor %}
    </ul>
</body>
</html>

 跨关系查询,数据去重

models.py

class Author(models.Model):
    name=models.CharField(max_length=16)

class Article(models.Model):
    content=models.TextField() # 可不带参数
    author=models.ForeignKey(Author,on_delete=models.CASCADE)

views.py

def getauthor(request):
    # 不能写成Article__content__contains,即模型名在此必须全小写
    # 规则:模型类名__属性名__比较运算符=
    authors=Author.objects.filter(article__content__contains='黄山') # 关键代码,跨关系查询
    # 不支持distinct操作,下面这行会报错:
    # authors=Author.objects.filter(article__content__contains='黄山').distinct('name')
    author_list=[]
    for author in authors: # 关键代码,数据去重
        if author not in author_list:
            author_list.append(author)
    return render(request,'authors.html',context=locals())

authors.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>作者清单</title>
</head>
<body>
    <ul>
        {% for author in author_list %}
            <li>{{ author.name }}</li>
        {% endfor %}
    </ul>
</body>
</html>
authors.html

 聚合、自相关查询、F/Q对象、显隐属性

models.py

class Student(models.Model):
    # 主键未指定会自动产生id,为隐式属性;若指定,则不会自动产生,为显式属性
    sno=models.AutoField(primary_key=True) # 关键代码
    name=models.CharField(max_length=16)
    python=models.IntegerField(default=0)
    linux=models.IntegerField(default=0)

views.py

def getstudent(request):
   # 聚合
# result=Student.objects.aggregate(Avg('python')) # 平均值={'python__avg': 84.2} # result=Student.objects.aggregate(Count('python')) # 计数={'python__count': 5} # result=Student.objects.aggregate(Max('python')) # 最大值={'python__max': 99} # result=Student.objects.aggregate(Min('python')) # 最小值={'python__min': 70} # result=Student.objects.aggregate(Sum('python')) # 最小值={'python__sum': 418} # return HttpResponse(f'查询成功:{result}') # F对象的应用:行(记录)自相关查询 # students=Student.objects.filter(python__gt=F('linux')) # students=Student.objects.filter(python__gt=F('linux')+5) # Q对象的应用:组合条件查询,与&、或|、非~ # students=Student.objects.filter(Q(python__gt=80) & Q(linux__gt=80)) # students=Student.objects.filter(Q(python__gt=80) | Q(linux__gt=80)) students=Student.objects.filter(~Q(Q(python__gt=80) | Q(linux__gt=80))) return render(request,'students.html',locals())

students.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>学生成绩清单</title>
</head>
<body>
    <ul>
        {% for student in students %}
            <li>学号={{ student.sno }},姓名={{ student.name }},Python={{ student.python }},Linux={{ student.linux }}</li>
        {% endfor %}
    </ul>
</body>
</html>
students.html

 自定义模型管理器

models.py

class TeacherManager(models.Manager):
    pass

class Teacher(models.Model):
    name=models.CharField(max_length=16)
    # teacherManager=models.Manager() # 自定义模型管理器,方式一
    teacherManager=TeacherManager() # 自定义模型管理器,方式二

views.py

def getteacher(request):
    # 模型管理器objects为隐性属性,未指定时会自动生成。
    # teacher=Teacher.objects.first()
    # 若自定义模型管理器teacherManager,则objects就不存在了
    teacher=Teacher.teacherManager.first()
    return HttpResponse(f'获取老师成功:{teacher.name}')

 逻辑删除

models.py

class TeacherManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_delete=False)

class Teacher(models.Model):
    name=models.CharField(max_length=16)
    is_delete=models.BooleanField(default=False) # 逻辑删除字段

    teacherManager=TeacherManager() # 实际逻辑删除后的查询集,方式二

views.py

def getteachers(request):
    # 实际逻辑删除后的查询集,方式一
    # teachers=Teacher.objects.filter(is_delete=False)

    # 实际逻辑删除后的查询集,方式二
    teachers=Teacher.teacherManager.all()

    return render(request,'teachers.html',locals())

 批量添加数据,此法不适合多表级联

models.py

class Teacher(models.Model):
    name=models.CharField(max_length=16)

views.py

def addteacher(request):
    teachers=[]
    for i in range(5):
        teacher=Teacher()
        teacher.name='李四{}'.format(i)
        # teacher.save() # 批量添加时不建议用save(),因为save()会频繁的建立/关闭数据库连接
        teachers.append(teacher)
    Teacher.objects.bulk_create(teachers) # 批量添加时的建议方法
    return HttpResponse(f'添加老师成功:{teachers}')

  一对一,1:1,多表数据级联操作

 models.py

class IDCard(models.Model):
    idno=models.CharField(max_length=32,unique=True)

class Phone(models.Model):
    phoneno=models.CharField(max_length=16)

    # CASCADE:删除IDCard会自动删除Phone,删除Phone不会删除IDCard
    # idcard=models.OneToOneField(IDCard,on_delete=models.CASCADE) # 使用外键+唯一约束实现一对一关系

    # PROTECT:删IDCard前必须先删Phone
    idcard=models.OneToOneField(IDCard,on_delete=models.PROTECT)

    # SET_NULL:前提是此字段可为空,否则无法设置。
    # idcard=models.OneToOneField(IDCard,null=True,on_delete=models.SET_NULL)

    # SET_DEFAULT:前提是此字段必须设置默认值,且默认值的数据类型要正确,且未被其他对象关联。
    # idcard=models.OneToOneField(IDCard,default=4,on_delete=models.SET_DEFAULT)

    # 前提是设置的值(如666)要存在,且未被其他对象关联。
    # idcard=models.OneToOneField(IDCard,default=4,on_delete=models.SET(666))

views.py

def addidcard(request):
    idcard=IDCard()
    idcard.idno='x002'
    idcard.save()
    return HttpResponse(f'添加身份证成功:{idcard.idno}')

def addphone(request):
    idcard=IDCard.objects.last()
    phone=Phone()
    phone.phoneno='132'
    phone.idcard=idcard
    phone.save()
    return HttpResponse(f'添加手机号成功:{phone.phoneno}')

def delidcard(request):
    idcard=IDCard.objects.first()
    idcard.delete()
    return HttpResponse(f'删除身份证成功:{idcard.idno}')

def delphone(request):
    phone=Phone.objects.first()
    phone.delete()
    return HttpResponse(f'删除手机号成功:{phone.phoneno}')

 主从表数据获取

views.py

def get_idcard_by_phone(request):
    phone=Phone.objects.first()
    # idcard为Phone的显性属性
    idcard=phone.idcard
    return HttpResponse(f'通过手机号:{phone.phoneno},获取身份证:{idcard.idno}成功。')

def get_phone_by_idcard(request):
    idcard=IDCard.objects.last()
    # 主表获取从表通过隐性属性,即从表名小写
    # phone为IDCard的隐性属性
    phone=idcard.phone
    return HttpResponse(f'通过身份证:{idcard.idno},获取手机号:{phone.phoneno}成功。')

一对多,1:N

models.py

class Province(models.Model):
    name=models.CharField(max_length=16)

class City(models.Model):
    name=models.CharField(max_length=16)
    province=models.ForeignKey(Province,on_delete=models.PROTECT) # 一对多通过外键实现

views.py

def get_province_by_city(request):
    city=City.objects.first()
    province=city.province # 显性属性
    return HttpResponse(f'通过城市:{city.name},获取省:{province.name}成功。')

def get_citys_by_province(request):
    province=Province.objects.last()
    cities=province.city_set.all() # city_set为Province的隐性属性
    return render(request,'cities.html',locals())

添加数据:

def add_city(request):
    province=Province.objects.last()
    city=City()
    city.name='常州'
    # city.province=province # 方式一
    city.save()
    province.city_set.add(city) # 方式二,前提是city的外键省字段province可为空,即province=models.ForeignKey(Province,null=True,on_delete=models.PROTECT)
 return HttpResponse(f'给省:{province.name}添加城市:{city.name},成功!')

一对多数据删除同一对一


 多对多,M:N,由于会创建额外的关系表,所以哪个表申明关系不像一对一和一对多那样很介意了。

models.py

class Reader(models.Model):
    name=models.CharField(max_length=16) # 更重要的设为主表

class News(models.Model):
    content=models.TextField()
   # 次重要的申明关系,设为从表 reader
=models.ManyToManyField(Reader) # 会自动创建额外的关系表维护多对多关系

views.py

def add_reader(request):
    reader=Reader()
    reader.name='王五'
    reader.save()
    return HttpResponse(f'添加读者成功,{reader.name}')

def add_news(request):
    news=News()
    news.content='劳动法将严格执行'
    news.save()
    return HttpResponse(f'添加新闻成功,{news.content}')

def add_reader_news(request):
    reader=Reader.objects.last()
    news=News.objects.last()

    # news.reader.add(reader) # 多对多关系绑定,方式一,从表的显性属性绑定
    reader.news_set.add(news) # 多对多关系绽定,方式二,主表的隐性属性绑定。是否有remove、clear等方法?

    return HttpResponse(f'添加读者-新闻关系成功,{reader.name},{news.content}')

 模型继承

models.py

class Animal(models.Model):
    name=models.CharField(max_length=16)
    
    # 这种写法更好,只创建Dog一张表,单表的性能更好。否则会创建Animal和Dog两张表,多表级联会降低性能。
    class Meta:
        abstract=True

class Dog(Animal):
    color=models.CharField(max_length=16)

T 模板语法

countrys.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>幸福国信息</title>
</head>
<body>
<h6>{{ countrys.0.name }}</h6> {# 索引属性 #}
<h6>{{ countrys.1.develop }}</h6> {# 方法,不能传参 #}
<h6>{{ provinces.安徽 }}</h6> {# 字典的键 #}
{# 单行注释 #}
{% comment %}
    多行注释
{% endcomment %}
<!--
    不建议用HTML的注释,能在浏览器查看源码中显示。
-->
<ul>
    {% for country in countrys %}
    {% if forloop.first %}
    <li style="color:#ff0000">{{forloop.counter0}} {{ country.name }}</li>
    {% elif forloop.last %}
    <li style="color:#0f0">{{forloop.counter}} {{ country.name }}</li>
    {% else %}
    <li>{{ forloop.revcounter0 }} {{ country.name }} {{ forloop.revcounter }}</li>
    {% endif %}

    {% empty %}
    <h2>for循环为空</h2>
    {% endfor %}
</ul>
</body>
</html>

models.py

class Country(models.Model):
    name=models.CharField(max_length=16)
    def develop(self):
        return '杀贪官、斩奸商!人民幸福!人人爱国敬业诚信友善!'
models.py

views.py

def getcountrys(request):
    countrys=Country.objects.all()
    provinces={
        '安徽':'合肥',
        '江苏':'南京',
    }
    return render(request,'countrys.html',locals())
views.py

 T 模板语法 继续

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模板语法</title>
    <script type="text/javascript">
        alert('网站崩溃了!')
    </script>
</head>
<body>
{% widthratio 5 1 2 %} {# 数 分母 分子 #}
<hr>
<ul>
    {% for name in names %}
    {% if forloop.counter|divisibleby:2 %} {# 整除 #}
    <li style="color:red">{{name}}</li>
    {% else %}
    <li style="color:blue">{{name}}</li>
    {% endif %}
    {% endfor %}
</ul>
<hr>
    {% for name in names %}
        {% ifequal name 'Tom' %}
            <li>{{name}}</li>
        {% endifequal %}

        {% ifnotequal name "Tom" %}
            <li>{{name}}</li>
        {% endifnotequal %}
    {% endfor %}
<hr>
{{5|add:10}}
{{10|add:-5}}
{{'Tom'|lower}}
{{"Tom"|upper}}
<hr>
{{names|join:'-'}}
<hr>
{{var|default:'中国'}}
<hr>
{{dateVal|date:'y-m-d'}}
<hr>
{{code|safe}}
<hr>
{% autoescape off %}
    {{code}}
{% endautoescape %}
<hr>
{% autoescape on %}
    {{code}}
{% endautoescape %}
</body>
</html>

views.py

def index(request):
    names=['Tom','Jerry']
    dateVal=datetime.now()

    # code='<h1>好好学习</h1>'

    # code='''
    #     <script type="text/javascript">
    #     alert('网站崩溃了!')
    #     </script>
    # '''

    code='''
        <script type="text/javascript">
            window.onload=function(){
                var lis=document.getElementsByTagName("li");
                for(var i=0;i<lis.length;i++){
                    console.log(i);
                    lis[i].innerHTML="你的网站崩溃了!";
                }
            }
        </script>
    '''
    return render(request,'index.html',locals())

 结构标签,block+extends,化整为零,推荐使用

base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{Title}}</title>

    {% block extJS %}
    {% endblock %}

    {% block extCSS%}
    {% endblock %}
</head>
<body>

{% block header %}
{# 首次出现为定义 #}
{% endblock %}

{% block banner %}
{% endblock %}

{% block content %}
{% endblock %}

{% block footer %}
{% endblock %}

</body>
</html>

loginBase.html

{% extends 'base.html' %}

{% block header %}
<h1>再次出现为重写</h1>
{% endblock %}

{% block logininfo %} {# 继承之后再次挖坑,后面的继承此坑不生效 #}
{% endblock %}

login.html

{% extends 'loginBase.html' %}

{% block header %}
{{ block.super }} {# 调用父类后面就为追加 #}
<h2>三次及以上出现为覆盖之前的重写</h2>
{% endblock %}

{% block logininfo %} {# 未生效 #}
<h3>这是登录信息</h3>
{% endblock %}

{% block banner %}
<h4>这是轮播</h4>
{% endblock %}

{% block content %}
<h5>这是内容</h5>
{% endblock %}

{% block footer %}
<h6>这是底部信息</h6>
{% endblock %}

views.py

def login(request):
    title='结构标签'
    return render(request,'login.html',locals())

结构标签,include,聚零为整,不推荐使用,性能差

 first.html

<h1>这是first.html的内容</h1>

second.html

<h2>这是second.html的内容</h2>

all.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
</head>
<body>
    {% include 'first.html' %}
    {% include 'second.html' %}
</body>
</html>

views.py

def all(request):
    title='结构标签include'
    return render(request,'all.html',locals())

响应json数据

views.py

def getJson(request):
    data={
        'status':0,
        'msg':'ok'
    }
    return JsonResponse(data)

urls.py  path('getJson',views.getJson), 


 页面跳转

hi_info.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>页面跳转</title>
</head>
<body>
<a href="/myapp/hi">跳转到hi页</a>
</body>
</html>

views.py

def hi_info(request):
    return render(request,'hi_info.html')

def hi(request):
    return HttpResponse('hi')

urls.py  path('hi_info',views.hi_info), 、 path('hi',views.hi) 和 path('hi/',views.hi) 有区别,注:路径后是否以/结尾有区别


 自定义404页面未找到,Page not found (404)

直接在templates目录中创建404.html

404.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>页面未找到</title>
</head>
<body>
页面未找到
</body>
</html>

设置settings.py  DEBUG = False 和 ALLOWED_HOSTS = ['*'] ,ALLOWED_HOSTS不为空就好。


HttpRequest对象

views.py

def hi(request,num,name):
    print('----------------')
    print(request) # <WSGIRequest: GET '/myapp/hi/666/good/?key=%27%E4%B8%AD%E5%9B%BD%27'>
    print(request.path) # /myapp/hi/666/good/
    print(request.method) # GET
    print(request.encoding) # None
    print(request.GET) # <QueryDict: {'key': ["'中国'"]}>
    print(request.POST) # <QueryDict: {}>
    print(request.FILES) # <MultiValueDict: {}>
    print(request.COOKIES) # {'csrftoken': 'QybCRghQbKBAaZ7FHsiYWVWTukk6h940GssD4Tt8YRLk3XAfJZ77mWGrZp8Vz9gh'}
    print(request.session) # <django.contrib.sessions.backends.db.SessionStore object at 0x000002E36FCFE3A0>
    print(request.is_ajax()) # False
    print('################')
    print(request.META)
    print('----------------')
    return HttpResponse('hi')

urls.py  path('hi/<int:num>/<str:name>/',views.hi) 

浏览器访问:http://127.0.0.1:8000/myapp/hi/666/good/?key='中国'

 request.META 好多重要信息都在这里面!

{'PUBLIC': 'C:\Users\Public', 'LOCALAPPDATA': 'C:\Users\xiongjiawei\AppData\Local', 'PROGRAMFILES': 'C:\Program Files', 'VIRTUAL_ENV': 'D:\妗岄
潰\mypro\venv', 'USERNAME': 'xiongjiawei', 'COMMONPROGRAMW6432': 'C:\Program Files\Common Files', 'LG_PATH': 'D:\Program Files (x86)\HP\LoadRunne
r\', 'IDEA_INITIAL_DIRECTORY': 'C:\Program Files\JetBrains\PyCharm 2020.1\bin', 'NUMBER_OF_PROCESSORS': '4', 'PROCESSOR_ARCHITECTURE': 'AMD64', 'PR
OMPT': '(venv) $P$G', '__INTELLIJ_COMMAND_HISTFILE__': 'C:\Users\xiongjiawei\AppData\Roaming\JetBrains\PyCharm2020.1\terminal\history\history-1
', 'SYSTEMDRIVE': 'C:', 'CONFIGSETROOT': 'C:\WINDOWS\ConfigSetRoot', 'CLASSPATH': 'C:\Program Files\Java\jdk1.8.0_131\lib\dt.jar;C:\Program File
s\Java\jdk1.8.0_131\lib\tools.jar', 'PROGRAMDATA': 'C:\ProgramData', 'PROCESSOR_REVISION': '8e09', 'COMMONPROGRAMFILES(X86)': 'C:\Program Files (x
86)\Common Files', 'USERPROFILE': 'C:\Users\xiongjiawei', 'ANALYSIS_PATH': 'D:\Program Files (x86)\HP\LoadRunner\', '_OLD_VIRTUAL_PATH': 'D:\app
\xiongjiawei\product\11.2.0\dbhome_1\bin;C:\ProgramData\Oracle\Java\javapath;C:\Program Files (x86)\Intel\iCLS Client\;C:\Program Files\I
ntel\iCLS Client\;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x8
6)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\Inte
l\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files (x86)\NVIDIA Corpo
ration\PhysX\Common;D:\Program Files\TortoiseSVN\bin;D:\Users\xiongjiawei\AppData\Local\Android\sdk\platform-tools;D:\Program Files\VanDyk
e Software\Clients\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;D:\Program Files (
x86)\Appium\node_modules\.bin;D:\Program Files\nodejs\;D:\Program Files\android-sdk-windows\tools;C:\Program Files\Java\jdk1.8.0_131\bin;D:
\Program Files\android-sdk-windows\platform-tools;C:\Program Files\Java\jdk1.8.0_131\jre\bin;C:\WINDOWS\System32\OpenSSH\;d:\Program Files
(x86)\Tesseract-OCR;d:\Program Files\Git\cmd;D:\Program Files\Python\Python38\Scripts\;D:\Program Files\Python\Python38\;C:\Users\xiongji
awei\AppData\Local\Microsoft\WindowsApps;C:\Users\xiongjiawei\AppData\Roaming\npm;%PyCharm Community Edition%;D:\Program Files (x86)\Tesserac
t-OCR;', 'USERDOMAIN': 'LAPTOP-BAIQPBL2', 'PSMODULEPATH': 'C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0
Modules', 'PROCESSOR_LEVEL': '6', 'TEMP': 'C:\Users\XIONGJ~1\AppData\Local\Temp', 'PROGRAMW6432': 'C:\Program Files', 'TESSDATA_PREFIX': 'd:\Pro
gram Files (x86)\Tesseract-OCR\', 'APPDATA': 'C:\Users\xiongjiawei\AppData\Roaming', 'LOGONSERVER': '\\LAPTOP-BAIQPBL2', 'JAVA_HOME': 'C:\Progr
am Files\Java\jdk1.8.0_131', 'PATHEXT': '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC', 'TERMINAL_EMULATOR': 'JetBrains-JediTerm', 'LR_PATH':
 'D:\Program Files (x86)\HP\LoadRunner\', 'WINDIR': 'C:\WINDOWS', '_OLD_VIRTUAL_PROMPT': '$P$G', 'SYSTEMROOT': 'C:\WINDOWS', 'COMPUTERNAME': 'LAPT
OP-BAIQPBL2', 'SESSIONNAME': 'Console', 'PROGRAMFILES(X86)': 'C:\Program Files (x86)', 'PROCESSOR_IDENTIFIER': 'Intel64 Family 6 Model 142 Stepping 9,
GenuineIntel', 'HOMEPATH': '\Users\xiongjiawei', 'DRIVERDATA': 'C:\Windows\System32\Drivers\DriverData', 'VUGEN_PATH': 'D:\Program Files (x86)\H
P\LoadRunner\', 'TMP': 'C:\Users\XIONGJ~1\AppData\Local\Temp', 'COMSPEC': 'C:\WINDOWS\system32\cmd.exe', 'ALLUSERSPROFILE': 'C:\ProgramData',
 'PATH': 'D:\妗岄潰\mypro\venv\Scripts;D:\app\xiongjiawei\product\11.2.0\dbhome_1\bin;C:\ProgramData\Oracle\Java\javapath;C:\Program File
s (x86)\Intel\iCLS Client\;C:\Program Files\Intel\iCLS Client\;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System3
2\WindowsPowerShell\v1.0\;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management En
gine Components\DAL;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\IPT;C:\Program Files\Intel\Intel(R) Management Engine Com
ponents\IPT;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;D:\Program Files\TortoiseSVN\bin;D:\Users\xiongjiawei\AppData\Local\Andr
oid\sdk\platform-tools;D:\Program Files\VanDyke Software\Clients\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\Syste
m32\WindowsPowerShell\v1.0\;D:\Program Files (x86)\Appium\node_modules\.bin;D:\Program Files\nodejs\;D:\Program Files\android-sdk-windows\t
ools;C:\Program Files\Java\jdk1.8.0_131\bin;D:\Program Files\android-sdk-windows\platform-tools;C:\Program Files\Java\jdk1.8.0_131\jre\bin;C
:\WINDOWS\System32\OpenSSH\;d:\Program Files (x86)\Tesseract-OCR;d:\Program Files\Git\cmd;D:\Program Files\Python\Python38\Scripts\;D:\Pr
ogram Files\Python\Python38\;C:\Users\xiongjiawei\AppData\Local\Microsoft\WindowsApps;C:\Users\xiongjiawei\AppData\Roaming\npm;%PyCharm Co
mmunity Edition%;D:\Program Files (x86)\Tesseract-OCR;', 'ANDROID_HOME': 'D:\Program Files\android-sdk-windows', 'HOMEDRIVE': 'C:', 'COMMONPROGRAMFI
LES': 'C:\Program Files\Common Files', 'ONEDRIVE': 'C:\Users\xiongjiawei\OneDrive', 'USERDOMAIN_ROAMINGPROFILE': 'LAPTOP-BAIQPBL2', 'OS': 'Windows_
NT', 'LOG_FILE': 'C:\Users\xiongjiawei\AppData\Local\Temp\ihp_custom_batches.log', 'DJANGO_SETTINGS_MODULE': 'mypro.settings', 'RUN_MAIN': 'true',
 'SERVER_NAME': 'LAPTOP-BAIQPBL2', 'GATEWAY_INTERFACE': 'CGI/1.1', 'SERVER_PORT': '8000', 'REMOTE_HOST': '', 'CONTENT_LENGTH': '', 'SCRIPT_NAME': '', 'S
ERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'WSGIServer/0.2', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/myapp/hi/666/good/', 'QUERY_STRING': 'key=%27%
E4%B8%AD%E5%9B%BD%27', 'REMOTE_ADDR': '127.0.0.1', 'CONTENT_TYPE': 'text/plain', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_C
ACHE_CONTROL': 'max-age=0', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHT
ML, like Gecko) Chrome/85.0.4183.121 Safari/537.36', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/a
png,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'HTTP_SEC_FETCH_SITE': 'none', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_SEC_FETCH_USER': '?1', 'H
TTP_SEC_FETCH_DEST': 'document', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.9', 'HTTP_COOKIE': 'csrftoken=QybCRg
hQbKBAaZ7FHsiYWVWTukk6h940GssD4Tt8YRLk3XAfJZ77mWGrZp8Vz9gh', 'wsgi.input': <django.core.handlers.wsgi.LimitedStream object at 0x000002B0A8BD3E20>, 'wsgi
.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.version': (1, 0), 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsg
i.multithread': True, 'wsgi.multiprocess': False, 'wsgi.file_wrapper': <class 'wsgiref.util.FileWrapper'>, 'CSRF_COOKIE': 'QybCRghQbKBAaZ7FHsiYWVWTukk6h
940GssD4Tt8YRLk3XAfJZ77mWGrZp8Vz9gh'}
View Code

get请求传递多个同名参数获取方式:

 views.py

def hi(request,num,name):
    print('----------------')
    print(request.GET.get('name')) # xyz
    print(request.GET['name']) # xyz
    print(request.GET.getlist('name')) # ['abc', 'xyz']
    print(request.GET.get('notexist')) # None
    print(request.GET.getlist('notexist')) # []
    print('----------------')
    return HttpResponse('hi')

浏览器访问:http://127.0.0.1:8000/myapp/hi/666/good/?name=abc&name=xyz


 POST请求

CSRF验证失败,解决方式一:注释掉settiings中的相应代码 'django.middleware.csrf.CsrfViewMiddleware' 

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action="/myapp/login/" method="post">
    {% csrf_token %} {# CSRF验证失败,解决方式二 #}
<!--    <input type="hidden" name="csrfmiddlewaretoken" value="DFXxHTfWPbUlw37XwtuJAj4wh7EXGLSKtzeyUwreCi45p1Axy0jS0kO4McsMYL41">-->
    {# 若不写name,则request.POST为空 #}
    <input type="text" placeholder="请输入账号" name="username">
    <input type="password" placeholder="请输入密码" name="password">
<!--    <button>登录</button>--> {# 登录按钮写法一 #}
    <input type="submit" value="登录"> {# 登录按钮写法二 #}
</form>
</body>
</html>

views.py

def login(request):
    if request.method=='GET':
        return render(request,'login.html')
    elif request.method=='POST':
        # login.html中账号和密码未写name则为<QueryDict: {}>
        # 写了name为<QueryDict: {'username': ['111'], 'password': ['222']}>
        print(request.POST) # 获取其中具体的值同request.GET
        return HttpResponse('登录成功')

HttpResponseJsonResponse

views.py

def getresponse(request):
    # Content-Type: text/html
    response=HttpResponse()
    response.content=''
    response.content='' # 覆盖
    response.write('') # 追加
    response.write('') # 还是追加
    response.flush()

    # Content-Type: application/json
    # response=JsonResponse({'country':'中国'})
    return response

 重定向

views.py

def first(request):
    # return HttpResponse('first')
    return HttpResponseRedirect('/myapp/second/') # 重定向,方式一
    # return redirect('/myapp/second') # 重定向,方式二

def second(request):
    return HttpResponse('second')

urls.py

path('first/',views.first),
path('second/',views.second),

会话技术

Cookie客户端会话技术

 views.py

def login(request):
    if request.method=='GET':
        return render(request,'login.html')
    elif request.method=='POST':
        username=request.POST.get('username')
        password=request.POST.get('password')
        response=redirect('/myapp/mine/')
        # Cookie是服务器设置给客户端的,通过response实现
        # Cookie不支持中文,可通过存时编码,取时解码实现。
        username=base64.standard_b64encode(username.encode('utf8')).decode('utf8')
        password=base64.standard_b64encode(password.encode('utf8')).decode('utf8')
        # response.set_cookie('username',username)
        # response.set_cookie('password',password)
        # response.set_signed_cookie('username',username,'salt')
        # response.set_signed_cookie('password',password,'salt')
        # response.set_signed_cookie('username',username,SECRET_KEY)
        # response.set_signed_cookie('password',password,SECRET_KEY)
        # 设置过期时间max_age单位秒,0表示浏览器关闭失效。
        # expires单位秒,还支持timedelta(days=1),1天后过期
        response.set_signed_cookie('username',username,SECRET_KEY,max_age=60)
        response.set_signed_cookie('password',password,SECRET_KEY,expires=timedelta(days=1))
        return response

def mine(request):
    # 在请求时Cookie自动被携带,Cookie不能跨浏览器,且IP(域名)隔离
    # username=request.COOKIES.get('username')
    # password=request.COOKIES.get('password')
    try:
        # username=request.get_signed_cookie('username',salt='salt')
        # password=request.get_signed_cookie('password',salt='salt')
        username=request.get_signed_cookie('username',salt=SECRET_KEY)
        password=request.get_signed_cookie('password',salt=SECRET_KEY)
        username=base64.standard_b64decode(username.encode('utf8')).decode('utf8')
        password=base64.standard_b64decode(password.encode('utf8')).decode('utf8')
    except:
        username=password=None
    print('username=',username)

    # 写法一
    # if username and password:
    #     return HttpResponse('个人中心')
    # return HttpResponse('游客')

    # 写法二
    return render(request,'mine.html',locals())

def logout(request):
    response=HttpResponse('退出登录')
    response.delete_cookie('username')
    response.delete_cookie('password')
    return response

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action="/myapp/login/" method="post">
    {% csrf_token %}
    <input type="text" placeholder="请输入账号" name="username">
    <input type="password" placeholder="请输入密码" name="password">
    <button>登录</button>
</form>
</body>
</html>

mine.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>个人中心</title>
</head>
<body>
    {% if username %}
        <h1>欢迎:{{ username }}</h1>
        <a href="/myapp/logout/">退出</a>
    {% else %}
        <h2>游客</h2>
        <a href="/myapp/login/">登录</a>
    {% endif %}
</body>
</html>

urls.py

    path('login/',views.login),
    path('mine/',views.mine),
    path('logout/',views.logout),

session服务端会话技术

views.py

def login(request):
    if request.method=='GET':
        return render(request,'login.html')
    elif request.method=='POST':
        username=request.POST.get('username')
        password=request.POST.get('password')
        response=redirect('/myapp/mine/')

        # 支持中文,默认用base64编码/解码
        request.session['username']=username
        request.session['password']=password

        return response

def mine(request):
    username=request.session.get('username')
    password=request.session.get('password')

    # 写法一
    # if username and password:
    #     return HttpResponse('个人中心')
    # return HttpResponse('游客')

    # 写法二
    return render(request,'mine.html',locals())

def logout(request):
    response=HttpResponse('退出登录')

    # 退出会话,方式一,不会同步删除django_session表中的session会话记录
    # response.delete_cookie('sessionid')

    # 退出会话,方式二,本质是将django_session表中的session会话记录的session_data字段中关于用户信息的数据置空{}
    # del request.session['username']
    # del request.session['password']

    # 退出会话,方式三,推荐方式,django_session表不会产生垃圾数据
    request.session.flush()

    return response

Token自定义服务端会话技术。Cookie依赖于浏览器,Session要利用Cookie的Set-Cookie:sessionid=xxx,所以还是依赖浏览器。对于非BS架构的,如CS架构,就不能使用Cookie和Session会话技术。

models.py

class Staff(models.Model):
    name=models.CharField(max_length=16)
    token=models.CharField(max_length=16,null=True)

views.py

def login(request):
    name=request.POST.get('name')
    print('name=',name)
    try:
        staff=Staff.objects.get(name=name)
        token=uuid.uuid4().hex
        staff.token=token
        staff.save()
        data={
            'msg':'ok',
            'token':token
        }
        return JsonResponse(data)
    except:
        return JsonResponse({'msg': '用户不存在,或不唯一'})


def mine(request):
    token=request.GET.get('token')
    try:
        staff=Staff.objects.get(token=token)
        data={
            'msg':'ok',
            'name':staff.name
        }
        return JsonResponse(data)
    except:
        return JsonResponse({'msg':'用户不存在,或不唯一'})

settings.py  # 'django.middleware.csrf.CsrfViewMiddleware', 

类似手机应用服务端接口,所以使用 Postman配合验证。

原文地址:https://www.cnblogs.com/xiongjiawei/p/13751483.html