优化发送验证码

新增手机验证码登录
pull/495/head
Karson 2025-04-30 17:24:55 +08:00
parent bdde403060
commit 04af358649
7 changed files with 220 additions and 37 deletions

View File

@ -271,7 +271,11 @@ return [
'usercenter' => true,
//会员注册验证码类型email/mobile/wechat/text/false
'user_register_captcha' => 'text',
//会员主页URL规则
//是否启用发送前验证码(用于短信和邮件发送)
'user_api_captcha' => false,
//会员登录默认类型,支持mobile和account
'user_login_type' => 'account',
//会员主页URL规则{uid}表示用户的ID
'user_home_url' => '/u/{uid}',
//是否启用会员字母头像
'user_letter_avatar' => true,

View File

@ -7,6 +7,7 @@ use app\common\controller\Frontend;
use app\common\library\Ems;
use app\common\library\Sms;
use app\common\model\Attachment;
use fast\Random;
use think\Config;
use think\Cookie;
use think\Hook;
@ -19,7 +20,7 @@ use think\Validate;
class User extends Frontend
{
protected $layout = 'default';
protected $noNeedLogin = ['login', 'register', 'third'];
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'third'];
protected $noNeedRight = ['*'];
public function _initialize()
@ -122,7 +123,8 @@ class User extends Frontend
$this->error(__($validate->getError()), null, ['token' => $this->request->token()]);
}
if ($this->auth->register($username, $password, $email, $mobile)) {
$this->success(__('Sign up successful'), $url ? $url : url('user/index'));
$this->auth->getUser()->save(['verification' => ['email' => $captchaType == 'email' ? 1 : 0, 'mobile' => $captchaType == 'mobile' ? 1 : 0]]);
$this->success(__('Sign up successful'), $url ?: url('user/index'));
} else {
$this->error($this->auth->getError(), null, ['token' => $this->request->token()]);
}
@ -132,6 +134,7 @@ class User extends Frontend
if (!$url && $referer && !preg_match("/(user\/login|user\/register|user\/logout)/i", $referer)) {
$url = $referer;
}
$this->view->assign('captchaType', config('fastadmin.user_register_captcha'));
$this->view->assign('url', $url);
$this->view->assign('title', __('Register'));
@ -175,7 +178,7 @@ class User extends Frontend
$this->error(__($validate->getError()), null, ['token' => $this->request->token()]);
}
if ($this->auth->login($account, $password)) {
$this->success(__('Logged in successful'), $url ? $url : url('user/index'));
$this->success(__('Logged in successful'), $url ?: url('user/index'), '', 0);
} else {
$this->error($this->auth->getError(), null, ['token' => $this->request->token()]);
}
@ -185,6 +188,57 @@ class User extends Frontend
if (!$url && $referer && !preg_match("/(user\/login|user\/register|user\/logout)/i", $referer)) {
$url = $referer;
}
$this->view->assign('loginType', config('fastadmin.user_login_type') ?? 'mobile');
$this->view->assign('loginAction', config('fastadmin.user_login_type') === 'mobile' ? url('user/mobilelogin') : url('user/login'));
$this->view->assign('url', $url);
$this->view->assign('title', __('Login'));
return $this->view->fetch();
}
/**
* 手机号验证码登录
*/
public function mobilelogin()
{
$url = $this->request->request('url', '', 'url_clean');
if ($this->request->isPost()) {
$mobile = $this->request->post('mobile');
$captcha = $this->request->post('smscode', $this->request->post('captcha'));
if (!$mobile || !$captcha) {
$this->error(__('Invalid parameters'));
}
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
if (!Sms::check($mobile, $captcha, 'mobilelogin')) {
$this->error(__('Captcha is incorrect'));
}
$user = \app\common\model\User::getByMobile($mobile);
if ($user) {
if ($user->status != 'normal') {
$this->error(__('Account is locked'));
}
//如果已经有账号则直接登录
$ret = $this->auth->direct($user->id);
} else {
$ret = $this->auth->register($mobile, Random::alnum(), '', $mobile, []);
//如果是手机号首次注册则直接设定为已验证
$this->auth->getUser()->save(['verification' => ['email' => 0, 'mobile' => 1]]);
}
if ($ret) {
Sms::flush($mobile, 'mobilelogin');
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $url);
} else {
$this->error($this->auth->getError());
}
}
//判断来源
$referer = $this->request->server('HTTP_REFERER');
if (!$url && (strtolower(parse_url($referer, PHP_URL_HOST)) == strtolower($this->request->host()))
&& !preg_match("/(user\/login|user\/register|user\/logout)/i", $referer)) {
$url = $referer;
}
$this->view->assign('url', $url);
$this->view->assign('title', __('Login'));
return $this->view->fetch();

