feat: 改为邮箱验证码注册,注册完成后自动登入
后端改动: - RegisterSerializer 改为接收用户名+邮箱+手机号+验证码 - 验证邮箱是否已存在、用户名是否已存在 - 验证验证码有效性和正确性 - RegisterView 返回 JWT token,实现自动登入 前端改动: - RegisterView.vue 改为邮箱验证码注册流程 - 保留用户名、邮箱、手机号字段 - 获取验证码后输入验证码完成注册 - 注册成功后自动保存 token 并跳转到首页 流程:用户名+邮箱+手机号 → 获取验证码 → 输入验证码 → 注册完成并自动登入 → 跳转首页 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
72e7244ea0
commit
99220b6daf
|
|
@ -5,15 +5,59 @@ from .models import VerificationCode
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class RegisterSerializer(serializers.ModelSerializer):
|
class RegisterSerializer(serializers.Serializer):
|
||||||
password = serializers.CharField(write_only=True, min_length=6)
|
"""邮箱验证码注册 serializer"""
|
||||||
|
username = serializers.CharField(max_length=150)
|
||||||
|
email = serializers.EmailField()
|
||||||
|
phone = serializers.CharField(max_length=20)
|
||||||
|
code = serializers.CharField(max_length=6, min_length=6)
|
||||||
|
|
||||||
class Meta:
|
def validate_username(self, value):
|
||||||
model = User
|
"""验证用户名是否已存在"""
|
||||||
fields = ['username', 'email', 'phone', 'password']
|
if User.objects.filter(username=value).exists():
|
||||||
|
raise serializers.ValidationError('用户名已存在')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_email(self, value):
|
||||||
|
"""验证邮箱是否已存在"""
|
||||||
|
if User.objects.filter(email=value).exists():
|
||||||
|
raise serializers.ValidationError('邮箱已被注册')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""验证邮箱和验证码"""
|
||||||
|
email = attrs.get('email')
|
||||||
|
code = attrs.get('code')
|
||||||
|
|
||||||
|
# 检查验证码
|
||||||
|
try:
|
||||||
|
vc = VerificationCode.objects.filter(email=email).latest('created_at')
|
||||||
|
except VerificationCode.DoesNotExist:
|
||||||
|
raise serializers.ValidationError({'code': '请先获取验证码'})
|
||||||
|
|
||||||
|
# 检查验证码是否有效
|
||||||
|
if not vc.is_valid():
|
||||||
|
raise serializers.ValidationError({'code': '验证码已过期或已使用'})
|
||||||
|
|
||||||
|
# 验证码是否正确
|
||||||
|
if vc.code != code:
|
||||||
|
vc.increment_attempts()
|
||||||
|
raise serializers.ValidationError({'code': '验证码错误'})
|
||||||
|
|
||||||
|
attrs['vc'] = vc
|
||||||
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
return User.objects.create_user(**validated_data, role='seeker')
|
"""创建用户并标记验证码为已使用"""
|
||||||
|
vc = validated_data.pop('vc')
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=validated_data['username'],
|
||||||
|
email=validated_data['email'],
|
||||||
|
phone=validated_data['phone'],
|
||||||
|
role='seeker'
|
||||||
|
)
|
||||||
|
vc.mark_as_verified()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,32 @@ class CustomTokenObtainPairView(TokenObtainPairView):
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
class RegisterView(generics.CreateAPIView):
|
class RegisterView(APIView):
|
||||||
serializer_class = RegisterSerializer
|
"""邮箱验证码注册"""
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = RegisterSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
# 创建用户
|
||||||
|
user = serializer.save()
|
||||||
|
|
||||||
|
# 生成 JWT token
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'user': {
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.username,
|
||||||
|
'email': user.email,
|
||||||
|
'role': user.role,
|
||||||
|
},
|
||||||
|
'refresh': str(refresh),
|
||||||
|
'access': str(refresh.access_token),
|
||||||
|
}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class MeView(APIView):
|
class MeView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
|
||||||
|
|
@ -3,42 +3,201 @@
|
||||||
<el-card class="auth-card">
|
<el-card class="auth-card">
|
||||||
<h2>求职者注册</h2>
|
<h2>求职者注册</h2>
|
||||||
<el-form :model="form" @submit.prevent="handleRegister">
|
<el-form :model="form" @submit.prevent="handleRegister">
|
||||||
<el-form-item><el-input v-model="form.username" placeholder="用户名" /></el-form-item>
|
<!-- 用户名、邮箱、手机号 -->
|
||||||
<el-form-item><el-input v-model="form.email" placeholder="邮箱" /></el-form-item>
|
<el-form-item v-if="!codeSent">
|
||||||
<el-form-item><el-input v-model="form.phone" placeholder="手机号" /></el-form-item>
|
<el-input
|
||||||
<el-form-item><el-input v-model="form.password" type="password" placeholder="密码(至少6位)" show-password /></el-form-item>
|
v-model="form.username"
|
||||||
<el-button type="primary" native-type="submit" :loading="loading" style="width:100%">注册</el-button>
|
placeholder="请输入用户名"
|
||||||
|
:disabled="codeSent"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="!codeSent">
|
||||||
|
<el-input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
:disabled="codeSent"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="!codeSent">
|
||||||
|
<el-input
|
||||||
|
v-model="form.phone"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
:disabled="codeSent"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 获取验证码按钮 -->
|
||||||
|
<el-form-item v-if="!codeSent">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="handleSendCode"
|
||||||
|
:loading="sendingCode"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
获取验证码
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 验证码输入 -->
|
||||||
|
<el-form-item v-if="codeSent">
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
placeholder="请输入验证码(6位)"
|
||||||
|
maxlength="6"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="handleResendCode"
|
||||||
|
:disabled="codeCountdown > 0"
|
||||||
|
>
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '重新获取' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 注册按钮 -->
|
||||||
|
<el-button
|
||||||
|
v-if="codeSent"
|
||||||
|
type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="registering"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div style="margin-top:12px;text-align:center">
|
|
||||||
已有账号?<router-link to="/login">立即登录</router-link>
|
<div style="margin-top: 12px; text-align: center; font-size: 12px;">
|
||||||
|
已有账号?<router-link to="/login" style="color: #409eff; text-decoration: none;">立即登录</router-link>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { register } from '@/api/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { sendCode } from '@/api/auth'
|
||||||
|
|
||||||
|
const form = reactive({ username: '', email: '', phone: '', code: '' })
|
||||||
|
const codeSent = ref(false)
|
||||||
|
const sendingCode = ref(false)
|
||||||
|
const registering = ref(false)
|
||||||
|
const codeCountdown = ref(0)
|
||||||
|
|
||||||
const form = reactive({ username: '', email: '', phone: '', password: '' })
|
|
||||||
const loading = ref(false)
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
async function handleRegister() {
|
// 倒计时逻辑
|
||||||
loading.value = true
|
const startCountdown = () => {
|
||||||
|
codeCountdown.value = 60
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
codeCountdown.value--
|
||||||
|
if (codeCountdown.value <= 0) clearInterval(interval)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
async function handleSendCode() {
|
||||||
|
if (!form.username) {
|
||||||
|
ElMessage.warning('请输入用户名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.email) {
|
||||||
|
ElMessage.warning('请输入邮箱')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.phone) {
|
||||||
|
ElMessage.warning('请输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendingCode.value = true
|
||||||
try {
|
try {
|
||||||
await register(form)
|
await sendCode(form.email)
|
||||||
ElMessage.success('注册成功,请登录')
|
ElMessage.success('验证码已发送到您的邮箱')
|
||||||
router.push('/login')
|
codeSent.value = true
|
||||||
} catch (e) {
|
startCountdown()
|
||||||
ElMessage.error(e.response?.data?.username?.[0] || '注册失败,请重试')
|
} catch (err) {
|
||||||
|
ElMessage.error(err.response?.data?.email?.[0] || err.response?.data?.error || '发送失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
sendingCode.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新发送验证码
|
||||||
|
async function handleResendCode() {
|
||||||
|
form.code = ''
|
||||||
|
await handleSendCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
async function handleRegister() {
|
||||||
|
if (!form.code || form.code.length !== 6) {
|
||||||
|
ElMessage.warning('请输入正确的6位验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registering.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: form.username,
|
||||||
|
email: form.email,
|
||||||
|
phone: form.phone,
|
||||||
|
code: form.code,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
const errorMsg = data.code?.[0] || data.username?.[0] || data.email?.[0] || data.phone?.[0] || '注册失败'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// 自动登入用户
|
||||||
|
localStorage.setItem('access_token', data.access)
|
||||||
|
localStorage.setItem('refresh_token', data.refresh)
|
||||||
|
await auth.fetchMe()
|
||||||
|
|
||||||
|
ElMessage.success('注册成功')
|
||||||
|
router.push('/home')
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('注册失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
registering.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.auth-page { display:flex; justify-content:center; align-items:center; min-height:60vh; }
|
.auth-page {
|
||||||
.auth-card { width: 380px; }
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue