register.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // 注册处理模块
  2. const formidable = require('formidable');
  3. const path = require('path');
  4. const fs = require('fs');
  5. const crypto = require('crypto');
  6. const { promisify } = require('util');
  7. const { getDatabase } = require('./sql');
  8. const mkdir = promisify(fs.mkdir);
  9. const access = promisify(fs.access);
  10. const copyFile = promisify(fs.copyFile);
  11. const unlink = promisify(fs.unlink);
  12. // 固定验证码
  13. const FIXED_VERIFICATION_CODE = '9527';
  14. // 默认头像目录
  15. const DEFAULT_AVATAR_DIR = path.join(__dirname, 'avatar');
  16. // 用户数据根目录
  17. const USERS_DIR = path.join(__dirname, 'users');
  18. const readdir = promisify(fs.readdir);
  19. // 确保目录存在
  20. async function ensureDir(dirPath) {
  21. try {
  22. await access(dirPath);
  23. } catch (error) {
  24. await mkdir(dirPath, { recursive: true });
  25. console.log('[Register] 创建目录:', dirPath);
  26. }
  27. }
  28. // 获取用户文件夹路径
  29. function getUserDir(username) {
  30. return path.join(USERS_DIR, username);
  31. }
  32. // 从默认头像中随机选择一个
  33. async function getRandomDefaultAvatar() {
  34. try {
  35. await ensureDir(DEFAULT_AVATAR_DIR);
  36. const files = await readdir(DEFAULT_AVATAR_DIR);
  37. const imageFiles = files.filter(file => {
  38. const ext = path.extname(file).toLowerCase();
  39. return ['.png', '.jpg', '.jpeg', '.gif'].includes(ext);
  40. });
  41. if (imageFiles.length === 0) {
  42. return null;
  43. }
  44. // 随机选择一个
  45. const randomIndex = Math.floor(Math.random() * imageFiles.length);
  46. return path.join(DEFAULT_AVATAR_DIR, imageFiles[randomIndex]);
  47. } catch (error) {
  48. console.error('[Register] 获取默认头像失败:', error);
  49. return null;
  50. }
  51. }
  52. // 密码加密(使用 SHA256)
  53. function hashPassword(password) {
  54. return crypto.createHash('sha256').update(password).digest('hex');
  55. }
  56. // 保存头像文件到用户文件夹
  57. async function saveAvatar(file, username) {
  58. const userDir = getUserDir(username);
  59. await ensureDir(userDir);
  60. // 生成文件名(使用 avatar 作为基础名,保留扩展名)
  61. const ext = file ? path.extname(file.originalFilename || file.name || '') : '.png';
  62. const filename = `avatar${ext}`;
  63. const filepath = path.join(userDir, filename);
  64. if (file) {
  65. // 复制用户上传的文件到用户文件夹
  66. await copyFile(file.filepath, filepath);
  67. }
  68. // 返回相对路径(用于存储到数据库)
  69. return `users/${username}/${filename}`;
  70. }
  71. // 复制默认头像到用户文件夹
  72. async function copyDefaultAvatarToUser(username) {
  73. const defaultAvatarPath = await getRandomDefaultAvatar();
  74. if (!defaultAvatarPath) {
  75. return null;
  76. }
  77. const userDir = getUserDir(username);
  78. await ensureDir(userDir);
  79. const ext = path.extname(defaultAvatarPath);
  80. const filename = `avatar${ext}`;
  81. const userAvatarPath = path.join(userDir, filename);
  82. // 复制默认头像到用户文件夹
  83. await copyFile(defaultAvatarPath, userAvatarPath);
  84. // 返回相对路径
  85. return `users/${username}/${filename}`;
  86. }
  87. // 处理注册请求
  88. async function handleRegisterRequest(req, res) {
  89. // 设置 CORS 头
  90. res.setHeader('Access-Control-Allow-Origin', '*');
  91. res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  92. res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  93. if (req.method === 'OPTIONS') {
  94. res.writeHead(200);
  95. res.end();
  96. return;
  97. }
  98. if (req.method !== 'POST') {
  99. res.writeHead(405, { 'Content-Type': 'application/json' });
  100. res.end(JSON.stringify({ success: false, message: 'Method not allowed' }));
  101. return;
  102. }
  103. try {
  104. const form = formidable.formidable({
  105. multiples: false,
  106. maxFileSize: 5 * 1024 * 1024, // 5MB
  107. keepExtensions: true
  108. });
  109. const [fields, files] = await form.parse(req);
  110. // 提取表单字段(formidable 可能返回数组)
  111. const username = Array.isArray(fields.username) ? fields.username[0] : (fields.username || '');
  112. const phone = Array.isArray(fields.phone) ? fields.phone[0] : (fields.phone || '');
  113. const code = Array.isArray(fields.code) ? fields.code[0] : (fields.code || '');
  114. const password = Array.isArray(fields.password) ? fields.password[0] : (fields.password || '');
  115. const passwordConfirm = Array.isArray(fields.passwordConfirm) ? fields.passwordConfirm[0] : (fields.passwordConfirm || '');
  116. // 验证必填字段
  117. if (!username || !phone || !code || !password || !passwordConfirm) {
  118. res.writeHead(400, { 'Content-Type': 'application/json' });
  119. res.end(JSON.stringify({ success: false, message: '请填写所有必填字段' }));
  120. return;
  121. }
  122. // 验证用户名格式(现代网站标准)
  123. // 长度:4-20个字符
  124. if (username.length < 4 || username.length > 20) {
  125. res.writeHead(400, { 'Content-Type': 'application/json' });
  126. res.end(JSON.stringify({ success: false, message: '用户名长度为4-20个字符' }));
  127. return;
  128. }
  129. // 只能包含字母、数字、下划线、连字符
  130. if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(username)) {
  131. res.writeHead(400, { 'Content-Type': 'application/json' });
  132. res.end(JSON.stringify({ success: false, message: '用户名只能包含字母、数字、下划线和连字符,且必须以字母开头' }));
  133. return;
  134. }
  135. // 不能全部是数字
  136. if (/^\d+$/.test(username)) {
  137. res.writeHead(400, { 'Content-Type': 'application/json' });
  138. res.end(JSON.stringify({ success: false, message: '用户名不能全部是数字' }));
  139. return;
  140. }
  141. // 转换为小写进行存储和查询(不区分大小写)
  142. const normalizedUsername = username.toLowerCase();
  143. // 验证手机号格式
  144. if (!/^1[3-9]\d{9}$/.test(phone)) {
  145. res.writeHead(400, { 'Content-Type': 'application/json' });
  146. res.end(JSON.stringify({ success: false, message: '请输入正确的手机号' }));
  147. return;
  148. }
  149. // 验证验证码(固定验证码 9527)
  150. if (code !== FIXED_VERIFICATION_CODE) {
  151. res.writeHead(400, { 'Content-Type': 'application/json' });
  152. res.end(JSON.stringify({ success: false, message: '验证码错误' }));
  153. return;
  154. }
  155. // 验证密码格式
  156. if (password.length < 8 || password.length > 20) {
  157. res.writeHead(400, { 'Content-Type': 'application/json' });
  158. res.end(JSON.stringify({ success: false, message: '密码长度为8-20位' }));
  159. return;
  160. }
  161. if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/\d/.test(password)) {
  162. res.writeHead(400, { 'Content-Type': 'application/json' });
  163. res.end(JSON.stringify({ success: false, message: '密码必须包含大小写字母和数字' }));
  164. return;
  165. }
  166. // 验证两次密码是否一致
  167. if (password !== passwordConfirm) {
  168. res.writeHead(400, { 'Content-Type': 'application/json' });
  169. res.end(JSON.stringify({ success: false, message: '两次输入的密码不一致' }));
  170. return;
  171. }
  172. // 获取数据库实例
  173. const db = await getDatabase();
  174. // 检查用户名是否已存在(不区分大小写)
  175. const existingUserByUsername = await db.findUserByUsername(normalizedUsername);
  176. if (existingUserByUsername) {
  177. res.writeHead(400, { 'Content-Type': 'application/json' });
  178. res.end(JSON.stringify({ success: false, message: '用户名已存在' }));
  179. return;
  180. }
  181. // 检查手机号是否已存在
  182. const existingUserByPhone = await db.findUserByPhone(phone);
  183. if (existingUserByPhone) {
  184. res.writeHead(400, { 'Content-Type': 'application/json' });
  185. res.end(JSON.stringify({ success: false, message: '手机号已被注册' }));
  186. return;
  187. }
  188. // 创建用户文件夹
  189. const userDir = getUserDir(normalizedUsername);
  190. await ensureDir(userDir);
  191. // 创建用户的 disk_data 目录
  192. const userDiskDataDir = path.join(userDir, 'disk_data');
  193. await ensureDir(userDiskDataDir);
  194. // 处理头像
  195. let avatarPath = null;
  196. const avatarFile = Array.isArray(files.avatar) ? files.avatar[0] : (files.avatar || null);
  197. if (avatarFile && avatarFile.size > 0) {
  198. // 用户上传了新头像
  199. // 验证文件类型
  200. const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
  201. if (!allowedTypes.includes(avatarFile.mimetype)) {
  202. res.writeHead(400, { 'Content-Type': 'application/json' });
  203. res.end(JSON.stringify({ success: false, message: '头像必须是图片文件' }));
  204. return;
  205. }
  206. try {
  207. avatarPath = await saveAvatar(avatarFile, normalizedUsername);
  208. } catch (error) {
  209. console.error('[Register] 保存头像失败:', error);
  210. res.writeHead(500, { 'Content-Type': 'application/json' });
  211. res.end(JSON.stringify({ success: false, message: '保存头像失败' }));
  212. return;
  213. }
  214. } else {
  215. // 用户没有上传头像,使用随机默认头像
  216. try {
  217. avatarPath = await copyDefaultAvatarToUser(normalizedUsername);
  218. if (!avatarPath) {
  219. // 如果没有默认头像,仍然允许注册(头像可以为空)
  220. console.warn('[Register] 没有可用的默认头像');
  221. }
  222. } catch (error) {
  223. console.error('[Register] 复制默认头像失败:', error);
  224. // 即使默认头像复制失败,也允许注册继续
  225. }
  226. }
  227. // 加密密码
  228. const hashedPassword = hashPassword(password);
  229. // 创建用户(使用原始用户名显示,但存储时使用小写进行唯一性检查)
  230. const newUser = await db.createUser({
  231. username: username, // 显示时保持原始大小写
  232. phone,
  233. password: hashedPassword,
  234. avatar: avatarPath
  235. });
  236. // 清理临时文件
  237. if (avatarFile && avatarFile.filepath) {
  238. try {
  239. await unlink(avatarFile.filepath);
  240. } catch (error) {
  241. // 忽略删除临时文件错误
  242. }
  243. }
  244. res.writeHead(200, { 'Content-Type': 'application/json' });
  245. res.end(JSON.stringify({
  246. success: true,
  247. message: '注册成功',
  248. user: {
  249. id: newUser.id,
  250. username: newUser.username,
  251. phone: newUser.phone,
  252. avatar: newUser.avatar
  253. }
  254. }));
  255. } catch (error) {
  256. console.error('[Register] 注册处理错误:', error);
  257. res.writeHead(500, { 'Content-Type': 'application/json' });
  258. res.end(JSON.stringify({ success: false, message: '服务器错误' }));
  259. }
  260. }
  261. module.exports = {
  262. handleRegisterRequest
  263. };