View File

@ -1 +1,24 @@
{if $Think.config.fastadmin.user_api_captcha}
<script type="text/html" id="captchatpl">
<div class="p-4 form-section">
<form name="captcha-form" class="form-vertical" action="" method="post">
<div class="form-group mb-4">
<label class="control-label hidden">{:__('Captcha')}</label>
<div class="controls">
<div class="input-group">
<input type="text" name="captcha" class="form-control" data-rule="required;length({$Think.config.captcha.length})" />
<span class="input-group-btn" style="padding:0;border:none;">
<img src="{:captcha_src()}" width="107" height="32" class="captcha-img" onclick="this.src = '{:captcha_src()}?r=' + Math.random();"/>
</span>
</div>
<span class="msg-box n-right" style="left:0;top:33px;text-align: left;" for="captcha"></span>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" type="submit">{:__('Submit')}</button>
</div>
</form>
</div>
</script>
{/if}
<script src="__CDN__/assets/js/require{$Think.config.app_debug?'':'.min'}.js" data-main="__CDN__/assets/js/require-frontend{$Think.config.app_debug?'':'.min'}.js?v={$site.version|htmlentities}"></script>

View File

@ -1,19 +1,45 @@
<div id="content-container" class="container">
<div class="user-section login-section">
<div class="logon-tab clearfix"><a class="active">{:__('Sign in')}</a> <a href="{:url('user/register')}?url={$url|urlencode|htmlentities}">{:__('Sign up')}</a></div>
<div class="logon-tab clearfix">
<a class="active">{:__('Sign in')}</a>
{if config('fastadmin.user_register')}
<a href="{:url('user/register')}?url={$url|urlencode|htmlentities}">{:__('Sign up')}</a>
{/if}
</div>
<div class="login-main">
<form name="form" id="login-form" class="form-vertical" method="POST" action="">
<form name="form" id="login-form" class="form-vertical" method="POST" action="{$loginAction}">
<!--@IndexLoginFormBegin-->
<input type="hidden" name="url" value="{$url|htmlentities}"/>
{:token()}
<div class="form-group">
<div class="form-group {:$loginType==='mobile'?'':'hidden'}" data-login="mobile">
<label class="control-label">{:__('Mobile')}</label>
<div class="controls">
<input type="text" id="mobile" name="mobile" value="13211111111" data-rule="required;mobile" class="form-control" placeholder="{:__('Please enter your mobile phone number')}">
<p class="help-block"></p>
</div>
</div>
<div class="form-group {:$loginType==='mobile'?'':'hidden'}" data-login="mobile">
<label class="control-label">{:__('Captcha')}</label>
<div class="controls">
<div class="input-group">
<input type="text" name="captcha" class="form-control" placeholder="{:__('Please enter %s numbers', $Think.config.captcha.length)}" maxlength="{$Think.config.captcha.length}" data-rule="required;length({$Think.config.captcha.length});integer[+]"/>
<span class="input-group-btn" style="padding:0;border:none;">
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/sms/send')}" data-id="login" data-type="mobile" data-event="mobilelogin">{:__('Send verification code')}</a>
</span>
</div>
<p class="help-block"></p>
</div>
</div>
<div class="form-group {:$loginType==='account'?'':'hidden'}" data-login="account">
<label class="control-label" for="account">{:__('Account')}</label>
<div class="controls">
<input class="form-control" id="account" type="text" name="account" value="" data-rule="required" placeholder="{:__('Email/Mobile/Username')}" autocomplete="off">
<div class="help-block"></div>
</div>
</div>
<div class="form-group">
<div class="form-group {:$loginType==='account'?'':'hidden'}" data-login="account">
<label class="control-label" for="password">{:__('Password')}</label>
<div class="controls">
<input class="form-control" id="password" type="password" name="password" data-rule="required;password" placeholder="{:__('Password')}" autocomplete="off">
@ -26,12 +52,25 @@
<input type="checkbox" name="keeplogin" checked="checked" value="1"> {:__('Keep login')}
</label>
</div>
<div class="pull-right"><a href="javascript:;" class="btn-forgot">{:__('Forgot password')}</a></div>
<div class="pull-right {:$loginType==='account'?'':'hidden'}" data-login="account"><a href="javascript:;" class="btn-forgot">{:__('Forgot password')}</a></div>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-lg btn-block">{:__('Sign in')}</button>
<a href="{:url('user/register')}?url={$url|urlencode|htmlentities}" class="btn btn-default btn-lg btn-block mt-3 no-border">{:__("Don't have an account? Sign up")}</a>
<div class="row">
<div class="col-xs-6 text-left py-2">
<a href="javascript:" class="btn-switchlogin" data-type="{$loginType}"
data-account-action="{:url('user/login')}"
data-mobile-action="{:url('user/mobilelogin')}"
data-account-text='{:__("Sign in with account")}'
data-mobile-text='{:__("Sign in with mobile phone")}'>{:$loginType==='mobile'?__("Sign in with account"):__("Sign in with mobile phone")}</a>
</div>
{if config('fastadmin.user_register')}
<div class="col-xs-6 text-right py-2">
<a href="{:url('user/register')}?url={$url|urlencode|htmlentities}">{:__("Don\'t have an account? Sign up")}</a>
</div>
{/if}
</div>
</div>
<!--@IndexLoginFormEnd-->
</form>
@ -91,4 +130,4 @@
</div>
</div>
</form>
</script>
</script>

