first commit
This commit is contained in:
282
backend/main.py
Normal file
282
backend/main.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any, List
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from xhs_login import XHSLoginService
|
||||
|
||||
app = FastAPI(title="小红书登录API")
|
||||
|
||||
# CORS配置
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 生产环境应该限制具体域名
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 全局登录服务实例
|
||||
login_service = XHSLoginService()
|
||||
|
||||
# 临时文件存储目录
|
||||
TEMP_DIR = Path("temp_uploads")
|
||||
TEMP_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# 请求模型
|
||||
class SendCodeRequest(BaseModel):
|
||||
phone: str
|
||||
country_code: str = "+86"
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
phone: str
|
||||
code: str
|
||||
country_code: str = "+86"
|
||||
|
||||
class PublishNoteRequest(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
images: Optional[list] = None
|
||||
topics: Optional[list] = None
|
||||
|
||||
class InjectCookiesRequest(BaseModel):
|
||||
cookies: list
|
||||
|
||||
# 响应模型
|
||||
class BaseResponse(BaseModel):
|
||||
code: int
|
||||
message: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""启动时不初始化浏览器,等待第一次请求时再初始化"""
|
||||
pass
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""关闭时清理浏览器"""
|
||||
await login_service.close_browser()
|
||||
|
||||
@app.post("/api/xhs/send-code", response_model=BaseResponse)
|
||||
async def send_code(request: SendCodeRequest):
|
||||
"""
|
||||
发送验证码
|
||||
通过playwright访问小红书官网,输入手机号并触发验证码发送
|
||||
"""
|
||||
try:
|
||||
# 调用登录服务发送验证码
|
||||
result = await login_service.send_verification_code(
|
||||
phone=request.phone,
|
||||
country_code=request.country_code
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="验证码已发送,请在小红书APP中查看",
|
||||
data={"sent_at": datetime.now().isoformat()}
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("error", "发送验证码失败"),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"发送验证码异常: {str(e)}")
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"发送验证码失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/login", response_model=BaseResponse)
|
||||
async def login(request: LoginRequest):
|
||||
"""
|
||||
登录验证
|
||||
用户填写验证码后,完成登录并获取小红书返回的数据
|
||||
"""
|
||||
try:
|
||||
# 调用登录服务进行登录
|
||||
result = await login_service.login(
|
||||
phone=request.phone,
|
||||
code=request.code,
|
||||
country_code=request.country_code
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="登录成功",
|
||||
data={
|
||||
"user_info": result.get("user_info"),
|
||||
"cookies": result.get("cookies"), # 键值对格式(前端展示)
|
||||
"cookies_full": result.get("cookies_full"), # Playwright完整格式(数据库存储/脚本使用)
|
||||
"login_time": datetime.now().isoformat()
|
||||
}
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("error", "登录失败"),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"登录异常: {str(e)}")
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"登录失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""健康检查"""
|
||||
return {"status": "ok", "message": "小红书登录服务运行中"}
|
||||
|
||||
@app.post("/api/xhs/inject-cookies", response_model=BaseResponse)
|
||||
async def inject_cookies(request: InjectCookiesRequest):
|
||||
"""
|
||||
注入Cookies并验证登录状态
|
||||
允许使用之前保存的Cookies跳过登录
|
||||
"""
|
||||
try:
|
||||
# 关闭旧的浏览器(如果有)
|
||||
if login_service.browser:
|
||||
await login_service.close_browser()
|
||||
|
||||
# 使用Cookies初始化浏览器
|
||||
await login_service.init_browser(cookies=request.cookies)
|
||||
|
||||
# 验证登录状态
|
||||
result = await login_service.verify_login_status()
|
||||
|
||||
if result.get("logged_in"):
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="Cookie注入成功,已登录",
|
||||
data={
|
||||
"logged_in": True,
|
||||
"user_info": result.get("user_info"),
|
||||
"cookies": result.get("cookies"), # 键值对格式
|
||||
"cookies_full": result.get("cookies_full"), # Playwright完整格式
|
||||
"url": result.get("url")
|
||||
}
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("message", "Cookie已失效,请重新登录"),
|
||||
data={
|
||||
"logged_in": False
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"注入Cookies异常: {str(e)}")
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"注入失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/publish", response_model=BaseResponse)
|
||||
async def publish_note(request: PublishNoteRequest):
|
||||
"""
|
||||
发布笔记
|
||||
登录后可以发布图文笔记到小红书
|
||||
"""
|
||||
try:
|
||||
# 调用登录服务发布笔记
|
||||
result = await login_service.publish_note(
|
||||
title=request.title,
|
||||
content=request.content,
|
||||
images=request.images,
|
||||
topics=request.topics
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return BaseResponse(
|
||||
code=0,
|
||||
message="笔记发布成功",
|
||||
data={
|
||||
"url": result.get("url"),
|
||||
"publish_time": datetime.now().isoformat()
|
||||
}
|
||||
)
|
||||
else:
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=result.get("error", "发布失败"),
|
||||
data=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"发布笔记异常: {str(e)}")
|
||||
return BaseResponse(
|
||||
code=1,
|
||||
message=f"发布失败: {str(e)}",
|
||||
data=None
|
||||
)
|
||||
|
||||
@app.post("/api/xhs/upload-images")
|
||||
async def upload_images(files: List[UploadFile] = File(...)):
|
||||
"""
|
||||
上传图片到服务器临时目录
|
||||
返回图片的服务器路径
|
||||
"""
|
||||
try:
|
||||
uploaded_paths = []
|
||||
|
||||
for file in files:
|
||||
# 检查文件类型
|
||||
if not file.content_type.startswith('image/'):
|
||||
return {
|
||||
"code": 1,
|
||||
"message": f"文件 {file.filename} 不是图片类型",
|
||||
"data": None
|
||||
}
|
||||
|
||||
# 生成唯一文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
file_ext = os.path.splitext(file.filename)[1]
|
||||
safe_filename = f"{timestamp}{file_ext}"
|
||||
file_path = TEMP_DIR / safe_filename
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# 使用绝对路径
|
||||
abs_path = str(file_path.absolute())
|
||||
uploaded_paths.append(abs_path)
|
||||
print(f"已上传图片: {abs_path}")
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"message": f"成功上传 {len(uploaded_paths)} 张图片",
|
||||
"data": {
|
||||
"paths": uploaded_paths,
|
||||
"count": len(uploaded_paths)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"上传图片异常: {str(e)}")
|
||||
return {
|
||||
"code": 1,
|
||||
"message": f"上传失败: {str(e)}",
|
||||
"data": None
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user