This commit is contained in:
sjk
2026-01-06 19:36:42 +08:00
parent 15b579d64a
commit 19942144fb
261 changed files with 24034 additions and 5477 deletions

View File

@@ -2,7 +2,10 @@ package controller
import (
"ai_xhs/common"
"ai_xhs/config"
"ai_xhs/service"
"ai_xhs/utils"
"context"
"github.com/gin-gonic/gin"
)
@@ -98,3 +101,137 @@ func (ctrl *AuthController) PhoneLogin(c *gin.Context) {
},
})
}
// PhonePasswordLogin 手机号密码登录
func (ctrl *AuthController) PhonePasswordLogin(c *gin.Context) {
var req struct {
Phone string `json:"phone" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
return
}
// 调用手机号密码登录服务
token, employee, err := ctrl.authService.PhonePasswordLogin(req.Phone, req.Password)
if err != nil {
common.Error(c, common.CodeServerError, err.Error())
return
}
// 获取用户显示名称(优先使用真实姓名,其次用户名)
displayName := employee.RealName
if displayName == "" {
displayName = employee.Username
}
common.SuccessWithMessage(c, "登录成功", gin.H{
"token": token,
"employee": gin.H{
"id": employee.ID,
"name": displayName,
"username": employee.Username,
"real_name": employee.RealName,
"phone": employee.Phone,
"role": employee.Role,
"enterprise_id": employee.EnterpriseID,
"enterprise_name": employee.EnterpriseName,
"is_bound_xhs": employee.IsBoundXHS,
},
})
}
// XHSPhoneCodeLogin 小红书手机号验证码登录
func (ctrl *AuthController) XHSPhoneCodeLogin(c *gin.Context) {
var req struct {
Phone string `json:"phone" binding:"required"`
Code string `json:"code" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
return
}
// 调用手机号验证码登录服务
token, employee, err := ctrl.authService.XHSPhoneCodeLogin(req.Phone, req.Code)
if err != nil {
common.Error(c, common.CodeServerError, err.Error())
return
}
// 获取用户显示名称(优先使用真实姓名,其次用户名)
displayName := employee.RealName
if displayName == "" {
displayName = employee.Username
}
common.SuccessWithMessage(c, "登录成功", gin.H{
"token": token,
"employee": gin.H{
"id": employee.ID,
"name": displayName,
"username": employee.Username,
"real_name": employee.RealName,
"phone": employee.Phone,
"role": employee.Role,
"enterprise_id": employee.EnterpriseID,
"enterprise_name": employee.EnterpriseName,
"is_bound_xhs": employee.IsBoundXHS,
},
})
}
// SendXHSVerificationCode 发送小红书手机号验证码
func (ctrl *AuthController) SendXHSVerificationCode(c *gin.Context) {
var req struct {
Phone string `json:"phone" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
return
}
// 预检查验证手机号是否存在于user表中
if err := ctrl.authService.CheckPhoneExists(req.Phone); err != nil {
common.Error(c, common.CodeServerError, err.Error())
return
}
// 调用短信服务发送验证码
smsService := service.GetSmsService()
code, err := smsService.SendVerificationCode(req.Phone)
if err != nil {
common.Error(c, common.CodeServerError, err.Error())
return
}
// 开发环境返回验证码,生产环境不返回
response := gin.H{
"message": "验证码已发送5分钟内有效",
}
if config.AppConfig.Server.Mode == "debug" {
response["code"] = code // 仅开发环境返回
}
common.SuccessWithMessage(c, "验证码已发送", response)
}
// Logout 退出登录删除Redis中的Token
func (ctrl *AuthController) Logout(c *gin.Context) {
employeeID := c.GetInt("employee_id")
// 从Redis删除token
ctx := context.Background()
if err := utils.RevokeToken(ctx, employeeID); err != nil {
// 即使删除失败也返回成功因为token有过期时间
common.SuccessWithMessage(c, "退出成功", nil)
return
}
common.SuccessWithMessage(c, "退出成功", nil)
}

View File

@@ -2,8 +2,17 @@ package controller
import (
"ai_xhs/common"
"ai_xhs/database"
"ai_xhs/models"
"ai_xhs/service"
"ai_xhs/utils"
"bytes"
"context"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
@@ -29,7 +38,10 @@ func (ctrl *EmployeeController) SendXHSCode(c *gin.Context) {
return
}
err := ctrl.service.SendXHSCode(req.XHSPhone)
// 获取当前登录用户ID
employeeID := c.GetInt("employee_id")
err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID)
if err != nil {
common.Error(c, common.CodeInternalError, err.Error())
return
@@ -59,24 +71,149 @@ func (ctrl *EmployeeController) GetProfile(c *gin.Context) {
"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,
"xhs_account": employee.XHSAccount,
"xhs_phone": employee.XHSPhone,
"has_xhs_cookie": employee.XHSCookie != "", // 标识是否有Cookie不返回完整Cookie
}
if employee.BoundAt != nil {
data["bound_at"] = employee.BoundAt.Format("2006-01-02 15:04:05")
// 如果已绑定,从 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)
}
// BindXHS 绑定小红书账号
// 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")
@@ -90,17 +227,31 @@ func (ctrl *EmployeeController) BindXHS(c *gin.Context) {
return
}
xhsAccount, err := ctrl.service.BindXHS(employeeID, req.XHSPhone, req.Code)
_, err := ctrl.service.BindXHS(employeeID, req.XHSPhone, req.Code)
if err != nil {
common.Error(c, common.CodeBindXHSFailed, err.Error())
return
}
common.SuccessWithMessage(c, "绑定成功", map[string]interface{}{
"xhs_account": xhsAccount,
// 立即返回成功,告知前端正在处理
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")
@@ -246,14 +397,24 @@ func (ctrl *EmployeeController) CheckXHSStatus(c *gin.Context) {
// GetProducts 获取产品列表
func (ctrl *EmployeeController) GetProducts(c *gin.Context) {
data, err := ctrl.service.GetProducts()
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,
"list": data,
"has_more": hasMore,
})
}
@@ -294,3 +455,294 @@ func (ctrl *EmployeeController) UpdateArticleStatus(c *gin.Context) {
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(&currentUser).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)
}

View File

@@ -0,0 +1,104 @@
package controller
import (
"ai_xhs/common"
"ai_xhs/models"
"ai_xhs/service"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// CreateFeedbackRequest 创建反馈请求
type CreateFeedbackRequest struct {
FeedbackType string `json:"feedback_type" binding:"required"`
Description string `json:"description" binding:"required,max=500"`
ContactInfo string `json:"contact_info"`
Nickname string `json:"nickname"`
}
// FeedbackController 反馈控制器
type FeedbackController struct {
feedbackService *service.FeedbackService
}
// NewFeedbackController 创建反馈控制器
func NewFeedbackController(feedbackService *service.FeedbackService) *FeedbackController {
return &FeedbackController{
feedbackService: feedbackService,
}
}
// CreateFeedback 创建反馈
func (fc *FeedbackController) CreateFeedback(c *gin.Context) {
var req CreateFeedbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
return
}
// 从上下文获取员工ID
employeeID, exists := c.Get("employee_id")
if !exists {
common.Error(c, common.CodeUnauthorized, "未登录")
return
}
feedback := &models.Feedback{
FeedbackType: req.FeedbackType,
Description: req.Description,
ContactInfo: req.ContactInfo,
Nickname: req.Nickname,
CreatedUserID: employeeID.(int),
Status: "待处理",
}
if err := fc.feedbackService.CreateFeedback(feedback); err != nil {
common.Error(c, common.CodeInternalError, "提交反馈失败: "+err.Error())
return
}
common.SuccessWithMessage(c, "反馈提交成功", feedback)
}
// GetFeedbackList 获取反馈列表
func (fc *FeedbackController) GetFeedbackList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
feedbackType := c.Query("feedback_type")
status := c.Query("status")
// 从上下文获取员工ID仅查看自己的反馈
employeeID, exists := c.Get("employee_id")
if !exists {
common.Error(c, common.CodeUnauthorized, "未登录")
return
}
feedbacks, total, err := fc.feedbackService.GetFeedbackList(employeeID.(int), page, pageSize, feedbackType, status)
if err != nil {
common.Error(c, common.CodeInternalError, "获取反馈列表失败: "+err.Error())
return
}
c.JSON(http.StatusOK, common.SuccessResponseWithPage(feedbacks, total, page, pageSize, "获取成功"))
}
// GetFeedbackDetail 获取反馈详情
func (fc *FeedbackController) GetFeedbackDetail(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
common.Error(c, common.CodeInvalidParams, "无效的反馈ID")
return
}
feedback, err := fc.feedbackService.GetFeedbackByID(id)
if err != nil {
common.Error(c, common.CodeNotFound, "反馈不存在")
return
}
common.SuccessWithMessage(c, "获取成功", feedback)
}