View File

@ -58,6 +58,67 @@ define(['jquery', 'bootstrap', 'toastr', 'layer', 'lang'], function ($, undefine
}
},
api: {
//发送验证码
sendcaptcha: function (btn, type, data, success, error) {
$(btn).addClass("disabled", true).text("发送中...");
var si = {};
Frontend.api.ajax({url: $(btn).data("url"), data: data}, function (data, ret) {
clearInterval(si[type]);
var seconds = 60;
si[type] = setInterval(function () {
seconds--;
if (seconds <= 0) {
clearInterval(si);
$(btn).removeClass("disabled").text("发送验证码");
} else {
$(btn).addClass("disabled").text(seconds + "秒后可再次发送");
}
}, 1000);
if (typeof success == 'function') {
success.call(this, data, ret);
}
}, function (data, ret) {
$(btn).removeClass("disabled").text('发送验证码');
if (typeof error == 'function') {
error.call(this, data, ret);
}
});
},
//准备验证码
preparecaptcha: function (btn, type, data) {
require(['form'], function (Form) {
Layer.open({
title: '请完成验证',
type: 1,
content: Template("captchatpl", {}),
offset: "130px",
btnAlign: 'c',
success: function (layero, index) {
var form = $("form", layero);
$("input[name=captcha]", form).focus();
form.data("validator-options", {
valid: function (ret) {
data.captcha = $("input[name=captcha]", form).val();
if(data.captcha.length < 4){
Toastr.error("验证码不正确");
$("input[name=captcha]", form).focus();
return false;
}
Frontend.api.sendcaptcha(btn, type, data, function (data, ret) {
Layer.close(index);
$(btn).closest("form").find("input[name='captcha']").focus();
}, function (data, ret) {
$("img.captcha-img", form).trigger("click");
});
return true;
}
})
Form.api.bindevent(form);
}
});
});
},
//发送Ajax请求
ajax: function (options, success, error) {
options = typeof options === 'string' ? {url: options} : options;

View File

@ -7,28 +7,6 @@ define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
$(document).on("click", ".btn-captcha", function (e) {
var type = $(this).data("type") ? $(this).data("type") : 'mobile';
var btn = this;
Frontend.api.sendcaptcha = function (btn, type, data, callback) {
$(btn).addClass("disabled", true).text("发送中...");
Frontend.api.ajax({url: $(btn).data("url"), data: data}, function (data, ret) {
clearInterval(si[type]);
var seconds = 60;
si[type] = setInterval(function () {
seconds--;
if (seconds <= 0) {
clearInterval(si);
$(btn).removeClass("disabled").text("发送验证码");
} else {
$(btn).addClass("disabled").text(seconds + "秒后可再次发送");
}
}, 1000);
if (typeof callback == 'function') {
callback.call(this, data, ret);
}
}, function () {
$(btn).removeClass("disabled").text('发送验证码');
});
};
if (['mobile', 'email'].indexOf(type) > -1) {
var element = $(this).data("input-id") ? $("#" + $(this).data("input-id")) : $("input[name='" + type + "']", $(this).closest("form"));
var text = type === 'email' ? '邮箱' : '手机号码';
@ -47,9 +25,13 @@ define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
}
element.isValid(function (v) {
if (v) {
var data = {event: $(btn).data("event")};
var data = {event: $(btn).data("event"), id: $(btn).data("id")};
data[type] = element.val();
Frontend.api.sendcaptcha(btn, type, data);
if ($("#captchatpl").length === 0) {
Frontend.api.sendcaptcha(btn, type, data);
} else {
Frontend.api.preparecaptcha(btn, type, data);
}
} else {
Layer.msg("请确认已经输入了正确的" + text + "");
}
@ -57,7 +39,13 @@ define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
} else {
var data = {event: $(btn).data("event")};
Frontend.api.sendcaptcha(btn, type, data, function (data, ret) {
Layer.open({title: false, area: ["400px", "430px"], content: "<img src='" + data.image + "' width='400' height='400' /><div class='text-center panel-title'>扫一扫关注公众号获取验证码</div>", type: 1});
Layer.open({
title: false,
area: [Math.min($(window).width(), 400) + "px", "430px"],
offset: "130px",
content: "<img src='" + data.image + "' width='400' height='400' /><div class='text-center panel-title'>扫一扫关注公众号获取验证码</div>",
type: 1
});
});
}
return false;

View File

@ -4,7 +4,8 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
$.each(errors, function (i, j) {
Layer.msg(j);
});
}
},
ignore: ':hidden'
};
var Controller = {
login: function () {
@ -26,6 +27,7 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
Layer.open({
type: 1,
title: __('Reset password'),
offset: "130px",
area: [$(window).width() < 450 ? ($(window).width() - 10) + "px" : "450px", "355px"],
content: content,
success: function (layero) {
@ -45,6 +47,18 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
}
});
});
//切换账号手机验证码登录
$(document).on("click", ".btn-switchlogin", function () {
var form = $(this).closest("form");
var current = $(this).data("type");
var type = current == 'mobile' ? 'account' : 'mobile';
var text = $(this).data(current + "-text");
$(this).text(text).data("type", type);
$("[data-login]").addClass("hidden");
$("[data-login='" + type + "']").removeClass("hidden");
form.attr("action", $(this).data(type + "-action"));
});
},
register: function () {
//本地验证未通过时提示