博客 (64)

Nuget 安装:X.PagedList.Mvc.Core


控制器:

using X.PagedList;

public IActionResult Index(int page = 1)
{
    ……
    return View(q.ToPagedList(page, size));
}


视图:

@using X.PagedList
@using X.PagedList.Mvc.Core
@model IPagedList<xxx>

@Html.PagedListPager(Model, page => Url.Action("Index", new { page }))


自定义(options 默认值):

@Html.PagedListPager(
    Model, 
    page => Url.Action("Index", new { page }), 
    new X.PagedList.Mvc.Common.PagedListRenderOptionsBase
    {
        HtmlEncoder = HtmlEncoder.get_Default(),
        DisplayLinkToFirstPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToLastPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToPreviousPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToNextPage = PagedListDisplayMode.IfNeeded,
        DisplayLinkToIndividualPages = true,
        DisplayPageCountAndCurrentLocation = false, // 显示总页数和当前页码
        MaximumPageNumbersToDisplay = 10, // 最多显示页码数
        DisplayEllipsesWhenNotShowingAllPageNumbers = true,
        EllipsesFormat = "&#8230;",
        LinkToFirstPageFormat = "<<",
        LinkToPreviousPageFormat = "<",
        LinkToIndividualPageFormat = "{0}",
        LinkToNextPageFormat = ">",
        LinkToLastPageFormat = ">>",
        PageCountAndCurrentLocationFormat = "Page {0} of {1}.",
        ItemSliceAndTotalFormat = "Showing items {0} through {1} of {2}.",
        FunctionToDisplayEachPageNumber = null,
        ClassToApplyToFirstListItemInPager = null,
        ClassToApplyToLastListItemInPager = null,
        ContainerDivClasses = new string[1]
        {
            "pagination-container"
        },
        UlElementClasses = new string[1]
        {
            "pagination"
        },
        LiElementClasses = Enumerable.Empty<string>(),
        PageClasses = Enumerable.Empty<string>(),
        UlElementattributes = null,
        ActiveLiElementClass = "active",
        EllipsesElementClass = "PagedList-ellipses",
        PreviousElementClass = "PagedList-skipToPrevious",
        NextElementClass = "PagedList-skipToNext",
    })


保留地址栏参数:

@{ 
    string query = Context.Request.QueryString.Value;
}
@Html.PagedListPager(Model, page => Regex.IsMatch(query, @"[?&]page=\d+")
    ? Regex.Replace(query, @"([?&])page=\d+", $"$1page={page}")
    : (query.StartsWith("?") ? $"{query}&page={page}" : $"{query}?page={page}"),
    new X.PagedList.Web.Common.PagedListRenderOptionsBase
    {
        DisplayPageCountAndCurrentLocation = true,
        MaximumPageNumbersToDisplay = 5,
    })

这里从查询字符串中判断并替换 page 值,如果有更简单的方法敬请告知。比如 Webdiyer 的分页组件会自动携带所有参数。


更多使用方式参官方文档:https://github.com/dncuug/X.PagedList


附应用于 Unify Template(一款基于 Bootstrap 的 HTML 模板)中的配置:

