Merge branch '1028_bugfix' into feature_2025/1028_bugfix

* 1028_bugfix:
  手机号格式适配-前端修改
  添加微信扫码的几种其他状态
  整合register端口进入login端口
This commit is contained in:
zdl
2025-10-29 16:26:02 +08:00
4 changed files with 216 additions and 20 deletions

181
app.py
View File

@@ -1849,6 +1849,15 @@ def send_verification_code():
if not credential or not code_type: if not credential or not code_type:
return jsonify({'success': False, 'error': '缺少必要参数'}), 400 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() 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: if not credential or not verification_code or not login_type:
return jsonify({'success': False, 'error': '缺少必要参数'}), 400 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' session_key = f'verification_code_{login_type}_{credential}_login'
stored_code_info = session.get(session_key) stored_code_info = session.get(session_key)
@@ -1968,12 +1988,51 @@ def login_with_verification_code():
username = f"{base_username}_{counter}" username = f"{base_username}_{counter}"
counter += 1 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 = User(username=username)
user.email_confirmed = True
# 设置手机号或邮箱
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.add(user)
db.session.commit() 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) session.pop(session_key, None)
@@ -1989,10 +2048,13 @@ def login_with_verification_code():
# 更新最后登录时间 # 更新最后登录时间
user.update_last_seen() user.update_last_seen()
# 根据是否为新用户返回不同的消息
message = '注册成功,欢迎加入!' if is_new_user else '登录成功'
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '注册成功' if is_new_user else '登录成功', 'message': message,
'isNewUser': is_new_user, 'is_new_user': is_new_user,
'user': { 'user': {
'id': user.id, 'id': user.id,
'username': user.username, '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: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"验证码登录/注册错误: {e}") print(f"验证码登录/注册错误: {e}")
@@ -2611,8 +2726,19 @@ def wechat_callback():
state = request.args.get('state') state = request.args.get('state')
error = request.args.get('error') 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') return redirect('/auth/signin?error=wechat_auth_failed')
# 验证state # 验证state
@@ -2627,14 +2753,28 @@ def wechat_callback():
return redirect('/auth/signin?error=session_expired') return redirect('/auth/signin?error=session_expired')
try: 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) token_data = get_wechat_access_token(code)
if not token_data: 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') 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']) user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid'])
if not user_info: 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') return redirect('/auth/signin?error=userinfo_failed')
# 查找或创建用户 / 或处理绑定 # 查找或创建用户 / 或处理绑定
@@ -2679,6 +2819,8 @@ def wechat_callback():
return redirect('/home?bind=failed') return redirect('/home?bind=failed')
user = None user = None
is_new_user = False
if unionid: if unionid:
user = User.query.filter_by(wechat_union_id=unionid).first() user = User.query.filter_by(wechat_union_id=unionid).first()
if not user: if not user:
@@ -2709,6 +2851,9 @@ def wechat_callback():
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
is_new_user = True
print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}")
# 更新最后登录时间 # 更新最后登录时间
user.update_last_seen() user.update_last_seen()
@@ -2722,18 +2867,30 @@ def wechat_callback():
# Flask-Login 登录 # Flask-Login 登录
login_user(user, remember=True) login_user(user, remember=True)
# 清理微信session(仅登录/注册流程清理;绑定流程在上方已处理,不在此处清理) # 更新微信session状态,供前端轮询检测
if state in wechat_qr_sessions: if state in wechat_qr_sessions:
# 仅当不是绑定流程,或没有模式信息时清理 session_item = wechat_qr_sessions[state]
if not wechat_qr_sessions[state].get('mode'): # 仅处理登录/注册流程,不处理绑定流程
del 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') return redirect('/home')
except Exception as e: except Exception as e:
print(f"❌ 微信登录失败: {e}") print(f"❌ 微信登录失败: {e}")
import traceback
traceback.print_exc()
db.session.rollback() 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') return redirect('/auth/signin?error=login_failed')

View File

@@ -143,7 +143,10 @@ export default function AuthFormContent() {
return; return;
} }
if (!/^1[3-9]\d{9}$/.test(credential)) { // 清理手机号格式字符(空格、横线、括号等)
const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) {
toast({ toast({
title: "请输入有效的手机号", title: "请输入有效的手机号",
status: "warning", status: "warning",
@@ -156,7 +159,7 @@ export default function AuthFormContent() {
setSendingCode(true); setSendingCode(true);
const requestData = { const requestData = {
credential: credential.trim(), // 添加 trim() 防止空格 credential: cleanedCredential, // 使用清理后的手机号
type: 'phone', type: 'phone',
purpose: config.api.purpose purpose: config.api.purpose
}; };
@@ -189,13 +192,13 @@ export default function AuthFormContent() {
if (response.ok && data.success) { if (response.ok && data.success) {
// ❌ 移除成功 toast静默处理 // ❌ 移除成功 toast静默处理
logger.info('AuthFormContent', '验证码发送成功', { logger.info('AuthFormContent', '验证码发送成功', {
credential: credential.substring(0, 3) + '****' + credential.substring(7), credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7),
dev_code: data.dev_code dev_code: data.dev_code
}); });
// ✅ 开发环境下在控制台显示验证码 // ✅ 开发环境下在控制台显示验证码
if (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); setVerificationCodeSent(true);
@@ -205,7 +208,7 @@ export default function AuthFormContent() {
} }
} catch (error) { } catch (error) {
logger.api.error('POST', '/api/auth/send-verification-code', 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; return;
} }
if (!/^1[3-9]\d{9}$/.test(phone)) { // 清理手机号格式字符(空格、横线、括号等)
const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) {
toast({ toast({
title: "请输入有效的手机号", title: "请输入有效的手机号",
status: "warning", status: "warning",
@@ -258,13 +264,13 @@ export default function AuthFormContent() {
// 构建请求体 // 构建请求体
const requestBody = { const requestBody = {
credential: phone.trim(), // 添加 trim() 防止空格 credential: cleanedPhone, // 使用清理后的手机号
verification_code: verificationCode.trim(), // 添加 trim() 防止空格 verification_code: verificationCode.trim(), // 添加 trim() 防止空格
login_type: 'phone', login_type: 'phone',
}; };
logger.api.request('POST', '/api/auth/login-with-code', { 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) + '****', verification_code: verificationCode.substring(0, 2) + '****',
login_type: 'phone' login_type: 'phone'
}); });

View File

@@ -35,6 +35,8 @@ const getStatusColor = (status) => {
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字 case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字 case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
case WECHAT_STATUS.REGISTER_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"; 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) { } catch (error) {
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId }); logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
// 轮询过程中的错误不显示给用户,避免频繁提示 // 轮询过程中的错误不显示给用户,避免频繁提示

View File

@@ -147,6 +147,8 @@ export const WECHAT_STATUS = {
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized' LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized' REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
EXPIRED: 'expired', EXPIRED: 'expired',
AUTH_DENIED: 'auth_denied', // 用户拒绝授权
AUTH_FAILED: 'auth_failed', // 授权失败
}; };
/** /**
@@ -157,6 +159,8 @@ export const STATUS_MESSAGES = {
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认', [WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...', [WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
[WECHAT_STATUS.EXPIRED]: '二维码已过期', [WECHAT_STATUS.EXPIRED]: '二维码已过期',
[WECHAT_STATUS.AUTH_DENIED]: '用户取消授权',
[WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试',
}; };
export default authService; export default authService;