refactor: remove Gorse integration and implement HotRank feature
Some checks failed
Build Backend / build-docker (push) Has been cancelled
Build Backend / build (push) Has been cancelled

- 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:
lafay
2026-03-24 05:18:30 +08:00
parent b41567a39a
commit 176cd20847
32 changed files with 735 additions and 1128 deletions

View File

@@ -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
}

View File

@@ -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)