<style>
    .u-pagination-v1-1--active .u-pagination-v1-1 { color: #fff; border-color: #72c02c; }
    .PagedList-pageCountAndLocation { float: right !important; }
</style>
@{
    string query = Context.Request.QueryString.Value;
}
@Html.PagedListPager(Model, page => Regex.IsMatch(query, @"[?&]page=\d+")
    ? Regex.Replace(query, @"([?&])page=\d+", $"$1page={page}")
    : (query.StartsWith("?") ? $"{query}&page={page}" : $"{query}?page={page}"),
    new X.PagedList.Web.Common.PagedListRenderOptionsBase
    {
        DisplayPageCountAndCurrentLocation = true,
        MaximumPageNumbersToDisplay = 5,
        UlElementClasses = new string[] { "list-inline" },
        LiElementClasses = new string[] { "list-inline-item" },
        PageClasses = new string[] { "u-pagination-v1__item", "u-pagination-v1-1", "g-pa-7-14" },
        ActiveLiElementClass = "u-pagination-v1-1--active",
        EllipsesElementClass = "g-pa-7-14",
    })


xoyozo 5 年前
9,663

当 MySQL 中使用 tinyint(1) 作为布尔值类型时,ASP.NET Core DBFirst 创建模型时会将其定义为 sbyte,运行时会抛出异常:

InvalidCastException: Unable to cast object of type 'System.Boolean' to type 'System.SByte'.

我们可以在数据库连接字符串中加入 TreatTinyAsBoolean=false 来实现不抛出异常,但程序中赋值和判断该字段时还是会比较麻烦。

因此我更倾向于将数据库中该字段类型改为 bit(1)


2019.11.28 注:这回遇到 tinyint(1) 映射到了 C# 的 bool,而 bit(1) 却映射到了 ulong。具体什么原因未作排查,反正哪个最终为 bool 就选哪个吧。

xoyozo 5 年前
3,650
  1. 微信的 H5 页面前端引导到:

    https://api.magcloud.cc/magshare/{siteId}?jump_url={jump_url}&content_url={content_url}

    各参数含义见官方文档:http://doc.magcloud.net/325339

    content_url 地址(一般仍为活动页面)接收参数 magshareredirect,值为 1 则跳转到自定义的引导页。

  2. 自定义引导页按 iOS 和安卓显示图文和箭头,引导用户点击右上角菜单,在浏览器中打开。

  3. 在浏览器 H5 中引导打开:

    {siteId}://pagejump?jump_url={jump_url}

    各参数含义见第 1 步中的文档。

    这里涉及一个“若未安装则引导安装”的过程,可以选择下方合适的一种方式实现:

    • A 标签的 href 填入下载地址,onclick 事件 location 到 App

    • onclick 事件 location 到 App,再用 setTimeOut location 到下载地址

    • 若仍无法实现预期效果,可以在弹出层中放置“未安装,前往下载”和“已安装,立即打开”两个按钮,让用户自己选择

  4. 若下载地址为应用宝,那么在 iOS 中可能会出现“打开 App 后继而自动打开 App Store”的情况,可以自建中转的 App 介绍页引导到 App Store 或应用宝。

  5. 分别实现微信和 MAGAPP 客户端的分享

xoyozo 5 年前
4,886

前言:

ChatGPT 给了 3 条建议:

  1. 在应用程序中正确释放数据库连接。确保在使用完数据库连接后,将其关闭并将其返回到连接池中。您可以使用 using 语句来确保连接在使用完毕后被正确释放。

  2. 调整连接池的大小。默认情况下,连接池的最大大小为 100。如果您的应用程序需要更多的连接,则可以增加连接池的大小。您可以在连接字符串中设置 Max Pool Size 属性来调整连接池的大小。

  3. 调整连接池的超时时间。默认情况下,连接池中的连接在 30 秒钟内未使用时将被关闭。如果您的应用程序需要更长的连接时间,则可以增加连接池的超时时间。您可以在连接字符串中设置 Connection Lifetime 属性来调整连接池的超时时间。

亲测有效,尤其是第 3 条,原因是 Connection Lifetime 的默认值是 0,即没有超时限制。

—— 2023.5

一般地,我们使用 EF 连接数据库前会先初始化一个数据库上下文:

dbEntities db = new dbEntities();

虽然 ASP.NET 会在查询完毕后自动关闭该连接,但是在什么情况下回收等都是不确定的,所以会导致 MySQL 中出现很多 Sleep 的连接(执行命令 SHOW FULL PROCESSLIST 可见),占用数据库的连接数,除非主动调用 Dispose():

db.Dispose();

官方建议的写法是使用 using 语法:

using (dbEntities db = new dbEntities())
{
}

using 会自动调用 Dispose()。这样对减少连接数是很有效的,但官方提示为了提高下一次连接的速度,并不会完全关闭所有连接。

C# 8 建议写法:

using dbEntities db = new dbEntities();

在实际项目中(该项目有 500+处数据库连接)测试结果,在不执行 Dispose() 时稳定为 140 个左右连接数,使用 using 或 Dispose() 后稳定变为 40 个左右。


如果不小心在 using 外部或 Dispose() 后再次对该上下文执行查询操作会出现异常:

无法完成该操作,因为 DbContext 已释放。

此 ObjectContext 实例已释放,不可再用于需要连接的操作。

所以要避免出现这种情况。这里还有一种另类的解决方法(不建议),根据上下文的特性,只要在 using 内查询一次(譬如视图中需要用到的导航属性,即外键关联的表),就可以在外部使用这个属性。


(建议)在 ASP.NET MVC 或 Web API 项目中,如果一个控制器中仅在 Action 外部定义一个 DbContext,那么,只要重写该控制器的 Dispose() 方法即可:

private dbEntities db = new dbEntities();

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        db.Dispose();
    }
    base.Dispose(disposing);
}

