package controller import ( "ai_xhs/common" "ai_xhs/database" "ai_xhs/models" "ai_xhs/service" "ai_xhs/utils" "bytes" "context" "encoding/base64" "fmt" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) type EmployeeController struct { service *service.EmployeeService } func NewEmployeeController() *EmployeeController { return &EmployeeController{ service: &service.EmployeeService{}, } } // SendXHSCode 发送小红书验证码 func (ctrl *EmployeeController) SendXHSCode(c *gin.Context) { var req struct { XHSPhone string `json:"xhs_phone" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误:手机号不能为空") return } // 获取当前登录用户ID employeeID := c.GetInt("employee_id") data, err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } // 检查是否需要扫码验证 if needCaptcha, ok := data["need_captcha"].(bool); ok && needCaptcha { // 发送验证码时触发风控,返回二维码 common.SuccessWithMessage(c, "需要扫码验证", data) return } common.SuccessWithMessage(c, "验证码已发送,请在小红书APP中查看", data) } // GetProfile 获取员工个人信息 func (ctrl *EmployeeController) GetProfile(c *gin.Context) { employeeID := c.GetInt("employee_id") employee, err := ctrl.service.GetProfile(employeeID) if err != nil { common.Error(c, common.CodeNotFound, "员工不存在") return } // 获取用户显示名称(优先使用真实姓名,其次用户名) displayName := employee.RealName if displayName == "" { displayName = employee.Username } data := map[string]interface{}{ "id": employee.ID, "name": displayName, "username": employee.Username, "real_name": employee.RealName, "nickname": employee.Nickname, "email": employee.Email, "phone": employee.Phone, "role": employee.Role, "enterprise_id": employee.EnterpriseID, "enterprise_name": employee.Enterprise.Name, "avatar": employee.Icon, "is_bound_xhs": employee.IsBoundXHS, } // 如果已绑定,从 ai_authors 表获取小红书账号信息(根据 created_user_id 查询) if employee.IsBoundXHS == 1 { 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 { data["xhs_account"] = author.XHSAccount data["xhs_phone"] = author.XHSPhone data["has_xhs_cookie"] = author.XHSCookie != "" if author.BoundAt != nil { data["bound_at"] = author.BoundAt.Format("2006-01-02 15:04:05") } } else { // 没有找到author记录,返回默认值 data["xhs_account"] = "" data["xhs_phone"] = "" data["has_xhs_cookie"] = false } } else { data["xhs_account"] = "" data["xhs_phone"] = "" data["has_xhs_cookie"] = false } common.Success(c, data) } // UpdateProfile 更新个人资料(昵称、邮箱、头像) func (ctrl *EmployeeController) UpdateProfile(c *gin.Context) { employeeID := c.GetInt("employee_id") var req struct { Nickname *string `json:"nickname"` Email *string `json:"email"` Avatar *string `json:"avatar"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } if req.Nickname == nil && req.Email == nil && req.Avatar == nil { common.Error(c, common.CodeInvalidParams, "没有可更新的字段") return } // 简单校验邮箱格式 if req.Email != nil && *req.Email != "" { if !strings.Contains(*req.Email, "@") { common.Error(c, common.CodeInvalidParams, "邮箱格式不正确") return } } if err := ctrl.service.UpdateProfile(employeeID, req.Nickname, req.Email, req.Avatar); err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "更新成功", nil) } // UploadAvatar 上传头像 func (ctrl *EmployeeController) UploadAvatar(c *gin.Context) { employeeID := c.GetInt("employee_id") // 获取上传的文件 file, err := c.FormFile("file") if err != nil { common.Error(c, common.CodeInvalidParams, "请选择要上传的图片") return } // 校验文件类型 contentType := file.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "image/") { common.Error(c, common.CodeInvalidParams, "只能上传图片文件") return } // 校验文件大小(5MB) if file.Size > 5*1024*1024 { common.Error(c, common.CodeInvalidParams, "图片大小不能超过5MB") return } // 打开文件 src, err := file.Open() if err != nil { common.Error(c, common.CodeInternalError, "打开文件失败") return } defer src.Close() // 读取文件内容 buf := new(bytes.Buffer) _, err = buf.ReadFrom(src) if err != nil { common.Error(c, common.CodeInternalError, "读取文件失败") return } // 上传到 OSS fileExt := ".jpg" if strings.Contains(contentType, "png") { fileExt = ".png" } else if strings.Contains(contentType, "webp") { fileExt = ".webp" } fileName := fmt.Sprintf("avatar_%d_%d%s", employeeID, time.Now().Unix(), fileExt) ossURL, err := utils.UploadToOSS(bytes.NewReader(buf.Bytes()), fileName) if err != nil { common.Error(c, common.CodeInternalError, fmt.Sprintf("上传失败: %s", err.Error())) return } // 更新数据库 if err := ctrl.service.UpdateProfile(employeeID, nil, nil, &ossURL); err != nil { common.Error(c, common.CodeInternalError, "更新头像失败") return } common.Success(c, map[string]interface{}{ "url": ossURL, }) } // BindXHS 绑定小红书账号(异步处理) func (ctrl *EmployeeController) BindXHS(c *gin.Context) { employeeID := c.GetInt("employee_id") var req struct { XHSPhone string `json:"xhs_phone" binding:"required"` Code string `json:"code" binding:"required"` SessionID string `json:"session_id"` // 发送验证码时返回的session_id,用于复用浏览器 } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } _, err := ctrl.service.BindXHS(employeeID, req.XHSPhone, req.Code, req.SessionID) if err != nil { common.Error(c, common.CodeBindXHSFailed, err.Error()) return } // 立即返回成功,告知前端正在处理 common.SuccessWithMessage(c, "正在验证登录,请稍候...", map[string]interface{}{ "status": "processing", }) } // GetBindXHSStatus 获取小红书绑定状态 func (ctrl *EmployeeController) GetBindXHSStatus(c *gin.Context) { employeeID := c.GetInt("employee_id") status, err := ctrl.service.GetBindXHSStatus(employeeID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.Success(c, status) } // UnbindXHS 解绑小红书账号 func (ctrl *EmployeeController) UnbindXHS(c *gin.Context) { employeeID := c.GetInt("employee_id") if err := ctrl.service.UnbindXHS(employeeID); err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "解绑成功", nil) } // GetAvailableCopies 获取可领取文案列表 func (ctrl *EmployeeController) GetAvailableCopies(c *gin.Context) { employeeID := c.GetInt("employee_id") productID, err := strconv.Atoi(c.Query("product_id")) if err != nil { common.Error(c, common.CodeInvalidParams, "产品ID参数错误") return } data, err := ctrl.service.GetAvailableCopies(employeeID, productID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.Success(c, data) } // ClaimCopy 领取文案 func (ctrl *EmployeeController) ClaimCopy(c *gin.Context) { employeeID := c.GetInt("employee_id") var req struct { CopyID int `json:"copy_id" binding:"required"` ProductID int `json:"product_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } data, err := ctrl.service.ClaimCopy(employeeID, req.CopyID, req.ProductID) if err != nil { common.Error(c, common.CodeAlreadyClaimed, err.Error()) return } common.SuccessWithMessage(c, "领取成功", data) } // ClaimRandomCopy 随机领取文案 func (ctrl *EmployeeController) ClaimRandomCopy(c *gin.Context) { employeeID := c.GetInt("employee_id") var req struct { ProductID int `json:"product_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } data, err := ctrl.service.ClaimRandomCopy(employeeID, req.ProductID) if err != nil { common.Error(c, common.CodeCopyNotAvailable, err.Error()) return } common.SuccessWithMessage(c, "领取成功", data) } // Publish 发布内容 func (ctrl *EmployeeController) Publish(c *gin.Context) { employeeID := c.GetInt("employee_id") var req service.PublishRequest if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } recordID, err := ctrl.service.Publish(employeeID, req) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "发布成功", map[string]interface{}{ "record_id": recordID, }) } // GetMyPublishRecords 获取我的发布记录 func (ctrl *EmployeeController) GetMyPublishRecords(c *gin.Context) { employeeID := c.GetInt("employee_id") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) data, err := ctrl.service.GetMyPublishRecords(employeeID, page, pageSize) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.Success(c, data) } // GetPublishRecordDetail 获取发布记录详情 func (ctrl *EmployeeController) GetPublishRecordDetail(c *gin.Context) { employeeID := c.GetInt("employee_id") recordID, err := strconv.Atoi(c.Param("id")) if err != nil { common.Error(c, common.CodeInvalidParams, "记录ID参数错误") return } data, err := ctrl.service.GetPublishRecordDetail(employeeID, recordID) if err != nil { common.Error(c, common.CodeNotFound, err.Error()) return } common.Success(c, data) } // CheckXHSStatus 检查小红书绑定与Cookie状态 func (ctrl *EmployeeController) CheckXHSStatus(c *gin.Context) { employeeID := c.GetInt("employee_id") status, err := ctrl.service.CheckXHSStatus(employeeID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.Success(c, status) } // GetProducts 获取产品列表 func (ctrl *EmployeeController) GetProducts(c *gin.Context) { employeeID := c.GetInt("employee_id") if employeeID == 0 { common.Error(c, common.CodeUnauthorized, "未登录或token无效") return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) data, hasMore, err := ctrl.service.GetProducts(employeeID, page, pageSize) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.Success(c, map[string]interface{}{ "list": data, "has_more": hasMore, }) } // 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) } // UpdateArticleContent 更新文案内容(标题、正文) func (ctrl *EmployeeController) UpdateArticleContent(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 { Title string `json:"title" binding:"required"` Content string `json:"content" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } // 验证标题和内容字数 if len([]rune(req.Title)) > 20 { common.Error(c, common.CodeInvalidParams, "标题最多20字") return } if len([]rune(req.Content)) > 1000 { common.Error(c, common.CodeInvalidParams, "内容最多1000字") return } err = ctrl.service.UpdateArticleContent(employeeID, articleID, req.Title, req.Content) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "更新成功", nil) } // UpdatePublishRecord 编辑发布记录(修改标题、内容、图片、标签) func (ctrl *EmployeeController) UpdatePublishRecord(c *gin.Context) { employeeID := c.GetInt("employee_id") recordID, err := strconv.Atoi(c.Param("id")) if err != nil { common.Error(c, common.CodeInvalidParams, "记录ID参数错误") return } var req service.UpdatePublishRecordRequest if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } // 验证标题和内容字数 if req.Title != nil && len([]rune(*req.Title)) > 20 { common.Error(c, common.CodeInvalidParams, "标题最多20字") return } if req.Content != nil && len([]rune(*req.Content)) > 1000 { common.Error(c, common.CodeInvalidParams, "内容最多1000字") return } if err := ctrl.service.UpdatePublishRecord(employeeID, recordID, req); err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "更新成功", nil) } // RepublishRecord 重新发布种草内容 func (ctrl *EmployeeController) RepublishRecord(c *gin.Context) { employeeID := c.GetInt("employee_id") recordID, err := strconv.Atoi(c.Param("id")) if err != nil { common.Error(c, common.CodeInvalidParams, "记录ID参数错误") return } publishLink, err := ctrl.service.RepublishRecord(employeeID, recordID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "重新发布成功", map[string]interface{}{ "publish_link": publishLink, }) } // AddArticleImage 添加文案图片 func (ctrl *EmployeeController) AddArticleImage(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 { ImageURL string `json:"image_url" binding:"required"` ImageThumbURL string `json:"image_thumb_url"` KeywordsName string `json:"keywords_name"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } // 如果没有缩略图,使用原图 if req.ImageThumbURL == "" { req.ImageThumbURL = req.ImageURL } image, err := ctrl.service.AddArticleImage(employeeID, articleID, req.ImageURL, req.ImageThumbURL, req.KeywordsName) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "添加成功", image) } // DeleteArticleImage 删除文案图片 func (ctrl *EmployeeController) DeleteArticleImage(c *gin.Context) { employeeID := c.GetInt("employee_id") imageID, err := strconv.Atoi(c.Param("imageId")) if err != nil { common.Error(c, common.CodeInvalidParams, "图片ID参数错误") return } err = ctrl.service.DeleteArticleImage(employeeID, imageID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "删除成功", nil) } // UpdateArticleImagesOrder 更新文案图片排序 func (ctrl *EmployeeController) UpdateArticleImagesOrder(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 { ImageOrders []map[string]int `json:"image_orders" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } err = ctrl.service.UpdateArticleImagesOrder(employeeID, articleID, req.ImageOrders) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "更新成功", nil) } // UploadImage 上传图片(支持base64和multipart/form-data) func (ctrl *EmployeeController) UploadImage(c *gin.Context) { // 尝试从表单获取文件 file, header, err := c.Request.FormFile("file") if err == nil { // 处理文件上传 defer file.Close() // 验证文件类型 contentType := header.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "image/") { common.Error(c, common.CodeInvalidParams, "只支持图片文件") return } // 上传到OSS imageURL, err := utils.UploadToOSS(file, header.Filename) if err != nil { common.Error(c, common.CodeInternalError, fmt.Sprintf("上传失败: %v", err)) return } common.SuccessWithMessage(c, "上传成功", map[string]interface{}{ "image_url": imageURL, "image_thumb_url": imageURL, // 简化处理,缩略图与原图相同 }) return } // 尝试介ase64获取 var req struct { Base64 string `json:"base64" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "请上传文件或base64数据") return } // 解析base64 var imageData []byte if strings.Contains(req.Base64, "base64,") { // 移除data:image/xxx;base64,前缀 parts := strings.Split(req.Base64, "base64,") if len(parts) != 2 { common.Error(c, common.CodeInvalidParams, "base64格式错误") return } imageData, err = base64.StdEncoding.DecodeString(parts[1]) } else { imageData, err = base64.StdEncoding.DecodeString(req.Base64) } if err != nil { common.Error(c, common.CodeInvalidParams, "base64解码失败") return } // 上传到OSS reader := bytes.NewReader(imageData) imageURL, err := utils.UploadToOSS(reader, "image.jpg") if err != nil { common.Error(c, common.CodeInternalError, fmt.Sprintf("上传失败: %v", err)) return } common.SuccessWithMessage(c, "上传成功", map[string]interface{}{ "image_url": imageURL, "image_thumb_url": imageURL, }) } // RevokeUserToken 禁用用户(撤销Token) func (ctrl *EmployeeController) RevokeUserToken(c *gin.Context) { // 只有管理员可以禁用用户 employeeID := c.GetInt("employee_id") // 获取当前用户信息,检查是否为管理员 var currentUser models.User if err := database.DB.Where("id = ?", employeeID).First(¤tUser).Error; err != nil { common.Error(c, common.CodeUnauthorized, "用户不存在") return } if currentUser.Role != "admin" { common.Error(c, common.CodeUnauthorized, "无权操作,只有管理员可以禁用用户") return } var req struct { TargetUserID int `json:"target_user_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误:需要提供目标用户ID") return } // 不能禁用自己 if req.TargetUserID == employeeID { common.Error(c, common.CodeInvalidParams, "不能禁用自己") return } // 检查目标用户是否存在 var targetUser models.User if err := database.DB.Where("id = ?", req.TargetUserID).First(&targetUser).Error; err != nil { common.Error(c, common.CodeNotFound, "目标用户不存在") return } // 撤销该用户的Token ctx := context.Background() if err := utils.RevokeToken(ctx, req.TargetUserID); err != nil { common.Error(c, common.CodeInternalError, fmt.Sprintf("禁用失败: %v", err)) return } common.SuccessWithMessage(c, fmt.Sprintf("已禁用用户 %s (手机号: %s),该用户需要重新登录", targetUser.Username, targetUser.Phone), nil) } // SaveQRCodeLogin 保存扫码登录的绑定信息 // 由Python后端调用,不需要认证 func (ctrl *EmployeeController) SaveQRCodeLogin(c *gin.Context) { var req struct { EmployeeID int `json:"employee_id" binding:"required"` CookiesFull []interface{} `json:"cookies_full"` UserInfo map[string]interface{} `json:"user_info"` LoginState map[string]interface{} `json:"login_state"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } // 调用service层直接保存,与验证码登录相同的逻辑 err := ctrl.service.SaveQRCodeLogin(req.EmployeeID, req.CookiesFull, req.UserInfo, req.LoginState) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "绑定成功", nil) } // SaveLogin 保存验证码登录的绑定信息 func (ctrl *EmployeeController) SaveLogin(c *gin.Context) { var req struct { EmployeeID int `json:"employee_id" binding:"required"` CookiesFull []interface{} `json:"cookies_full"` StorageState map[string]interface{} `json:"storage_state"` StorageStatePath string `json:"storage_state_path"` UserInfo map[string]interface{} `json:"user_info"` // 新增: 用户信息 } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } // 调用service层保存 err := ctrl.service.SaveLogin(req.EmployeeID, req.CookiesFull, req.StorageState, req.StorageStatePath, req.UserInfo) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } common.SuccessWithMessage(c, "绑定成功", nil) } // StartQRCodeLogin 启动扫码登录,转发到Python服务 func (ctrl *EmployeeController) StartQRCodeLogin(c *gin.Context) { employeeID := c.GetInt("employee_id") data, err := ctrl.service.StartQRCodeLogin(employeeID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } // 直接返回Python的响应格式,保持code=0 c.JSON(http.StatusOK, data) } // GetQRCodeStatus 获取扫码状态,转发到Python服务 func (ctrl *EmployeeController) GetQRCodeStatus(c *gin.Context) { employeeID := c.GetInt("employee_id") var req struct { SessionID string `json:"session_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } data, err := ctrl.service.GetQRCodeStatus(employeeID, req.SessionID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } // 直接返回Python的响应格式,保持code=0或code=2 c.JSON(http.StatusOK, data) } // RefreshQRCode 刷新二维码,转发到Python服务 func (ctrl *EmployeeController) RefreshQRCode(c *gin.Context) { employeeID := c.GetInt("employee_id") var req struct { SessionID string `json:"session_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } data, err := ctrl.service.RefreshQRCode(employeeID, req.SessionID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } // 直接返回Python的响应格式,保持code=0或code=3 c.JSON(http.StatusOK, data) } // CancelQRCodeLogin 取消扫码登录,释放浏览器资源 func (ctrl *EmployeeController) CancelQRCodeLogin(c *gin.Context) { var req struct { SessionID string `json:"session_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { common.Error(c, common.CodeInvalidParams, "参数错误") return } data, err := ctrl.service.CancelQRCodeLogin(req.SessionID) if err != nil { common.Error(c, common.CodeInternalError, err.Error()) return } // 直接返回Python的响应格式 c.JSON(http.StatusOK, data) }