Files
ai_wht_wechat/go_backend/service/employee_service.go

2745 lines
86 KiB
Go
Raw Normal View History

2025-12-19 22:36:48 +08:00
package service
import (
2026-01-06 19:36:42 +08:00
"ai_xhs/config"
2025-12-19 22:36:48 +08:00
"ai_xhs/database"
"ai_xhs/models"
2026-01-06 19:36:42 +08:00
"ai_xhs/utils"
2025-12-19 22:36:48 +08:00
"bytes"
2026-01-06 19:36:42 +08:00
"context"
2025-12-19 22:36:48 +08:00
"encoding/json"
"errors"
"fmt"
2026-01-06 19:36:42 +08:00
"io"
2025-12-19 22:36:48 +08:00
"log"
2026-01-06 19:36:42 +08:00
"net/http"
"os"
2025-12-19 22:36:48 +08:00
"os/exec"
"path/filepath"
"strings"
2026-01-06 19:36:42 +08:00
"sync"
2025-12-19 22:36:48 +08:00
"time"
"gorm.io/gorm"
)
type EmployeeService struct{}
type XHSCookieVerifyResult struct {
LoggedIn bool
CookieExpired bool
}
2026-01-06 19:36:42 +08:00
// SendXHSCode 发送小红书验证码调用Python HTTP服务增加限流控制
2026-01-09 23:27:52 +08:00
func (s *EmployeeService) SendXHSCode(phone string, employeeID int) (map[string]interface{}, error) {
2026-01-06 19:36:42 +08:00
ctx := context.Background()
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 预检查:验证该手机号是否已被其他用户绑定
var conflictAuthor models.Author
err := database.DB.Where(
"xhs_phone = ? AND status = 'active' AND created_user_id != ?",
phone, employeeID,
).First(&conflictAuthor).Error
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
if err == nil {
// 找到了其他用户的绑定记录
log.Printf("发送验证码 - 用户%d - 失败: 手机号%s已被用户%d绑定",
employeeID, phone, conflictAuthor.CreatedUserID)
2026-01-09 23:27:52 +08:00
return nil, errors.New("该手机号已被其他用户绑定")
2026-01-06 19:36:42 +08:00
} else if err != gorm.ErrRecordNotFound {
// 数据库查询异常
log.Printf("发送验证码 - 用户%d - 检查手机号失败: %v", employeeID, err)
2026-01-09 23:27:52 +08:00
return nil, fmt.Errorf("检查手机号失败: %w", err)
2026-01-06 19:36:42 +08:00
}
// err == gorm.ErrRecordNotFound 表示该手机号未被绑定,可以继续
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 1. 限流检查: 1分钟内同一手机号只能发送一次
rateLimitKey := fmt.Sprintf("rate:sms:%s", phone)
exists, err := utils.ExistsCache(ctx, rateLimitKey)
if err == nil && exists {
2026-01-09 23:27:52 +08:00
return nil, errors.New("验证码发送过于频繁,请稍后再试")
2026-01-06 19:36:42 +08:00
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
pythonServiceURL = "http://localhost:8000"
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
// 从Dingzhi构造HTTP请求
url := fmt.Sprintf("%s/api/xhs/send-code", pythonServiceURL)
requestData := map[string]string{
"phone": phone,
"country_code": "+86",
2025-12-19 22:36:48 +08:00
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
jsonData, err := json.Marshal(requestData)
2025-12-19 22:36:48 +08:00
if err != nil {
2026-01-06 19:36:42 +08:00
log.Printf("[发送验证码] 序列化请求数据失败: %v", err)
2026-01-09 23:27:52 +08:00
return nil, errors.New("网络错误,请稍后重试")
2025-12-19 22:36:48 +08:00
}
2026-01-07 22:55:12 +08:00
log.Printf("[发送验证码] 调用Python HTTP服务: %s, 请求参数: phone=%s", url, phone)
startTime := time.Now()
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 发送HTTP POST请求增加超时控制60秒
client := &http.Client{
Timeout: 60 * time.Second, // 设置60秒超时
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("[发送验证码] 创建请求失败: %v", err)
2026-01-09 23:27:52 +08:00
return nil, errors.New("网络错误,请稍后重试")
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
req.Header.Set("Content-Type", "application/json")
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
resp, err := client.Do(req)
if err != nil {
log.Printf("[发送验证码] 调用Python服务失败: %v", err)
// 判断是否是超时错误
if strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "deadline exceeded") {
2026-01-09 23:27:52 +08:00
return nil, errors.New("请求超时,请稍后重试")
2025-12-19 22:36:48 +08:00
}
2026-01-09 23:27:52 +08:00
return nil, errors.New("网络错误,请稍后重试")
2026-01-06 19:36:42 +08:00
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("[发送验证码] 读取响应失败: %v", err)
2026-01-09 23:27:52 +08:00
return nil, errors.New("网络错误,请稍后重试")
2026-01-06 19:36:42 +08:00
}
2026-01-07 22:55:12 +08:00
log.Printf("[发送验证码] Python服务响应: 状态码=%d, 耗时=%.2fs", resp.StatusCode, time.Since(startTime).Seconds())
2026-01-06 19:36:42 +08:00
// 解析响应FastAPI返回格式: {code, message, data}
var apiResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
if err := json.Unmarshal(body, &apiResponse); err != nil {
log.Printf("[发送验证码] 解析Python响应失败: %v, body: %s", err, string(body))
2026-01-09 23:27:52 +08:00
return nil, errors.New("网络错误,请稍后重试")
2026-01-06 19:36:42 +08:00
}
2026-01-09 23:27:52 +08:00
log.Printf("[Python响应] code=%d, message=%s, data=%v", apiResponse.Code, apiResponse.Message, apiResponse.Data)
2026-01-06 19:36:42 +08:00
// 检查响应codeFastAPI返回code=0为成功
if apiResponse.Code != 0 {
log.Printf("[发送验证码] 失败: %s", apiResponse.Message)
// 根据错误信息返回用户友好的提示
2026-01-09 23:27:52 +08:00
return nil, s.getFriendlyErrorMessage(apiResponse.Message)
2025-12-19 22:36:48 +08:00
}
2026-01-09 23:27:52 +08:00
// 返回完整的data包括need_captcha、qrcode_image、session_id
log.Printf("[发送验证码] 成功, 返回数据: %v", apiResponse.Data)
2026-01-06 19:36:42 +08:00
// 2. 发送成功后设置限流标记1分钟
if err := utils.SetCache(ctx, rateLimitKey, "1", 1*time.Minute); err != nil {
log.Printf("设置限流缓存失败: %v", err)
}
2026-01-09 23:27:52 +08:00
return apiResponse.Data, nil
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
// getFriendlyErrorMessage 将技术错误信息转换为用户友好提示
func (s *EmployeeService) getFriendlyErrorMessage(errMsg string) error {
// 小写化错误信息用于匹配
lowerMsg := strings.ToLower(errMsg)
// DOM相关错误
if strings.Contains(lowerMsg, "element is not attached") ||
strings.Contains(lowerMsg, "dom") ||
strings.Contains(lowerMsg, "element not found") {
return errors.New("页面加载异常,请稍后重试")
}
// 超时错误
if strings.Contains(lowerMsg, "timeout") || strings.Contains(lowerMsg, "超时") {
return errors.New("请求超时,请检查网络后重试")
}
// 网络错误
if strings.Contains(lowerMsg, "network") ||
strings.Contains(lowerMsg, "connection") ||
strings.Contains(lowerMsg, "网络") {
return errors.New("网络连接失败,请检查网络后重试")
}
// 手机号错误
if strings.Contains(lowerMsg, "phone") ||
strings.Contains(lowerMsg, "手机号") ||
strings.Contains(lowerMsg, "输入手机号") {
return errors.New("请检查手机号是否正确")
}
// 验证码发送频繁
if strings.Contains(lowerMsg, "too many") ||
strings.Contains(lowerMsg, "频繁") ||
strings.Contains(lowerMsg, "rate limit") {
return errors.New("验证码发送过于频繁,请稍后再试")
}
// 浏览器/页面错误
if strings.Contains(lowerMsg, "browser") ||
strings.Contains(lowerMsg, "page") ||
strings.Contains(lowerMsg, "浏览器") {
return errors.New("系统繁忙,请稍后重试")
}
// 如果是其他错误,检查是否已经是中文提示
if strings.ContainsAny(errMsg, "一二三四五六七八九十") {
// 已经是中文提示,直接返回
return errors.New(errMsg)
}
// 默认通用错误提示
return errors.New("发送失败,请稍后重试")
}
// GetProfile 获取员工个人信息(增加缓存支持)
2025-12-19 22:36:48 +08:00
func (s *EmployeeService) GetProfile(employeeID int) (*models.User, error) {
2026-01-06 19:36:42 +08:00
ctx := context.Background()
cacheKey := fmt.Sprintf("user:profile:%d", employeeID)
// 1. 尝试从缓存获取
var cachedUser models.User
if err := utils.GetCache(ctx, cacheKey, &cachedUser); err == nil {
log.Printf("命中缓存: 用户ID=%d", employeeID)
return &cachedUser, nil
}
// 2. 缓存未命中,从数据库查询
2025-12-19 22:36:48 +08:00
var employee models.User
2026-01-06 19:36:42 +08:00
err := database.DB.First(&employee, employeeID).Error
2025-12-19 22:36:48 +08:00
if err != nil {
return nil, err
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
// 手动查询企业信息(避免 GORM 关联查询问题)
if employee.EnterpriseID > 0 {
var enterprise models.Enterprise
if err := database.DB.Select("id", "name").First(&enterprise, employee.EnterpriseID).Error; err == nil {
employee.Enterprise = enterprise
2025-12-19 22:36:48 +08:00
}
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
// 3. 存入缓存30分钟
if err := utils.SetCache(ctx, cacheKey, employee, 30*time.Minute); err != nil {
log.Printf("设置缓存失败: %v", err)
}
// 注意: 小红书绑定信息现在存储在 ai_authors 表中
// 这里的 is_bound_xhs 字段仅作为快速判断标识
// 详细信息需要从 ai_authors 表查询
2025-12-19 22:36:48 +08:00
return &employee, nil
}
2026-01-06 19:36:42 +08:00
// UpdateProfile 更新个人资料(昵称、邮箱、头像)
func (s *EmployeeService) UpdateProfile(employeeID int, nickname, email, avatar *string) error {
2025-12-19 22:36:48 +08:00
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
2026-01-06 19:36:42 +08:00
return errors.New("用户不存在")
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
updates := make(map[string]interface{})
if nickname != nil {
updates["nickname"] = strings.TrimSpace(*nickname)
}
if email != nil {
updates["email"] = strings.TrimSpace(*email)
}
if avatar != nil {
updates["icon"] = strings.TrimSpace(*avatar)
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
if len(updates) == 0 {
return nil
}
if err := database.DB.Model(&models.User{}).
Where("id = ?", employeeID).
Updates(updates).Error; err != nil {
return err
}
// 更新后清除缓存
ctx := context.Background()
cacheKey := fmt.Sprintf("user:profile:%d", employeeID)
if err := utils.DelCache(ctx, cacheKey); err != nil {
log.Printf("清除缓存失败: %v", err)
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
return nil
}
// BindXHS 绑定小红书账号(异步处理,立即返回)
2026-01-07 22:55:12 +08:00
func (s *EmployeeService) BindXHS(employeeID int, xhsPhone, code, sessionID string) (string, error) {
2026-01-06 19:36:42 +08:00
if code == "" {
return "", errors.New("验证码不能为空")
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
ctx := context.Background()
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 检查是否有正在进行的绑定任务
bindStatusKey := fmt.Sprintf("xhs_bind_status:%d", employeeID)
statusValue, err := database.RDB.Get(ctx, bindStatusKey).Result()
if err == nil && (statusValue == "processing" || statusValue == `{"status":"processing"}`) {
return "", errors.New("正在处理绑定请求,请稍候")
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
// 设置绑定状态为processing180秒有效期
// 直接使用Redis Set存储纯字符串避免JSON序列化
if err := database.RDB.Set(ctx, bindStatusKey, "processing", 180*time.Second).Err(); err != nil {
log.Printf("设置绑定状态缓存失败: %v", err)
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
// 异步执行绑定流程
2026-01-07 22:55:12 +08:00
go s.asyncBindXHS(employeeID, xhsPhone, code, sessionID)
2026-01-06 19:36:42 +08:00
// 立即返回成功,告知前端正在处理
2026-01-07 22:55:12 +08:00
log.Printf("绑定小红书 - 用户%d - 异步任务已启动 (session_id=%s)", employeeID, sessionID)
2026-01-06 19:36:42 +08:00
return "", nil
}
// asyncBindXHS 异步执行小红书绑定流程
2026-01-07 22:55:12 +08:00
func (s *EmployeeService) asyncBindXHS(employeeID int, xhsPhone, code, sessionID string) {
2026-01-06 19:36:42 +08:00
ctx := context.Background()
cacheService := NewCacheService()
// 使用分布式锁保护绑定操作
lockResource := fmt.Sprintf("bind_xhs:%d", employeeID)
xhsNickname := ""
bindStatusKey := fmt.Sprintf("xhs_bind_status:%d", employeeID)
err := cacheService.WithLock(ctx, lockResource, 180*time.Second, func() error {
// 获取员工信息
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return err
}
// 关键检查:验证该手机号是否已被其他用户绑定
var conflictAuthor models.Author
err := database.DB.Where(
"xhs_phone = ? AND status = 'active' AND created_user_id != ?",
xhsPhone, employeeID,
).First(&conflictAuthor).Error
2025-12-19 22:36:48 +08:00
if err == nil {
2026-01-06 19:36:42 +08:00
// 找到了其他用户的绑定记录
log.Printf("绑定小红书 - 用户%d - 失败: 手机号%s已被用户%d绑定",
employeeID, xhsPhone, conflictAuthor.CreatedUserID)
return errors.New("该手机号已被其他用户绑定")
} else if err != gorm.ErrRecordNotFound {
// 数据库查询异常
log.Printf("绑定小红书 - 用户%d - 检查手机号失败: %v", employeeID, err)
return fmt.Errorf("检查手机号失败: %w", err)
}
// err == gorm.ErrRecordNotFound 表示该手机号未被绑定,可以继续
2026-01-07 22:55:12 +08:00
// 调用Python服务进行验证码验证和登录传递session_id
loginResult, err := s.callPythonLogin(xhsPhone, code, sessionID)
2026-01-06 19:36:42 +08:00
if err != nil {
return fmt.Errorf("小红书登录失败: %w", err)
}
2026-01-07 12:18:55 +08:00
// 检柦Python服务返回结果
2026-01-06 19:36:42 +08:00
if loginResult.Code != 0 {
2026-01-07 12:18:55 +08:00
// 检查是否需要扫码验证
if needCaptcha, ok := loginResult.Data["need_captcha"].(bool); ok && needCaptcha {
// 出现验证码,更新绑定状态为"need_captcha"
captchaData := map[string]interface{}{
"status": "need_captcha",
"captcha_type": loginResult.Data["captcha_type"],
"message": loginResult.Data["message"],
}
// 如果有二维码图片,也一并返回
if qrcodeImage, ok := loginResult.Data["qrcode_image"].(string); ok {
captchaData["qrcode_image"] = qrcodeImage
}
// 将captchaData序列化并存入Redis
captchaJSON, _ := json.Marshal(captchaData)
if err := database.RDB.Set(ctx, bindStatusKey, string(captchaJSON), 180*time.Second).Err(); err != nil {
log.Printf("绑定小红书 - 用户%d - 更新验证状态失败: %v", employeeID, err)
}
log.Printf("绑定小红书 - 用户%d - 需要验证码验证: %s", employeeID, loginResult.Data["captcha_type"])
return fmt.Errorf("需要验证码验证")
}
2026-01-06 19:36:42 +08:00
return fmt.Errorf("小红书登录失败: %s", loginResult.Message)
}
// 从返回结果中提取用户信息和完整登录状态
userInfo, _ := loginResult.Data["user_info"].(map[string]interface{})
// 优先使用 login_state完整登录状态如果没有则降级使用cookies
var loginStateJSON string
if loginState, ok := loginResult.Data["login_state"].(map[string]interface{}); ok && 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)
}
2025-12-19 22:36:48 +08:00
} else {
2026-01-06 19:36:42 +08:00
// 降级:使用旧版本的 cookies_full 或 cookies
log.Printf("绑定小红书 - 用户%d - 警告: 未找到login_state降级使用cookies", employeeID)
var cookiesData interface{}
if cookiesFull, ok := loginResult.Data["cookies_full"].([]interface{}); ok && len(cookiesFull) > 0 {
cookiesData = cookiesFull
} else if cookiesMap, ok := loginResult.Data["cookies"].(map[string]interface{}); ok && len(cookiesMap) > 0 {
cookiesData = cookiesMap
}
if cookiesData != nil {
cookiesBytes, err := json.Marshal(cookiesData)
if err == nil {
loginStateJSON = string(cookiesBytes)
log.Printf("绑定小红书 - 用户%d - Cookie长度: %d", employeeID, len(loginStateJSON))
}
}
2025-12-19 22:36:48 +08:00
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
if loginStateJSON == "" {
log.Printf("绑定小红书 - 用户%d - 错误: 未能获取到任何登录数据", employeeID)
return errors.New("登录成功但未能获取到登录数据,请重试")
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 提取小红书账号昵称
xhsNickname = "小红书用户"
if userInfo != nil {
if nickname, ok := userInfo["nickname"].(string); ok && nickname != "" {
xhsNickname = nickname
} else if username, ok := userInfo["username"].(string); ok && username != "" {
xhsNickname = username
}
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
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, // 存储完整的login_state JSON
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 {
// 更新现有记录,使用 WHERE 条件明确指定要更新的记录(根据 created_user_id
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, // 存储完整的login_state JSON
"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 {
2025-12-19 22:36:48 +08:00
tx.Rollback()
2026-01-06 19:36:42 +08:00
log.Printf("绑定小红书 - 用户%d - 更新用户绑定标识失败: %v", employeeID, err)
return fmt.Errorf("更新用户绑定标识失败: %w", err)
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
log.Printf("绑定小红书 - 用户%d - 数据库更新成功", employeeID)
// 提交事务
if err := tx.Commit().Error; err != nil {
log.Printf("绑定小红书 - 用户%d - 事务提交失败: %v", employeeID, err)
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
})
2025-12-19 22:36:48 +08:00
if err != nil {
2026-01-06 19:36:42 +08:00
// 绑定失败设置失败状态保留5分钟供前端查询
failData := map[string]string{
"status": "failed",
"error": err.Error(),
}
failJSON, _ := json.Marshal(failData)
// 直接使用Redis Set存储JSON字符串
database.RDB.Set(ctx, bindStatusKey, string(failJSON), 5*time.Minute)
log.Printf("绑定小红书 - 用户%d - 绑定失败: %v", employeeID, err)
return
2025-12-19 22:36:48 +08:00
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
// 清除相关缓存
if err := cacheService.ClearUserRelatedCache(ctx, employeeID); err != nil {
log.Printf("清除缓存失败: %v", err)
2025-12-19 22:36:48 +08:00
}
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
// 绑定成功设置成功状态保留5分钟供前端查询
successData := map[string]string{
"status": "success",
"xhs_account": xhsNickname,
}
successJSON, _ := json.Marshal(successData)
// 直接使用Redis Set存储JSON字符串
database.RDB.Set(ctx, bindStatusKey, string(successJSON), 5*time.Minute)
2025-12-19 22:36:48 +08:00
log.Printf("绑定小红书 - 用户%d - 绑定成功 - 账号: %s", employeeID, xhsNickname)
}
2026-01-06 19:36:42 +08:00
// GetBindXHSStatus 获取小红书绑定状态
func (s *EmployeeService) GetBindXHSStatus(employeeID int) (map[string]interface{}, error) {
ctx := context.Background()
bindStatusKey := fmt.Sprintf("xhs_bind_status:%d", employeeID)
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
statusJSON, err := database.RDB.Get(ctx, bindStatusKey).Result()
if err != nil {
// 没有找到状态,可能已完成或从未开始
log.Printf("获取绑定状态 - 用户%d - Redis查询失败: %v", employeeID, err)
return map[string]interface{}{
"status": "idle",
}, nil
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
log.Printf("获取绑定状态 - 用户%d - Redis原始数据: %s", employeeID, statusJSON)
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 处理中状态(纯字符串)
if statusJSON == "processing" {
log.Printf("获取绑定状态 - 用户%d - 状态: processing", employeeID)
return map[string]interface{}{
"status": "processing",
"message": "正在登录小红书,请稍候...",
}, nil
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 尝试解析JSON状态
2026-01-07 12:18:55 +08:00
var statusData map[string]interface{}
2026-01-06 19:36:42 +08:00
if err := json.Unmarshal([]byte(statusJSON), &statusData); err != nil {
log.Printf("获取绑定状态 - 用户%d - JSON解析失败: %v, 原始数据: %s", employeeID, err, statusJSON)
// 如果不是JSON可能是纯字符串状态
return map[string]interface{}{
"status": "unknown",
"error": fmt.Sprintf("解析状态失败: %s", statusJSON),
}, nil
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
log.Printf("获取绑定状态 - 用户%d - 解析后的状态: %+v", employeeID, statusData)
result := map[string]interface{}{
"status": statusData["status"],
}
2026-01-07 12:18:55 +08:00
if status, ok := statusData["status"].(string); ok {
switch status {
case "success":
if xhsAccount, ok := statusData["xhs_account"].(string); ok {
result["xhs_account"] = xhsAccount
}
result["message"] = "绑定成功"
log.Printf("获取绑定状态 - 用户%d - 绑定成功", employeeID)
case "failed":
if errorMsg, ok := statusData["error"].(string); ok {
result["error"] = errorMsg
}
log.Printf("获取绑定状态 - 用户%d - 绑定失败", employeeID)
case "processing":
result["message"] = "正在登录小红书,请稍候..."
log.Printf("获取绑定状态 - 用户%d - 状态: processing", employeeID)
case "need_captcha":
// 需要验证码验证
if message, ok := statusData["message"].(string); ok {
result["message"] = message
}
if captchaType, ok := statusData["captcha_type"].(string); ok {
result["captcha_type"] = captchaType
}
// 如果有二维码图片,也返回
if qrcodeImage, ok := statusData["qrcode_image"].(string); ok {
result["qrcode_image"] = qrcodeImage
}
log.Printf("获取绑定状态 - 用户%d - 需要验证码: %s", employeeID, statusData["captcha_type"])
}
2026-01-06 19:36:42 +08:00
}
return result, nil
}
// callPythonLogin 调用Python HTTP服务完成小红书登录优化使用浏览器池
2026-01-07 22:55:12 +08:00
func (s *EmployeeService) callPythonLogin(phone, code, sessionID string) (*PythonLoginResponse, error) {
2026-01-06 19:36:42 +08:00
// 从配置获取Python服务地址
pythonServiceURL := config.AppConfig.XHS.PythonServiceURL
if pythonServiceURL == "" {
pythonServiceURL = "http://localhost:8000"
}
// 构造HTTP请求
url := fmt.Sprintf("%s/api/xhs/login", pythonServiceURL)
requestData := map[string]string{
"phone": phone,
"code": code,
"country_code": "+86",
2026-01-07 22:55:12 +08:00
"session_id": sessionID, // 关键传递session_id用于复用浏览器
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
jsonData, err := json.Marshal(requestData)
2025-12-19 22:36:48 +08:00
if err != nil {
2026-01-06 19:36:42 +08:00
return nil, fmt.Errorf("序列化请求数据失败: %w", err)
2025-12-19 22:36:48 +08:00
}
2026-01-07 22:55:12 +08:00
log.Printf("[绑定小红书] 调用Python HTTP服务: %s, session_id=%s", url, sessionID)
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 发送HTTP POST请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("HTTP请求失败: %w", err)
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
defer resp.Body.Close()
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
log.Printf("[绑定小红书] Python服务响应状态: %d", resp.StatusCode)
// 解析响应FastAPI返回格式: {code, message, data}
var apiResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
if err := json.Unmarshal(body, &apiResponse); err != nil {
return nil, fmt.Errorf("解析Python响应失败: %w, body: %s", err, string(body))
}
// 检查响应codeFastAPI返回code=0为成功
if apiResponse.Code != 0 {
2025-12-19 22:36:48 +08:00
return &PythonLoginResponse{
Code: 1,
2026-01-06 19:36:42 +08:00
Message: apiResponse.Message,
2025-12-19 22:36:48 +08:00
}, nil
}
2026-01-06 19:36:42 +08:00
log.Printf("[绑定小红书] 登录成功获取到Cookie数据")
2025-12-19 22:36:48 +08:00
return &PythonLoginResponse{
Code: 0,
Message: "登录成功",
2026-01-06 19:36:42 +08:00
Data: apiResponse.Data,
2025-12-19 22:36:48 +08:00
}, nil
}
// PythonLoginResponse Python服务登录响应
type PythonLoginResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
// UnbindXHS 解绑小红书账号
func (s *EmployeeService) UnbindXHS(employeeID int) error {
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return err
}
if employee.IsBoundXHS == 0 {
return errors.New("未绑定小红书账号")
}
// 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
2026-01-06 19:36:42 +08:00
// 删除或禁用 ai_authors 表中的小红书作者记录(根据 created_user_id
err := tx.Model(&models.Author{}).Where(
"created_user_id = ? AND enterprise_id = ? AND channel = 1",
employeeID, employee.EnterpriseID,
).Updates(map[string]interface{}{
"status": "inactive",
"xhs_cookie": "",
"xhs_phone": "",
"xhs_account": "",
"bound_at": nil,
2025-12-19 22:36:48 +08:00
}).Error
if err != nil {
tx.Rollback()
2026-01-06 19:36:42 +08:00
return fmt.Errorf("删除作者记录失败: %w", err)
}
// 更新 ai_users 表的绑定标识
log.Printf("解绑小红书 - 用户%d - 开始更新用户绑定标识", employeeID)
if err := tx.Model(&employee).Update("is_bound_xhs", 0).Error; err != nil {
tx.Rollback()
log.Printf("解绑小红书 - 用户%d - 更新用户绑定标识失败: %v", employeeID, err)
return fmt.Errorf("更新用户绑定标识失败: %w", err)
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
log.Printf("解绑小红书 - 用户%d - 更新用户绑定标识成功", employeeID)
2025-12-19 22:36:48 +08:00
// 提交事务
if err := tx.Commit().Error; err != nil {
2026-01-06 19:36:42 +08:00
log.Printf("解绑小红书 - 用户%d - 事务提交失败: %v", employeeID, err)
2025-12-19 22:36:48 +08:00
return fmt.Errorf("提交事务失败: %w", err)
}
2026-01-06 19:36:42 +08:00
// 清除相关缓存
ctx := context.Background()
userCacheKey := fmt.Sprintf("user:profile:%d", employeeID)
authorCacheKey := fmt.Sprintf("author:user:%d", employeeID)
statusCacheKey := fmt.Sprintf("xhs:status:%d", employeeID)
if err := utils.DelCache(ctx, userCacheKey, authorCacheKey, statusCacheKey); err != nil {
log.Printf("清除缓存失败: %v", err)
}
log.Printf("解绑小红书 - 用户%d - 解绑成功", employeeID)
2025-12-19 22:36:48 +08:00
return nil
}
// verifyCookieWithPython 使用Python脚本验证Cookie并返回登录与过期状态
func (s *EmployeeService) verifyCookieWithPython(rawCookie string) (*XHSCookieVerifyResult, error) {
// 解析Cookie
var cookies []interface{}
if err := json.Unmarshal([]byte(rawCookie), &cookies); err != nil {
return nil, fmt.Errorf("解析Cookie失败: %w", err)
}
// 调用Python脚本验证Cookie
backendDir := filepath.Join("..", "backend")
pythonScript := filepath.Join(backendDir, "xhs_cli.py")
pythonCmd := getPythonPath(backendDir)
// 将cookies序列化为JSON字符串
cookiesJSON, err := json.Marshal(cookies)
if err != nil {
return nil, fmt.Errorf("序列化Cookie失败: %w", err)
}
// 执行Python脚本: inject_cookies
cmd := exec.Command(pythonCmd, pythonScript, "inject_cookies", string(cookiesJSON))
cmd.Dir = backendDir
// 捕获输出
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
// 执行命令
err = cmd.Run()
// 打印Python脚本的日志输出(stderr)
if stderr.Len() > 0 {
log.Printf("[Python日志-验证Cookie] %s", stderr.String())
}
if err != nil {
return nil, fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String())
}
// 解析返回结果
outputStr := stdout.String()
var result map[string]interface{}
if err := json.Unmarshal([]byte(outputStr), &result); err != nil {
return nil, fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr)
}
loggedIn, _ := result["logged_in"].(bool)
cookieExpired, _ := result["cookie_expired"].(bool)
return &XHSCookieVerifyResult{
LoggedIn: loggedIn,
CookieExpired: cookieExpired,
}, nil
}
// VerifyCookieAndClear 验证Cookie并在失效时清空
func (s *EmployeeService) VerifyCookieAndClear(employeeID int) error {
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return err
}
// 检查是否已绑定
2026-01-06 19:36:42 +08:00
if employee.IsBoundXHS == 0 {
return nil // 没有绑定,直接返回
}
// 查询对应的 author 记录(根据 created_user_id
var author models.Author
err := database.DB.Where(
"created_user_id = ? AND enterprise_id = ? AND channel = 1 AND status = 'active'",
employeeID, employee.EnterpriseID,
).First(&author).Error
if err != nil || author.XHSCookie == "" {
return nil // 没有找到有效的author记录或已无Cookie
2025-12-19 22:36:48 +08:00
}
2025-12-20 01:05:46 +08:00
2025-12-19 22:36:48 +08:00
// 检查绑定时间刚绑定的30秒内不验证避免与绑定操作冲突
2026-01-06 19:36:42 +08:00
if author.BoundAt != nil {
timeSinceBound := time.Since(*author.BoundAt)
2025-12-19 22:36:48 +08:00
if timeSinceBound < 30*time.Second {
log.Printf("验证Cookie - 用户%d刚绑定%.0f秒,跳过验证", employeeID, timeSinceBound.Seconds())
return nil
}
}
// 调用Python脚本验证Cookie
2026-01-06 19:36:42 +08:00
verifyResult, err := s.verifyCookieWithPython(author.XHSCookie)
2025-12-19 22:36:48 +08:00
if err != nil {
log.Printf("执行Python脚本失败: %v", err)
// 执行失败,不清空Cookie
return err
}
if !verifyResult.LoggedIn || verifyResult.CookieExpired {
// Cookie已失效,清空数据库
log.Printf("检测到Cookie已失效,清空用户%d的Cookie", employeeID)
return s.clearXHSCookie(employeeID)
}
log.Printf("用户%d的Cookie有效", employeeID)
return nil
}
// XHSStatus 小红书绑定及Cookie状态
type XHSStatus struct {
IsBound bool `json:"is_bound"`
HasCookie bool `json:"has_cookie"`
CookieValid bool `json:"cookie_valid"`
CookieExpired bool `json:"cookie_expired"`
Message string `json:"message"`
}
2026-01-06 19:36:42 +08:00
// CheckXHSStatus 检查小红书绑定与Cookie健康状态增加缓存
2025-12-19 22:36:48 +08:00
func (s *EmployeeService) CheckXHSStatus(employeeID int) (*XHSStatus, error) {
2026-01-06 19:36:42 +08:00
ctx := context.Background()
cacheKey := fmt.Sprintf("xhs:status:%d", employeeID)
// 1. 尝试从缓存获取状态5分钟有效期
var cachedStatus XHSStatus
if err := utils.GetCache(ctx, cacheKey, &cachedStatus); err == nil {
log.Printf("命中小红书状态缓存: 用户ID=%d", employeeID)
return &cachedStatus, nil
}
// 2. 缓存未命中,查询数据库并验证
2025-12-19 22:36:48 +08:00
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return nil, err
}
status := &XHSStatus{
IsBound: employee.IsBoundXHS == 1,
2026-01-06 19:36:42 +08:00
HasCookie: false,
2025-12-19 22:36:48 +08:00
CookieValid: false,
CookieExpired: false,
}
if employee.IsBoundXHS == 0 {
status.Message = "未绑定小红书账号"
2026-01-06 19:36:42 +08:00
// 缓存未绑定状态1分钟
utils.SetCache(ctx, cacheKey, status, 1*time.Minute)
2025-12-19 22:36:48 +08:00
return status, nil
}
2026-01-06 19:36:42 +08:00
// 查询对应的 author 记录(根据 created_user_id
var author models.Author
err := database.DB.Where(
"created_user_id = ? AND enterprise_id = ? AND channel = 1 AND status = 'active'",
employeeID, employee.EnterpriseID,
).First(&author).Error
if err != nil || author.XHSCookie == "" {
2025-12-19 22:36:48 +08:00
status.CookieExpired = true
status.Message = "已绑定但无有效Cookie可直接重新绑定"
2026-01-06 19:36:42 +08:00
// 缓存Cookie过期状态2分钟
utils.SetCache(ctx, cacheKey, status, 2*time.Minute)
2025-12-19 22:36:48 +08:00
return status, nil
}
2026-01-06 19:36:42 +08:00
status.HasCookie = true
2025-12-19 22:36:48 +08:00
// 刚绑定30秒内视为有效避免频繁触发验证
2026-01-06 19:36:42 +08:00
if author.BoundAt != nil {
timeSinceBound := time.Since(*author.BoundAt)
2025-12-19 22:36:48 +08:00
if timeSinceBound < 30*time.Second {
status.CookieValid = true
status.Message = "刚绑定小于30秒暂不检测视为有效"
2026-01-06 19:36:42 +08:00
// 缓存有效状态5分钟
utils.SetCache(ctx, cacheKey, status, 5*time.Minute)
2025-12-19 22:36:48 +08:00
return status, nil
}
}
2026-01-06 19:36:42 +08:00
verifyResult, err := s.verifyCookieWithPython(author.XHSCookie)
2025-12-19 22:36:48 +08:00
if err != nil {
status.Message = fmt.Sprintf("验证Cookie失败: %v", err)
return status, err
}
if !verifyResult.LoggedIn || verifyResult.CookieExpired {
// Cookie已失效清空后允许直接重新绑定
if err := s.clearXHSCookie(employeeID); err != nil {
return nil, err
}
status.HasCookie = false
status.CookieExpired = true
status.CookieValid = false
status.Message = "Cookie已失效已清空可直接重新绑定"
2026-01-06 19:36:42 +08:00
// 缓存Cookie失效状态2分钟
utils.SetCache(ctx, cacheKey, status, 2*time.Minute)
2025-12-19 22:36:48 +08:00
return status, nil
}
status.CookieValid = true
status.CookieExpired = false
status.Message = "Cookie有效已登录"
2026-01-06 19:36:42 +08:00
// 缓存Cookie有效状态5分钟
utils.SetCache(ctx, cacheKey, status, 5*time.Minute)
2025-12-19 22:36:48 +08:00
return status, nil
}
// clearXHSCookie 清空小红书Cookie(保留绑定状态)
func (s *EmployeeService) clearXHSCookie(employeeID int) error {
2026-01-06 19:36:42 +08:00
// 查询用户信息
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return err
}
// 清空 ai_authors 表中的 Cookie根据 created_user_id
err := database.DB.Model(&models.Author{}).Where(
"created_user_id = ? AND enterprise_id = ? AND channel = 1",
employeeID, employee.EnterpriseID,
).Update("xhs_cookie", "").Error
2025-12-19 22:36:48 +08:00
if err != nil {
return fmt.Errorf("清空Cookie失败: %w", err)
}
log.Printf("已清空用户%d的XHS Cookie", employeeID)
return nil
}
2025-12-20 01:05:46 +08:00
// GetAvailableCopies 获取可领取的文案列表根据作者ID、产品ID和状态筛选
2025-12-19 22:36:48 +08:00
func (s *EmployeeService) GetAvailableCopies(employeeID int, productID int) (map[string]interface{}, error) {
2026-01-06 19:36:42 +08:00
ctx := context.Background()
2025-12-20 01:05:46 +08:00
2026-01-06 19:36:42 +08:00
// 1. 先尝试从缓存获取作者信息
2025-12-20 01:05:46 +08:00
var author models.Author
2026-01-06 19:36:42 +08:00
authorCacheKey := fmt.Sprintf("author:user:%d", employeeID)
if err := utils.GetCache(ctx, authorCacheKey, &author); err != nil {
// 缓存未命中,从数据库查询
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 查找对应的作者记录(根据 created_user_id
if err := database.DB.Where("created_user_id = ? AND enterprise_id = ?", employeeID, employee.EnterpriseID).First(&author).Error; err != nil {
return nil, fmt.Errorf("未找到对应的作者记录: %w", err)
}
// 存入缓存1小时
if err := utils.SetCache(ctx, authorCacheKey, author, 1*time.Hour); err != nil {
log.Printf("设置作者缓存失败: %v", err)
}
} else {
log.Printf("命中作者缓存: 用户ID=%d, 作者ID=%d", employeeID, author.ID)
2025-12-20 01:05:46 +08:00
}
2025-12-19 22:36:48 +08:00
// 获取产品信息
var product models.Product
if err := database.DB.First(&product, productID).Error; err != nil {
return nil, err
}
2025-12-20 01:05:46 +08:00
// 根据产品ID、作者ID和状态筛选文案
// status = 'assign_authors' 表示已分配作者的文案
2025-12-19 22:36:48 +08:00
var copies []models.Article
2026-01-06 19:36:42 +08:00
query := database.DB.Preload("Images", func(db *gorm.DB) *gorm.DB {
return db.Order("sort_order ASC")
}).Where("product_id = ? AND author_id = ? AND status = ?", productID, author.ID, "assign_authors")
2025-12-20 01:05:46 +08:00
if err := query.Order("created_at DESC").Find(&copies).Error; err != nil {
return nil, fmt.Errorf("查询文案列表失败: %w", err)
2025-12-19 22:36:48 +08:00
}
2025-12-20 01:05:46 +08:00
log.Printf("[获取文案列表] 用户ID=%d, 作者ID=%d, 产品ID=%d, 筛选到 %d 条文案", employeeID, author.ID, productID, len(copies))
2025-12-19 22:36:48 +08:00
return map[string]interface{}{
"product": map[string]interface{}{
"id": product.ID,
"name": product.Name,
"image": product.ImageURL,
},
"copies": copies,
2025-12-20 01:05:46 +08:00
"author": map[string]interface{}{
"id": author.ID,
"name": author.AuthorName,
},
2025-12-19 22:36:48 +08:00
}, nil
}
2025-12-20 01:05:46 +08:00
// UpdateArticleStatus 更新文案状态(通过/拒绝)
func (s *EmployeeService) UpdateArticleStatus(employeeID int, articleID int, status string) error {
// 获取当前用户信息
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return fmt.Errorf("获取用户信息失败: %w", err)
}
2026-01-06 19:36:42 +08:00
// 查找对应的作者记录(根据 created_user_id
2025-12-20 01:05:46 +08:00
var author models.Author
2026-01-06 19:36:42 +08:00
if err := database.DB.Where("created_user_id = ? AND enterprise_id = ?", employeeID, employee.EnterpriseID).First(&author).Error; err != nil {
2025-12-20 01:05:46 +08:00
return fmt.Errorf("未找到对应的作者记录: %w", err)
}
// 获取文案信息
var article models.Article
if err := database.DB.First(&article, articleID).Error; err != nil {
return fmt.Errorf("获取文案信息失败: %w", err)
}
// 验证文案是否属于当前作者且状态为assign_authors
if article.AuthorID == nil || *article.AuthorID != author.ID {
return fmt.Errorf("无权操作此文案")
}
if article.Status != "assign_authors" {
return fmt.Errorf("文案当前状态为%s无法操作", article.Status)
}
// 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
now := time.Now()
// 更新文案状态
err := tx.Model(&article).Updates(map[string]interface{}{
"status": status,
"review_user_id": employeeID,
}).Error
if err != nil {
tx.Rollback()
return fmt.Errorf("更新状态失败: %w", err)
}
// 创建操作记录到 ai_article_published_records
actionType := "通过"
if status == "rejected" {
actionType = "拒绝"
}
record := models.PublishRecord{
ArticleID: &article.ID,
EnterpriseID: employee.EnterpriseID,
ProductID: article.ProductID,
Topic: article.Topic,
Title: article.Title,
CreatedUserID: article.CreatedUserID,
ReviewUserID: &employeeID,
Status: status,
PublishTime: &now,
WordCount: article.WordCount,
ImageCount: article.ImageCount,
Channel: article.Channel,
ReviewComment: fmt.Sprintf("作者%s于%s", actionType, now.Format("2006-01-02 15:04:05")),
}
if err := tx.Create(&record).Error; err != nil {
tx.Rollback()
return fmt.Errorf("创建操作记录失败: %w", err)
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
log.Printf("[更新文案状态] 用户ID=%d, 作者ID=%d, 文案ID=%d, 状态: assign_authors => %s, 记录ID=%d", employeeID, author.ID, articleID, status, record.ID)
return nil
}
2026-01-06 19:36:42 +08:00
// UpdateArticleContent 更新文案内容(标题、正文)
func (s *EmployeeService) UpdateArticleContent(employeeID int, articleID int, title, content string) error {
var article models.Article
if err := database.DB.First(&article, articleID).Error; err != nil {
return errors.New("文案不存在")
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
// 更新标题和内容
updates := map[string]interface{}{
"title": title,
"content": content,
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
err := database.DB.Model(&article).Updates(updates).Error
if err != nil {
return err
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 记录日志
s.createLog(nil, employeeID, "article_content_update", "article", &article.ID,
fmt.Sprintf("文案ID:%d 内容已更新", article.ID), "", "success")
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
return nil
}
// AddArticleImage 添加文案图片
func (s *EmployeeService) AddArticleImage(employeeID int, articleID int, imageURL, imageThumbURL, keywordsName string) (*models.ArticleImage, error) {
// 验证文案是否存在
var article models.Article
if err := database.DB.First(&article, articleID).Error; err != nil {
return nil, errors.New("文案不存在")
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
// 获取当前最大的 sort_order
var maxSortOrder int
database.DB.Model(&models.ArticleImage{}).Where("article_id = ?", articleID).Select("COALESCE(MAX(sort_order), 0)").Scan(&maxSortOrder)
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 获取当前最大的 image_id
var maxImageID int
database.DB.Model(&models.ArticleImage{}).Select("COALESCE(MAX(image_id), 0)").Scan(&maxImageID)
// 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 创建图片记录
image := models.ArticleImage{
EnterpriseID: article.EnterpriseID,
ArticleID: articleID,
ImageID: maxImageID + 1,
ImageURL: imageURL,
ImageThumbURL: imageThumbURL,
SortOrder: maxSortOrder + 1,
KeywordsName: keywordsName,
ImageSource: 2, // 2=change 表示用户手动添加
}
if err := tx.Create(&image).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("添加图片失败: %w", err)
}
// 更新文案的图片数量
var imageCount int64
tx.Model(&models.ArticleImage{}).Where("article_id = ?", articleID).Count(&imageCount)
tx.Model(&article).Update("image_count", imageCount)
// 记录日志
s.createLog(tx, employeeID, "article_image_add", "article", &articleID,
fmt.Sprintf("文案ID:%d 添加图片: %s", articleID, imageURL), "", "success")
// 提交事务
if err := tx.Commit().Error; err != nil {
return nil, fmt.Errorf("提交事务失败: %w", err)
}
return &image, nil
}
// DeleteArticleImage 删除文案图片
func (s *EmployeeService) DeleteArticleImage(employeeID int, imageID int) error {
// 查询图片信息
var image models.ArticleImage
if err := database.DB.First(&image, imageID).Error; err != nil {
return errors.New("图片不存在")
}
articleID := image.ArticleID
// 验证文案是否存在
var article models.Article
if err := database.DB.First(&article, articleID).Error; err != nil {
return errors.New("文案不存在")
}
// 检查删除后是否至少还有一张图片
var imageCount int64
database.DB.Model(&models.ArticleImage{}).Where("article_id = ?", articleID).Count(&imageCount)
if imageCount <= 1 {
return errors.New("文案至少需要保留一张图片")
}
// 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 删除图片
if err := tx.Delete(&image).Error; err != nil {
tx.Rollback()
return fmt.Errorf("删除图片失败: %w", err)
}
// 更新文案的图片数量
tx.Model(&models.ArticleImage{}).Where("article_id = ?", articleID).Count(&imageCount)
tx.Model(&article).Update("image_count", imageCount)
// 记录日志
s.createLog(tx, employeeID, "article_image_delete", "article", &articleID,
fmt.Sprintf("文案ID:%d 删除图片ID:%d", articleID, imageID), "", "success")
// 提交事务
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}
// UpdateArticleImagesOrder 更新文案图片排序
func (s *EmployeeService) UpdateArticleImagesOrder(employeeID int, articleID int, imageOrders []map[string]int) error {
// 验证文案是否存在
var article models.Article
if err := database.DB.First(&article, articleID).Error; err != nil {
return errors.New("文案不存在")
}
// 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 更新每个图片的排序
for _, order := range imageOrders {
imageID, imageIDOk := order["id"]
sortOrder, sortOrderOk := order["sort_order"]
if !imageIDOk || !sortOrderOk {
continue
}
if err := tx.Model(&models.ArticleImage{}).Where("id = ? AND article_id = ?", imageID, articleID).Update("sort_order", sortOrder).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新图片排序失败: %w", err)
}
}
// 记录日志
s.createLog(tx, employeeID, "article_images_reorder", "article", &articleID,
fmt.Sprintf("文案ID:%d 更新图片排序", articleID), "", "success")
// 提交事务
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}
// ClaimCopy 领取文案(新版本:直接返回文案信息,不再创建领取记录)
func (s *EmployeeService) ClaimCopy(employeeID int, copyID int, productID int) (map[string]interface{}, error) {
// 检查文案是否存在且可用注意新数据库中status有更多状态
// assign_authors: 已分配给作者,可以直接发布
// draft: 草稿状态
// approved: 已审核通过
var copy models.Article
if err := database.DB.Where("id = ? AND status IN ?", copyID, []string{"draft", "approved", "assign_authors"}).First(&copy).Error; err != nil {
return nil, errors.New("文案不存在或不可用")
}
// 获取关联的图片如果有ai_article_images表
var images []string
// TODO: 从 ai_article_images 表获取图片
return map[string]interface{}{
"copy": map[string]interface{}{
"id": copy.ID,
"title": copy.Title,
"content": copy.Content,
"images": images,
},
}, nil
}
// ClaimRandomCopy 随机领取文案
func (s *EmployeeService) ClaimRandomCopy(employeeID int, productID int) (map[string]interface{}, error) {
// 查询未领取的可用文案注意新数据库中status有更多状态
var copy models.Article
query := database.DB.Where("product_id = ? AND status IN ?", productID, []string{"draft", "approved", "assign_authors"})
if err := query.Order("RAND()").First(&copy).Error; err != nil {
return nil, errors.New("暂无可领取的文案")
}
// 领取该文案
return s.ClaimCopy(employeeID, copy.ID, productID)
}
// Publish 发布内容
func (s *EmployeeService) Publish(employeeID int, req PublishRequest) (int, error) {
// 检查文案是否存在
var copy models.Article
2025-12-19 22:36:48 +08:00
if err := database.DB.First(&copy, req.CopyID).Error; err != nil {
return 0, errors.New("文案不存在")
}
// 检查文案是否已被发布
if copy.Status == "published" || copy.Status == "published_review" {
return 0, errors.New("文案已被发布或处于发布审核中")
}
2026-01-06 19:36:42 +08:00
// 验证标题不为空
if req.Title == "" {
return 0, errors.New("标题不能为空")
}
trimmedTitle := strings.TrimSpace(req.Title)
if trimmedTitle == "" {
return 0, errors.New("标题不能为空")
}
// 验证内容不为空从article表中获取
if copy.Content == "" || strings.TrimSpace(copy.Content) == "" {
return 0, errors.New("文案内容不能为空")
}
// 验证文案至少有一张图片
var imageCount int64
database.DB.Model(&models.ArticleImage{}).Where("article_id = ?", req.CopyID).Count(&imageCount)
if imageCount < 1 {
return 0, errors.New("请至少添加一张图片")
}
2025-12-19 22:36:48 +08:00
// 获取员工信息
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return 0, err
}
// 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
now := time.Now()
var recordID int
var publishStatus string = "published_review" // 默认为发布审核中
var errMessage string
// 1. 更新文案状态为 published_review
if err := tx.Model(&models.Article{}).Where("id = ?", req.CopyID).Updates(map[string]interface{}{
"status": publishStatus,
"publish_user_id": employeeID,
"publish_time": now,
}).Error; err != nil {
publishStatus = "failed"
errMessage = "更新文案状态失败: " + err.Error()
// 记录失败日志
s.createLog(tx, employeeID, "article_publish_update_failed", "article", &copy.ID,
"发布文案-更新状态失败", errMessage, "error")
tx.Rollback()
return 0, errors.New(errMessage)
}
// 记录更新文案状态日志
s.createLog(tx, employeeID, "article_status_update", "article", &copy.ID,
fmt.Sprintf("文案ID:%d 状态更新为 %s", copy.ID, publishStatus), "", "success")
// 2. 创建发布记录
record := models.PublishRecord{
ArticleID: &copy.ID,
EnterpriseID: employee.EnterpriseID,
ProductID: copy.ProductID,
Topic: copy.Topic,
Title: req.Title,
CreatedUserID: employeeID,
PublishUserID: &employeeID,
Status: publishStatus,
PublishTime: &now,
PublishLink: req.PublishLink,
WordCount: copy.WordCount,
ImageCount: copy.ImageCount,
Channel: copy.Channel,
}
if err := tx.Create(&record).Error; err != nil {
publishStatus = "failed"
errMessage = "创建发布记录失败: " + err.Error()
// 记录失败日志
s.createLog(tx, employeeID, "publish_record_create_failed", "publish_record", nil,
"创建发布记录失败", errMessage, "error")
// 回滚文案状态为failed
tx.Model(&models.Article{}).Where("id = ?", req.CopyID).Update("status", "failed")
s.createLog(tx, employeeID, "article_status_rollback", "article", &copy.ID,
fmt.Sprintf("文案ID:%d 状态回滚为 failed", copy.ID), errMessage, "warning")
tx.Rollback()
return 0, errors.New(errMessage)
}
recordID = record.ID
// 记录创建发布记录日志
s.createLog(tx, employeeID, "publish_record_create", "publish_record", &recordID,
fmt.Sprintf("创建发布记录ID:%d, 文案ID:%d, 状态:%s", recordID, copy.ID, publishStatus), "", "success")
// 提交事务
if err := tx.Commit().Error; err != nil {
publishStatus = "failed"
errMessage = "提交事务失败: " + err.Error()
// 事务提交失败需要在新事务中更新状态为failed
database.DB.Model(&models.Article{}).Where("id = ?", req.CopyID).Update("status", "failed")
s.createLog(nil, employeeID, "publish_transaction_failed", "article", &copy.ID,
"发布事务提交失败状态更新为failed", errMessage, "error")
return 0, errors.New(errMessage)
}
// 成功日志
s.createLog(nil, employeeID, "article_publish_success", "article", &copy.ID,
fmt.Sprintf("文案ID:%d 发布成功记录ID:%d", copy.ID, recordID), "", "success")
return recordID, nil
}
// createLog 创建日志记录
func (s *EmployeeService) createLog(tx *gorm.DB, userID int, action, targetType string, targetID *int, description, errMsg, status string) {
2026-01-06 19:36:42 +08:00
// 如果没有请求和响应数据设置为空JSON对象
requestData := "{}"
responseData := "{}"
2025-12-19 22:36:48 +08:00
log := models.Log{
UserID: &userID,
Action: action,
TargetType: targetType,
TargetID: targetID,
Description: description,
2026-01-06 19:36:42 +08:00
RequestData: requestData,
ResponseData: responseData,
2025-12-19 22:36:48 +08:00
Status: status,
ErrorMessage: errMsg,
}
db := database.DB
if tx != nil {
db = tx
}
if err := db.Create(&log).Error; err != nil {
// 日志创建失败不影响主流程,只输出错误
fmt.Printf("创建日志失败: %v\n", err)
}
}
2026-01-06 19:36:42 +08:00
// GetMyPublishRecords 获取我的发布记录(优化版:批量预加载)
2025-12-19 22:36:48 +08:00
func (s *EmployeeService) GetMyPublishRecords(employeeID int, page, pageSize int) (map[string]interface{}, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
var total int64
var records []models.PublishRecord
// 查询总数使用publish_user_id字段
database.DB.Model(&models.PublishRecord{}).Where("publish_user_id = ?", employeeID).Count(&total)
// 查询列表不使用Preload直接使用冗余字段
offset := (page - 1) * pageSize
err := database.DB.Where("publish_user_id = ?", employeeID).
Order("publish_time DESC").
Limit(pageSize).
Offset(offset).
Find(&records).Error
if err != nil {
return nil, err
}
2026-01-06 19:36:42 +08:00
// 批量收集所有article_id和product_id
articleIDs := make([]int, 0)
productIDs := make(map[int]bool)
for _, record := range records {
if record.ArticleID != nil && *record.ArticleID > 0 {
articleIDs = append(articleIDs, *record.ArticleID)
}
if record.ProductID > 0 {
productIDs[record.ProductID] = true
}
}
// 批量查询产品
productMap := make(map[int]string)
if len(productIDs) > 0 {
productIDList := make([]int, 0, len(productIDs))
for pid := range productIDs {
productIDList = append(productIDList, pid)
}
var products []models.Product
if err := database.DB.Where("id IN ?", productIDList).Select("id, name").Find(&products).Error; err == nil {
for _, p := range products {
productMap[p.ID] = p.Name
}
}
}
// 批量查询文章图片
imagesMap := make(map[int][]map[string]interface{})
if len(articleIDs) > 0 {
var articleImages []models.ArticleImage
if err := database.DB.Where("article_id IN ?", articleIDs).Order("article_id ASC, sort_order ASC").Find(&articleImages).Error; err == nil {
for _, img := range articleImages {
if _, ok := imagesMap[img.ArticleID]; !ok {
imagesMap[img.ArticleID] = make([]map[string]interface{}, 0)
}
imagesMap[img.ArticleID] = append(imagesMap[img.ArticleID], map[string]interface{}{
"id": img.ID,
"image_url": img.ImageURL,
"image_thumb_url": img.ImageThumbURL,
"sort_order": img.SortOrder,
"keywords_name": img.KeywordsName,
})
}
}
}
// 批量查询文章标签
tagsMap := make(map[int][]string)
if len(articleIDs) > 0 {
var articleTags []models.ArticleTag
if err := database.DB.Where("article_id IN ?", articleIDs).Select("article_id, coze_tag").Find(&articleTags).Error; err == nil {
for _, tag := range articleTags {
if tag.CozeTag != "" {
for _, t := range splitTags(tag.CozeTag) {
if t != "" {
tagsMap[tag.ArticleID] = append(tagsMap[tag.ArticleID], t)
}
}
}
}
}
}
2025-12-19 22:36:48 +08:00
// 构造返回数据
2026-01-06 19:36:42 +08:00
list := make([]map[string]interface{}, 0, len(records))
2025-12-19 22:36:48 +08:00
for _, record := range records {
publishTimeStr := ""
if record.PublishTime != nil {
publishTimeStr = record.PublishTime.Format("2006-01-02 15:04:05")
}
2026-01-06 19:36:42 +08:00
// 从批量查询结果中获取数据
productName := productMap[record.ProductID]
2025-12-19 22:36:48 +08:00
var images []map[string]interface{}
var tags []string
if record.ArticleID != nil && *record.ArticleID > 0 {
2026-01-06 19:36:42 +08:00
images = imagesMap[*record.ArticleID]
tags = tagsMap[*record.ArticleID]
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
// 确保返回空数组而不null
if images == nil {
images = make([]map[string]interface{}, 0)
}
if tags == nil {
tags = make([]string, 0)
2025-12-19 22:36:48 +08:00
}
list = append(list, map[string]interface{}{
"id": record.ID,
"product_id": record.ProductID,
"product_name": productName,
"topic": record.Topic,
"title": record.Title,
"publish_link": record.PublishLink,
"publish_time": publishTimeStr,
"images": images,
"tags": tags,
})
}
return map[string]interface{}{
"total": total,
"list": list,
}, nil
}
2026-01-06 19:36:42 +08:00
// GetPublishRecordDetail 获取发布记录详情(优化版)
2025-12-19 22:36:48 +08:00
func (s *EmployeeService) GetPublishRecordDetail(employeeID int, recordID int) (map[string]interface{}, error) {
var record models.PublishRecord
err := database.DB.Where("id = ?", recordID).First(&record).Error
if err != nil {
return nil, errors.New("发布记录不存在")
}
publishTimeStr := ""
if record.PublishTime != nil {
publishTimeStr = record.PublishTime.Format("2006-01-02 15:04:05")
}
// 通过ArticleID关联查询文章内容
content := ""
var images []map[string]interface{}
var tags []string
articleCozeTag := ""
// 查询产品名称
productName := ""
2026-01-06 19:36:42 +08:00
if record.ProductID > 0 {
var product models.Product
// 优化:只查询需要的字段
if err := database.DB.Select("name").Where("id = ?", record.ProductID).First(&product).Error; err == nil {
productName = product.Name
}
2025-12-19 22:36:48 +08:00
}
if record.ArticleID != nil && *record.ArticleID > 0 {
2026-01-06 19:36:42 +08:00
// 优化:使用单次查询获取文章基本信息
var article models.Article
if err := database.DB.Select("id, content, coze_tag").Where("id = ?", *record.ArticleID).First(&article).Error; err == nil {
2025-12-19 22:36:48 +08:00
content = article.Content
articleCozeTag = article.CozeTag
2026-01-06 19:36:42 +08:00
// 并行查询图片和标签(使用 goroutine
var wg sync.WaitGroup
wg.Add(2)
// 查询图片
go func() {
defer wg.Done()
var articleImages []models.ArticleImage
if err := database.DB.Select("id, image_url, image_thumb_url, sort_order, keywords_name").
Where("article_id = ?", article.ID).
Order("sort_order ASC").
Find(&articleImages).Error; err == nil {
for _, img := range articleImages {
images = append(images, map[string]interface{}{
"id": img.ID,
"image_url": img.ImageURL,
"image_thumb_url": img.ImageThumbURL,
"sort_order": img.SortOrder,
"keywords_name": img.KeywordsName,
})
2025-12-19 22:36:48 +08:00
}
}
2026-01-06 19:36:42 +08:00
}()
// 查询标签
go func() {
defer wg.Done()
var articleTag models.ArticleTag
if err := database.DB.Select("coze_tag").Where("article_id = ?", article.ID).First(&articleTag).Error; err == nil && articleTag.CozeTag != "" {
// 使用ai_article_tags表的标签
articleCozeTag = articleTag.CozeTag
}
}()
wg.Wait()
2025-12-19 22:36:48 +08:00
// 解析标签
if articleCozeTag != "" {
for _, tag := range splitTags(articleCozeTag) {
if tag != "" {
tags = append(tags, tag)
}
}
}
}
}
2026-01-06 19:36:42 +08:00
// 确保返回空数组而不null
if images == nil {
images = make([]map[string]interface{}, 0)
}
if tags == nil {
tags = make([]string, 0)
}
2025-12-19 22:36:48 +08:00
return map[string]interface{}{
"id": record.ID,
"article_id": record.ArticleID,
"product_id": record.ProductID,
"product_name": productName,
"topic": record.Topic,
"title": record.Title,
"content": content,
"images": images,
"tags": tags,
"coze_tag": articleCozeTag,
"publish_link": record.PublishLink,
"status": record.Status,
"publish_time": publishTimeStr,
}, nil
}
// splitTags 分割标签字符串
func splitTags(tagStr string) []string {
if tagStr == "" {
return []string{}
}
// 尝试多种分隔符
var tags []string
// 先尝试逗号分割
if strings.Contains(tagStr, ",") {
for _, tag := range strings.Split(tagStr, ",") {
tag = strings.TrimSpace(tag)
if tag != "" {
tags = append(tags, tag)
}
}
} else if strings.Contains(tagStr, "") {
// 中文逗号
for _, tag := range strings.Split(tagStr, "") {
tag = strings.TrimSpace(tag)
if tag != "" {
tags = append(tags, tag)
}
}
} else if strings.Contains(tagStr, "|") {
// 竪线分隔
for _, tag := range strings.Split(tagStr, "|") {
tag = strings.TrimSpace(tag)
if tag != "" {
tags = append(tags, tag)
}
}
} else {
// 单个标签
tags = append(tags, strings.TrimSpace(tagStr))
}
return tags
}
2026-01-06 19:36:42 +08:00
// GetProducts 获取产品列表(按企业和状态筛选,支持分页)
func (s *EmployeeService) GetProducts(employeeID int, page, pageSize int) ([]map[string]interface{}, bool, error) {
// 参数校验
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
ctx := context.Background()
// 获取当前用户信息,确定所属企业
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return nil, false, fmt.Errorf("获取用户信息失败: %w", err)
}
// 1. 尝试从缓存获取产品列表10分钟缓存
cacheKey := fmt.Sprintf("products:enterprise:%d:page:%d:size:%d", employee.EnterpriseID, page, pageSize)
type CachedProducts struct {
Products []map[string]interface{} `json:"products"`
HasMore bool `json:"has_more"`
}
var cached CachedProducts
if err := utils.GetCache(ctx, cacheKey, &cached); err == nil {
log.Printf("命中产品列表缓存: 企业ID=%d, page=%d", employee.EnterpriseID, page)
return cached.Products, cached.HasMore, nil
}
// 2. 缓存未命中,从数据库查询
// 按企业和状态筛选产品按ID排序
offset := (page - 1) * pageSize
2025-12-19 22:36:48 +08:00
var products []models.Product
2026-01-06 19:36:42 +08:00
query := database.DB.Where("enterprise_id = ? AND status = ?", employee.EnterpriseID, "active")
if err := query.Order("id ASC").Offset(offset).Limit(pageSize + 1).Find(&products).Error; err != nil {
return nil, false, err
2025-12-19 22:36:48 +08:00
}
2026-01-06 19:36:42 +08:00
// 判断是否还有更多数据
hasMore := false
if len(products) > pageSize {
hasMore = true
products = products[:pageSize]
}
2025-12-19 22:36:48 +08:00
2026-01-06 19:36:42 +08:00
result := make([]map[string]interface{}, 0, len(products))
for _, product := range products {
2025-12-19 22:36:48 +08:00
result = append(result, map[string]interface{}{
2026-01-06 19:36:42 +08:00
"id": product.ID,
"name": product.Name,
"image": product.ImageURL,
"knowledge": product.Knowledge,
2025-12-19 22:36:48 +08:00
})
}
2026-01-06 19:36:42 +08:00
// 3. 存入缓存10分钟
cachedData := CachedProducts{
Products: result,
HasMore: hasMore,
}
if err := utils.SetCache(ctx, cacheKey, cachedData, 10*time.Minute); err != nil {
log.Printf("设置产品列表缓存失败: %v", err)
}
return result, hasMore, nil
2025-12-19 22:36:48 +08:00
}
// PublishRequest 发布请求参数
type PublishRequest struct {
CopyID int `json:"copy_id" binding:"required"`
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
PublishLink string `json:"publish_link"`
XHSNoteID string `json:"xhs_note_id"`
}
2026-01-06 19:36:42 +08:00
// UpdatePublishRecordRequest 更新发布记录请求参数
type UpdatePublishRecordRequest struct {
Title *string `json:"title"` // 标题(可选)
Content *string `json:"content"` // 内容(可选)
Images []UpdateImageRequest `json:"images"` // 图片列表(可选)
Tags []string `json:"tags"` // 标签列表(可选)
}
// UpdateImageRequest 更新图片请求参数
type UpdateImageRequest struct {
ImageURL string `json:"image_url" binding:"required"` // 图片URL
ImageThumbURL string `json:"image_thumb_url"` // 缩略图URL可选
SortOrder int `json:"sort_order"` // 排序
KeywordsName string `json:"keywords_name"` // 关键词名称(可选)
}
// UpdatePublishRecord 编辑发布记录(修改标题、内容、图片、标签)
func (s *EmployeeService) UpdatePublishRecord(employeeID int, recordID int, req UpdatePublishRecordRequest) error {
// 1. 查询发布记录
var record models.PublishRecord
if err := database.DB.First(&record, recordID).Error; err != nil {
return fmt.Errorf("发布记录不存在: %w", err)
}
// 2. 权限检查:只有创建者或管理员可以编辑
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return err
}
if record.CreatedUserID != employeeID && employee.Role != "admin" {
return errors.New("无权编辑此发布记录")
}
// 3. 开启事务
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 4. 更新发布记录基本信息
updates := make(map[string]interface{})
if req.Title != nil {
updates["title"] = *req.Title
}
// 如果有更新字段,执行更新
if len(updates) > 0 {
if err := tx.Model(&record).Updates(updates).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新发布记录失败: %w", err)
}
}
// 5. 更新关联的文章内容如果有article_id
if record.ArticleID != nil && *record.ArticleID > 0 {
articleUpdates := make(map[string]interface{})
if req.Title != nil {
articleUpdates["title"] = *req.Title
}
if req.Content != nil {
articleUpdates["content"] = *req.Content
}
if len(articleUpdates) > 0 {
if err := tx.Model(&models.Article{}).Where("id = ?", *record.ArticleID).Updates(articleUpdates).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新文章内容失败: %w", err)
}
}
// 6. 更新图片(如果提供)
if req.Images != nil && len(req.Images) > 0 {
// 先删除旧图片
if err := tx.Where("article_id = ?", *record.ArticleID).Delete(&models.ArticleImage{}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("删除旧图片失败: %w", err)
}
// 插入新图片
for _, img := range req.Images {
articleImage := models.ArticleImage{
EnterpriseID: record.EnterpriseID,
ArticleID: *record.ArticleID,
ImageURL: img.ImageURL,
ImageThumbURL: img.ImageThumbURL,
SortOrder: img.SortOrder,
KeywordsName: img.KeywordsName,
ImageSource: 2, // 2=手动修改
}
if err := tx.Create(&articleImage).Error; err != nil {
tx.Rollback()
return fmt.Errorf("创建新图片失败: %w", err)
}
}
// 更新文章的图片数量
if err := tx.Model(&models.Article{}).Where("id = ?", *record.ArticleID).Update("image_count", len(req.Images)).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新图片数量失败: %w", err)
}
}
// 7. 更新标签(如果提供)
if req.Tags != nil && len(req.Tags) > 0 {
tagsStr := strings.Join(req.Tags, ",")
// 更新或创建 ai_article_tags 记录
var articleTag models.ArticleTag
err := tx.Where("article_id = ?", *record.ArticleID).First(&articleTag).Error
if err == gorm.ErrRecordNotFound {
// 创建新标签记录
articleTag = models.ArticleTag{
EnterpriseID: record.EnterpriseID,
ArticleID: *record.ArticleID,
CozeTag: tagsStr,
}
if err := tx.Create(&articleTag).Error; err != nil {
tx.Rollback()
return fmt.Errorf("创建标签失败: %w", err)
}
} else if err == nil {
// 更新已有标签
if err := tx.Model(&articleTag).Update("coze_tag", tagsStr).Error; err != nil {
tx.Rollback()
return fmt.Errorf("更新标签失败: %w", err)
}
} else {
tx.Rollback()
return fmt.Errorf("查询标签失败: %w", err)
}
}
}
// 8. 记录操作日志
s.createLog(tx, employeeID, "publish_record_update", "publish_record", &recordID,
fmt.Sprintf("编辑发布记录ID:%d", recordID), "", "success")
// 9. 提交事务
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}
return nil
}
// RepublishRecord 重新发布种草内容到小红书
func (s *EmployeeService) RepublishRecord(employeeID int, recordID int) (string, error) {
// 1. 查询发布记录
var record models.PublishRecord
if err := database.DB.First(&record, recordID).Error; err != nil {
return "", fmt.Errorf("发布记录不存在: %w", err)
}
// 2. 权限检查
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return "", err
}
if record.CreatedUserID != employeeID && employee.Role != "admin" {
return "", errors.New("无权重新发布此内容")
}
// 3. 检查用户是否绑定小红书
if employee.IsBoundXHS != 1 {
return "", errors.New("请先绑定小红书账号")
}
// 4. 查询对应的 author 记录获取Cookie
var author models.Author
if err := database.DB.Where(
"phone = ? AND enterprise_id = ? AND channel = 1 AND status = 'active'",
employee.Phone, employee.EnterpriseID,
).First(&author).Error; err != nil {
return "", fmt.Errorf("未找到有效的小红书作者记录: %w", err)
}
if author.XHSCookie == "" {
return "", errors.New("小红书Cookie已失效请重新绑定")
}
// 5. 获取文章内容
var content string
var images []string
var tags []string
if record.ArticleID != nil && *record.ArticleID > 0 {
// 从关联的文章获取内容
var article models.Article
if err := database.DB.First(&article, *record.ArticleID).Error; err == nil {
content = article.Content
// 获取图片
var articleImages []models.ArticleImage
if err := database.DB.Where("article_id = ?", article.ID).Order("sort_order ASC").Find(&articleImages).Error; err == nil {
for _, img := range articleImages {
if img.ImageURL != "" {
images = append(images, img.ImageURL)
}
}
}
// 获取标签
var articleTag models.ArticleTag
if err := database.DB.Where("article_id = ?", article.ID).First(&articleTag).Error; err == nil {
if articleTag.CozeTag != "" {
tags = splitTags(articleTag.CozeTag)
}
}
}
}
if content == "" {
return "", errors.New("发布记录无关联内容")
}
// 6. 解析Cookie
var cookies interface{}
if err := json.Unmarshal([]byte(author.XHSCookie), &cookies); err != nil {
return "", fmt.Errorf("解析Cookie失败: %w", err)
}
// 7. 构造发布配置
publishConfig := map[string]interface{}{
"cookies": cookies,
"title": record.Title,
"content": content,
"images": images,
"tags": tags,
}
// 8. 调用Python脚本发布
backendDir := filepath.Join("..", "backend")
pythonScript := filepath.Join(backendDir, "xhs_publish.py")
pythonCmd := getPythonPath(backendDir)
// 将配置写入临时文件
configFile := filepath.Join(backendDir, fmt.Sprintf("publish_config_temp_%d_%d.json", employeeID, recordID))
configData, err := json.MarshalIndent(publishConfig, "", " ")
if err != nil {
return "", fmt.Errorf("序列化配置失败: %w", err)
}
if err := os.WriteFile(configFile, configData, 0644); err != nil {
return "", fmt.Errorf("写入配置文件失败: %w", err)
}
defer os.Remove(configFile)
// 执行Python脚本
cmd := exec.Command(pythonCmd, pythonScript, "--config", configFile)
cmd.Dir = backendDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if stderr.Len() > 0 {
log.Printf("[Python日志-重新发布%d] %s", recordID, stderr.String())
}
if err != nil {
return "", fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String())
}
// 9. 解析发布结果
outputStr := stdout.String()
lines := strings.Split(strings.TrimSpace(outputStr), "\n")
var result map[string]interface{}
found := false
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
if strings.HasPrefix(line, "{") {
if err := json.Unmarshal([]byte(line), &result); err == nil {
found = true
break
}
}
}
if !found {
return "", fmt.Errorf("Python脚本未返回有效JSON结果, output: %s", outputStr)
}
success, ok := result["success"].(bool)
if !ok || !success {
errMsg := "未知错误"
if errStr, ok := result["error"].(string); ok {
errMsg = errStr
}
return "", fmt.Errorf("重新发布失败: %s", errMsg)
}
// 10. 提取发布链接
publishLink := ""
if note, ok := result["note"].(map[string]interface{}); ok {
if noteID, ok := note["note_id"].(string); ok {
publishLink = fmt.Sprintf("https://www.xiaohongshu.com/explore/%s", noteID)
}
}
// 11. 更新发布记录的链接和时间
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
now := time.Now()
if err := tx.Model(&record).Updates(map[string]interface{}{
"publish_link": publishLink,
"publish_time": now,
"status": "published",
}).Error; err != nil {
tx.Rollback()
return "", fmt.Errorf("更新发布记录失败: %w", err)
}
// 记录日志
s.createLog(tx, employeeID, "publish_record_republish", "publish_record", &recordID,
fmt.Sprintf("重新发布记录ID:%d, 链接:%s", recordID, publishLink), "", "success")
if err := tx.Commit().Error; err != nil {
return "", fmt.Errorf("提交事务失败: %w", err)
}
return publishLink, nil
}
2026-01-07 22:55:12 +08:00
// 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
}
2026-01-10 21:46:50 +08:00
// 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)
}
2026-01-23 16:27:47 +08:00
// 优先使用 cookies_full如果没有则降级使用 storage_state
2026-01-10 21:46:50 +08:00
var loginStateJSON string
2026-01-23 16:27:47 +08:00
if len(cookiesFull) > 0 {
// 优先:直接使用 cookies_fullAdsPower Cookie
2026-01-10 21:46:50 +08:00
cookiesBytes, err := json.Marshal(cookiesFull)
if err == nil {
loginStateJSON = string(cookiesBytes)
2026-01-23 16:27:47 +08:00
log.Printf("验证码登录 - 用户%d - 使用cookies_fullCookie长度: %d", employeeID, len(loginStateJSON))
} else {
log.Printf("验证码登录 - 用户%d - 序列化cookies_full失败: %v", employeeID, err)
}
} else if len(storageState) > 0 {
// 降级:从 storage_state 中提取 cookies
log.Printf("验证码登录 - 用户%d - 警告: 未找到cookies_full从storage_state提取cookies", employeeID)
if cookies, ok := storageState["cookies"].([]interface{}); ok && len(cookies) > 0 {
cookiesBytes, err := json.Marshal(cookies)
if err == nil {
loginStateJSON = string(cookiesBytes)
log.Printf("验证码登录 - 用户%d - 从storage_state提取的Cookie长度: %d", employeeID, len(loginStateJSON))
}
} else {
// 最终降级:保存整个 storage_state
log.Printf("验证码登录 - 用户%d - 无法提取cookies保存整个storage_state", employeeID)
storageStateBytes, err := json.Marshal(storageState)
if err == nil {
loginStateJSON = string(storageStateBytes)
log.Printf("验证码登录 - 用户%d - StorageState长度: %d", employeeID, len(loginStateJSON))
}
2026-01-10 21:46:50 +08:00
}
}
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
}
2026-01-07 22:55:12 +08:00
// 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
}