完善文案的状态流转
This commit is contained in:
@@ -27,7 +27,7 @@ xhs:
|
||||
python_service_url: "http://localhost:8000" # Python服务地址
|
||||
|
||||
scheduler:
|
||||
enabled: true # 是否启用定时任务
|
||||
enabled: false # 是否启用定时任务
|
||||
publish_cron: "* * * * * *" # 每1小时执行一次(开发环境测试用)
|
||||
max_concurrent: 2 # 最大并发发布数
|
||||
publish_timeout: 300 # 发布超时时间(秒)
|
||||
|
||||
@@ -27,7 +27,7 @@ xhs:
|
||||
python_service_url: "http://localhost:8000" # Python服务地址,生产环境请修改为实际地址
|
||||
|
||||
scheduler:
|
||||
enabled: true # 是否启用定时任务
|
||||
enabled: false # 是否启用定时任务
|
||||
publish_cron: "0 0 */2 * * *" # 每2小时执行一次(防封号策略)
|
||||
max_concurrent: 2 # 最大并发发布数
|
||||
publish_timeout: 300 # 发布超时时间(秒)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type EmployeeController struct {
|
||||
service *service.EmployeeService
|
||||
}
|
||||
@@ -255,3 +256,41 @@ func (ctrl *EmployeeController) GetProducts(c *gin.Context) {
|
||||
"list": data,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateArticleStatus 更新文案状态(通过/拒绝)
|
||||
func (ctrl *EmployeeController) UpdateArticleStatus(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
articleID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "文案ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证status值
|
||||
if req.Status != "approved" && req.Status != "rejected" {
|
||||
common.Error(c, common.CodeInvalidParams, "status只能为approved或rejected")
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.service.UpdateArticleStatus(employeeID, articleID, req.Status)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
message := "已通过"
|
||||
if req.Status == "rejected" {
|
||||
message = "已拒绝"
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, message, nil)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ func AutoMigrate() error {
|
||||
&models.PublishRecord{},
|
||||
&models.ArticleImage{},
|
||||
&models.ArticleTag{},
|
||||
&models.Author{},
|
||||
&models.Log{},
|
||||
// &models.XHSAccount{}, // 不再使用,小红书信息直接存储在 ai_users 表中
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ type Enterprise struct {
|
||||
Status string `gorm:"type:enum('active','disabled');not null;default:'active';index:idx_status" json:"status" comment:"状态"`
|
||||
UsersTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"users_total" comment:"员工总数"`
|
||||
ProductsTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"products_total" comment:"产品总数"`
|
||||
PublishedTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"published_total" comment:"文章发布总数"`
|
||||
ArticlesTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"articles_total" comment:"文章总数"`
|
||||
ReleasedMonthTotal int `gorm:"type:int(10) unsigned;not null;default:0" json:"released_month_total" comment:"本月发布数量"`
|
||||
LinkedToXHSNum int `gorm:"type:int(10) unsigned;not null;default:0" json:"linked_to_xhs_num" comment:"绑定小红书"`
|
||||
@@ -37,15 +38,15 @@ type User struct {
|
||||
RealName string `gorm:"type:varchar(50)" json:"real_name" comment:"真实姓名"`
|
||||
Email string `gorm:"type:varchar(100)" json:"email" comment:"邮箱"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone" comment:"手机号"`
|
||||
WechatOpenID *string `gorm:"type:varchar(100);uniqueIndex:uk_wechat_openid" json:"wechat_openid,omitempty" comment:"微信OpenID"`
|
||||
WechatUnionID *string `gorm:"type:varchar(100)" json:"wechat_unionid,omitempty" comment:"微信UnionID"`
|
||||
WechatOpenID *string `gorm:"column:wechat_openid;type:varchar(100);uniqueIndex:uk_wechat_openid" json:"wechat_openid,omitempty" comment:"微信OpenID"`
|
||||
WechatUnionID *string `gorm:"column:wechat_unionid;type:varchar(100)" json:"wechat_unionid,omitempty" comment:"微信UnionID"`
|
||||
XHSPhone string `gorm:"type:varchar(20);not null;default:''" json:"xhs_phone" comment:"小红书绑定手机号"`
|
||||
XHSAccount string `gorm:"type:varchar(255);not null;default:''" json:"xhs_account" comment:"小红书账号名称"`
|
||||
XHSCookie string `gorm:"type:text" json:"xhs_cookie" comment:"小红书Cookie"`
|
||||
IsBoundXHS int `gorm:"type:tinyint(1);not null;default:0;index:idx_is_bound_xhs" json:"is_bound_xhs" comment:"是否绑定小红书:0=未绑定,1=已绑定"`
|
||||
BoundAt *time.Time `json:"bound_at" comment:"绑定小红书的时间"`
|
||||
Department string `gorm:"type:varchar(50)" json:"department" comment:"部门"`
|
||||
Role string `gorm:"type:enum('admin','editor','reviewer','publisher','each_title_reviewer');default:'editor'" json:"role" comment:"角色"`
|
||||
Role string `gorm:"type:enum('admin','editor','reviewer','publisher','each_title_reviewer','enterprise');default:'editor'" json:"role" comment:"角色"`
|
||||
Status string `gorm:"type:enum('active','inactive','deleted');default:'active';index:idx_status" json:"status" comment:"状态"`
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
@@ -59,6 +60,7 @@ type Product struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"`
|
||||
Name string `gorm:"type:varchar(200);not null;default:''" json:"name" comment:"产品名称"`
|
||||
TypeID int `gorm:"type:int unsigned;not null;default:0" json:"type_id" comment:"type_id类型ID"`
|
||||
TypeName string `gorm:"type:varchar(128);not null;default:''" json:"type_name" comment:"产品类型"`
|
||||
ImageURL string `gorm:"type:varchar(500);not null;default:''" json:"image_url" comment:"产品主图URL"`
|
||||
ImageThumbnailURL string `gorm:"type:varchar(500);not null;default:''" json:"image_thumbnail_url" comment:"缩略图URL"`
|
||||
@@ -90,7 +92,7 @@ type Article struct {
|
||||
CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"`
|
||||
ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"`
|
||||
PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','assign_authors','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"`
|
||||
ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"`
|
||||
PublishTime *time.Time `json:"publish_time" comment:"发布时间"`
|
||||
@@ -117,7 +119,7 @@ type PublishRecord struct {
|
||||
CreatedUserID int `gorm:"not null;default:0;index:idx_created_user_id" json:"created_user_id" comment:"创建用户ID"`
|
||||
ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"`
|
||||
PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','assign_authors','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"`
|
||||
ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"`
|
||||
PublishTime *time.Time `json:"publish_time" comment:"发布时间"`
|
||||
@@ -167,6 +169,7 @@ type ProductImage struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"`
|
||||
ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"`
|
||||
ProductName string `gorm:"type:varchar(256);not null;default:''" json:"product_name" comment:"产品名称"`
|
||||
ImageID int `gorm:"not null;default:0" json:"image_id" comment:"图片ID"`
|
||||
ImageName string `gorm:"type:varchar(255);not null;default:''" json:"image_name" comment:"图片名称"`
|
||||
ImageURL string `gorm:"type:varchar(500);not null;default:''" json:"image_url" comment:"图片URL"`
|
||||
@@ -185,6 +188,7 @@ type ProductImage struct {
|
||||
// ArticleImage 文章图片表
|
||||
type ArticleImage struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0" json:"enterprise_id" comment:"所属企业ID"`
|
||||
ArticleID int `gorm:"not null;default:0;index:idx_article_id" json:"article_id" comment:"文章ID"`
|
||||
ImageID int `gorm:"not null;default:0;index:idx_image_id" json:"image_id" comment:"图片ID"`
|
||||
ImageURL string `gorm:"type:varchar(500);not null;default:''" json:"image_url" comment:"图片URL"`
|
||||
@@ -203,6 +207,7 @@ type ArticleImage struct {
|
||||
// ArticleTag 文章标签表
|
||||
type ArticleTag struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0" json:"enterprise_id" comment:"所属企业ID"`
|
||||
ArticleID int `gorm:"not null;default:0;uniqueIndex:uk_article_tag" json:"article_id" comment:"文章ID"`
|
||||
CozeTag string `gorm:"type:varchar(500)" json:"coze_tag" comment:"Coze生成的标签"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
@@ -238,6 +243,31 @@ type Log struct {
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
}
|
||||
|
||||
// Author 作者表(对应ai_authors)
|
||||
type Author struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0" json:"enterprise_id" comment:"所属企业ID"`
|
||||
CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone" comment:"手机号"`
|
||||
AuthorName string `gorm:"type:varchar(100);not null;default:''" json:"author_name" comment:"作者名称"`
|
||||
AppID string `gorm:"type:varchar(127);not null;default:''" json:"app_id" comment:"应用ID"`
|
||||
AppToken string `gorm:"type:varchar(127);not null;default:''" json:"app_token" comment:"应用Token"`
|
||||
DepartmentID int `gorm:"not null;default:0" json:"department_id" comment:"部门ID"`
|
||||
DepartmentName string `gorm:"type:varchar(255);not null;default:''" json:"department_name" comment:"部门名称"`
|
||||
Department string `gorm:"type:varchar(50);not null;default:''" json:"department" comment:"部门"`
|
||||
Title string `gorm:"type:varchar(50)" json:"title" comment:"职称"`
|
||||
Hospital string `gorm:"type:varchar(100)" json:"hospital" comment:"医院"`
|
||||
Specialty string `gorm:"type:text" json:"specialty" comment:"专业"`
|
||||
ToutiaoCookie string `gorm:"type:text" json:"toutiao_cookie" comment:"头条Cookie"`
|
||||
ToutiaoImagesCookie string `gorm:"type:text" json:"toutiao_images_cookie" comment:"头条图片Cookie"`
|
||||
Introduction string `gorm:"type:text" json:"introduction" comment:"介绍"`
|
||||
AvatarURL string `gorm:"type:varchar(255)" json:"avatar_url" comment:"头像URL"`
|
||||
Status string `gorm:"type:enum('active','inactive');default:'active';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// TableName 指定表名(带ai_前缀)
|
||||
func (Enterprise) TableName() string {
|
||||
return "ai_enterprises"
|
||||
@@ -286,3 +316,7 @@ func (DataStatistics) TableName() string {
|
||||
func (Log) TableName() string {
|
||||
return "ai_logs"
|
||||
}
|
||||
|
||||
func (Author) TableName() string {
|
||||
return "ai_authors"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ func SetupRouter(r *gin.Engine) {
|
||||
|
||||
// 10.9 检查小红书绑定与Cookie状态
|
||||
employee.GET("/xhs/status", employeeCtrl.CheckXHSStatus)
|
||||
|
||||
// 10.10 更新文案状态(通过/拒绝)
|
||||
employee.POST("/article/:id/status", employeeCtrl.UpdateArticleStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthService struct{}
|
||||
@@ -141,33 +143,95 @@ func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 根据OpenID查找或创建员工
|
||||
var employee models.User
|
||||
|
||||
// 优先通过OpenID查找(注意:使用IS NOT NULL过滤空值)
|
||||
result := database.DB.Where("wechat_openid = ? AND wechat_openid IS NOT NULL", wxResp.OpenID).First(&employee)
|
||||
|
||||
if result.Error != nil {
|
||||
// OpenID不存在,需要绑定OpenID
|
||||
// 2. 根据手机号查找用户(手机号必填)
|
||||
if phone == "" {
|
||||
return "", nil, errors.New("首次登录请提供手机号")
|
||||
return "", nil, errors.New("请提供手机号")
|
||||
}
|
||||
|
||||
var employee models.User
|
||||
// 通过手机号查找员工
|
||||
result = database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
|
||||
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
|
||||
if result.Error != nil {
|
||||
return "", nil, errors.New("员工不存在,请联系管理员添加")
|
||||
// 手机号不存在,不允许登录
|
||||
return "", nil, errors.New("手机号不存在,请联系管理员添加")
|
||||
}
|
||||
|
||||
// 绑定OpenID和UnionID(使用指针)
|
||||
// 3. 检查微信绑定信息
|
||||
// 如果该用户已绑定微信信息,必须与当前登录的微信信息一致(单设备登录)
|
||||
if employee.WechatOpenID != nil && *employee.WechatOpenID != "" {
|
||||
// 已绑定微信,检查是否一致
|
||||
if *employee.WechatOpenID != wxResp.OpenID {
|
||||
return "", nil, errors.New("该账号已在其他设备登录,请使用原设备登录或联系管理员")
|
||||
}
|
||||
|
||||
// 如果有UnionID,也需要检查一致性
|
||||
if employee.WechatUnionID != nil && *employee.WechatUnionID != "" && wxResp.UnionID != "" {
|
||||
if *employee.WechatUnionID != wxResp.UnionID {
|
||||
return "", nil, errors.New("微信账号信息不匹配,请使用原设备登录")
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[微信登录] 用户 %s (ID:%d) 微信验证通过", employee.Phone, employee.ID)
|
||||
} else {
|
||||
// 微信信息为空,说明是新用户首次登录,保存微信信息
|
||||
log.Printf("[微信登录] 新用户首次登录,绑定微信信息: OpenID=%s, UnionID=%s", wxResp.OpenID, wxResp.UnionID)
|
||||
|
||||
employee.WechatOpenID = &wxResp.OpenID
|
||||
if wxResp.UnionID != "" {
|
||||
employee.WechatUnionID = &wxResp.UnionID
|
||||
}
|
||||
database.DB.Save(&employee)
|
||||
|
||||
// 使用事务保存微信绑定信息并创建作者记录
|
||||
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 保存微信绑定信息
|
||||
if err := tx.Save(&employee).Error; err != nil {
|
||||
return fmt.Errorf("保存微信绑定信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 生成JWT token
|
||||
// 2. 检查是否已存在作者记录(通过手机号和企业ID)
|
||||
var existingAuthor models.Author
|
||||
result := tx.Where("phone = ? AND enterprise_id = ?", employee.Phone, employee.EnterpriseID).First(&existingAuthor)
|
||||
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
// 作者记录不存在,创建新记录
|
||||
author := models.Author{
|
||||
EnterpriseID: employee.EnterpriseID,
|
||||
CreatedUserID: employee.ID,
|
||||
Phone: employee.Phone,
|
||||
AuthorName: employee.RealName,
|
||||
Department: employee.Department,
|
||||
Status: "active",
|
||||
Channel: 3, // 3=weixin (微信小程序)
|
||||
}
|
||||
|
||||
// 如果真实姓名为空,使用用户名
|
||||
if author.AuthorName == "" {
|
||||
author.AuthorName = employee.Username
|
||||
}
|
||||
|
||||
if err := tx.Create(&author).Error; err != nil {
|
||||
return fmt.Errorf("创建作者记录失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[微信登录] 创建作者记录成功: ID=%d, Name=%s", author.ID, author.AuthorName)
|
||||
} else if result.Error != nil {
|
||||
// 其他数据库错误
|
||||
return fmt.Errorf("检查作者记录失败: %v", result.Error)
|
||||
} else {
|
||||
log.Printf("[微信登录] 作者记录已存在: ID=%d", existingAuthor.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
log.Printf("[微信登录] 用户 %s (ID:%d) 微信绑定成功", employee.Phone, employee.ID)
|
||||
}
|
||||
|
||||
// 4. 生成JWT token
|
||||
token, err := utils.GenerateToken(employee.ID)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
|
||||
@@ -492,20 +492,37 @@ func (s *EmployeeService) clearXHSCookie(employeeID int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableCopies 获取可领取的文案列表
|
||||
// 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
|
||||
}
|
||||
|
||||
// 获取该产品下所有可用文案(注意:新数据库中status有更多状态)
|
||||
// 根据产品ID、作者ID和状态筛选文案
|
||||
// status = 'assign_authors' 表示已分配作者的文案
|
||||
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
|
||||
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,
|
||||
@@ -513,14 +530,106 @@ func (s *EmployeeService) GetAvailableCopies(employeeID int, productID int) (map
|
||||
"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"}).First(©).Error; err != nil {
|
||||
if err := database.DB.Where("id = ? AND status IN ?", copyID, []string{"draft", "approved", "assign_authors"}).First(©).Error; err != nil {
|
||||
return nil, errors.New("文案不存在或不可用")
|
||||
}
|
||||
|
||||
@@ -542,7 +651,7 @@ func (s *EmployeeService) ClaimCopy(employeeID int, copyID int, productID int) (
|
||||
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"})
|
||||
query := database.DB.Where("product_id = ? AND status IN ?", productID, []string{"draft", "approved", "assign_authors"})
|
||||
|
||||
if err := query.Order("RAND()").First(©).Error; err != nil {
|
||||
return nil, errors.New("暂无可领取的文案")
|
||||
|
||||
@@ -25,8 +25,8 @@ interface EnvConfig {
|
||||
const API_CONFIG: Record<EnvType, EnvConfig> = {
|
||||
// 开发环境 - 本地开发
|
||||
dev: {
|
||||
baseURL: 'http://localhost:8080', // 本地Go服务
|
||||
pythonURL: 'http://localhost:8000', // 本地Python服务
|
||||
baseURL: 'http://192.168.17.127:8080', // 本地Go服务
|
||||
pythonURL: 'http://192.168.17.127:8000', // 本地Python服务
|
||||
timeout: 90000
|
||||
},
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ Page({
|
||||
currentCopy: null as any, // 当前显示的文案
|
||||
loading: false,
|
||||
claiming: false, // 领取中
|
||||
publishing: false // 发布中
|
||||
publishing: false, // 发布中
|
||||
rejecting: false // 拒绝中
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
@@ -150,7 +151,7 @@ Page({
|
||||
|
||||
// 换一个文案
|
||||
changeArticle() {
|
||||
if (this.data.claiming || this.data.publishing) return;
|
||||
if (this.data.claiming || this.data.publishing || this.data.rejecting) return;
|
||||
|
||||
const { allCopies, currentIndex } = this.data;
|
||||
|
||||
@@ -162,6 +163,14 @@ Page({
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载动画
|
||||
wx.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
// 模拟加载效果,让切换更平滑
|
||||
setTimeout(() => {
|
||||
// 切换到下一个文案
|
||||
const nextIndex = (currentIndex + 1) % allCopies.length;
|
||||
|
||||
@@ -170,11 +179,9 @@ Page({
|
||||
currentCopy: allCopies[nextIndex]
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: `${nextIndex + 1}/${allCopies.length}`,
|
||||
icon: 'none',
|
||||
duration: 1000
|
||||
});
|
||||
// 隐藏加载动画
|
||||
wx.hideLoading();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 一键发布(先领取,再发布)
|
||||
@@ -281,6 +288,84 @@ Page({
|
||||
};
|
||||
},
|
||||
|
||||
// 拒绝文案
|
||||
async rejectArticle() {
|
||||
if (!this.data.currentCopy) {
|
||||
wx.showToast({
|
||||
title: '请先选择文案',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '确认拒绝',
|
||||
content: '确定要拒绝这篇文案吗?',
|
||||
confirmText: '拒绝',
|
||||
confirmColor: '#ff4d4f',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await this.doRejectArticle();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 执行拒绝操作
|
||||
async doRejectArticle() {
|
||||
this.setData({ rejecting: true });
|
||||
|
||||
try {
|
||||
const response = await EmployeeService.updateArticleStatus(
|
||||
this.data.currentCopy.id,
|
||||
'rejected'
|
||||
);
|
||||
|
||||
if (response.code === 200) {
|
||||
wx.showToast({
|
||||
title: '已拒绝',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
// 从列表中移除当前文案
|
||||
const { allCopies, currentIndex } = this.data;
|
||||
allCopies.splice(currentIndex, 1);
|
||||
|
||||
// 更新显示
|
||||
if (allCopies.length > 0) {
|
||||
const newIndex = currentIndex >= allCopies.length ? 0 : currentIndex;
|
||||
this.setData({
|
||||
allCopies,
|
||||
currentIndex: newIndex,
|
||||
currentCopy: allCopies[newIndex],
|
||||
rejecting: false
|
||||
});
|
||||
} else {
|
||||
// 没有更多文案了
|
||||
this.setData({
|
||||
allCopies: [],
|
||||
currentCopy: null,
|
||||
rejecting: false
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message || '拒绝失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('拒绝失败:', error);
|
||||
this.setData({ rejecting: false });
|
||||
wx.showToast({
|
||||
title: error.message || '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
return {
|
||||
|
||||
@@ -53,16 +53,24 @@
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar" wx:if="{{currentCopy}}">
|
||||
<button
|
||||
class="action-btn secondary"
|
||||
class="action-btn refresh"
|
||||
bindtap="changeArticle"
|
||||
disabled="{{claiming || publishing}}"
|
||||
disabled="{{claiming || publishing || rejecting}}"
|
||||
>
|
||||
<text class="btn-text">换一换</text>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn reject"
|
||||
bindtap="rejectArticle"
|
||||
disabled="{{claiming || publishing || rejecting}}"
|
||||
loading="{{rejecting}}"
|
||||
>
|
||||
<text class="btn-text">{{rejecting ? '拒绝中...' : '拒绝'}}</text>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
bindtap="publishArticle"
|
||||
disabled="{{claiming || publishing}}"
|
||||
disabled="{{claiming || publishing || rejecting}}"
|
||||
loading="{{claiming || publishing}}"
|
||||
>
|
||||
<text class="btn-text">{{claiming ? '领取中...' : (publishing ? '发布中...' : '一键发布')}}</text>
|
||||
|
||||
@@ -173,7 +173,7 @@ page {
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
background: white;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
@@ -182,11 +182,11 @@ page {
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 80rpx;
|
||||
height: 88rpx;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -198,20 +198,34 @@ page {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
flex: 0 0 180rpx;
|
||||
/* 换一换按钮 */
|
||||
.action-btn.refresh {
|
||||
flex: 0 0 220rpx;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-btn.secondary:active {
|
||||
.action-btn.refresh:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
/* 拒绝按钮 */
|
||||
.action-btn.reject {
|
||||
flex: 0 0 180rpx;
|
||||
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.reject:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 一键发布按钮 */
|
||||
.action-btn.primary {
|
||||
flex: 1;
|
||||
background: #07c160;
|
||||
background: linear-gradient(135deg, #07c160 0%, #0ae97a 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.primary:active {
|
||||
@@ -219,7 +233,7 @@ page {
|
||||
}
|
||||
|
||||
.action-btn[disabled] {
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
|
||||
@@ -210,4 +210,13 @@ export class EmployeeService {
|
||||
publish_time: string;
|
||||
}>(`/api/employee/publish-record/${recordId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文案状态(通过/拒绝)
|
||||
*/
|
||||
static async updateArticleStatus(articleId: number, status: 'approved' | 'rejected') {
|
||||
return post(`/api/employee/article/${articleId}/status`, {
|
||||
status
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user