package service import ( "ai_xhs/database" "ai_xhs/models" "bytes" "encoding/json" "errors" "fmt" "log" "os/exec" "path/filepath" "strings" "time" "gorm.io/gorm" ) type EmployeeService struct{} type XHSCookieVerifyResult struct { LoggedIn bool CookieExpired bool } // SendXHSCode 发送小红书验证码 func (s *EmployeeService) SendXHSCode(phone string) error { // 获取Python脚本路径和venv中的Python解释器 backendDir := filepath.Join("..", "backend") pythonScript := filepath.Join(backendDir, "xhs_cli.py") // 使用venv中的Python解释器 (跨平台) pythonCmd := getPythonPath(backendDir) // 执行Python脚本 cmd := exec.Command(pythonCmd, pythonScript, "send_code", phone, "+86") 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日志-发送验证码] %s", stderr.String()) } if err != nil { return fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) } // 获取UTF-8编码的输出 outputStr := stdout.String() // 解析JSON输出 var result map[string]interface{} if err := json.Unmarshal([]byte(outputStr), &result); err != nil { return fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) } // 检查success字段 if success, ok := result["success"].(bool); !ok || !success { if errMsg, ok := result["error"].(string); ok { return fmt.Errorf("%s", errMsg) } return errors.New("发送验证码失败") } return nil } // GetProfile 获取员工个人信息 func (s *EmployeeService) GetProfile(employeeID int) (*models.User, error) { var employee models.User err := database.DB.Preload("Enterprise").First(&employee, employeeID).Error if err != nil { return nil, err } // 如果已绑定小红书且有Cookie,验证Cookie是否有效 if employee.IsBoundXHS == 1 && employee.XHSCookie != "" { // 检查绑定时间,刚绑定的30秒内不验证(避免与绑定操作冲突) if employee.BoundAt != nil { timeSinceBound := time.Since(*employee.BoundAt) if timeSinceBound < 30*time.Second { log.Printf("GetProfile - 用户%d刚绑定%.0f秒,跳过Cookie验证", employeeID, timeSinceBound.Seconds()) return &employee, nil } } // 异步验证Cookie(不阻塞返回) - 暂时禁用自动验证,避免频繁清空Cookie // TODO: 改为定时任务验证,而不是每次GetProfile都验证 log.Printf("GetProfile - 用户%d有Cookie,长度: %d(已跳过自动验证)", employeeID, len(employee.XHSCookie)) // go s.VerifyCookieAndClear(employeeID) } return &employee, nil } // BindXHS 绑定小红书账号 func (s *EmployeeService) BindXHS(employeeID int, xhsPhone, code string) (string, error) { if code == "" { return "", errors.New("验证码不能为空") } // 获取员工信息 var employee models.User if err := database.DB.First(&employee, employeeID).Error; err != nil { return "", err } // 检查是否已绑定(如果Cookie已失效,允许重新绑定) if employee.IsBoundXHS == 1 && employee.XHSCookie != "" { return "", errors.New("已绑定小红书账号,请先解绑") } // 调用Python服务进行验证码验证和登录 loginResult, err := s.callPythonLogin(xhsPhone, code) if err != nil { return "", fmt.Errorf("小红书登录失败: %w", err) } // 检查Python服务返回结果 if loginResult.Code != 0 { return "", fmt.Errorf("小红书登录失败: %s", loginResult.Message) } // 从返回结果中提取用户信息和cookies userInfo, _ := loginResult.Data["user_info"].(map[string]interface{}) // 优先使用 cookies_full(Playwright完整格式),如果没有则使用 cookies(键值对格式) 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 } // 提取小红书账号昵称 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 } } // 序列化cookies为JSON字符串(使用完整格式) cookiesJSON := "" if cookiesData != nil { cookiesBytes, err := json.Marshal(cookiesData) if err == nil { cookiesJSON = string(cookiesBytes) log.Printf("绑定小红书 - 用户%d - Cookie长度: %d", employeeID, len(cookiesJSON)) } else { log.Printf("绑定小红书 - 用户%d - 序列化Cookie失败: %v", employeeID, err) } } else { log.Printf("绑定小红书 - 用户%d - 警告: cookiesData为nil", employeeID) } if cookiesJSON == "" { log.Printf("绑定小红书 - 用户%d - 错误: 未能获取到Cookie数据", employeeID) return "", errors.New("登录成功但未能获取到Cookie数据,请重试") } now := time.Now() // 开启事务 tx := database.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 更新 ai_users 表的绑定状态和cookie信息 log.Printf("绑定小红书 - 用户%d - 开始更新数据库", employeeID) err = tx.Model(&employee).Updates(map[string]interface{}{ "is_bound_xhs": 1, "xhs_account": xhsNickname, "xhs_phone": xhsPhone, "xhs_cookie": cookiesJSON, "bound_at": &now, }).Error if 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) } log.Printf("绑定小红书 - 用户%d - 绑定成功 - 账号: %s", employeeID, xhsNickname) return xhsNickname, nil } // callPythonLogin 调用Python脚本完成小红书登录 func (s *EmployeeService) callPythonLogin(phone, code string) (*PythonLoginResponse, error) { // 获取Python脚本路径和venv中的Python解释器 backendDir := filepath.Join("..", "backend") pythonScript := filepath.Join(backendDir, "xhs_cli.py") // 使用venv中的Python解释器 (跨平台) pythonCmd := getPythonPath(backendDir) // 执行Python脚本 cmd := exec.Command(pythonCmd, pythonScript, "login", phone, code, "+86") 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日志] %s", stderr.String()) } if err != nil { return nil, fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String()) } // 获取UTF-8编码的输出 outputStr := stdout.String() // 解析JSON输出 var result map[string]interface{} if err := json.Unmarshal([]byte(outputStr), &result); err != nil { return nil, fmt.Errorf("解析Python输出失败: %w, output: %s", err, outputStr) } // 检查success字段 if success, ok := result["success"].(bool); !ok || !success { errorMsg := "登录失败" if errStr, ok := result["error"].(string); ok { errorMsg = errStr } return &PythonLoginResponse{ Code: 1, Message: errorMsg, }, nil } return &PythonLoginResponse{ Code: 0, Message: "登录成功", Data: result, }, 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() } }() // 清空 ai_users 表的绑定信息和cookie err := tx.Model(&employee).Updates(map[string]interface{}{ "is_bound_xhs": 0, "xhs_account": "", "xhs_phone": "", "xhs_cookie": "", "bound_at": nil, }).Error if err != nil { tx.Rollback() return fmt.Errorf("更新员工绑定状态失败: %w", err) } // 提交事务 if err := tx.Commit().Error; err != nil { return fmt.Errorf("提交事务失败: %w", err) } 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 } // 检查是否已绑定 if employee.IsBoundXHS == 0 || employee.XHSCookie == "" { return nil // 没有绑定或已无Cookie,直接返回 } // 检查绑定时间,刚绑定的30秒内不验证(避免与绑定操作冲突) if employee.BoundAt != nil { timeSinceBound := time.Since(*employee.BoundAt) if timeSinceBound < 30*time.Second { log.Printf("验证Cookie - 用户%d刚绑定%.0f秒,跳过验证", employeeID, timeSinceBound.Seconds()) return nil } } // 调用Python脚本验证Cookie verifyResult, err := s.verifyCookieWithPython(employee.XHSCookie) 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"` } // CheckXHSStatus 检查小红书绑定与Cookie健康状态 func (s *EmployeeService) CheckXHSStatus(employeeID int) (*XHSStatus, error) { var employee models.User if err := database.DB.First(&employee, employeeID).Error; err != nil { return nil, err } status := &XHSStatus{ IsBound: employee.IsBoundXHS == 1, HasCookie: employee.XHSCookie != "", CookieValid: false, CookieExpired: false, } if employee.IsBoundXHS == 0 { status.Message = "未绑定小红书账号" return status, nil } if employee.XHSCookie == "" { status.CookieExpired = true status.Message = "已绑定但无有效Cookie,可直接重新绑定" return status, nil } // 刚绑定30秒内视为有效,避免频繁触发验证 if employee.BoundAt != nil { timeSinceBound := time.Since(*employee.BoundAt) if timeSinceBound < 30*time.Second { status.CookieValid = true status.Message = "刚绑定,小于30秒,暂不检测,视为有效" return status, nil } } verifyResult, err := s.verifyCookieWithPython(employee.XHSCookie) 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已失效,已清空,可直接重新绑定" return status, nil } status.CookieValid = true status.CookieExpired = false status.Message = "Cookie有效,已登录" return status, nil } // clearXHSCookie 清空小红书Cookie(保留绑定状态) func (s *EmployeeService) clearXHSCookie(employeeID int) error { // 只清空Cookie,保留is_bound_xhs、xhs_account和xhs_phone err := database.DB.Model(&models.User{}).Where("id = ?", employeeID).Updates(map[string]interface{}{ "xhs_cookie": "", }).Error if err != nil { return fmt.Errorf("清空Cookie失败: %w", err) } log.Printf("已清空用户%d的XHS Cookie", employeeID) return nil } // GetAvailableCopies 获取可领取的文案列表 func (s *EmployeeService) GetAvailableCopies(employeeID int, productID int) (map[string]interface{}, error) { // 获取产品信息 var product models.Product if err := database.DB.First(&product, productID).Error; err != nil { return nil, err } // 获取该产品下所有可用文案(注意:新数据库中status有更多状态) var copies []models.Article if err := database.DB.Where("product_id = ? AND status IN ?", productID, []string{"draft", "approved"}).Order("created_at DESC").Find(&copies).Error; err != nil { return nil, err } return map[string]interface{}{ "product": map[string]interface{}{ "id": product.ID, "name": product.Name, "image": product.ImageURL, }, "copies": copies, }, nil } // ClaimCopy 领取文案(新版本:直接返回文案信息,不再创建领取记录) func (s *EmployeeService) ClaimCopy(employeeID int, copyID int, productID int) (map[string]interface{}, error) { // 检查文案是否存在且可用(注意:新数据库中status有更多状态) var copy models.Article if err := database.DB.Where("id = ? AND status IN ?", copyID, []string{"draft", "approved"}).First(©).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"}) if err := query.Order("RAND()").First(©).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 if err := database.DB.First(©, req.CopyID).Error; err != nil { return 0, errors.New("文案不存在") } // 检查文案是否已被发布 if copy.Status == "published" || copy.Status == "published_review" { return 0, errors.New("文案已被发布或处于发布审核中") } // 获取员工信息 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", ©.ID, "发布文案-更新状态失败", errMessage, "error") tx.Rollback() return 0, errors.New(errMessage) } // 记录更新文案状态日志 s.createLog(tx, employeeID, "article_status_update", "article", ©.ID, fmt.Sprintf("文案ID:%d 状态更新为 %s", copy.ID, publishStatus), "", "success") // 2. 创建发布记录 record := models.PublishRecord{ ArticleID: ©.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", ©.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", ©.ID, "发布事务提交失败,状态更新为failed", errMessage, "error") return 0, errors.New(errMessage) } // 成功日志 s.createLog(nil, employeeID, "article_publish_success", "article", ©.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) { log := models.Log{ UserID: &userID, Action: action, TargetType: targetType, TargetID: targetID, Description: description, 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) } } // GetMyPublishRecords 获取我的发布记录 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 } // 构造返回数据 list := make([]map[string]interface{}, 0) for _, record := range records { publishTimeStr := "" if record.PublishTime != nil { publishTimeStr = record.PublishTime.Format("2006-01-02 15:04:05") } // 查询产品名称 var product models.Product productName := "" if err := database.DB.Where("id = ?", record.ProductID).First(&product).Error; err == nil { productName = product.Name } // 查询文章图片和标签 var images []map[string]interface{} var tags []string if record.ArticleID != nil && *record.ArticleID > 0 { // 查询文章图片 var articleImages []models.ArticleImage if err := database.DB.Where("article_id = ?", *record.ArticleID).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, }) } } // 查询文章标签 var articleTag models.ArticleTag if err := database.DB.Where("article_id = ?", *record.ArticleID).First(&articleTag).Error; err == nil && articleTag.CozeTag != "" { // 解析标签 for _, tag := range splitTags(articleTag.CozeTag) { if tag != "" { tags = append(tags, tag) } } } } 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 } // GetPublishRecordDetail 获取发布记录详情 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关联查询文章内容 var article models.Article content := "" var images []map[string]interface{} var tags []string articleCozeTag := "" // 查询产品名称 var product models.Product productName := "" if err := database.DB.Where("id = ?", record.ProductID).First(&product).Error; err == nil { productName = product.Name } if record.ArticleID != nil && *record.ArticleID > 0 { // 优先使用ArticleID关联 if err := database.DB.Where("id = ?", *record.ArticleID).First(&article).Error; err == nil { content = article.Content articleCozeTag = article.CozeTag // 查询文章图片 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 { 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, }) } } // 查询文章标签(ai_article_tags表) var articleTag models.ArticleTag if err := database.DB.Where("article_id = ?", article.ID).First(&articleTag).Error; err == nil && articleTag.CozeTag != "" { // 使用ai_article_tags表的标签 articleCozeTag = articleTag.CozeTag } // 解析标签(假设标签是逗号分隔的字符串) if articleCozeTag != "" { // 尝试按逗号分割 for _, tag := range splitTags(articleCozeTag) { if tag != "" { tags = append(tags, tag) } } } } } else { // 备用方案:通过title和product_id关联(向后兼容) if err := database.DB.Where("title = ? AND product_id = ?", record.Title, record.ProductID).First(&article).Error; err == nil { content = article.Content articleCozeTag = article.CozeTag // 解析标签 if articleCozeTag != "" { for _, tag := range splitTags(articleCozeTag) { if tag != "" { tags = append(tags, tag) } } } } } 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 } // GetProducts 获取产品列表 func (s *EmployeeService) GetProducts() ([]map[string]interface{}, error) { var products []models.Product if err := database.DB.Find(&products).Error; err != nil { return nil, err } result := make([]map[string]interface{}, 0) for _, product := range products { // 统计该产品下可用文案数量(注意:新数据库中status有更多状态) var totalCopies int64 database.DB.Model(&models.Article{}).Where("product_id = ? AND status IN ?", product.ID, []string{"draft", "approved"}).Count(&totalCopies) result = append(result, map[string]interface{}{ "id": product.ID, "name": product.Name, "image": product.ImageURL, "knowledge": product.Knowledge, "available_copies": totalCopies, }) } return result, nil } // 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"` }