register.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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 AVATAR_DIR = path.join(__dirname, 'avatar');
  16. // 确保头像目录存在
  17. async function ensureAvatarDir() {
  18. try {
  19. await access(AVATAR_DIR);
  20. } catch (error) {
  21. await mkdir(AVATAR_DIR, { recursive: true });
  22. console.log('[Register] 创建avatar目录:', AVATAR_DIR);
  23. }
  24. }
  25. // 密码加密(使用 SHA256)
  26. function hashPassword(password) {
  27. return crypto.createHash('sha256').update(password).digest('hex');
  28. }
  29. // 保存头像文件
  30. async function saveAvatar(file) {
  31. if (!file) {
  32. return null;
  33. }
  34. await ensureAvatarDir();
  35. // 生成唯一文件名
  36. const ext = path.extname(file.originalFilename || file.name || '');
  37. const filename = `${Date.now()}_${Math.random().toString(36).substring(7)}${ext}`;
  38. const filepath = path.join(AVATAR_DIR, filename);
  39. // 复制文件到目标位置
  40. await copyFile(file.filepath, filepath);
  41. // 返回相对路径(用于存储到数据库)
  42. return `avatar/${filename}`;
  43. }
  44. // 处理注册请求
  45. async function handleRegisterRequest(req, res) {
  46. // 设置 CORS 头
  47. res.setHeader('Access-Control-Allow-Origin', '*');
  48. res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  49. res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  50. if (req.method === 'OPTIONS') {
  51. res.writeHead(200);
  52. res.end();
  53. return;
  54. }
  55. if (req.method !== 'POST') {
  56. res.writeHead(405, { 'Content-Type': 'application/json' });
  57. res.end(JSON.stringify({ success: false, message: 'Method not allowed' }));
  58. return;
  59. }
  60. try {
  61. const form = formidable.formidable({
  62. multiples: false,
  63. maxFileSize: 5 * 1024 * 1024, // 5MB
  64. keepExtensions: true
  65. });
  66. const [fields, files] = await form.parse(req);
  67. // 提取表单字段(formidable 可能返回数组)
  68. const username = Array.isArray(fields.username) ? fields.username[0] : (fields.username || '');
  69. const phone = Array.isArray(fields.phone) ? fields.phone[0] : (fields.phone || '');
  70. const code = Array.isArray(fields.code) ? fields.code[0] : (fields.code || '');
  71. const password = Array.isArray(fields.password) ? fields.password[0] : (fields.password || '');
  72. const passwordConfirm = Array.isArray(fields.passwordConfirm) ? fields.passwordConfirm[0] : (fields.passwordConfirm || '');
  73. // 验证必填字段
  74. if (!username || !phone || !code || !password || !passwordConfirm) {
  75. res.writeHead(400, { 'Content-Type': 'application/json' });
  76. res.end(JSON.stringify({ success: false, message: '请填写所有必填字段' }));
  77. return;
  78. }
  79. // 验证用户名格式(现代网站标准)
  80. // 长度:4-20个字符
  81. if (username.length < 4 || username.length > 20) {
  82. res.writeHead(400, { 'Content-Type': 'application/json' });
  83. res.end(JSON.stringify({ success: false, message: '用户名长度为4-20个字符' }));
  84. return;
  85. }
  86. // 只能包含字母、数字、下划线、连字符
  87. if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(username)) {
  88. res.writeHead(400, { 'Content-Type': 'application/json' });
  89. res.end(JSON.stringify({ success: false, message: '用户名只能包含字母、数字、下划线和连字符,且必须以字母开头' }));
  90. return;
  91. }
  92. // 不能全部是数字
  93. if (/^\d+$/.test(username)) {
  94. res.writeHead(400, { 'Content-Type': 'application/json' });
  95. res.end(JSON.stringify({ success: false, message: '用户名不能全部是数字' }));
  96. return;
  97. }
  98. // 转换为小写进行存储和查询(不区分大小写)
  99. const normalizedUsername = username.toLowerCase();
  100. // 验证手机号格式
  101. if (!/^1[3-9]\d{9}$/.test(phone)) {
  102. res.writeHead(400, { 'Content-Type': 'application/json' });
  103. res.end(JSON.stringify({ success: false, message: '请输入正确的手机号' }));
  104. return;
  105. }
  106. // 验证验证码(固定验证码 9527)
  107. if (code !== FIXED_VERIFICATION_CODE) {
  108. res.writeHead(400, { 'Content-Type': 'application/json' });
  109. res.end(JSON.stringify({ success: false, message: '验证码错误' }));
  110. return;
  111. }
  112. // 验证密码格式
  113. if (password.length < 8 || password.length > 20) {
  114. res.writeHead(400, { 'Content-Type': 'application/json' });
  115. res.end(JSON.stringify({ success: false, message: '密码长度为8-20位' }));
  116. return;
  117. }
  118. if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/\d/.test(password)) {
  119. res.writeHead(400, { 'Content-Type': 'application/json' });
  120. res.end(JSON.stringify({ success: false, message: '密码必须包含大小写字母和数字' }));
  121. return;
  122. }
  123. // 验证两次密码是否一致
  124. if (password !== passwordConfirm) {
  125. res.writeHead(400, { 'Content-Type': 'application/json' });
  126. res.end(JSON.stringify({ success: false, message: '两次输入的密码不一致' }));
  127. return;
  128. }
  129. // 获取数据库实例
  130. const db = await getDatabase();
  131. // 检查用户名是否已存在(不区分大小写)
  132. const existingUserByUsername = await db.findUserByUsername(normalizedUsername);
  133. if (existingUserByUsername) {
  134. res.writeHead(400, { 'Content-Type': 'application/json' });
  135. res.end(JSON.stringify({ success: false, message: '用户名已存在' }));
  136. return;
  137. }
  138. // 检查手机号是否已存在
  139. const existingUserByPhone = await db.findUserByPhone(phone);
  140. if (existingUserByPhone) {
  141. res.writeHead(400, { 'Content-Type': 'application/json' });
  142. res.end(JSON.stringify({ success: false, message: '手机号已被注册' }));
  143. return;
  144. }
  145. // 处理头像上传
  146. let avatarPath = null;
  147. const avatarFile = Array.isArray(files.avatar) ? files.avatar[0] : (files.avatar || null);
  148. if (avatarFile && avatarFile.size > 0) {
  149. // 验证文件类型
  150. const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
  151. if (!allowedTypes.includes(avatarFile.mimetype)) {
  152. res.writeHead(400, { 'Content-Type': 'application/json' });
  153. res.end(JSON.stringify({ success: false, message: '头像必须是图片文件' }));
  154. return;
  155. }
  156. try {
  157. avatarPath = await saveAvatar(avatarFile);
  158. } catch (error) {
  159. console.error('[Register] 保存头像失败:', error);
  160. res.writeHead(500, { 'Content-Type': 'application/json' });
  161. res.end(JSON.stringify({ success: false, message: '保存头像失败' }));
  162. return;
  163. }
  164. }
  165. // 加密密码
  166. const hashedPassword = hashPassword(password);
  167. // 创建用户(使用原始用户名显示,但存储时使用小写进行唯一性检查)
  168. const newUser = await db.createUser({
  169. username: username, // 显示时保持原始大小写
  170. phone,
  171. password: hashedPassword,
  172. avatar: avatarPath
  173. });
  174. // 清理临时文件
  175. if (avatarFile && avatarFile.filepath) {
  176. try {
  177. await unlink(avatarFile.filepath);
  178. } catch (error) {
  179. // 忽略删除临时文件错误
  180. }
  181. }
  182. res.writeHead(200, { 'Content-Type': 'application/json' });
  183. res.end(JSON.stringify({
  184. success: true,
  185. message: '注册成功',
  186. user: {
  187. id: newUser.id,
  188. username: newUser.username,
  189. phone: newUser.phone,
  190. avatar: newUser.avatar
  191. }
  192. }));
  193. } catch (error) {
  194. console.error('[Register] 注册处理错误:', error);
  195. res.writeHead(500, { 'Content-Type': 'application/json' });
  196. res.end(JSON.stringify({ success: false, message: '服务器错误' }));
  197. }
  198. }
  199. module.exports = {
  200. handleRegisterRequest
  201. };