refactor: remove Gorse integration and implement HotRank feature
- Removed Gorse-related configurations, handlers, and dependencies from the codebase. - Introduced HotRank feature with configuration options for ranking posts based on recent activity. - Updated application structure to support HotRank processing, including new caching mechanisms and database interactions. - Cleaned up related DTOs and repository methods to reflect the removal of Gorse and the addition of HotRank functionality.
This commit is contained in:
@@ -1,255 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/config"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/gorse"
|
||||
"carrot_bbs/internal/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gorseio "github.com/gorse-io/gorse-go"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GorseHandler Gorse推荐处理器
|
||||
type GorseHandler struct {
|
||||
importPassword string
|
||||
gorseConfig config.GorseConfig
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGorseHandler 创建Gorse处理器
|
||||
func NewGorseHandler(cfg config.GorseConfig, db *gorm.DB) *GorseHandler {
|
||||
return &GorseHandler{
|
||||
importPassword: cfg.ImportPassword,
|
||||
gorseConfig: cfg,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
zap.L().Error("gorse import failed", zap.Error(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) {
|
||||
hasPassword := h.importPassword != ""
|
||||
response.Success(c, gin.H{
|
||||
"enabled": h.gorseConfig.Enabled,
|
||||
"has_password": hasPassword,
|
||||
})
|
||||
}
|
||||
|
||||
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 := h.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 {
|
||||
zap.L().Warn("get embedding failed for post",
|
||||
zap.String("postID", post.ID),
|
||||
zap.Error(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 {
|
||||
zap.L().Warn("import item failed",
|
||||
zap.String("postID", post.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
stats["items"] = stats["items"].(int) + 1
|
||||
}
|
||||
|
||||
// 导入用户
|
||||
var users []model.User
|
||||
if err := h.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 {
|
||||
zap.L().Warn("import user failed",
|
||||
zap.String("userID", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
stats["users"] = stats["users"].(int) + 1
|
||||
}
|
||||
|
||||
// 导入点赞
|
||||
var likes []model.PostLike
|
||||
if err := h.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 {
|
||||
zap.L().Warn("import like failed",
|
||||
zap.String("userID", like.UserID),
|
||||
zap.String("postID", like.PostID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
stats["likes"] = stats["likes"].(int) + 1
|
||||
}
|
||||
|
||||
// 导入收藏
|
||||
var favorites []model.Favorite
|
||||
if err := h.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 {
|
||||
zap.L().Warn("import favorite failed",
|
||||
zap.String("userID", fav.UserID),
|
||||
zap.String("postID", fav.PostID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
stats["favorites"] = stats["favorites"].(int) + 1
|
||||
}
|
||||
|
||||
// 导入评论(按用户-帖子去重)
|
||||
var comments []model.Comment
|
||||
if err := h.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 {
|
||||
zap.L().Warn("import comment failed",
|
||||
zap.String("userID", cm.UserID),
|
||||
zap.String("postID", cm.PostID),
|
||||
zap.Error(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
|
||||
}
|
||||
@@ -181,9 +181,6 @@ func (h *PostHandler) List(c *gin.Context) {
|
||||
case "hot":
|
||||
// 获取热门帖子
|
||||
posts, total, err = h.postService.GetHotPosts(c.Request.Context(), page, pageSize)
|
||||
case "recommend":
|
||||
// 推荐帖子(从Gorse获取个性化推荐)
|
||||
posts, total, err = h.postService.GetRecommendedPosts(c.Request.Context(), currentUserID, page, pageSize)
|
||||
case "latest":
|
||||
// 最新帖子
|
||||
if userID != "" && userID == currentUserID {
|
||||
@@ -245,9 +242,6 @@ func (h *PostHandler) ListByCursor(c *gin.Context) {
|
||||
case "hot":
|
||||
// 获取热门帖子
|
||||
result, err = h.postService.GetHotPostsByCursor(c.Request.Context(), req)
|
||||
case "recommend":
|
||||
// 推荐帖子(从Gorse获取个性化推荐)
|
||||
result, err = h.postService.GetRecommendedPostsByCursor(c.Request.Context(), currentUserID, req)
|
||||
case "latest":
|
||||
// 最新帖子
|
||||
result, err = h.postService.ListByCursor(c.Request.Context(), userID, includePending, req)
|
||||
|
||||
Reference in New Issue
Block a user