上下文使用 private 修饰即可。


参考:SQL Server 连接池 (ADO.NET)

xoyozo 5 年前
5,295

原标题:关于 PHP 获取 IP 地址的几种方法


PHP 获取客户端的 IP 地址有 4 种方式:

  1. REMOTE_ADDR:浏览当前页面的用户计算机的 IP 地址

  2. HTTP_X_FORWARDED_FOR:记录代理信息,会把每一层代理都记录

  3. HTTP_CLIENT_IP:客户端的 IP

  4. X-Real-IP:没有标准,由上一跳决定


REMOTE_ADDR 一般是不能伪造的,因为是通过服务器与客户端握手协议来获取的。而 HTTP_X_FORWARDED_FOR 和 HTTP_CLIENT_IP 是在 header 信息里面,所以客户端是可以进行很轻松的伪造。下面是对这三种方式详解:

https://www.v2ex.com/t/230367

https://www.test404.com/post-1448.html

https://www.cnblogs.com/mypath/articles/5239687.html


一、关于 REMOTE_ADDR
这个变量获取到的是“直接来源”的 IP 地址,所谓“直接来源”指的是直接请求该地址的客户端 IP 。这个 IP 在单服务器的情况下,很准确的是客户端 IP ,无法伪造。当然并不是所有的程序都一定是单服务器,比如在采用负载均衡的情况(比如采用 haproxy 或者 nginx 进行负载均衡),这个 IP 就是转发机器的 IP ,因为过程是客户端 -> 负载均衡 -> 服务端。是由负载均衡直接访问的服务端而不是客户端。


二、关于 HTTP_X_FORWARDED_FOR 和 HTTP_CLIENT_IP
基于“一”,在负载均衡的情况下直接使用 REMOTE_ADDR 是无法获取客户端 IP 的,这就是一个问题,必须解决。于是就衍生出了负载均衡端将客户端 IP 加入到 HEAD 中发送给服务端,让服务端可以获取到客户端的真实 IP 。当然也就产生了各位所说的伪造,毕竟 HEAD 除了协议里固定的那几个数据,其他数据都是可自定义的。


三、为何网上找到获取客户端 IP 的代码都要依次获取 HTTP_CLIENT_IP 、 HTTP_X_FORWARDED_FOR 和 REMOTE_ADDR
基于“一”和“二”以及对程序通用性的考虑,所以才这样做。 假设你在程序里直接写死了 REMOTE_ADDR ,有一天你们的程序需要做负载均衡了,那么你有得改了。当然如果你想这么做也行,看个人爱好和应用场景。也可以封装一个只有 REMOTE_ADDR 的方法,等到需要的时候改这一个方法就行了。


X_FORWARDED_FOR 与 X-Real-IP 的区别:

X_FORWARDED_FOR:记录了所有链路里的代理 IP 值,比如下面所说的,经过了 3 次代理,分别是 1.1.1.1, 2.2.2.2, 3.3.3.3,其中 1.1.1.1 是客户端 IP,2.2.2.2 是代理服务器,接下来同理。

X-Real-IP:是只会记录前一次代理的 IP 地址。

一般来说,X-Forwarded-For是用于记录代理信息的,每经过一级代理(匿名代理除外),代理服务器都会把这次请求的来源IP追加在X-Forwarded-For

