Compare commits
8 Commits
71df2b605b
...
feature_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2380c420c | ||
| 8417ab17be | |||
| dd59cb6385 | |||
|
|
d456c3cd5f | ||
|
|
b221c2669c | ||
|
|
356f865f09 | ||
| 512aca16d8 | |||
|
|
e05ea154a2 |
181
app.py
181
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')
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -15,6 +15,8 @@ import { FaQrcode } from "react-icons/fa";
|
||||
import { FiAlertCircle } from "react-icons/fi";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
// 配置常量
|
||||
@@ -33,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";
|
||||
}
|
||||
};
|
||||
@@ -45,6 +49,10 @@ const getStatusText = (status) => {
|
||||
};
|
||||
|
||||
export default function WechatRegister() {
|
||||
// 获取关闭弹窗方法
|
||||
const { closeModal } = useAuthModal();
|
||||
const { refreshSession } = useAuth();
|
||||
|
||||
// 状态管理
|
||||
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
|
||||
const [wechatSessionId, setWechatSessionId] = useState("");
|
||||
@@ -58,6 +66,7 @@ export default function WechatRegister() {
|
||||
const timeoutRef = useRef(null);
|
||||
const isMountedRef = useRef(true); // 追踪组件挂载状态
|
||||
const containerRef = useRef(null); // 容器DOM引用
|
||||
const sessionIdRef = useRef(null); // 存储最新的 sessionId,避免闭包陷阱
|
||||
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
@@ -90,6 +99,7 @@ export default function WechatRegister() {
|
||||
|
||||
/**
|
||||
* 清理所有定时器
|
||||
* 注意:不清理 sessionIdRef,因为 startPolling 时也会调用此函数
|
||||
*/
|
||||
const clearTimers = useCallback(() => {
|
||||
if (pollIntervalRef.current) {
|
||||
@@ -124,14 +134,14 @@ export default function WechatRegister() {
|
||||
}
|
||||
|
||||
showSuccess(
|
||||
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "注册成功",
|
||||
"正在跳转..."
|
||||
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "欢迎回来!"
|
||||
);
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
navigate("/home");
|
||||
}, 1000);
|
||||
// 刷新 AuthContext 状态
|
||||
await refreshSession();
|
||||
|
||||
// 关闭认证弹窗,留在当前页面
|
||||
closeModal();
|
||||
} else {
|
||||
throw new Error(response?.error || '登录失败');
|
||||
}
|
||||
@@ -139,17 +149,27 @@ export default function WechatRegister() {
|
||||
logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId });
|
||||
showError("登录失败", error.message || "请重试");
|
||||
}
|
||||
}, [navigate, showSuccess, showError]);
|
||||
}, [showSuccess, showError, closeModal, refreshSession]);
|
||||
|
||||
/**
|
||||
* 检查微信扫码状态
|
||||
* 使用 sessionIdRef.current 避免闭包陷阱
|
||||
*/
|
||||
const checkWechatStatus = useCallback(async () => {
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current || !wechatSessionId) return;
|
||||
// 检查组件是否已卸载,使用 ref 获取最新的 sessionId
|
||||
if (!isMountedRef.current || !sessionIdRef.current) {
|
||||
logger.debug('WechatRegister', 'checkWechatStatus 跳过', {
|
||||
isMounted: isMountedRef.current,
|
||||
hasSessionId: !!sessionIdRef.current
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSessionId = sessionIdRef.current;
|
||||
logger.debug('WechatRegister', '检查微信状态', { sessionId: currentSessionId });
|
||||
|
||||
try {
|
||||
const response = await authService.checkWechatStatus(wechatSessionId);
|
||||
const response = await authService.checkWechatStatus(currentSessionId);
|
||||
|
||||
// 安全检查:确保 response 存在且包含 status
|
||||
if (!response || typeof response.status === 'undefined') {
|
||||
@@ -158,6 +178,7 @@ export default function WechatRegister() {
|
||||
}
|
||||
|
||||
const { status } = response;
|
||||
logger.debug('WechatRegister', '微信状态', { status });
|
||||
|
||||
// 组件卸载后不再更新状态
|
||||
if (!isMountedRef.current) return;
|
||||
@@ -167,23 +188,14 @@ export default function WechatRegister() {
|
||||
// 处理成功状态
|
||||
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
|
||||
clearTimers(); // 停止轮询
|
||||
sessionIdRef.current = null; // 清理 sessionId
|
||||
|
||||
// 显示"扫码成功,登录中"提示
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "扫码成功",
|
||||
description: "正在登录,请稍候...",
|
||||
status: "info",
|
||||
duration: 2000,
|
||||
isClosable: false,
|
||||
});
|
||||
}
|
||||
|
||||
await handleLoginSuccess(wechatSessionId, status);
|
||||
await handleLoginSuccess(currentSessionId, status);
|
||||
}
|
||||
// 处理过期状态
|
||||
else if (status === WECHAT_STATUS.EXPIRED) {
|
||||
clearTimers();
|
||||
sessionIdRef.current = null; // 清理 sessionId
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "授权已过期",
|
||||
@@ -194,12 +206,40 @@ 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: wechatSessionId });
|
||||
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
|
||||
// 轮询过程中的错误不显示给用户,避免频繁提示
|
||||
// 但如果错误持续发生,停止轮询避免无限重试
|
||||
if (error.message.includes('网络连接失败')) {
|
||||
clearTimers();
|
||||
sessionIdRef.current = null; // 清理 sessionId
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "网络连接失败",
|
||||
@@ -211,12 +251,17 @@ export default function WechatRegister() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [wechatSessionId, handleLoginSuccess, clearTimers, toast]);
|
||||
}, [handleLoginSuccess, clearTimers, toast]);
|
||||
|
||||
/**
|
||||
* 启动轮询
|
||||
*/
|
||||
const startPolling = useCallback(() => {
|
||||
logger.debug('WechatRegister', '启动轮询', {
|
||||
sessionId: sessionIdRef.current,
|
||||
interval: POLL_INTERVAL
|
||||
});
|
||||
|
||||
// 清理旧的定时器
|
||||
clearTimers();
|
||||
|
||||
@@ -227,7 +272,9 @@ export default function WechatRegister() {
|
||||
|
||||
// 设置超时
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
logger.debug('WechatRegister', '二维码超时');
|
||||
clearTimers();
|
||||
sessionIdRef.current = null; // 清理 sessionId
|
||||
setWechatStatus(WECHAT_STATUS.EXPIRED);
|
||||
}, QR_CODE_TIMEOUT);
|
||||
}, [checkWechatStatus, clearTimers]);
|
||||
@@ -254,10 +301,17 @@ export default function WechatRegister() {
|
||||
throw new Error(response.message || '获取二维码失败');
|
||||
}
|
||||
|
||||
// 同时更新 ref 和 state,确保轮询能立即读取到最新值
|
||||
sessionIdRef.current = response.data.session_id;
|
||||
setWechatAuthUrl(response.data.auth_url);
|
||||
setWechatSessionId(response.data.session_id);
|
||||
setWechatStatus(WECHAT_STATUS.WAITING);
|
||||
|
||||
logger.debug('WechatRegister', '获取二维码成功', {
|
||||
sessionId: response.data.session_id,
|
||||
authUrl: response.data.auth_url
|
||||
});
|
||||
|
||||
// 启动轮询检查扫码状态
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
@@ -293,43 +347,10 @@ export default function WechatRegister() {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
clearTimers();
|
||||
sessionIdRef.current = null; // 清理 sessionId
|
||||
};
|
||||
}, [clearTimers]);
|
||||
|
||||
/**
|
||||
* 备用轮询机制 - 防止丢失状态
|
||||
* 每3秒检查一次,仅在获取到二维码URL且状态为waiting时执行
|
||||
*/
|
||||
useEffect(() => {
|
||||
// 只在有auth_url、session_id且状态为waiting时启动备用轮询
|
||||
if (wechatAuthUrl && wechatSessionId && wechatStatus === WECHAT_STATUS.WAITING) {
|
||||
logger.debug('WechatRegister', '备用轮询:启动备用轮询机制');
|
||||
|
||||
backupPollIntervalRef.current = setInterval(() => {
|
||||
try {
|
||||
if (wechatStatus === WECHAT_STATUS.WAITING && isMountedRef.current) {
|
||||
logger.debug('WechatRegister', '备用轮询:检查微信状态');
|
||||
// 添加 .catch() 静默处理异步错误,防止被 ErrorBoundary 捕获
|
||||
checkWechatStatus().catch(error => {
|
||||
logger.warn('WechatRegister', '备用轮询检查失败(静默处理)', { error: error.message });
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 捕获所有同步错误,防止被 ErrorBoundary 捕获
|
||||
logger.warn('WechatRegister', '备用轮询执行出错(静默处理)', { error: error.message });
|
||||
}
|
||||
}, BACKUP_POLL_INTERVAL);
|
||||
}
|
||||
|
||||
// 清理备用轮询
|
||||
return () => {
|
||||
if (backupPollIntervalRef.current) {
|
||||
clearInterval(backupPollIntervalRef.current);
|
||||
backupPollIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [wechatAuthUrl, wechatSessionId, wechatStatus, checkWechatStatus]);
|
||||
|
||||
/**
|
||||
* 测量容器尺寸并计算缩放比例
|
||||
*/
|
||||
@@ -397,7 +418,7 @@ export default function WechatRegister() {
|
||||
textAlign="center"
|
||||
mb={3} // 12px底部间距
|
||||
>
|
||||
微信扫码
|
||||
微信登陆
|
||||
</Heading>
|
||||
|
||||
{/* ========== 二维码区域 ========== */}
|
||||
@@ -414,19 +435,26 @@ export default function WechatRegister() {
|
||||
bg="gray.50"
|
||||
boxShadow="sm" // ✅ 添加轻微阴影
|
||||
>
|
||||
{wechatStatus === WECHAT_STATUS.WAITING ? (
|
||||
{wechatStatus !== WECHAT_STATUS.NONE ? (
|
||||
/* 已获取二维码:显示iframe */
|
||||
<iframe
|
||||
src={wechatAuthUrl}
|
||||
title="微信扫码登录"
|
||||
width="300"
|
||||
height="350"
|
||||
scrolling="no" // ✅ 新增:禁止滚动
|
||||
style={{
|
||||
border: 'none',
|
||||
transform: 'scale(0.77) translateY(-20px)', // ✅ 裁剪顶部logo
|
||||
transform: 'scale(0.77) translateY(-35px)', // ✅ 裁剪顶部logo
|
||||
transformOrigin: 'top left',
|
||||
marginLeft: '-5px'
|
||||
marginLeft: '-5px',
|
||||
pointerEvents: 'auto', // 允许点击 │ │
|
||||
overflow: 'hidden', // 尝试隐藏滚动条(可能不起作用)
|
||||
}}
|
||||
// 使用 onWheel 事件阻止滚动 │ │
|
||||
onWheel={(e) => e.preventDefault()} // ✅ 在父容器上阻止滚动
|
||||
onTouchMove={(e) => e.preventDefault()} // ✅ 移动端也阻止
|
||||
|
||||
/>
|
||||
) : (
|
||||
/* 未获取:显示占位符 */
|
||||
|
||||
@@ -136,7 +136,9 @@ export const authHandlers = [
|
||||
});
|
||||
|
||||
// 模拟微信授权 URL(实际是微信的 URL)
|
||||
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=mock&redirect_uri=&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
|
||||
// 使用真实的微信 AppID 和真实的授权回调地址(必须与微信开放平台配置的域名一致)
|
||||
const mockRedirectUri = encodeURIComponent('http://valuefrontier.cn/api/auth/wechat/callback');
|
||||
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=wxa8d74c47041b5f87&redirect_uri=${mockRedirectUri}&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
|
||||
|
||||
console.log('[Mock] 生成微信二维码:', { sessionId, authUrl });
|
||||
|
||||
@@ -147,16 +149,16 @@ export const authHandlers = [
|
||||
session.status = 'scanned';
|
||||
console.log(`[Mock] 模拟用户扫码: ${sessionId}`);
|
||||
|
||||
// 再过2秒自动确认登录
|
||||
// 再过5秒自动确认登录(延长时间让用户看到 scanned 状态)
|
||||
setTimeout(() => {
|
||||
const session2 = mockWechatSessions.get(sessionId);
|
||||
if (session2 && session2.status === 'scanned') {
|
||||
session2.status = 'confirmed';
|
||||
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与后端保持一致
|
||||
session2.user = {
|
||||
id: 999,
|
||||
nickname: '微信用户',
|
||||
wechat_openid: 'mock_openid_' + sessionId,
|
||||
avatar_url: 'https://i.pravatar.cc/150?img=99',
|
||||
avatar_url: 'https://ui-avatars.com/api/?name=微信用户&size=150&background=4299e1&color=fff',
|
||||
phone: null,
|
||||
email: null,
|
||||
has_wechat: true,
|
||||
@@ -168,6 +170,7 @@ export const authHandlers = [
|
||||
is_subscription_active: true,
|
||||
subscription_days_left: 0
|
||||
};
|
||||
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
|
||||
console.log(`[Mock] 模拟用户确认登录: ${sessionId}`, session2.user);
|
||||
}
|
||||
}, 2000);
|
||||
@@ -185,7 +188,7 @@ export const authHandlers = [
|
||||
}),
|
||||
|
||||
// 4. 检查微信扫码状态
|
||||
http.post('/api/auth/wechat/check-status', async ({ request }) => {
|
||||
http.post('/api/auth/wechat/check', async ({ request }) => {
|
||||
await delay(200); // 轮询请求,延迟短一些
|
||||
|
||||
const body = await request.json();
|
||||
@@ -209,18 +212,16 @@ export const authHandlers = [
|
||||
|
||||
console.log('[Mock] 检查微信状态:', { session_id, status: session.status });
|
||||
|
||||
// ✅ 返回与后端真实 API 一致的扁平化数据结构
|
||||
return HttpResponse.json({
|
||||
code: 0,
|
||||
message: '成功',
|
||||
data: {
|
||||
status: session.status,
|
||||
user: session.user
|
||||
}
|
||||
status: session.status,
|
||||
user_info: session.user_info,
|
||||
expires_in: Math.floor((session.createdAt + 5 * 60 * 1000 - Date.now()) / 1000)
|
||||
});
|
||||
}),
|
||||
|
||||
// 5. 微信登录确认
|
||||
http.post('/api/auth/wechat/login', async ({ request }) => {
|
||||
http.post('/api/auth/login/wechat', async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const body = await request.json();
|
||||
@@ -228,7 +229,7 @@ export const authHandlers = [
|
||||
|
||||
const session = mockWechatSessions.get(session_id);
|
||||
|
||||
if (!session || session.status !== 'confirmed') {
|
||||
if (!session || session.status !== 'authorized') { // ✅ 使用 'authorized' 状态,与前端保持一致
|
||||
return HttpResponse.json({
|
||||
success: false,
|
||||
error: '微信登录未确认或已过期'
|
||||
@@ -386,12 +387,12 @@ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK
|
||||
setTimeout(() => {
|
||||
const session2 = mockWechatSessions.get(targetSessionId);
|
||||
if (session2 && session2.status === 'scanned') {
|
||||
session2.status = 'confirmed';
|
||||
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与自动扫码流程保持一致
|
||||
session2.user = {
|
||||
id: 999,
|
||||
nickname: '微信测试用户',
|
||||
wechat_openid: 'mock_openid_' + targetSessionId,
|
||||
avatar_url: 'https://i.pravatar.cc/150?img=99',
|
||||
avatar_url: 'https://ui-avatars.com/api/?name=微信测试用户&size=150&background=4299e1&color=fff',
|
||||
phone: null,
|
||||
email: null,
|
||||
has_wechat: true,
|
||||
@@ -402,6 +403,7 @@ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_MOCK
|
||||
is_subscription_active: true,
|
||||
subscription_days_left: 0
|
||||
};
|
||||
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
|
||||
console.log(`[Mock API] ✅ 模拟确认登录: ${targetSessionId}`, session2.user);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -144,19 +144,23 @@ export const WECHAT_STATUS = {
|
||||
WAITING: 'waiting',
|
||||
SCANNED: 'scanned',
|
||||
AUTHORIZED: 'authorized',
|
||||
LOGIN_SUCCESS: 'login_success',
|
||||
REGISTER_SUCCESS: 'register_success',
|
||||
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
||||
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
||||
EXPIRED: 'expired',
|
||||
AUTH_DENIED: 'auth_denied', // 用户拒绝授权
|
||||
AUTH_FAILED: 'auth_failed', // 授权失败
|
||||
};
|
||||
|
||||
/**
|
||||
* 状态提示信息映射
|
||||
*/
|
||||
export const STATUS_MESSAGES = {
|
||||
[WECHAT_STATUS.WAITING]: '请使用微信扫码',
|
||||
[WECHAT_STATUS.WAITING]: '使用微信扫一扫登陆',
|
||||
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
|
||||
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
|
||||
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
|
||||
[WECHAT_STATUS.AUTH_DENIED]: '用户取消授权',
|
||||
[WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试',
|
||||
};
|
||||
|
||||
export default authService;
|
||||
|
||||
Reference in New Issue
Block a user