feat: 邮箱验证码注册+登录协议勾选

- 后端: VerificationCode模型+send-code API+register增加verifyCode校验
- 前端: 注册表单增加验证码输入框+60s倒计时获取按钮
- 前端: 登录表单增加用户协议/隐私政策勾选
- 前端: 登录按钮增加disabled状态(未勾选时不可点击)
- 后端: nodemailer邮件发送工具(可配置SMTP,未配置时返回devCode)
This commit is contained in:
Yuzhiran Dev
2026-07-12 16:02:57 +08:00
parent dd5c1bc7ac
commit 23de8bc86a
12 changed files with 626 additions and 253 deletions
+119 -16
View File
@@ -1,10 +1,11 @@
const express = require('express')
const router = express.Router()
const jwt = require('jsonwebtoken')
const { User, Admin, AdminLog, LoginLog } = require('../models')
const { User, Admin, AdminLog, LoginLog, VerificationCode } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const logger = require('../utils/logger')
const { sendVerificationCode } = require('../utils/mail')
const axios = require('axios')
/**
@@ -312,10 +313,17 @@ router.post('/admin/login', async (req, res) => {
*/
router.post('/user/login', async (req, res) => {
try {
const { username, password } = req.body
const { username, email, password } = req.body
// 支持邮箱或用户名登录
const identifier = email || username
// 查找用户(显式包含密码字段)
const user = await User.findOne({ username }).select('+password')
const user = await User.findOne({
$or: [
{ email: identifier },
{ username: identifier }
]
}).select('+password')
if (!user) {
return ApiResponse.error(res, '用户名或密码错误', 401)
}
@@ -349,28 +357,123 @@ router.post('/user/login', async (req, res) => {
}, '登录成功')
} catch (error) {
logger.error('用户登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
logger.error('用户登录失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户注册
* POST /api/auth/user/register
*/
/**
* 发送邮箱验证码
* POST /api/auth/user/send-code
*/
router.post('/user/send-code', async (req, res) => {
try {
const { email } = req.body
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return ApiResponse.error(res, '邮箱格式不正确', 400)
}
// 检查邮箱是否已被注册
const existing = await User.findOne({ email })
if (existing) {
return ApiResponse.error(res, '该邮箱已被注册', 400)
}
// 检查发送频率(1分钟内只能发一次)
const recent = await VerificationCode.findOne({
email,
createdAt: { $gte: new Date(Date.now() - 60000) }
})
if (recent) {
return ApiResponse.error(res, '发送过于频繁,请 1 分钟后再试', 429)
}
// 生成6位随机验证码
const code = String(Math.floor(100000 + Math.random() * 900000))
// 10分钟过期
const expiresAt = new Date(Date.now() + 10 * 60 * 1000)
// 使之前的验证码失效
await VerificationCode.updateMany({ email, used: false }, { $set: { used: true } })
// 保存新验证码
await VerificationCode.create({ email, code, expiresAt })
// 发送邮件
const result = await sendVerificationCode(email, code)
if (result.success) {
return ApiResponse.success(res, {
devCode: result.devCode || undefined
}, '验证码已发送')
} else {
return ApiResponse.error(res, '验证码发送失败,请稍后重试', 500)
}
} catch (error) {
logger.error('发送验证码失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* H5环境用户注册(需邮箱验证码)
* POST /api/auth/user/register
*/
router.post('/user/register', async (req, res) => {
try {
const { username, password, nickName, avatarUrl } = req.body
const { username, email, password, nickName, avatarUrl, verifyCode } = req.body
const identifier = email || username
if (!identifier) {
return ApiResponse.error(res, '请输入邮箱或用户名', 400)
}
if (!email) {
return ApiResponse.error(res, '邮箱为必填项', 400)
}
// 验证邮箱验证码
if (!verifyCode) {
return ApiResponse.error(res, '请输入邮箱验证码', 400)
}
const validCode = await VerificationCode.findOne({
email,
code: verifyCode,
used: false,
expiresAt: { $gt: new Date() }
})
if (!validCode) {
return ApiResponse.error(res, '验证码无效或已过期', 400)
}
// 检查邮箱是否已存在
if (email) {
const existingEmail = await User.findOne({ email })
if (existingEmail) {
return ApiResponse.error(res, '该邮箱已被注册', 400)
}
}
// 检查用户名是否已存在
const existingUser = await User.findOne({ username })
if (existingUser) {
return ApiResponse.error(res, '用户名已存在', 400)
if (username) {
const existingUser = await User.findOne({ username })
if (existingUser) {
return ApiResponse.error(res, '用户名已存在', 400)
}
}
// 标记验证码为已使用
validCode.used = true
await validCode.save()
// 自动生成用户名(若未提供)
const finalUsername = username || email.split('@')[0]
// 创建新用户
const user = new User({
username,
username: finalUsername,
email: email || '',
password,
nickName: nickName || '星辰旅行者',
avatarUrl: avatarUrl || '',