feat:会员注册时通过API获取可用的验证方式、会员注册验证邮件实现

This commit is contained in:
妙码生花 2022-07-14 19:45:46 +08:00
parent b168debf42
commit 952208934f
8 changed files with 201 additions and 53 deletions

View File

@ -14,10 +14,11 @@ use think\exception\ValidateException;
use app\api\validate\Account as AccountValidate;
use app\common\library\Email;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
use app\api\validate\User as UserValidate;
class Account extends Frontend
{
protected $noNeedLogin = ['sendRetrievePasswordCode', 'retrievePassword'];
protected $noNeedLogin = ['sendRetrievePasswordCode', 'sendRegisterCode', 'retrievePassword'];
protected $model = null;
@ -131,6 +132,44 @@ class Account extends Frontend
]);
}
public function sendRegisterCode()
{
$data = $this->request->only(['registerType', 'email', 'mobile', 'username', 'password']);
$validate = new UserValidate();
try {
$validate->scene('send-register-code')->check($data);
} catch (ValidateException $e) {
$this->error($e->getMessage());
}
// 生成一个验证码
$captcha = new Captcha();
$account = $data['registerType'] == 'email' ? $data['email'] : $data['mobile'];
$code = $captcha->create($account);
if ($data['registerType'] == 'email') {
$mail = new Email();
if (!$mail->configured) {
$this->error(__('Mail sending service unavailable'));
}
try {
$mail->isSMTP();
$mail->addAddress($account);
$mail->isHTML(true);
$mail->setSubject(__('Member registration verification') . '-' . get_sys_config('site_name'));
$mail->Body = __('Your verification code is: %s', [$code]);
$mail->send();
} catch (PHPMailerException $e) {
$this->error($mail->ErrorInfo);
}
$this->success(__('Mail sent successfully~'));
} else {
$this->error(__('Unknown operation'));
}
}
public function sendRetrievePasswordCode()
{
$data = $this->request->only(['type', 'account']);

View File

@ -52,7 +52,7 @@ class User extends Frontend
}
if ($this->request->isPost()) {
$params = $this->request->post(['tab', 'email', 'mobile', 'username', 'password', 'keep', 'captcha', 'captchaId']);
$params = $this->request->post(['tab', 'email', 'mobile', 'username', 'password', 'keep', 'captcha', 'captchaId', 'registerType']);
if ($params['tab'] != 'login' && $params['tab'] != 'register') {
$this->error(__('Unknown operation'));
}
@ -63,15 +63,17 @@ class User extends Frontend
} catch (ValidateException $e) {
$this->error($e->getMessage());
}
$captchaObj = new Captcha();
if (!$captchaObj->check($params['captcha'], $params['captchaId'])) {
$this->error(__('Please enter the correct verification code'));
}
if ($params['tab'] == 'login') {
if (!$captchaObj->check($params['captcha'], $params['captchaId'])) {
$this->error(__('Please enter the correct verification code'));
}
$res = $this->auth->login($params['username'], $params['password'], (bool)$params['keep']);
} elseif ($params['tab'] == 'register') {
if (!$captchaObj->check($params['captcha'], $params['registerType'] == 'email' ? $params['email'] : $params['mobile'])) {
$this->error(__('Please enter the correct verification code'));
}
$res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email']);
}

View File

