diff --git a/app.py b/app.py index ef0fbac1..26d89f64 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}") @@ -2619,11 +2734,19 @@ def wechat_callback(): state = request.args.get('state') error = request.args.get('error') - print(f"🎯 [CALLBACK] 微信回调被调用!code={code[:10] if code else None}..., state={state[:8] if state else None}..., error={error}") + # 错误处理:用户拒绝授权 + 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 error or not code or not state: - print(f"❌ [CALLBACK] 参数错误: error={error}, has_code={bool(code)}, has_state={bool(state)}") + # 参数验证 + 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 @@ -2643,20 +2766,28 @@ def wechat_callback(): return redirect('/auth/signin?error=session_expired') try: - # 获取access_token - print(f"🔑 [CALLBACK] 开始获取 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: - print(f"❌ [CALLBACK] 获取 access_token 失败") + session_data['status'] = 'auth_failed' + session_data['error'] = '获取访问令牌失败' + print(f"❌ 获取微信access_token失败: state={state}") return redirect('/auth/signin?error=token_failed') - print(f"✅ [CALLBACK] 获取 access_token 成功, openid={token_data.get('openid', '')[:8]}...") + # 步骤3: Token获取成功,标记为已授权 + session_data['status'] = 'authorized' + print(f"✅ 微信授权成功: openid={token_data['openid']}") - # 获取用户信息 - print(f"👤 [CALLBACK] 开始获取用户信息...") + # 步骤4: 获取用户信息 user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid']) if not user_info: - print(f"❌ [CALLBACK] 获取用户信息失败") + session_data['status'] = 'auth_failed' + session_data['error'] = '获取用户信息失败' + print(f"❌ 获取微信用户信息失败: openid={token_data['openid']}") return redirect('/auth/signin?error=userinfo_failed') print(f"✅ [CALLBACK] 获取用户信息成功, nickname={user_info.get('nickname', 'N/A')}") @@ -2736,23 +2867,31 @@ def wechat_callback(): db.session.add(user) db.session.commit() - # 更新 wechat_qr_sessions 状态,供前端轮询检测 - print(f"🔍 [DEBUG] state={state}, state in wechat_qr_sessions: {state in wechat_qr_sessions}") + is_new_user = True + print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}") + + # 更新最后登录时间 + user.update_last_seen() + + # 设置session + session.permanent = True + session['user_id'] = user.id + session['username'] = user.username + session['logged_in'] = True + session['wechat_login'] = True # 标记是微信登录 + + # Flask-Login 登录 + login_user(user, remember=True) + + # 更新微信session状态,供前端轮询检测 if state in wechat_qr_sessions: session_item = wechat_qr_sessions[state] - mode = session_item.get('mode') - print(f"🔍 [DEBUG] session_item mode: {mode}, is_new_user: {is_new_user}") - # 不是绑定模式才更新为登录状态 - if not mode: - new_status = 'register_success' if is_new_user else 'login_success' - session_item['status'] = new_status - session_item['user_info'] = { - 'user_id': user.id, - 'is_new_user': is_new_user - } - print(f"✅ [DEBUG] 更新 wechat_qr_sessions 状态: {new_status}, user_id: {user.id}") - else: - print(f"⚠️ [DEBUG] 跳过状态更新,因为 mode={mode}") + # 仅处理登录/注册流程,不处理绑定流程 + 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 ''' @@ -2772,10 +2911,16 @@ def wechat_callback(): ''', 200 except Exception as e: - print(f"❌ [CALLBACK] 微信登录失败: {e}") + print(f"❌ 微信登录失败: {e}") import traceback - print(f"❌ [CALLBACK] 错误堆栈:\n{traceback.format_exc()}") + 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 9a01abc9..54ff7b99 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"; } }; @@ -215,6 +217,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;