This commit is contained in:
sjk
2026-01-09 23:27:52 +08:00
parent 8446c004e7
commit 3b66018271
18 changed files with 2006 additions and 508 deletions

View File

@@ -56,7 +56,7 @@ PROXY_POOL = [
"server": "http://210.51.27.194:50001",
"username": "hb6su3",
"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

View File

@@ -13,7 +13,7 @@ from config import init_config, get_config
from dotenv import load_dotenv
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 pydantic import BaseModel
from typing import Optional, Dict, Any, List
@@ -51,6 +51,75 @@ scheduler = 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]:
"""从代理池接口获取一个代理地址http://ip:port获取失败返回None"""
@@ -97,6 +166,7 @@ class SendCodeRequest(BaseModel):
phone: str
country_code: str = "+86"
login_page: Optional[str] = None # 登录页面creator 或 home为None时使用配置文件默认值
session_id: Optional[str] = None # 可选前端生成的session_id用于WebSocket通知
class VerifyCodeRequest(BaseModel):
phone: str
@@ -288,10 +358,14 @@ async def send_code(request: SendCodeRequest):
支持选择从创作者中心或小红书首页登录
并发支持:为每个请求分配独立的浏览器实例
"""
# 使用随机UUID作为session_id确保每次都创建全新浏览器,完全不复用
import uuid
session_id = f"xhs_login_{uuid.uuid4().hex}"
print(f"[发送验证码] 创建全新浏览器实例 session_id={session_id}, phone={request.phone}", file=sys.stderr)
# 使用前端传递的session_id如果没有则生成新的
if request.session_id:
session_id = request.session_id
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参数
config = get_config()
@@ -312,9 +386,24 @@ async def send_code(request: SendCodeRequest):
result = await request_login_service.send_verification_code(
phone=request.phone,
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 browser_pool and session_id in browser_pool.temp_browsers:
@@ -835,7 +924,22 @@ async def login(request: LoginRequest):
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:
try:
await browser_pool.release_temp_browser(session_id)
@@ -1217,6 +1321,228 @@ async def upload_images(files: List[UploadFile] = File(...)):
"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__":
import uvicorn
@@ -1227,7 +1553,9 @@ if __name__ == "__main__":
debug = config.get_bool('server.debug', 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(
app,

View 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()

View File

@@ -14,3 +14,4 @@ alibabacloud_credentials==0.3.4
alibabacloud_tea_openapi==0.3.9
alibabacloud_tea_util==0.3.13
loguru==0.7.2
websockets==12.0

File diff suppressed because it is too large Load Diff

View File

@@ -42,13 +42,20 @@ func (ctrl *EmployeeController) SendXHSCode(c *gin.Context) {
// 获取当前登录用户ID
employeeID := c.GetInt("employee_id")
err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID)
data, err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID)
if err != nil {
common.Error(c, common.CodeInternalError, err.Error())
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 获取员工个人信息

View File

@@ -36,6 +36,13 @@ func (ctrl *XHSController) SendCode(c *gin.Context) {
return
}
// 检查是否需要扫码验证
if needCaptcha, ok := result.Data["need_captcha"].(bool); ok && needCaptcha {
// 发送验证码时触发风控,返回二维码
common.SuccessWithMessage(c, result.Message, result.Data)
return
}
// 判断Python服务返回的结果
if result.Code != 0 {
common.Error(c, result.Code, result.Message)

View File

@@ -31,7 +31,7 @@ type XHSCookieVerifyResult struct {
}
// 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()
// 预检查:验证该手机号是否已被其他用户绑定
@@ -45,11 +45,11 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
// 找到了其他用户的绑定记录
log.Printf("发送验证码 - 用户%d - 失败: 手机号%s已被用户%d绑定",
employeeID, phone, conflictAuthor.CreatedUserID)
return errors.New("该手机号已被其他用户绑定")
return nil, errors.New("该手机号已被其他用户绑定")
} else if err != gorm.ErrRecordNotFound {
// 数据库查询异常
log.Printf("发送验证码 - 用户%d - 检查手机号失败: %v", employeeID, err)
return fmt.Errorf("检查手机号失败: %w", err)
return nil, fmt.Errorf("检查手机号失败: %w", err)
}
// err == gorm.ErrRecordNotFound 表示该手机号未被绑定,可以继续
@@ -57,7 +57,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
rateLimitKey := fmt.Sprintf("rate:sms:%s", phone)
exists, err := utils.ExistsCache(ctx, rateLimitKey)
if err == nil && exists {
return errors.New("验证码发送过于频繁,请稍后再试")
return nil, errors.New("验证码发送过于频繁,请稍后再试")
}
// 从配置获取Python服务地址
@@ -76,7 +76,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
jsonData, err := json.Marshal(requestData)
if err != nil {
log.Printf("[发送验证码] 序列化请求数据失败: %v", err)
return errors.New("网络错误,请稍后重试")
return nil, errors.New("网络错误,请稍后重试")
}
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))
if err != nil {
log.Printf("[发送验证码] 创建请求失败: %v", err)
return errors.New("网络错误,请稍后重试")
return nil, errors.New("网络错误,请稍后重试")
}
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)
// 判断是否是超时错误
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()
@@ -109,7 +109,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[发送验证码] 读取响应失败: %v", err)
return errors.New("网络错误,请稍后重试")
return nil, errors.New("网络错误,请稍后重试")
}
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 {
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)
// 检查响应codeFastAPI返回code=0为成功
if apiResponse.Code != 0 {
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分钟
if err := utils.SetCache(ctx, rateLimitKey, "1", 1*time.Minute); err != nil {
log.Printf("设置限流缓存失败: %v", err)
}
log.Printf("[发送验证码] 验证码发送成功")
return nil
return apiResponse.Data, nil
}
// getFriendlyErrorMessage 将技术错误信息转换为用户友好提示

View File

@@ -254,11 +254,38 @@ Page({
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;
}
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({

View File

@@ -17,6 +17,7 @@ Page({
countryCodeIndex: 0,
pollTimer: null as any, // 轮询定时器
pollCount: 0, // 轮询次数
socketTask: null as any, // WebSocket连接
// 验证码相关
needCaptcha: false, // 是否需要验证码
captchaType: '', // 验证码类型
@@ -38,6 +39,15 @@ Page({
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);
},
onUnload() {
@@ -48,6 +58,10 @@ Page({
if (this.data.pollTimer) {
clearInterval(this.data.pollTimer);
}
// 关闭WebSocket连接
if (this.data.socketTask) {
this.data.socketTask.close();
}
},
// 区号选择
@@ -77,7 +91,7 @@ Page({
return;
}
const { phone, countryCodes, countryCodeIndex } = this.data;
const { phone, countryCodes, countryCodeIndex, sessionId, socketTask } = this.data;
if (phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
@@ -87,43 +101,52 @@ Page({
return;
}
// 显示加载
this.setData({
showLoading: true
});
try {
// 调用后端API发送验证码
const countryCode = countryCodes[countryCodeIndex];
console.log('发送验证码:', phone, '区号:', countryCode);
console.log('[发送验证码] 开始,手机号:', phone, '区号:', countryCode);
console.log('[发送验证码] 使用现有session_id:', sessionId);
const result = await EmployeeService.sendXHSCode(phone);
// 保存session_id用于后续复用浏览器
if (result.data && result.data.session_id) {
this.setData({
sessionId: result.data.session_id
});
console.log('已保存session_id:', result.data.session_id);
// 检查WebSocket连接
if (!socketTask) {
console.error('[发送验证码] WebSocket连接不存在重新建立...');
this.connectWebSocket(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
}
this.setData({
showLoading: false
// 通过WebSocket发送send_code消息
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) {
this.setData({
showLoading: false
});
console.error('[发送验证码] 异常:', error);
wx.showToast({
title: error.message || '发送失败,请重试',
icon: 'none',
@@ -164,7 +187,12 @@ Page({
// 绑定账号
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) {
@@ -196,42 +224,50 @@ Page({
return;
}
// 显示加载
this.setData({
showLoading: true,
loadingText: '正在验证...',
pollCount: 0
});
try {
// 调用后端API进行绑定异步处理
console.log('绑定小红书账号:', { phone, code, sessionId });
console.log('[绑定账号] 通过WebSocket发送验证码验证请求...');
const result = await EmployeeService.bindXHS(phone, code, sessionId);
const socketTask = this.data.socketTask;
if (!socketTask) {
throw new Error('WebSocket连接已断开请重新发送验证码');
}
// 后端立即返回,开始轮询绑定状态
this.setData({
loadingText: '正在登录小红书...'
const countryCode = countryCodes[countryCodeIndex];
wx.showToast({
title: '正在验证...',
icon: 'loading',
duration: 60000
});
// 开始轮询绑定状态
this.startPollingBindStatus();
socketTask.send({
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) {
this.setData({
showLoading: false
console.error('[绑定账号] 异常:', error);
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);
}
// 初始化轮询计数
this.setData({
pollCount: 0
});
// 立即查询一次
this.checkBindStatus();
@@ -264,17 +305,12 @@ Page({
// 超过60次轮询60秒停止轮询
if (pollCount > 60) {
this.stopPolling();
this.setData({
showLoading: false,
showFail: true
wx.hideToast();
wx.showToast({
title: '登录超时,请重试',
icon: 'none',
duration: 2000
});
setTimeout(() => {
this.setData({
showFail: false
});
}, 2000);
return;
}
@@ -293,10 +329,12 @@ Page({
if (status.status === 'success') {
// 绑定成功
this.stopPolling();
wx.hideToast();
this.setData({
showLoading: false,
showSuccess: true
wx.showToast({
title: '绑定成功',
icon: 'success',
duration: 2000
});
// 更新本地绑定状态缓存
@@ -312,10 +350,6 @@ Page({
console.log('[手机号登录] 绑定成功,已更新本地缓存');
setTimeout(() => {
this.setData({
showSuccess: false
});
// 绑定成功后返回个人中心
wx.navigateBack();
}, 2000);
@@ -323,43 +357,36 @@ Page({
} else if (status.status === 'failed') {
// 绑定失败
this.stopPolling();
wx.hideToast();
this.setData({
showLoading: false,
showFail: true
});
wx.showToast({
title: status.error || '绑定失败',
icon: 'none',
duration: 3000
});
setTimeout(() => {
this.setData({
showFail: false
});
}, 2000);
} else if (status.status === 'need_captcha') {
// 需要验证码验证
this.stopPolling();
wx.hideToast();
console.log('需要验证码验证:', status.captcha_type);
wx.showToast({
title: status.message || '需要验证码验证',
icon: 'none',
duration: 3000
});
this.setData({
showLoading: false,
needCaptcha: true,
captchaType: status.captcha_type || 'unknown',
qrcodeImage: status.qrcode_image || '',
loadingText: status.message || '需要验证码验证'
qrcodeImage: status.qrcode_image || ''
});
} else if (status.status === 'processing') {
// 仍在处理中,继续轮询
this.setData({
loadingText: status.message || '正在登录小红书...'
});
console.log('登录处理中:', status.message);
}
} catch (error: any) {
@@ -380,9 +407,22 @@ Page({
// 切换到手机号登录
switchToPhone() {
console.log('[切换登录] 切换到手机号登录');
// 停止二维码轮询
this.stopQRCodePolling();
// 关闭二维码弹窗(如果正在显示)
if (this.data.needCaptcha) {
console.log('[切换登录] 检测到风控弹窗,关闭...');
this.setData({
needCaptcha: false
});
// 关闭WebSocket连接
this.closeWebSocket();
}
// 清空所有扫码登录相关数据
this.setData({
loginType: 'phone',
@@ -401,6 +441,8 @@ Page({
countdown: 0,
isCounting: false
});
console.log('[切换登录] 切换完成');
},
// 切换到扫码登录
@@ -848,5 +890,536 @@ Page({
this.stopQRCodePolling();
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 });
}
});

View File

@@ -1,5 +1,10 @@
<!--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>
<!-- 绑定内容 -->
<view class="bind-content">
<!-- 小红书Logo -->
@@ -69,11 +74,18 @@
<!-- 手机号登录区域 -->
<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}}">
<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>
<view class="bind-form" wx:if="{{!needCaptcha}}">
@@ -127,13 +139,6 @@
</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">
<view class="toast-icon info">

View File

@@ -248,6 +248,33 @@ page {
color: #999;
text-align: center;
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 {
@@ -465,6 +492,17 @@ page {
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 {
position: absolute;

View File

@@ -74,9 +74,10 @@ export class EmployeeService {
* 发送小红书验证码
* 返回 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, {
xhs_phone: xhsPhone
xhs_phone: xhsPhone,
session_id: sessionId // 传递session_id给后端
}, showLoading);
}

1
验证.txt Normal file
View File

@@ -0,0 +1 @@
https://www.xiaohongshu.com/website-login/captcha?redirectPath=https%3A%2F%2Fwww.xiaohongshu.com%2Fexplore%3FexSource%3D&verifyUuid=e3d62847-8664-4bd0-8c7c-9a9032d6eaff&verifyType=124&verifyBiz=461