基于React与MongoDB的手机号OTP验证及账号流转方案咨询
React + MongoDB 账号流程实现方案
后端实现(Express + Mongoose)
前端无法直接操作MongoDB,必须通过后端接口完成数据交互,以下是基于Node.js+Express+Mongoose的核心实现:
1. 数据模型定义
创建用户模型,核心字段包含手机号(唯一索引)、OTP信息、邮箱及验证状态:
const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ phone: { type: String, required: true, unique: true, trim: true }, otp: { type: String, default: null }, otpExpiresAt: { type: Date, default: null }, email: { type: String, default: null, trim: true }, isEmailVerified: { type: Boolean, default: false }, isPhoneVerified: { type: Boolean, default: false } }); // 手机号唯一索引,防止重复创建 userSchema.index({ phone: 1 }, { unique: true }); module.exports = mongoose.model('User', userSchema);
2. 核心接口实现
(1)手机号校验接口
const User = require('../models/User'); app.post('/api/check-phone', async (req, res) => { try { const { phone } = req.body; const existingUser = await User.findOne({ phone }); if (existingUser) { return res.status(200).json({ exists: true }); } // 手机号不存在,创建初始用户记录(未验证状态) const newUser = await User.create({ phone }); res.status(201).json({ exists: false, userId: newUser._id.toString() }); } catch (err) { // 捕获唯一索引冲突(并发请求创建同一手机号) if (err.code === 11000) { return res.status(200).json({ exists: true }); } res.status(500).json({ error: '服务器错误' }); } });
(2)OTP生成与验证接口
const crypto = require('crypto'); // 发送OTP(5分钟过期) app.post('/api/send-otp', async (req, res) => { try { const { userId } = req.body; const otp = crypto.randomInt(100000, 999999).toString(); const otpExpiresAt = new Date(Date.now() + 5 * 60 * 1000); await User.findByIdAndUpdate(userId, { otp, otpExpiresAt }); // 调用第三方短信服务发送OTP(如阿里云/腾讯云短信API) // sendSms(phone, otp); res.status(200).json({ message: 'OTP已发送' }); } catch (err) { res.status(500).json({ error: '发送OTP失败' }); } }); // 验证OTP app.post('/api/verify-otp', async (req, res) => { try { const { userId, otp } = req.body; const user = await User.findById(userId); if (!user || user.otp !== otp || user.otpExpiresAt < new Date()) { return res.status(400).json({ error: 'OTP无效或已过期' }); } // 标记手机号已验证,清空OTP await User.findByIdAndUpdate(userId, { isPhoneVerified: true, otp: null, otpExpiresAt: null }); res.status(200).json({ success: true }); } catch (err) { res.status(500).json({ error: '验证OTP失败' }); } });
(3)邮箱验证接口
const jwt = require('jsonwebtoken'); // 发送邮箱验证链接 app.post('/api/send-email-verification', async (req, res) => { try { const { userId, email } = req.body; // 生成1小时有效期的验证token const token = jwt.sign({ userId, email }, process.env.JWT_SECRET, { expiresIn: '1h' }); const verifyLink = `http://your-domain.com/api/verify-email?token=${token}`; // 调用第三方邮件服务发送链接(如Nodemailer) // sendEmail(email, verifyLink); // 先更新邮箱到用户记录 await User.findByIdAndUpdate(userId, { email }); res.status(200).json({ message: '验证邮件已发送' }); } catch (err) { res.status(500).json({ error: '发送验证邮件失败' }); } }); // 验证邮箱链接 app.get('/api/verify-email', async (req, res) => { try { const { token } = req.query; const decoded = jwt.verify(token, process.env.JWT_SECRET); const { userId } = decoded; await User.findByIdAndUpdate(userId, { isEmailVerified: true }); // 跳转至前端注册页 res.redirect('/register'); } catch (err) { res.status(400).json({ error: '验证链接无效或已过期' }); } });
前端React实现(React Router v6)
1. 路由配置
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import PhoneCheckPage from './pages/PhoneCheckPage'; import LoginPage from './pages/LoginPage'; import OTPVerifyPage from './pages/OTPVerifyPage'; import EmailVerifyPage from './pages/EmailVerifyPage'; import RegisterPage from './pages/RegisterPage'; function App() { return ( <Router> <Routes> <Route path="/" element={<PhoneCheckPage />} /> <Route path="/login" element={<LoginPage />} /> <Route path="/verify-otp" element={<OTPVerifyPage />} /> <Route path="/verify-email" element={<EmailVerifyPage />} /> <Route path="/register" element={<RegisterPage />} /> <Route path="*" element={<Navigate to="/" />} /> </Routes> </Router> ); } export default App;
2. 手机号校验页面(核心入口)
import { useState } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; function PhoneCheckPage() { const [phone, setPhone] = useState(''); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); if (!phone.trim()) return; setLoading(true); try { const res = await axios.post('/api/check-phone', { phone }); if (res.data.exists) { navigate('/login'); } else { // 存储userId到localStorage,供后续页面使用 localStorage.setItem('userId', res.data.userId); navigate('/verify-otp'); } } catch (err) { alert('网络错误,请重试'); } finally { setLoading(false); } }; return ( <div className="phone-check"> <h2>输入手机号</h2> <form onSubmit={handleSubmit}> <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="请输入手机号" disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '校验中...' : '下一步'} </button> </form> </div> ); } export default PhoneCheckPage;
3. OTP验证页面
import { useState, useEffect } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; function OTPVerifyPage() { const [otp, setOtp] = useState(''); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const userId = localStorage.getItem('userId'); // 页面加载自动发送OTP useEffect(() => { if (!userId) { navigate('/'); return; } const sendOTP = async () => { try { await axios.post('/api/send-otp', { userId }); } catch (err) { alert('发送OTP失败,请重试'); } }; sendOTP(); }, [userId, navigate]); const handleVerify = async (e) => { e.preventDefault(); if (otp.length !== 6) return; setLoading(true); try { const res = await axios.post('/api/verify-otp', { userId, otp }); if (res.data.success) { navigate('/verify-email'); } } catch (err) { alert('OTP无效或已过期'); } finally { setLoading(false); } }; return ( <div className="otp-verify"> <h2>输入验证码</h2> <form onSubmit={handleVerify}> <input type="text" value={otp} onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))} maxLength={6} placeholder="6位验证码" disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '验证中...' : '验证'} </button> </form> </div> ); } export default OTPVerifyPage;
4. 邮箱验证页面
import { useState, useEffect } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; function EmailVerifyPage() { const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); const [sent, setSent] = useState(false); const navigate = useNavigate(); const userId = localStorage.getItem('userId'); useEffect(() => { if (!userId) navigate('/'); }, [userId, navigate]); const handleSendVerification = async (e) => { e.preventDefault(); if (!email.trim()) return; setLoading(true); try { await axios.post('/api/send-email-verification', { userId, email }); setSent(true); alert('验证邮件已发送,请查收'); } catch (err) { alert('发送邮件失败,请重试'); } finally { setLoading(false); } }; return ( <div className="email-verify"> {!sent ? ( <> <h2>输入邮箱</h2> <form onSubmit={handleSendVerification}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="请输入邮箱" disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '发送中...' : '发送验证邮件'} </button> </form> </> ) : ( <div> <h3>验证邮件已发送</h3> <p>请点击邮件中的链接完成验证,验证后将自动跳转至注册页</p> </div> )} </div> ); } export default EmailVerifyPage;
5. 注册页(补全用户信息)
import { useState, useEffect } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import bcrypt from 'bcryptjs'; function RegisterPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const userId = localStorage.getItem('userId'); useEffect(() => { if (!userId) navigate('/'); }, [userId, navigate]); const handleRegister = async (e) => { e.preventDefault(); if (!username.trim() || password.length < 6) return; setLoading(true); try { const hashedPassword = await bcrypt.hash(password, 10); await axios.put('/api/complete-profile', { userId, username, password: hashedPassword }); localStorage.removeItem('userId'); navigate('/login'); alert('注册成功'); } catch (err) { alert('注册失败,请重试'); } finally { setLoading(false); } }; return ( <div className="register"> <h2>完成注册</h2> <form onSubmit={handleRegister}> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="用户名" disabled={loading} /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码(至少6位)" disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '注册中...' : '完成注册'} </button> </form> </div> ); } export default RegisterPage;
内容的提问来源于stack exchange,提问作者Shrutik Dawane
相关产品推荐
相关产品推荐

