Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
234 lines
6.1 KiB
Go
234 lines
6.1 KiB
Go
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
|
|
} |