This commit is contained in:
sjk
2026-01-10 21:46:50 +08:00
parent 3b66018271
commit 213229953b
14 changed files with 1499 additions and 282 deletions

View File

@@ -15,6 +15,7 @@ type EnvType = 'dev' | 'test' | 'prod';
interface EnvConfig {
baseURL: string; // 主服务地址
pythonURL?: string; // Python服务地址(可选)
websocketURL?: string; // WebSocket服务地址(可选默认使用pythonURL)
timeout: number; // 请求超时时间
}
@@ -27,6 +28,7 @@ const API_CONFIG: Record<EnvType, EnvConfig> = {
dev: {
baseURL: 'http://localhost:8080', // 本地Go服务
pythonURL: 'http://localhost:8000', // 本地Python服务
websocketURL: 'ws://localhost:8000', // 本地WebSocket服务
timeout: 90000
},
@@ -34,6 +36,7 @@ const API_CONFIG: Record<EnvType, EnvConfig> = {
test: {
baseURL: 'https://lehang.tech', // 测试服务器Go服务
pythonURL: 'https://lehang.tech', // 测试服务器Python服务
websocketURL: 'wss://lehang.tech', // 测试服务器WebSocket服务
timeout: 90000
},
@@ -41,6 +44,7 @@ const API_CONFIG: Record<EnvType, EnvConfig> = {
prod: {
baseURL: 'https://lehang.tech', // 生产环境Go服务
pythonURL: 'https://lehang.tech', // 生产环境Python服务
websocketURL: 'wss://lehang.tech', // 生产环境WebSocket服务
timeout: 90000
}
};
@@ -90,6 +94,9 @@ console.log(`[API Config] 主服务: ${currentConfig().baseURL}`);
if (currentConfig().pythonURL) {
console.log(`[API Config] Python服务: ${currentConfig().pythonURL}`);
}
if (currentConfig().websocketURL) {
console.log(`[API Config] WebSocket服务: ${currentConfig().websocketURL}`);
}
// API端点
export const API = {
@@ -100,6 +107,16 @@ export const API = {
get pythonURL(): string | undefined {
return currentConfig().pythonURL;
},
get websocketURL(): string {
// 如果配置了websocketURL则使用否则从pythonURL或baseURL自动转换
const config = currentConfig();
if (config.websocketURL) {
return config.websocketURL;
}
// 降级从pythonURL或baseURL自动转换
const httpURL = config.pythonURL || config.baseURL;
return httpURL.replace('http://', 'ws://').replace('https://', 'wss://');
},
get timeout(): number {
return currentConfig().timeout;
},

View File

@@ -1,6 +1,6 @@
// pages/profile/platform-bind/platform-bind.ts
import { EmployeeService } from '../../../services/employee';
import { API } from '../../../config/api';
import { API, isDevelopment } from '../../../config/api';
Page({
data: {
@@ -18,10 +18,15 @@ Page({
pollTimer: null as any, // 轮询定时器
pollCount: 0, // 轮询次数
socketTask: null as any, // WebSocket连接
socketConnected: false, // WebSocket连接状态
socketReadyState: -1, // WebSocket ReadyState (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)
// 验证码相关
needCaptcha: false, // 是否需要验证码
captchaType: '', // 验证码类型
qrcodeImage: '', // 二维码图片base64
captchaTitle: '', // 二维码标题提示
qrcodeStatus: 0, // 二维码状态 (1=等待扫码, 2=已扫码, 4=验证成功, 5=已过期)
qrcodeStatusText: '', // 二维码状态文本
sessionId: '', // 发送验证码时返回的session_id用于复用浏览器
// 登录方式
loginType: 'phone' as 'phone' | 'qrcode', // phone: 手机号登录, qrcode: 扫码登录
@@ -34,23 +39,23 @@ Page({
qrId: '', // 二维码ID
qrCode: '', // 二维码code
qrcodeError: '', // 二维码加载错误提示
qrcodeLoading: false // 二维码是否正在加载
qrcodeLoading: false, // 二维码是否正在加载
isDevelopment: isDevelopment(), // 是否开发环境
isGettingCode: false // 是否正在获取验证码(防抖)
},
onLoad() {
console.log('小红书绑定页面加载');
// 页面加载时就生成session_id并建立WebSocket连接
const sessionId = `xhs_login_${Date.now().toString(36)}${Math.random().toString(36).substr(2, 9)}`;
console.log('[页面加载] 生成session_id:', sessionId);
this.setData({ sessionId });
// 建立WebSocket连接
console.log('[页面加载] 建立WebSocket连接...');
this.connectWebSocket(sessionId);
// 不再在页面加载时建立连接,等待用户输入手机号并点击获取验证码
console.log('[页面加载] 等待用户输入手机号...');
},
onUnload() {
console.log('[页面生命周期] onUnload - 页面卸载');
// 清理sessionId阻止重连
this.setData({ sessionId: '' });
if (this.data.countdownTimer) {
clearInterval(this.data.countdownTimer);
}
@@ -59,8 +64,69 @@ Page({
clearInterval(this.data.pollTimer);
}
// 关闭WebSocket连接
if (this.data.socketTask) {
this.data.socketTask.close();
console.log('[页面生命周期] 关闭WebSocket连接');
this.closeWebSocket();
},
onHide() {
console.log('[页面生命周期] onHide - 页面隐藏');
console.log('[页面生命周期] 当前登录方式:', this.data.loginType);
console.log('[页面生命周期] 是否正在等待登录结果:', (this.data as any).waitingLoginResult);
console.log('[页面生命周期] 是否需要验证码:', this.data.needCaptcha);
// 临时标记为隐藏状态,阻止重连
this.setData({ pageHidden: true } as any);
// 如果正在等待登录结果不关闭WebSocket
if ((this.data as any).waitingLoginResult) {
console.log('[页面生命周期] 正在等待登录结果保持WebSocket连接');
return;
}
// 如果是扫码登录模式不关闭WebSocket用户可能切到小红书APP扫码
if (this.data.loginType === 'qrcode') {
console.log('[页面生命周期] 扫码登录模式保持WebSocket连接用户可能在扫码');
return;
}
// 如果出现风控验证码弹窗不关闭WebSocket用户可能切到小红书APP扫码
if (this.data.needCaptcha) {
console.log('[页面生命周期] 风控验证中保持WebSocket连接用户可能在扫码');
return;
}
console.log('[页面生命周期] 关闭WebSocket连接');
// 页面隐藏时也关闭WebSocket连接
this.closeWebSocket();
},
onShow() {
console.log('[页面生命周期] onShow - 页面显示');
// 清除隐藏标记
this.setData({ pageHidden: false } as any);
// 检查WebSocket连接状态
const socketTask = this.data.socketTask;
if (socketTask) {
try {
const readyState = (socketTask as any).readyState;
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
if (readyState === 0 || readyState === 1) {
console.log('[页面生命周期] WebSocket连接已存在且正常无需重建');
return;
}
console.log(`[页面生命周期] WebSocket连接状态异常: ${readyState},准备重建`);
} catch (e) {
console.log('[页面生命周期] 检查WebSocket状态失败:', e);
}
}
// 页面显示时重新建立连接
const sessionId = this.data.sessionId;
if (sessionId) {
console.log('[页面生命周期] 重新建立WebSocket连接:', sessionId);
this.connectWebSocket(sessionId);
}
},
@@ -73,9 +139,19 @@ Page({
// 手机号输入
onPhoneInput(e: any) {
const phone = e.detail.value;
this.setData({
phone: e.detail.value
phone: phone
});
// 不再在输入手机号时建立连接,等待点击获取验证码
if (phone.length === 11) {
const sessionId = `xhs_login_${phone}`;
console.log('[手机号输入] 手机号已输入完成:', phone);
console.log('[手机号输入] 生成session_id:', sessionId);
// 只更新session_id不建立连接
this.setData({ sessionId });
}
},
// 验证码输入
@@ -87,11 +163,17 @@ Page({
// 获取验证码
async getVerifyCode() {
// 防抖:如果正在获取中,直接返回
if (this.data.isGettingCode) {
console.log('[发送验证码] 正在处理中,忽略重复点击');
return;
}
if (this.data.countdown > 0) {
return;
}
const { phone, countryCodes, countryCodeIndex, sessionId, socketTask } = this.data;
const { phone, countryCodes, countryCodeIndex } = this.data;
if (phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
@@ -101,26 +183,96 @@ Page({
return;
}
// 设置防抖标记
this.setData({ isGettingCode: true });
// 立即显示加载动画
wx.showToast({
title: '正在连接...',
icon: 'loading',
duration: 20000,
mask: true
});
try {
const countryCode = countryCodes[countryCodeIndex];
console.log('[发送验证码] 开始,手机号:', phone, '区号:', countryCode);
console.log('[发送验证码] 使用现有session_id:', sessionId);
// 使用手机号生成session_id
const sessionId = `xhs_login_${phone}`;
// 检查WebSocket连接
console.log('[发送验证码] 手机号:', phone);
console.log('[发送验证码] session_id:', sessionId);
// 更新session_id
this.setData({ sessionId });
// 检查是否已经有连接
const socketTask = this.data.socketTask;
if (!socketTask) {
console.error('[发送验证码] WebSocket连接不存在重新建立...');
// 没有连接,建立新连接
console.log('[发送验证码] 没有WebSocket连接建立连接...');
this.connectWebSocket(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
// 等待连接建立
console.log('[发送验证码] 等待连接建立...');
await new Promise(resolve => setTimeout(resolve, 2000));
} else {
// 已有连接,检查状态
const readyState = (socketTask as any).readyState;
console.log('[发送验证码] 已有连接,状态:', readyState);
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
if (readyState === 3 || readyState === 2) {
// 连接已关闭或正在关闭,重新建立
console.log('[发送验证码] 连接已关闭,重新建立...');
this.connectWebSocket(sessionId);
await new Promise(resolve => setTimeout(resolve, 2000));
} else if (readyState === 0) {
// 连接建立中,等待
console.log('[发送验证码] 连接建立中,等待...');
await new Promise(resolve => setTimeout(resolve, 1000));
}
// readyState === 1 连接已打开,直接使用
}
// 通过WebSocket发送send_code消息
console.log('[发送验证码] 通过WebSocket发送请求...');
const currentSocketTask = this.data.socketTask;
if (!currentSocketTask) {
throw new Error('WebSocket连接未建立');
const countryCode = countryCodes[countryCodeIndex];
// 再次检查WebSocket连接
const finalSocketTask = this.data.socketTask;
if (!finalSocketTask) {
console.log('[发送验证码] WebSocket连接不存在异常情况');
throw new Error('WebSocket连接未建立请刷新页面');
}
currentSocketTask.send({
// 检查连接状态
const readyState = (finalSocketTask as any).readyState;
console.log('[发送验证码] 最终WebSocket连接状态:', readyState);
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
if (readyState !== 1) {
// 如果连接还在建立中,再等待一下
if (readyState === 0) {
console.log('[发送验证码] 连接建立中,等待打开...');
await new Promise(resolve => setTimeout(resolve, 1000));
const newReadyState = (finalSocketTask as any).readyState;
console.log('[发送验证码] 等待后的连接状态:', newReadyState);
if (newReadyState !== 1) {
throw new Error('连接建立失败,请重试');
}
} else {
throw new Error('连接已关闭,请重试');
}
}
// 更新加载提示
wx.showToast({
title: '正在发送验证码...',
icon: 'loading',
duration: 20000,
mask: true
});
finalSocketTask.send({
data: JSON.stringify({
type: 'send_code',
phone: phone,
@@ -129,14 +281,11 @@ Page({
}),
success: () => {
console.log('[发送验证码] WebSocket消息发送成功');
wx.showToast({
title: '正在发送验证码...',
icon: 'loading',
duration: 20000
});
},
fail: (err: any) => {
console.error('[发送验证码] WebSocket消息发送失败:', err);
// 清除防抖标记
this.setData({ isGettingCode: false });
wx.showToast({
title: '发送失败,请重试',
icon: 'none',
@@ -147,6 +296,8 @@ Page({
} catch (error: any) {
console.error('[发送验证码] 异常:', error);
// 清除防抖标记
this.setData({ isGettingCode: false });
wx.showToast({
title: error.message || '发送失败,请重试',
icon: 'none',
@@ -232,6 +383,17 @@ Page({
throw new Error('WebSocket连接已断开请重新发送验证码');
}
// 检查连接状态
const readyState = (socketTask as any).readyState;
console.log('[绑定账号] WebSocket连接状态:', readyState);
if (readyState !== 1) {
throw new Error('WebSocket连接已关闭请重新发送验证码');
}
// 标记正在等待登录结果防止onHide关闭连接
this.setData({ waitingLoginResult: true } as any);
const countryCode = countryCodes[countryCodeIndex];
wx.showToast({
@@ -253,6 +415,8 @@ Page({
},
fail: (err: any) => {
console.error('[绑定账号] WebSocket消息发送失败:', err);
// 清除等待标记
this.setData({ waitingLoginResult: false } as any);
wx.showToast({
title: '发送失败,请重试',
icon: 'none',
@@ -263,6 +427,8 @@ Page({
} catch (error: any) {
console.error('[绑定账号] 异常:', error);
// 清除等待标记
this.setData({ waitingLoginResult: false } as any);
wx.showToast({
title: error.message || '绑定失败,请重试',
icon: 'none',
@@ -892,25 +1058,102 @@ Page({
}
},
// 保存验证码登录信息
async saveLoginInfo(loginData: any) {
try {
console.log('开始保存验证码登录信息...', loginData);
// 获取用户token
const token = wx.getStorageSync('token');
if (!token) {
throw new Error('未登录,请先登录');
}
const saveResult: any = await new Promise((resolve, reject) => {
wx.request({
url: `${API.baseURL}/api/xhs/save-login`,
method: 'POST',
header: {
'Content-Type': 'application/json'
},
data: {
employee_id: (wx.getStorageSync('employeeInfo') || {}).id,
storage_state: loginData.storage_state || {},
storage_state_path: loginData.storage_state_path || '',
user_info: loginData.user_info || {} // 新增: 传递用户信息
},
success: (res) => resolve(res.data),
fail: (err) => reject(err)
});
});
console.log('保存结果:', saveResult);
if (saveResult.code === 0 || saveResult.code === 200) {
// 更新本地缓存
const bindings = wx.getStorageSync('socialBindings') || {};
bindings.xiaohongshu = {
phone: this.data.phone,
xhs_account: '小红书用户',
bindTime: new Date().getTime(),
cookieExpired: false
};
wx.setStorageSync('socialBindings', bindings);
console.log('[验证码登录] 绑定成功,已更新本地缓存', bindings.xiaohongshu);
} else {
throw new Error(saveResult.message || '保存失败');
}
} catch (error: any) {
console.error('保存验证码登录信息失败:', error);
throw error; // 抛出错误让调用方处理
}
},
// 建立WebSocket连接
connectWebSocket(sessionId: string) {
console.log('[WebSocket] ========== connectWebSocket被调用 ==========');
console.log('[WebSocket] 调用栈:', new Error().stack?.split('\n').slice(1, 4).join('\n'));
console.log('[WebSocket] SessionID:', sessionId);
console.log('[WebSocket] 当前needCaptcha状态:', this.data.needCaptcha);
console.log('[WebSocket] ===========================================');
// 清除正常关闭标记,新连接默认为非正常关闭
// 重置重连计数,允许新连接重连
this.setData({
normalClose: false,
reconnectCount: 0
} as any);
// 关闭旧连接
if (this.data.socketTask) {
try {
console.log('[WebSocket] 检测到旧连接,准备关闭...');
// 标记为正常关闭,不触发重连
this.setData({
normalClose: true,
reconnectCount: 999
} as any);
this.data.socketTask.close();
console.log('[WebSocket] 已调用close(),等待关闭...');
} catch (e) {
console.log('[WebSocket] 关闭旧连接失败(可能已关闭):', e);
} finally {
// 重置标记,允许新连接重连
this.setData({
normalClose: false,
reconnectCount: 0
} as any);
}
}
// 获取Python服务地址WebSocket端点在Python后端
const pythonURL = API.pythonURL || API.baseURL;
// 将http/https转为ws/wss
const wsURL = pythonURL.replace('http://', 'ws://').replace('https://', 'wss://');
// 获取WebSocket服务地址使用配置化地址
const wsURL = API.websocketURL;
const url = `${wsURL}/ws/login/${sessionId}`;
console.log('[WebSocket] 开始连接:', url);
console.log('[WebSocket] Python服务地址:', pythonURL);
console.log('[WebSocket] WebSocket服务地址:', wsURL);
// 小程序环境检查
if (url.includes('localhost') || url.includes('127.0.0.1')) {
@@ -942,9 +1185,20 @@ Page({
// 监听连接打开
socketTask.onOpen(() => {
console.log('[WebSocket] 连接已打开');
console.log('[WebSocket] ========================================');
console.log('[WebSocket] 连接已成功打开!');
console.log('[WebSocket] Session ID:', sessionId);
console.log('[WebSocket] 连接URL:', url);
console.log('[WebSocket] 时间:', new Date().toLocaleString());
console.log('[WebSocket] ========================================');
socketConnected = true;
// 更新页面状态
this.setData({
socketConnected: true,
socketReadyState: 1 // OPEN
});
// 重置重连计数
this.setData({ reconnectCount: 0 } as any);
@@ -975,8 +1229,46 @@ Page({
try {
const data = JSON.parse(res.data as string);
// 处理二维码状态消息
if (data.type === 'qrcode_status') {
console.log('📡 二维码状态变化:', `status=${data.status}`, data.message);
// 更新二维码弹窗的提示文字和状态
if (this.data.needCaptcha) {
// 根据状态显示不同的提示
let captchaTitle = '扫码验证';
let qrcodeStatusText = '';
if (data.status === 1) {
captchaTitle = '请使用小红书APP扫码';
qrcodeStatusText = '等待扫码...';
} else if (data.status === 2) {
captchaTitle = '扫码完成请在APP中确认';
qrcodeStatusText = '请在小红书APP中点击确认';
} else if (data.status === 4) {
captchaTitle = '验证成功';
qrcodeStatusText = '验证完成,正在处理...';
} else if (data.status === 5) {
captchaTitle = '二维码已过期';
qrcodeStatusText = '二维码已过期,请重新获取';
} else {
captchaTitle = data.message || '扫码验证';
qrcodeStatusText = data.message || '';
}
// 更新弹窗标题和状态
this.setData({
captchaTitle: captchaTitle,
qrcodeStatus: data.status,
qrcodeStatusText: qrcodeStatusText
} as any);
console.log(`[二维码状态] 已更新提示: ${captchaTitle}`);
console.log(`[二维码状态] 状态文本: ${qrcodeStatusText}`);
}
}
// 处理扫码成功消息(发送验证码阶段的风控)
if (data.type === 'qrcode_scan_success') {
else if (data.type === 'qrcode_scan_success') {
console.log('✅ 扫码验证完成!', data.message);
// 关闭验证码弹窗
@@ -1019,6 +1311,9 @@ Page({
}
// 处理登录成功消息(点击登录按钮阶段的风控)
else if (data.type === 'login_success') {
// 清除等待标记
this.setData({ waitingLoginResult: false } as any);
// 判断是扫码验证成功还是真正的登录成功
if (data.storage_state) {
// 真正的登录成功,包含 storage_state
@@ -1034,27 +1329,46 @@ Page({
// 关闭WebSocket
this.closeWebSocket();
// 显示绑定成功动画
this.setData({
showSuccess: true
// 显示保存中
wx.showLoading({
title: '正在保存登录信息...',
mask: true
});
setTimeout(() => {
// 保存登录信息到数据库
this.saveLoginInfo(data).then(() => {
wx.hideLoading();
// 显示绑定成功动画
this.setData({
showSuccess: false
showSuccess: true
});
// 跳转回上一页
wx.navigateBack({
delta: 1,
success: () => {
console.log('✅ 跳转回上一页');
},
fail: (err) => {
console.error('⚠️ 跳转失败:', err);
}
setTimeout(() => {
this.setData({
showSuccess: false
});
// 跳转回上一页
wx.navigateBack({
delta: 1,
success: () => {
console.log('✅ 跳转回上一页');
},
fail: (err) => {
console.error('⚠️ 跳转失败:', err);
}
});
}, 2000);
}).catch((err) => {
wx.hideLoading();
console.error('保存登录信息失败:', err);
wx.showToast({
title: err.message || '保存失败,请重试',
icon: 'none',
duration: 3000
});
}, 2000);
});
} else {
// 扫码验证成功,但还需要继续登录
console.log('✅ 扫码验证成功!', data.user_info);
@@ -1101,6 +1415,9 @@ Page({
else if (data.type === 'code_sent') {
console.log('[WebSocket] 验证码发送结果:', data);
// 清除防抖标记
this.setData({ isGettingCode: false });
wx.hideToast();
if (data.success) {
@@ -1124,6 +1441,9 @@ Page({
else if (data.type === 'login_result') {
console.log('[WebSocket] 登录结果:', data);
// 清除等待标记
this.setData({ waitingLoginResult: false } as any);
wx.hideToast();
if (!data.success) {
@@ -1141,44 +1461,77 @@ Page({
// 监听错误
socketTask.onError((err) => {
console.error('[WebSocket] 连接错误:', err);
console.error('========== WebSocket连接错误 ==========')
console.error('[WebSocket] Session ID:', sessionId);
console.error('[WebSocket] 错误信息:', err);
console.error('[WebSocket] 错误类型:', err.errMsg || '未知');
console.error('===========================================')
socketConnected = false;
// 更新页面状态
this.setData({
socketConnected: false,
socketReadyState: 3 // CLOSED
});
// 记录错误,准备重连
console.log('[WebSocket] 将在关闭后尝试重连');
});
// 监听关闭
socketTask.onClose(() => {
console.log('[WebSocket] 连接关闭');
socketTask.onClose((res: any) => {
console.log('========== WebSocket连接关闭 ==========')
console.log('[WebSocket] Session ID:', sessionId);
console.log('[WebSocket] 关闭原因:', res.reason || '未提供原因');
console.log('[WebSocket] 关闭代码:', res.code || '未知');
console.log('[WebSocket] 是否正常关闭:', res.code === 1000 ? '是' : '否');
console.log('[WebSocket] 是否主动关闭:', (this.data as any).normalClose ? '是' : '否');
console.log('=========================================')
socketConnected = false;
// 更新页面状态
this.setData({
socketConnected: false,
socketReadyState: 3 // CLOSED
});
// 清理ping定时器
if ((this.data as any).pingTimer) {
clearInterval((this.data as any).pingTimer);
}
// 检查是否需要重连(只有弹窗还在时才重连)
if (this.data.needCaptcha && sessionId) {
// 增加重连计数
const reconnectCount = (this.data as any).reconnectCount || 0;
// 判断是否是正常关闭
const isNormalClose = (this.data as any).normalClose || res.code === 1000;
// 清除正常关闭标记
this.setData({ normalClose: false } as any);
// 检查是否需要重连(只要页面还在且未隐藏就重连)
// 增加重连计数
const reconnectCount = (this.data as any).reconnectCount || 0;
// 最多重连5次
if (reconnectCount < 5) {
const delay = Math.min(1000 * Math.pow(2, reconnectCount), 10000); // 指数退避: 1s, 2s, 4s, 8s, 10s
console.log(`[WebSocket] 将在${delay}ms后进行第${reconnectCount + 1}次重连`);
// 最多重连3次
if (reconnectCount < 3) {
const delay = Math.min(1000 * Math.pow(2, reconnectCount), 5000); // 指数退避: 1s, 2s, 4s
console.log(`[WebSocket] 将在${delay}ms后进行第${reconnectCount + 1}次重连`);
setTimeout(() => {
if (this.data.needCaptcha) { // 再次检查弹窗是否还在
console.log('[WebSocket] 开始重连...');
this.setData({ reconnectCount: reconnectCount + 1 } as any);
this.connectWebSocket(sessionId);
}
}, delay);
} else {
console.log('[WebSocket] 已达到最大重连次数,停止重连');
setTimeout(() => {
// 检查页面是否还在且未隐藏通过sessionId存在和pageHidden来判断
if (this.data.sessionId && !(this.data as any).pageHidden) {
console.log('[WebSocket] 开始重连...');
this.setData({ reconnectCount: reconnectCount + 1 } as any);
this.connectWebSocket(this.data.sessionId);
} else {
console.log('[WebSocket] 页面已退出或隐藏,取消重连');
}
}, delay);
} else {
console.log('[WebSocket] 已达到最大重连次数,停止重连');
// 只有非正常关闭才提示用户
if (!isNormalClose) {
wx.showToast({
title: 'WebSocket连接失败,请重新发送验证码',
title: 'WebSocket连接断开,请刷新页面',
icon: 'none',
duration: 3000
});
@@ -1193,8 +1546,11 @@ Page({
closeWebSocket() {
console.log('[WebSocket] 开始关闭连接');
// 重置重连计数(主动关闭不需要重连
this.setData({ reconnectCount: 999 } as any); // 设置为很大的数阻止重连
// 标记为主动关闭不需要重连和提示
this.setData({
reconnectCount: 999, // 阻止重连
normalClose: true // 标记为正常关闭
} as any);
// 清理ping定时器
if ((this.data as any).pingTimer) {
@@ -1236,6 +1592,23 @@ Page({
} else {
console.log('[WebSocket] 没有活跃的WebSocket连接');
}
// 重置连接状态显示
this.setData({
socketConnected: false,
socketReadyState: -1
});
},
// 强制关闭连接(手动按钮)
forceCloseWebSocket() {
console.log('[WebSocket] 用户手动关闭连接');
this.closeWebSocket();
wx.showToast({
title: '已关闭连接',
icon: 'success',
duration: 1000
});
},
// 保存二维码
@@ -1317,6 +1690,18 @@ Page({
});
},
// 关闭验证码弹窗
closeCaptcha() {
console.log('[关闭弹窗] 用户手动关闭二维码弹窗');
this.setData({
needCaptcha: false,
qrcodeImage: '',
captchaTitle: '',
qrcodeStatus: 0,
qrcodeStatusText: ''
});
},
// 测试WebSocket连接
testWebSocket() {
console.log('[测试] 开始测试WebSocket...');
@@ -1334,9 +1719,8 @@ Page({
}
}
// 获取WebSocket地址
const pythonURL = API.pythonURL || API.baseURL;
const wsURL = pythonURL.replace('http://', 'ws://').replace('https://', 'wss://');
// 获取WebSocket地址(使用配置化地址)
const wsURL = API.websocketURL;
const url = `${wsURL}/ws/login/${testSessionId}`;
console.log('[测试] WebSocket地址:', url);
@@ -1421,5 +1805,80 @@ Page({
});
this.setData({ socketTask });
},
// ========== 调试工具方法 ==========
/**
* 调试:显示指定状态的二维码
*/
debugShowQRCode(e: any) {
const status = parseInt(e.currentTarget.dataset.status);
console.log(`[调试工具] 显示状态1${status}的二维码`);
// 生成模拟二维码图片(使用纯色图片代替)
const colors: { [key: number]: string } = {
1: '#2196F3', // 蓝色 - 等待扫码
2: '#4CAF50', // 绿色 - 已扫码
4: '#FF9800', // 橙色 - 验证成功
5: '#F44336' // 红色 - 已过期
};
const color = colors[status] || '#999';
// 生成SVG二维码图片
const svgQRCode = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Crect width='200' height='200' fill='white'/%3E%3Crect x='10' y='10' width='180' height='180' fill='${encodeURIComponent(color)}'/%3E%3Ctext x='100' y='100' text-anchor='middle' dominant-baseline='middle' font-size='24' fill='white' font-weight='bold'%3E状态${status}%3C/text%3E%3C/svg%3E`;
// 根据状态设置标题和文本
let captchaTitle = '';
let qrcodeStatusText = '';
switch(status) {
case 1:
captchaTitle = '请使用小红书APP扫码';
qrcodeStatusText = '等待扫码...';
break;
case 2:
captchaTitle = '扫码完成请在APP中确认';
qrcodeStatusText = '请在小红书APP中点击确认';
break;
case 4:
captchaTitle = '验证成功';
qrcodeStatusText = '验证完成,正在处理...';
break;
case 5:
captchaTitle = '二维码已过期';
qrcodeStatusText = '二维码已过期,请重新获取';
break;
}
// 显示二维码弹窗
this.setData({
needCaptcha: true,
qrcodeImage: svgQRCode,
captchaTitle: captchaTitle,
qrcodeStatus: status,
qrcodeStatusText: qrcodeStatusText,
captchaType: 'qrcode'
});
wx.showToast({
title: `已切换至状态1${status}`,
icon: 'none',
duration: 1500
});
},
/**
* 调试:关闭二维码弹窗
*/
debugCloseQRCode() {
console.log('[调试工具] 关闭二维码弹窗');
this.setData({
needCaptcha: false,
qrcodeImage: '',
captchaTitle: '',
qrcodeStatus: 0,
qrcodeStatusText: ''
});
}
});

View File

@@ -1,10 +1,17 @@
<!--pages/profile/platform-bind/platform-bind.wxml-->
<view class="container">
<!-- WebSocket测试按钮 -->
<view style="position: fixed; top: 20rpx; right: 20rpx; z-index: 9999;">
<button style="padding: 10rpx 20rpx; font-size: 24rpx; background: #4CAF50; color: white; border-radius: 8rpx;" bindtap="testWebSocket">测试WebSocket</button>
<!-- 调试工具:二维码状态模拟 -->
<view wx:if="{{isDevelopment}}" style="position: fixed; bottom: 20rpx; right: 20rpx; z-index: 9999; background: rgba(0,0,0,0.85); padding: 20rpx; border-radius: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.3);">
<view style="color: #fff; font-size: 24rpx; font-weight: 600; margin-bottom: 16rpx; text-align: center;">🛠️ 调试工具</view>
<view style="display: flex; flex-direction: column; gap: 8rpx;">
<button style="padding: 8rpx 16rpx; font-size: 22rpx; background: #2196F3; color: white; border-radius: 6rpx; border: none;" bindtap="debugShowQRCode" data-status="1">状态1 - 等待扫码</button>
<button style="padding: 8rpx 16rpx; font-size: 22rpx; background: #4CAF50; color: white; border-radius: 6rpx; border: none;" bindtap="debugShowQRCode" data-status="2">状态2 - 已扫码</button>
<button style="padding: 8rpx 16rpx; font-size: 22rpx; background: #FF9800; color: white; border-radius: 6rpx; border: none;" bindtap="debugShowQRCode" data-status="4">状态4 - 验证成功</button>
<button style="padding: 8rpx 16rpx; font-size: 22rpx; background: #F44336; color: white; border-radius: 6rpx; border: none;" bindtap="debugShowQRCode" data-status="5">状态5 - 已过期</button>
<button style="padding: 8rpx 16rpx; font-size: 22rpx; background: #666; color: white; border-radius: 6rpx; border: none; margin-top: 8rpx;" bindtap="debugCloseQRCode">关闭弹窗</button>
</view>
</view>
<!-- 绑定内容 -->
<view class="bind-content">
<!-- 小红书Logo -->
@@ -80,12 +87,45 @@
<text class="loading-text">{{loadingText}}</text>
</view>
<!-- 二维码验证区域(验证码验证时出现 -->
<view class="qrcode-section" wx:if="{{needCaptcha && qrcodeImage}}">
<text class="qrcode-title">请使用小红书APP扫描二维码</text>
<image class="qrcode" src="{{qrcodeImage}}" mode="aspectFit"></image>
<text class="qrcode-hint">扫码后即可继续绑定流程</text>
<button class="save-qrcode-btn" bindtap="saveQRCode">保存二维码</button>
<!-- 二维码验证区域(扁平化设计 -->
<view class="qrcode-modal" wx:if="{{needCaptcha && qrcodeImage}}">
<view class="qrcode-mask" bindtap="closeCaptcha"></view>
<view class="qrcode-card">
<!-- 二维码图片 -->
<view class="qr-box">
<image class="qr-img"
src="{{qrcodeImage}}"
mode="aspectFit"
bindlongpress="saveQRCode"></image>
<!-- 已扫码透明层 -->
<view class="scan-overlay success" wx:if="{{qrcodeStatus === 2}}">
<view class="scan-icon">
<text class="icon-checkmark">✓</text>
</view>
</view>
<!-- 已过期透明层 -->
<view class="scan-overlay error" wx:if="{{qrcodeStatus === 5}}">
<view class="scan-icon">
<text class="icon-cross">×</text>
</view>
</view>
</view>
<!-- 提示文本 -->
<view class="qr-tips">
<text class="tip-main">{{captchaTitle || '请使用小红书APP扫码'}}</text>
<text class="tip-sub" wx:if="{{qrcodeStatus === 2}}">在APP中确认完成验证</text>
<text class="tip-sub" wx:if="{{qrcodeStatus !== 2 && qrcodeStatus !== 5}}">长按二维码可保存</text>
<text class="tip-sub error" wx:if="{{qrcodeStatus === 5}}">请关闭后重新获取</text>
</view>
</view>
<!-- 关闭按钮(外部) -->
<view class="close-btn-outer" bindtap="closeCaptcha">
<text class="close-icon">✕</text>
</view>
</view>
<view class="bind-form" wx:if="{{!needCaptcha}}">

View File

@@ -214,67 +214,191 @@ page {
width: 100%;
}
/* 二维码验证区域 */
.qrcode-section {
width: 100%;
/* ========== 二维码弹窗(扁平化设计) ========== */
/* 蒙层 */
.qrcode-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0;
margin-bottom: 48rpx;
justify-content: center;
}
.qrcode-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 48rpx;
text-align: center;
.qrcode-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
}
.qrcode {
width: 500rpx;
height: 500rpx;
border: 2rpx solid #E5E5E5;
border-radius: 16rpx;
padding: 32rpx;
/* 卡片 */
.qrcode-card {
position: relative;
z-index: 1;
width: 600rpx;
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
margin-bottom: 32rpx;
border-radius: 24rpx;
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.qrcode-hint {
font-size: 26rpx;
color: #999;
text-align: center;
line-height: 1.6;
margin-bottom: 24rpx;
}
/* 保存二维码按钮 */
.save-qrcode-btn {
width: 100%;
/* 外部关闭按钮 */
.close-btn-outer {
position: relative;
z-index: 1;
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #FF2442 0%, #FF4F6A 100%);
border-radius: 12rpx;
font-size: 30rpx;
font-weight: 500;
color: #fff;
border: none;
margin-top: 40rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 16rpx rgba(255, 36, 66, 0.3);
transition: all 0.3s;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
transition: all 0.2s;
}
.save-qrcode-btn::after {
border: none;
.close-btn-outer:active {
background: rgba(255, 255, 255, 0.7);
transform: scale(0.95);
}
.save-qrcode-btn:active {
opacity: 0.8;
transform: scale(0.98);
.close-icon {
font-size: 40rpx;
color: #666;
line-height: 1;
}
/* 二维码区域 */
.qr-box {
position: relative;
width: 420rpx;
height: 420rpx;
margin-bottom: 36rpx;
}
.qr-img {
width: 100%;
height: 100%;
padding: 24rpx;
background: #fff;
border: 2rpx solid #eee;
border-radius: 16rpx;
box-sizing: border-box;
}
/* 状态透明层(已扫码/已过期) */
.scan-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
.scan-overlay.success {
background: rgba(82, 196, 26, 0.15);
}
.scan-overlay.error {
background: rgba(255, 77, 79, 0.15);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 状态图标 */
.scan-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: scaleIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.scan-overlay.success .scan-icon {
background: #52C41A;
box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.4);
}
.scan-overlay.error .scan-icon {
background: #FF4D4F;
box-shadow: 0 8rpx 24rpx rgba(255, 77, 79, 0.4);
}
@keyframes scaleIn {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.icon-checkmark {
font-size: 72rpx;
color: #fff;
font-weight: bold;
line-height: 1;
}
.icon-cross {
font-size: 80rpx;
color: #fff;
font-weight: 300;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 提示文本 */
.qr-tips {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.tip-main {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
}
.tip-sub {
font-size: 26rpx;
color: #999;
text-align: center;
}
.tip-sub.error {
color: #FF4D4F;
}
.bind-form {