Files
ai_wht_wechat/go_backend/service/employee_service.go
2025-12-20 01:05:46 +08:00

1064 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_fullPlaywright完整格式如果没有则使用 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 获取可领取的文案列表根据作者ID、产品ID和状态筛选
func (s *EmployeeService) GetAvailableCopies(employeeID int, productID int) (map[string]interface{}, error) {
// 获取当前用户信息查找对应的作者ID
var employee models.User
if err := database.DB.First(&employee, employeeID).Error; err != nil {
return nil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 查找对应的作者记录
var author models.Author
if err := database.DB.Where("phone = ? AND enterprise_id = ?", employee.Phone, employee.EnterpriseID).First(&author).Error; err != nil {
return nil, fmt.Errorf("未找到对应的作者记录: %w", err)
}
// 获取产品信息
var product models.Product
if err := database.DB.First(&product, productID).Error; err != nil {
return nil, err
}
// 根据产品ID、作者ID和状态筛选文案
// status = 'assign_authors' 表示已分配作者的文案
var copies []models.Article
query := database.DB.Where("product_id = ? AND author_id = ? AND status = ?", productID, author.ID, "assign_authors")
if err := query.Order("created_at DESC").Find(&copies).Error; err != nil {
return nil, fmt.Errorf("查询文案列表失败: %w", err)
}
log.Printf("[获取文案列表] 用户ID=%d, 作者ID=%d, 产品ID=%d, 筛选到 %d 条文案", employeeID, author.ID, productID, len(copies))
return map[string]interface{}{
"product": map[string]interface{}{
"id": product.ID,
"name": product.Name,
"image": product.ImageURL,
},
"copies": copies,
"author": map[string]interface{}{
"id": author.ID,
"name": author.AuthorName,
},
}, nil
}
// 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)
}
// 查找对应的作者记录
var author models.Author
if err := database.DB.Where("phone = ? AND enterprise_id = ?", employee.Phone, employee.EnterpriseID).First(&author).Error; err != nil {
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
}
// 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
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("文案已被发布或处于发布审核中")
}
// 获取员工信息
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) {
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"`
}