340 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			340 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | 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 | |||
|  |       console.log(phone) | |||
|  |     // 验证手机号格式
 | |||
|  |     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; |