From 4901557217ed4661ff7cb6a333c412ec417fe3c2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 30 Mar 2025 13:15:00 +0800 Subject: [PATCH] :recycle: Globally apply the V2 api --- .idea/.gitignore | 8 -- .idea/Interactive.iml | 9 -- .idea/dataSources.xml | 12 -- .idea/inspectionProfiles/Project_Default.xml | 6 - .idea/modules.xml | 8 -- .idea/vcs.xml | 6 - pkg/internal/http/api/activitypub_api.go | 2 +- pkg/internal/http/api/index.go | 1 - pkg/internal/http/api/posts_api.go | 105 +++++++------- pkg/internal/http/api/publishers_api.go | 2 +- pkg/internal/http/api/recommendation_api.go | 6 +- pkg/internal/http/api/replies_api.go | 4 +- pkg/internal/http/api/what_new_api.dart.go | 2 +- pkg/internal/services/feed.go | 6 +- pkg/internal/services/posts.go | 112 +++++++-------- pkg/internal/services/queries/posts.go | 137 +++++++++++++------ 16 files changed, 201 insertions(+), 225 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/Interactive.iml delete mode 100644 .idea/dataSources.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410ca..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/Interactive.iml b/.idea/Interactive.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/Interactive.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index c4444d9..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/hy_interactive - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 03d9549..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 79e159c..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pkg/internal/http/api/activitypub_api.go b/pkg/internal/http/api/activitypub_api.go index 5972334..c182025 100644 --- a/pkg/internal/http/api/activitypub_api.go +++ b/pkg/internal/http/api/activitypub_api.go @@ -65,7 +65,7 @@ func apUserOutbox(c *fiber.Ctx) error { } var activities []activitypub.Item - if posts, err := services.ListPost(tx, limit, (page-1)*limit, "published_at DESC", nil); err != nil { + if posts, err := services.ListPostV1(tx, limit, (page-1)*limit, "published_at DESC", nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } else { for _, post := range posts { diff --git a/pkg/internal/http/api/index.go b/pkg/internal/http/api/index.go index fd02cd2..28ebeba 100644 --- a/pkg/internal/http/api/index.go +++ b/pkg/internal/http/api/index.go @@ -59,7 +59,6 @@ func MapControllers(app *fiber.App, baseURL string) { posts := api.Group("/posts").Name("Posts API") { posts.Get("/", listPost) - posts.Get("/v2", listPostV2) posts.Get("/search", searchPost) posts.Get("/minimal", listPostMinimal) posts.Get("/drafts", listDraftPost) diff --git a/pkg/internal/http/api/posts_api.go b/pkg/internal/http/api/posts_api.go index 5e71a6b..956cb0f 100644 --- a/pkg/internal/http/api/posts_api.go +++ b/pkg/internal/http/api/posts_api.go @@ -26,17 +26,26 @@ func getPost(c *fiber.Ctx) error { var item models.Post var err error - tx := database.C + var userId *uint + if user, authenticated := c.Locals("user").(authm.Account); authenticated { + userId = &user.ID + } + tx := database.C if tx, err = services.UniversalPostFilter(c, tx, services.UniversalPostFilterConfig{ - ShowReply: true, - ShowDraft: true, + ShowReply: true, + ShowDraft: true, + ShowCollapsed: true, }); err != nil { return err } if numericId, paramErr := strconv.Atoi(id); paramErr == nil { - item, err = services.GetPost(tx, uint(numericId)) + if c.Get("X-API-Version", "1") == "2" { + item, err = queries.GetPost(tx, uint(numericId), userId) + } else { + item, err = services.GetPost(tx, uint(numericId)) + } } else { segments := strings.Split(id, ":") if len(segments) != 2 { @@ -44,7 +53,11 @@ func getPost(c *fiber.Ctx) error { } area := segments[0] alias := segments[1] - item, err = services.GetPostByAlias(tx, alias, area) + if c.Get("X-API-Version", "1") == "2" { + item, err = queries.GetPostByAlias(tx, alias, area, userId) + } else { + item, err = services.GetPostByAlias(tx, alias, area) + } } if err != nil { @@ -88,22 +101,27 @@ func searchPost(c *fiber.Ctx) error { userId = &user.ID } + var count int64 countTx := tx - count, err := services.CountPost(countTx) + count, err = services.CountPost(countTx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset, "published_at DESC", userId) + var items []models.Post + + if c.Get("X-API-Version", "1") == "2" { + items, err = queries.ListPost(tx, take, offset, "published_at DESC", userId) + } else { + items, err = services.ListPostV1(tx, take, offset, "published_at DESC", userId) + } if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } if c.QueryBool("truncate", true) { for _, item := range items { - if item != nil { - item = lo.ToPtr(services.TruncatePostContent(*item)) - } + item = services.TruncatePostContent(item) } } @@ -129,54 +147,20 @@ func listPost(c *fiber.Ctx) error { userId = &user.ID } + var count int64 countTx := tx - count, err := services.CountPost(countTx) + count, err = services.CountPost(countTx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset, "published_at DESC", userId) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + var items []models.Post + + if c.Get("X-API-Version", "1") == "2" { + items, err = queries.ListPost(tx, take, offset, "published_at DESC", userId) + } else { + items, err = services.ListPostV1(tx, take, offset, "published_at DESC", userId) } - - if c.QueryBool("truncate", true) { - for _, item := range items { - if item != nil { - item = lo.ToPtr(services.TruncatePostContent(*item)) - } - } - } - - return c.JSON(fiber.Map{ - "count": count, - "data": items, - }) -} - -func listPostV2(c *fiber.Ctx) error { - take := c.QueryInt("take", 10) - offset := c.QueryInt("offset", 0) - - tx := database.C - - var err error - if tx, err = services.UniversalPostFilter(c, tx); err != nil { - return err - } - - var userId *uint - if user, authenticated := c.Locals("user").(authm.Account); authenticated { - userId = &user.ID - } - - countTx := tx - count, err := services.CountPost(countTx) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - items, err := queries.ListPostV2(tx, take, offset, "published_at DESC", userId) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -238,6 +222,7 @@ func listDraftPost(c *fiber.Ctx) error { } user := c.Locals("user").(authm.Account) + var err error tx := services.FilterPostWithAuthorDraft(database.C, user.ID) var userId *uint @@ -245,21 +230,27 @@ func listDraftPost(c *fiber.Ctx) error { userId = &user.ID } - count, err := services.CountPost(tx) + var count int64 + countTx := tx + count, err = services.CountPost(countTx) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset, "created_at DESC", userId, true) + var items []models.Post + + if c.Get("X-API-Version", "1") == "2" { + items, err = queries.ListPost(tx, take, offset, "published_at DESC", userId) + } else { + items, err = services.ListPostV1(tx, take, offset, "published_at DESC", userId) + } if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } if c.QueryBool("truncate", true) { for _, item := range items { - if item != nil { - item = lo.ToPtr(services.TruncatePostContent(*item)) - } + item = services.TruncatePostContent(item) } } diff --git a/pkg/internal/http/api/publishers_api.go b/pkg/internal/http/api/publishers_api.go index 0ac4d1f..a2c8d1d 100644 --- a/pkg/internal/http/api/publishers_api.go +++ b/pkg/internal/http/api/publishers_api.go @@ -33,7 +33,7 @@ func listPinnedPost(c *fiber.Ctx) error { userId = &user.ID } - items, err := services.ListPost(tx, 100, 0, "published_at DESC", userId) + items, err := services.ListPostV1(tx, 100, 0, "published_at DESC", userId) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/pkg/internal/http/api/recommendation_api.go b/pkg/internal/http/api/recommendation_api.go index 7cdd204..cc6a071 100644 --- a/pkg/internal/http/api/recommendation_api.go +++ b/pkg/internal/http/api/recommendation_api.go @@ -29,7 +29,7 @@ func listRecommendation(c *fiber.Ctx) error { } tx := database.C.Where("id IN ?", postIdx) - newPosts, err := services.ListPost(tx, featuredMax, 0, "id ASC", userId) + newPosts, err := services.ListPostV1(tx, featuredMax, 0, "id ASC", userId) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -67,7 +67,7 @@ func listRecommendationShuffle(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset, "RANDOM()", userId) + items, err := services.ListPostV1(tx, take, offset, "RANDOM()", userId) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -92,7 +92,7 @@ func getRecommendationFeed(c *fiber.Ctx) error { var cursorTime *time.Time if cursor > 0 { - cursorTime = lo.ToPtr(time.UnixMilli(int64(cursor) + 1)) + cursorTime = lo.ToPtr(time.UnixMilli(int64(cursor - 1))) } var userId *uint diff --git a/pkg/internal/http/api/replies_api.go b/pkg/internal/http/api/replies_api.go index c0b73da..25d13e9 100644 --- a/pkg/internal/http/api/replies_api.go +++ b/pkg/internal/http/api/replies_api.go @@ -47,7 +47,7 @@ func listPostReplies(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset, "published_at DESC", userId) + items, err := services.ListPostV1(tx, take, offset, "published_at DESC", userId) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -90,7 +90,7 @@ func listPostFeaturedReply(c *fiber.Ctx) error { tx = services.FilterPostWithTag(tx, c.Query("tag")) } - items, err := services.ListPost(tx, take, 0, "(COALESCE(total_upvote, 0) - COALESCE(total_downvote, 0)) DESC, published_at DESC", userId) + items, err := services.ListPostV1(tx, take, 0, "(COALESCE(total_upvote, 0) - COALESCE(total_downvote, 0)) DESC, published_at DESC", userId) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/pkg/internal/http/api/what_new_api.dart.go b/pkg/internal/http/api/what_new_api.dart.go index 3431f82..e7b9e05 100644 --- a/pkg/internal/http/api/what_new_api.dart.go +++ b/pkg/internal/http/api/what_new_api.dart.go @@ -40,7 +40,7 @@ func getWhatsNew(c *fiber.Ctx) error { order = "published_at DESC, (COALESCE(total_upvote, 0) - COALESCE(total_downvote, 0)) DESC" } - items, err := services.ListPost(tx, 10, 0, order, userId) + items, err := services.ListPostV1(tx, 10, 0, order, userId) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/pkg/internal/services/feed.go b/pkg/internal/services/feed.go index 48b8000..1ff8b80 100644 --- a/pkg/internal/services/feed.go +++ b/pkg/internal/services/feed.go @@ -79,14 +79,14 @@ func GetFeed(c *fiber.Ctx, limit int, user *uint, cursor *time.Time) ([]FeedEntr // Only manage to pulling the content only func ListPostForFeed(tx *gorm.DB, limit int, user *uint) ([]FeedEntry, error) { - posts, err := ListPost(tx, limit, -1, "published_at DESC", user) + posts, err := ListPostV1(tx, limit, -1, "published_at DESC", user) if err != nil { return nil, err } - entries := lo.Map(posts, func(post *models.Post, _ int) FeedEntry { + entries := lo.Map(posts, func(post models.Post, _ int) FeedEntry { return FeedEntry{ Type: "interactive.post", - Data: TruncatePostContent(*post), + Data: TruncatePostContent(post), CreatedAt: post.CreatedAt, } }) diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index 149f3e5..a0bcd09 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -339,14 +339,7 @@ func PreloadGeneral(tx *gorm.DB) *gorm.DB { Preload("Tags"). Preload("Categories"). Preload("Publisher"). - Preload("ReplyTo"). - Preload("ReplyTo.Publisher"). - Preload("ReplyTo.Tags"). - Preload("ReplyTo.Categories"). - Preload("RepostTo"). - Preload("RepostTo.Publisher"). - Preload("RepostTo.Tags"). - Preload("RepostTo.Categories") + Preload("Poll") } func GetPost(tx *gorm.DB, id uint) (models.Post, error) { @@ -403,7 +396,7 @@ func CountPostReactions(id uint) int64 { return count } -func ListPost(tx *gorm.DB, take int, offset int, order any, user *uint, noReact ...bool) ([]*models.Post, error) { +func ListPostV1(tx *gorm.DB, take int, offset int, order any, user *uint, noReact ...bool) ([]models.Post, error) { if take > 100 { take = 100 } @@ -415,77 +408,64 @@ func ListPost(tx *gorm.DB, take int, offset int, order any, user *uint, noReact tx = tx.Offset(offset) } - var items []*models.Post - if err := PreloadGeneral(tx). - Order(order). - Find(&items).Error; err != nil { - return items, err + tx = tx.Preload("Tags"). + Preload("Categories"). + Preload("Publisher") + + // Fetch posts + var posts []models.Post + if err := tx.Order(order).Find(&posts).Error; err != nil { + return nil, err } - idx := lo.Map(items, func(item *models.Post, index int) uint { - return item.ID - }) + // If no posts found, return early + if len(posts) == 0 { + return posts, nil + } - // Load reactions - if len(noReact) <= 0 || !noReact[0] { - if mapping, err := BatchListPostReactions(database.C.Where("post_id IN ?", idx), "post_id"); err != nil { - return items, err - } else { - itemMap := lo.SliceToMap(items, func(item *models.Post) (uint, *models.Post) { - return item.ID, item - }) + // Collect post IDs + idx := make([]uint, len(posts)) + itemMap := make(map[uint]*models.Post, len(posts)) + for i, item := range posts { + idx[i] = item.ID + itemMap[item.ID] = &item + } - for k, v := range mapping { - if post, ok := itemMap[k]; ok { - post.Metric = models.PostMetric{ - ReactionList: v, - } - } + // Batch load reactions + if mapping, err := BatchListPostReactions(database.C.Where("post_id IN ?", idx), "post_id"); err != nil { + return posts, err + } else { + for postID, reactions := range mapping { + if post, exists := itemMap[postID]; exists { + post.Metric.ReactionList = reactions } } } - // Load replies - if len(noReact) <= 0 || !noReact[0] { - var replies []struct { - PostID uint - Count int64 - } - - if err := database.C.Model(&models.Post{}). - Select("reply_id as post_id, COUNT(id) as count"). - Where("reply_id IN (?)", idx). - Group("post_id"). - Scan(&replies).Error; err != nil { - return items, err - } - - itemMap := lo.SliceToMap(items, func(item *models.Post) (uint, *models.Post) { - return item.ID, item - }) - - list := map[uint]int64{} - for _, info := range replies { - list[info.PostID] = info.Count - } - - for k, v := range list { - if post, ok := itemMap[k]; ok { - post.Metric = models.PostMetric{ - ReactionList: post.Metric.ReactionList, - ReplyCount: v, - } - } + // Batch load reply counts efficiently + var replies []struct { + PostID uint + Count int64 + } + if err := database.C.Model(&models.Post{}). + Select("reply_id as post_id, COUNT(id) as count"). + Where("reply_id IN (?)", idx). + Group("post_id"). + Find(&replies).Error; err != nil { + return posts, err + } + for _, info := range replies { + if post, exists := itemMap[info.PostID]; exists { + post.Metric.ReplyCount = info.Count } } + // Add post views for the user if user != nil { - AddPostViews(lo.Map(items, func(item *models.Post, index int) models.Post { - return *item - }), *user) + AddPostViews(posts, *user) } - return items, nil + return posts, nil } func ListPostMinimal(tx *gorm.DB, take int, offset int, order any) ([]*models.Post, error) { diff --git a/pkg/internal/services/queries/posts.go b/pkg/internal/services/queries/posts.go index 6266f7c..c496bd9 100644 --- a/pkg/internal/services/queries/posts.go +++ b/pkg/internal/services/queries/posts.go @@ -2,6 +2,7 @@ package queries import ( "fmt" + "github.com/goccy/go-json" "git.solsynth.dev/hypernet/interactive/pkg/internal/database" @@ -19,47 +20,18 @@ import ( var singularAttachmentFields = []string{"video", "thumbnail"} -// This api still is experimental and finally with replace the old one -// Some changes between ListPost and ListPostV2: -// - Post reply to and repost to are not included -func ListPostV2(tx *gorm.DB, take int, offset int, order any, user *uint) ([]models.Post, error) { - if take > 100 { - take = 100 - } - - if take >= 0 { - tx = tx.Limit(take) - } - if offset >= 0 { - tx = tx.Offset(offset) - } - - tx = tx.Preload("Tags"). - Preload("Categories"). - Preload("Publisher") - - // Fetch posts - var posts []models.Post - if err := tx.Order(order).Find(&posts).Error; err != nil { - return nil, err - } - - // If no posts found, return early - if len(posts) == 0 { - return posts, nil - } - +func CompletePostMeta(in ...models.Post) ([]models.Post, error) { // Collect post IDs - idx := make([]uint, len(posts)) - itemMap := make(map[uint]*models.Post, len(posts)) - for i, item := range posts { + idx := make([]uint, len(in)) + itemMap := make(map[uint]*models.Post, len(in)) + for i, item := range in { idx[i] = item.ID itemMap[item.ID] = &item } // Batch load reactions if mapping, err := services.BatchListPostReactions(database.C.Where("post_id IN ?", idx), "post_id"); err != nil { - return posts, err + return in, err } else { for postID, reactions := range mapping { if post, exists := itemMap[postID]; exists { @@ -78,7 +50,7 @@ func ListPostV2(tx *gorm.DB, take int, offset int, order any, user *uint) ([]mod Where("reply_id IN (?)", idx). Group("post_id"). Find(&replies).Error; err != nil { - return posts, err + return in, err } for _, info := range replies { if post, exists := itemMap[info.PostID]; exists { @@ -93,12 +65,12 @@ func ListPostV2(tx *gorm.DB, take int, offset int, order any, user *uint) ([]mod // Scan records that can be load eagerly var bodies []models.PostStoryBody { - raw, _ := json.Marshal(lo.Map(posts, func(item models.Post, _ int) map[string]any { + raw, _ := json.Marshal(lo.Map(in, func(item models.Post, _ int) map[string]any { return item.Body })) json.Unmarshal(raw, &bodies) } - for idx, info := range posts { + for idx, info := range in { if info.Publisher.AccountID != nil { usersId = append(usersId, *info.Publisher.AccountID) } @@ -117,19 +89,19 @@ func ListPostV2(tx *gorm.DB, take int, offset int, order any, user *uint) ([]mod attachmentsRid = lo.Uniq(attachmentsRid) attachments, err := filekit.ListAttachment(gap.Nx, attachmentsRid) if err != nil { - return posts, fmt.Errorf("failed to load attachments: %v", err) + return in, fmt.Errorf("failed to load attachments: %v", err) } // Batch load publisher users usersId = lo.Uniq(usersId) users, err := authkit.ListUser(gap.Nx, usersId) if err != nil { - return posts, fmt.Errorf("failed to load users: %v", err) + return in, fmt.Errorf("failed to load users: %v", err) } // Putting information back to data log.Info().Int("attachments", len(attachments)).Int("users", len(users)).Msg("Batch loaded metadata for listing post...") - for idx, item := range posts { + for idx, item := range in { var this []fmodels.Attachment if len(bodies[idx].Attachments) > 0 { this = lo.Filter(attachments, func(item fmodels.Attachment, _ int) bool { @@ -152,7 +124,90 @@ func ListPostV2(tx *gorm.DB, take int, offset int, order any, user *uint) ([]mod } return acc.ID == *item.Publisher.AccountID }) - posts[idx] = item + in[idx] = item + } + + return in, nil +} + +func GetPost(tx *gorm.DB, id uint, user *uint) (models.Post, error) { + var post models.Post + if err := tx.Preload("Tags"). + Preload("Categories"). + Preload("Publisher"). + Preload("Poll"). + First(&post, id).Error; err != nil { + return post, err + } + + out, err := CompletePostMeta(post) + if err != nil { + return post, err + } + + if user != nil { + services.AddPostView(post, *user) + } + + return out[0], nil +} + +func GetPostByAlias(tx *gorm.DB, alias, area string, user *uint) (models.Post, error) { + var post models.Post + if err := tx.Preload("Tags"). + Preload("Categories"). + Preload("Publisher"). + Preload("Poll"). + Where("alias = ?", alias). + Where("alias_prefix = ?", area). + First(&post).Error; err != nil { + return post, err + } + + out, err := CompletePostMeta(post) + if err != nil { + return post, err + } + + if user != nil { + services.AddPostView(post, *user) + } + + return out[0], nil +} + +func ListPost(tx *gorm.DB, take int, offset int, order any, user *uint) ([]models.Post, error) { + if take > 100 { + take = 100 + } + + if take >= 0 { + tx = tx.Limit(take) + } + if offset >= 0 { + tx = tx.Offset(offset) + } + + tx = tx.Preload("Tags"). + Preload("Categories"). + Preload("Publisher"). + Preload("Poll") + + // Fetch posts + var posts []models.Post + if err := tx.Order(order).Find(&posts).Error; err != nil { + return nil, err + } + + // If no posts found, return early + if len(posts) == 0 { + return posts, nil + } + + // Load data eagerly + posts, err := CompletePostMeta(posts...) + if err != nil { + return nil, err } // Add post views for the user