微信小程序+Django—微信登录手机号码登录

时间:2020-04-11
本文章向大家介绍微信小程序+Django—微信登录手机号码登录,主要包括微信小程序+Django—微信登录手机号码登录使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

第一步注册微信小程序

https://mp.weixin.qq.com/

 

第二步下载微信开发工具

http://www.ionic.wang/weixin/devtools/download.html

 

第三步新建小程序项目

 

项目结构:

 

修改app.js

新增小程序登录页面

 

打开项目pages目录-->login 文件夹

修改login.js    login.json     login.wxml  login.wxss

login.js 代码如下

// pages/login/login.js
const app = getApp()
const {
  $Toast
} = require('../../dist/base/index')
const {
  $Message
} = require('../../dist/base/index');
Page({

  /**
   * 页面的初始数据
   */
  data: {
    yhxyVisible: false,
    ystkVisible: false
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    wx.getUserInfo({
      success: function (res) {
        var userInfo = res.userInfo
        var nickName = userInfo.nickName
        console.log(nickName)
        app.globalData.userInfo.nickName = nickName
        var avatarUrl = userInfo.avatarUrl
        app.globalData.userInfo.avatarUrl = avatarUrl
        var gender = userInfo.gender //性别 0:未知、1:男、2:女
        app.globalData.userInfo.gender = gender
        var province = userInfo.province
        var city = userInfo.city
        var country = userInfo.country
      }
    })

  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {

  },
  //手机号登录
  mobileLogin(e) {
    $Toast({
      content: '登录中...',
      type: 'loading',
      duration: 0,
      mask: false
    });
    console.log(e.detail.errMsg)
    console.log(e.detail.iv)
    console.log(e.detail.encryptedData)
    wx.login({
      success: res => {
        console.log(res)
        //请求后端换取openid的接口
        wx.request({
          url: 'http://199394.wezoz.com/landlorde/app/mobileLogin/',
          method: 'POST',
          data: {
            //将code和用户基础信息传到后端
            jscode: res.code,
            iv: e.detail.iv,
            encryptedData: e.detail.encryptedData,
            nickname: app.globalData.userInfo.nickName,
            avatar_url: app.globalData.userInfo.avatarUrl,
            gender: app.globalData.userInfo.gender
          },
          success: res => {
            //获取到openid作为账号
            console.log("res=============", res)
            console.log(app.globalData.userInfo)
            if (res.data.code == 200 && res.data.msg == "ok") {
              //this.reFreshUserProfile()
              wx.setStorageSync('token', res.data.token)
              app.globalData.isLogin = true
              app.globalData.hasUserInfo = true
              app.globalData.token = res.data.token
              $Toast.hide();
              wx.redirectTo({
                url: '../index/index?id=1'
              })
            } else {
              wx.showToast({
                title: '网络发生错误请稍后再试!',
                icon: 'error',
                duration: 2000
              })
            }
          }
        })
      }
    })
  },
  //微信登录
  wxlogin() {
    $Toast({
      content: '登录中...',
      type: 'loading',
      duration: 0,
      mask: false
    });
    wx.login({
      success: res => {
        console.log(res)
        //请求后端换取openid的接口
        wx.request({
          url: 'http://199394.wezoz.com/landlorde/app/wxlogin/',
          method: 'POST',
          data: {
            //将code传到后端
            jscode: res.code
          },
          success: res => {
            //获取到openid作为账号
            console.log("res=============", res)
            console.log(app.globalData.userInfo)
            if (res.data.code == 200 && res.data.msg == "ok") {
              //this.reFreshUserProfile()
              wx.setStorageSync('token', res.data.token)
              app.globalData.isLogin = true
              app.globalData.hasUserInfo = true
              app.globalData.token = res.data.token
              $Toast.hide();
              wx.redirectTo({
                url: '../index/index?id=1'
              })
            } else {
              wx.showToast({
                title: '登录失败请稍后再试!',
                icon: 'error',
                duration: 2000
              })
            }
          }
        })
      }
    })
  },
  yhxy() {
    this.setData({
      yhxyVisible: true
    });
  },
  yhxyok() {
    this.setData({
      yhxyVisible: false
    });
  },
  yhxycancel() {
    this.setData({
      yhxyVisible: false
    });
  },
  ystk() {
    this.setData({
      ystkVisible: true
    });
  },
  ystkok() {
    this.setData({
      ystkVisible: false
    });
  },
  ystkcancel() {
    this.setData({
      ystkVisible: false
    });
  },
})

 

 login.json

{
  "navigationBarTitleText": "登录",
  "usingComponents": {
    "i-button": "../../dist/button/index",
    "i-panel": "../../dist/panel/index",
    "i-avatar": "../../dist/avatar/index",
    "i-row": "../../dist/row/index",
    "i-col": "../../dist/col/index",
    "i-toast": "../../dist/toast/index",
    "i-modal": "../../dist/modal/index",
    "i-message": "../../dist/message/index"
  }
}

 

 login.wxml

<!--pages/login/login.wxml-->
<view class="container">
    <image class="logStyle" src="http://img11.360buyimg.com/mobilecms/s700x256_jfs/t20539/229/82605291/66559/df347eb5/5af96a18N9451b1a1.jpg"></image>
    <i-button bindgetphonenumber="mobileLogin" style="background: #ff6700;" class="btnLoginStyle" open-type="getPhoneNumber" type="warning" shape="circle" size="large">手机快捷登录</i-button>
  <text class="weChatLogin" bindtap="wxlogin">微信登录</text>
</view>
<view class="company">
  <view>登录/注册代表您已阅读并同意 <text class="descStyle" bindtap="yhxy">用户协议</text><text class="descStyle" bindtap="ystk">隐私条款</text> </view>
</view>
<i-toast id="toast" />

<i-modal title="用户协议" visible="{{ yhxyVisible }}" bind:ok="yhxyok" bind:cancel="yhxycancel">
  <view>1.用户协议用户协议用户协议</view>
  <view>2.用户协议用户协议用户协议</view>
  <view>3.用户协议用户协议用户协议</view>
</i-modal>


<i-modal title="隐私条款" visible="{{ ystkVisible }}" bind:ok="ystkok" bind:cancel="ystkcancel">
  <view>1.隐私条款隐私条款隐私条款</view>
  <view>2.隐私条款隐私条款隐私条款</view>
  <view>3.隐私条款隐私条款隐私条款</view>
</i-modal>

login.wxss

/* pages/login/login.wxss */
.item {
  height: 100px;
  text-align: center;
}

.container {
  display: block;
  width: 200px;
  height: 100px;
  margin: 0 auto;
  position: relative;
  text-align: center;
}

.logStyle {
  width: 100px;
  height: 100px;
  background-color: #eeeeee;
  margin-top: 90rpx;
  margin-bottom: 50rpx;
  border-radius: 50%;
}
.i-btn-warning{
  background: #ff6700 !important;
}
.descStyle {
  color: #ff6700;
}
.company {
  position: absolute;
  bottom: 30rpx;
  width: 100%;
  display: flex;
  justify-content: center;
  font-size: 12px;
}

.weChatLogin{
  font-size: 14px;
}

 

效果图如下

 

 

第四步新建django项目

   可参照微信开发入门篇一

    https://www.cnblogs.com/wangcongxing/p/11546780.html

 

第五步处理微信登录

 打开django项目,结构如下

 

authentication.py
from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.settings import api_settings


class UserAuthentication(BaseAuthentication):
    def authenticate(self, request):
        if 'token' in request.data:
            try:
                token = request.data['token']
                jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
                user_dict = jwt_decode_handler(token)
                return (user_dict, token)
            except Exception as ex:
                raise exceptions.AuthenticationFailed(detail={'code': 401, 'msg': 'skey已过期'})
        else:
            raise exceptions.AuthenticationFailed(detail={'code': 400, 'msg': '缺少token'})

    def authenticate_header(self, request):
        return 'skey'
WXBizDataCrypt.py
import base64
import json
from Crypto.Cipher import AES

class WXBizDataCrypt:
    def __init__(self, appId, sessionKey):
        self.appId = appId
        self.sessionKey = sessionKey

    def decrypt(self, encryptedData, iv):
        # base64 decode
        sessionKey = base64.b64decode(self.sessionKey)
        encryptedData = base64.b64decode(encryptedData)
        iv = base64.b64decode(iv)

        cipher = AES.new(sessionKey, AES.MODE_CBC, iv)

        decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData)))

        if decrypted['watermark']['appid'] != self.appId:
            raise Exception('Invalid Buffer')

        return decrypted

    def _unpad(self, s):
        return s[:-ord(s[len(s)-1:])]
