package handler import ( "context" "log" "strings" "time" "carrot_bbs/internal/config" "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/gorse" "carrot_bbs/internal/pkg/response" gorseio "github.com/gorse-io/gorse-go" "github.com/gin-gonic/gin" ) // GorseHandler Gorse推荐处理器 type GorseHandler struct { importPassword string gorseConfig config.GorseConfig } // NewGorseHandler 创建Gorse处理器 func NewGorseHandler(cfg config.GorseConfig) *GorseHandler { return &GorseHandler{ importPassword: cfg.ImportPassword, gorseConfig: cfg, } } // ImportRequest 导入请求 type ImportRequest struct { Password string `json:"password"` } // ImportData 导入数据到Gorse // POST /api/v1/gorse/import func (h *GorseHandler) ImportData(c *gin.Context) { // 验证密码 if h.importPassword == "" { response.BadRequest(c, "Gorse import is disabled") return } var req ImportRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "invalid request body") return } if req.Password != h.importPassword { response.Unauthorized(c, "invalid password") return } ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute) defer cancel() stats, err := h.importAllData(ctx) if err != nil { log.Printf("[ERROR] gorse import failed: %v", err) response.InternalServerError(c, "gorse import failed: "+err.Error()) return } response.Success(c, gin.H{ "message": "import completed", "status": "done", "stats": stats, }) } // GetStatus 获取Gorse状态 // GET /api/v1/gorse/status func (h *GorseHandler) GetStatus(c *gin.Context) { // 返回Gorse连接状态和配置信息 hasPassword := h.importPassword != "" response.Success(c, gin.H{ "enabled": h.gorseConfig.Enabled, "has_password": hasPassword, "address": h.gorseConfig.Address, "api_key": strings.Repeat("*", 8), // 不返回实际APIKey }) } func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) { gorseClient := gorseio.NewGorseClient(h.gorseConfig.Address, h.gorseConfig.APIKey) gorse.InitEmbeddingWithConfig(h.gorseConfig.EmbeddingAPIKey, h.gorseConfig.EmbeddingURL, h.gorseConfig.EmbeddingModel) stats := gin.H{ "items": 0, "users": 0, "likes": 0, "favorites": 0, "comments": 0, } // 导入帖子 var posts []model.Post if err := model.DB.Find(&posts).Error; err != nil { return nil, err } for _, post := range posts { embedding, err := gorse.GetEmbedding(strings.TrimSpace(post.Title + " " + post.Content)) if err != nil { log.Printf("[WARN] get embedding failed for post %s: %v", post.ID, err) embedding = make([]float64, 1024) } _, err = gorseClient.InsertItem(ctx, gorseio.Item{ ItemId: post.ID, IsHidden: post.DeletedAt.Valid, Categories: buildPostCategories(&post), Comment: post.Title, Timestamp: post.CreatedAt.UTC().Truncate(time.Second), Labels: map[string]any{ "embedding": embedding, }, }) if err != nil { log.Printf("[WARN] import item failed (%s): %v", post.ID, err) continue } stats["items"] = stats["items"].(int) + 1 } // 导入用户 var users []model.User if err := model.DB.Find(&users).Error; err != nil { return nil, err } for _, user := range users { _, err := gorseClient.InsertUser(ctx, gorseio.User{ UserId: user.ID, Labels: map[string]any{ "posts_count": user.PostsCount, "followers_count": user.FollowersCount, "following_count": user.FollowingCount, }, Comment: user.Nickname, }) if err != nil { log.Printf("[WARN] import user failed (%s): %v", user.ID, err) continue } stats["users"] = stats["users"].(int) + 1 } // 导入点赞 var likes []model.PostLike if err := model.DB.Find(&likes).Error; err != nil { return nil, err } for _, like := range likes { _, err := gorseClient.InsertFeedback(ctx, []gorseio.Feedback{{ FeedbackType: string(gorse.FeedbackTypeLike), UserId: like.UserID, ItemId: like.PostID, Timestamp: like.CreatedAt.UTC().Truncate(time.Second), }}) if err != nil { log.Printf("[WARN] import like failed (%s/%s): %v", like.UserID, like.PostID, err) continue } stats["likes"] = stats["likes"].(int) + 1 } // 导入收藏 var favorites []model.Favorite if err := model.DB.Find(&favorites).Error; err != nil { return nil, err } for _, fav := range favorites { _, err := gorseClient.InsertFeedback(ctx, []gorseio.Feedback{{ FeedbackType: string(gorse.FeedbackTypeStar), UserId: fav.UserID, ItemId: fav.PostID, Timestamp: fav.CreatedAt.UTC().Truncate(time.Second), }}) if err != nil { log.Printf("[WARN] import favorite failed (%s/%s): %v", fav.UserID, fav.PostID, err) continue } stats["favorites"] = stats["favorites"].(int) + 1 } // 导入评论(按用户-帖子去重) var comments []model.Comment if err := model.DB.Where("status = ?", model.CommentStatusPublished).Find(&comments).Error; err != nil { return nil, err } seen := make(map[string]struct{}) for _, cm := range comments { key := cm.UserID + ":" + cm.PostID if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} _, err := gorseClient.InsertFeedback(ctx, []gorseio.Feedback{{ FeedbackType: string(gorse.FeedbackTypeComment), UserId: cm.UserID, ItemId: cm.PostID, Timestamp: cm.CreatedAt.UTC().Truncate(time.Second), }}) if err != nil { log.Printf("[WARN] import comment failed (%s/%s): %v", cm.UserID, cm.PostID, err) continue } stats["comments"] = stats["comments"].(int) + 1 } return stats, nil } func buildPostCategories(post *model.Post) []string { var categories []string if post.ViewsCount > 1000 { categories = append(categories, "hot_high") } else if post.ViewsCount > 100 { categories = append(categories, "hot_medium") } if post.LikesCount > 100 { categories = append(categories, "likes_100+") } else if post.LikesCount > 10 { categories = append(categories, "likes_10+") } age := time.Since(post.CreatedAt) if age < 24*time.Hour { categories = append(categories, "today") } else if age < 7*24*time.Hour { categories = append(categories, "this_week") } return categories }