Files
jurong_circle_black/routes/sms.js
2025-09-10 18:10:40 +08:00

341 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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')
/**
* @swagger
* tags:
* name: SMS
* description: 短信验证码相关接口
*/
/**
* @swagger
* components:
* schemas:
* SMSVerification:
* type: object
* properties:
* phone:
* type: string
* description: 手机号码
* code:
* type: string
* description: 验证码
*/
// 阿里云短信配置
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();
}
/**
* @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: 服务器错误
*/
router.post('/send', async (req, res) => {
try {
const { phone } = req.body
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(phone)) {
return res.json({
success: false,
message: '手机号格式不正确'
})
}
// 检查发送频率限制
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}秒后再发送`
})
}
// 生成6位数字验证码
const code = Math.random().toString().slice(-6)
// 存储验证码信息
smsCodeStore.set(phone, {
code,
timestamp: Date.now(),
attempts: 0
})
// 记录发送时间
smsCodeStore.set(`last_send_${phone}`, Date.now())
// 生产环境发送真实短信
try {
console.log(code);
res.json({
success: true,
message: '验证码发送成功'
})
return
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({
phoneNumbers: phone,
signName: SMS_CONFIG.signName,
templateCode: SMS_CONFIG.templateCode,
templateParam: JSON.stringify({ code })
})
const response = await client.sendSms(sendSmsRequest)
console.log(response.body);
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: '发送失败,请稍后重试'
})
}
} catch (error) {
console.error('发送短信验证码失败:', error)
res.status(500).json({
success: false,
message: '发送失败,请稍后重试'
})
}
});
/**
* @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: 服务器错误
*/
router.post('/verify', async (req, res) => {
try {
const { phone, code } = req.body;
if (!phone || !code) {
return res.status(400).json({ success: false, message: '手机号和验证码不能为空' });
}
const storedData = smsCodeStore.get(phone);
if (!storedData) {
return res.status(400).json({ success: false, message: '验证码不存在或已过期' });
}
// 检查验证码是否过期5分钟
if (Date.now() - storedData.timestamp > 300000) {
smsCodeStore.delete(phone);
return res.status(400).json({ success: false, message: '验证码已过期' });
}
// 检查尝试次数最多3次
if (storedData.attempts >= 3) {
smsCodeStore.delete(phone);
return res.status(400).json({ success: false, message: '验证码错误次数过多,请重新获取' });
}
// 验证验证码
if (storedData.code !== code) {
storedData.attempts++;
smsCodeStore.set(phone, storedData);
return res.status(400).json({
success: false,
message: `验证码错误,还可尝试${3 - storedData.attempts}`
});
}
// 验证成功,删除验证码
smsCodeStore.delete(phone);
smsCodeStore.delete(`time_${phone}`);
res.json({
success: true,
message: '手机号验证成功',
data: {
phone: phone,
verified: true
}
});
} 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);
if (!storedData) {
return false;
}
// 检查是否过期
if (Date.now() - storedData.timestamp > 300000) {
smsCodeStore.delete(phone);
return false;
}
// 检查尝试次数
if (storedData.attempts >= 3) {
smsCodeStore.delete(phone);
return false;
}
// 验证验证码
if (storedData.code === code) {
smsCodeStore.delete(phone);
smsCodeStore.delete(`time_${phone}`);
return true;
}
return false;
}
// 清理过期验证码的定时任务
setInterval(() => {
const now = Date.now();
for (const [key, value] of smsCodeStore.entries()) {
if (key.startsWith('time_')) continue;
if (value.timestamp && now - value.timestamp > 300000) {
smsCodeStore.delete(key);
smsCodeStore.delete(`time_${key}`);
}
}
}, 60000); // 每分钟清理一次
module.exports = router;
module.exports.verifySMSCode = verifySMSCode;