urls.py
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from app import views
from app import test_view

urlpatterns = [
    # 微信小程序登录
    path('wxlogin/', views.code2Session),
    # 微信手机号码登录
    path('mobileLogin/', views.mobileLogin),
    # 通过token获取用转换用户信息
    path('checkToken/', views.checkToken),
]
models.py
from django.db import models

# Create your models here.
from django.contrib.auth.models import AbstractUser
from django.db import models


class NewUser(AbstractUser):
    nickname = models.CharField(max_length=225, verbose_name="昵称", default="")
    avatar_url = models.CharField(max_length=225, verbose_name="头像", default="")
    gender = models.CharField(max_length=225, verbose_name="性别", default="")
    session_key = models.CharField(max_length=225, verbose_name="session_key", default="")
    mobilePhoneNumber = models.CharField(max_length=225, verbose_name="手机号码", default="")
views.py
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
# Create your views here.
from django.http import HttpResponse
from django.contrib.auth.models import Permission, User
from django.contrib import auth
from django.views.decorators.http import require_http_methods
from rest_framework.views import APIView
from rest_framework_jwt.settings import api_settings
from django.core import serializers
from django.http import JsonResponse, HttpResponse, HttpResponseRedirect
from django.shortcuts import render, redirect
import uuid, datetime, json, time
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from wechatpy import WeChatClient
import json, re, urllib3
import os
import json
from wechatpy import WeChatPay
from django.views.decorators.csrf import csrf_exempt
import time
import random
from django.core.cache import cache  # 引入缓存模块
from wechatpy.utils import timezone
from django.db import transaction
from django import forms
from django.db.models import F
import requests
from django.contrib.auth.models import Permission, User
from django.db import connection
from django.db.models import Sum


@csrf_exempt
def login(request):
    if request.method == "POST":
        username = request.POST.get('username')
        passwd = request.POST.get('passwd')

        user = User.objects.filter(username=username).first()
        auth.login(request, user)
        # 登录成功
        print(username)
        print(passwd)
        return HttpResponse("登录成功!")
    else:
        return HttpResponse("请求错误")

#小程序appid
appid = "wxxxxxxxxx"
# 小程序秘钥
appsecret = "xxxxxxxxxxxx"

'''
登录函数:
'''


@require_http_methods(['POST'])
@csrf_exempt
def GetOpenIdView(request):
    data = json.loads(request.body)
    jscode = data['jscode']

    openid, session_key = OpenId(jscode).get_openid()
    return JsonResponse({
        'openid': openid,
        'session_key': session_key
    })


from app import models


@require_http_methods(['POST'])
@csrf_exempt
def login_or_create_account(request):
    data = json.loads(request.body)
    print(data)
    openid = data['openid']
    nickname = data['nickname']
    avatar_url = data['avatar_url']
    gender = data['gender']

    try:
        user = models.NewUser.objects.get(username=openid)
    except models.NewUser.DoesNotExist:
        user = None

    if user:
        user = models.NewUser.objects.get(username=openid)
    else:
        user = models.NewUser.objects.create(
            username=openid,
            password=openid,
            nickname=nickname,
            avatar_url=avatar_url,
            gender=gender
        )

    jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
    jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    payload = jwt_payload_handler(user)
    token = jwt_encode_handler(payload)
    res = {
        'status': 'success',
        'nickname': user.nickname,
        'user_id': user.id,
        'token': token
    }
    return JsonResponse(res)