来自4.4.4.4的一个请求,header 包含这样一行

X-Forwarded-For: 1.1.1.1, 2.2.2.2, 3.3.3.3

代表请求由1.1.1.1发出,经过三层代理,第一层是2.2.2.2,第二层是3.3.3.3,而本次请求的来源IP4.4.4.4是第三层代理

X-Real-IP,没有相关标准,上面的例子,如果配置了X-Read-IP,可能会有两种情况

// 最后一跳是正向代理,可能会保留真实客户端IP
X-Real-IP: 1.1.1.1
// 最后一跳是反向代理,比如Nginx,一般会是与之直接连接的客户端IP
X-Real-IP: 3.3.3.3

所以 ,如果只有一层代理,这两个头的值就是一样的。

那一般在后端取值(比如 node.js 通过 nginx 代理)是用哪个值呢?我看 sf 上看一般推荐是用 X-Forwarded-For,直接用 X-Real-IP 岂不是更方便点?

X-Forwarded-For 确实是一般的做法

  1. 他在正向(如 squid)反向(如 nginx)代理中都是标准用法,而正向代理中是没有 x-real-ip 相关的标准的,也就是说,如果用户访问你的 nginx 反向代理之前,还经过了一层正向代理,你即使在 nginx 中配置了 x-real-ip,取到的也只是正向代理的 IP 而不是客户端真实 IP

  2. 大部分 nginx 反向代理配置文章中都没有推荐加上 x-real-ip ,而只有 x-forwarded-for,因此更通用的做法自然是取 x-forwarded-for

  3. 多级代理很少见,只有一级代理的情况下二者是等效的

  4. 如果有多级代理,x-forwarded-for 效果是大于 x-real-ip 的,可以记录完整的代理链路


1、将以下代码保存为 Client.php

// php 脚本开始
$ch = curl_init();
$url = "http://localhost/ser.php";
$header = array('CLIENT-IP:208.165.188.175', 'X-FORWARDED-FOR:208.165.188.175');
// 声明伪造 head 请求头
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
$page_content = curl_exec($ch); curl_close($ch);
echo $page_content;


2、将以下代码保存为 ser.php

// php 脚本开始
echo getenv('HTTP_CLIENT_IP');
echo getenv('HTTP_X_FORWARDED_FOR');
echo getenv('REMOTE_ADDR');


测试结果为

208.165.188.175
208.165.188.175
127.0.0.1


上面结果可看出,http_client_ip、http_x_forwarded_for 都被伪造了,而 remote_addr 还是127.0.0.1 就是客户端 IP

总结:

如果我们没用到负载均衡(CDN)的话直接用 REMOTE_ADDR 获取 IP。

如果使用了一级 CDN 的话,CDN 会把 REMOTE_ADDR 转发成 X-Real-IP,我们服务器可以获取 X-Real-IP 来获取 IP 值。如果是多级 CDN 的话需要我们来做 nginx 代理,详细:https://www.cnblogs.com/princessd8251/articles/6268943.html

就是第一台负载服务器获取 REMOTE_ADDR 转发成 X-Real-IP,之后的去继承前一台的 X-Real-IP 就可以了,这里记住必须是去继承,不然第二台会把第一台的 REMOTE_ADDR 转发成 X-Real-IP 导致错误。

在 discuz! 中的获取 IP 的方法:

private function _get_client_ip() {
    $ip = $_SERVER['REMOTE_ADDR'];
    if (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    } elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
        foreach ($matches[0] AS $xip) {
            if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
                $ip = $xip;
                break;
            }
        }
    }
    return $ip;
}

这里 DZ 为了符合有些用户会用代理,所以才首先使用了两个容易伪造的方法,如果有需要可以自行修改。

h
转自 hykeda 5 年前
6,493

前提:

