feat: 改为邮箱验证码注册,注册完成后自动登入

后端改动:
- RegisterSerializer 改为接收用户名+邮箱+手机号+验证码
- 验证邮箱是否已存在、用户名是否已存在
- 验证验证码有效性和正确性
- RegisterView 返回 JWT token,实现自动登入

前端改动:
- RegisterView.vue 改为邮箱验证码注册流程
- 保留用户名、邮箱、手机号字段
- 获取验证码后输入验证码完成注册
- 注册成功后自动保存 token 并跳转到首页

流程:用户名+邮箱+手机号 → 获取验证码 → 输入验证码 → 注册完成并自动登入 → 跳转首页

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
TianyangZhang 2026-03-25 15:51:03 +08:00
parent 72e7244ea0
commit 99220b6daf
3 changed files with 253 additions and 28 deletions

View File

@ -5,15 +5,59 @@ from .models import VerificationCode
User = get_user_model()
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=6)
class RegisterSerializer(serializers.Serializer):
"""邮箱验证码注册 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:
model = User
fields = ['username', 'email', 'phone', 'password']
def validate_username(self, value):
"""验证用户名是否已存在"""
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):
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):

View File

@ -77,10 +77,32 @@ class CustomTokenObtainPairView(TokenObtainPairView):
return token
class RegisterView(generics.CreateAPIView):
serializer_class = RegisterSerializer
class RegisterView(APIView):
"""邮箱验证码注册"""
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):
permission_classes = [IsAuthenticated]

View File

@ -3,42 +3,201 @@
<el-card class="auth-card">
<h2>求职者注册</h2>
<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><el-input v-model="form.phone" placeholder="手机号" /></el-form-item>
<el-form-item><el-input v-model="form.password" type="password" placeholder="密码至少6位" show-password /></el-form-item>
<el-button type="primary" native-type="submit" :loading="loading" style="width:100%">注册</el-button>
<!-- 用户名邮箱手机号 -->
<el-form-item v-if="!codeSent">
<el-input
v-model="form.username"
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>
<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>
</el-card>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { register } from '@/api/auth'
import { useAuthStore } from '@/stores/auth'
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 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 {
await register(form)
ElMessage.success('注册成功,请登录')
router.push('/login')
} catch (e) {
ElMessage.error(e.response?.data?.username?.[0] || '注册失败,请重试')
await sendCode(form.email)
ElMessage.success('验证码已发送到您的邮箱')
codeSent.value = true
startCountdown()
} catch (err) {
ElMessage.error(err.response?.data?.email?.[0] || err.response?.data?.error || '发送失败')
} 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>
<style scoped>
.auth-page { display:flex; justify-content:center; align-items:center; min-height:60vh; }
.auth-card { width: 380px; }
.auth-page {
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>