class OpenId:
    def __init__(self, jscode):
        self.url = 'https://api.weixin.qq.com/sns/jscode2session'
        self.app_id = appid
        self.app_secret = appsecret
        self.jscode = jscode

    def get_openid(self):
        url = self.url + "?appid=" + self.app_id + "&secret=" + self.app_secret + "&js_code=" + self.jscode + "&grant_type=authorization_code"
        res = requests.get(url)
        try:
            openid = res.json()['openid']
            session_key = res.json()['session_key']
        except KeyError:
            return 'fail'
        else:
            return openid, session_key


import hashlib
import json
import requests
from rest_framework import status
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from django_redis import get_redis_connection
from app.WXBizDataCrypt import WXBizDataCrypt

appid = "wx723afa664e007e3d"
secret = "9ae908c6ee8d5ac3559bbb4a5d0d5fc8"


@api_view(['POST'])
@authentication_classes([])  # 添加
def code2Session(request):
    js_code = request.data['jscode']
    url = 'https://api.weixin.qq.com/sns/jscode2session' + '?appid=' + appid + '&secret=' + secret + '&js_code=' + js_code + '&grant_type=authorization_code'
    response = json.loads(requests.get(url).content)  # 将json数据包转成字典
    if 'errcode' in response:
        # 有错误码
        return Response(data={'code': response['errcode'], 'msg': response['errmsg']})
    # 登录成功
    openid = response['openid']
    session_key = response['session_key']
    # 保存openid, 需要先判断数据库中有没有这个openid
    user = models.NewUser.objects.filter(username=openid).first()
    if user is None:
        user = models.NewUser.objects.create(
            username=openid,
            password=uuid.uuid4(),
            session_key=session_key
        )
    else:
        user.session_key = session_key
        user.save()

    # 将自定义登录态保存到缓存中, 两个小时过期
    jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
    jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    payload = jwt_payload_handler(user)
    skey = jwt_encode_handler(payload)
    print(skey)
    return JsonResponse({'code': 200, 'msg': 'ok', 'token': skey})


def filter_emoji(desstr, restr=''):
    # 过滤表情
    try:
        res = re.compile(u'[\U00010000-\U0010ffff]')
    except re.error:
        res = re.compile(u'[\uD800-\uDBFF][\uDC00-\uDFFF]')
    return res.sub(restr, desstr)


@api_view(['POST'])
@authentication_classes([])  # 添加
def mobileLogin(request):
    js_code = request.data['jscode']
    iv = request.data["iv"]
    encryptedData = request.data["encryptedData"]
    nickname = request.data["nickname"]
    avatar_url = request.data["avatar_url"]
    gender = request.data["gender"]
    nickname = filter_emoji(nickname, '')
    if js_code is None or iv is None or encryptedData is None or avatar_url is None:
        return JsonResponse({'code': 400, 'msg': '系统维护,请稍后再试!'})
    url = 'https://api.weixin.qq.com/sns/jscode2session' + '?appid=' + appid + '&secret=' + secret + '&js_code=' + js_code + '&grant_type=authorization_code'
    response = json.loads(requests.get(url).content)  # 将json数据包转成字典
    if 'errcode' in response:
        # 有错误码
        return JsonResponse({'code': response['errcode'], 'msg': response['errmsg']})
    try:
        openid = response['openid']
        session_key = response['session_key']
        wxdc = WXBizDataCrypt(appid, session_key)
        pResult = wxdc.decrypt(encryptedData, iv)
        print(pResult)
        # 保存openid, 需要先判断数据库中有没有这个openid
        user = models.NewUser.objects.filter(username=openid).first()
        if user is None:
            user = models.NewUser.objects.create(
                username=openid,
                password=uuid.uuid4(),
                session_key=session_key,
                nickname=nickname,
                avatar_url=avatar_url,
                gender=gender,
                mobilePhoneNumber=pResult["phoneNumber"]
            )
        else:
            user.session_key = session_key
            user.nickname = nickname,
            user.avatar_url = avatar_url,
            user.gender = gender
            user.save()
        # token有效期1天,settings.py 文件中设置
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        print(token)
        return JsonResponse({'code': 200, 'msg': 'ok', 'token': token})
    except Exception as ex:
        return JsonResponse({'code': 400, 'msg': "发生错误请稍后再试!"})


