This commit is contained in:
sjk
2026-01-07 22:55:12 +08:00
parent cb267e8d5e
commit 4720ab2a15
76 changed files with 3110 additions and 7168 deletions

View File

@@ -79,7 +79,8 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
return errors.New("网络错误,请稍后重试")
}
log.Printf("[发送验证码] 调用Python HTTP服务: %s", url)
log.Printf("[发送验证码] 调用Python HTTP服务: %s, 请求参数: phone=%s", url, phone)
startTime := time.Now()
// 发送HTTP POST请求增加超时控制60秒
client := &http.Client{
@@ -111,7 +112,7 @@ func (s *EmployeeService) SendXHSCode(phone string, employeeID int) error {
return errors.New("网络错误,请稍后重试")
}
log.Printf("[发送验证码] Python服务响应状态: %d", resp.StatusCode)
log.Printf("[发送验证码] Python服务响应: 状态码=%d, 耗时=%.2fs", resp.StatusCode, time.Since(startTime).Seconds())
// 解析响应FastAPI返回格式: {code, message, data}
var apiResponse struct {
@@ -276,7 +277,7 @@ func (s *EmployeeService) UpdateProfile(employeeID int, nickname, email, avatar
}
// BindXHS 绑定小红书账号(异步处理,立即返回)
func (s *EmployeeService) BindXHS(employeeID int, xhsPhone, code string) (string, error) {
func (s *EmployeeService) BindXHS(employeeID int, xhsPhone, code, sessionID string) (string, error) {
if code == "" {
return "", errors.New("验证码不能为空")
}
@@ -297,15 +298,15 @@ func (s *EmployeeService) BindXHS(employeeID int, xhsPhone, code string) (string
}
// 异步执行绑定流程
go s.asyncBindXHS(employeeID, xhsPhone, code)
go s.asyncBindXHS(employeeID, xhsPhone, code, sessionID)
// 立即返回成功,告知前端正在处理
log.Printf("绑定小红书 - 用户%d - 异步任务已启动", employeeID)
log.Printf("绑定小红书 - 用户%d - 异步任务已启动 (session_id=%s)", employeeID, sessionID)
return "", nil
}
// asyncBindXHS 异步执行小红书绑定流程
func (s *EmployeeService) asyncBindXHS(employeeID int, xhsPhone, code string) {
func (s *EmployeeService) asyncBindXHS(employeeID int, xhsPhone, code, sessionID string) {
ctx := context.Background()
cacheService := NewCacheService()
@@ -340,8 +341,8 @@ func (s *EmployeeService) asyncBindXHS(employeeID int, xhsPhone, code string) {
}
// err == gorm.ErrRecordNotFound 表示该手机号未被绑定,可以继续
// 调用Python服务进行验证码验证和登录
loginResult, err := s.callPythonLogin(xhsPhone, code)
// 调用Python服务进行验证码验证和登录传递session_id
loginResult, err := s.callPythonLogin(xhsPhone, code, sessionID)
if err != nil {
return fmt.Errorf("小红书登录失败: %w", err)
}
@@ -608,7 +609,7 @@ func (s *EmployeeService) GetBindXHSStatus(employeeID int) (map[string]interface
}
// callPythonLogin 调用Python HTTP服务完成小红书登录优化使用浏览器池
func (s *EmployeeService) callPythonLogin(phone, code string) (*PythonLoginResponse, error) {
func (s *EmployeeService) callPythonLogin(phone, code, sessionID string) (*PythonLoginResponse, error) {
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
@@ -621,6 +622,7 @@ func (s *EmployeeService) callPythonLogin(phone, code string) (*PythonLoginRespo
"phone": phone,
"code": code,
"country_code": "+86",
"session_id": sessionID, // 关键传递session_id用于复用浏览器
}
jsonData, err := json.Marshal(requestData)
@@ -628,7 +630,7 @@ func (s *EmployeeService) callPythonLogin(phone, code string) (*PythonLoginRespo
return nil, fmt.Errorf("序列化请求数据失败: %w", err)
}
log.Printf("[绑定小红书] 调用Python HTTP服务: %s", url)
log.Printf("[绑定小红书] 调用Python HTTP服务: %s, session_id=%s", url, sessionID)
// 发送HTTP POST请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
@@ -2198,3 +2200,388 @@ func (s *EmployeeService) RepublishRecord(employeeID int, recordID int) (string,
return publishLink, nil
}
// SaveQRCodeLogin 保存扫码登录的绑定信息
// 复用BindXHS的保存逻辑但不需要调用Python后端直接保存数据
func (s *EmployeeService) SaveQRCodeLogin(employeeID int, cookiesFull []interface{}, userInfo map[string]interface{}, loginState 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)
}
// 优先使用 login_state完整登录状态如果没有则降级使用cookies
var loginStateJSON string
if len(loginState) > 0 {
// 新版使用完整的login_state包含cookies + localStorage + sessionStorage
loginStateBytes, err := json.Marshal(loginState)
if err == nil {
loginStateJSON = string(loginStateBytes)
log.Printf("扫码登录 - 用户%d - 完整LoginState长度: %d", employeeID, len(loginStateJSON))
} else {
log.Printf("扫码登录 - 用户%d - 序列化login_state失败: %v", employeeID, err)
}
} else if len(cookiesFull) > 0 {
// 降级:使用旧版本的 cookies_full
log.Printf("扫码登录 - 用户%d - 警告: 未找到login_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("登录成功但未能获取到登录数据,请重试")
}
// 提取小红书账号昵称
xhsNickname := "小红书用户"
xhsPhone := "" // 扫码登录没有手机号
if nickname, ok := userInfo["nickname"].(string); ok && nickname != "" {
xhsNickname = nickname
} else if username, ok := userInfo["username"].(string); ok && username != "" {
xhsNickname = username
}
// 尝试从 userInfo 提取 red_id 作为 phone
if redID, ok := userInfo["red_id"].(string); ok && redID != "" {
xhsPhone = redID
}
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 - 绑定成功 - 账号: %s", employeeID, xhsNickname)
return nil
}
// StartQRCodeLogin 启动扫码登录转发到Python服务
func (s *EmployeeService) StartQRCodeLogin(employeeID int) (map[string]interface{}, error) {
log.Printf("[启动扫码登录] 用户ID: %d", employeeID)
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
pythonServiceURL = "http://localhost:8000"
}
url := fmt.Sprintf("%s/api/xhs/qrcode/start", pythonServiceURL)
log.Printf("[启动扫码登录] 调用Python服务: %s", url)
// 发送HTTP POST请求启动扫码需要启动浏览器+加载页面+获取二维码设置90秒超时
client := &http.Client{
Timeout: 90 * time.Second,
}
req, err := http.NewRequest("POST", url, nil)
if err != nil {
log.Printf("[启动扫码登录] 创建请求失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("[启动扫码登录] 调用Python服务失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[启动扫码登录] 读取响应失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
// 解析响应,直接返回完整响应体
var apiResponse map[string]interface{}
if err := json.Unmarshal(body, &apiResponse); err != nil {
log.Printf("[启动扫码登录] 解析响应失败: %v, body: %s", err, string(body))
return nil, errors.New("网络错误,请稍后重试")
}
// 检查Python响应的code字段
if code, ok := apiResponse["code"].(float64); ok && code != 0 {
if msg, ok := apiResponse["message"].(string); ok {
log.Printf("[启动扫码登录] 失败: %s", msg)
return nil, errors.New(msg)
}
return nil, errors.New("启动失败")
}
log.Printf("[启动扫码登录] 成功")
// 返回完整的Python响应保持code=0格式
return apiResponse, nil
}
// GetQRCodeStatus 获取扫码状态转发到Python服务
func (s *EmployeeService) GetQRCodeStatus(employeeID int, sessionID string) (map[string]interface{}, error) {
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
pythonServiceURL = "http://localhost:8000"
}
url := fmt.Sprintf("%s/api/xhs/qrcode/status", pythonServiceURL)
requestData := map[string]string{
"session_id": sessionID,
}
jsonData, err := json.Marshal(requestData)
if err != nil {
log.Printf("[扫码状态] 序列化请求数据失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
// 发送HTTP POST请求
client := &http.Client{
Timeout: 30 * time.Second,
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("[扫码状态] 创建请求失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("[扫码状态] 调用Python服务失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[扫码状态] 读取响应失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
// 解析响应,直接返回完整响应体
var apiResponse map[string]interface{}
if err := json.Unmarshal(body, &apiResponse); err != nil {
log.Printf("[扫码状态] 解析响应失败: %v, body: %s", err, string(body))
return nil, errors.New("网络错误,请稍后重试")
}
// 扫码状态接口可能返回 code=2 表示 session 失效
// 这种情况不算错误,直接返回给前端处理
// 直接返回完整的Python响应让前端自己判断
return apiResponse, nil
}
// RefreshQRCode 刷新二维码转发到Python服务
func (s *EmployeeService) RefreshQRCode(employeeID int, sessionID string) (map[string]interface{}, error) {
log.Printf("[刷新二维码] 用户ID: %d, SessionID: %s", employeeID, sessionID)
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
pythonServiceURL = "http://localhost:8000"
}
url := fmt.Sprintf("%s/api/xhs/qrcode/refresh", pythonServiceURL)
requestData := map[string]string{
"session_id": sessionID,
}
jsonData, err := json.Marshal(requestData)
if err != nil {
log.Printf("[刷新二维码] 序列化请求数据失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
// 发送HTTP POST请求刷新二维码需要重新加载页面设置60秒超时
client := &http.Client{
Timeout: 60 * time.Second,
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("[刷新二维码] 创建请求失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("[刷新二维码] 调用Python服务失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[刷新二维码] 读取响应失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
// 解析响应,直接返回完整响应体
var apiResponse map[string]interface{}
if err := json.Unmarshal(body, &apiResponse); err != nil {
log.Printf("[刷新二维码] 解析响应失败: %v, body: %s", err, string(body))
return nil, errors.New("网络错误,请稍后重试")
}
// 刷新接口可能返回 code=3 表示需要重启
// 这种情况不算错误,直接返回给前端处理
// 直接返回完整的Python响应让前端自己判断
log.Printf("[刷新二维码] 成功")
return apiResponse, nil
}
// CancelQRCodeLogin 取消扫码登录转发到Python服务
func (s *EmployeeService) CancelQRCodeLogin(sessionID string) (map[string]interface{}, error) {
log.Printf("[取消扫码] SessionID: %s", sessionID)
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
pythonServiceURL = "http://localhost:8000"
}
url := fmt.Sprintf("%s/api/xhs/qrcode/cancel", pythonServiceURL)
requestData := map[string]string{
"session_id": sessionID,
}
jsonData, err := json.Marshal(requestData)
if err != nil {
log.Printf("[取消扫码] 序列化请求数据失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
// 发送HTTP POST请求
client := &http.Client{
Timeout: 10 * time.Second, // 短超时,取消操作应该很快
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("[取消扫码] 创建请求失败: %v", err)
return nil, errors.New("网络错误,请稍后重试")
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("[取消扫码] 调用Python服务失败: %v", err)
// 取消失败也返回成功,不影响用户体验
return map[string]interface{}{
"code": 0,
"message": "已取消扫码登录",
}, nil
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[取消扫码] 读取响应失败: %v", err)
return map[string]interface{}{
"code": 0,
"message": "已取消扫码登录",
}, nil
}
// 解析响应,直接返回完整响应体
var apiResponse map[string]interface{}
if err := json.Unmarshal(body, &apiResponse); err != nil {
log.Printf("[取消扫码] 解析响应失败: %v, body: %s", err, string(body))
return map[string]interface{}{
"code": 0,
"message": "已取消扫码登录",
}, nil
}
log.Printf("[取消扫码] 成功")
return apiResponse, nil
}