2025-08-26 10:06:23 +08:00
|
|
|
|
const express = require('express')
|
|
|
|
|
|
const router = express.Router()
|
|
|
|
|
|
const { getDB } = require('../database')
|
|
|
|
|
|
const Dysmsapi20170525 = require('@alicloud/dysmsapi20170525')
|
|
|
|
|
|
const OpenApi = require('@alicloud/openapi-client')
|
|
|
|
|
|
const { Config } = require('@alicloud/openapi-client')
|
|
|
|
|
|
|
2025-08-28 09:14:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* tags:
|
|
|
|
|
|
* name: SMS
|
|
|
|
|
|
* description: 短信验证码相关接口
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @swagger
|
|
|
|
|
|
* components:
|
|
|
|
|
|
* schemas:
|
|
|
|
|
|
* SMSVerification:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* phone:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 手机号码
|
|
|
|
|
|
* code:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 验证码
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 阿里云短信配置
|
|
|
|
|
|
const config = new Config({
|
|
|
|
|
|
// 您的AccessKey ID
|
|
|
|
|
|
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'your_access_key_id',
|
|
|
|
|
|
// 您的AccessKey Secret
|
|
|
|
|
|
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'your_access_key_secret',
|
|
|
|
|
|
// 访问的域名
|
|
|
|
|
|
endpoint: 'dysmsapi.aliyuncs.com'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 创建短信客户端
|
|
|
|
|
|
const client = new Dysmsapi20170525.default(config)
|
|
|
|
|
|
|
|
|
|
|
|
// 短信模板配置
|
|
|
|
|
|
const SMS_CONFIG = {
|
|
|
|
|
|
signName: process.env.ALIYUN_SMS_SIGN_NAME || '您的签名', // 短信签名
|
|
|
|
|
|
templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE || 'SMS_XXXXXX', // 短信模板CODE
|
|
|
|
|
|
// 开发环境标识
|
|
|
|
|
|
isDevelopment: process.env.NODE_ENV !== 'production'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 存储验证码的内存对象(生产环境建议使用Redis)
|
|
|
|
|
|
const smsCodeStore = new Map()
|
|
|
|
|
|
|
|
|
|
|
|
// 验证码有效期(5分钟)
|
|
|
|
|
|
const CODE_EXPIRE_TIME = 5 * 60 * 1000
|
|
|
|
|
|
// 最大尝试次数
|
|
|
|
|
|
const MAX_ATTEMPTS = 3
|
|
|
|
|
|
// 发送频率限制(60秒)
|
|
|
|
|
|
const SEND_INTERVAL = 60 * 1000
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成6位数字验证码
|
|
|
|
|
|
* @returns {string} 验证码
|
|
|
|
|
|
*/
|
|
|
|
|
|
function generateSMSCode() {
|
|
|
|
|
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-28 09:14:56 +08:00
|
|
|
|
* @swagger
|
|
|
|
|
|
* /api/sms/send:
|
|
|
|
|
|
* post:
|
|
|
|
|
|
* summary: 发送短信验证码
|
|
|
|
|
|
* tags: [SMS]
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* required:
|
|
|
|
|
|
* - phone
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* phone:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 手机号码
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 验证码发送成功
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* success:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* example: true
|
|
|
|
|
|
* message:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* example: 验证码发送成功
|
|
|
|
|
|
* 400:
|
|
|
|
|
|
* description: 参数错误或发送频率限制
|
|
|
|
|
|
* 500:
|
|
|
|
|
|
* description: 服务器错误
|
2025-08-26 10:06:23 +08:00
|
|
|
|
*/
|
|
|
|
|
|
router.post('/send', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { phone } = req.body
|
2025-09-16 17:39:51 +08:00
|
|
|
|
console.log(phone)
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 验证手机号格式
|
|
|
|
|
|
const phoneRegex = /^1[3-9]\d{9}$/
|
|
|
|
|
|
if (!phoneRegex.test(phone)) {
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '手机号格式不正确'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 检查发送频率限制
|
|
|
|
|
|
const lastSendTime = smsCodeStore.get(`last_send_${phone}`)
|
|
|
|
|
|
if (lastSendTime && Date.now() - lastSendTime < SEND_INTERVAL) {
|
|
|
|
|
|
const remainingTime = Math.ceil((SEND_INTERVAL - (Date.now() - lastSendTime)) / 1000)
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `请等待${remainingTime}秒后再发送`
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 生成6位数字验证码
|
|
|
|
|
|
const code = Math.random().toString().slice(-6)
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 存储验证码信息
|
|
|
|
|
|
smsCodeStore.set(phone, {
|
|
|
|
|
|
code,
|
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
|
attempts: 0
|
|
|
|
|
|
})
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 记录发送时间
|
|
|
|
|
|
smsCodeStore.set(`last_send_${phone}`, Date.now())
|
|
|
|
|
|
// 生产环境发送真实短信
|
|
|
|
|
|
try {
|
2025-09-10 18:10:40 +08:00
|
|
|
|
console.log(code);
|
2025-08-26 10:06:23 +08:00
|
|
|
|
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({
|
|
|
|
|
|
phoneNumbers: phone,
|
|
|
|
|
|
signName: SMS_CONFIG.signName,
|
|
|
|
|
|
templateCode: SMS_CONFIG.templateCode,
|
|
|
|
|
|
templateParam: JSON.stringify({ code })
|
|
|
|
|
|
})
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
const response = await client.sendSms(sendSmsRequest)
|
2025-09-10 18:10:40 +08:00
|
|
|
|
console.log(response.body);
|
|
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
if (response.body.code === 'OK') {
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '验证码发送成功'
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('阿里云短信发送失败:', response.body)
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '发送失败,请稍后重试'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (smsError) {
|
|
|
|
|
|
console.error('阿里云短信API调用失败:', smsError)
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '发送失败,请稍后重试'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('发送短信验证码失败:', error)
|
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: '发送失败,请稍后重试'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-28 09:14:56 +08:00
|
|
|
|
* @swagger
|
|
|
|
|
|
* /api/sms/verify:
|
|
|
|
|
|
* post:
|
|
|
|
|
|
* summary: 验证短信验证码
|
|
|
|
|
|
* tags: [SMS]
|
|
|
|
|
|
* requestBody:
|
|
|
|
|
|
* required: true
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* required:
|
|
|
|
|
|
* - phone
|
|
|
|
|
|
* - code
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* phone:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 手机号码
|
|
|
|
|
|
* code:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* description: 验证码
|
|
|
|
|
|
* responses:
|
|
|
|
|
|
* 200:
|
|
|
|
|
|
* description: 验证码验证成功
|
|
|
|
|
|
* content:
|
|
|
|
|
|
* application/json:
|
|
|
|
|
|
* schema:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* success:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* example: true
|
|
|
|
|
|
* message:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* example: 手机号验证成功
|
|
|
|
|
|
* data:
|
|
|
|
|
|
* type: object
|
|
|
|
|
|
* properties:
|
|
|
|
|
|
* phone:
|
|
|
|
|
|
* type: string
|
|
|
|
|
|
* verified:
|
|
|
|
|
|
* type: boolean
|
|
|
|
|
|
* 400:
|
|
|
|
|
|
* description: 参数错误或验证码错误
|
|
|
|
|
|
* 500:
|
|
|
|
|
|
* description: 服务器错误
|
2025-08-26 10:06:23 +08:00
|
|
|
|
*/
|
|
|
|
|
|
router.post('/verify', async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { phone, code } = req.body;
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
if (!phone || !code) {
|
|
|
|
|
|
return res.status(400).json({ success: false, message: '手机号和验证码不能为空' });
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
const storedData = smsCodeStore.get(phone);
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
if (!storedData) {
|
|
|
|
|
|
return res.status(400).json({ success: false, message: '验证码不存在或已过期' });
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 检查验证码是否过期(5分钟)
|
|
|
|
|
|
if (Date.now() - storedData.timestamp > 300000) {
|
|
|
|
|
|
smsCodeStore.delete(phone);
|
|
|
|
|
|
return res.status(400).json({ success: false, message: '验证码已过期' });
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 检查尝试次数(最多3次)
|
|
|
|
|
|
if (storedData.attempts >= 3) {
|
|
|
|
|
|
smsCodeStore.delete(phone);
|
|
|
|
|
|
return res.status(400).json({ success: false, message: '验证码错误次数过多,请重新获取' });
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 验证验证码
|
|
|
|
|
|
if (storedData.code !== code) {
|
|
|
|
|
|
storedData.attempts++;
|
|
|
|
|
|
smsCodeStore.set(phone, storedData);
|
2025-09-10 18:10:40 +08:00
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `验证码错误,还可尝试${3 - storedData.attempts}次`
|
2025-08-26 10:06:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 验证成功,删除验证码
|
|
|
|
|
|
smsCodeStore.delete(phone);
|
|
|
|
|
|
smsCodeStore.delete(`time_${phone}`);
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: '手机号验证成功',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
phone: phone,
|
|
|
|
|
|
verified: true
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('验证短信验证码错误:', error);
|
|
|
|
|
|
res.status(500).json({ success: false, message: '验证失败' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 导出验证手机号的函数供其他模块使用
|
|
|
|
|
|
* @param {string} phone 手机号
|
|
|
|
|
|
* @param {string} code 验证码
|
|
|
|
|
|
* @returns {boolean} 验证结果
|
|
|
|
|
|
*/
|
|
|
|
|
|
function verifySMSCode(phone, code) {
|
|
|
|
|
|
const storedData = smsCodeStore.get(phone);
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
if (!storedData) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 检查是否过期
|
|
|
|
|
|
if (Date.now() - storedData.timestamp > 300000) {
|
|
|
|
|
|
smsCodeStore.delete(phone);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 检查尝试次数
|
|
|
|
|
|
if (storedData.attempts >= 3) {
|
|
|
|
|
|
smsCodeStore.delete(phone);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
// 验证验证码
|
|
|
|
|
|
if (storedData.code === code) {
|
|
|
|
|
|
smsCodeStore.delete(phone);
|
|
|
|
|
|
smsCodeStore.delete(`time_${phone}`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理过期验证码的定时任务
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
for (const [key, value] of smsCodeStore.entries()) {
|
|
|
|
|
|
if (key.startsWith('time_')) continue;
|
2025-09-10 18:10:40 +08:00
|
|
|
|
|
2025-08-26 10:06:23 +08:00
|
|
|
|
if (value.timestamp && now - value.timestamp > 300000) {
|
|
|
|
|
|
smsCodeStore.delete(key);
|
|
|
|
|
|
smsCodeStore.delete(`time_${key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 60000); // 每分钟清理一次
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|
|
|
|
|
|
module.exports.verifySMSCode = verifySMSCode;
|