Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
This commit is contained in:
234
internal/handler/gorse_handler.go
Normal file
234
internal/handler/gorse_handler.go
Normal file
@@ -0,0 +1,234 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user