@ -1,11 +1,13 @@
<?php
return [
'avatar' => '头像',
'username' => '用户名',
'nickname' => '昵称',
'birthday' => '生日',
'email' => '电子邮箱',
'mobile' => '手机号',
'password' => '密码',
'captcha' => '验证码',
'Old password error' => '旧密码错误',
'Data updated successfully~' => '资料更新成功~',
'Please input correct password' => '请输入正确的密码',
@ -13,9 +15,12 @@ return [
'Password has been changed~' => '密码已修改~',
'Password has been changed, please login again~' => '密码已修改,请重新登录~',
'Retrieve password verification' => '找回密码验证',
'Member registration verification' => '会员注册验证',
'Your verification code is: %s' => '您的验证码是:%s十分钟内有效~',
'Mail sent successfully~' => '邮件发送成功~',
'Account does not exist~' => '账户不存在~',
'Failed to modify password, please try again later~' => '修改密码失败,请稍后重试~',
'Please enter the correct verification code' => '请输入正确的验证码',
'%s has been registered' => '%s已被注册请直接登录~',
'Mail sending service unavailable' => '邮件发送服务不可用,请联系网站管理员进行配置~',
];

View File

@ -10,8 +10,8 @@ class User extends Validate
protected $rule = [
'username' => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
'email' => 'require|email|unique:user',
'mobile' => 'require|mobile|unique:user',
'email' => 'email|unique:user',
'mobile' => 'mobile|unique:user',
'password' => 'require|regex:^[a-zA-Z0-9_]{6,32}$',
'captcha' => 'require',
'captchaId' => 'require',
@ -21,8 +21,9 @@ class User extends Validate
* 验证场景
*/
protected $scene = [
'login' => ['password', 'captcha', 'captchaId'],
'register' => ['email', 'username', 'password', 'mobile', 'captcha', 'captchaId'],
'login' => ['password', 'captcha', 'captchaId'],
'register' => ['email', 'username', 'password', 'mobile', 'captcha'],
'send-register-code' => ['email', 'username', 'password', 'mobile'],
];
public function __construct()

View File

@ -12,6 +12,11 @@ use PHPMailer\PHPMailer\Exception;
*/
class Email extends PHPMailer
{
/**
* 是否已在管理后台配置好邮件服务
*/
public $configured = false;
/**
* 默认配置
*/
@ -37,14 +42,14 @@ class Email extends PHPMailer
$this->setLanguage($this->options['lang'], root_path() . 'vendor' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR);
$this->CharSet = $this->options['charset'];
$sysMailConfig = get_sys_config('', 'mail');
$configured = true;
$sysMailConfig = get_sys_config('', 'mail');
$this->configured = true;
foreach ($sysMailConfig as $item) {
if (!$item) {
$configured = false;
$this->configured = false;
}
}
if ($configured) {
if ($this->configured) {
$this->Host = $sysMailConfig['smtp_server'];
$this->SMTPAuth = true;
$this->Username = $sysMailConfig['smtp_user'];

View File

@ -100,6 +100,19 @@ export function sendRetrievePasswordCode(type: string, account: string): ApiProm
) as ApiPromise
}
export function sendRegisterCode(params: anyObj): ApiPromise {
return createAxios(
{
url: accountUrl + 'sendRegisterCode',
method: 'POST',
data: params,
},
{
showSuccessMessage: true,
}
) as ApiPromise
}
export function retrievePassword(params: anyObj): ApiPromise {
return createAxios(
{

View File

@ -32,6 +32,7 @@ export default {
'No account yet? Click Register': '还没有账户?点击注册',
'Retrieve password': '找回密码',
'Retrieval method': '找回方式',
'Registration verification method': '验证方式',
'Via email': '通过邮箱',
'Via mobile number': '通过手机号',
'New password': '新密码',

View File

@ -10,18 +10,24 @@
{{ t('user.user.' + state.form.tab) + t('user.user.reach') + siteConfig.site_name }}
</div>
<el-form ref="formRef" @keyup.enter="onSubmit(formRef)" :rules="rules" :model="state.form">
<!-- 注册邮箱 -->
<el-form-item v-if="state.form.tab == 'register'" prop="email">
<el-input
v-model="state.form.email"
:placeholder="t('Please input field', { field: t('user.user.mailbox') })"
:clearable="true"
size="large"
>
<template #prefix>
<Icon name="fa fa-envelope" size="16" color="var(--el-input-icon-color)" />
</template>
</el-input>
<!-- 注册验证方式 -->
<el-form-item v-if="state.form.tab == 'register'">
<el-radio-group size="large" v-model="state.form.registerType">
<el-radio
class="register-verification-radio"
label="email"
:disabled="!state.accountVerificationType.includes('email')"
border
>{{ t('user.user.Via email') + t('user.user.register') }}</el-radio
>
<el-radio
class="register-verification-radio"
label="mobile"
:disabled="!state.accountVerificationType.includes('mobile')"
border
>{{ t('user.user.Via mobile number') + t('user.user.register') }}</el-radio
>
</el-radio-group>
</el-form-item>
<!-- 登录注册用户名 -->
@ -57,22 +63,8 @@
</el-input>
</el-form-item>
<!-- 注册手机号 -->
<el-form-item v-if="state.form.tab == 'register'" prop="mobile">
<el-input
v-model="state.form.mobile"
:placeholder="t('Please input field', { field: t('user.user.mobile') })"
:clearable="true"
size="large"
>
<template #prefix>
<Icon name="fa fa-tablet" size="16" color="var(--el-input-icon-color)" />
</template>
</el-input>
</el-form-item>
<!-- 登录注册验证码 -->
<el-form-item prop="captcha">
<!-- 登录验证码 -->
<el-form-item v-if="state.form.tab == 'login'" prop="captcha">
<el-row class="w100">
<el-col :span="16">
<el-input
@ -98,6 +90,66 @@
</el-row>
</el-form-item>
<!-- 注册手机号 -->
<el-form-item v-if="state.form.tab == 'register' && state.form.registerType == 'mobile'" prop="mobile">
<el-input
v-model="state.form.mobile"
:placeholder="t('Please input field', { field: t('user.user.mobile') })"
:clearable="true"
size="large"
>
<template #prefix>
<Icon name="fa fa-tablet" size="16" color="var(--el-input-icon-color)" />
</template>
</el-input>
</el-form-item>
<!-- 注册邮箱 -->
<el-form-item v-if="state.form.tab == 'register' && state.form.registerType == 'email'" prop="email">
<el-input
v-model="state.form.email"
:placeholder="t('Please input field', { field: t('user.user.mailbox') })"
:clearable="true"
size="large"
>
<template #prefix>
<Icon name="fa fa-envelope" size="16" color="var(--el-input-icon-color)" />
</template>
</el-input>
</el-form-item>
<!-- 注册验证码 -->
<el-form-item v-if="state.form.tab == 'register'" prop="captcha">
<el-row class="w100">
<el-col :span="16">
<el-input
size="large"
v-model="state.form.captcha"
:placeholder="t('Please input field', { field: t('user.user.Verification Code') })"
autocomplete="off"
>
<template #prefix>
<Icon name="fa fa-ellipsis-h" size="16" color="var(--el-input-icon-color)" />
</template>
</el-input>
</el-col>
<el-col class="captcha-box" :span="8">
<el-button
size="large"
@click="sendRegisterCaptcha(formRef)"
:loading="state.sendCaptchaLoading"
:disabled="state.codeSendCountdown <= 0 ? false : true"
type="primary"
>{{
state.codeSendCountdown <= 0
? t('user.user.send')
: state.codeSendCountdown + t('user.user.seconds')
}}</el-button
>
</el-col>
</el-row>
</el-form-item>
<div v-if="state.form.tab != 'register'" class="form-footer">
<el-checkbox v-model="state.form.keep" :label="t('user.user.Remember me')" size="default"></el-checkbox>
<div
@ -190,7 +242,7 @@
<el-col class="captcha-box" :span="8">
<el-button
@click="sendRetrieveCaptcha(retrieveFormRef)"
:loading="state.sendRetrieveCaptchaLoading"
:loading="state.sendCaptchaLoading"
:disabled="state.codeSendCountdown <= 0 ? false : true"
type="primary"
>{{
@ -232,14 +284,16 @@ import { buildCaptchaUrl } from '/@/api/common'
import { uuid } from '/@/utils/random'
import { useI18n } from 'vue-i18n'
import { buildValidatorData, validatorAccount } from '/@/utils/validate'
import { checkIn, sendRetrievePasswordCode, retrievePassword } from '/@/api/frontend/user/index'
import { checkIn, sendRetrievePasswordCode, retrievePassword, sendRegisterCode } from '/@/api/frontend/user/index'
import { onResetForm } from '/@/utils/common'
import { useUserInfo } from '/@/stores/userInfo'
import { useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import type { ElForm, FormItemRule } from 'element-plus'
var timer: NodeJS.Timer
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const userInfo = useUserInfo()
const siteConfig = useSiteConfig()
@ -256,6 +310,7 @@ interface State {
captcha: string
keep: boolean
captchaId: string
registerType: 'email' | 'mobile'
}
formLoading: boolean
showCaptcha: boolean
@ -270,7 +325,7 @@ interface State {
accountVerificationType: string[]
codeSendCountdown: number
submitRetrieveLoading: boolean
sendRetrieveCaptchaLoading: boolean
sendCaptchaLoading: boolean
}
const state: State = reactive({
@ -283,6 +338,7 @@ const state: State = reactive({
captcha: '',
keep: false,
captchaId: uuid(),
registerType: 'email',
},
formLoading: false,
showCaptcha: false,
@ -297,7 +353,7 @@ const state: State = reactive({
accountVerificationType: [],
codeSendCountdown: 0,
submitRetrieveLoading: false,
sendRetrieveCaptchaLoading: false,
sendCaptchaLoading: false,
})
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
@ -382,20 +438,40 @@ const onSubmitRetrieve = (formRef: InstanceType<typeof ElForm> | undefined = und
}
})
}
const sendRetrieveCaptcha = (formRef: InstanceType<typeof ElForm> | undefined = undefined) => {
const sendRegisterCaptcha = (formRef: InstanceType<typeof ElForm> | undefined = undefined) => {
if (state.codeSendCountdown > 0) return
formRef!.validateField('account').then((valid) => {
formRef!.validateField([state.form.registerType, 'username', 'password']).then((valid) => {
if (valid) {
state.sendRetrieveCaptchaLoading = true
sendRetrievePasswordCode(state.retrievePasswordForm.type, state.retrievePasswordForm.account)
state.sendCaptchaLoading = true
sendRegisterCode(state.form)
.then((res) => {
state.sendRetrieveCaptchaLoading = false
state.sendCaptchaLoading = false
if (res.code == 1) {
startTiming(60)
}
})
.catch(() => {
state.sendRetrieveCaptchaLoading = false
state.sendCaptchaLoading = false
})
}
})
}
const sendRetrieveCaptcha = (formRef: InstanceType<typeof ElForm> | undefined = undefined) => {
if (state.codeSendCountdown > 0) return
formRef!.validateField('account').then((valid) => {
if (valid) {
state.sendCaptchaLoading = true
sendRetrievePasswordCode(state.retrievePasswordForm.type, state.retrievePasswordForm.account)
.then((res) => {
state.sendCaptchaLoading = false
if (res.code == 1) {
startTiming(60)
}
})
.catch(() => {
state.sendCaptchaLoading = false
})
}
})
@ -423,6 +499,8 @@ onMounted(() => {
state.accountVerificationType = res.data.accountVerificationType
state.retrievePasswordForm.type = res.data.accountVerificationType.length > 0 ? res.data.accountVerificationType[0] : ''
})
if (route.query.type == 'register') state.form.tab = 'register'
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
@ -457,16 +535,20 @@ onUnmounted(() => {
margin-left: 0;
}
}
.register-verification-radio {
margin-top: 10px;
}
.captcha-box {
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-end;
.captcha-img {
width: 90%;
margin-left: auto;
}
.el-button {
width: 90%;
height: 100%;
}
}
.form-footer {