commit
This commit is contained in:
@@ -56,7 +56,7 @@ PROXY_POOL = [
|
|||||||
"server": "http://210.51.27.194:50001",
|
"server": "http://210.51.27.194:50001",
|
||||||
"username": "hb6su3",
|
"username": "hb6su3",
|
||||||
"password": "acv2ciow",
|
"password": "acv2ciow",
|
||||||
"enabled": False
|
"enabled": True
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 245 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
344
backend/main.py
344
backend/main.py
@@ -13,7 +13,7 @@ from config import init_config, get_config
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv() # 从 .env 文件加载环境变量(可选,用于覆盖配置文件)
|
load_dotenv() # 从 .env 文件加载环境变量(可选,用于覆盖配置文件)
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
from fastapi import FastAPI, HTTPException, File, UploadFile, Form, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
@@ -51,6 +51,75 @@ scheduler = None
|
|||||||
# 全局阿里云短信服务实例
|
# 全局阿里云短信服务实例
|
||||||
sms_service = None
|
sms_service = None
|
||||||
|
|
||||||
|
# WebSocket连接管理器
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self):
|
||||||
|
# session_id -> WebSocket连接
|
||||||
|
self.active_connections: Dict[str, WebSocket] = {}
|
||||||
|
# session_id -> 消息队列(用于缓存连接建立前的消息)
|
||||||
|
self.pending_messages: Dict[str, list] = {}
|
||||||
|
|
||||||
|
async def connect(self, session_id: str, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections[session_id] = websocket
|
||||||
|
print(f"[WebSocket] 新连接: {session_id}", file=sys.stderr)
|
||||||
|
print(f"[WebSocket] 当前活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 立即检查缓存消息(不等待)
|
||||||
|
if session_id in self.pending_messages:
|
||||||
|
pending_count = len(self.pending_messages[session_id])
|
||||||
|
print(f"[WebSocket] 发现缓存消息: {pending_count} 条", file=sys.stderr)
|
||||||
|
print(f"[WebSocket] 缓存消息内容: {self.pending_messages[session_id]}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 等待100ms让前端监听器就绪
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
for idx, message in enumerate(self.pending_messages[session_id]):
|
||||||
|
try:
|
||||||
|
print(f"[WebSocket] 准备发送第{idx+1}条消息...", file=sys.stderr)
|
||||||
|
await websocket.send_json(message)
|
||||||
|
print(f"[WebSocket] 已发送缓存消息 [{idx+1}/{pending_count}]: {message.get('type')}", file=sys.stderr)
|
||||||
|
# 每条消息间隔100ms
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WebSocket] 发送缓存消息失败: {str(e)}", file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
del self.pending_messages[session_id]
|
||||||
|
print(f"[WebSocket] 缓存消息已清空: {session_id}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"[WebSocket] 没有缓存消息: {session_id}", file=sys.stderr)
|
||||||
|
|
||||||
|
def disconnect(self, session_id: str):
|
||||||
|
if session_id in self.active_connections:
|
||||||
|
del self.active_connections[session_id]
|
||||||
|
print(f"[WebSocket] 断开连接: {session_id}", file=sys.stderr)
|
||||||
|
# 清理缓存消息
|
||||||
|
if session_id in self.pending_messages:
|
||||||
|
del self.pending_messages[session_id]
|
||||||
|
|
||||||
|
async def send_message(self, session_id: str, message: dict):
|
||||||
|
if session_id in self.active_connections:
|
||||||
|
try:
|
||||||
|
await self.active_connections[session_id].send_json(message)
|
||||||
|
print(f"[WebSocket] 发送消息到 {session_id}: {message.get('type')}", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WebSocket] 发送消息失败 {session_id}: {str(e)}", file=sys.stderr)
|
||||||
|
self.disconnect(session_id)
|
||||||
|
else:
|
||||||
|
# WebSocket还未连接,缓存消息
|
||||||
|
print(f"[WebSocket] 连接尚未建立,缓存消息: {session_id}", file=sys.stderr)
|
||||||
|
if session_id not in self.pending_messages:
|
||||||
|
self.pending_messages[session_id] = []
|
||||||
|
self.pending_messages[session_id].append(message)
|
||||||
|
# 最多缓存10条消息
|
||||||
|
if len(self.pending_messages[session_id]) > 10:
|
||||||
|
self.pending_messages[session_id].pop(0)
|
||||||
|
|
||||||
|
# 全局WebSocket管理器
|
||||||
|
ws_manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
async def fetch_proxy_from_pool() -> Optional[str]:
|
async def fetch_proxy_from_pool() -> Optional[str]:
|
||||||
"""从代理池接口获取一个代理地址(http://ip:port),获取失败返回None"""
|
"""从代理池接口获取一个代理地址(http://ip:port),获取失败返回None"""
|
||||||
@@ -97,6 +166,7 @@ class SendCodeRequest(BaseModel):
|
|||||||
phone: str
|
phone: str
|
||||||
country_code: str = "+86"
|
country_code: str = "+86"
|
||||||
login_page: Optional[str] = None # 登录页面:creator 或 home,为None时使用配置文件默认值
|
login_page: Optional[str] = None # 登录页面:creator 或 home,为None时使用配置文件默认值
|
||||||
|
session_id: Optional[str] = None # 可选:前端生成的session_id,用于WebSocket通知
|
||||||
|
|
||||||
class VerifyCodeRequest(BaseModel):
|
class VerifyCodeRequest(BaseModel):
|
||||||
phone: str
|
phone: str
|
||||||
@@ -288,10 +358,14 @@ async def send_code(request: SendCodeRequest):
|
|||||||
支持选择从创作者中心或小红书首页登录
|
支持选择从创作者中心或小红书首页登录
|
||||||
并发支持:为每个请求分配独立的浏览器实例
|
并发支持:为每个请求分配独立的浏览器实例
|
||||||
"""
|
"""
|
||||||
# 使用随机UUID作为session_id,确保每次都创建全新浏览器,完全不复用
|
# 使用前端传递的session_id,如果没有则生成新的
|
||||||
import uuid
|
if request.session_id:
|
||||||
session_id = f"xhs_login_{uuid.uuid4().hex}"
|
session_id = request.session_id
|
||||||
print(f"[发送验证码] 创建全新浏览器实例 session_id={session_id}, phone={request.phone}", file=sys.stderr)
|
print(f"[发送验证码] 使用前端传递的session_id={session_id}, phone={request.phone}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
import uuid
|
||||||
|
session_id = f"xhs_login_{uuid.uuid4().hex}"
|
||||||
|
print(f"[发送验证码] 前端未传session_id,生成新的session_id={session_id}, phone={request.phone}", file=sys.stderr)
|
||||||
|
|
||||||
# 获取配置中的默认login_page,如果API传入了则优先使用API参数
|
# 获取配置中的默认login_page,如果API传入了则优先使用API参数
|
||||||
config = get_config()
|
config = get_config()
|
||||||
@@ -312,9 +386,24 @@ async def send_code(request: SendCodeRequest):
|
|||||||
result = await request_login_service.send_verification_code(
|
result = await request_login_service.send_verification_code(
|
||||||
phone=request.phone,
|
phone=request.phone,
|
||||||
country_code=request.country_code,
|
country_code=request.country_code,
|
||||||
login_page=login_page # 传递登录页面参数
|
login_page=login_page, # 传递登录页面参数
|
||||||
|
session_id=session_id # 传递session_id用于WebSocket通知
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 检查是否需要验证(发送验证码时触发风控)
|
||||||
|
if result.get("need_captcha"):
|
||||||
|
print(f"[发送验证码] 检测到需要扫码验证,保持session {session_id} 的浏览器继续运行", file=sys.stderr)
|
||||||
|
return BaseResponse(
|
||||||
|
code=0, # 成功返回二维码
|
||||||
|
message=result.get("message", "需要扫码验证"),
|
||||||
|
data={
|
||||||
|
"need_captcha": True,
|
||||||
|
"captcha_type": result.get("captcha_type"),
|
||||||
|
"qrcode_image": result.get("qrcode_image"),
|
||||||
|
"session_id": session_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
# 验证浏览器是否已保存到池中
|
# 验证浏览器是否已保存到池中
|
||||||
if browser_pool and session_id in browser_pool.temp_browsers:
|
if browser_pool and session_id in browser_pool.temp_browsers:
|
||||||
@@ -835,7 +924,22 @@ async def login(request: LoginRequest):
|
|||||||
login_page=login_page # 传递登录页面参数
|
login_page=login_page # 传递登录页面参数
|
||||||
)
|
)
|
||||||
|
|
||||||
# 释放临时浏览器(无论成功还是失败)
|
# 检查是否需要扫码验证
|
||||||
|
if result.get("need_captcha"):
|
||||||
|
# 需要扫码验证,不释放浏览器,保持session_id对应的浏览器继续运行
|
||||||
|
print(f"[登录验证] 检测到需要扫码验证,保持session {session_id} 的浏览器继续运行", file=sys.stderr)
|
||||||
|
return BaseResponse(
|
||||||
|
code=0, # 成功返回二维码
|
||||||
|
message=result.get("message", "需要扫码验证"),
|
||||||
|
data={
|
||||||
|
"need_captcha": True,
|
||||||
|
"captcha_type": result.get("captcha_type"),
|
||||||
|
"qrcode_image": result.get("qrcode_image"),
|
||||||
|
"session_id": session_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 释放临时浏览器(仅在登录成功或失败时释放)
|
||||||
if session_id and browser_pool:
|
if session_id and browser_pool:
|
||||||
try:
|
try:
|
||||||
await browser_pool.release_temp_browser(session_id)
|
await browser_pool.release_temp_browser(session_id)
|
||||||
@@ -1217,6 +1321,228 @@ async def upload_images(files: List[UploadFile] = File(...)):
|
|||||||
"data": None
|
"data": None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def handle_send_code_ws(session_id: str, phone: str, country_code: str, login_page: str, websocket: WebSocket):
|
||||||
|
"""
|
||||||
|
异步处理WebSocket发送验证码请求
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"[WebSocket-SendCode] 开始处理: session={session_id}, phone={phone}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 创建登录服务实例
|
||||||
|
request_login_service = XHSLoginService(
|
||||||
|
use_pool=True,
|
||||||
|
headless=login_service.headless,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用登录服务发送验证码
|
||||||
|
result = await request_login_service.send_verification_code(
|
||||||
|
phone=phone,
|
||||||
|
country_code=country_code,
|
||||||
|
login_page=login_page,
|
||||||
|
session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查是否需要验证(发送验证码时触发风控)
|
||||||
|
if result.get("need_captcha"):
|
||||||
|
print(f"[WebSocket-SendCode] 检测到风控,需要扫码", file=sys.stderr)
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "need_captcha",
|
||||||
|
"captcha_type": result.get("captcha_type"),
|
||||||
|
"qrcode_image": result.get("qrcode_image"),
|
||||||
|
"message": result.get("message", "需要扫码验证")
|
||||||
|
})
|
||||||
|
print(f"[WebSocket-SendCode] 已推送风控信息", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
print(f"[WebSocket-SendCode] 验证码发送成功", file=sys.stderr)
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "code_sent",
|
||||||
|
"success": True,
|
||||||
|
"message": "验证码已发送,请在小红书APP中查看"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print(f"[WebSocket-SendCode] 发送失败: {result.get('error')}", file=sys.stderr)
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "code_sent",
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("error", "发送验证码失败")
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WebSocket-SendCode] 异常: {str(e)}", file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
try:
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "code_sent",
|
||||||
|
"success": False,
|
||||||
|
"message": f"发送验证码失败: {str(e)}"
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_code: str, login_page: str, websocket: WebSocket):
|
||||||
|
"""
|
||||||
|
异步处理WebSocket验证码验证请求
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"[WebSocket-VerifyCode] 开始验证: session={session_id}, phone={phone}, code={code}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 从浏览器池中获取之前的浏览器实例
|
||||||
|
if session_id not in browser_pool.temp_browsers:
|
||||||
|
print(f"[WebSocket-VerifyCode] 未找到session: {session_id}", file=sys.stderr)
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "login_result",
|
||||||
|
"success": False,
|
||||||
|
"message": "会话已过期,请重新发送验证码"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# 获取浏览器实例
|
||||||
|
browser_data = browser_pool.temp_browsers[session_id]
|
||||||
|
request_login_service = browser_data['service']
|
||||||
|
|
||||||
|
# 调用登录服务验证登录
|
||||||
|
result = await request_login_service.login_with_code(
|
||||||
|
phone=phone,
|
||||||
|
code=code,
|
||||||
|
country_code=country_code,
|
||||||
|
login_page=login_page
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查是否需要验证(登录时触发风控)
|
||||||
|
if result.get("need_captcha"):
|
||||||
|
print(f"[WebSocket-VerifyCode] 登录时检测到风控", file=sys.stderr)
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "need_captcha",
|
||||||
|
"captcha_type": result.get("captcha_type"),
|
||||||
|
"qrcode_image": result.get("qrcode_image"),
|
||||||
|
"message": result.get("message", "需要扫码验证")
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
print(f"[WebSocket-VerifyCode] 登录成功", file=sys.stderr)
|
||||||
|
|
||||||
|
# 获取storage_state
|
||||||
|
storage_state = result.get("storage_state")
|
||||||
|
|
||||||
|
# 保存storage_state到文件
|
||||||
|
storage_state_path = None
|
||||||
|
if storage_state:
|
||||||
|
import os
|
||||||
|
os.makedirs('storage_states', exist_ok=True)
|
||||||
|
storage_state_path = f"storage_states/{phone}_state.json"
|
||||||
|
|
||||||
|
import json
|
||||||
|
with open(storage_state_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(storage_state, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"[WebSocket-VerifyCode] 已保存storage_state: {storage_state_path}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 推送登录成功消息
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "login_success",
|
||||||
|
"success": True,
|
||||||
|
"storage_state": storage_state,
|
||||||
|
"storage_state_path": storage_state_path,
|
||||||
|
"message": "登录成功"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 释放浏览器
|
||||||
|
try:
|
||||||
|
await browser_pool.release_temp_browser(session_id)
|
||||||
|
print(f"[WebSocket-VerifyCode] 已释放浏览器: {session_id}", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WebSocket-VerifyCode] 释放浏览器失败: {str(e)}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"[WebSocket-VerifyCode] 登录失败: {result.get('error')}", file=sys.stderr)
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "login_result",
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("error", "登录失败")
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WebSocket-VerifyCode] 异常: {str(e)}", file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
try:
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "login_result",
|
||||||
|
"success": False,
|
||||||
|
"message": f"登录失败: {str(e)}"
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.websocket("/ws/login/{session_id}")
|
||||||
|
async def websocket_login(websocket: WebSocket, session_id: str):
|
||||||
|
"""
|
||||||
|
WebSocket端点:实时监听登录状态
|
||||||
|
用于扫码验证后的实时通知
|
||||||
|
"""
|
||||||
|
await ws_manager.connect(session_id, websocket)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 保持连接,等待消息或断开
|
||||||
|
while True:
|
||||||
|
# 接收客户端消息(ping/pong保持连接)
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
print(f"[WebSocket] 收到客户端消息 {session_id}: {data}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 处理ping消息
|
||||||
|
if data == "ping":
|
||||||
|
await websocket.send_text("pong")
|
||||||
|
else:
|
||||||
|
# 尝试解析JSON消息
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
msg = json.loads(data)
|
||||||
|
msg_type = msg.get('type', 'unknown')
|
||||||
|
print(f"[WebSocket] 解析消息类型: {msg_type}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 处理测试消息
|
||||||
|
if msg_type == 'test':
|
||||||
|
print(f"[WebSocket] 收到测试消息: {msg.get('message')}", file=sys.stderr)
|
||||||
|
# 回复测试消息
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "test_response",
|
||||||
|
"message": "Test message received by backend successfully!",
|
||||||
|
"timestamp": data
|
||||||
|
})
|
||||||
|
print(f"[WebSocket] 已回复测试消息", file=sys.stderr)
|
||||||
|
|
||||||
|
# 处理发送验证码消息
|
||||||
|
elif msg_type == 'send_code':
|
||||||
|
phone = msg.get('phone')
|
||||||
|
country_code = msg.get('country_code', '+86')
|
||||||
|
login_page = msg.get('login_page', 'creator')
|
||||||
|
print(f"[WebSocket] 收到发送验证码请求: phone={phone}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 启动异步任务处理发送验证码
|
||||||
|
asyncio.create_task(handle_send_code_ws(session_id, phone, country_code, login_page, websocket))
|
||||||
|
|
||||||
|
# 处理验证码验证消息
|
||||||
|
elif msg_type == 'verify_code':
|
||||||
|
phone = msg.get('phone')
|
||||||
|
code = msg.get('code')
|
||||||
|
country_code = msg.get('country_code', '+86')
|
||||||
|
login_page = msg.get('login_page', 'creator')
|
||||||
|
print(f"[WebSocket] 收到验证码验证请求: phone={phone}, code={code}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 启动异步任务处理验证码验证
|
||||||
|
asyncio.create_task(handle_verify_code_ws(session_id, phone, code, country_code, login_page, websocket))
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"[WebSocket] 无法解析为JSON: {data}", file=sys.stderr)
|
||||||
|
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
ws_manager.disconnect(session_id)
|
||||||
|
print(f"[WebSocket] 客户端断开: {session_id}", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
ws_manager.disconnect(session_id)
|
||||||
|
print(f"[WebSocket] 连接异常 {session_id}: {str(e)}", file=sys.stderr)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
@@ -1227,7 +1553,9 @@ if __name__ == "__main__":
|
|||||||
debug = config.get_bool('server.debug', False)
|
debug = config.get_bool('server.debug', False)
|
||||||
reload = config.get_bool('server.reload', False)
|
reload = config.get_bool('server.reload', False)
|
||||||
|
|
||||||
print(f"[\u542f\u52a8\u670d\u52a1] \u4e3b\u673a: {host}, \u7aef\u53e3: {port}, \u8c03\u8bd5: {debug}, \u70ed\u91cd\u8f7d: {reload}")
|
print(f"[启动服务] 主机: {host}, 端口: {port}, 调试: {debug}, 热重载: {reload}")
|
||||||
|
print(f"[WebSocket] WebSocket服务地址: ws://{host}:{port}/ws/login/{{session_id}}")
|
||||||
|
print(f"[WebSocket] 示例: ws://{host}:{port}/ws/login/xhs_login_xxxxx")
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app,
|
app,
|
||||||
|
|||||||
65
backend/replace_print_with_logger.py
Normal file
65
backend/replace_print_with_logger.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
批量替换 xhs_login.py 中的 print 为 logger
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
def replace_print_to_logger(content):
|
||||||
|
"""将 print 语句替换为对应的 logger 语句"""
|
||||||
|
|
||||||
|
# 替换规则:根据内容判断日志级别
|
||||||
|
def determine_log_level_and_replace(match):
|
||||||
|
text = match.group(1)
|
||||||
|
|
||||||
|
# 错误相关
|
||||||
|
if any(keyword in text for keyword in ['失败', '错误', '异常', '❌', 'error', 'Error', 'failed', 'Failed']):
|
||||||
|
return f'logger.error({text})'
|
||||||
|
|
||||||
|
# 警告相关
|
||||||
|
elif any(keyword in text for keyword in ['警告', '⚠️', 'warning', 'Warning', '未找到', '检测到']):
|
||||||
|
return f'logger.warning({text})'
|
||||||
|
|
||||||
|
# 成功相关
|
||||||
|
elif any(keyword in text for keyword in ['成功', '✅', 'success', 'Success', '已', '完成']):
|
||||||
|
return f'logger.success({text})'
|
||||||
|
|
||||||
|
# 调试相关
|
||||||
|
elif any(keyword in text for keyword in ['调试', 'debug', 'Debug', '查找', '正在', '开始']):
|
||||||
|
return f'logger.debug({text})'
|
||||||
|
|
||||||
|
# 默认 info
|
||||||
|
else:
|
||||||
|
return f'logger.info({text})'
|
||||||
|
|
||||||
|
# 匹配 print(xxx, file=sys.stderr)
|
||||||
|
pattern1 = r'print\((.*?),\s*file=sys\.stderr\)'
|
||||||
|
content = re.sub(pattern1, determine_log_level_and_replace, content)
|
||||||
|
|
||||||
|
# 匹配普通 print(xxx)
|
||||||
|
pattern2 = r'print\((.*?)\)(?!\s*#.*logger)'
|
||||||
|
content = re.sub(pattern2, determine_log_level_and_replace, content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 读取文件
|
||||||
|
with open('xhs_login.py', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 替换
|
||||||
|
new_content = replace_print_to_logger(content)
|
||||||
|
|
||||||
|
# 备份原文件
|
||||||
|
with open('xhs_login.py.bak', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# 写入新文件
|
||||||
|
with open('xhs_login.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print("✅ 替换完成!")
|
||||||
|
print("原文件已备份到 xhs_login.py.bak")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -14,3 +14,4 @@ alibabacloud_credentials==0.3.4
|
|||||||
alibabacloud_tea_openapi==0.3.9
|
alibabacloud_tea_openapi==0.3.9
|
||||||
alibabacloud_tea_util==0.3.13
|
alibabacloud_tea_util==0.3.13
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
|
websockets==12.0
|
||||||
|
|||||||
1189
backend/xhs_login.py
1189
backend/xhs_login.py
File diff suppressed because it is too large
Load Diff
@@ -42,13 +42,20 @@ func (ctrl *EmployeeController) SendXHSCode(c *gin.Context) {
|
|||||||
// 获取当前登录用户ID
|
// 获取当前登录用户ID
|
||||||
employeeID := c.GetInt("employee_id")
|
employeeID := c.GetInt("employee_id")
|
||||||
|
|
||||||
err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID)
|
data, err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Error(c, common.CodeInternalError, err.Error())
|
common.Error(c, common.CodeInternalError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SuccessWithMessage(c, "验证码已发送,请在小红书APP中查看", nil)
|
// 检查是否需要扫码验证
|
||||||
|
if needCaptcha, ok := data["need_captcha"].(bool); ok && needCaptcha {
|
||||||
|
// 发送验证码时触发风控,返回二维码
|
||||||
|
common.SuccessWithMessage(c, "需要扫码验证", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.SuccessWithMessage(c, "验证码已发送,请在小红书APP中查看", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfile 获取员工个人信息
|
// GetProfile 获取员工个人信息
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ func (ctrl *XHSController) SendCode(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否需要扫码验证
|
||||||
|
if needCaptcha, ok := result.Data["need_captcha"].(bool); ok && needCaptcha {
|
||||||
|
// 发送验证码时触发风控,返回二维码
|
||||||
|
common.SuccessWithMessage(c, result.Message, result.Data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 判断Python服务返回的结果
|
// 判断Python服务返回的结果
|
||||||
if result.Code != 0 {
|
if result.Code != 0 {
|
||||||
common.Error(c, result.Code, result.Message)
|
common.Error(c, result.Code, result.Message)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type XHSCookieVerifyResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendXHSCode 发送小红书验证码(调用Python HTTP服务,增加限流控制)
|
// SendXHSCode 发送小红书验证码(调用Python HTTP服务,增加限流控制)
|
||||||
func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
func (s *EmployeeService) SendXHSCode(phone string, employeeID int) (map[string]interface{}, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// 预检查:验证该手机号是否已被其他用户绑定
|
// 预检查:验证该手机号是否已被其他用户绑定
|
||||||
@@ -45,11 +45,11 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
// 找到了其他用户的绑定记录
|
// 找到了其他用户的绑定记录
|
||||||
log.Printf("发送验证码 - 用户%d - 失败: 手机号%s已被用户%d绑定",
|
log.Printf("发送验证码 - 用户%d - 失败: 手机号%s已被用户%d绑定",
|
||||||
employeeID, phone, conflictAuthor.CreatedUserID)
|
employeeID, phone, conflictAuthor.CreatedUserID)
|
||||||
return errors.New("该手机号已被其他用户绑定")
|
return nil, errors.New("该手机号已被其他用户绑定")
|
||||||
} else if err != gorm.ErrRecordNotFound {
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
// 数据库查询异常
|
// 数据库查询异常
|
||||||
log.Printf("发送验证码 - 用户%d - 检查手机号失败: %v", employeeID, err)
|
log.Printf("发送验证码 - 用户%d - 检查手机号失败: %v", employeeID, err)
|
||||||
return fmt.Errorf("检查手机号失败: %w", err)
|
return nil, fmt.Errorf("检查手机号失败: %w", err)
|
||||||
}
|
}
|
||||||
// err == gorm.ErrRecordNotFound 表示该手机号未被绑定,可以继续
|
// err == gorm.ErrRecordNotFound 表示该手机号未被绑定,可以继续
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
rateLimitKey := fmt.Sprintf("rate:sms:%s", phone)
|
rateLimitKey := fmt.Sprintf("rate:sms:%s", phone)
|
||||||
exists, err := utils.ExistsCache(ctx, rateLimitKey)
|
exists, err := utils.ExistsCache(ctx, rateLimitKey)
|
||||||
if err == nil && exists {
|
if err == nil && exists {
|
||||||
return errors.New("验证码发送过于频繁,请稍后再试")
|
return nil, errors.New("验证码发送过于频繁,请稍后再试")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从配置获取Python服务地址
|
// 从配置获取Python服务地址
|
||||||
@@ -76,7 +76,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
jsonData, err := json.Marshal(requestData)
|
jsonData, err := json.Marshal(requestData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[发送验证码] 序列化请求数据失败: %v", err)
|
log.Printf("[发送验证码] 序列化请求数据失败: %v", err)
|
||||||
return errors.New("网络错误,请稍后重试")
|
return nil, errors.New("网络错误,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[发送验证码] 调用Python HTTP服务: %s, 请求参数: phone=%s", url, phone)
|
log.Printf("[发送验证码] 调用Python HTTP服务: %s, 请求参数: phone=%s", url, phone)
|
||||||
@@ -90,7 +90,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[发送验证码] 创建请求失败: %v", err)
|
log.Printf("[发送验证码] 创建请求失败: %v", err)
|
||||||
return errors.New("网络错误,请稍后重试")
|
return nil, errors.New("网络错误,请稍后重试")
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
@@ -99,9 +99,9 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
log.Printf("[发送验证码] 调用Python服务失败: %v", err)
|
log.Printf("[发送验证码] 调用Python服务失败: %v", err)
|
||||||
// 判断是否是超时错误
|
// 判断是否是超时错误
|
||||||
if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "deadline exceeded") {
|
if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "deadline exceeded") {
|
||||||
return errors.New("请求超时,请稍后重试")
|
return nil, errors.New("请求超时,请稍后重试")
|
||||||
}
|
}
|
||||||
return errors.New("网络错误,请稍后重试")
|
return nil, errors.New("网络错误,请稍后重试")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[发送验证码] 读取响应失败: %v", err)
|
log.Printf("[发送验证码] 读取响应失败: %v", err)
|
||||||
return errors.New("网络错误,请稍后重试")
|
return nil, errors.New("网络错误,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[发送验证码] Python服务响应: 状态码=%d, 耗时=%.2fs", resp.StatusCode, time.Since(startTime).Seconds())
|
log.Printf("[发送验证码] Python服务响应: 状态码=%d, 耗时=%.2fs", resp.StatusCode, time.Since(startTime).Seconds())
|
||||||
@@ -123,25 +123,27 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
|
|||||||
|
|
||||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||||
log.Printf("[发送验证码] 解析Python响应失败: %v, body: %s", err, string(body))
|
log.Printf("[发送验证码] 解析Python响应失败: %v, body: %s", err, string(body))
|
||||||
return errors.New("网络错误,请稍后重试")
|
return nil, errors.New("网络错误,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[Python响应] code=%d, message=%s", apiResponse.Code, apiResponse.Message)
|
log.Printf("[Python响应] code=%d, message=%s, data=%v", apiResponse.Code, apiResponse.Message, apiResponse.Data)
|
||||||
|
|
||||||
// 检查响应code(FastAPI返回code=0为成功)
|
// 检查响应code(FastAPI返回code=0为成功)
|
||||||
if apiResponse.Code != 0 {
|
if apiResponse.Code != 0 {
|
||||||
log.Printf("[发送验证码] 失败: %s", apiResponse.Message)
|
log.Printf("[发送验证码] 失败: %s", apiResponse.Message)
|
||||||
// 根据错误信息返回用户友好的提示
|
// 根据错误信息返回用户友好的提示
|
||||||
return s.getFriendlyErrorMessage(apiResponse.Message)
|
return nil, s.getFriendlyErrorMessage(apiResponse.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 返回完整的data,包括need_captcha、qrcode_image、session_id
|
||||||
|
log.Printf("[发送验证码] 成功, 返回数据: %v", apiResponse.Data)
|
||||||
|
|
||||||
// 2. 发送成功后设置限流标记(1分钟)
|
// 2. 发送成功后设置限流标记(1分钟)
|
||||||
if err := utils.SetCache(ctx, rateLimitKey, "1", 1*time.Minute); err != nil {
|
if err := utils.SetCache(ctx, rateLimitKey, "1", 1*time.Minute); err != nil {
|
||||||
log.Printf("设置限流缓存失败: %v", err)
|
log.Printf("设置限流缓存失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[发送验证码] 验证码发送成功")
|
return apiResponse.Data, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFriendlyErrorMessage 将技术错误信息转换为用户友好提示
|
// getFriendlyErrorMessage 将技术错误信息转换为用户友好提示
|
||||||
|
|||||||
@@ -254,11 +254,38 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.data.canLogin) {
|
const { phone, code, password, loginType, countdown } = this.data;
|
||||||
|
|
||||||
|
// 检查手机号
|
||||||
|
if (phone.length !== 11) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入正确的手机号',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { phone, code, password, loginType } = this.data;
|
// 检查验证码或密码
|
||||||
|
if (loginType === 'code') {
|
||||||
|
if (!code || code.length < 4) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入验证码',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!password || password.length < 6) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入密码(至少6位)',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 显示加载提示
|
// 显示加载提示
|
||||||
wx.showLoading({
|
wx.showLoading({
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Page({
|
|||||||
countryCodeIndex: 0,
|
countryCodeIndex: 0,
|
||||||
pollTimer: null as any, // 轮询定时器
|
pollTimer: null as any, // 轮询定时器
|
||||||
pollCount: 0, // 轮询次数
|
pollCount: 0, // 轮询次数
|
||||||
|
socketTask: null as any, // WebSocket连接
|
||||||
// 验证码相关
|
// 验证码相关
|
||||||
needCaptcha: false, // 是否需要验证码
|
needCaptcha: false, // 是否需要验证码
|
||||||
captchaType: '', // 验证码类型
|
captchaType: '', // 验证码类型
|
||||||
@@ -38,6 +39,15 @@ Page({
|
|||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
console.log('小红书绑定页面加载');
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnload() {
|
onUnload() {
|
||||||
@@ -48,6 +58,10 @@ Page({
|
|||||||
if (this.data.pollTimer) {
|
if (this.data.pollTimer) {
|
||||||
clearInterval(this.data.pollTimer);
|
clearInterval(this.data.pollTimer);
|
||||||
}
|
}
|
||||||
|
// 关闭WebSocket连接
|
||||||
|
if (this.data.socketTask) {
|
||||||
|
this.data.socketTask.close();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 区号选择
|
// 区号选择
|
||||||
@@ -77,7 +91,7 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { phone, countryCodes, countryCodeIndex } = this.data;
|
const { phone, countryCodes, countryCodeIndex, sessionId, socketTask } = this.data;
|
||||||
if (phone.length !== 11) {
|
if (phone.length !== 11) {
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: '请输入正确的手机号',
|
title: '请输入正确的手机号',
|
||||||
@@ -87,43 +101,52 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载
|
|
||||||
this.setData({
|
|
||||||
showLoading: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用后端API发送验证码
|
|
||||||
const countryCode = countryCodes[countryCodeIndex];
|
const countryCode = countryCodes[countryCodeIndex];
|
||||||
console.log('发送验证码到:', phone, '区号:', countryCode);
|
console.log('[发送验证码] 开始,手机号:', phone, '区号:', countryCode);
|
||||||
|
console.log('[发送验证码] 使用现有session_id:', sessionId);
|
||||||
|
|
||||||
const result = await EmployeeService.sendXHSCode(phone);
|
// 检查WebSocket连接
|
||||||
|
if (!socketTask) {
|
||||||
// 保存session_id用于后续复用浏览器
|
console.error('[发送验证码] WebSocket连接不存在,重新建立...');
|
||||||
if (result.data && result.data.session_id) {
|
this.connectWebSocket(sessionId);
|
||||||
this.setData({
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
sessionId: result.data.session_id
|
|
||||||
});
|
|
||||||
console.log('已保存session_id:', result.data.session_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setData({
|
// 通过WebSocket发送send_code消息
|
||||||
showLoading: false
|
console.log('[发送验证码] 通过WebSocket发送请求...');
|
||||||
|
const currentSocketTask = this.data.socketTask;
|
||||||
|
if (!currentSocketTask) {
|
||||||
|
throw new Error('WebSocket连接未建立');
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSocketTask.send({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: 'send_code',
|
||||||
|
phone: phone,
|
||||||
|
country_code: countryCode,
|
||||||
|
login_page: 'home' // 使用小红书首页登录
|
||||||
|
}),
|
||||||
|
success: () => {
|
||||||
|
console.log('[发送验证码] WebSocket消息发送成功');
|
||||||
|
wx.showToast({
|
||||||
|
title: '正在发送验证码...',
|
||||||
|
icon: 'loading',
|
||||||
|
duration: 20000
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
console.error('[发送验证码] WebSocket消息发送失败:', err);
|
||||||
|
wx.showToast({
|
||||||
|
title: '发送失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wx.showToast({
|
|
||||||
title: '验证码已发送,请在小红书APP中查看',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
|
|
||||||
// 开始倒计时
|
|
||||||
this.startCountdown();
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.setData({
|
console.error('[发送验证码] 异常:', error);
|
||||||
showLoading: false
|
|
||||||
});
|
|
||||||
|
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: error.message || '发送失败,请重试',
|
title: error.message || '发送失败,请重试',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -164,7 +187,12 @@ Page({
|
|||||||
|
|
||||||
// 绑定账号
|
// 绑定账号
|
||||||
async bindAccount() {
|
async bindAccount() {
|
||||||
const { phone, code, sessionId } = this.data;
|
const { phone, code, sessionId, countryCodes, countryCodeIndex } = this.data;
|
||||||
|
|
||||||
|
console.log('[绑定账号] 开始绑定');
|
||||||
|
console.log('[绑定账号] phone:', phone);
|
||||||
|
console.log('[绑定账号] code:', code);
|
||||||
|
console.log('[绑定账号] sessionId:', sessionId);
|
||||||
|
|
||||||
// 验证手机号
|
// 验证手机号
|
||||||
if (!phone || phone.length !== 11) {
|
if (!phone || phone.length !== 11) {
|
||||||
@@ -196,42 +224,50 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载
|
|
||||||
this.setData({
|
|
||||||
showLoading: true,
|
|
||||||
loadingText: '正在验证...',
|
|
||||||
pollCount: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用后端API进行绑定(异步处理)
|
console.log('[绑定账号] 通过WebSocket发送验证码验证请求...');
|
||||||
console.log('绑定小红书账号:', { phone, code, sessionId });
|
|
||||||
|
|
||||||
const result = await EmployeeService.bindXHS(phone, code, sessionId);
|
const socketTask = this.data.socketTask;
|
||||||
|
if (!socketTask) {
|
||||||
|
throw new Error('WebSocket连接已断开,请重新发送验证码');
|
||||||
|
}
|
||||||
|
|
||||||
// 后端立即返回,开始轮询绑定状态
|
const countryCode = countryCodes[countryCodeIndex];
|
||||||
this.setData({
|
|
||||||
loadingText: '正在登录小红书...'
|
wx.showToast({
|
||||||
|
title: '正在验证...',
|
||||||
|
icon: 'loading',
|
||||||
|
duration: 60000
|
||||||
});
|
});
|
||||||
|
|
||||||
// 开始轮询绑定状态
|
socketTask.send({
|
||||||
this.startPollingBindStatus();
|
data: JSON.stringify({
|
||||||
|
type: 'verify_code',
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
country_code: countryCode,
|
||||||
|
login_page: 'home' // 使用小红书首页登录
|
||||||
|
}),
|
||||||
|
success: () => {
|
||||||
|
console.log('[绑定账号] WebSocket消息发送成功,等待后端响应...');
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
console.error('[绑定账号] WebSocket消息发送失败:', err);
|
||||||
|
wx.showToast({
|
||||||
|
title: '发送失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.setData({
|
console.error('[绑定账号] 异常:', error);
|
||||||
showLoading: false
|
wx.showToast({
|
||||||
|
title: error.message || '绑定失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定失败
|
|
||||||
this.setData({
|
|
||||||
showFail: true
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.setData({
|
|
||||||
showFail: false
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -242,6 +278,11 @@ Page({
|
|||||||
clearInterval(this.data.pollTimer);
|
clearInterval(this.data.pollTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化轮询计数
|
||||||
|
this.setData({
|
||||||
|
pollCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
// 立即查询一次
|
// 立即查询一次
|
||||||
this.checkBindStatus();
|
this.checkBindStatus();
|
||||||
|
|
||||||
@@ -264,17 +305,12 @@ Page({
|
|||||||
// 超过60次轮询(60秒),停止轮询
|
// 超过60次轮询(60秒),停止轮询
|
||||||
if (pollCount > 60) {
|
if (pollCount > 60) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
this.setData({
|
wx.hideToast();
|
||||||
showLoading: false,
|
wx.showToast({
|
||||||
showFail: true
|
title: '登录超时,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.setData({
|
|
||||||
showFail: false
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,10 +329,12 @@ Page({
|
|||||||
if (status.status === 'success') {
|
if (status.status === 'success') {
|
||||||
// 绑定成功
|
// 绑定成功
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
wx.hideToast();
|
||||||
|
|
||||||
this.setData({
|
wx.showToast({
|
||||||
showLoading: false,
|
title: '绑定成功',
|
||||||
showSuccess: true
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新本地绑定状态缓存
|
// 更新本地绑定状态缓存
|
||||||
@@ -312,10 +350,6 @@ Page({
|
|||||||
console.log('[手机号登录] 绑定成功,已更新本地缓存');
|
console.log('[手机号登录] 绑定成功,已更新本地缓存');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setData({
|
|
||||||
showSuccess: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绑定成功后返回个人中心
|
// 绑定成功后返回个人中心
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -323,11 +357,7 @@ Page({
|
|||||||
} else if (status.status === 'failed') {
|
} else if (status.status === 'failed') {
|
||||||
// 绑定失败
|
// 绑定失败
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
wx.hideToast();
|
||||||
this.setData({
|
|
||||||
showLoading: false,
|
|
||||||
showFail: true
|
|
||||||
});
|
|
||||||
|
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: status.error || '绑定失败',
|
title: status.error || '绑定失败',
|
||||||
@@ -335,31 +365,28 @@ Page({
|
|||||||
duration: 3000
|
duration: 3000
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.setData({
|
|
||||||
showFail: false
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
} else if (status.status === 'need_captcha') {
|
} else if (status.status === 'need_captcha') {
|
||||||
// 需要验证码验证
|
// 需要验证码验证
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
wx.hideToast();
|
||||||
|
|
||||||
console.log('需要验证码验证:', status.captcha_type);
|
console.log('需要验证码验证:', status.captcha_type);
|
||||||
|
|
||||||
|
wx.showToast({
|
||||||
|
title: status.message || '需要验证码验证',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
showLoading: false,
|
|
||||||
needCaptcha: true,
|
needCaptcha: true,
|
||||||
captchaType: status.captcha_type || 'unknown',
|
captchaType: status.captcha_type || 'unknown',
|
||||||
qrcodeImage: status.qrcode_image || '',
|
qrcodeImage: status.qrcode_image || ''
|
||||||
loadingText: status.message || '需要验证码验证'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} else if (status.status === 'processing') {
|
} else if (status.status === 'processing') {
|
||||||
// 仍在处理中,继续轮询
|
// 仍在处理中,继续轮询
|
||||||
this.setData({
|
console.log('登录处理中:', status.message);
|
||||||
loadingText: status.message || '正在登录小红书...'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -380,9 +407,22 @@ Page({
|
|||||||
|
|
||||||
// 切换到手机号登录
|
// 切换到手机号登录
|
||||||
switchToPhone() {
|
switchToPhone() {
|
||||||
|
console.log('[切换登录] 切换到手机号登录');
|
||||||
|
|
||||||
// 停止二维码轮询
|
// 停止二维码轮询
|
||||||
this.stopQRCodePolling();
|
this.stopQRCodePolling();
|
||||||
|
|
||||||
|
// 关闭二维码弹窗(如果正在显示)
|
||||||
|
if (this.data.needCaptcha) {
|
||||||
|
console.log('[切换登录] 检测到风控弹窗,关闭...');
|
||||||
|
this.setData({
|
||||||
|
needCaptcha: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭WebSocket连接
|
||||||
|
this.closeWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
// 清空所有扫码登录相关数据
|
// 清空所有扫码登录相关数据
|
||||||
this.setData({
|
this.setData({
|
||||||
loginType: 'phone',
|
loginType: 'phone',
|
||||||
@@ -401,6 +441,8 @@ Page({
|
|||||||
countdown: 0,
|
countdown: 0,
|
||||||
isCounting: false
|
isCounting: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[切换登录] 切换完成');
|
||||||
},
|
},
|
||||||
|
|
||||||
// 切换到扫码登录
|
// 切换到扫码登录
|
||||||
@@ -848,5 +890,536 @@ Page({
|
|||||||
this.stopQRCodePolling();
|
this.stopQRCodePolling();
|
||||||
this.switchToPhone();
|
this.switchToPhone();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 建立WebSocket连接
|
||||||
|
connectWebSocket(sessionId: string) {
|
||||||
|
// 关闭旧连接
|
||||||
|
if (this.data.socketTask) {
|
||||||
|
try {
|
||||||
|
this.data.socketTask.close();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[WebSocket] 关闭旧连接失败(可能已关闭):', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Python服务地址(WebSocket端点在Python后端)
|
||||||
|
const pythonURL = API.pythonURL || API.baseURL;
|
||||||
|
// 将http/https转为ws/wss
|
||||||
|
const wsURL = pythonURL.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||||
|
const url = `${wsURL}/ws/login/${sessionId}`;
|
||||||
|
|
||||||
|
console.log('[WebSocket] 开始连接:', url);
|
||||||
|
console.log('[WebSocket] Python服务地址:', pythonURL);
|
||||||
|
|
||||||
|
// 小程序环境检查
|
||||||
|
if (url.includes('localhost') || url.includes('127.0.0.1')) {
|
||||||
|
console.warn('[WebSocket] 检测到localhost地址,请确保开发工具中已勾选"不校验合法域名"');
|
||||||
|
console.warn('[WebSocket] 另外需要安装websockets库: pip install websockets');
|
||||||
|
}
|
||||||
|
|
||||||
|
let socketConnected = false; // 标记连接是否成功建立
|
||||||
|
|
||||||
|
const socketTask = wx.connectSocket({
|
||||||
|
url: url,
|
||||||
|
success: () => {
|
||||||
|
console.log('[WebSocket] 连接请求发送成功');
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[WebSocket] 连接请求失败:', err);
|
||||||
|
socketConnected = false;
|
||||||
|
|
||||||
|
// 检查是否是localhost问题
|
||||||
|
if (url.includes('localhost') || url.includes('127.0.0.1')) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请在开发工具中勾选"不校验合法域名"并确保Python服务已安装websockets库',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听连接打开
|
||||||
|
socketTask.onOpen(() => {
|
||||||
|
console.log('[WebSocket] 连接已打开');
|
||||||
|
socketConnected = true;
|
||||||
|
|
||||||
|
// 重置重连计数
|
||||||
|
this.setData({ reconnectCount: 0 } as any);
|
||||||
|
|
||||||
|
// 启动ping/pong保持连接
|
||||||
|
const pingTimer = setInterval(() => {
|
||||||
|
if (socketTask && socketConnected) {
|
||||||
|
socketTask.send({
|
||||||
|
data: 'ping',
|
||||||
|
success: () => console.log('[WebSocket] Ping发送成功'),
|
||||||
|
fail: (err) => console.error('[WebSocket] Ping发送失败:', err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 30000); // 每30秒ping一次
|
||||||
|
|
||||||
|
this.setData({ pingTimer } as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听消息
|
||||||
|
socketTask.onMessage((res) => {
|
||||||
|
console.log('[WebSocket] 收到消息:', res.data);
|
||||||
|
|
||||||
|
// 过滤pong消息(服务端的心跳响应)
|
||||||
|
if (res.data === 'pong') {
|
||||||
|
console.log('[WebSocket] 收到pong响应');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data as string);
|
||||||
|
|
||||||
|
// 处理扫码成功消息(发送验证码阶段的风控)
|
||||||
|
if (data.type === 'qrcode_scan_success') {
|
||||||
|
console.log('✅ 扫码验证完成!', data.message);
|
||||||
|
|
||||||
|
// 关闭验证码弹窗
|
||||||
|
this.setData({
|
||||||
|
needCaptcha: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示提示:扫码成功,请重新发送验证码
|
||||||
|
wx.showToast({
|
||||||
|
title: data.message || '扫码成功,请重新发送验证码',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[WebSocket] 扫码验证完成,提示用户重新发送验证码');
|
||||||
|
console.log('[WebSocket] 保持连接,等待后续操作');
|
||||||
|
|
||||||
|
// 不关闭WebSocket,保持连接用于后续登录流程
|
||||||
|
}
|
||||||
|
// 处理二维码失效消息
|
||||||
|
else if (data.type === 'qrcode_expired') {
|
||||||
|
console.log('⚠️ 二维码已失效!', data.message);
|
||||||
|
|
||||||
|
// 关闭验证码弹窗
|
||||||
|
this.setData({
|
||||||
|
needCaptcha: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示提示:二维码已失效
|
||||||
|
wx.showToast({
|
||||||
|
title: data.message || '二维码已失效,请重新发送验证码',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[WebSocket] 二维码已失效,关闭弹窗');
|
||||||
|
console.log('[WebSocket] 保持连接,等待用户重新操作');
|
||||||
|
|
||||||
|
// 不关闭WebSocket,保持连接用于重新发送验证码
|
||||||
|
}
|
||||||
|
// 处理登录成功消息(点击登录按钮阶段的风控)
|
||||||
|
else if (data.type === 'login_success') {
|
||||||
|
// 判断是扫码验证成功还是真正的登录成功
|
||||||
|
if (data.storage_state) {
|
||||||
|
// 真正的登录成功,包含 storage_state
|
||||||
|
console.log('✅ 登录成功!', data);
|
||||||
|
|
||||||
|
wx.hideToast();
|
||||||
|
|
||||||
|
// 关闭验证码弹窗
|
||||||
|
this.setData({
|
||||||
|
needCaptcha: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭WebSocket
|
||||||
|
this.closeWebSocket();
|
||||||
|
|
||||||
|
// 显示绑定成功动画
|
||||||
|
this.setData({
|
||||||
|
showSuccess: true
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setData({
|
||||||
|
showSuccess: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 跳转回上一页
|
||||||
|
wx.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
success: () => {
|
||||||
|
console.log('✅ 跳转回上一页');
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('⚠️ 跳转失败:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
// 扫码验证成功,但还需要继续登录
|
||||||
|
console.log('✅ 扫码验证成功!', data.user_info);
|
||||||
|
|
||||||
|
// 关闭验证码弹窗
|
||||||
|
this.setData({
|
||||||
|
needCaptcha: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示提示:扫码成功,请重新发送验证码
|
||||||
|
wx.showToast({
|
||||||
|
title: '扫码成功,请重新发送验证码',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[WebSocket] 扫码验证完成,提示用户重新发送验证码');
|
||||||
|
console.log('[WebSocket] 保持连接,等待后续登录操作');
|
||||||
|
|
||||||
|
// 不关闭WebSocket,保持连接用于后续登录流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理need_captcha消息(发送验证码或登录时触发风控)
|
||||||
|
else if (data.type === 'need_captcha') {
|
||||||
|
console.log('⚠️ 需要扫码验证:', data.captcha_type);
|
||||||
|
|
||||||
|
// 显示二维码弹窗
|
||||||
|
this.setData({
|
||||||
|
needCaptcha: true,
|
||||||
|
captchaType: data.captcha_type || 'unknown',
|
||||||
|
qrcodeImage: data.qrcode_image || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
wx.hideToast();
|
||||||
|
wx.showToast({
|
||||||
|
title: data.message || '需要扫码验证',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[WebSocket] 已显示风控二维码');
|
||||||
|
}
|
||||||
|
// 处理code_sent消息(验证码发送结果)
|
||||||
|
else if (data.type === 'code_sent') {
|
||||||
|
console.log('[WebSocket] 验证码发送结果:', data);
|
||||||
|
|
||||||
|
wx.hideToast();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
wx.showToast({
|
||||||
|
title: data.message || '验证码已发送',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
this.startCountdown();
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: data.message || '发送失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理login_result消息(登录结果)
|
||||||
|
else if (data.type === 'login_result') {
|
||||||
|
console.log('[WebSocket] 登录结果:', data);
|
||||||
|
|
||||||
|
wx.hideToast();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
wx.showToast({
|
||||||
|
title: data.message || '登录失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[WebSocket] 解析消息失败:', e, '原始数据:', res.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误
|
||||||
|
socketTask.onError((err) => {
|
||||||
|
console.error('[WebSocket] 连接错误:', err);
|
||||||
|
socketConnected = false;
|
||||||
|
|
||||||
|
// 记录错误,准备重连
|
||||||
|
console.log('[WebSocket] 将在关闭后尝试重连');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听关闭
|
||||||
|
socketTask.onClose(() => {
|
||||||
|
console.log('[WebSocket] 连接关闭');
|
||||||
|
socketConnected = false;
|
||||||
|
|
||||||
|
// 清理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;
|
||||||
|
|
||||||
|
// 最多重连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] 已达到最大重连次数,停止重连');
|
||||||
|
wx.showToast({
|
||||||
|
title: 'WebSocket连接失败,请重新发送验证码',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setData({ socketTask });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 关闭WebSocket连接
|
||||||
|
closeWebSocket() {
|
||||||
|
console.log('[WebSocket] 开始关闭连接');
|
||||||
|
|
||||||
|
// 重置重连计数(主动关闭不需要重连)
|
||||||
|
this.setData({ reconnectCount: 999 } as any); // 设置为很大的数阻止重连
|
||||||
|
|
||||||
|
// 清理ping定时器
|
||||||
|
if ((this.data as any).pingTimer) {
|
||||||
|
clearInterval((this.data as any).pingTimer);
|
||||||
|
console.log('[WebSocket] 已清理ping定时器');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭WebSocket连接
|
||||||
|
const socketTask = this.data.socketTask;
|
||||||
|
if (socketTask) {
|
||||||
|
try {
|
||||||
|
// 检查WebSocket连接状态
|
||||||
|
// readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
|
||||||
|
const readyState = (socketTask as any).readyState;
|
||||||
|
|
||||||
|
if (readyState === 0 || readyState === 1) {
|
||||||
|
// 只有连接中或已打开的连接才关闭
|
||||||
|
console.log(`[WebSocket] 连接状态: ${readyState}, 执行关闭`);
|
||||||
|
socketTask.close({
|
||||||
|
success: () => {
|
||||||
|
console.log('[WebSocket] 连接关闭成功');
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
// 忽略关闭失败错误,可能已经关闭
|
||||||
|
console.log('[WebSocket] 连接关闭失败(忽略):', err.errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`[WebSocket] 连接已关闭或正在关闭, readyState: ${readyState}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空socketTask引用
|
||||||
|
this.setData({ socketTask: null });
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[WebSocket] 关闭连接异常(忽略):', e);
|
||||||
|
// 仍然清空socketTask引用
|
||||||
|
this.setData({ socketTask: null });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[WebSocket] 没有活跃的WebSocket连接');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存二维码
|
||||||
|
saveQRCode() {
|
||||||
|
if (!this.data.qrcodeImage) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '二维码不存在',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[保存二维码] 开始保存');
|
||||||
|
|
||||||
|
// base64转为临时文件
|
||||||
|
const base64Data = this.data.qrcodeImage.replace(/^data:image\/\w+;base64,/, '');
|
||||||
|
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`;
|
||||||
|
|
||||||
|
// 写入临时文件
|
||||||
|
const fs = wx.getFileSystemManager();
|
||||||
|
fs.writeFile({
|
||||||
|
filePath: filePath,
|
||||||
|
data: base64Data,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: () => {
|
||||||
|
console.log('[保存二维码] 临时文件写入成功:', filePath);
|
||||||
|
|
||||||
|
// 保存到相册
|
||||||
|
wx.saveImageToPhotosAlbum({
|
||||||
|
filePath: filePath,
|
||||||
|
success: () => {
|
||||||
|
console.log('[保存二维码] 保存成功');
|
||||||
|
wx.showToast({
|
||||||
|
title: '二维码已保存到相册',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[保存二维码] 保存到相册失败:', err);
|
||||||
|
|
||||||
|
// 检查是否是权限问题
|
||||||
|
if (err.errMsg.includes('auth')) {
|
||||||
|
wx.showModal({
|
||||||
|
title: '需要授权',
|
||||||
|
content: '请允许保存图片到相册',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
wx.openSetting({
|
||||||
|
success: (settingRes) => {
|
||||||
|
if (settingRes.authSetting['scope.writePhotosAlbum']) {
|
||||||
|
// 获得授权后重试
|
||||||
|
this.saveQRCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '保存失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[保存二维码] 临时文件写入失败:', err);
|
||||||
|
wx.showToast({
|
||||||
|
title: '保存失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 测试WebSocket连接
|
||||||
|
testWebSocket() {
|
||||||
|
console.log('[测试] 开始测试WebSocket...');
|
||||||
|
|
||||||
|
// 生成测试session_id
|
||||||
|
const testSessionId = `test_${Date.now()}`;
|
||||||
|
console.log('[测试] 测试session_id:', testSessionId);
|
||||||
|
|
||||||
|
// 关闭旧连接
|
||||||
|
if (this.data.socketTask) {
|
||||||
|
try {
|
||||||
|
this.data.socketTask.close();
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[测试] 关闭旧连接失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取WebSocket地址
|
||||||
|
const pythonURL = API.pythonURL || API.baseURL;
|
||||||
|
const wsURL = pythonURL.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||||
|
const url = `${wsURL}/ws/login/${testSessionId}`;
|
||||||
|
|
||||||
|
console.log('[测试] WebSocket地址:', url);
|
||||||
|
|
||||||
|
const socketTask = wx.connectSocket({
|
||||||
|
url: url,
|
||||||
|
success: () => {
|
||||||
|
console.log('[测试] 连接请求成功');
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('[测试] 连接请求失败:', err);
|
||||||
|
wx.showToast({
|
||||||
|
title: '连接失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听连接打开
|
||||||
|
socketTask.onOpen(() => {
|
||||||
|
console.log('[测试] 连接已打开');
|
||||||
|
wx.showToast({
|
||||||
|
title: 'WebSocket连接成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3秒后发送测试消息
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[测试] 发送测试消息...');
|
||||||
|
socketTask.send({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: 'test',
|
||||||
|
message: 'This is a test message from frontend'
|
||||||
|
}),
|
||||||
|
success: () => {
|
||||||
|
console.log('[测试] 测试消息发送成功');
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
console.error('[测试] 测试消息发送失败:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听消息
|
||||||
|
socketTask.onMessage((res) => {
|
||||||
|
console.log('[测试] 收到消息:', res.data);
|
||||||
|
|
||||||
|
// 过滤pong消息
|
||||||
|
if (res.data === 'pong') {
|
||||||
|
console.log('[测试] 收到pong响应');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data as string);
|
||||||
|
console.log('[测试] 解析后的消息:', data);
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
wx.showModal({
|
||||||
|
title: '收到WebSocket消息',
|
||||||
|
content: `类型: ${data.type}\n内容: ${data.message || JSON.stringify(data)}`,
|
||||||
|
showCancel: false
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[测试] 解析消息失败:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误
|
||||||
|
socketTask.onError((err) => {
|
||||||
|
console.error('[测试] 连接错误:', err);
|
||||||
|
wx.showToast({
|
||||||
|
title: '连接错误',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听关闭
|
||||||
|
socketTask.onClose(() => {
|
||||||
|
console.log('[测试] 连接关闭');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setData({ socketTask });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<!--pages/profile/platform-bind/platform-bind.wxml-->
|
<!--pages/profile/platform-bind/platform-bind.wxml-->
|
||||||
<view class="container">
|
<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>
|
||||||
|
|
||||||
<!-- 绑定内容 -->
|
<!-- 绑定内容 -->
|
||||||
<view class="bind-content">
|
<view class="bind-content">
|
||||||
<!-- 小红书Logo -->
|
<!-- 小红书Logo -->
|
||||||
@@ -69,11 +74,18 @@
|
|||||||
|
|
||||||
<!-- 手机号登录区域 -->
|
<!-- 手机号登录区域 -->
|
||||||
<view class="phone-login-section" wx:if="{{loginType === 'phone'}}">
|
<view class="phone-login-section" wx:if="{{loginType === 'phone'}}">
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<view class="inline-loading" wx:if="{{showLoading}}">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
<text class="loading-text">{{loadingText}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 二维码验证区域(验证码验证时出现) -->
|
<!-- 二维码验证区域(验证码验证时出现) -->
|
||||||
<view class="qrcode-section" wx:if="{{needCaptcha && qrcodeImage}}">
|
<view class="qrcode-section" wx:if="{{needCaptcha && qrcodeImage}}">
|
||||||
<text class="qrcode-title">请使用小红书APP扫描二维码</text>
|
<text class="qrcode-title">请使用小红书APP扫描二维码</text>
|
||||||
<image class="qrcode" src="{{qrcodeImage}}" mode="aspectFit"></image>
|
<image class="qrcode" src="{{qrcodeImage}}" mode="aspectFit"></image>
|
||||||
<text class="qrcode-hint">扫码后即可继续绑定流程</text>
|
<text class="qrcode-hint">扫码后即可继续绑定流程</text>
|
||||||
|
<button class="save-qrcode-btn" bindtap="saveQRCode">保存二维码</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="bind-form" wx:if="{{!needCaptcha}}">
|
<view class="bind-form" wx:if="{{!needCaptcha}}">
|
||||||
@@ -127,13 +139,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="toast-overlay" wx:if="{{showLoading && loginType === 'phone'}}">
|
|
||||||
<view class="toast">
|
|
||||||
<view class="toast-loading"></view>
|
|
||||||
<text class="toast-text">{{loadingText}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="toast-overlay" wx:if="{{showFail}}">
|
<view class="toast-overlay" wx:if="{{showFail}}">
|
||||||
<view class="toast">
|
<view class="toast">
|
||||||
<view class="toast-icon info">
|
<view class="toast-icon info">
|
||||||
|
|||||||
@@ -248,6 +248,33 @@ page {
|
|||||||
color: #999;
|
color: #999;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保存二维码按钮 */
|
||||||
|
.save-qrcode-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
background: linear-gradient(135deg, #FF2442 0%, #FF4F6A 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8rpx 16rpx rgba(255, 36, 66, 0.3);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-qrcode-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-qrcode-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bind-form {
|
.bind-form {
|
||||||
@@ -465,6 +492,17 @@ page {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 内联加载提示(手机号登录区域) */
|
||||||
|
.inline-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 80rpx 0;
|
||||||
|
min-height: 300rpx;
|
||||||
|
}
|
||||||
|
|
||||||
/* 二维码错误提示 */
|
/* 二维码错误提示 */
|
||||||
.qrcode-error {
|
.qrcode-error {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ export class EmployeeService {
|
|||||||
* 发送小红书验证码
|
* 发送小红书验证码
|
||||||
* 返回 session_id 用于后续复用浏览器
|
* 返回 session_id 用于后续复用浏览器
|
||||||
*/
|
*/
|
||||||
static async sendXHSCode(xhsPhone: string, showLoading = true) {
|
static async sendXHSCode(xhsPhone: string, showLoading = true, sessionId?: string) {
|
||||||
return post<{ sent_at: string; session_id: string }>(API.xhs.sendCode, {
|
return post<{ sent_at: string; session_id: string }>(API.xhs.sendCode, {
|
||||||
xhs_phone: xhsPhone
|
xhs_phone: xhsPhone,
|
||||||
|
session_id: sessionId // 传递session_id给后端
|
||||||
}, showLoading);
|
}, showLoading);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user