初次提交
This commit is contained in:
355
routes/auth.js
Normal file
355
routes/auth.js
Normal file
@@ -0,0 +1,355 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const {getDB} = require('../database');
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
const {
|
||||
username,
|
||||
phone,
|
||||
password,
|
||||
city,
|
||||
district_id: district,
|
||||
province,
|
||||
inviter = null,
|
||||
captchaId,
|
||||
captchaText,
|
||||
smsCode, // 短信验证码
|
||||
role = 'user'
|
||||
} = req.body;
|
||||
|
||||
if (!username || !phone || !password || !city || !district || !province) {
|
||||
return res.status(400).json({success: false, message: '用户名、手机号、密码、城市和区域不能为空'});
|
||||
}
|
||||
|
||||
if (!captchaId || !captchaText) {
|
||||
return res.status(400).json({success: false, message: '图形验证码不能为空'});
|
||||
}
|
||||
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||
console.log(storedCaptcha);
|
||||
|
||||
if (!storedCaptcha) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码不存在或已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > storedCaptcha.expires) {
|
||||
global.captchaStore.delete(captchaId);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证验证码(不区分大小写)
|
||||
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||
|
||||
// 删除已验证的验证码
|
||||
global.captchaStore.delete(captchaId);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码错误'
|
||||
});
|
||||
}
|
||||
if (!smsCode) {
|
||||
return res.status(400).json({success: false, message: '短信验证码不能为空'});
|
||||
}
|
||||
// 验证短信验证码
|
||||
const smsAPI = require('./sms');
|
||||
const smsValid = smsAPI.verifySMSCode(phone, smsCode);
|
||||
if (!smsValid) {
|
||||
return res.status(400).json({success: false, message: '短信验证码错误或已过期'});
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
return res.status(400).json({success: false, message: '手机号格式不正确'});
|
||||
}
|
||||
|
||||
|
||||
// 检查用户是否已存在
|
||||
const [existingUsers] = await db.execute(
|
||||
'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?',
|
||||
[username, phone]
|
||||
);
|
||||
|
||||
if (existingUsers.length > 0) {
|
||||
return res.status(400).json({success: false, message: '用户名或手机号已存在'});
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 创建用户(初始状态为未支付)
|
||||
const [result] = await db.execute(
|
||||
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)',
|
||||
[username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter]
|
||||
);
|
||||
|
||||
const userId = result.insertId;
|
||||
await db.query('COMMIT');
|
||||
|
||||
// 生成JWT token(用于支付流程)
|
||||
const token = jwt.sign(
|
||||
{userId: userId, username, role},
|
||||
JWT_SECRET,
|
||||
{expiresIn: '24h'}
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '用户信息创建成功,请完成支付以激活账户',
|
||||
token,
|
||||
user: {
|
||||
id: userId,
|
||||
username,
|
||||
phone,
|
||||
role,
|
||||
points: 0,
|
||||
audit_status: 'pending',
|
||||
city,
|
||||
district,
|
||||
paymentStatus: 'unpaid'
|
||||
},
|
||||
needPayment: true
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
await getDB().query('ROLLBACK');
|
||||
} catch (rollbackError) {
|
||||
console.error('回滚错误:', rollbackError);
|
||||
}
|
||||
console.error('注册错误详情:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '注册失败',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const {username, password, captchaId, captchaText,type} = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({success: false, message: '用户名和密码不能为空'});
|
||||
}
|
||||
|
||||
if (!captchaId || !captchaText) {
|
||||
return res.status(400).json({success: false, message: '验证码不能为空'});
|
||||
}
|
||||
// 获取存储的验证码
|
||||
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||
console.log(storedCaptcha);
|
||||
|
||||
if (!storedCaptcha) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码不存在或已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > storedCaptcha.expires) {
|
||||
global.captchaStore.delete(captchaId);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证验证码(不区分大小写)
|
||||
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||
|
||||
// 删除已验证的验证码
|
||||
global.captchaStore.delete(captchaId);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
||||
|
||||
// 查找用户(包含支付状态)
|
||||
console.log('登录尝试 - 用户名:', username);
|
||||
const [users] = await db.execute(
|
||||
'SELECT * FROM users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
console.log('查找到的用户数量:', users.length);
|
||||
if (users.length === 0) {
|
||||
console.log('用户不存在:', username);
|
||||
return res.status(401).json({success: false, message: '用户名或密码错误'});
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null');
|
||||
|
||||
// 验证密码
|
||||
console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null');
|
||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||
console.log('密码验证结果:', isValidPassword);
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log('密码验证失败');
|
||||
return res.status(401).json({success: false, message: '用户名或密码错误'});
|
||||
}
|
||||
|
||||
// 检查支付状态(管理员除外)
|
||||
if (user.role !== 'admin' && user.payment_status === 'unpaid' && type!== 'app') {
|
||||
const token = jwt.sign(
|
||||
{userId: user.id, username: user.username, role: user.role},
|
||||
JWT_SECRET,
|
||||
{expiresIn: '5m'}
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: false,
|
||||
message: '您的账户尚未激活,请完成支付后再登录',
|
||||
needPayment: true,
|
||||
user: user[0],
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
|
||||
if (user.role !== 'admin' && user.audit_status === 'rejected') {
|
||||
return res.status(403).json({success: false, message: '您的账户审核未通过,请联系管理员'});
|
||||
}
|
||||
// 待审核用户可以正常登录使用系统,但匹配功能会有限制
|
||||
|
||||
// 生成JWT token
|
||||
let token;
|
||||
if(type === 'app') {
|
||||
token = jwt.sign(
|
||||
{userId: user.id, username: user.username, role: user.role},
|
||||
JWT_SECRET,
|
||||
{expiresIn: '999999h'}
|
||||
);
|
||||
}else {
|
||||
token = jwt.sign(
|
||||
{userId: user.id, username: user.username, role: user.role},
|
||||
JWT_SECRET,
|
||||
{expiresIn: '24h'}
|
||||
);
|
||||
}
|
||||
|
||||
const [is_distribution] = await db.execute(`
|
||||
SELECT *
|
||||
FROM distribution
|
||||
WHERE user_id = ?`, [user.id]);
|
||||
user.distribution = is_distribution.length > 0;
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
token,
|
||||
user
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
res.status(500).json({success: false, message: '登录失败'});
|
||||
}
|
||||
});
|
||||
|
||||
// 验证token中间件
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({success: false, message: '访问令牌缺失'});
|
||||
}
|
||||
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({success: false, message: '访问令牌无效'});
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// 获取当前用户信息
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const [users] = await db.execute(
|
||||
'SELECT * FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({success: false, message: '用户不存在'});
|
||||
}
|
||||
|
||||
res.json({success: true, user: users[0]});
|
||||
} catch (error) {
|
||||
console.error('获取用户信息错误:', error);
|
||||
res.status(500).json({success: false, message: '获取用户信息失败'});
|
||||
}
|
||||
});
|
||||
|
||||
// 修改密码
|
||||
router.put('/change-password', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const {currentPassword, newPassword} = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({success: false, message: '旧密码和新密码不能为空'});
|
||||
}
|
||||
|
||||
// 获取用户当前密码
|
||||
const [users] = await db.execute(
|
||||
'SELECT password FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({success: false, message: '用户不存在'});
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({success: false, message: '旧密码错误'});
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// 更新密码
|
||||
await db.execute(
|
||||
'UPDATE users SET password = ? WHERE id = ?',
|
||||
[hashedNewPassword, req.user.userId]
|
||||
);
|
||||
|
||||
res.json({success: true, message: '密码修改成功'});
|
||||
} catch (error) {
|
||||
console.error('修改密码错误:', error);
|
||||
res.status(500).json({success: false, message: '修改密码失败'});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports.authenticateToken = authenticateToken;
|
||||
158
routes/captcha.js
Normal file
158
routes/captcha.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 生成随机验证码字符串
|
||||
* @param {number} length 验证码长度
|
||||
* @returns {string} 验证码字符串
|
||||
*/
|
||||
function generateCaptchaText(length = 4) {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成SVG验证码图片
|
||||
* @param {string} text 验证码文本
|
||||
* @returns {string} SVG字符串
|
||||
*/
|
||||
function generateCaptchaSVG(text) {
|
||||
const width = 120;
|
||||
const height = 40;
|
||||
const fontSize = 18;
|
||||
|
||||
// 生成随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
// 生成干扰线
|
||||
const generateNoise = () => {
|
||||
let noise = '';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const x1 = Math.random() * width;
|
||||
const y1 = Math.random() * height;
|
||||
const x2 = Math.random() * width;
|
||||
const y2 = Math.random() * height;
|
||||
noise += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${getRandomColor()}" stroke-width="1" opacity="0.3"/>`;
|
||||
}
|
||||
return noise;
|
||||
};
|
||||
|
||||
// 生成干扰点
|
||||
const generateDots = () => {
|
||||
let dots = '';
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const x = Math.random() * width;
|
||||
const y = Math.random() * height;
|
||||
const r = Math.random() * 2 + 1;
|
||||
dots += `<circle cx="${x}" cy="${y}" r="${r}" fill="${getRandomColor()}" opacity="0.4"/>`;
|
||||
}
|
||||
return dots;
|
||||
};
|
||||
|
||||
// 生成文字
|
||||
let textElements = '';
|
||||
const charWidth = width / text.length;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const x = charWidth * i + charWidth / 2;
|
||||
const y = height / 2 + fontSize / 3;
|
||||
const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度
|
||||
const color = getRandomColor();
|
||||
|
||||
textElements += `
|
||||
<text x="${x}" y="${y}"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="${fontSize}"
|
||||
font-weight="bold"
|
||||
fill="${color}"
|
||||
text-anchor="middle"
|
||||
transform="rotate(${rotation} ${x} ${y})">
|
||||
${char}
|
||||
</text>`;
|
||||
}
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="${width}" height="${height}" fill="url(#bg)" stroke="#dee2e6" stroke-width="1"/>
|
||||
${generateNoise()}
|
||||
${generateDots()}
|
||||
${textElements}
|
||||
</svg>`;
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
router.get('/generate', (req, res) => {
|
||||
try {
|
||||
// 生成验证码文本
|
||||
const captchaText = generateCaptchaText();
|
||||
|
||||
// 生成唯一ID
|
||||
const captchaId = crypto.randomUUID();
|
||||
|
||||
// 存储验证码(5分钟过期)
|
||||
global.captchaStore.set(captchaId, {
|
||||
text: captchaText.toLowerCase(), // 存储小写用于比较
|
||||
expires: Date.now() + 5 * 60 * 1000 // 5分钟过期
|
||||
});
|
||||
|
||||
// 生成SVG图片
|
||||
const svgImage = generateCaptchaSVG(captchaText);
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
captchaId,
|
||||
image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('生成验证码失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '生成验证码失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 清理过期验证码的定时任务
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [id, captcha] of global.captchaStore.entries()) {
|
||||
if (now > captcha.expires) {
|
||||
global.captchaStore.delete(id);
|
||||
}
|
||||
}
|
||||
}, 60 * 1000); // 每分钟清理一次
|
||||
|
||||
// 导出验证函数供其他模块使用
|
||||
module.exports = router;
|
||||
module.exports.verifyCaptcha = (captchaId, captchaText) => {
|
||||
const captcha = global.captchaStore.get(captchaId);
|
||||
if (!captcha) {
|
||||
return false; // 验证码不存在或已过期
|
||||
}
|
||||
|
||||
if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) {
|
||||
return false; // 验证码错误
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码(一次性使用)
|
||||
global.captchaStore.delete(captchaId);
|
||||
return true;
|
||||
};
|
||||
175
routes/sms.js
Normal file
175
routes/sms.js
Normal file
@@ -0,0 +1,175 @@
|
||||
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')
|
||||
|
||||
// 阿里云短信配置
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
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: '发送失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 导出验证手机号的函数供其他模块使用
|
||||
* @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;
|
||||
243
routes/upload.js
Normal file
243
routes/upload.js
Normal file
@@ -0,0 +1,243 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const { auth } = require('../middleware/auth');
|
||||
const { authenticateToken } = require('./auth');
|
||||
const minioService = require('../services/minioService');
|
||||
const { initializeBuckets } = require('../config/minio');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
// 配置multer内存存储(用于MinIO上传)
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
// 文件过滤器 - 支持图片和视频
|
||||
const fileFilter = (req, file, cb) => {
|
||||
// 允许图片和视频文件
|
||||
if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('只能上传图片或视频文件'), false);
|
||||
}
|
||||
};
|
||||
|
||||
// 单文件上传配置
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB
|
||||
files: 1 // 一次只能上传一个文件
|
||||
}
|
||||
});
|
||||
|
||||
// 多文件上传配置
|
||||
const multiUpload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB (视频文件更大)
|
||||
files: 10 // 最多10个文件
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/image', authenticateToken, (req, res) => {
|
||||
upload.single('file')(req, res, async (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件大小不能超过 5MB'
|
||||
});
|
||||
}
|
||||
if (err.code === 'LIMIT_FILE_COUNT') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '一次只能上传一个文件'
|
||||
});
|
||||
}
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件上传失败:' + err.message
|
||||
});
|
||||
} else if (err) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的文件'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用MinIO服务上传文件
|
||||
const type = req.body.type || 'document';
|
||||
const result = await minioService.uploadFile(
|
||||
req.file.buffer,
|
||||
req.file.originalname,
|
||||
req.file.mimetype,
|
||||
type
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件上传成功',
|
||||
data: result.data
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('文件上传到MinIO失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文件上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/', authenticateToken, (req, res) => {
|
||||
multiUpload.array('file', 10)(req, res, async (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件大小不能超过 10MB'
|
||||
});
|
||||
}
|
||||
if (err.code === 'LIMIT_FILE_COUNT') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '一次最多只能上传10个文件'
|
||||
});
|
||||
}
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件上传失败:' + err.message
|
||||
});
|
||||
} else if (err) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的文件'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用MinIO服务上传多个文件
|
||||
const type = req.body.type || 'document';
|
||||
const files = req.files.map(file => ({
|
||||
buffer: file.buffer,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype
|
||||
}));
|
||||
|
||||
const result = await minioService.uploadMultipleFiles(files, type);
|
||||
|
||||
// 如果只上传了一个文件,返回单文件格式以保持兼容性
|
||||
if (result.data.files.length === 1) {
|
||||
result.data.files.forEach(element => {
|
||||
element.path = '/' + element.path
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件上传成功',
|
||||
data: {
|
||||
...result.data.files[0],
|
||||
urls: result.data.urls // 同时提供urls数组格式
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 多文件返回数组格式
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功上传${result.data.files.length}个文件`,
|
||||
data: result.data
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传到MinIO失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文件上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/single', auth, (req, res) => {
|
||||
upload.single('file')(req, res, async (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件上传失败:' + err.message
|
||||
});
|
||||
} else if (err) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ success: false, message: '没有上传文件' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用MinIO服务上传文件
|
||||
const type = req.body.type || 'document';
|
||||
const result = await minioService.uploadFile(
|
||||
req.file.buffer,
|
||||
req.file.originalname,
|
||||
req.file.mimetype,
|
||||
type
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '文件上传成功',
|
||||
url: result.data.url,
|
||||
filename: result.data.filename,
|
||||
originalname: result.data.originalname,
|
||||
size: result.data.size
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('文件上传到MinIO失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文件上传失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
router.use((error, req, res, next) => {
|
||||
if (error instanceof multer.MulterError) {
|
||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ success: false, message: '文件大小不能超过10MB' });
|
||||
}
|
||||
if (error.code === 'LIMIT_FILE_COUNT') {
|
||||
return res.status(400).json({ success: false, message: '一次最多只能上传10个文件' });
|
||||
}
|
||||
}
|
||||
|
||||
if (error.message === '只能上传图片或视频文件') {
|
||||
return res.status(400).json({ success: false, message: error.message });
|
||||
}
|
||||
|
||||
res.status(500).json({ success: false, message: '上传失败' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
0
routes/user.js
Normal file
0
routes/user.js
Normal file
Reference in New Issue
Block a user