from app import authentication


@api_view(['POST'])
@authentication_classes([authentication.UserAuthentication])  # 添加
def checkToken(request):
    print(request.user)
    print(request.user["username"])
    return JsonResponse({'code': 200, 'msg': 'ok', 'userInfo': request.user})
settings.py
"""
Django settings for landlorde project.

Generated by 'django-admin startproject' using Django 3.0.5.

For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '1mf87-kx7x6*zoio^f6@8oqu*t=**pmv*i^kduz*hc)iw4r(5q'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["*"]


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app.apps.AppConfig',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'landlorde.urls'

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',
            ],
        },
    },
]

WSGI_APPLICATION = 'landlorde.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'NAME': 'landlorde',
        'USER': 'root',
        'PASSWORD': '123456'
    }
}
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    },
}
REDIS_TIMEOUT = 7 * 24 * 60 * 60
CUBES_REDIS_TIMEOUT = 60 * 60
NEVER_REDIS_TIMEOUT = 365 * 24 * 60 * 60
# 开发redis 路径 C:\Program Files\Redis redis-server redis.windows.conf
'''
windows下安装Redis第一次启动报错:

[2368] 21 Apr 02:57:05.611 # Creating Server TCP listening socket 127.0.0.1:6379: bind: No error
解决方法:在命令行中运行
redis-cli.exe
127.0.0.1:6379>shutdown
not connected>exit
然后重新运行redis-server.exe redis.windows.conf
'''
'''
测试地址
http://199394.wezoz.com/landlorde/app/get-openid/

http://199394.wezoz.com/landlorde/app/wx-login/
'''
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/landlordestatic/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'landlordestatic'),
)
# SIMPLEUI 配置
SIMPLEUI_STATIC_OFFLINE = True
SIMPLEUI_HOME_INFO = False

# 图片上传路径
MEDIA_URL = '/'
MEDIA_ROOT = r'D:\landlordestatic'

# 服务号配置
domain = "http://www.xxxx.com/"

keysPath = os.path.join(BASE_DIR, 'keys')

AUTH_USER_MODEL = "app.NewUser"

weChatConfig = {
    'appid': "wxxxxxxxxxx",
    'appsecret': "xxxxxxxxxxxxxxx",
    'mch_id': "xxxxxxxx",
    'notify_url': domain + '/notify_url/',
    'baiduapiak': 'YL28EqCk1aMU86fVvWrpb6GqBGdGZsMu',
    'baiduapiwebak': 'UErCZCjbvgXXCIziTZH5xOicFiLq8INt',
    'mch_cert': os.path.join(keysPath, r"apiclient_cert.pem"),
    'mch_key': os.path.join(keysPath, r"apiclient_key.pem"),
}


# 在末尾添加上
import datetime

# 在末尾添加上
'''
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',# JWT认证,在前面的认证方案优先
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}
'''

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), #JWT_EXPIRATION_DELTA 指明token的有效期
}


REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'app.authentication.UserAuthentication',  # 用自定义的认证类
    ),
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    ),
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
    ),
}

 

最终效果:

 

控制台输出

 

微信开发工具控制台输出

 

 

$flag 上一页 下一页