// 注册处理模块 const formidable = require('formidable'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const { promisify } = require('util'); const { getDatabase } = require('./sql'); const mkdir = promisify(fs.mkdir); const access = promisify(fs.access); const copyFile = promisify(fs.copyFile); const unlink = promisify(fs.unlink); // 固定验证码 const FIXED_VERIFICATION_CODE = '9527'; // 默认头像目录 const DEFAULT_AVATAR_DIR = path.join(__dirname, 'avatar'); // 用户数据根目录 const USERS_DIR = path.join(__dirname, 'users'); const readdir = promisify(fs.readdir); // 确保目录存在 async function ensureDir(dirPath) { try { await access(dirPath); } catch (error) { await mkdir(dirPath, { recursive: true }); console.log('[Register] 创建目录:', dirPath); } } // 获取用户文件夹路径 function getUserDir(username) { return path.join(USERS_DIR, username); } // 从默认头像中随机选择一个 async function getRandomDefaultAvatar() { try { await ensureDir(DEFAULT_AVATAR_DIR); const files = await readdir(DEFAULT_AVATAR_DIR); const imageFiles = files.filter(file => { const ext = path.extname(file).toLowerCase(); return ['.png', '.jpg', '.jpeg', '.gif'].includes(ext); }); if (imageFiles.length === 0) { return null; } // 随机选择一个 const randomIndex = Math.floor(Math.random() * imageFiles.length); return path.join(DEFAULT_AVATAR_DIR, imageFiles[randomIndex]); } catch (error) { console.error('[Register] 获取默认头像失败:', error); return null; } } // 密码加密(使用 SHA256) function hashPassword(password) { return crypto.createHash('sha256').update(password).digest('hex'); } // 保存头像文件到用户文件夹 async function saveAvatar(file, username) { const userDir = getUserDir(username); await ensureDir(userDir); // 生成文件名(使用 avatar 作为基础名,保留扩展名) const ext = file ? path.extname(file.originalFilename || file.name || '') : '.png'; const filename = `avatar${ext}`; const filepath = path.join(userDir, filename); if (file) { // 复制用户上传的文件到用户文件夹 await copyFile(file.filepath, filepath); } // 返回相对路径(用于存储到数据库) return `users/${username}/${filename}`; } // 复制默认头像到用户文件夹 async function copyDefaultAvatarToUser(username) { const defaultAvatarPath = await getRandomDefaultAvatar(); if (!defaultAvatarPath) { return null; } const userDir = getUserDir(username); await ensureDir(userDir); const ext = path.extname(defaultAvatarPath); const filename = `avatar${ext}`; const userAvatarPath = path.join(userDir, filename); // 复制默认头像到用户文件夹 await copyFile(defaultAvatarPath, userAvatarPath); // 返回相对路径 return `users/${username}/${filename}`; } // 处理注册请求 async function handleRegisterRequest(req, res) { // 设置 CORS 头 res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (req.method !== 'POST') { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); return; } try { const form = formidable.formidable({ multiples: false, maxFileSize: 5 * 1024 * 1024, // 5MB keepExtensions: true }); const [fields, files] = await form.parse(req); // 提取表单字段(formidable 可能返回数组) const username = Array.isArray(fields.username) ? fields.username[0] : (fields.username || ''); const phone = Array.isArray(fields.phone) ? fields.phone[0] : (fields.phone || ''); const code = Array.isArray(fields.code) ? fields.code[0] : (fields.code || ''); const password = Array.isArray(fields.password) ? fields.password[0] : (fields.password || ''); const passwordConfirm = Array.isArray(fields.passwordConfirm) ? fields.passwordConfirm[0] : (fields.passwordConfirm || ''); // 验证必填字段 if (!username || !phone || !code || !password || !passwordConfirm) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '请填写所有必填字段' })); return; } // 验证用户名格式(现代网站标准) // 长度:4-20个字符 if (username.length < 4 || username.length > 20) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '用户名长度为4-20个字符' })); return; } // 只能包含字母、数字、下划线、连字符 if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(username)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '用户名只能包含字母、数字、下划线和连字符,且必须以字母开头' })); return; } // 不能全部是数字 if (/^\d+$/.test(username)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '用户名不能全部是数字' })); return; } // 转换为小写进行存储和查询(不区分大小写) const normalizedUsername = username.toLowerCase(); // 验证手机号格式 if (!/^1[3-9]\d{9}$/.test(phone)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '请输入正确的手机号' })); return; } // 验证验证码(固定验证码 9527) if (code !== FIXED_VERIFICATION_CODE) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '验证码错误' })); return; } // 验证密码格式 if (password.length < 8 || password.length > 20) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '密码长度为8-20位' })); return; } if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/\d/.test(password)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '密码必须包含大小写字母和数字' })); return; } // 验证两次密码是否一致 if (password !== passwordConfirm) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '两次输入的密码不一致' })); return; } // 获取数据库实例 const db = await getDatabase(); // 检查用户名是否已存在(不区分大小写) const existingUserByUsername = await db.findUserByUsername(normalizedUsername); if (existingUserByUsername) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '用户名已存在' })); return; } // 检查手机号是否已存在 const existingUserByPhone = await db.findUserByPhone(phone); if (existingUserByPhone) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '手机号已被注册' })); return; } // 创建用户文件夹 const userDir = getUserDir(normalizedUsername); await ensureDir(userDir); // 创建用户的 disk_data 目录 const userDiskDataDir = path.join(userDir, 'disk_data'); await ensureDir(userDiskDataDir); // 处理头像 let avatarPath = null; const avatarFile = Array.isArray(files.avatar) ? files.avatar[0] : (files.avatar || null); if (avatarFile && avatarFile.size > 0) { // 用户上传了新头像 // 验证文件类型 const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(avatarFile.mimetype)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '头像必须是图片文件' })); return; } try { avatarPath = await saveAvatar(avatarFile, normalizedUsername); } catch (error) { console.error('[Register] 保存头像失败:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '保存头像失败' })); return; } } else { // 用户没有上传头像,使用随机默认头像 try { avatarPath = await copyDefaultAvatarToUser(normalizedUsername); if (!avatarPath) { // 如果没有默认头像,仍然允许注册(头像可以为空) console.warn('[Register] 没有可用的默认头像'); } } catch (error) { console.error('[Register] 复制默认头像失败:', error); // 即使默认头像复制失败,也允许注册继续 } } // 加密密码 const hashedPassword = hashPassword(password); // 创建用户(使用原始用户名显示,但存储时使用小写进行唯一性检查) const newUser = await db.createUser({ username: username, // 显示时保持原始大小写 phone, password: hashedPassword, avatar: avatarPath }); // 清理临时文件 if (avatarFile && avatarFile.filepath) { try { await unlink(avatarFile.filepath); } catch (error) { // 忽略删除临时文件错误 } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: '注册成功', user: { id: newUser.id, username: newUser.username, phone: newUser.phone, avatar: newUser.avatar } })); } catch (error) { console.error('[Register] 注册处理错误:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: '服务器错误' })); } } module.exports = { handleRegisterRequest };