| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- // 注册处理模块
- 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
- };
|