若需获取用户 unionid,则小程序必须已绑定到微信开放平台。


  1. 小程序调用 wx.login(),将 code 发送到开发者服务器

  2. 开发者服务器请求微信接口 code2Session,获得 session_keyopenid,有机会获得 unionid参 UnionID 机制说明)。因此无需任何用户授权即可获取 openid

  3. 访问须要已获取用户信息的页面时,可通过小程序端使用 button + getUserInfo 的方式请求用户授权

  4. 访问须要已获取手机号码的页面时,可通过小程序端使用 button + getPhoneNumber 的方式请求用户授权

  5. code2session、getUserInfo、getPhoneNumber 等接口返回数据存入数据库 dt_weapp_user

  6. 服务端的任何接口均返回口令状态包(含自定义会话口令等信息),小程序端应保存于 storage

  7. 请求 getUserInfo 后应判断 detail.errMsg 是否为 getUserInfo:ok,请求 getPhoneNumber 后应判断 detail.errMsg 是否为 getPhoneNumber:ok

  8. 授权窗口的弹出情况此文

  9. 口令状态包是用来传递到客户端用来标识用户唯一凭证的,包含字段:会话口令(自定义用户凭证)、会话口令有效时间(秒)、是否已获取 unionid 或昵称(视业务需求)、是否已获取手机号码uid(可选),以及其他用来标识业务身份字段。服务端可根据会话口令从数据库中获取该用户的所有已知信息。

    一般情况下,当已获取手机号码时,uid 必有值,否则,uid 必为空。因此 uid 不是必须传递到客户端的。

    通过 code2Session 获得的 session_key、以及 openidunionid手机号码等机密信息不允许包含在口令状态包中并缓存在客户端,客户端需要用到手机号码时单独调用接口获取。

  10. 若业务不要求以手机号码作为用户唯一标识,那么口令状态包中不需要包含是否已获取手机号码,仅 uid 即可。在客户端访问须要业务用户登录的页面时,也仅判断 uid 有值即可。


流程图:


微信小程序授权登录获取 unionid、用户信息流程.png


为更好地适应 2017.7.19 发布的《小程序内用户帐号登录规范调整和优化建议》开发者服务器提供的所有业务逻辑接口的返回值中都应包含口令状态包,约定有以下相关的返回值和处理动作(返回值按个人喜好来定):

口令状态接口返回ASP.NET Web API 语句处理动作
access_token 空或失效401 状态码return Unauthorized();login() + code2Session,获取 openid,生成 access_token
access_token 有效,但未获取 uniond 或用户信息401 状态码,http 头 WWW-Authenticate 中标注 getUserInforeturn Unauthorized(new AuthenticationHeaderValue("getUserInfo"));getUserInfo()
access_token 有效,但未获取手机号码(即未找到业务用户)401 状态码,http 头 WWW-Authenticate 中标注 getPhoneNumberreturn Unauthorized(new AuthenticationHeaderValue("getPhoneNumber"));getPhoneNumber()
xoyozo 6 年前
8,628

第一种方式,直接执行上一页的原生方法:

var pages = getCurrentPages();
if (pages.length > 1) {
	var prePage = pages[pages.length - 2];
	prePage.onPullDownRefresh();
}

第二种方式,在上一页的 onShow() 中对数据进行更新。

推荐使用第一种方式,但只能调用原生方法,不能直接调用 methods 中的自定义方法(详见)。第二种方式的缺点是,有时候不需要在返回上一页时更新数据,但它仍会执行。

xoyozo 6 年前
2,589

2019/2/16 补充:参见 JavaScript 中的 TruthyFalsy

使用以下代码对字符串 s 进行判断:

var s = null;
if (s) {
    console.log('true');
} else {
    console.log('false');
}

测试当 s 为不同值时的结果如下:

s 的值if(s) 的结果
s = null;false
s = undefined;false
s = '';false
s = "";false
s = 0;false
s = 1;
true
s = 2;
true
s = -1;
true
s = '0';
true
s= ' ';true

结论:

s 未定义或未赋值时,结果为 false

s 为字符串类型时,长度为 0 即 false,长度大于 0 即 true

s 为数字类型时,等于 0 为 false,不等于 0 为 true


如果判断 s 的长度:

var s = null;
if (s.length > 0) {
    console.log('true');
} else {
    console.log('false');
}

那么会有以下结果:

