diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 7701289..835b467 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -33,7 +33,6 @@ func main() { authHandler := handlers.NewAuthHandler(jwtManager) commentHandler := handlers.NewCommentHandler() likeHandler := handlers.NewLikeHandler() - microHandler := handlers.NewMicroHandler() // 创建路由 r := gin.New() @@ -71,15 +70,6 @@ func main() { api.GET("/likes", likeHandler.GetLikeStatus) api.POST("/likes", likeHandler.ToggleLike) // 允许访客点赞(基于 IP Hash) - // 微语公开接口 - api.GET("/micro", microHandler.GetMicros) - api.GET("/micro/stats", microHandler.GetMicroStats) - api.GET("/micro/heatmap", microHandler.GetMicroHeatmap) - api.GET("/micro/tags", microHandler.GetMicroTags) - api.GET("/micro/:id", microHandler.GetMicro) - api.POST("/micro/:id/like", microHandler.ToggleMicroLike) // 允许访客点赞 - api.GET("/micro-comments", microHandler.GetMicroComments) // 获取微语评论 - // 需要认证的接口 authGroup := api.Group("") authGroup.Use(middleware.AuthMiddleware(jwtManager)) @@ -91,14 +81,6 @@ func main() { // 评论相关(需要登录才能评论) authGroup.POST("/comments", commentHandler.CreateComment) authGroup.DELETE("/comments/:id", commentHandler.DeleteComment) - - // 微语相关(需要登录才能发布和删除) - authGroup.POST("/micro", microHandler.CreateMicro) - authGroup.DELETE("/micro/:id", microHandler.DeleteMicro) - - // 微语评论相关 - authGroup.POST("/micro-comments", microHandler.CreateMicroComment) - authGroup.DELETE("/micro-comments/:id", microHandler.DeleteMicroComment) } // 管理员接口 diff --git a/server/internal/database/database.go b/server/internal/database/database.go index f0c853f..c7da001 100644 --- a/server/internal/database/database.go +++ b/server/internal/database/database.go @@ -54,9 +54,6 @@ func autoMigrate() error { &models.Like{}, &models.LikeCount{}, &models.PostMeta{}, - &models.Micro{}, - &models.MicroLike{}, - &models.MicroComment{}, ) } diff --git a/server/internal/handlers/micro.go b/server/internal/handlers/micro.go deleted file mode 100644 index acc1220..0000000 --- a/server/internal/handlers/micro.go +++ /dev/null @@ -1,595 +0,0 @@ -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": "评论已删除"}) -} \ No newline at end of file diff --git a/server/internal/models/models.go b/server/internal/models/models.go index 8e0769c..87829e1 100644 --- a/server/internal/models/models.go +++ b/server/internal/models/models.go @@ -62,41 +62,3 @@ type PostMeta struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } - -// Micro 微语模型 -type Micro struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` - UserID uint `json:"user_id" gorm:"index;not null"` - Content string `json:"content" gorm:"type:text;not null"` - Images string `json:"images" gorm:"type:text"` // JSON array of image URLs - Tags string `json:"tags" gorm:"type:text"` // JSON array of tags - LikeCount int `json:"like_count" gorm:"default:0"` - CommentCount int `json:"comment_count" gorm:"default:0"` - User User `json:"user" gorm:"foreignKey:UserID"` -} - -// MicroLike 微语点赞模型 -type MicroLike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - MicroID uint `json:"micro_id" gorm:"uniqueIndex:idx_micro_user;not null"` - UserID *uint `json:"user_id" gorm:"uniqueIndex:idx_micro_user;index"` - IPHash string `json:"-" gorm:"uniqueIndex:idx_micro_ip;size:64"` -} - -// MicroComment 微语评论模型 -type MicroComment struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` - MicroID uint `json:"micro_id" gorm:"index;not null"` - UserID uint `json:"user_id" gorm:"index;not null"` - ParentID *uint `json:"parent_id" gorm:"index"` - Content string `json:"content" gorm:"type:text;not null"` - User User `json:"user" gorm:"foreignKey:UserID"` - Replies []MicroComment `json:"replies,omitempty" gorm:"foreignKey:ParentID"` -} diff --git a/src/components/HeatmapCalendar.vue b/src/components/HeatmapCalendar.vue deleted file mode 100644 index 9be9a28..0000000 --- a/src/components/HeatmapCalendar.vue +++ /dev/null @@ -1,230 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/MicroCommentSection.vue b/src/components/MicroCommentSection.vue deleted file mode 100644 index 92e34a0..0000000 --- a/src/components/MicroCommentSection.vue +++ /dev/null @@ -1,285 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/MicroFeed.vue b/src/components/MicroFeed.vue deleted file mode 100644 index 9ac891a..0000000 --- a/src/components/MicroFeed.vue +++ /dev/null @@ -1,518 +0,0 @@ - - - \ No newline at end of file diff --git a/src/components/MicroPost.vue b/src/components/MicroPost.vue deleted file mode 100644 index e00955b..0000000 --- a/src/components/MicroPost.vue +++ /dev/null @@ -1,352 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/MicroSidebar.vue b/src/components/MicroSidebar.vue deleted file mode 100644 index c6ea8f8..0000000 --- a/src/components/MicroSidebar.vue +++ /dev/null @@ -1,191 +0,0 @@ - - - \ No newline at end of file diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index bee5cfd..3570113 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -112,7 +112,6 @@ const socialImageURL = image.startsWith('http') ? image : new URL(image, site).h