diff --git a/app.py b/app.py index c48734e0..09d858b9 100755 --- a/app.py +++ b/app.py @@ -1849,6 +1849,15 @@ def send_verification_code(): if not credential or not code_type: return jsonify({'success': False, 'error': '缺少必要参数'}), 400 + # 清理格式字符(空格、横线、括号等) + if code_type == 'phone': + # 移除手机号中的空格、横线、括号、加号等格式字符 + credential = re.sub(r'[\s\-\(\)\+]', '', credential) + print(f"📱 清理后的手机号: {credential}") + elif code_type == 'email': + # 邮箱只移除空格 + credential = credential.strip() + # 生成验证码 verification_code = generate_verification_code() @@ -1907,6 +1916,17 @@ def login_with_verification_code(): if not credential or not verification_code or not login_type: return jsonify({'success': False, 'error': '缺少必要参数'}), 400 + # 清理格式字符(空格、横线、括号等) + if login_type == 'phone': + # 移除手机号中的空格、横线、括号、加号等格式字符 + original_credential = credential + credential = re.sub(r'[\s\-\(\)\+]', '', credential) + if original_credential != credential: + print(f"📱 登录时清理手机号: {original_credential} -> {credential}") + elif login_type == 'email': + # 邮箱只移除前后空格 + credential = credential.strip() + # 检查验证码 session_key = f'verification_code_{login_type}_{credential}_login' stored_code_info = session.get(session_key) @@ -1968,12 +1988,51 @@ def login_with_verification_code(): username = f"{base_username}_{counter}" counter += 1 + # 如果用户不存在,自动创建新用户 + if not user: + try: + # 生成用户名 + if login_type == 'phone': + # 使用手机号生成用户名 + base_username = f"用户{credential[-4:]}" + elif login_type == 'email': + # 使用邮箱前缀生成用户名 + base_username = credential.split('@')[0] + else: + base_username = "新用户" + + # 确保用户名唯一 + username = base_username + counter = 1 + while User.is_username_taken(username): + username = f"{base_username}_{counter}" + counter += 1 + # 创建新用户 - user = User(username=username, email=credential) - user.email_confirmed = True + user = User(username=username) + + # 设置手机号或邮箱 + if login_type == 'phone': + user.phone = credential + elif login_type == 'email': + user.email = credential + + # 设置默认密码(使用随机密码,用户后续可以修改) + user.set_password(uuid.uuid4().hex) + user.status = 'active' + user.nickname = username + db.session.add(user) db.session.commit() + is_new_user = True + print(f"✅ 自动创建新用户: {username}, {login_type}: {credential}") + + except Exception as e: + print(f"❌ 创建用户失败: {e}") + db.session.rollback() + return jsonify({'success': False, 'error': '创建用户失败'}), 500 + # 清除验证码 session.pop(session_key, None) @@ -1989,10 +2048,13 @@ def login_with_verification_code(): # 更新最后登录时间 user.update_last_seen() + # 根据是否为新用户返回不同的消息 + message = '注册成功,欢迎加入!' if is_new_user else '登录成功' + return jsonify({ 'success': True, - 'message': '注册成功' if is_new_user else '登录成功', - 'isNewUser': is_new_user, + 'message': message, + 'is_new_user': is_new_user, 'user': { 'id': user.id, 'username': user.username, @@ -2004,6 +2066,59 @@ def login_with_verification_code(): } }) + except Exception as e: + print(f"验证码登录错误: {e}") + db.session.rollback() + return jsonify({'success': False, 'error': '登录失败'}), 500 + + +@app.route('/api/auth/register', methods=['POST']) +def register(): + """用户注册 - 使用Session""" + username = request.form.get('username') + email = request.form.get('email') + password = request.form.get('password') + + # 验证输入 + if not all([username, email, password]): + return jsonify({'success': False, 'error': '所有字段都是必填的'}), 400 + + # 检查用户名和邮箱是否已存在 + if User.is_username_taken(username): + return jsonify({'success': False, 'error': '用户名已存在'}), 400 + + if User.is_email_taken(email): + return jsonify({'success': False, 'error': '邮箱已被使用'}), 400 + + try: + # 创建新用户 + user = User(username=username, email=email) + user.set_password(password) + user.email_confirmed = True # 暂时默认已确认 + + db.session.add(user) + db.session.commit() + + # 自动登录 + session.permanent = True + session['user_id'] = user.id + session['username'] = user.username + session['logged_in'] = True + + # Flask-Login 登录 + login_user(user, remember=True) + + return jsonify({ + 'success': True, + 'message': '注册成功', + 'user': { + 'id': user.id, + 'username': user.username, + 'nickname': user.nickname or user.username, + 'email': user.email + } + }), 201 + except Exception as e: db.session.rollback() print(f"验证码登录/注册错误: {e}") @@ -2611,8 +2726,19 @@ def wechat_callback(): state = request.args.get('state') error = request.args.get('error') - # 错误处理 - if error or not code or not state: + # 错误处理:用户拒绝授权 + if error: + if state in wechat_qr_sessions: + wechat_qr_sessions[state]['status'] = 'auth_denied' + wechat_qr_sessions[state]['error'] = '用户拒绝授权' + print(f"❌ 用户拒绝授权: state={state}") + return redirect('/auth/signin?error=wechat_auth_denied') + + # 参数验证 + if not code or not state: + if state in wechat_qr_sessions: + wechat_qr_sessions[state]['status'] = 'auth_failed' + wechat_qr_sessions[state]['error'] = '授权参数缺失' return redirect('/auth/signin?error=wechat_auth_failed') # 验证state @@ -2627,14 +2753,28 @@ def wechat_callback(): return redirect('/auth/signin?error=session_expired') try: - # 获取access_token + # 步骤1: 用户已扫码并授权(微信回调过来说明用户已完成扫码+授权) + session_data['status'] = 'scanned' + print(f"✅ 微信扫码回调: state={state}, code={code[:10]}...") + + # 步骤2: 获取access_token token_data = get_wechat_access_token(code) if not token_data: + session_data['status'] = 'auth_failed' + session_data['error'] = '获取访问令牌失败' + print(f"❌ 获取微信access_token失败: state={state}") return redirect('/auth/signin?error=token_failed') - # 获取用户信息 + # 步骤3: Token获取成功,标记为已授权 + session_data['status'] = 'authorized' + print(f"✅ 微信授权成功: openid={token_data['openid']}") + + # 步骤4: 获取用户信息 user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid']) if not user_info: + session_data['status'] = 'auth_failed' + session_data['error'] = '获取用户信息失败' + print(f"❌ 获取微信用户信息失败: openid={token_data['openid']}") return redirect('/auth/signin?error=userinfo_failed') # 查找或创建用户 / 或处理绑定 @@ -2679,6 +2819,8 @@ def wechat_callback(): return redirect('/home?bind=failed') user = None + is_new_user = False + if unionid: user = User.query.filter_by(wechat_union_id=unionid).first() if not user: @@ -2709,6 +2851,9 @@ def wechat_callback(): db.session.add(user) db.session.commit() + is_new_user = True + print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}") + # 更新最后登录时间 user.update_last_seen() @@ -2722,18 +2867,30 @@ def wechat_callback(): # Flask-Login 登录 login_user(user, remember=True) - # 清理微信session(仅登录/注册流程清理;绑定流程在上方已处理,不在此处清理) + # 更新微信session状态,供前端轮询检测 if state in wechat_qr_sessions: - # 仅当不是绑定流程,或没有模式信息时清理 - if not wechat_qr_sessions[state].get('mode'): - del wechat_qr_sessions[state] + session_item = wechat_qr_sessions[state] + # 仅处理登录/注册流程,不处理绑定流程 + if not session_item.get('mode'): + # 更新状态和用户信息 + session_item['status'] = 'register_ready' if is_new_user else 'login_ready' + session_item['user_info'] = {'user_id': user.id} + print(f"✅ 微信扫码状态已更新: {session_item['status']}, user_id: {user.id}") # 直接跳转到首页 return redirect('/home') except Exception as e: print(f"❌ 微信登录失败: {e}") + import traceback + traceback.print_exc() db.session.rollback() + + # 更新session状态为失败 + if state in wechat_qr_sessions: + wechat_qr_sessions[state]['status'] = 'auth_failed' + wechat_qr_sessions[state]['error'] = str(e) + return redirect('/auth/signin?error=login_failed') diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js index 793f2293..6de1fa70 100644 --- a/src/components/Auth/AuthFormContent.js +++ b/src/components/Auth/AuthFormContent.js @@ -143,7 +143,10 @@ export default function AuthFormContent() { return; } - if (!/^1[3-9]\d{9}$/.test(credential)) { + // 清理手机号格式字符(空格、横线、括号等) + const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, ''); + + if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) { toast({ title: "请输入有效的手机号", status: "warning", @@ -156,7 +159,7 @@ export default function AuthFormContent() { setSendingCode(true); const requestData = { - credential: credential.trim(), // 添加 trim() 防止空格 + credential: cleanedCredential, // 使用清理后的手机号 type: 'phone', purpose: config.api.purpose }; @@ -189,13 +192,13 @@ export default function AuthFormContent() { if (response.ok && data.success) { // ❌ 移除成功 toast,静默处理 logger.info('AuthFormContent', '验证码发送成功', { - credential: credential.substring(0, 3) + '****' + credential.substring(7), + credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7), dev_code: data.dev_code }); // ✅ 开发环境下在控制台显示验证码 if (data.dev_code) { - console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;'); + console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;'); } setVerificationCodeSent(true); @@ -205,7 +208,7 @@ export default function AuthFormContent() { } } catch (error) { logger.api.error('POST', '/api/auth/send-verification-code', error, { - credential: credential.substring(0, 3) + '****' + credential.substring(7) + credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7) }); // ✅ 显示错误提示给用户 @@ -247,7 +250,10 @@ export default function AuthFormContent() { return; } - if (!/^1[3-9]\d{9}$/.test(phone)) { + // 清理手机号格式字符(空格、横线、括号等) + const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, ''); + + if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) { toast({ title: "请输入有效的手机号", status: "warning", @@ -258,13 +264,13 @@ export default function AuthFormContent() { // 构建请求体 const requestBody = { - credential: phone.trim(), // 添加 trim() 防止空格 + credential: cleanedPhone, // 使用清理后的手机号 verification_code: verificationCode.trim(), // 添加 trim() 防止空格 login_type: 'phone', }; logger.api.request('POST', '/api/auth/login-with-code', { - credential: phone.substring(0, 3) + '****' + phone.substring(7), + credential: cleanedPhone.substring(0, 3) + '****' + cleanedPhone.substring(7), verification_code: verificationCode.substring(0, 2) + '****', login_type: 'phone' }); diff --git a/src/components/Auth/WechatRegister.js b/src/components/Auth/WechatRegister.js index e4964e4a..6bbba397 100644 --- a/src/components/Auth/WechatRegister.js +++ b/src/components/Auth/WechatRegister.js @@ -35,6 +35,8 @@ const getStatusColor = (status) => { case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字 case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字 case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600"; + case WECHAT_STATUS.AUTH_DENIED: return "red.600"; // ✅ 红色文字 + case WECHAT_STATUS.AUTH_FAILED: return "red.600"; // ✅ 红色文字 default: return "gray.600"; } }; @@ -204,6 +206,33 @@ export default function WechatRegister() { }); } } + // 处理用户拒绝授权 + else if (status === WECHAT_STATUS.AUTH_DENIED) { + clearTimers(); + if (isMountedRef.current) { + toast({ + title: "授权已取消", + description: "您已取消微信授权登录", + status: "warning", + duration: 3000, + isClosable: true, + }); + } + } + // 处理授权失败 + else if (status === WECHAT_STATUS.AUTH_FAILED) { + clearTimers(); + if (isMountedRef.current) { + const errorMsg = response.error || "授权过程出现错误"; + toast({ + title: "授权失败", + description: errorMsg, + status: "error", + duration: 5000, + isClosable: true, + }); + } + } } catch (error) { logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId }); // 轮询过程中的错误不显示给用户,避免频繁提示 diff --git a/src/services/authService.js b/src/services/authService.js index 040e38db..a73369ef 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -147,6 +147,8 @@ export const WECHAT_STATUS = { LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized' REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized' EXPIRED: 'expired', + AUTH_DENIED: 'auth_denied', // 用户拒绝授权 + AUTH_FAILED: 'auth_failed', // 授权失败 }; /** @@ -157,6 +159,8 @@ export const STATUS_MESSAGES = { [WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认', [WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...', [WECHAT_STATUS.EXPIRED]: '二维码已过期', + [WECHAT_STATUS.AUTH_DENIED]: '用户取消授权', + [WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试', }; export default authService;