s 的值if(s.length > 0) 的结果
s = null;Uncaught TypeError
s = undefined;Uncaught TypeError
s = '';false
s = "";false
s = 0;false
s = 1;
false
s = 2;
false
s = -1;
false
s = '0';
true
s= ' ';true

结论:

s 未定义或未赋值时,异常;

s 为字符串类型时,长度为 0 即 false,长度大于 0 即 true

s 为数字类型时,永远为 false

这样的话,if(s.length > 0) 等同于 if(s.length)


综上所述,判断一个字符串是否“有值且长度大于 0 ”,可以这样写:

var s = null;
if (s && s.length) {
    console.log('true');
} else {
    console.log('false');
}

相反,是否“无值或长度为 0”,可以这样写:

var s = null;
if (!s || !s.length) {
    console.log('true');
} else {
    console.log('false');
}

当然,如果要将不为 0 的数字类型也视为“有值”,只要 if(s) 或 if(!s) 即可。


xoyozo 6 年前
7,690

TIM图片20181126142732.png

uni-app 的 input,当 type = "text" 时,设置 confirm-type = "next" 可以提供键盘的“下一项”,通过 @confirm 触发设置下一个文本框的焦点,即可实现只操作键盘即可完成表单填写。

我们以从 a 组件跳转到 b 组件为例:

<input v-model="a" type="text" confirm-type="next" @confirm="moveNext('b')" />

uni-app 的组件只有属性和事件,不提供方法,那么我们只能设置提供了 focus 属性的组件的焦点。

<input v-model="b" :focus="focusList['b']" />

moveNext() 方法实现:

moveNext(dom){
	this.focusList[dom] = true;
}

当然 focusList 必须先定义并初始化,否则设置 focus 为 true 将失效:

data() {
	return {
		a: '',
		b: '',
		focusList: {
			b: false
		}
	}
}

几个注意点:

  • confirm-type = "next" 仅支持微信小程序

  • “下一项”按钮效果是一次性的,使用一次后再次在 a 上触摸“下一项”将不再跳转到 b,即使在 moveNext() 中先设置为 false 再设置为 true 也没用

  • 其它组件也可以通过自带的方法实现跳转到带 focus 属性的组件(如在 picker 的 @change 中设置 this.focusList['b'] = true)

  • 限制于 confirm-type = "next" 只当 type = "text" 时有效,因此非文本键盘(如数字键盘)就不能提供“下一项”功能

  • textarea 组件的右下角始终是“换行”,由其右上角的“完成”按钮来实现“下一项”功能

  • textarea 组件在“下一项”跳转过程中,组件可能会被 tabBar 遮挡(几率很大)

xoyozo 6 年前
9,048

本文作为 多平台用户登录模块设计 的扩展设计,即以手机号作为用户的唯一凭证。


官方文档的小程序登录时序:
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html



(图片摘自 2018.10.30)


session_key 是密钥,仅保存于开发者服务器,用于将小程序通过前端接口获取到的数据解密来验证其真实性。详见 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html


小程序与服务器是通过自定义登录态来识别用户身份的,以下简称口令(token)。


由于微信未告知 session_key 的有效期,那么为保证小程序发起业务请求成功,token 须要永久有效,但这会带来安全隐患。

如果设置 token 的有效期(如 7200 秒),那么小程序发起业务请求后,服务器必须把 token 的验证结果告知小程序,若失效则重新登录。


微信授权登录(含绑定手机号码)流程图

1.png


子流程:授权登录

2.png


子流程:绑定手机号

2.png


为保证数据安全,针对每个须要授权登录的业务请求,服务器都都会检验 token 的有效性。如果小程序同时发起多个业务请求,并几乎同时收到 token 过期,那么会同时发起多个重新登录流程,服务器多次 code2Session,重新生成多个 token 返回到小程序,那么小程序最终保存的 token 可能不是服务器上认为的最新的一个 token。这样,如果程序设计为获取到 token 继而重新发起业务请求,可能会进入死循环。解决的方法是在同时发起多个业务请求之前先向服务器验证一次 token 的有效性,再发起多个业务请求时就不会出现都过期的情况了。

查看详细的短信验证码登录或绑定流程

xoyozo 6 年前
16,743