commit
This commit is contained in:
@@ -18,6 +18,14 @@ database:
|
||||
max_connections: 10
|
||||
min_connections: 2
|
||||
|
||||
# ========== Redis配置 ==========
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
pool_size: 10
|
||||
|
||||
# ========== 浏览器池配置 ==========
|
||||
browser_pool:
|
||||
idle_timeout: 1800 # 空闲超时(秒),已禁用自动清理,保持常驻
|
||||
|
||||
@@ -18,6 +18,14 @@ database:
|
||||
max_connections: 20
|
||||
min_connections: 5
|
||||
|
||||
# ========== Redis配置 ==========
|
||||
redis:
|
||||
host: 8.140.194.184
|
||||
port: 6379
|
||||
password: "Redis@123456"
|
||||
db: 0
|
||||
pool_size: 10
|
||||
|
||||
# ========== 浏览器池配置 ==========
|
||||
browser_pool:
|
||||
idle_timeout: 1800 # 空闲超时(秒),已禁用自动清理,保持常驻
|
||||
|
||||
@@ -95,6 +95,16 @@ def load_config(env: str = None) -> Config:
|
||||
if os.getenv('DB_NAME'):
|
||||
config_dict.setdefault('database', {})['dbname'] = os.getenv('DB_NAME')
|
||||
|
||||
# Redis配置
|
||||
if os.getenv('REDIS_HOST'):
|
||||
config_dict.setdefault('redis', {})['host'] = os.getenv('REDIS_HOST')
|
||||
if os.getenv('REDIS_PORT'):
|
||||
config_dict.setdefault('redis', {})['port'] = int(os.getenv('REDIS_PORT'))
|
||||
if os.getenv('REDIS_PASSWORD'):
|
||||
config_dict.setdefault('redis', {})['password'] = os.getenv('REDIS_PASSWORD')
|
||||
if os.getenv('REDIS_DB'):
|
||||
config_dict.setdefault('redis', {})['db'] = int(os.getenv('REDIS_DB'))
|
||||
|
||||
# 调度器配置
|
||||
if os.getenv('SCHEDULER_ENABLED'):
|
||||
config_dict.setdefault('scheduler', {})['enabled'] = os.getenv('SCHEDULER_ENABLED').lower() == 'true'
|
||||
@@ -122,6 +132,7 @@ def load_config(env: str = None) -> Config:
|
||||
print(f"[配置] 已加载配置文件: {config_file}")
|
||||
print(f"[配置] 环境: {env}")
|
||||
print(f"[配置] 数据库: {config_dict.get('database', {}).get('host')}:{config_dict.get('database', {}).get('port')}")
|
||||
print(f"[配置] Redis: {config_dict.get('redis', {}).get('host')}:{config_dict.get('redis', {}).get('port')}")
|
||||
print(f"[配置] 调度器: {'启用' if config_dict.get('scheduler', {}).get('enabled') else '禁用'}")
|
||||
|
||||
return Config(config_dict)
|
||||
|
||||
@@ -50,13 +50,13 @@ PROXY_POOL = [
|
||||
"server": "http://111.132.40.72:50002",
|
||||
"username": "ih3z07",
|
||||
"password": "078bt7o5",
|
||||
"enabled": False
|
||||
"enabled": True
|
||||
}, {
|
||||
"name": "天启03",
|
||||
"server": "http://210.51.27.194:50001",
|
||||
"username": "hb6su3",
|
||||
"password": "acv2ciow",
|
||||
"enabled": True
|
||||
"enabled": False
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
216
backend/main.py
216
backend/main.py
@@ -62,8 +62,11 @@ class ConnectionManager:
|
||||
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] ========== 新连接建立 ==========", file=sys.stderr)
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 当前活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||
print(f"[WebSocket] 连接时间: {__import__('datetime').datetime.now()}", file=sys.stderr)
|
||||
print(f"[WebSocket] ===================================", file=sys.stderr)
|
||||
|
||||
# 立即检查缓存消息(不等待)
|
||||
if session_id in self.pending_messages:
|
||||
@@ -91,22 +94,42 @@ class ConnectionManager:
|
||||
else:
|
||||
print(f"[WebSocket] 没有缓存消息: {session_id}", file=sys.stderr)
|
||||
|
||||
def disconnect(self, session_id: str):
|
||||
def disconnect(self, session_id: str, reason: str = "未知原因"):
|
||||
"""断开WebSocket连接并记录原因"""
|
||||
if session_id in self.active_connections:
|
||||
del self.active_connections[session_id]
|
||||
print(f"[WebSocket] 断开连接: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] ========== 连接断开 ==========", file=sys.stderr)
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 断开原因: {reason}", file=sys.stderr)
|
||||
print(f"[WebSocket] 剩余活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||
print(f"[WebSocket] ===================================", file=sys.stderr)
|
||||
# 清理缓存消息
|
||||
if session_id in self.pending_messages:
|
||||
pending_count = len(self.pending_messages[session_id])
|
||||
if pending_count > 0:
|
||||
print(f"[WebSocket] 清理 {pending_count} 条未发送的缓存消息", file=sys.stderr)
|
||||
del self.pending_messages[session_id]
|
||||
|
||||
async def send_message(self, session_id: str, message: dict):
|
||||
print(f"[WebSocket] ========== 尝试发送消息 ==========", file=sys.stderr)
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 消息类型: {message.get('type')}", file=sys.stderr)
|
||||
print(f"[WebSocket] 当前活跃连接数: {len(self.active_connections)}", file=sys.stderr)
|
||||
print(f"[WebSocket] 活跃连接session_ids: {list(self.active_connections.keys())}", file=sys.stderr)
|
||||
print(f"[WebSocket] session_id在连接中: {session_id in self.active_connections}", file=sys.stderr)
|
||||
print(f"[WebSocket] ===================================", file=sys.stderr)
|
||||
|
||||
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)
|
||||
print(f"[WebSocket] ========== 发送消息失败 ==========", file=sys.stderr)
|
||||
print(f"[WebSocket] Session ID: {session_id}", file=sys.stderr)
|
||||
print(f"[WebSocket] 失败原因: {str(e)}", file=sys.stderr)
|
||||
print(f"[WebSocket] 消息类型: {message.get('type')}", file=sys.stderr)
|
||||
print(f"[WebSocket] ===================================", file=sys.stderr)
|
||||
self.disconnect(session_id, reason=f"发送消息失败: {str(e)}")
|
||||
else:
|
||||
# WebSocket还未连接,缓存消息
|
||||
print(f"[WebSocket] 连接尚未建立,缓存消息: {session_id}", file=sys.stderr)
|
||||
@@ -811,6 +834,87 @@ async def save_bind_info(request: dict):
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/save-login")
|
||||
async def save_login(request: dict):
|
||||
"""
|
||||
保存验证码登录的信息到Go后端
|
||||
与扫码登录不同,验证码登录返回的是storage_state数据
|
||||
"""
|
||||
try:
|
||||
employee_id = request.get('employee_id')
|
||||
storage_state = request.get('storage_state', {})
|
||||
storage_state_path = request.get('storage_state_path', '')
|
||||
user_info = request.get('user_info', {}) # 新增: 获取用户信息
|
||||
|
||||
if not employee_id:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="employee_id不能为空",
|
||||
data=None
|
||||
)
|
||||
|
||||
if not storage_state:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message="storage_state不能为空",
|
||||
data=None
|
||||
)
|
||||
|
||||
# 调用Go后端API保存
|
||||
config = get_config()
|
||||
go_backend_url = config.get_str('go_backend.url', 'http://localhost:8080')
|
||||
|
||||
# 从 storage_state 中提取 cookies
|
||||
cookies_full = storage_state.get('cookies', [])
|
||||
|
||||
# 构造保存数据
|
||||
save_data = {
|
||||
"employee_id": employee_id,
|
||||
"cookies_full": cookies_full,
|
||||
"storage_state": storage_state,
|
||||
"storage_state_path": storage_state_path,
|
||||
"user_info": user_info # 新增: 传递用户信息
|
||||
}
|
||||
|
||||
print(f"[保存验证码登录] employee_id={employee_id}, cookies数量={len(cookies_full)}, 用户={user_info.get('nickname', '未知')}", file=sys.stderr)
|
||||
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 获取小程序传来的token
|
||||
auth_header = request.get('Authorization', '')
|
||||
|
||||
async with session.post(
|
||||
f"{go_backend_url}/api/xhs/save-login",
|
||||
json=save_data,
|
||||
headers={'Authorization': auth_header} if auth_header else {}
|
||||
) as resp:
|
||||
result = await resp.json()
|
||||
|
||||
if resp.status == 200 and result.get('code') == 200:
|
||||
print(f"[保存验证码登录] 保存成功", file=sys.stderr)
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="保存成功",
|
||||
data=result.get('data')
|
||||
)
|
||||
else:
|
||||
print(f"[保存验证码登录] 保存失败: {result.get('message')}", file=sys.stderr)
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get('message', '保存失败'),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[保存验证码登录] 异常: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"保存失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/qrcode/cancel")
|
||||
async def cancel_qrcode_login(request: dict):
|
||||
"""
|
||||
@@ -1324,6 +1428,7 @@ async def upload_images(files: List[UploadFile] = File(...)):
|
||||
async def handle_send_code_ws(session_id: str, phone: str, country_code: str, login_page: str, websocket: WebSocket):
|
||||
"""
|
||||
异步处理WebSocket发送验证码请求
|
||||
返回: (result, service_instance) - result是结果字典,service_instance是XHSLoginService实例
|
||||
"""
|
||||
try:
|
||||
print(f"[WebSocket-SendCode] 开始处理: session={session_id}, phone={phone}", file=sys.stderr)
|
||||
@@ -1343,6 +1448,13 @@ async def handle_send_code_ws(session_id: str, phone: str, country_code: str, lo
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
# 将service实例存储到浏览器池,供后续验证码验证使用
|
||||
if session_id in browser_pool.temp_browsers:
|
||||
browser_pool.temp_browsers[session_id]['service'] = request_login_service
|
||||
print(f"[WebSocket-SendCode] 已存储service实例: {session_id}", file=sys.stderr)
|
||||
else:
|
||||
print(f"[WebSocket-SendCode] 警告: session_id {session_id} 不在temp_browsers中", file=sys.stderr)
|
||||
|
||||
# 检查是否需要验证(发送验证码时触发风控)
|
||||
if result.get("need_captcha"):
|
||||
print(f"[WebSocket-SendCode] 检测到风控,需要扫码", file=sys.stderr)
|
||||
@@ -1353,7 +1465,8 @@ async def handle_send_code_ws(session_id: str, phone: str, country_code: str, lo
|
||||
"message": result.get("message", "需要扫码验证")
|
||||
})
|
||||
print(f"[WebSocket-SendCode] 已推送风控信息", file=sys.stderr)
|
||||
return
|
||||
# 返回service实例,供外部启动监听任务
|
||||
return result, request_login_service
|
||||
|
||||
if result["success"]:
|
||||
print(f"[WebSocket-SendCode] 验证码发送成功", file=sys.stderr)
|
||||
@@ -1369,6 +1482,9 @@ async def handle_send_code_ws(session_id: str, phone: str, country_code: str, lo
|
||||
"success": False,
|
||||
"message": result.get("error", "发送验证码失败")
|
||||
})
|
||||
|
||||
return result, request_login_service
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WebSocket-SendCode] 异常: {str(e)}", file=sys.stderr)
|
||||
import traceback
|
||||
@@ -1381,6 +1497,7 @@ async def handle_send_code_ws(session_id: str, phone: str, country_code: str, lo
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return {"success": False, "error": str(e)}, None
|
||||
|
||||
async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_code: str, login_page: str, websocket: WebSocket):
|
||||
"""
|
||||
@@ -1404,7 +1521,7 @@ async def handle_verify_code_ws(session_id: str, phone: str, code: str, country_
|
||||
request_login_service = browser_data['service']
|
||||
|
||||
# 调用登录服务验证登录
|
||||
result = await request_login_service.login_with_code(
|
||||
result = await request_login_service.login(
|
||||
phone=phone,
|
||||
code=code,
|
||||
country_code=country_code,
|
||||
@@ -1483,6 +1600,43 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
"""
|
||||
await ws_manager.connect(session_id, websocket)
|
||||
|
||||
# 启动Redis订阅任务
|
||||
import asyncio
|
||||
import redis.asyncio as aioredis
|
||||
import json
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
redis_host = config.get_str('redis.host', 'localhost')
|
||||
redis_port = config.get_int('redis.port', 6379)
|
||||
redis_password = config.get_str('redis.password', '')
|
||||
|
||||
# 创建Redis订阅客户端
|
||||
redis_url = f"redis://:{redis_password}@{redis_host}:{redis_port}" if redis_password else f"redis://{redis_host}:{redis_port}"
|
||||
redis_client = await aioredis.from_url(redis_url, decode_responses=True)
|
||||
pubsub = redis_client.pubsub()
|
||||
channel = f"ws_message:{session_id}"
|
||||
await pubsub.subscribe(channel)
|
||||
print(f"[WebSocket] 已订阅Redis频道: {channel}", file=sys.stderr)
|
||||
|
||||
# 启动后台任务监听Redis消息
|
||||
async def redis_subscriber():
|
||||
try:
|
||||
async for message in pubsub.listen():
|
||||
if message['type'] == 'message':
|
||||
try:
|
||||
data = json.loads(message['data'])
|
||||
print(f"[WebSocket] 从Redis收到消息: {data}", file=sys.stderr)
|
||||
await websocket.send_json(data)
|
||||
print(f"[WebSocket] 已转发消息到前端: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 处理Redis消息失败: {str(e)}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] Redis订阅异常: {str(e)}", file=sys.stderr)
|
||||
|
||||
# 在后台启动Redis监听
|
||||
redis_task = asyncio.create_task(redis_subscriber())
|
||||
|
||||
try:
|
||||
# 保持连接,等待消息或断开
|
||||
while True:
|
||||
@@ -1496,7 +1650,6 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
else:
|
||||
# 尝试解析JSON消息
|
||||
try:
|
||||
import json
|
||||
msg = json.loads(data)
|
||||
msg_type = msg.get('type', 'unknown')
|
||||
print(f"[WebSocket] 解析消息类型: {msg_type}", file=sys.stderr)
|
||||
@@ -1519,8 +1672,15 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
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))
|
||||
# 直接处理发送验证码(不使用create_task)
|
||||
result, service_instance = await handle_send_code_ws(session_id, phone, country_code, login_page, websocket)
|
||||
|
||||
# 如果需要扫码,在当前协程中启动监听
|
||||
if result.get("need_captcha") and service_instance:
|
||||
print(f"[WebSocket] 在主协程中启动扫码监听: {session_id}", file=sys.stderr)
|
||||
# 使用create_task在后台监听,但不阻塞当前消息循环
|
||||
asyncio.create_task(service_instance._monitor_qrcode_scan(session_id))
|
||||
print(f"[WebSocket] 已启动扫码监听任务", file=sys.stderr)
|
||||
|
||||
# 处理验证码验证消息
|
||||
elif msg_type == 'verify_code':
|
||||
@@ -1536,12 +1696,38 @@ async def websocket_login(websocket: WebSocket, session_id: str):
|
||||
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 WebSocketDisconnect as e:
|
||||
reason = f"客户端主动断开连接 (code: {e.code if hasattr(e, 'code') else 'unknown'})"
|
||||
ws_manager.disconnect(session_id, reason=reason)
|
||||
except Exception as e:
|
||||
ws_manager.disconnect(session_id)
|
||||
print(f"[WebSocket] 连接异常 {session_id}: {str(e)}", file=sys.stderr)
|
||||
reason = f"连接异常: {type(e).__name__} - {str(e)}"
|
||||
ws_manager.disconnect(session_id, reason=reason)
|
||||
finally:
|
||||
# 清理Redis订阅
|
||||
try:
|
||||
redis_task.cancel()
|
||||
await pubsub.unsubscribe(channel)
|
||||
await pubsub.close()
|
||||
await redis_client.close()
|
||||
print(f"[WebSocket] 已取消Redis订阅: {channel}", file=sys.stderr)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 释放浏览器实例
|
||||
try:
|
||||
# 检查是否有临时浏览器需要释放
|
||||
if session_id in browser_pool.temp_browsers:
|
||||
print(f"[WebSocket] 检测到未释放的临时浏览器,开始清理: {session_id}", file=sys.stderr)
|
||||
await browser_pool.release_temp_browser(session_id)
|
||||
print(f"[WebSocket] 已释放临时浏览器: {session_id}", file=sys.stderr)
|
||||
|
||||
# 检查是否有扫码页面需要释放
|
||||
if session_id in browser_pool.qrcode_pages:
|
||||
print(f"[WebSocket] 检测到未释放的扫码页面,开始清理: {session_id}", file=sys.stderr)
|
||||
await browser_pool.release_qrcode_page(session_id)
|
||||
print(f"[WebSocket] 已释放扫码页面: {session_id}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] 释放浏览器异常: {str(e)}", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -576,13 +576,12 @@ class XHSLoginService:
|
||||
|
||||
# 尝试查找二维码图片元素
|
||||
qrcode_selectors = [
|
||||
'.qrcode-img', # 根据您提供的HTML
|
||||
'.qrcode-img', # 小红书风控二维码的特定class
|
||||
'img.qrcode-img',
|
||||
'.qrcode-container img',
|
||||
'img[src*="data:image"]', # base64图片
|
||||
'img[src*="qrcode"]',
|
||||
'img[alt*="二维码"]',
|
||||
'img[alt*="qrcode"]',
|
||||
'.qrcode-container img', # 二维码容器内的图片
|
||||
'.verify-captcha img', # 验证弹窗内的图片
|
||||
'img[alt*="二维码"]', # alt属性包含"二维码"
|
||||
'img[alt*="qrcode"]', # alt属性包含"qrcode"
|
||||
]
|
||||
|
||||
for selector in qrcode_selectors:
|
||||
@@ -643,9 +642,9 @@ class XHSLoginService:
|
||||
后台监听扫码后的页面跳转和二维码失效
|
||||
通过监听小红书API https://edith.xiaohongshu.com/api/redcaptcha/v2/qr/status/query
|
||||
来精准判断二维码状态:
|
||||
- status=1: 未过期,等待扫码
|
||||
- status=5: 已扫码,等待确认
|
||||
- 其他: 失效或已完成
|
||||
- status=1: 正常,等待扫码
|
||||
- status=2: 扫码完成,待APP确认
|
||||
- status=5: 二维码已过期/失效
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
@@ -653,17 +652,27 @@ class XHSLoginService:
|
||||
try:
|
||||
logger.info(f"[WebSocket] 开始监听扫码状态: {session_id}")
|
||||
|
||||
# 等待1秒,确保WebSocket连接完全建立
|
||||
logger.info(f"[WebSocket] 等待WebSocket连接建立...")
|
||||
await asyncio.sleep(1.0)
|
||||
logger.info(f"[WebSocket] 等待完成,开始监听")
|
||||
|
||||
if not self.page:
|
||||
logger.error(f"[WebSocket] 页面对象不存在: {session_id}")
|
||||
return
|
||||
|
||||
# 用于存储最新的二维码状态
|
||||
latest_qr_status = {"status": 1, "scanned": False}
|
||||
latest_qr_status = {"status": 1}
|
||||
# 标记是否已推送失效消息
|
||||
expired_notified = False
|
||||
# 标记是否已推送扫码成功消息
|
||||
scan_success_notified = False
|
||||
# 记录上次推送的状态,避免重复推送
|
||||
last_notified_status = None
|
||||
|
||||
# 设置响应监听,拦截二维码状态查询API
|
||||
async def handle_qr_status_response(response):
|
||||
nonlocal last_notified_status
|
||||
try:
|
||||
if '/api/redcaptcha/v2/qr/status/query' in response.url:
|
||||
json_data = await response.json()
|
||||
@@ -671,11 +680,56 @@ class XHSLoginService:
|
||||
status = json_data['data'].get('status')
|
||||
latest_qr_status['status'] = status
|
||||
|
||||
if status == 5:
|
||||
latest_qr_status['scanned'] = True
|
||||
logger.info(f"[WebSocket] 检测到二维码已扫描,等待确认: status={status}")
|
||||
elif status == 1:
|
||||
logger.debug(f"[WebSocket] 二维码未过期,等待扫码: status={status}")
|
||||
# 推送状态变化给前端
|
||||
if status != last_notified_status:
|
||||
status_message = {
|
||||
1: "等待扫码",
|
||||
2: "扫码完成,请在APP中确认",
|
||||
5: "二维码已过期"
|
||||
}.get(status, f"二维码状态: {status}")
|
||||
|
||||
try:
|
||||
# 使用Redis发布消息,避免事件循环隔离问题
|
||||
import redis
|
||||
import json as json_lib
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
redis_host = config.get_str('redis.host', 'localhost')
|
||||
redis_port = config.get_int('redis.port', 6379)
|
||||
redis_password = config.get_str('redis.password', '')
|
||||
|
||||
redis_client = redis.Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
password=redis_password if redis_password else None,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
message = {
|
||||
"type": "qrcode_status",
|
||||
"status": status,
|
||||
"message": status_message
|
||||
}
|
||||
|
||||
# 发布到Redis频道
|
||||
channel = f"ws_message:{session_id}"
|
||||
redis_client.publish(channel, json_lib.dumps(message))
|
||||
logger.info(f"[WebSocket] 已通过Redis推送二维码状态: status={status}, channel={channel}")
|
||||
last_notified_status = status
|
||||
|
||||
redis_client.close()
|
||||
except Exception as ws_error:
|
||||
logger.error(f"[WebSocket] 推送状态失败: {str(ws_error)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if status == 1:
|
||||
logger.debug(f"[WebSocket] 二维码正常,等待扫码: status={status}")
|
||||
elif status == 2:
|
||||
logger.info(f"[WebSocket] 检测到扫码完成,等待APP确认: status={status}")
|
||||
elif status == 5:
|
||||
logger.warning(f"[WebSocket] 检测到二维码已过期: status={status}")
|
||||
else:
|
||||
logger.info(f"[WebSocket] 二维码状态: status={status}")
|
||||
except Exception as e:
|
||||
@@ -694,88 +748,104 @@ class XHSLoginService:
|
||||
|
||||
# 1. 检测是否跳转回首页(不再是captcha/verify页)
|
||||
if 'captcha' not in current_url.lower() and 'verify' not in current_url.lower():
|
||||
# 如果已经推送过扫码成功消息,跳过
|
||||
if scan_success_notified:
|
||||
continue
|
||||
|
||||
# 检查是否跳转到小红书首页
|
||||
if 'xiaohongshu.com' in current_url:
|
||||
logger.success(f"[WebSocket] 检测到扫码完成,页面跳转回: {current_url}")
|
||||
|
||||
# 等待500ms确保WebSocket连接完全建立
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 通过WebSocket推送扫码成功消息
|
||||
try:
|
||||
from main import ws_manager
|
||||
await ws_manager.send_message(session_id, {
|
||||
# 使用Redis发布消息
|
||||
import redis
|
||||
import json as json_lib
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
redis_host = config.get_str('redis.host', 'localhost')
|
||||
redis_port = config.get_int('redis.port', 6379)
|
||||
redis_password = config.get_str('redis.password', '')
|
||||
|
||||
redis_client = redis.Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
password=redis_password if redis_password else None,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
message = {
|
||||
"type": "qrcode_scan_success",
|
||||
"message": "扫码验证完成,请重新发送验证码"
|
||||
})
|
||||
logger.success(f"[WebSocket] 已推送扫码成功消息: {session_id}")
|
||||
}
|
||||
|
||||
channel = f"ws_message:{session_id}"
|
||||
redis_client.publish(channel, json_lib.dumps(message))
|
||||
logger.success(f"[WebSocket] 已通过Redis推送扫码成功消息: channel={channel}")
|
||||
scan_success_notified = True
|
||||
|
||||
redis_client.close()
|
||||
except Exception as ws_error:
|
||||
logger.error(f"[WebSocket] 推送消息失败: {str(ws_error)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
break
|
||||
# 不退出监听,继续等待用户后续操作
|
||||
logger.info(f"[WebSocket] 扫码成功,保持监听状态")
|
||||
|
||||
# 2. 检测二维码是否失效(通过API状态判断)
|
||||
if 'captcha' in current_url.lower() or 'verify' in current_url.lower():
|
||||
# 如果已经推送过失效消息,跳过后续检测
|
||||
# 如果已经推送过失效消息,跳过后续检测
|
||||
if expired_notified:
|
||||
continue
|
||||
|
||||
# 如果状态不是1和5,说明二维码可能已失效
|
||||
if latest_qr_status['status'] not in [1, 5]:
|
||||
logger.warning(f"[WebSocket] 检测到二维码失效: status={latest_qr_status['status']}")
|
||||
# 如果状态是5,说明二维码已过期
|
||||
if latest_qr_status['status'] == 5:
|
||||
logger.warning(f"[WebSocket] API检测到二维码过期: status=5")
|
||||
|
||||
# 等待500ms确保WebSocket连接完全建立
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 通过WebSocket推送失效消息
|
||||
try:
|
||||
from main import ws_manager
|
||||
await ws_manager.send_message(session_id, {
|
||||
# 使用Redis发布消息
|
||||
import redis
|
||||
import json as json_lib
|
||||
from config import get_config
|
||||
|
||||
config = get_config()
|
||||
redis_host = config.get_str('redis.host', 'localhost')
|
||||
redis_port = config.get_int('redis.port', 6379)
|
||||
redis_password = config.get_str('redis.password', '')
|
||||
|
||||
redis_client = redis.Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
password=redis_password if redis_password else None,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
message = {
|
||||
"type": "qrcode_expired",
|
||||
"message": "二维码已失效,请重新发送验证码"
|
||||
})
|
||||
logger.success(f"[WebSocket] 已推送二维码失效消息: {session_id}")
|
||||
expired_notified = True # 标记已推送
|
||||
}
|
||||
|
||||
channel = f"ws_message:{session_id}"
|
||||
redis_client.publish(channel, json_lib.dumps(message))
|
||||
logger.success(f"[WebSocket] 已通过Redis推送二维码失效消息: channel={channel}")
|
||||
expired_notified = True
|
||||
|
||||
redis_client.close()
|
||||
except Exception as ws_error:
|
||||
logger.error(f"[WebSocket] 推送消息失败: {str(ws_error)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
break # 退出监听循环
|
||||
|
||||
# 备用方案:检查页面文本(以防API未返回)
|
||||
try:
|
||||
expired_selectors = [
|
||||
'text="已过期"',
|
||||
'text="二维码已失效"',
|
||||
'text="二维码过期"',
|
||||
]
|
||||
|
||||
for selector in expired_selectors:
|
||||
expired_elem = await self.page.query_selector(selector)
|
||||
if expired_elem:
|
||||
is_visible = await expired_elem.is_visible()
|
||||
if is_visible:
|
||||
# 进一步检查元素文本内容
|
||||
text_content = await expired_elem.text_content()
|
||||
# 只在明确显示"已过期"或"已失效"时才认为失效,忽略"二维码X分钟失效"这种提示
|
||||
if text_content and ('已过期' in text_content or '已失效' in text_content):
|
||||
logger.warning(f"[WebSocket] DOM检测到二维码失效: {selector}, 文本: {text_content}")
|
||||
|
||||
# 通过WebSocket推送失效消息
|
||||
try:
|
||||
from main import ws_manager
|
||||
await ws_manager.send_message(session_id, {
|
||||
"type": "qrcode_expired",
|
||||
"message": "二维码已失效,请重新发送验证码"
|
||||
})
|
||||
logger.success(f"[WebSocket] 已推送二维码失效消息: {session_id}")
|
||||
expired_notified = True # 标记已推送
|
||||
except Exception as ws_error:
|
||||
logger.error(f"[WebSocket] 推送消息失败: {str(ws_error)}")
|
||||
|
||||
# 退出所有循环
|
||||
break
|
||||
|
||||
# 如果检测到失效,退出外层循环
|
||||
if expired_notified:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
# 页面可能已关闭,忽略错误
|
||||
pass
|
||||
# 不退出监听,继续等待用户重新操作
|
||||
|
||||
# 每30秒打印一次状态
|
||||
if i > 0 and i % 60 == 0:
|
||||
@@ -783,21 +853,11 @@ class XHSLoginService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[WebSocket] 监听异常: {str(e)}")
|
||||
break
|
||||
# 不退出,继续监听
|
||||
|
||||
# 超时5分钟未扫码,通知前端关闭弹窗
|
||||
logger.warning(f"[WebSocket] 扫码监听超时(5分钟): {session_id}")
|
||||
try:
|
||||
from main import ws_manager
|
||||
await ws_manager.send_message(session_id, {
|
||||
"type": "qrcode_expired",
|
||||
"message": "二维码已超时,请重新发送验证码"
|
||||
})
|
||||
logger.success(f"[WebSocket] 已推送超时消息: {session_id}")
|
||||
except Exception as ws_error:
|
||||
logger.error(f"[WebSocket] 推送消息失败: {str(ws_error)}")
|
||||
|
||||
logger.info(f"[WebSocket] 监听任务结束: {session_id}")
|
||||
# 超时5分钟,通知前端(但不退出监听)
|
||||
logger.warning(f"[WebSocket] 监听已运行5分钟: {session_id}")
|
||||
logger.info(f"[WebSocket] 监听仍将继续,直到用户关闭页面")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[WebSocket] 监听任务异常: {str(e)}")
|
||||
@@ -850,6 +910,26 @@ class XHSLoginService:
|
||||
logger.warning(f"[页面导航] 导航超时,但尝试继续: {str(e)}")
|
||||
logger.info(f"[页面导航] 当前URL: {current_url}")
|
||||
|
||||
# 检测小红书反爬JSON页面
|
||||
await asyncio.sleep(0.5) # 等待页面内容加载
|
||||
try:
|
||||
page_content = await self.page.content()
|
||||
# 检查页面是否只返回JSON(小红书的检测机制)
|
||||
if page_content and len(page_content) < 500: # JSON页面通常很短
|
||||
# 尝试解析JSON
|
||||
if '{"code"' in page_content and '"success":true' in page_content:
|
||||
logger.warning("="*50)
|
||||
logger.warning("⚠️ 检测到小红书反爬JSON页面")
|
||||
logger.warning(f"页面内容: {page_content[:200]}")
|
||||
logger.warning("="*50)
|
||||
# 抛出异常,让外层处理
|
||||
raise Exception("ANTI_CRAWL_JSON")
|
||||
except Exception as e:
|
||||
if "ANTI_CRAWL_JSON" in str(e):
|
||||
raise # 重新抛出,让外层捕获
|
||||
# 其他异常忽略,继续执行
|
||||
pass
|
||||
|
||||
# 等待二维码API请求(最多等待timeout秒)
|
||||
wait_count = 0
|
||||
max_wait = timeout * 10 # 每次等待0.1秒
|
||||
@@ -919,12 +999,30 @@ class XHSLoginService:
|
||||
else:
|
||||
# 页面变了,重新访问登录页
|
||||
logger.success(f"[预热] 页面已变更 ({current_url}),重新访问{page_name}登录页...")
|
||||
try:
|
||||
await self._navigate_with_qrcode_listener(login_url)
|
||||
except Exception as e:
|
||||
if "ANTI_CRAWL_JSON" in str(e):
|
||||
logger.error("⚠️ 检测到小红书反爬检测,请稍后再试")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "当前IP被小红书检测,请等待5分钟后再试"
|
||||
}
|
||||
raise
|
||||
else:
|
||||
# 未预热或不是池模式,使用监听机制访问页面
|
||||
logger.debug(f"正在访问{page_name}登录页...")
|
||||
|
||||
try:
|
||||
await self._navigate_with_qrcode_listener(login_url)
|
||||
except Exception as e:
|
||||
if "ANTI_CRAWL_JSON" in str(e):
|
||||
logger.error("⚠️ 检测到小红书反爬检测,请稍后再试")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "当前IP被小红书检测,请等待5分钟后再试"
|
||||
}
|
||||
raise
|
||||
|
||||
logger.success(f"✅ 已进入{page_name}登录页面")
|
||||
|
||||
@@ -951,9 +1049,8 @@ class XHSLoginService:
|
||||
logger.info(f"二维码数据长度: {len(qrcode_data)} 字符")
|
||||
logger.info("返回二维码给前端,等待用户扫码后重新调用接口")
|
||||
|
||||
# 启动后台任务监听页面跳转,扫码完成后通知前端
|
||||
asyncio.create_task(self._monitor_qrcode_scan(session_id))
|
||||
logger.info(f"[WebSocket] 已启动扫码监听任务: {session_id}")
|
||||
# 不再在这里启动监听任务,由main.py中的WebSocket端点启动
|
||||
# asyncio.create_task(self._monitor_qrcode_scan(session_id))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
@@ -1517,14 +1614,14 @@ class XHSLoginService:
|
||||
logger.success(f"✅ 检测到登录成功,用户: {user_me_data.get('nickname')}")
|
||||
|
||||
# 通过WebSocket推送登录成功消息
|
||||
if session_id:
|
||||
if self.session_id:
|
||||
try:
|
||||
from main import ws_manager
|
||||
await ws_manager.send_message(session_id, {
|
||||
await ws_manager.send_message(self.session_id, {
|
||||
"type": "login_success",
|
||||
"user_info": user_me_data
|
||||
})
|
||||
logger.info(f"[WebSocket] 已推送登录成功消息: {session_id}")
|
||||
logger.info(f"[WebSocket] 已推送登录成功消息: {self.session_id}")
|
||||
except Exception as ws_error:
|
||||
logger.error(f"[WebSocket] 推送消息失败: {str(ws_error)}")
|
||||
except Exception as e:
|
||||
@@ -1599,7 +1696,7 @@ class XHSLoginService:
|
||||
"need_captcha": True,
|
||||
"captcha_type": "qrcode",
|
||||
"qrcode_image": qrcode_data,
|
||||
"session_id": session_id, # 返回session_id,供后续轮询使用
|
||||
"session_id": self.session_id, # 返回session_id,供后续轮询使用
|
||||
"message": "需要扫码验证,请使用小红书APP扫描二维码"
|
||||
}
|
||||
else:
|
||||
@@ -1626,25 +1723,44 @@ class XHSLoginService:
|
||||
logger.info(f"最终URL: {self.page.url}")
|
||||
logger.info("="*50)
|
||||
|
||||
# 2. 即使URL没变,也要检测页面上是否出现二维码弹窗
|
||||
logger.info("检测页面上是否出现扫码验证...")
|
||||
# 2. 只有在未检测到风控且未登录成功时,才检测页面上是否出现二维码弹窗
|
||||
current_url = self.page.url
|
||||
# 如果已经跳转到成功页面,不再检测二维码
|
||||
if 'explore' in current_url or 'creator' in current_url or 'xiaohongshu.com' in current_url:
|
||||
if 'login' not in current_url:
|
||||
logger.info("已跳转到登录成功页面,跳过二维码检测")
|
||||
else:
|
||||
logger.info("仍在登录页,检测页面上是否出现扫码验证...")
|
||||
# 先检测提示文本
|
||||
try:
|
||||
tip_elem = await self.page.query_selector('.tip')
|
||||
if tip_elem:
|
||||
tip_text = await tip_elem.inner_text()
|
||||
logger.info(f"检测到提示文本: {tip_text}")
|
||||
if '扫码' in tip_text or '二维码' in tip_text:
|
||||
logger.warning("⚠️ 确认检测到扫码验证提示")
|
||||
except Exception as e:
|
||||
logger.debug(f"检测提示文本失败: {str(e)}")
|
||||
|
||||
qrcode_selectors = [
|
||||
'.qrcode-img',
|
||||
'.qrcode-img', # 小红书风控二维码的特定class
|
||||
'img.qrcode-img',
|
||||
'.qrcode-container img',
|
||||
'img[src*="data:image"]',
|
||||
'img[src*="qrcode"]',
|
||||
'img[alt*="二维码"]',
|
||||
'img[alt*="qrcode"]',
|
||||
'.qrcode-container img', # 二维码容器内的图片
|
||||
'.qrcode .qrcode-img', # 二维码容器下的二维码图片
|
||||
'.verify-captcha img', # 验证弹窗内的图片
|
||||
'.login-container .qrcode-img', # 登录容器内的二维码
|
||||
'img[alt*="二维码"]', # alt属性包含"二维码"
|
||||
'img[alt*="qrcode"]', # alt属性包含"qrcode"
|
||||
]
|
||||
|
||||
for selector in qrcode_selectors:
|
||||
try:
|
||||
qrcode_elem = await self.page.query_selector(selector)
|
||||
if qrcode_elem:
|
||||
logger.warning(f"⚠️ 检测到页面上出现二维码: {selector}")
|
||||
logger.info(f"检测到符合选择器的元素: {selector},尝试提取二维码...")
|
||||
qrcode_data = await self.extract_verification_qrcode()
|
||||
if qrcode_data:
|
||||
logger.warning(f"⚠️ 确认检测到风控二维码: {selector}")
|
||||
logger.success("✅ 成功提取扫码验证二维码,返回给前端")
|
||||
# 注意:不移除API监听,保持session_id对应的浏览器继续运行
|
||||
return {
|
||||
@@ -1652,14 +1768,69 @@ class XHSLoginService:
|
||||
"need_captcha": True,
|
||||
"captcha_type": "qrcode",
|
||||
"qrcode_image": qrcode_data,
|
||||
"session_id": session_id, # 返回session_id,供后续轮询使用
|
||||
"session_id": self.session_id, # 返回session_id,供后续轮询使用
|
||||
"message": "需要扫码验证,请使用小红书APP扫描二维码"
|
||||
}
|
||||
else:
|
||||
logger.debug(f"选择器 {selector} 匹配到元素但无法提取二维码,可能不是风控二维码")
|
||||
break
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.debug(f"选择器 {selector} 检测失败: {str(e)}")
|
||||
continue
|
||||
|
||||
logger.info("未检测到扫码验证,继续等待登录...")
|
||||
logger.info("未检测到扫码验证")
|
||||
else:
|
||||
logger.info("仍在登录页,检测页面上是否出现扫码验证...")
|
||||
# 先检测提示文本
|
||||
try:
|
||||
tip_elem = await self.page.query_selector('.tip')
|
||||
if tip_elem:
|
||||
tip_text = await tip_elem.inner_text()
|
||||
logger.info(f"检测到提示文本: {tip_text}")
|
||||
if '扫码' in tip_text or '二维码' in tip_text:
|
||||
logger.warning("⚠️ 确认检测到扫码验证提示")
|
||||
except Exception as e:
|
||||
logger.debug(f"检测提示文本失败: {str(e)}")
|
||||
|
||||
qrcode_selectors = [
|
||||
'.qrcode-img', # 小红书风控二维码的特定class
|
||||
'img.qrcode-img',
|
||||
'.qrcode-container img', # 二维码容器内的图片
|
||||
'.qrcode .qrcode-img', # 二维码容器下的二维码图片
|
||||
'.verify-captcha img', # 验证弹窗内的图片
|
||||
'.login-container .qrcode-img', # 登录容器内的二维码
|
||||
'img[alt*="二维码"]', # alt属性包含"二维码"
|
||||
'img[alt*="qrcode"]', # alt属性包含"qrcode"
|
||||
]
|
||||
|
||||
for selector in qrcode_selectors:
|
||||
try:
|
||||
qrcode_elem = await self.page.query_selector(selector)
|
||||
if qrcode_elem:
|
||||
logger.info(f"检测到符合选择器的元素: {selector},尝试提取二维码...")
|
||||
qrcode_data = await self.extract_verification_qrcode()
|
||||
if qrcode_data:
|
||||
logger.warning(f"⚠️ 确认检测到风控二维码: {selector}")
|
||||
logger.success("✅ 成功提取扫码验证二维码,返回给前端")
|
||||
# 注意:不移除API监听,保持session_id对应的浏览器继续运行
|
||||
return {
|
||||
"success": False,
|
||||
"need_captcha": True,
|
||||
"captcha_type": "qrcode",
|
||||
"qrcode_image": qrcode_data,
|
||||
"session_id": self.session_id, # 返回session_id,供后续轮询使用
|
||||
"message": "需要扫码验证,请使用小红书APP扫描二维码"
|
||||
}
|
||||
else:
|
||||
logger.debug(f"选择器 {selector} 匹配到元素但无法提取二维码,可能不是风控二维码")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"选择器 {selector} 检测失败: {str(e)}")
|
||||
continue
|
||||
|
||||
logger.info("未检测到扫码验证")
|
||||
|
||||
logger.info("继续等待登录...")
|
||||
|
||||
# 等待URL跳转或API响应(最多30秒)
|
||||
logger.info("[登录检测] 等待扫码完成或登录跳转...")
|
||||
@@ -1908,6 +2079,7 @@ class XHSLoginService:
|
||||
"localStorage": localStorage_data, # API 返回:localStorage数据
|
||||
"sessionStorage": sessionStorage_data, # API 返回:sessionStorage数据
|
||||
"url": current_url,
|
||||
"storage_state": storage_state_data, # 新增:Playwright storage_state对象
|
||||
"storage_state_path": storage_state_path # 新增:storage_state文件路径
|
||||
}
|
||||
|
||||
@@ -3162,6 +3334,26 @@ class XHSLoginService:
|
||||
current_url = self.page.url
|
||||
logger.success(f"[扫码登录] 页面加载完成, 当前URL: {current_url}")
|
||||
|
||||
# 检测小红书反爬JSON页面
|
||||
await asyncio.sleep(0.5) # 等待页面内容加载
|
||||
try:
|
||||
page_content = await self.page.content()
|
||||
# 检查页面是否只返回JSON(小红书的检测机制)
|
||||
if page_content and len(page_content) < 500: # JSON页面通常很短
|
||||
# 尝试解析JSON
|
||||
if '{"code"' in page_content and '"success":true' in page_content:
|
||||
logger.warning("="*50)
|
||||
logger.warning("⚠️ 检测到小红书反爬JSON页面")
|
||||
logger.warning(f"页面内容: {page_content[:200]}")
|
||||
logger.warning("="*50)
|
||||
return {
|
||||
"success": False,
|
||||
"error": "当前IP被小红书检测,请等待5分钟后再试"
|
||||
}
|
||||
except Exception as e:
|
||||
# 其他异常忽略,继续执行
|
||||
pass
|
||||
|
||||
# 检查是否跳转到验证码页面
|
||||
if '/website-login/captcha' in current_url or 'verifyUuid=' in current_url:
|
||||
logger.warning(f"[扫码登录] 检测到风控验证页面,尝试等待或跳过...")
|
||||
|
||||
@@ -781,6 +781,31 @@ func (ctrl *EmployeeController) SaveQRCodeLogin(c *gin.Context) {
|
||||
common.SuccessWithMessage(c, "绑定成功", nil)
|
||||
}
|
||||
|
||||
// SaveLogin 保存验证码登录的绑定信息
|
||||
func (ctrl *EmployeeController) SaveLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
EmployeeID int `json:"employee_id" binding:"required"`
|
||||
CookiesFull []interface{} `json:"cookies_full"`
|
||||
StorageState map[string]interface{} `json:"storage_state"`
|
||||
StorageStatePath string `json:"storage_state_path"`
|
||||
UserInfo map[string]interface{} `json:"user_info"` // 新增: 用户信息
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用service层保存
|
||||
err := ctrl.service.SaveLogin(req.EmployeeID, req.CookiesFull, req.StorageState, req.StorageStatePath, req.UserInfo)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "绑定成功", nil)
|
||||
}
|
||||
|
||||
// StartQRCodeLogin 启动扫码登录,转发到Python服务
|
||||
func (ctrl *EmployeeController) StartQRCodeLogin(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
@@ -130,6 +130,8 @@ func SetupRouter(r *gin.Engine) {
|
||||
|
||||
// 保存扫码登录的绑定信息
|
||||
xhs.POST("/save-qrcode-login", xhsCtrl.SaveQRCodeLogin)
|
||||
// 保存验证码登录的绑定信息
|
||||
xhs.POST("/save-login", xhsCtrl.SaveLogin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2341,6 +2341,151 @@ func (s *EmployeeService) SaveQRCodeLogin(employeeID int, cookiesFull []interfac
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveLogin 保存验证码登录的信息
|
||||
func (s *EmployeeService) SaveLogin(employeeID int, cookiesFull []interface{}, storageState map[string]interface{}, storageStatePath string, userInfo map[string]interface{}) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// 查询用户信息
|
||||
var employee models.User
|
||||
if err := database.DB.First(&employee, employeeID).Error; err != nil {
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 优先使用 storage_state,如果没有则降级使用 cookies_full
|
||||
var loginStateJSON string
|
||||
|
||||
if len(storageState) > 0 {
|
||||
// 新版:使用 Playwright 的 storage_state
|
||||
storageStateBytes, err := json.Marshal(storageState)
|
||||
if err == nil {
|
||||
loginStateJSON = string(storageStateBytes)
|
||||
log.Printf("验证码登录 - 用户%d - StorageState长度: %d", employeeID, len(loginStateJSON))
|
||||
} else {
|
||||
log.Printf("验证码登录 - 用户%d - 序列化storage_state失败: %v", employeeID, err)
|
||||
}
|
||||
} else if len(cookiesFull) > 0 {
|
||||
// 降级:使用旧版本的 cookies_full
|
||||
log.Printf("验证码登录 - 用户%d - 警告: 未找到storage_state,降级使用cookies", employeeID)
|
||||
cookiesBytes, err := json.Marshal(cookiesFull)
|
||||
if err == nil {
|
||||
loginStateJSON = string(cookiesBytes)
|
||||
log.Printf("验证码登录 - 用户%d - Cookie长度: %d", employeeID, len(loginStateJSON))
|
||||
}
|
||||
}
|
||||
|
||||
if loginStateJSON == "" {
|
||||
log.Printf("验证码登录 - 用户%d - 错误: 未能获取到任何登录数据", employeeID)
|
||||
return errors.New("登录成功但未能获取到登录数据,请重试")
|
||||
}
|
||||
|
||||
// 从 userInfo 提取小红书账号信息
|
||||
xhsNickname := "小红书用户" // 默认值
|
||||
xhsPhone := "" // red_id
|
||||
xhsUserId := "" // user_id
|
||||
|
||||
if len(userInfo) > 0 {
|
||||
// 优先使用 nickname
|
||||
if nickname, ok := userInfo["nickname"].(string); ok && nickname != "" {
|
||||
xhsNickname = nickname
|
||||
}
|
||||
// 提取 red_id 作为 xhs_phone
|
||||
if redID, ok := userInfo["red_id"].(string); ok && redID != "" {
|
||||
xhsPhone = redID
|
||||
}
|
||||
// 提取 user_id
|
||||
if userID, ok := userInfo["user_id"].(string); ok && userID != "" {
|
||||
xhsUserId = userID
|
||||
}
|
||||
log.Printf("验证码登录 - 用户%d - 提取的用户信息: nickname=%s, red_id=%s, user_id=%s", employeeID, xhsNickname, xhsPhone, xhsUserId)
|
||||
} else {
|
||||
log.Printf("验证码登录 - 用户%d - 警告: userInfo为空,使用默认值", employeeID)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 开启事务
|
||||
tx := database.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建或更新 ai_authors 表的小红书账号记录
|
||||
log.Printf("验证码登录 - 用户%d - 开始创建或更新作者记录", employeeID)
|
||||
|
||||
author := models.Author{
|
||||
EnterpriseID: employee.EnterpriseID,
|
||||
CreatedUserID: employeeID,
|
||||
Phone: employee.Phone,
|
||||
AuthorName: xhsNickname,
|
||||
XHSCookie: loginStateJSON,
|
||||
XHSPhone: xhsPhone,
|
||||
XHSAccount: xhsNickname,
|
||||
BoundAt: &now,
|
||||
Channel: 1, // 1=小红书
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
// 查询是否已存在记录
|
||||
var existingAuthor models.Author
|
||||
err := database.DB.Where("created_user_id = ? AND enterprise_id = ? AND channel = 1",
|
||||
employeeID, employee.EnterpriseID).First(&existingAuthor).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
if err := tx.Create(&author).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("验证码登录 - 用户%d - 创建作者记录失败: %v", employeeID, err)
|
||||
return fmt.Errorf("创建作者记录失败: %w", err)
|
||||
}
|
||||
log.Printf("验证码登录 - 用户%d - 创建作者记录成功", employeeID)
|
||||
} else {
|
||||
// 更新现有记录
|
||||
if err := tx.Model(&models.Author{}).Where(
|
||||
"created_user_id = ? AND enterprise_id = ? AND channel = 1",
|
||||
employeeID, employee.EnterpriseID,
|
||||
).Updates(map[string]interface{}{
|
||||
"author_name": xhsNickname,
|
||||
"xhs_cookie": loginStateJSON,
|
||||
"xhs_phone": xhsPhone,
|
||||
"xhs_account": xhsNickname,
|
||||
"bound_at": &now,
|
||||
"status": "active",
|
||||
"phone": employee.Phone,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("验证码登录 - 用户%d - 更新作者记录失败: %v", employeeID, err)
|
||||
return fmt.Errorf("更新作者记录失败: %w", err)
|
||||
}
|
||||
log.Printf("验证码登录 - 用户%d - 更新作者记录成功", employeeID)
|
||||
}
|
||||
|
||||
// 更新 ai_users 表的绑定标识
|
||||
if err := tx.Model(&employee).Update("is_bound_xhs", 1).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("验证码登录 - 用户%d - 更新用户绑定标识失败: %v", employeeID, err)
|
||||
return fmt.Errorf("更新用户绑定标识失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("验证码登录 - 用户%d - 数据库更新成功", employeeID)
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Printf("验证码登录 - 用户%d - 事务提交失败: %v", employeeID, err)
|
||||
return fmt.Errorf("提交事务失败: %w", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
cacheService := NewCacheService()
|
||||
if err := cacheService.ClearUserRelatedCache(ctx, employeeID); err != nil {
|
||||
log.Printf("清除缓存失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("验证码登录 - 用户%d - 绑定成功", employeeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartQRCodeLogin 启动扫码登录,转发到Python服务
|
||||
func (s *EmployeeService) StartQRCodeLogin(employeeID int) (map[string]interface{}, error) {
|
||||
log.Printf("[启动扫码登录] 用户ID: %d", employeeID)
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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,6 +1329,16 @@ Page({
|
||||
// 关闭WebSocket
|
||||
this.closeWebSocket();
|
||||
|
||||
// 显示保存中
|
||||
wx.showLoading({
|
||||
title: '正在保存登录信息...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 保存登录信息到数据库
|
||||
this.saveLoginInfo(data).then(() => {
|
||||
wx.hideLoading();
|
||||
|
||||
// 显示绑定成功动画
|
||||
this.setData({
|
||||
showSuccess: true
|
||||
@@ -1055,6 +1360,15 @@ Page({
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}).catch((err) => {
|
||||
wx.hideLoading();
|
||||
console.error('保存登录信息失败:', err);
|
||||
wx.showToast({
|
||||
title: err.message || '保存失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
});
|
||||
} 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 isNormalClose = (this.data as any).normalClose || res.code === 1000;
|
||||
|
||||
// 清除正常关闭标记
|
||||
this.setData({ normalClose: false } as any);
|
||||
|
||||
// 检查是否需要重连(只要页面还在且未隐藏就重连)
|
||||
// 增加重连计数
|
||||
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
|
||||
// 最多重连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}次重连`);
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.needCaptcha) { // 再次检查弹窗是否还在
|
||||
// 检查页面是否还在且未隐藏(通过sessionId存在和pageHidden来判断)
|
||||
if (this.data.sessionId && !(this.data as any).pageHidden) {
|
||||
console.log('[WebSocket] 开始重连...');
|
||||
this.setData({ reconnectCount: reconnectCount + 1 } as any);
|
||||
this.connectWebSocket(sessionId);
|
||||
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: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<!--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>
|
||||
|
||||
<!-- 绑定内容 -->
|
||||
@@ -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}}">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user