package handlers import ( "crypto/sha256" "encoding/hex" "encoding/json" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/novablog/server/internal/database" "github.com/novablog/server/internal/middleware" "github.com/novablog/server/internal/models" "gorm.io/gorm" ) // MicroHandler 微语处理器 type MicroHandler struct{} // NewMicroHandler 创建微语处理器 func NewMicroHandler() *MicroHandler { return &MicroHandler{} } // CreateMicroRequest 创建微语请求 type CreateMicroRequest struct { Content string `json:"content" binding:"required,min=1,max=2000"` Images []string `json:"images"` Tags []string `json:"tags"` } // CreateMicro 创建微语 func (h *MicroHandler) CreateMicro(c *gin.Context) { userID, isLoggedIn := middleware.GetUserID(c) if !isLoggedIn || userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后再发布"}) return } var req CreateMicroRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 将图片数组转为 JSON 字符串 var imagesJSON string if len(req.Images) > 0 { imagesBytes, _ := json.Marshal(req.Images) imagesJSON = string(imagesBytes) } // 将标签数组转为 JSON 字符串 var tagsJSON string if len(req.Tags) > 0 { tagsBytes, _ := json.Marshal(req.Tags) tagsJSON = string(tagsBytes) } micro := models.Micro{ UserID: userID, Content: req.Content, Images: imagesJSON, Tags: tagsJSON, } if err := database.DB.Create(µ).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "发布失败"}) return } // 加载用户信息 database.DB.Preload("User").First(µ, micro.ID) c.JSON(http.StatusCreated, micro) } // GetMicros 获取微语列表 func (h *MicroHandler) GetMicros(c *gin.Context) { // 分页参数 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 50 { pageSize = 20 } // 标签过滤 tag := c.Query("tag") var micros []models.Micro var total int64 query := database.DB.Model(&models.Micro{}).Where("deleted_at IS NULL") // 如果有标签过滤 if tag != "" { query = query.Where("tags LIKE ?", "%\""+tag+"\"%") } query.Count(&total) if err := query. Preload("User"). Order("created_at DESC"). Offset((page - 1) * pageSize). Limit(pageSize). Find(µs).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) return } // 获取当前用户的点赞状态 userID, isLoggedIn := middleware.GetUserID(c) userLikes := make(map[uint]bool) if isLoggedIn && userID > 0 { var likes []models.MicroLike microIDs := make([]uint, len(micros)) for i, m := range micros { microIDs[i] = m.ID } database.DB.Where("micro_id IN ? AND user_id = ?", microIDs, userID).Find(&likes) for _, like := range likes { userLikes[like.MicroID] = true } } // 构建响应 type MicroResponse struct { models.Micro IsLiked bool `json:"is_liked"` } responses := make([]MicroResponse, len(micros)) for i, m := range micros { responses[i] = MicroResponse{ Micro: m, IsLiked: userLikes[m.ID], } } c.JSON(http.StatusOK, gin.H{ "data": responses, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_page": (total + int64(pageSize) - 1) / int64(pageSize), }, }) } // GetMicro 获取单条微语 func (h *MicroHandler) GetMicro(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) return } var micro models.Micro if err := database.DB.Preload("User").First(µ, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) return } // 获取点赞状态 userID, isLoggedIn := middleware.GetUserID(c) isLiked := false if isLoggedIn && userID > 0 { var like models.MicroLike if err := database.DB.Where("micro_id = ? AND user_id = ?", micro.ID, userID).First(&like).Error; err == nil { isLiked = true } } c.JSON(http.StatusOK, gin.H{ "data": micro, "is_liked": isLiked, }) } // DeleteMicro 删除微语 func (h *MicroHandler) DeleteMicro(c *gin.Context) { userID, _ := middleware.GetUserID(c) role, _ := c.Get("role") idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) return } var micro models.Micro if err := database.DB.First(µ, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) return } // 检查权限:本人或管理员可删除 if micro.UserID != userID && role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "无权限删除"}) return } // 软删除 if err := database.DB.Delete(µ).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) return } c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) } // ToggleMicroLike 切换点赞状态 func (h *MicroHandler) ToggleMicroLike(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) return } // 检查微语是否存在 var micro models.Micro if err := database.DB.First(µ, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) return } userID, isLoggedIn := middleware.GetUserID(c) var existingLike models.MicroLike var likeErr error if isLoggedIn && userID > 0 { // 登录用户:按 user_id 查找 likeErr = database.DB.Where("micro_id = ? AND user_id = ?", id, userID).First(&existingLike).Error } else { // 访客:按 IP Hash 查找 ipHash := getIPHash(c) likeErr = database.DB.Where("micro_id = ? AND ip_hash = ?", id, ipHash).First(&existingLike).Error } if likeErr == gorm.ErrRecordNotFound { // 创建点赞 newLike := models.MicroLike{ MicroID: uint(id), } if isLoggedIn && userID > 0 { newLike.UserID = &userID } else { newLike.IPHash = getIPHash(c) } if err := database.DB.Create(&newLike).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "点赞失败"}) return } // 更新点赞计数 database.DB.Model(µ).Update("like_count", gorm.Expr("like_count + 1")) c.JSON(http.StatusOK, gin.H{ "liked": true, "like_count": micro.LikeCount + 1, }) } else if likeErr == nil { // 取消点赞 if err := database.DB.Delete(&existingLike).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "取消点赞失败"}) return } // 更新点赞计数 newCount := micro.LikeCount - 1 if newCount < 0 { newCount = 0 } database.DB.Model(µ).Update("like_count", newCount) c.JSON(http.StatusOK, gin.H{ "liked": false, "like_count": newCount, }) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"}) return } } // GetMicroHeatmap 获取热力图数据 func (h *MicroHandler) GetMicroHeatmap(c *gin.Context) { yearStr := c.DefaultQuery("year", strconv.Itoa(time.Now().Year())) year, err := strconv.Atoi(yearStr) if err != nil { year = time.Now().Year() } // 计算年份的起止日期 startDate := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) endDate := time.Date(year, 12, 31, 23, 59, 59, 0, time.UTC) // 按日期分组统计 type DayCount struct { Date string `json:"date"` Count int `json:"count"` } var results []DayCount database.DB.Model(&models.Micro{}). Select("DATE(created_at) as date, COUNT(*) as count"). Where("created_at >= ? AND created_at <= ? AND deleted_at IS NULL", startDate, endDate). Group("DATE(created_at)"). Find(&results) // 转换为 map data := make(map[string]int) for _, r := range results { data[r.Date] = r.Count } c.JSON(http.StatusOK, gin.H{ "year": year, "data": data, }) } // GetMicroStats 获取统计数据 func (h *MicroHandler) GetMicroStats(c *gin.Context) { var totalMicros int64 var totalLikes int64 var totalComments int64 var monthMicros int64 // 总微语数 database.DB.Model(&models.Micro{}).Count(&totalMicros) // 总点赞数 database.DB.Model(&models.MicroLike{}).Count(&totalLikes) // 总评论数 database.DB.Model(&models.MicroComment{}).Count(&totalComments) // 本月发布数 now := time.Now() monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) database.DB.Model(&models.Micro{}).Where("created_at >= ? AND deleted_at IS NULL", monthStart).Count(&monthMicros) c.JSON(http.StatusOK, gin.H{ "total_micros": totalMicros, "total_likes": totalLikes, "total_comments": totalComments, "month_micros": monthMicros, }) } // GetMicroTags 获取热门标签 func (h *MicroHandler) GetMicroTags(c *gin.Context) { // 获取所有微语的标签 var micros []models.Micro database.DB.Where("tags IS NOT NULL AND tags != '' AND deleted_at IS NULL").Select("tags").Find(µs) // 统计标签使用次数 tagCount := make(map[string]int) for _, micro := range micros { if micro.Tags == "" { continue } var tags []string if err := json.Unmarshal([]byte(micro.Tags), &tags); err == nil { for _, tag := range tags { tagCount[tag]++ } } } // 转换为排序后的列表 type TagItem struct { Name string `json:"name"` Count int `json:"count"` } var tags []TagItem for name, count := range tagCount { tags = append(tags, TagItem{Name: name, Count: count}) } // 按使用次数排序 for i := 0; i < len(tags); i++ { for j := i + 1; j < len(tags); j++ { if tags[j].Count > tags[i].Count { tags[i], tags[j] = tags[j], tags[i] } } } // 只返回前20个 if len(tags) > 20 { tags = tags[:20] } c.JSON(http.StatusOK, gin.H{ "tags": tags, }) } // getIPHash 获取 IP 的哈希值 func getIPHash(c *gin.Context) string { ip := c.ClientIP() hash := sha256.Sum256([]byte(ip + "micro-salt")) return hex.EncodeToString(hash[:]) } // CreateMicroCommentRequest 创建微语评论请求 type CreateMicroCommentRequest struct { MicroID uint `json:"micro_id" binding:"required"` ParentID *uint `json:"parent_id"` Content string `json:"content" binding:"required,min=1,max=2000"` } // CreateMicroComment 创建微语评论 func (h *MicroHandler) CreateMicroComment(c *gin.Context) { userID, isLoggedIn := middleware.GetUserID(c) if !isLoggedIn || userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后再评论"}) return } var req CreateMicroCommentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 检查微语是否存在 var micro models.Micro if err := database.DB.First(µ, req.MicroID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) return } comment := models.MicroComment{ MicroID: req.MicroID, UserID: userID, ParentID: req.ParentID, Content: req.Content, } if err := database.DB.Create(&comment).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "评论失败"}) return } // 更新评论计数 database.DB.Model(µ).Update("comment_count", gorm.Expr("comment_count + 1")) // 加载用户信息 database.DB.Preload("User").First(&comment, comment.ID) c.JSON(http.StatusCreated, comment) } // GetMicroComments 获取微语评论列表 func (h *MicroHandler) GetMicroComments(c *gin.Context) { microIDStr := c.Query("micro_id") if microIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "micro_id is required"}) return } microID, err := strconv.ParseUint(microIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid micro_id"}) return } // 分页参数 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } // 获取顶级评论(非回复) var comments []models.MicroComment var total int64 query := database.DB.Model(&models.MicroComment{}). Where("micro_id = ? AND parent_id IS NULL", microID) query.Count(&total) if err := query. Preload("User"). Preload("Replies.User"). Order("created_at DESC"). Offset((page - 1) * pageSize). Limit(pageSize). Find(&comments).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论失败"}) return } // 手动加载每个评论的用户信息 for i := range comments { if comments[i].UserID > 0 { var user models.User if err := database.DB.First(&user, comments[i].UserID).Error; err == nil { comments[i].User = user } } // 加载回复的用户信息 for j := range comments[i].Replies { if comments[i].Replies[j].UserID > 0 { var replyUser models.User if err := database.DB.First(&replyUser, comments[i].Replies[j].UserID).Error; err == nil { comments[i].Replies[j].User = replyUser } } } } c.JSON(http.StatusOK, gin.H{ "data": comments, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_page": (total + int64(pageSize) - 1) / int64(pageSize), }, }) } // DeleteMicroComment 删除微语评论 func (h *MicroHandler) DeleteMicroComment(c *gin.Context) { userID, _ := middleware.GetUserID(c) role, _ := c.Get("role") idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid comment id"}) return } var comment models.MicroComment if err := database.DB.First(&comment, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } // 检查权限:本人或管理员可删除 if comment.UserID != userID && role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) return } // 软删除 if err := database.DB.Delete(&comment).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) return } // 更新评论计数 database.DB.Model(&models.Micro{}).Where("id = ?", comment.MicroID). Update("comment_count", gorm.Expr("comment_count - 1")) c.JSON(http.StatusOK, gin.H{"message": "评论已删除"}) }