From 40765e837dfb25a583f137b9459f05826ca470c3 Mon Sep 17 00:00:00 2001 From: miaowuawa <2484667023@qq.com> Date: Thu, 6 Nov 2025 15:46:02 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AD=BE=E5=88=B0=EF=BC=8C=E9=BB=91=E5=90=8D?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E5=B1=8F=E8=94=BD=EF=BC=8Csse=EF=BC=8C?= =?UTF-8?q?=E7=BB=8F=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto/api/v1/blockship.go | 87 + auto/api/v1/check_in_activitity.go | 82 + auto/api/v1/core.go | 61 + auto/api/v1/loose.go | 188 + auto/api/v1/muteship.go | 87 + auto/api/v1/priv.go | 250 + auto/api/v1/relax.go | 11 - docs/openapi/openapi.json | 4398 +++++++++++++++-- internal/conf/db.go | 2 + internal/conf/setting.go | 2 + internal/core/cs/cs.go | 33 + internal/core/cs/timeline.go | 21 + internal/core/cs/user.go | 42 - internal/core/level.go | 27 - internal/core/ms/articles.go | 58 + internal/core/ms/timeline.go | 22 + internal/core/timeline.go | 10 + internal/core/tweets.go | 7 +- internal/core/user.go | 4 +- internal/dao/cache/common.go | 6 +- internal/dao/jinzhu/block.go.disabled | 77 + internal/dao/jinzhu/dbr/block.go | 65 + internal/dao/jinzhu/dbr/common.go | 75 + internal/dao/jinzhu/dbr/level.go | 53 - internal/dao/jinzhu/dbr/mute.go | 65 + internal/dao/jinzhu/dbr/post_star.go | 17 +- internal/dao/jinzhu/dbr/user.go | 5 +- internal/dao/jinzhu/jinzhu.go | 5 +- internal/dao/jinzhu/level.go | 54 - internal/dao/jinzhu/level.go.disabled | 142 + internal/dao/jinzhu/mute.go.disabled | 73 + internal/dao/jinzhu/topics.go | 30 +- internal/dao/jinzhu/tweets.go | 18 +- internal/dao/jinzhu/user.go | 97 +- internal/dao/search/search.go | 1 + internal/model/joint/json.go | 17 + internal/model/web/activities.go | 60 +- internal/model/web/articles.go | 310 ++ internal/model/web/block.go | 28 + internal/model/web/core.go | 4 + internal/model/web/loose.go | 2 + internal/model/web/mute.go | 28 + internal/model/web/xerror.go | 32 + internal/servants/base/base.go | 15 +- internal/servants/web/block.go.disabled | 109 + internal/servants/web/core.go | 111 +- internal/servants/web/followship.go | 5 +- internal/servants/web/loose.go | 22 +- internal/servants/web/mute.go.disabled | 97 + mirc/web/v1/activities.go | 14 + mirc/web/v1/block.go | 21 + mirc/web/v1/core.go | 9 + mirc/web/v1/loose.go | 24 + mirc/web/v1/mute.go | 21 + mirc/web/v1/priv.go | 40 + ...add_user_exp_and_operation_tables.down.sql | 12 + ...7_add_user_exp_and_operation_tables.up.sql | 40 + .../mysql/0018_drop_user_level_table.down.sql | 23 + .../mysql/0018_drop_user_level_table.up.sql | 4 + .../mysql/0019_create_articles.down.sql | 10 + .../mysql/0019_create_articles.up.sql | 153 + .../mysql/0020_add_block_mute_tables.down.sql | 4 + .../mysql/0020_add_block_mute_tables.up.sql | 33 + ...add_user_exp_and_operation_tables.down.sql | 12 + ...7_add_user_exp_and_operation_tables.up.sql | 39 + .../0018_drop_user_level_table.down.sql | 24 + .../0018_drop_user_level_table.up.sql | 4 + .../0020_add_block_mute_tables.down.sql | 4 + .../0020_add_block_mute_tables.up.sql | 47 + ...add_user_exp_and_operation_tables.down.sql | 11 + ...7_add_user_exp_and_operation_tables.up.sql | 39 + .../0018_drop_user_level_table.down.sql | 24 + .../sqlite3/0018_drop_user_level_table.up.sql | 4 + .../0020_add_block_mute_tables.down.sql | 4 + .../sqlite3/0020_add_block_mute_tables.up.sql | 35 + 75 files changed, 7049 insertions(+), 621 deletions(-) create mode 100644 auto/api/v1/blockship.go create mode 100644 auto/api/v1/check_in_activitity.go create mode 100644 auto/api/v1/muteship.go delete mode 100644 internal/core/level.go create mode 100644 internal/core/ms/articles.go create mode 100644 internal/dao/jinzhu/block.go.disabled create mode 100644 internal/dao/jinzhu/dbr/block.go create mode 100644 internal/dao/jinzhu/dbr/common.go delete mode 100644 internal/dao/jinzhu/dbr/level.go create mode 100644 internal/dao/jinzhu/dbr/mute.go delete mode 100644 internal/dao/jinzhu/level.go create mode 100644 internal/dao/jinzhu/level.go.disabled create mode 100644 internal/dao/jinzhu/mute.go.disabled create mode 100644 internal/model/web/articles.go create mode 100644 internal/model/web/block.go create mode 100644 internal/model/web/mute.go create mode 100644 internal/servants/web/block.go.disabled create mode 100644 internal/servants/web/mute.go.disabled create mode 100644 mirc/web/v1/block.go create mode 100644 mirc/web/v1/mute.go create mode 100644 scripts/migration/mysql/0017_add_user_exp_and_operation_tables.down.sql create mode 100644 scripts/migration/mysql/0017_add_user_exp_and_operation_tables.up.sql create mode 100644 scripts/migration/mysql/0018_drop_user_level_table.down.sql create mode 100644 scripts/migration/mysql/0018_drop_user_level_table.up.sql create mode 100644 scripts/migration/mysql/0019_create_articles.down.sql create mode 100644 scripts/migration/mysql/0019_create_articles.up.sql create mode 100644 scripts/migration/mysql/0020_add_block_mute_tables.down.sql create mode 100644 scripts/migration/mysql/0020_add_block_mute_tables.up.sql create mode 100644 scripts/migration/postgres/0017_add_user_exp_and_operation_tables.down.sql create mode 100644 scripts/migration/postgres/0017_add_user_exp_and_operation_tables.up.sql create mode 100644 scripts/migration/postgres/0018_drop_user_level_table.down.sql create mode 100644 scripts/migration/postgres/0018_drop_user_level_table.up.sql create mode 100644 scripts/migration/postgres/0020_add_block_mute_tables.down.sql create mode 100644 scripts/migration/postgres/0020_add_block_mute_tables.up.sql create mode 100644 scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.down.sql create mode 100644 scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.up.sql create mode 100644 scripts/migration/sqlite3/0018_drop_user_level_table.down.sql create mode 100644 scripts/migration/sqlite3/0018_drop_user_level_table.up.sql create mode 100644 scripts/migration/sqlite3/0020_add_block_mute_tables.down.sql create mode 100644 scripts/migration/sqlite3/0020_add_block_mute_tables.up.sql diff --git a/auto/api/v1/blockship.go b/auto/api/v1/blockship.go new file mode 100644 index 00000000..54191f13 --- /dev/null +++ b/auto/api/v1/blockship.go @@ -0,0 +1,87 @@ +// Code generated by go-mir. DO NOT EDIT. +// versions: +// - mir 5.3 + +package v1 + +import ( + "net/http" + + "github.com/alimy/mir/v5" + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/model/web" +) + +type Blockship interface { + _default_ + + ListBlocks(*web.ListBlocksReq) (*web.ListBlocksResp, error) + UnblockUser(*web.UnblockUserReq) error + BlockUser(*web.BlockUserReq) error + + mustEmbedUnimplementedBlockshipServant() +} + +// RegisterBlockshipServant register Blockship servant to gin +func RegisterBlockshipServant(e *gin.Engine, s Blockship) { + router := e.Group("v1") + + // register routes info to router + router.Handle("GET", "user/blocks", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ListBlocksReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ListBlocks(req) + s.Render(c, resp, err) + }) + router.Handle("POST", "user/unblock", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.UnblockUserReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.UnblockUser(req)) + }) + router.Handle("POST", "user/block", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.BlockUserReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.BlockUser(req)) + }) +} + +// UnimplementedBlockshipServant can be embedded to have forward compatible implementations. +type UnimplementedBlockshipServant struct{} + +func (UnimplementedBlockshipServant) ListBlocks(req *web.ListBlocksReq) (*web.ListBlocksResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedBlockshipServant) UnblockUser(req *web.UnblockUserReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedBlockshipServant) BlockUser(req *web.BlockUserReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedBlockshipServant) mustEmbedUnimplementedBlockshipServant() {} diff --git a/auto/api/v1/check_in_activitity.go b/auto/api/v1/check_in_activitity.go new file mode 100644 index 00000000..ab9d6f3f --- /dev/null +++ b/auto/api/v1/check_in_activitity.go @@ -0,0 +1,82 @@ +// Code generated by go-mir. DO NOT EDIT. +// versions: +// - mir 5.3 + +package v1 + +import ( + "net/http" + + "github.com/alimy/mir/v5" + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/model/web" +) + +type CheckInActivitity interface { + _default_ + + // Chain provide handlers chain for gin + Chain() gin.HandlersChain + + GetCheckInRank(*web.GetCheckInRankReq) (*web.GetCheckInRankResp, error) + UserCheckIn(*web.UserCheckInReq) (*web.UserCheckInResp, error) + + mustEmbedUnimplementedCheckInActivitityServant() +} + +// RegisterCheckInActivitityServant register CheckInActivitity servant to gin +func RegisterCheckInActivitityServant(e *gin.Engine, s CheckInActivitity) { + router := e.Group("v1") + // use chain for router + middlewares := s.Chain() + router.Use(middlewares...) + + // register routes info to router + router.Handle("GET", "checkin/rank", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetCheckInRankReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetCheckInRank(req) + s.Render(c, resp, err) + }) + router.Handle("POST", "checkin", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.UserCheckInReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.UserCheckIn(req) + s.Render(c, resp, err) + }) +} + +// UnimplementedCheckInActivitityServant can be embedded to have forward compatible implementations. +type UnimplementedCheckInActivitityServant struct{} + +func (UnimplementedCheckInActivitityServant) Chain() gin.HandlersChain { + return nil +} + +func (UnimplementedCheckInActivitityServant) GetCheckInRank(req *web.GetCheckInRankReq) (*web.GetCheckInRankResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedCheckInActivitityServant) UserCheckIn(req *web.UserCheckInReq) (*web.UserCheckInResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedCheckInActivitityServant) mustEmbedUnimplementedCheckInActivitityServant() {} diff --git a/auto/api/v1/core.go b/auto/api/v1/core.go index 316c9258..f51f95e8 100644 --- a/auto/api/v1/core.go +++ b/auto/api/v1/core.go @@ -18,6 +18,9 @@ type Core interface { // Chain provide handlers chain for gin Chain() gin.HandlersChain + StreamMessages(*web.StreamMessagesReq, *gin.Context) error + GetUserArticleStars(*web.GetUserArticleStarsReq) (*web.GetUserArticleStarsResp, error) + GetUserArticleCollections(*web.GetUserArticleCollectionsReq) (*web.GetUserArticleCollectionsResp, error) TweetCollectionStatus(*web.TweetCollectionStatusReq) (*web.TweetCollectionStatusResp, error) TweetStarStatus(*web.TweetStarStatusReq) (*web.TweetStarStatusResp, error) SuggestTags(*web.SuggestTagsReq) (*web.SuggestTagsResp, error) @@ -46,6 +49,52 @@ func RegisterCoreServant(e *gin.Engine, s Core) { router.Use(middlewares...) // register routes info to router + router.Handle("GET", "user/messages/stream", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.StreamMessagesReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + if err := s.StreamMessages(req, c); err != nil { + s.Render(c, nil, err) + return + } + }) + router.Handle("GET", "user/articles/stars", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetUserArticleStarsReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetUserArticleStars(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "user/articles/collections", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetUserArticleCollectionsReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetUserArticleCollections(req) + s.Render(c, resp, err) + }) router.Handle("GET", "post/collection", func(c *gin.Context) { select { case <-c.Request.Context().Done(): @@ -283,6 +332,18 @@ func (UnimplementedCoreServant) Chain() gin.HandlersChain { return nil } +func (UnimplementedCoreServant) StreamMessages(req *web.StreamMessagesReq, c *gin.Context) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedCoreServant) GetUserArticleStars(req *web.GetUserArticleStarsReq) (*web.GetUserArticleStarsResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedCoreServant) GetUserArticleCollections(req *web.GetUserArticleCollectionsReq) (*web.GetUserArticleCollectionsResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + func (UnimplementedCoreServant) TweetCollectionStatus(req *web.TweetCollectionStatusReq) (*web.TweetCollectionStatusResp, error) { return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) } diff --git a/auto/api/v1/loose.go b/auto/api/v1/loose.go index 399d7c66..9d449327 100644 --- a/auto/api/v1/loose.go +++ b/auto/api/v1/loose.go @@ -18,6 +18,14 @@ type Loose interface { // Chain provide handlers chain for gin Chain() gin.HandlersChain + ArticleCollectionStatus(*web.ArticleCollectionStatusReq) (*web.ArticleCollectionStatusResp, error) + ArticleStarStatus(*web.ArticleStarStatusReq) (*web.ArticleStarStatusResp, error) + GetUserArticles(*web.GetUserArticlesReq) (*web.GetUserArticlesResp, error) + ListArticleSeries(*web.ListArticleSeriesReq) (*web.ListArticleSeriesResp, error) + GetArticleSeries(*web.GetArticleSeriesReq) (*web.GetArticleSeriesResp, error) + ArticleComments(*web.ArticleCommentsReq) (*web.ArticleCommentsResp, error) + ListArticles(*web.ListArticlesReq) (*web.ListArticlesResp, error) + GetArticle(*web.GetArticleReq) (*web.GetArticleResp, error) TweetDetail(*web.TweetDetailReq) (*web.TweetDetailResp, error) TweetComments(*web.TweetCommentsReq) (*web.TweetCommentsResp, error) TopicList(*web.TopicListReq) (*web.TopicListResp, error) @@ -36,6 +44,154 @@ func RegisterLooseServant(e *gin.Engine, s Loose) { router.Use(middlewares...) // register routes info to router + router.Handle("GET", "articles/collection", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ArticleCollectionStatusReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ArticleCollectionStatus(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "articles/star", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ArticleStarStatusReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ArticleStarStatus(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "user/articles", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetUserArticlesReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetUserArticles(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "articles/series", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ListArticleSeriesReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ListArticleSeries(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "articles/series/detail", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetArticleSeriesReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetArticleSeries(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "articles/comments", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ArticleCommentsReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ArticleComments(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "articles", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ListArticlesReq) + var bv _binding_ = req + if err := bv.Bind(c); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ListArticles(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "articles/detail", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetArticleReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetArticle(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) router.Handle("GET", "post", func(c *gin.Context) { select { case <-c.Request.Context().Done(): @@ -145,6 +301,38 @@ func (UnimplementedLooseServant) Chain() gin.HandlersChain { return nil } +func (UnimplementedLooseServant) ArticleCollectionStatus(req *web.ArticleCollectionStatusReq) (*web.ArticleCollectionStatusResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) ArticleStarStatus(req *web.ArticleStarStatusReq) (*web.ArticleStarStatusResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) GetUserArticles(req *web.GetUserArticlesReq) (*web.GetUserArticlesResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) ListArticleSeries(req *web.ListArticleSeriesReq) (*web.ListArticleSeriesResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) GetArticleSeries(req *web.GetArticleSeriesReq) (*web.GetArticleSeriesResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) ArticleComments(req *web.ArticleCommentsReq) (*web.ArticleCommentsResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) ListArticles(req *web.ListArticlesReq) (*web.ListArticlesResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) GetArticle(req *web.GetArticleReq) (*web.GetArticleResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + func (UnimplementedLooseServant) TweetDetail(req *web.TweetDetailReq) (*web.TweetDetailResp, error) { return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) } diff --git a/auto/api/v1/muteship.go b/auto/api/v1/muteship.go new file mode 100644 index 00000000..413c5446 --- /dev/null +++ b/auto/api/v1/muteship.go @@ -0,0 +1,87 @@ +// Code generated by go-mir. DO NOT EDIT. +// versions: +// - mir 5.3 + +package v1 + +import ( + "net/http" + + "github.com/alimy/mir/v5" + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/model/web" +) + +type Muteship interface { + _default_ + + ListMutes(*web.ListMutesReq) (*web.ListMutesResp, error) + UnmuteUser(*web.UnmuteUserReq) error + MuteUser(*web.MuteUserReq) error + + mustEmbedUnimplementedMuteshipServant() +} + +// RegisterMuteshipServant register Muteship servant to gin +func RegisterMuteshipServant(e *gin.Engine, s Muteship) { + router := e.Group("v1") + + // register routes info to router + router.Handle("GET", "user/mutes", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ListMutesReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.ListMutes(req) + s.Render(c, resp, err) + }) + router.Handle("POST", "user/unmute", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.UnmuteUserReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.UnmuteUser(req)) + }) + router.Handle("POST", "user/mute", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.MuteUserReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.MuteUser(req)) + }) +} + +// UnimplementedMuteshipServant can be embedded to have forward compatible implementations. +type UnimplementedMuteshipServant struct{} + +func (UnimplementedMuteshipServant) ListMutes(req *web.ListMutesReq) (*web.ListMutesResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedMuteshipServant) UnmuteUser(req *web.UnmuteUserReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedMuteshipServant) MuteUser(req *web.MuteUserReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedMuteshipServant) mustEmbedUnimplementedMuteshipServant() {} diff --git a/auto/api/v1/priv.go b/auto/api/v1/priv.go index 1349474a..6fe84634 100644 --- a/auto/api/v1/priv.go +++ b/auto/api/v1/priv.go @@ -18,6 +18,19 @@ type Priv interface { // Chain provide handlers chain for gin Chain() gin.HandlersChain + AdminDeleteArticle(*web.AdminDeleteArticleReq) error + DeleteArticleSeries(*web.DeleteArticleSeriesReq) error + UpdateArticleSeries(*web.UpdateArticleSeriesReq) error + CreateArticleSeries(*web.CreateArticleSeriesReq) (*web.CreateArticleSeriesResp, error) + DeleteArticleCommentReply(*web.DeleteArticleCommentReplyReq) error + CreateArticleCommentReply(*web.CreateArticleCommentReplyReq) (*web.CreateArticleCommentReplyResp, error) + DeleteArticleComment(*web.DeleteArticleCommentReq) error + CreateArticleComment(*web.CreateArticleCommentReq) (*web.CreateArticleCommentResp, error) + CollectionArticle(*web.ArticleCollectionReq) (*web.ArticleCollectionResp, error) + StarArticle(*web.ArticleStarReq) (*web.ArticleStarResp, error) + DeleteArticle(*web.DeleteArticleReq) error + UpdateArticle(*web.UpdateArticleReq) error + CreateArticle(*web.CreateArticleReq) (*web.CreateArticleResp, error) UnfollowTopic(*web.UnfollowTopicReq) error FollowTopic(*web.FollowTopicReq) error PinTopic(*web.PinTopicReq) (*web.PinTopicResp, error) @@ -47,6 +60,8 @@ type Priv interface { } type PrivChain interface { + ChainUpdateArticle() gin.HandlersChain + ChainCreateArticle() gin.HandlersChain ChainCreateTweet() gin.HandlersChain mustEmbedUnimplementedPrivChain() @@ -66,6 +81,181 @@ func RegisterPrivServant(e *gin.Engine, s Priv, m ...PrivChain) { router.Use(middlewares...) // register routes info to router + router.Handle("DELETE", "admin/articles", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.AdminDeleteArticleReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.AdminDeleteArticle(req)) + }) + router.Handle("DELETE", "articles/series", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.DeleteArticleSeriesReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.DeleteArticleSeries(req)) + }) + router.Handle("PUT", "articles/series", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.UpdateArticleSeriesReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.UpdateArticleSeries(req)) + }) + router.Handle("POST", "articles/series", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.CreateArticleSeriesReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.CreateArticleSeries(req) + s.Render(c, resp, err) + }) + router.Handle("DELETE", "articles/comment/reply", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.DeleteArticleCommentReplyReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.DeleteArticleCommentReply(req)) + }) + router.Handle("POST", "articles/comment/reply", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.CreateArticleCommentReplyReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.CreateArticleCommentReply(req) + s.Render(c, resp, err) + }) + router.Handle("DELETE", "articles/comment", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.DeleteArticleCommentReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.DeleteArticleComment(req)) + }) + router.Handle("POST", "articles/comment", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.CreateArticleCommentReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.CreateArticleComment(req) + s.Render(c, resp, err) + }) + router.Handle("POST", "articles/collection", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ArticleCollectionReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.CollectionArticle(req) + s.Render(c, resp, err) + }) + router.Handle("POST", "articles/star", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.ArticleStarReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.StarArticle(req) + s.Render(c, resp, err) + }) + router.Handle("DELETE", "articles", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.DeleteArticleReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.DeleteArticle(req)) + }) + router.Handle("PUT", "articles", append(cc.ChainUpdateArticle(), func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.UpdateArticleReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.UpdateArticle(req)) + })...) + router.Handle("POST", "articles", append(cc.ChainCreateArticle(), func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.CreateArticleReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.CreateArticle(req) + s.Render(c, resp, err) + })...) router.Handle("POST", "topic/unfollow", func(c *gin.Context) { select { case <-c.Request.Context().Done(): @@ -413,6 +603,58 @@ func (UnimplementedPrivServant) Chain() gin.HandlersChain { return nil } +func (UnimplementedPrivServant) AdminDeleteArticle(req *web.AdminDeleteArticleReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) DeleteArticleSeries(req *web.DeleteArticleSeriesReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) UpdateArticleSeries(req *web.UpdateArticleSeriesReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) CreateArticleSeries(req *web.CreateArticleSeriesReq) (*web.CreateArticleSeriesResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) DeleteArticleCommentReply(req *web.DeleteArticleCommentReplyReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) CreateArticleCommentReply(req *web.CreateArticleCommentReplyReq) (*web.CreateArticleCommentReplyResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) DeleteArticleComment(req *web.DeleteArticleCommentReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) CreateArticleComment(req *web.CreateArticleCommentReq) (*web.CreateArticleCommentResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) CollectionArticle(req *web.ArticleCollectionReq) (*web.ArticleCollectionResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) StarArticle(req *web.ArticleStarReq) (*web.ArticleStarResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) DeleteArticle(req *web.DeleteArticleReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) UpdateArticle(req *web.UpdateArticleReq) error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPrivServant) CreateArticle(req *web.CreateArticleReq) (*web.CreateArticleResp, error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + func (UnimplementedPrivServant) UnfollowTopic(req *web.UnfollowTopicReq) error { return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) } @@ -514,6 +756,14 @@ func (UnimplementedPrivServant) mustEmbedUnimplementedPrivServant() {} // UnimplementedPrivChain can be embedded to have forward compatible implementations. type UnimplementedPrivChain struct{} +func (b *UnimplementedPrivChain) ChainUpdateArticle() gin.HandlersChain { + return nil +} + +func (b *UnimplementedPrivChain) ChainCreateArticle() gin.HandlersChain { + return nil +} + func (b *UnimplementedPrivChain) ChainCreateTweet() gin.HandlersChain { return nil } diff --git a/auto/api/v1/relax.go b/auto/api/v1/relax.go index 44900432..73cc5de3 100644 --- a/auto/api/v1/relax.go +++ b/auto/api/v1/relax.go @@ -19,7 +19,6 @@ type Relax interface { Chain() gin.HandlersChain GetUnreadMsgCount(*web.GetUnreadMsgCountReq) (*web.GetUnreadMsgCountResp, error) - StreamUnreadMsgCount(*gin.Context) mustEmbedUnimplementedRelaxServant() } @@ -63,16 +62,6 @@ func RegisterRelaxServant(e *gin.Engine, s Relax, m ...RelaxChain) { var rv _render_ = resp rv.Render(c) })...) - - // Register SSE route for streaming unread message count - router.Handle("GET", "user/msgcount/stream", append(middlewares, func(c *gin.Context) { - select { - case <-c.Request.Context().Done(): - return - default: - } - s.StreamUnreadMsgCount(c) - })...) } // UnimplementedRelaxServant can be embedded to have forward compatible implementations. diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index 3e980c04..27f63d66 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -1,334 +1,4140 @@ { - "openapi": "3.0.0", - "info": { - "title": "paopao-ce API", - "description": "# The paopao-ce API documentation\n\nWelcome to the paopao-ce API documentation!\n\n", - "version": "0.1.0" - }, - "servers": [ - { - "url": "https://api.paopao.info" - } - ], - "tags": [], - "paths": { - "/{bucket}": { - "post": { - "summary": "Upload Image", - "description": "Upload an image to the given bucket.\nThe `content-type` header must be provided as well\nas the `content-length` header otherwise the request will be rejected.\n\nThe uploaded file must also not exceed the given `content-length`.", - "parameters": [ - { - "name": "bucket", - "schema": { - "type": "string" - }, - "in": "path", - "description": "The bucket that the image should be uploaded.", - "required": true, - "deprecated": false - }, - { - "name": "content-length", - "schema": { - "type": "integer", - "format": "uint64" - }, - "in": "header", - "description": "The total size of the image in bytes.", - "required": true, - "deprecated": false - }, - { - "name": "format", + "openapi": "3.0.0", + "info": { + "title": "paopao-ce API", + "description": "# paopao-ce API 文档\n\n欢迎使用 paopao-ce API 文档!\n\npaopao-ce 是一个开源的泡泡社区系统,提供完整的社交媒体功能。\n\n## 鉴权说明\n\n大部分API需要JWT Token进行鉴权,请在请求头中包含:\n```\nAuthorization: Bearer \n```\n\n## 响应格式\n\n所有API响应都遵循以下格式:\n```json\n{\n \"code\": 0,\n \"message\": \"success\",\n \"data\": {}\n}\n```\n\n## 错误码说明\n\n- `0`: 成功\n- `10001`: 未授权\n- `10002`: 参数错误\n- `10003`: 权限不足\n- `10004`: 资源不存在\n\n## 分页参数\n\n支持分页的API使用以下查询参数:\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)\n", + "version": "1.0.0", + "contact": { + "name": "paopao-ce", + "url": "https://github.com/rocboss/paopao-ce" + } + }, + "servers": [ + { + "url": "http://localhost:8008", + "description": "本地开发环境" + }, + { + "url": "https://api.paopao.info", + "description": "生产环境" + } + ], + "security": [ + { + "BearerAuth": [] + } + ], + "tags": [ + { + "name": "公开接口", + "description": "无需鉴权的公开API" + }, + { + "name": "用户管理", + "description": "用户相关的核心功能" + }, + { + "name": "动态管理", + "description": "动态(推文)的增删改查" + }, + { + "name": "评论管理", + "description": "评论和回复管理" + }, + { + "name": "长文章管理", + "description": "长文章相关功能" + }, + { + "name": "社交关系", + "description": "关注、拉黑等社交功能" + }, + { + "name": "管理员接口", + "description": "管理员专用接口" + } + ], + "paths": { + "/": { + "get": { + "tags": ["公开接口"], + "summary": "获取版本信息", + "description": "获取后端系统的版本信息,无需鉴权", + "operationId": "getVersion", + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionResp" + } + } + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": ["公开接口"], + "summary": "用户登录", + "description": "用户账号密码登录,无需鉴权\n\n**注意事项:**\n- 用户名和密码都是必填项\n- 登录成功后会返回JWT Token", + "operationId": "login", + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "$ref": "#/components/schemas/ImageKind" - }, - "in": "query", - "description": "The format that the uploaded image is encoded in.\n\nIf not provided, lust will guess the encoding.", - "required": false, - "deprecated": false + "$ref": "#/components/schemas/LoginReq" + } } - ], - "requestBody": { + } + }, + "responses": { + "200": { + "description": "登录成功", "content": { - "application/octet-stream": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/LoginResp" } } - }, - "required": true + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "用户名或密码错误" + } + } + } + }, + "/auth/register": { + "post": { + "tags": ["公开接口"], + "summary": "用户注册", + "description": "用户账号注册,无需鉴权\n\n**注意事项:**\n- 用户名和密码都是必填项\n- 用户名不能重复", + "operationId": "register", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterReq" + } + } + } + }, + "responses": { + "200": { + "description": "注册成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterResp" + } + } + } }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UploadInfo" - } + "400": { + "description": "参数错误或用户名已存在" + } + } + } + }, + "/captcha": { + "get": { + "tags": ["公开接口"], + "summary": "获取图片验证码", + "description": "获取图片验证码用于后续验证,无需鉴权\n\n**响应说明:**\n- `id`: 验证码ID,用于后续验证\n- `b64s`: Base64编码的验证码图片", + "operationId": "getCaptcha", + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCaptchaResp" } } - }, - "404": { - "description": "Bucket not found" - }, - "400": { - "description": "The image format was incorrect or the system was\nunable to guess the format of the image." - }, - "413": { - "description": "The upload exceeds the configured maximum file size." - }, - "401": { - "description": "You are not authorized to complete this action.\n\nThis normally means the `Authorization` bearer has been left out\nof the request or is invalid." } } } }, - "/{bucket}/{image_id}": { - "get": { - "summary": "Fetch Image", - "description": "Fetch the image from the storage backend and apply and additional affects\nif required.", - "parameters": [ - { - "name": "bucket", + "post": { + "tags": ["公开接口"], + "summary": "发送短信验证码", + "description": "发送短信验证码到指定手机号,无需鉴权\n\n**注意事项:**\n- 需要先获取图片验证码并验证通过\n- 手机号格式必须正确", + "operationId": "sendCaptcha", + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "string" - }, - "in": "path", - "description": "The bucket to try fetch the image from.", - "required": true, - "deprecated": false - }, - { - "name": "image_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "in": "path", - "description": "The id of the image.", - "required": true, - "deprecated": false - }, - { - "name": "format", - "schema": { - "$ref": "#/components/schemas/ImageKind" - }, - "in": "query", - "description": "The encoding format that the image should be returned as.", - "required": false, - "deprecated": false - }, - { - "name": "size", - "schema": { - "type": "string" - }, - "in": "query", - "description": "The size preset that should be used when returning the image.", - "required": false, - "deprecated": false - }, - { - "name": "width", - "schema": { - "type": "integer", - "format": "uint32" - }, - "in": "query", - "description": "A custom width to resize the returned image to.", - "required": false, - "deprecated": false - }, - { - "name": "height", - "schema": { - "type": "integer", - "format": "uint32" - }, - "in": "query", - "description": "A custom height to resize the returned image to.", - "required": false, - "deprecated": false - }, - { - "name": "accept", - "schema": { - "type": "string" - }, - "in": "header", - "description": "A set of `,` separated content-types that could be sent as a response.\nE.g. `image/png,image/webp,image/gif`", - "required": false, - "deprecated": false - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - }, - "headers": { - "CONTENT-TYPE": { - "required": true, - "deprecated": false, - "schema": { - "type": "string" - } - } + "$ref": "#/components/schemas/SendCaptchaReq" } - }, - "400": { - "description": "The request is invalid with the current configuration.\n\nSee the detail section for more info.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Detail" - } + } + } + }, + "responses": { + "200": { + "description": "验证码发送成功" + }, + "400": { + "description": "参数错误或图片验证码验证失败" + } + } + } + }, + "/user/info": { + "get": { + "tags": ["用户管理"], + "summary": "获取用户信息", + "description": "获取指定用户的基本信息,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 可以获取自己的信息或他人的公开信息", + "operationId": "getUserInfo", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "用户名", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserInfoResp" } } - }, - "404": { - "description": "Bucket does not exist or image does not exist.\n\nSee the detail section for more info.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Detail" - } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "用户不存在" + } + } + } + }, + "/user/messages": { + "get": { + "tags": ["用户管理"], + "summary": "获取消息列表", + "description": "获取当前用户的消息列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `style`: 消息类型 (1:系统消息, 2:私信, 3:@提醒)\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "getMessages", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "style", + "in": "query", + "description": "消息类型", + "required": true, + "schema": { + "type": "integer", + "enum": [1, 2, 3] + } + }, + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMessagesResp" } } } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/messages/stream": { + "get": { + "tags": ["用户管理"], + "summary": "SSE获取消息列表", + "description": "建立Server-Sent Events连接,实时获取用户的未读消息通知,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**连接特性:**\n- Server-Sent Events (SSE) 流式响应\n- 每5秒自动推送新消息\n- 只推送未读消息,避免重复通知\n- 自动断开连接处理\n\n**SSE事件类型:**\n- `connected`: 连接建立确认\n- `message`: 新消息通知\n- `error`: 错误信息\n\n**消息数据格式:**\n```json\n{\n \"id\": 123,\n \"type\": 1,\n \"brief\": \"消息摘要\",\n \"content\": \"消息内容\",\n \"sender_user_id\": 456,\n \"receiver_user_id\": 789,\n \"post_id\": 101,\n \"comment_id\": 202,\n \"reply_id\": 303,\n \"is_read\": 0,\n \"created_on\": 1640995200\n}\n```", + "operationId": "streamMessages", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "SSE流连接成功", + "content": { + "text/event-stream": { + "schema": { + "type": "string", + "description": "Server-Sent Events 流数据" + }, + "example": "event: connected\ndata: {\"user_id\": 123, \"timestamp\": 1640995200}\n\nevent: message\ndata: {\"id\": 456, \"type\": 1, \"brief\": \"新消息\", \"content\": \"消息内容\", \"sender_user_id\": 789, \"is_read\": 0, \"created_on\": 1640995200}\n\n" + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/user/message/read": { + "post": { + "tags": ["用户管理"], + "summary": "标记消息已读", + "description": "标记指定消息为已读状态,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能标记自己的消息", + "operationId": "readMessage", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadMessageReq" + } + } } }, - "delete": { - "summary": "Delete Image", - "description": "Delete the given image.\nThis will purge all variants of the image including sizing presets and formats.\n\nImages that do not exist already will be ignored and will not return a 404.", - "parameters": [ - { - "name": "bucket", + "responses": { + "200": { + "description": "成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "消息不存在" + } + } + } + }, + "/user/message/readall": { + "post": { + "tags": ["用户管理"], + "summary": "标记所有消息已读", + "description": "标记当前用户的所有未读消息为已读,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token", + "operationId": "readAllMessage", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/whisper": { + "post": { + "tags": ["用户管理"], + "summary": "发送私信", + "description": "向指定用户发送私信,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 不能给自己发私信\n- 内容不能为空", + "operationId": "sendUserWhisper", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "string" - }, - "in": "path", - "description": "The bucket to try delete the image from.", - "required": true, - "deprecated": false - }, - { - "name": "image_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "in": "path", - "description": "The image to delete try delete.", - "required": true, - "deprecated": false - } - ], - "responses": { - "200": { - "description": "" - }, - "401": { - "description": "You are not authorized to complete this action.\n\nThis normally means the `Authorization` bearer has been left out\nof the request or is invalid." - }, - "404": { - "description": "Bucket does not exist." + "$ref": "#/components/schemas/SendWhisperReq" + } } } + }, + "responses": { + "200": { + "description": "发送成功" + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } } } }, - "components": { - "schemas": { - "Detail": { - "type": "object", - "required": [ - "detail" - ], - "properties": { - "detail": { - "type": "string", - "description": "Additional information regarding the response." - } - } - }, - "ImageKind": { - "type": "string", - "enum": [ - "png", - "jpeg", - "webp", - "gif" - ] - }, - "ImageUploadInfo": { - "type": "object", - "required": [ - "sizing_id" - ], - "properties": { - "sizing_id": { + "/user/collections": { + "get": { + "tags": ["用户管理"], + "summary": "获取收藏列表", + "description": "获取当前用户的收藏列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "getCollections", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { "type": "integer", - "format": "uint32", - "description": "The computed image sizing id.\n\nThis is useful for tracking files outside of lust as this is\ngenerally used for filtering within the storage systems." - } - } - }, - "UploadInfo": { - "type": "object", - "required": [ - "image_id", - "processing_time", - "io_time", - "checksum", - "images", - "bucket_id" - ], - "properties": { - "image_id": { - "type": "string", - "format": "uuid", - "description": "The generated ID for the file.\n\nThis can be used to access the file for the given bucket." - }, - "processing_time": { - "type": "number", - "format": "float", - "description": "The time spent processing the image in seconds." - }, - "io_time": { - "type": "number", - "format": "float", - "description": "The time spent uploading the image to the persistent store." - }, - "checksum": { + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { "type": "integer", - "format": "uint32", - "description": "The crc32 checksum of the uploaded image." - }, - "images": { - "type": "array", - "description": "The information that is specific to the image.", - "items": { - "$ref": "#/components/schemas/ImageUploadInfo" + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCollectionsResp" + } } - }, - "bucket_id": { + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/stars": { + "get": { + "tags": ["用户管理"], + "summary": "获取点赞列表", + "description": "获取当前用户的点赞列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "getStars", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { "type": "integer", - "format": "uint32", - "description": "The id of the bucket the image was stored in.\n\nThis is useful for tracking files outside of lust as this is\ngenerally used for filtering within the storage systems." + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetStarsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/phone": { + "post": { + "tags": ["用户管理"], + "summary": "绑定手机号", + "description": "绑定手机号到当前用户账户,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 需要先获取短信验证码\n- 手机号不能已被其他用户绑定", + "operationId": "userPhoneBind", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPhoneBindReq" + } + } + } + }, + "responses": { + "200": { + "description": "绑定成功" + }, + "400": { + "description": "参数错误或手机号已被绑定" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/password": { + "post": { + "tags": ["用户管理"], + "summary": "修改密码", + "description": "修改当前用户的登录密码,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 需要提供旧密码进行验证\n- 新密码不能为空", + "operationId": "changePassword", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordReq" + } + } + } + }, + "responses": { + "200": { + "description": "修改成功" + }, + "400": { + "description": "参数错误或旧密码不正确" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/nickname": { + "post": { + "tags": ["用户管理"], + "summary": "修改昵称", + "description": "修改当前用户的昵称,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 昵称不能为空\n- 昵称长度限制在2-20个字符", + "operationId": "changeNickname", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeNicknameReq" + } + } + } + }, + "responses": { + "200": { + "description": "修改成功" + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/avatar": { + "post": { + "tags": ["用户管理"], + "summary": "修改头像", + "description": "修改当前用户的头像,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- avatar字段为头像URL地址", + "operationId": "changeAvatar", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeAvatarReq" + } + } + } + }, + "responses": { + "200": { + "description": "修改成功" + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/suggest/users": { + "get": { + "tags": ["用户管理"], + "summary": "搜索用户", + "description": "根据关键词搜索用户,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `keyword`: 搜索关键词", + "operationId": "suggestUsers", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "keyword", + "in": "query", + "description": "搜索关键词", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuggestUsersResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/suggest/tags": { + "get": { + "tags": ["用户管理"], + "summary": "搜索标签", + "description": "根据关键词搜索标签,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `keyword`: 搜索关键词", + "operationId": "suggestTags", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "keyword", + "in": "query", + "description": "搜索关键词", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuggestTagsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/post/star": { + "get": { + "tags": ["动态管理"], + "summary": "获取动态点赞状态", + "description": "获取指定动态的点赞状态,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `id`: 动态ID", + "operationId": "tweetStarStatus", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "动态ID", + "required": true, + "schema": { + "type": "integer" } } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TweetStarStatusResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + }, + "post": { + "tags": ["动态管理"], + "summary": "点赞动态", + "description": "对指定动态进行点赞操作,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复点赞会取消点赞", + "operationId": "starTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StarTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StarTweetResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "动态不存在" + } + } + } + }, + "/post/collection": { + "get": { + "tags": ["动态管理"], + "summary": "获取动态收藏状态", + "description": "获取指定动态的收藏状态,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `id`: 动态ID", + "operationId": "tweetCollectionStatus", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "动态ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TweetCollectionStatusResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + }, + "post": { + "tags": ["动态管理"], + "summary": "收藏动态", + "description": "对指定动态进行收藏操作,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复收藏会取消收藏", + "operationId": "collectionTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionTweetResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "动态不存在" + } + } + } + }, + "/user/articles/collections": { + "get": { + "tags": ["长文章管理"], + "summary": "获取长文章收藏列表", + "description": "获取当前用户的长文章收藏列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "getUserArticleCollections", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetUserArticleCollectionsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/articles/stars": { + "get": { + "tags": ["长文章管理"], + "summary": "获取长文章点赞列表", + "description": "获取当前用户的长文章点赞列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "getUserArticleStars", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetUserArticleStarsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/sync/index": { + "get": { + "tags": ["用户管理"], + "summary": "同步搜索索引", + "description": "同步搜索索引数据,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 通常用于后台管理或定时任务", + "operationId": "syncSearchIndex", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "同步成功" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/attachment": { + "post": { + "tags": ["动态管理"], + "summary": "上传附件", + "description": "上传文件附件,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 支持图片、视频等多种文件类型\n- 文件大小有限制", + "operationId": "uploadAttachment", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/UploadAttachmentReq" + } + } + } + }, + "responses": { + "200": { + "description": "上传成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadAttachmentResp" + } + } + } + }, + "400": { + "description": "参数错误或文件格式不支持" + }, + "401": { + "description": "未授权" + } + } + }, + "get": { + "tags": ["动态管理"], + "summary": "下载附件", + "description": "下载附件文件,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `id`: 附件ID\n\n**注意事项:**\n- 下载前需要先调用预检接口", + "operationId": "downloadAttachment", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "附件ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "下载成功", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限下载" + }, + "404": { + "description": "文件不存在" + } + } + } + }, + "/attachment/precheck": { + "get": { + "tags": ["动态管理"], + "summary": "下载附件预检", + "description": "下载附件前的权限预检,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `id`: 附件ID", + "operationId": "downloadAttachmentPrecheck", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "附件ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "预检通过", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadAttachmentPrecheckResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限下载" + } + } + } + }, + "/post": { + "post": { + "tags": ["动态管理"], + "summary": "发布动态", + "description": "发布新的动态,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 动态内容不能为空\n- 支持文本、图片、视频等多种内容", + "operationId": "createTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "发布成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTweetResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/post/{id}": { + "delete": { + "tags": ["动态管理"], + "summary": "删除动态", + "description": "删除指定的动态,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的动态\n- 删除后不可恢复", + "operationId": "deleteTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "动态ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "动态不存在" + } + } + } + }, + "/post/lock": { + "post": { + "tags": ["动态管理"], + "summary": "锁定动态", + "description": "锁定指定动态,禁止评论,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能锁定自己的动态", + "operationId": "lockTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LockTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LockTweetResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/post/stick": { + "post": { + "tags": ["动态管理"], + "summary": "置顶动态", + "description": "置顶指定动态,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能置顶自己的动态", + "operationId": "stickTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickTweetResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/post/highlight": { + "post": { + "tags": ["动态管理"], + "summary": "设置动态亮点", + "description": "设置动态的亮点内容,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能设置自己的动态", + "operationId": "highlightTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HighlightTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HighlightTweetResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/post/visibility": { + "post": { + "tags": ["动态管理"], + "summary": "修改动态可见度", + "description": "修改动态的可见度设置,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能修改自己的动态", + "operationId": "visibleTweet", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VisibleTweetReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VisibleTweetResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/post/comment": { + "post": { + "tags": ["评论管理"], + "summary": "发布动态评论", + "description": "对指定动态发布评论,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 评论内容不能为空\n- 如果动态被锁定,无法评论", + "operationId": "createTweetComment", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCommentReq" + } + } + } + }, + "responses": { + "200": { + "description": "评论成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCommentResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "动态被锁定,无法评论" + } + } + } + }, + "/post/comment/{id}": { + "delete": { + "tags": ["评论管理"], + "summary": "删除动态评论", + "description": "删除指定的动态评论,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的评论", + "operationId": "deleteTweetComment", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "评论ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "评论不存在" + } + } + } + }, + "/post/comment/highlight": { + "post": { + "tags": ["评论管理"], + "summary": "精选动态评论", + "description": "设置动态评论为精选评论,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能精选自己动态下的评论", + "operationId": "highlightComment", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HighlightCommentReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HighlightCommentResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/post/comment/reply": { + "post": { + "tags": ["评论管理"], + "summary": "发布评论回复", + "description": "对指定评论发布回复,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 回复内容不能为空", + "operationId": "createCommentReply", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCommentReplyReq" + } + } + } + }, + "responses": { + "200": { + "description": "回复成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCommentReplyResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/post/comment/reply/{id}": { + "delete": { + "tags": ["评论管理"], + "summary": "删除评论回复", + "description": "删除指定的评论回复,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的回复", + "operationId": "deleteCommentReply", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "回复ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "回复不存在" + } + } + } + }, + "/tweet/comment/thumbsup": { + "post": { + "tags": ["评论管理"], + "summary": "点赞评论", + "description": "对指定评论进行点赞,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复点赞会取消点赞", + "operationId": "thumbsUpTweetComment", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TweetCommentThumbsReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "评论不存在" + } + } + } + }, + "/tweet/comment/thumbsdown": { + "post": { + "tags": ["评论管理"], + "summary": "点踩评论", + "description": "对指定评论进行点踩,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复点踩会取消点踩", + "operationId": "thumbsDownTweetComment", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TweetCommentThumbsReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "评论不存在" + } + } + } + }, + "/tweet/reply/thumbsup": { + "post": { + "tags": ["评论管理"], + "summary": "点赞评论回复", + "description": "对指定评论回复进行点赞,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复点赞会取消点赞", + "operationId": "thumbsUpTweetReply", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TweetReplyThumbsReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "回复不存在" + } + } + } + }, + "/tweet/reply/thumbsdown": { + "post": { + "tags": ["评论管理"], + "summary": "点踩评论回复", + "description": "对指定评论回复进行点踩,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复点踩会取消点踩", + "operationId": "thumbsDownTweetReply", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TweetReplyThumbsReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "回复不存在" + } + } + } + }, + "/topic/stick": { + "post": { + "tags": ["动态管理"], + "summary": "置顶话题", + "description": "置顶指定话题,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 需要有相应权限", + "operationId": "stickTopic", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickTopicReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickTopicResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/topic/pin": { + "post": { + "tags": ["动态管理"], + "summary": "钉住话题", + "description": "钉住指定话题,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 需要有相应权限", + "operationId": "pinTopic", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PinTopicReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PinTopicResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限操作" + } + } + } + }, + "/topic/follow": { + "post": { + "tags": ["社交关系"], + "summary": "关注话题", + "description": "关注指定话题,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token", + "operationId": "followTopic", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FollowTopicReq" + } + } + } + }, + "responses": { + "200": { + "description": "关注成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "话题不存在" + } + } + } + }, + "/topic/unfollow": { + "post": { + "tags": ["社交关系"], + "summary": "取消关注话题", + "description": "取消关注指定话题,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token", + "operationId": "unfollowTopic", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnfollowTopicReq" + } + } + } + }, + "responses": { + "200": { + "description": "取消关注成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "话题不存在" + } + } + } + }, + "/articles": { + "post": { + "tags": ["长文章管理"], + "summary": "创建长文章", + "description": "创建新的长文章,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 文章标题和内容都是必填项\n- 支持markdown格式", + "operationId": "createArticle", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleReq" + } + } + } + }, + "responses": { + "200": { + "description": "创建成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + }, + "put": { + "tags": ["长文章管理"], + "summary": "更新长文章", + "description": "更新指定的长文章,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能更新自己的文章", + "operationId": "updateArticle", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateArticleReq" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限更新" + }, + "404": { + "description": "文章不存在" + } + } + } + }, + "/articles/{id}": { + "delete": { + "tags": ["长文章管理"], + "summary": "删除长文章", + "description": "删除指定的长文章,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的文章\n- 删除后不可恢复", + "operationId": "deleteArticle", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "文章ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "文章不存在" + } + } + } + }, + "/articles/star": { + "post": { + "tags": ["长文章管理"], + "summary": "点赞长文章", + "description": "对指定长文章进行点赞操作,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复点赞会取消点赞", + "operationId": "starArticle", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArticleStarReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArticleStarResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "文章不存在" + } + } + } + }, + "/articles/collection": { + "post": { + "tags": ["长文章管理"], + "summary": "收藏长文章", + "description": "对指定长文章进行收藏操作,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 重复收藏会取消收藏", + "operationId": "collectionArticle", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArticleCollectionReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArticleCollectionResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "文章不存在" + } + } + } + }, + "/articles/comment": { + "post": { + "tags": ["长文章管理"], + "summary": "发布长文章评论", + "description": "对指定长文章发布评论,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 评论内容不能为空", + "operationId": "createArticleComment", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleCommentReq" + } + } + } + }, + "responses": { + "200": { + "description": "评论成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleCommentResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/articles/comment/{id}": { + "delete": { + "tags": ["长文章管理"], + "summary": "删除长文章评论", + "description": "删除指定的长文章评论,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的评论", + "operationId": "deleteArticleComment", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "评论ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "评论不存在" + } + } + } + }, + "/articles/comment/reply": { + "post": { + "tags": ["长文章管理"], + "summary": "发布评论回复", + "description": "对指定长文章评论发布回复,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 回复内容不能为空", + "operationId": "createArticleCommentReply", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleCommentReplyReq" + } + } + } + }, + "responses": { + "200": { + "description": "回复成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleCommentReplyResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + } + }, + "/articles/comment/reply/{id}": { + "delete": { + "tags": ["长文章管理"], + "summary": "删除评论回复", + "description": "删除指定的长文章评论回复,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的回复", + "operationId": "deleteArticleCommentReply", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "回复ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "回复不存在" + } + } + } + }, + "/articles/series": { + "post": { + "tags": ["长文章管理"], + "summary": "创建长文章系列", + "description": "创建新的长文章系列,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token", + "operationId": "createArticleSeries", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleSeriesReq" + } + } + } + }, + "responses": { + "200": { + "description": "创建成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArticleSeriesResp" + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + } + } + }, + "put": { + "tags": ["长文章管理"], + "summary": "更新长文章系列", + "description": "更新指定的长文章系列,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能更新自己的系列", + "operationId": "updateArticleSeries", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateArticleSeriesReq" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限更新" + }, + "404": { + "description": "系列不存在" + } + } + } + }, + "/articles/series/{id}": { + "delete": { + "tags": ["长文章管理"], + "summary": "删除长文章系列", + "description": "删除指定的长文章系列,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 只能删除自己的系列\n- 删除后不可恢复", + "operationId": "deleteArticleSeries", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "系列ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限删除" + }, + "404": { + "description": "系列不存在" + } + } + } + }, + "/admin/articles/{id}": { + "delete": { + "tags": ["管理员接口"], + "summary": "管理员删除长文章", + "description": "管理员删除指定的长文章,需要JWT鉴权+管理员权限\n\n**鉴权方式:**\n- Bearer Token + 管理员权限\n\n**注意事项:**\n- 只有管理员可以调用此接口\n- 删除后不可恢复", + "operationId": "adminDeleteArticle", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "文章ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "删除成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "404": { + "description": "文章不存在" + } + } + } + }, + "/user/follow": { + "post": { + "tags": ["社交关系"], + "summary": "关注用户", + "description": "关注指定用户,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 不能关注自己\n- 重复关注无效", + "operationId": "followUser", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FollowUserReq" + } + } + } + }, + "responses": { + "200": { + "description": "关注成功" + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "用户不存在" + } + } + } + }, + "/user/unfollow": { + "post": { + "tags": ["社交关系"], + "summary": "取消关注用户", + "description": "取消关注指定用户,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token", + "operationId": "unfollowUser", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnfollowUserReq" + } + } + } + }, + "responses": { + "200": { + "description": "取消关注成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "用户不存在" + } + } + } + }, + "/user/follows": { + "get": { + "tags": ["社交关系"], + "summary": "获取关注列表", + "description": "获取当前用户的关注列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "listFollows", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFollowsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/followings": { + "get": { + "tags": ["社交关系"], + "summary": "获取粉丝列表", + "description": "获取当前用户的粉丝列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "listFollowings", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFollowingsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/user/block": { + "post": { + "tags": ["社交关系"], + "summary": "拉黑用户", + "description": "拉黑指定用户,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**注意事项:**\n- 不能拉黑自己\n- 被拉黑的用户无法关注你、评论你的动态等", + "operationId": "blockUser", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockUserReq" + } + } + } + }, + "responses": { + "200": { + "description": "拉黑成功" + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "用户不存在" + } + } + } + }, + "/user/unblock": { + "post": { + "tags": ["社交关系"], + "summary": "取消拉黑用户", + "description": "取消拉黑指定用户,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token", + "operationId": "unblockUser", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnblockUserReq" + } + } + } + }, + "responses": { + "200": { + "description": "取消拉黑成功" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "用户不存在" + } + } + } + }, + "/user/blocks": { + "get": { + "tags": ["社交关系"], + "summary": "获取拉黑列表", + "description": "获取当前用户的拉黑列表,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**分页参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "listBlocks", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListBlocksResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/trends/index": { + "get": { + "tags": ["动态管理"], + "summary": "获取广场动态索引", + "description": "获取广场页面动态条栏的索引项,需要JWT鉴权\n\n**鉴权方式:**\n- Bearer Token\n\n**请求参数:**\n- `page`: 页码 (默认: 1)\n- `page_size`: 每页数量 (默认: 20)", + "operationId": "getIndexTrends", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "页码", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "page_size", + "in": "query", + "description": "每页数量", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetIndexTrendsResp" + } + } + } + }, + "401": { + "description": "未授权" + } + } + } + }, + "/admin/user/status": { + "post": { + "tags": ["管理员接口"], + "summary": "管理用户状态", + "description": "管理员修改用户状态(禁言/解封),需要JWT鉴权+管理员权限\n\n**鉴权方式:**\n- Bearer Token + 管理员权限\n\n**注意事项:**\n- 只有管理员可以调用此接口", + "operationId": "changeUserStatus", + "security": [ + { + "BearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeUserStatusReq" + } + } + } + }, + "responses": { + "200": { + "description": "操作成功" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "404": { + "description": "用户不存在" + } + } + } + }, + "/admin/site/status": { + "get": { + "tags": ["管理员接口"], + "summary": "获取站点状态信息", + "description": "管理员获取站点统计信息,需要JWT鉴权+管理员权限\n\n**鉴权方式:**\n- Bearer Token + 管理员权限\n\n**注意事项:**\n- 只有管理员可以调用此接口", + "operationId": "siteInfo", + "security": [ + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "name": "type", + "in": "query", + "description": "统计类型", + "required": true, + "schema": { + "type": "string", + "enum": ["user", "tweet", "article", "comment"] + } + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiteInfoResp" + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + } + } + } + } + }, + "components": { + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "VersionResp": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "版本号" + }, + "build_time": { + "type": "string", + "description": "构建时间" + } + } + }, + "GetCaptchaResp": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "验证码ID" + }, + "b64s": { + "type": "string", + "description": "Base64编码的验证码图片" + } + } + }, + "SendCaptchaReq": { + "type": "object", + "required": ["phone", "img_captcha", "img_captcha_id"], + "properties": { + "phone": { + "type": "string", + "description": "手机号" + }, + "img_captcha": { + "type": "string", + "description": "图片验证码" + }, + "img_captcha_id": { + "type": "string", + "description": "图片验证码ID" + } + } + }, + "LoginReq": { + "type": "object", + "required": ["username", "password"], + "properties": { + "username": { + "type": "string", + "description": "用户名" + }, + "password": { + "type": "string", + "description": "密码" + } + } + }, + "LoginResp": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "JWT Token" + } + } + }, + "RegisterReq": { + "type": "object", + "required": ["username", "password"], + "properties": { + "username": { + "type": "string", + "description": "用户名" + }, + "password": { + "type": "string", + "description": "密码" + } + } + }, + "RegisterResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "username": { + "type": "string", + "description": "用户名" + } + } + }, + "UserInfoResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "nickname": { + "type": "string", + "description": "昵称" + }, + "username": { + "type": "string", + "description": "用户名" + }, + "status": { + "type": "integer", + "description": "用户状态" + }, + "avatar": { + "type": "string", + "description": "头像URL" + }, + "balance": { + "type": "integer", + "description": "账户余额" + }, + "phone": { + "type": "string", + "description": "手机号" + }, + "is_admin": { + "type": "boolean", + "description": "是否管理员" + }, + "created_on": { + "type": "integer", + "description": "创建时间戳" + }, + "follows": { + "type": "integer", + "description": "关注数" + }, + "followings": { + "type": "integer", + "description": "粉丝数" + }, + "tweets_count": { + "type": "integer", + "description": "动态数量" + }, + "experience": { + "type": "integer", + "description": "经验值" + }, + "level": { + "type": "integer", + "description": "等级" + } + } + }, + "GetMessagesResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "Message": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "消息ID" + }, + "content": { + "type": "string", + "description": "消息内容" + }, + "style": { + "type": "integer", + "description": "消息类型" + }, + "is_read": { + "type": "boolean", + "description": "是否已读" + }, + "created_on": { + "type": "integer", + "description": "创建时间戳" + } + } + }, + "ReadMessageReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "消息ID" + } + } + }, + "SendWhisperReq": { + "type": "object", + "required": ["user_id", "content"], + "properties": { + "user_id": { + "type": "integer", + "description": "接收用户ID" + }, + "content": { + "type": "string", + "description": "私信内容" + } + } + }, + "GetCollectionsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tweet" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "GetStarsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tweet" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "UserPhoneBindReq": { + "type": "object", + "required": ["phone", "captcha"], + "properties": { + "phone": { + "type": "string", + "description": "手机号" + }, + "captcha": { + "type": "string", + "description": "短信验证码" + } + } + }, + "ChangePasswordReq": { + "type": "object", + "required": ["password", "old_password"], + "properties": { + "password": { + "type": "string", + "description": "新密码" + }, + "old_password": { + "type": "string", + "description": "旧密码" + } + } + }, + "ChangeNicknameReq": { + "type": "object", + "required": ["nickname"], + "properties": { + "nickname": { + "type": "string", + "description": "新昵称" + } + } + }, + "ChangeAvatarReq": { + "type": "object", + "required": ["avatar"], + "properties": { + "avatar": { + "type": "string", + "description": "头像URL" + } + } + }, + "SuggestUsersResp": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "SuggestTagsResp": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + } + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "nickname": { + "type": "string", + "description": "昵称" + }, + "username": { + "type": "string", + "description": "用户名" + }, + "avatar": { + "type": "string", + "description": "头像URL" + } + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "标签ID" + }, + "name": { + "type": "string", + "description": "标签名称" + }, + "color": { + "type": "string", + "description": "标签颜色" + } + } + }, + "TweetStarStatusResp": { + "type": "object", + "properties": { + "is_star": { + "type": "boolean", + "description": "是否已点赞" + }, + "count": { + "type": "integer", + "description": "点赞数量" + } + } + }, + "TweetCollectionStatusResp": { + "type": "object", + "properties": { + "is_collection": { + "type": "boolean", + "description": "是否已收藏" + }, + "count": { + "type": "integer", + "description": "收藏数量" + } + } + }, + "GetUserArticleCollectionsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Article" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "GetUserArticleStarsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Article" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "UploadAttachmentReq": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "上传的文件" + }, + "type": { + "type": "string", + "description": "文件类型" + } + } + }, + "UploadAttachmentResp": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "附件ID" + }, + "url": { + "type": "string", + "description": "附件URL" + } + } + }, + "DownloadAttachmentPrecheckResp": { + "type": "object", + "properties": { + "can_download": { + "type": "boolean", + "description": "是否可以下载" + }, + "url": { + "type": "string", + "description": "下载URL" + } + } + }, + "CreateTweetReq": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "string", + "description": "动态内容" + }, + "images": { + "type": "array", + "items": { + "type": "string" + }, + "description": "图片URL列表" + }, + "videos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "视频URL列表" + }, + "visibility": { + "type": "integer", + "description": "可见度 0:公开 1:好友可见 2:私密", + "default": 0 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "标签列表" + } + } + }, + "CreateTweetResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + } + } + }, + "Tweet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + }, + "content": { + "type": "string", + "description": "动态内容" + }, + "images": { + "type": "array", + "items": { + "type": "string" + }, + "description": "图片URL列表" + }, + "videos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "视频URL列表" + }, + "user": { + "$ref": "#/components/schemas/User" + }, + "created_on": { + "type": "integer", + "description": "创建时间戳" + }, + "star_count": { + "type": "integer", + "description": "点赞数量" + }, + "comment_count": { + "type": "integer", + "description": "评论数量" + }, + "collection_count": { + "type": "integer", + "description": "收藏数量" + } + } + }, + "DeleteTweetReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + } + } + }, + "StarTweetReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + } + } + }, + "StarTweetResp": { + "type": "object", + "properties": { + "is_star": { + "type": "boolean", + "description": "是否已点赞" + } + } + }, + "CollectionTweetReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + } + } + }, + "CollectionTweetResp": { + "type": "object", + "properties": { + "is_collection": { + "type": "boolean", + "description": "是否已收藏" + } + } + }, + "LockTweetReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + } + } + }, + "LockTweetResp": { + "type": "object", + "properties": { + "is_locked": { + "type": "boolean", + "description": "是否已锁定" + } + } + }, + "StickTweetReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + } + } + }, + "StickTweetResp": { + "type": "object", + "properties": { + "is_stick": { + "type": "boolean", + "description": "是否已置顶" + } + } + }, + "HighlightTweetReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + }, + "content": { + "type": "string", + "description": "亮点内容" + } + } + }, + "HighlightTweetResp": { + "type": "object", + "properties": { + "highlight": { + "type": "string", + "description": "亮点内容" + } + } + }, + "VisibleTweetReq": { + "type": "object", + "required": ["id", "visibility"], + "properties": { + "id": { + "type": "integer", + "description": "动态ID" + }, + "visibility": { + "type": "integer", + "description": "可见度" + } + } + }, + "VisibleTweetResp": { + "type": "object", + "properties": { + "visibility": { + "type": "integer", + "description": "可见度" + } + } + }, + "CreateCommentReq": { + "type": "object", + "required": ["post_id", "content"], + "properties": { + "post_id": { + "type": "integer", + "description": "动态ID" + }, + "content": { + "type": "string", + "description": "评论内容" + }, + "reply_id": { + "type": "integer", + "description": "回复的评论ID" + } + } + }, + "CreateCommentResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "评论ID" + } + } + }, + "DeleteCommentReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "评论ID" + } + } + }, + "HighlightCommentReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "评论ID" + } + } + }, + "HighlightCommentResp": { + "type": "object", + "properties": { + "is_highlight": { + "type": "boolean", + "description": "是否精选" + } + } + }, + "CreateCommentReplyReq": { + "type": "object", + "required": ["comment_id", "content"], + "properties": { + "comment_id": { + "type": "integer", + "description": "评论ID" + }, + "content": { + "type": "string", + "description": "回复内容" + } + } + }, + "CreateCommentReplyResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "回复ID" + } + } + }, + "DeleteCommentReplyReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "回复ID" + } + } + }, + "TweetCommentThumbsReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "评论ID" + } + } + }, + "TweetReplyThumbsReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "回复ID" + } + } + }, + "StickTopicReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "话题ID" + } + } + }, + "StickTopicResp": { + "type": "object", + "properties": { + "is_stick": { + "type": "boolean", + "description": "是否已置顶" + } + } + }, + "PinTopicReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "话题ID" + } + } + }, + "PinTopicResp": { + "type": "object", + "properties": { + "is_pin": { + "type": "boolean", + "description": "是否已钉住" + } + } + }, + "FollowTopicReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "话题ID" + } + } + }, + "UnfollowTopicReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "话题ID" + } + } + }, + "CreateArticleReq": { + "type": "object", + "required": ["title", "content"], + "properties": { + "title": { + "type": "string", + "description": "文章标题" + }, + "content": { + "type": "string", + "description": "文章内容" + }, + "summary": { + "type": "string", + "description": "文章摘要" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "标签列表" + }, + "series_id": { + "type": "integer", + "description": "系列ID" + }, + "visibility": { + "type": "integer", + "description": "可见度" + } + } + }, + "CreateArticleResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + } + } + }, + "Article": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + }, + "title": { + "type": "string", + "description": "文章标题" + }, + "content": { + "type": "string", + "description": "文章内容" + }, + "summary": { + "type": "string", + "description": "文章摘要" + }, + "user": { + "$ref": "#/components/schemas/User" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "created_on": { + "type": "integer", + "description": "创建时间戳" + }, + "updated_on": { + "type": "integer", + "description": "更新时间戳" + }, + "star_count": { + "type": "integer", + "description": "点赞数量" + }, + "comment_count": { + "type": "integer", + "description": "评论数量" + }, + "collection_count": { + "type": "integer", + "description": "收藏数量" + } + } + }, + "UpdateArticleReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + }, + "title": { + "type": "string", + "description": "文章标题" + }, + "content": { + "type": "string", + "description": "文章内容" + }, + "summary": { + "type": "string", + "description": "文章摘要" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "标签列表" + } + } + }, + "DeleteArticleReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + } + } + }, + "ArticleStarReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + } + } + }, + "ArticleStarResp": { + "type": "object", + "properties": { + "is_star": { + "type": "boolean", + "description": "是否已点赞" + } + } + }, + "ArticleCollectionReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + } + } + }, + "ArticleCollectionResp": { + "type": "object", + "properties": { + "is_collection": { + "type": "boolean", + "description": "是否已收藏" + } + } + }, + "CreateArticleCommentReq": { + "type": "object", + "required": ["article_id", "content"], + "properties": { + "article_id": { + "type": "integer", + "description": "文章ID" + }, + "content": { + "type": "string", + "description": "评论内容" + } + } + }, + "CreateArticleCommentResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "评论ID" + } + } + }, + "DeleteArticleCommentReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "评论ID" + } + } + }, + "CreateArticleCommentReplyReq": { + "type": "object", + "required": ["comment_id", "content"], + "properties": { + "comment_id": { + "type": "integer", + "description": "评论ID" + }, + "content": { + "type": "string", + "description": "回复内容" + } + } + }, + "CreateArticleCommentReplyResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "回复ID" + } + } + }, + "DeleteArticleCommentReplyReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "回复ID" + } + } + }, + "CreateArticleSeriesReq": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "系列名称" + }, + "description": { + "type": "string", + "description": "系列描述" + } + } + }, + "CreateArticleSeriesResp": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "系列ID" + } + } + }, + "UpdateArticleSeriesReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "系列ID" + }, + "name": { + "type": "string", + "description": "系列名称" + }, + "description": { + "type": "string", + "description": "系列描述" + } + } + }, + "DeleteArticleSeriesReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "系列ID" + } + } + }, + "AdminDeleteArticleReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "文章ID" + } + } + }, + "FollowUserReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + } + } + }, + "UnfollowUserReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + } + } + }, + "ListFollowsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "ListFollowingsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "BlockUserReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + } + } + }, + "UnblockUserReq": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + } + } + }, + "ListBlocksResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "GetIndexTrendsResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tweet" + } + }, + "total": { + "type": "integer", + "description": "总数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "page_size": { + "type": "integer", + "description": "每页数量" + } + } + }, + "ChangeUserStatusReq": { + "type": "object", + "required": ["user_id", "status"], + "properties": { + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "status": { + "type": "integer", + "description": "用户状态 0:正常 1:禁言 2:封禁" + } + } + }, + "SiteInfoResp": { + "type": "object", + "properties": { + "total_users": { + "type": "integer", + "description": "总用户数" + }, + "total_tweets": { + "type": "integer", + "description": "总动态数" + }, + "total_articles": { + "type": "integer", + "description": "总文章数" + }, + "total_comments": { + "type": "integer", + "description": "总评论数" + }, + "active_users_today": { + "type": "integer", + "description": "今日活跃用户数" + }, + "new_users_today": { + "type": "integer", + "description": "今日新增用户数" + } } } } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/internal/conf/db.go b/internal/conf/db.go index f2cbeaa7..33a4e3df 100644 --- a/internal/conf/db.go +++ b/internal/conf/db.go @@ -42,6 +42,8 @@ const ( TableUser = "user" TableUserRelation = "user_relation" TableUserMetric = "user_metric" + TableOperationSettings = "operation_settings" + TableUserCheckin = "user_checkin" TableWalletRecharge = "wallet_recharge" TableWalletStatement = "wallet_statement" ) diff --git a/internal/conf/setting.go b/internal/conf/setting.go index b1ff1cc8..cc5d5ff3 100644 --- a/internal/conf/setting.go +++ b/internal/conf/setting.go @@ -410,6 +410,8 @@ func (s *databaseConf) TableNames() (res TableNameMap) { TableUser, TableUserRelation, TableUserMetric, + TableOperationSettings, + TableUserCheckin, TableWalletRecharge, TableWalletStatement, } diff --git a/internal/core/cs/cs.go b/internal/core/cs/cs.go index 4ac8eae9..1a606c69 100644 --- a/internal/core/cs/cs.go +++ b/internal/core/cs/cs.go @@ -6,3 +6,36 @@ // model define package cs + +// RelationTyp 用户关系类型 +type RelationTyp uint8 + +const ( + RelationUnknown RelationTyp = iota + RelationSelf + RelationFriend + RelationFollower + RelationFollowing + RelationAdmin + RelationGuest +) + +// String 返回关系类型的字符串表示 +func (t RelationTyp) String() string { + switch t { + case RelationSelf: + return "self" + case RelationFriend: + return "friend" + case RelationFollower: + return "follower" + case RelationFollowing: + return "following" + case RelationAdmin: + return "admin" + case RelationUnknown: + fallthrough + default: + return "unknown" + } +} diff --git a/internal/core/cs/timeline.go b/internal/core/cs/timeline.go index 118f371c..37dbd225 100644 --- a/internal/core/cs/timeline.go +++ b/internal/core/cs/timeline.go @@ -4,8 +4,29 @@ package cs +import ( + "github.com/rocboss/paopao-ce/internal/core/ms" +) + // TweetBox 推文列表盒子,包含其他一些关于推文列表的信息 type TweetBox struct { Tweets TweetList Total int64 } + +// ContentBox 综合内容列表盒子,包含动态和长文章 +type ContentBox struct { + Contents ContentList `json:"contents"` + Total int64 `json:"total"` +} + +// ContentItem 内容项 +type ContentItem struct { + Type ms.ContentType `json:"type"` + Tweet *TweetItem `json:"tweet,omitempty"` + Article interface{} `json:"article,omitempty"` + CreatedOn int64 `json:"created_on"` +} + +// ContentList 内容列表 +type ContentList []*ContentItem diff --git a/internal/core/cs/user.go b/internal/core/cs/user.go index b3ab8ed6..3cb32b63 100644 --- a/internal/core/cs/user.go +++ b/internal/core/cs/user.go @@ -4,30 +4,6 @@ package cs -const ( - RelationUnknown RelationTyp = iota - RelationSelf - RelationFriend - RelationFollower - RelationFollowing - RelationAdmin - RelationGuest -) - -type ( - // UserInfoList 用户信息列表 - UserInfoList []*UserInfo - - // - RelationTyp uint8 - - VistUser struct { - Username string - UserId int64 - RelTyp RelationTyp - } -) - // UserInfo 用户基本信息 type UserInfo struct { ID int64 `json:"id"` @@ -54,21 +30,3 @@ type UserProfile struct { Level int `json:"level"` } -func (t RelationTyp) String() string { - switch t { - case RelationSelf: - return "self" - case RelationFriend: - return "friend" - case RelationFollower: - return "follower" - case RelationFollowing: - return "following" - case RelationAdmin: - return "admin" - case RelationUnknown: - fallthrough - default: - return "unknown" - } -} diff --git a/internal/core/level.go b/internal/core/level.go deleted file mode 100644 index 527e067e..00000000 --- a/internal/core/level.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2023 ROC. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package core - -import ( - "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" -) - -// UserLevelService 用户等级服务接口 -type UserLevelService interface { - // GetLevelByExperience 根据经验值获取用户等级 - GetLevelByExperience(experience int) (*dbr.UserLevel, error) - - // GetAllLevels 获取所有等级配置 - GetAllLevels() ([]*dbr.UserLevel, error) - - // CreateLevel 创建等级配置 - CreateLevel(level *dbr.UserLevel) (*dbr.UserLevel, error) - - // UpdateLevel 更新等级配置 - UpdateLevel(level *dbr.UserLevel) error - - // DeleteLevel 删除等级配置 - DeleteLevel(level int) error -} diff --git a/internal/core/ms/articles.go b/internal/core/ms/articles.go new file mode 100644 index 00000000..a941d14a --- /dev/null +++ b/internal/core/ms/articles.go @@ -0,0 +1,58 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package ms + +// ArticleContentT 长文章内容类型 +type ArticleContentT int + +const ( + ArticleContentTypeTitle ArticleContentT = 1 + iota + ArticleContentTypeText + ArticleContentTypeImage + ArticleContentTypeVideo + ArticleContentTypeAudio + ArticleContentTypeLink + ArticleContentTypeAttachment + ArticleContentTypePaidAttachment +) + +// ArticleVisibleT 长文章可见性类型 +type ArticleVisibleT int + +const ( + ArticleVisiblePrivate ArticleVisibleT = 0 + iota // 私密 + ArticleVisiblePaid // 付费可见 + ArticleVisibleReserved1 // 保留 + ArticleVisibleReserved2 // 保留 + ArticleVisibleFriend // 好友可见 + ArticleVisibleFollowing // 关注可见 + ArticleVisibleReserved3 // 保留 + ArticleVisibleReserved4 // 保留 + ArticleVisiblePublic // 公开 +) + +// Article 文章别名 +type Article = interface{} + +// ArticleContent 文章内容别名 +type ArticleContent = interface{} + +// ArticleSeries 文章系列别名 +type ArticleSeries = interface{} + +// ArticleSeriesItem 系列文章关联别名 +type ArticleSeriesItem = interface{} + +// ArticleCollection 文章收藏别名 +type ArticleCollection = interface{} + +// ArticleStar 文章点赞别名 +type ArticleStar = interface{} + +// ArticleComment 文章评论别名 +type ArticleComment = interface{} + +// ArticleCommentReply 文章评论回复别名 +type ArticleCommentReply = interface{} diff --git a/internal/core/ms/timeline.go b/internal/core/ms/timeline.go index eb1169f4..7c66db55 100644 --- a/internal/core/ms/timeline.go +++ b/internal/core/ms/timeline.go @@ -8,3 +8,25 @@ type IndexTweetList struct { Tweets []*PostFormated Total int64 } + +// ContentType 内容类型 +type ContentType int + +const ( + ContentTypeTweet ContentType = iota + 1 + ContentTypeArticle +) + +// IndexContentItem 首页内容项 +type IndexContentItem struct { + Type ContentType `json:"type"` + Tweet *PostFormated `json:"tweet,omitempty"` + Article interface{} `json:"article,omitempty"` + CreatedOn int64 `json:"created_on"` +} + +// IndexContentList 首页综合内容列表 +type IndexContentList struct { + Contents []*IndexContentItem `json:"contents"` + Total int64 `json:"total"` +} diff --git a/internal/core/timeline.go b/internal/core/timeline.go index 722e79f3..8abda46f 100644 --- a/internal/core/timeline.go +++ b/internal/core/timeline.go @@ -18,3 +18,13 @@ type IndexPostsService interface { type IndexPostsServantA interface { IndexPosts(user *ms.User, limit int, offset int) (*cs.TweetBox, error) } + +// IndexContentService 广场首页综合内容服务(包含动态和长文章) +type IndexContentService interface { + IndexContent(user *ms.User, offset int, limit int) (*ms.IndexContentList, error) +} + +// IndexContentServantA 广场首页综合内容服务(版本A) +type IndexContentServantA interface { + IndexContent(user *ms.User, limit int, offset int) (*cs.ContentBox, error) +} diff --git a/internal/core/tweets.go b/internal/core/tweets.go index 3c49136c..32b8d76b 100644 --- a/internal/core/tweets.go +++ b/internal/core/tweets.go @@ -7,6 +7,7 @@ package core import ( "github.com/rocboss/paopao-ce/internal/core/cs" "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" ) // TweetService 推文检索服务 @@ -23,9 +24,9 @@ type TweetService interface { GetPostAttatchmentBill(postID, userID int64) (*ms.PostAttachmentBill, error) GetPostContentsByIDs(ids []int64) ([]*ms.PostContent, error) GetPostContentByID(id int64) (*ms.PostContent, error) - ListUserStarTweets(user *cs.VistUser, limit int, offset int) ([]*ms.PostStar, int64, error) - ListUserMediaTweets(user *cs.VistUser, limit int, offset int) ([]*ms.Post, int64, error) - ListUserCommentTweets(user *cs.VistUser, limit int, offset int) ([]*ms.Post, int64, error) + ListUserStarTweets(user *dbr.VistUser, limit int, offset int) ([]*ms.PostStar, int64, error) + ListUserMediaTweets(user *dbr.VistUser, limit int, offset int) ([]*ms.Post, int64, error) + ListUserCommentTweets(user *dbr.VistUser, limit int, offset int) ([]*ms.Post, int64, error) ListUserTweets(userId int64, style uint8, justEssence bool, limit, offset int) ([]*ms.Post, int64, error) ListFollowingTweets(userId int64, limit, offset int) ([]*ms.Post, int64, error) ListIndexNewestTweets(limit, offset int) ([]*ms.Post, int64, error) diff --git a/internal/core/user.go b/internal/core/user.go index 1b3f7209..08203113 100644 --- a/internal/core/user.go +++ b/internal/core/user.go @@ -5,8 +5,8 @@ package core import ( - "github.com/rocboss/paopao-ce/internal/core/cs" "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" ) // UserManageService 用户管理服务 @@ -16,7 +16,7 @@ type UserManageService interface { GetUserByPhone(phone string) ([]*ms.User, error) GetUsersByIDs(ids []int64) ([]*ms.User, error) GetUsersByKeyword(keyword string) ([]*ms.User, error) - UserProfileByName(username string) (*cs.UserProfile, error) + UserProfileByName(username string) (*dbr.UserProfile, error) CreateUser(user *ms.User) (*ms.User, error) UpdateUser(user *ms.User) error GetRegisterUserCount() (int64, error) diff --git a/internal/dao/cache/common.go b/internal/dao/cache/common.go index fe22b895..28be50ae 100644 --- a/internal/dao/cache/common.go +++ b/internal/dao/cache/common.go @@ -11,8 +11,8 @@ import ( "github.com/RoaringBitmap/roaring/roaring64" "github.com/rocboss/paopao-ce/internal/conf" "github.com/rocboss/paopao-ce/internal/core" - "github.com/rocboss/paopao-ce/internal/core/cs" "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" ) type cacheDataService struct { @@ -62,12 +62,12 @@ func (s *cacheDataService) GetUserByUsername(username string) (res *ms.User, err return } -func (s *cacheDataService) UserProfileByName(username string) (res *cs.UserProfile, err error) { +func (s *cacheDataService) UserProfileByName(username string) (res *dbr.UserProfile, err error) { // 先从缓存获取, 不处理错误 key := conf.KeyUserProfileByName.Get(username) if data, xerr := s.ac.Get(key); xerr == nil { buf := bytes.NewBuffer(data) - res = &cs.UserProfile{} + res = &dbr.UserProfile{} err = gob.NewDecoder(buf).Decode(res) return } diff --git a/internal/dao/jinzhu/block.go.disabled b/internal/dao/jinzhu/block.go.disabled new file mode 100644 index 00000000..9f882479 --- /dev/null +++ b/internal/dao/jinzhu/block.go.disabled @@ -0,0 +1,77 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package jinzhu + +import ( + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +var ( + _ core.UserBlockManageService = (*userBlockManageSrv)(nil) +) + +type userBlockManageSrv struct { + db *gorm.DB + b *dbr.UserBlock + u *dbr.User +} + +func newUserBlockManageService(db *gorm.DB) core.UserBlockManageService { + return &userBlockManageSrv{ + db: db, + b: &dbr.UserBlock{}, + u: &dbr.User{}, + } +} + +func (s *userBlockManageSrv) BlockUser(userId int64, targetUserId int64, reason string) error { + if _, err := s.b.GetUserBlock(s.db, userId, targetUserId); err != nil { + block := &dbr.UserBlock{ + UserId: userId, + TargetUserId: targetUserId, + Reason: reason, + } + if _, err = block.Create(s.db); err != nil { + logrus.Errorf("userBlockManageSrv.blockUser create new block err:%s", err) + return err + } + } + return nil +} + +func (s *userBlockManageSrv) UnblockUser(userId int64, targetUserId int64) error { + return s.b.DelUserBlock(s.db, userId, targetUserId) +} + +func (s *userBlockManageSrv) ListBlocks(userId int64, limit, offset int) (*ms.UserBlockList, error) { + blocks, total, err := s.b.ListUserBlocks(s.db, userId, limit, offset) + if err != nil { + return nil, err + } + res := &ms.UserBlockList{ + Total: total, + } + for _, block := range blocks { + res.Blocks = append(res.Blocks, ms.UserBlockItem{ + UserId: block.UserId, + TargetUserId: block.TargetUserId, + Reason: block.Reason, + CreatedOn: block.CreatedOn, + }) + } + return res, nil +} + +func (s *userBlockManageSrv) IsBlocked(userId int64, targetUserId int64) bool { + return s.b.IsBlocked(s.db, userId, targetUserId) +} + +func (s *userBlockManageSrv) IsBlocking(userId int64, targetUserId int64) bool { + return s.b.IsBlocked(s.db, targetUserId, userId) +} diff --git a/internal/dao/jinzhu/dbr/block.go b/internal/dao/jinzhu/dbr/block.go new file mode 100644 index 00000000..8d0f1830 --- /dev/null +++ b/internal/dao/jinzhu/dbr/block.go @@ -0,0 +1,65 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package dbr + +import ( + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type UserBlock struct { + *Model + UserId int64 `json:"user_id"` + TargetUserId int64 `json:"target_user_id"` + Reason string `json:"reason"` +} + +func (UserBlock) TableName() string { + return "p_user_block" +} + +func (b *UserBlock) GetUserBlock(db *gorm.DB, userId, targetUserId int64) (*UserBlock, error) { + var block UserBlock + err := db.Unscoped().Where("user_id = ? AND target_user_id = ?", userId, targetUserId).First(&block).Error + if err != nil { + logrus.Debugf("UserBlock.GetUserBlock get block error:%s", err) + return nil, err + } + return &block, nil +} + +func (b *UserBlock) DelUserBlock(db *gorm.DB, userId, targetUserId int64) error { + return db.Unscoped().Where("user_id = ? AND target_user_id = ?", userId, targetUserId).Delete(b).Error +} + +func (b *UserBlock) ListUserBlocks(db *gorm.DB, userId int64, limit int, offset int) (res []*UserBlock, total int64, err error) { + db = db.Model(b).Where("user_id=?", userId) + if err = db.Count(&total).Error; err != nil { + return + } + if offset >= 0 && limit > 0 { + db = db.Offset(offset).Limit(limit) + } + if err = db.Find(&res).Error; err != nil { + return + } + return +} + +func (b *UserBlock) IsBlocked(db *gorm.DB, userId, targetUserId int64) bool { + if _, err := b.GetUserBlock(db, userId, targetUserId); err == nil { + return true + } + return false +} + +func (b *UserBlock) Create(db *gorm.DB) (*UserBlock, error) { + err := db.Create(b).Error + return b, err +} + +func (b *UserBlock) UpdateInUnscoped(db *gorm.DB) error { + return db.Unscoped().Save(b).Error +} diff --git a/internal/dao/jinzhu/dbr/common.go b/internal/dao/jinzhu/dbr/common.go new file mode 100644 index 00000000..ed660f27 --- /dev/null +++ b/internal/dao/jinzhu/dbr/common.go @@ -0,0 +1,75 @@ +// Copyright 2023 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package dbr + +// RelationTyp 用户关系类型 +type RelationTyp uint8 + +const ( + RelationUnknown RelationTyp = iota + RelationSelf + RelationFriend + RelationFollower + RelationFollowing + RelationAdmin + RelationGuest +) + +// String 返回关系类型的字符串表示 +func (t RelationTyp) String() string { + switch t { + case RelationSelf: + return "self" + case RelationFriend: + return "friend" + case RelationFollower: + return "follower" + case RelationFollowing: + return "following" + case RelationAdmin: + return "admin" + case RelationUnknown: + fallthrough + default: + return "unknown" + } +} + +// UserInfo 用户基本信息 +type UserInfo struct { + ID int64 `json:"id"` + Nickname string `json:"nickname"` + Username string `json:"username"` + Status int `json:"status"` + Avatar string `json:"avatar"` + IsAdmin bool `json:"is_admin"` + CreatedOn int64 `json:"created_on"` +} + +// UserInfoList 用户信息列表 +type UserInfoList []*UserInfo + +// VistUser 访问用户信息 +type VistUser struct { + Username string + UserId int64 + RelTyp RelationTyp +} + +// UserProfile 用户资料 +type UserProfile struct { + ID int64 `json:"id" db:"id"` + Nickname string `json:"nickname"` + Username string `json:"username"` + Phone string `json:"phone"` + Status int `json:"status"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + IsAdmin bool `json:"is_admin"` + CreatedOn int64 `json:"created_on"` + TweetsCount int `json:"tweets_count"` + Experience int `json:"experience"` + Level int `json:"level"` +} diff --git a/internal/dao/jinzhu/dbr/level.go b/internal/dao/jinzhu/dbr/level.go deleted file mode 100644 index a93c1ee0..00000000 --- a/internal/dao/jinzhu/dbr/level.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023 ROC. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package dbr - -import ( - "gorm.io/gorm" -) - -// UserLevel 用户等级配置模型 -type UserLevel struct { - Level int `gorm:"column:level;primaryKey" json:"level"` // 等级 - MinExperience int `gorm:"column:min_experience" json:"min_experience"` // 最小经验值 - MaxExperience int `gorm:"column:max_experience" json:"max_experience"` // 最大经验值 - Name string `gorm:"column:name" json:"name"` // 等级名称 - Description string `gorm:"column:description" json:"description"` // 等级描述 -} - -// TableName 指定表名映射 -func (u *UserLevel) TableName() string { - return "p_user_level" -} - -// GetByExperience 根据经验值获取等级 -func (u *UserLevel) GetByExperience(db *gorm.DB, experience int) (*UserLevel, error) { - var level UserLevel - err := db.Where("min_experience <= ? AND max_experience >= ?", experience, experience).First(&level).Error - return &level, err -} - -// GetAll 获取所有等级配置 -func (u *UserLevel) GetAll(db *gorm.DB) ([]*UserLevel, error) { - var levels []*UserLevel - err := db.Order("level ASC").Find(&levels).Error - return levels, err -} - -// Create 创建等级配置 -func (u *UserLevel) Create(db *gorm.DB) (*UserLevel, error) { - err := db.Create(&u).Error - return u, err -} - -// Update 更新等级配置 -func (u *UserLevel) Update(db *gorm.DB) error { - return db.Model(&UserLevel{}).Where("level = ?", u.Level).Save(u).Error -} - -// Delete 删除等级配置 -func (u *UserLevel) Delete(db *gorm.DB) error { - return db.Where("level = ?", u.Level).Delete(&UserLevel{}).Error -} diff --git a/internal/dao/jinzhu/dbr/mute.go b/internal/dao/jinzhu/dbr/mute.go new file mode 100644 index 00000000..e67e6f77 --- /dev/null +++ b/internal/dao/jinzhu/dbr/mute.go @@ -0,0 +1,65 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package dbr + +import ( + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type UserMute struct { + *Model + UserId int64 `json:"user_id"` + TargetUserId int64 `json:"target_user_id"` + Reason string `json:"reason"` +} + +func (UserMute) TableName() string { + return "p_user_mute" +} + +func (m *UserMute) GetUserMute(db *gorm.DB, userId, targetUserId int64) (*UserMute, error) { + var mute UserMute + err := db.Unscoped().Where("user_id = ? AND target_user_id = ?", userId, targetUserId).First(&mute).Error + if err != nil { + logrus.Debugf("UserMute.GetUserMute get mute error:%s", err) + return nil, err + } + return &mute, nil +} + +func (m *UserMute) DelUserMute(db *gorm.DB, userId, targetUserId int64) error { + return db.Unscoped().Where("user_id = ? AND target_user_id = ?", userId, targetUserId).Delete(m).Error +} + +func (m *UserMute) ListUserMutes(db *gorm.DB, userId int64, limit int, offset int) (res []*UserMute, total int64, err error) { + db = db.Model(m).Where("user_id=?", userId) + if err = db.Count(&total).Error; err != nil { + return + } + if offset >= 0 && limit > 0 { + db = db.Offset(offset).Limit(limit) + } + if err = db.Find(&res).Error; err != nil { + return + } + return +} + +func (m *UserMute) IsMuted(db *gorm.DB, userId, targetUserId int64) bool { + if _, err := m.GetUserMute(db, userId, targetUserId); err == nil { + return true + } + return false +} + +func (m *UserMute) Create(db *gorm.DB) (*UserMute, error) { + err := db.Create(m).Error + return m, err +} + +func (m *UserMute) UpdateInUnscoped(db *gorm.DB) error { + return db.Unscoped().Save(m).Error +} diff --git a/internal/dao/jinzhu/dbr/post_star.go b/internal/dao/jinzhu/dbr/post_star.go index 33173e4b..47cafb56 100644 --- a/internal/dao/jinzhu/dbr/post_star.go +++ b/internal/dao/jinzhu/dbr/post_star.go @@ -7,7 +7,6 @@ package dbr import ( "time" - "github.com/rocboss/paopao-ce/internal/core/cs" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -53,7 +52,7 @@ func (p *PostStar) Delete(db *gorm.DB) error { }).Error } -func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, typ cs.RelationTyp, limit int, offset int) (res []*PostStar, err error) { +func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, typ RelationTyp, limit int, offset int) (res []*PostStar, err error) { tn := db.NamingStrategy.TableName("PostStar") + "." if offset >= 0 && limit > 0 { db = db.Offset(offset).Limit(limit) @@ -70,11 +69,11 @@ func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, typ cs.RelationTyp } db = db.Joins("Post") switch typ { - case cs.RelationAdmin: + case RelationAdmin: // admin have all permition to visit all type tweets - case cs.RelationFriend: + case RelationFriend: db = db.Where("visibility = ? OR visibility = ?", PostVisitPublic, PostVisitFriend) - case cs.RelationSelf: + case RelationSelf: db = db.Where("visibility <> ? OR (visibility = ? AND ? = ?)", PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID) default: db = db.Where("visibility=?", PostVisitPublic) @@ -84,7 +83,7 @@ func (p *PostStar) List(db *gorm.DB, conditions *ConditionsT, typ cs.RelationTyp return } -func (p *PostStar) Count(db *gorm.DB, typ cs.RelationTyp, conditions *ConditionsT) (res int64, err error) { +func (p *PostStar) Count(db *gorm.DB, typ RelationTyp, conditions *ConditionsT) (res int64, err error) { tn := db.NamingStrategy.TableName("PostStar") + "." if p.PostID > 0 { db = db.Where(tn+"post_id = ?", p.PostID) @@ -99,11 +98,11 @@ func (p *PostStar) Count(db *gorm.DB, typ cs.RelationTyp, conditions *Conditions } db = db.Joins("Post") switch typ { - case cs.RelationAdmin: + case RelationAdmin: // admin have all permition to visit all type tweets - case cs.RelationFriend: + case RelationFriend: db = db.Where("visibility = ? OR visibility = ?", PostVisitPublic, PostVisitFriend) - case cs.RelationSelf: + case RelationSelf: db = db.Where("visibility <> ? OR (visibility = ? AND ? = ?)", PostVisitPrivate, PostVisitPrivate, clause.Column{Table: "Post", Name: "user_id"}, p.UserID) default: db = db.Where("visibility=?", PostVisitPublic) diff --git a/internal/dao/jinzhu/dbr/user.go b/internal/dao/jinzhu/dbr/user.go index b216733f..b6ab8412 100644 --- a/internal/dao/jinzhu/dbr/user.go +++ b/internal/dao/jinzhu/dbr/user.go @@ -5,7 +5,6 @@ package dbr import ( - "github.com/rocboss/paopao-ce/internal/core/cs" "gorm.io/gorm" ) @@ -25,7 +24,7 @@ type User struct { Avatar string `json:"avatar"` Balance int64 `json:"balance"` IsAdmin bool `json:"is_admin"` - Experience int `gorm:"-" json:"experience"` + Experience int `json:"experience"` Level int `gorm:"-" json:"level"` } @@ -98,7 +97,7 @@ func (u *User) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([] return users, nil } -func (u *User) ListUserInfoById(db *gorm.DB, ids []int64) (res cs.UserInfoList, err error) { +func (u *User) ListUserInfoById(db *gorm.DB, ids []int64) (res UserInfoList, err error) { err = db.Model(u).Where("id IN ?", ids).Find(&res).Error return } diff --git a/internal/dao/jinzhu/jinzhu.go b/internal/dao/jinzhu/jinzhu.go index 0e6ea6cc..16b6d64a 100644 --- a/internal/dao/jinzhu/jinzhu.go +++ b/internal/dao/jinzhu/jinzhu.go @@ -42,7 +42,6 @@ type dataSrv struct { core.TrendsManageServantA core.UserManageService core.UserMetricServantA - core.UserLevelService core.ContactManageService core.FollowingManageService core.UserRelationService @@ -63,14 +62,12 @@ func NewDataService() (core.DataService, core.VersionInfo) { pvs := security.NewPhoneVerifyService() tms := newTweetMetricServentA(db) ums := newUserMetricServentA(db) - uls := newUserLevelService(db) cms := newCommentMetricServentA(db) cis := cache.NewEventCacheIndexSrv(tms) ds := &dataSrv{ TweetMetricServantA: tms, CommentMetricServantA: cms, UserMetricServantA: ums, - UserLevelService: uls, WalletService: newWalletService(db), MessageService: newMessageService(db), TopicService: newTopicService(db), @@ -80,7 +77,7 @@ func NewDataService() (core.DataService, core.VersionInfo) { CommentService: newCommentService(db), CommentManageService: newCommentManageService(db), TrendsManageServantA: newTrendsManageServentA(db), - UserManageService: newUserManageService(db, ums, uls), + UserManageService: newUserManageService(db, ums), ContactManageService: newContactManageService(db), FollowingManageService: newFollowingManageService(db), UserRelationService: newUserRelationService(db), diff --git a/internal/dao/jinzhu/level.go b/internal/dao/jinzhu/level.go deleted file mode 100644 index 1d3cb8f2..00000000 --- a/internal/dao/jinzhu/level.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2023 ROC. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package jinzhu - -import ( - "github.com/rocboss/paopao-ce/internal/core" - "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" - "gorm.io/gorm" -) - -var ( - _ core.UserLevelService = (*userLevelSrv)(nil) -) - -// userLevelSrv 实现用户等级服务接口 -type userLevelSrv struct { - db *gorm.DB -} - -// newUserLevelService 创建用户等级服务实例 -func newUserLevelService(db *gorm.DB) core.UserLevelService { - return &userLevelSrv{ - db: db, - } -} - -// GetLevelByExperience 根据经验值获取用户等级 -func (s *userLevelSrv) GetLevelByExperience(experience int) (*dbr.UserLevel, error) { - level := &dbr.UserLevel{} - return level.GetByExperience(s.db, experience) -} - -// GetAllLevels 获取所有等级配置 -func (s *userLevelSrv) GetAllLevels() ([]*dbr.UserLevel, error) { - level := &dbr.UserLevel{} - return level.GetAll(s.db) -} - -// CreateLevel 创建等级配置 -func (s *userLevelSrv) CreateLevel(level *dbr.UserLevel) (*dbr.UserLevel, error) { - return level.Create(s.db) -} - -// UpdateLevel 更新等级配置 -func (s *userLevelSrv) UpdateLevel(level *dbr.UserLevel) error { - return level.Update(s.db) -} - -// DeleteLevel 删除等级配置 -func (s *userLevelSrv) DeleteLevel(level int) error { - return (&dbr.UserLevel{Level: level}).Delete(s.db) -} diff --git a/internal/dao/jinzhu/level.go.disabled b/internal/dao/jinzhu/level.go.disabled new file mode 100644 index 00000000..535e4934 --- /dev/null +++ b/internal/dao/jinzhu/level.go.disabled @@ -0,0 +1,142 @@ +// Copyright 2023 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package jinzhu + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/dao/cache" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" + "gorm.io/gorm" +) + +const ( + // 等级配置缓存键 + levelConfigCacheKey = "user_level_config" + // 缓存过期时间(30分钟) + levelConfigCacheExpire = 30 * 60 +) + +var ( + levelConfigCacheOnce sync.Once + levelConfigAppCache core.AppCache +) + +// GetUserLevelByExperience 根据经验值获取用户等级 +func GetUserLevelByExperience(db *gorm.DB, experience int) (int, error) { + // 尝试从缓存获取等级配置 + levelMap, err := getLevelConfigFromCache() + if err != nil { + // 缓存获取失败,从数据库读取 + levelMap, err = getLevelConfigFromDB(db) + if err != nil { + // 如果获取失败,使用默认等级配置 + return getDefaultLevel(experience), nil + } + } + + // 遍历等级配置找到对应的等级 + for levelKey, expRange := range levelMap { + if experience >= expRange[0] && experience <= expRange[1] { + var level int + fmt.Sscanf(levelKey[2:], "%d", &level) // 从"lv1"提取数字 + return level, nil + } + } + + // 如果没有找到对应等级,返回默认等级 + return getDefaultLevel(experience), nil +} + +// getLevelConfigFromCache 从缓存获取等级配置 +func getLevelConfigFromCache() (map[string][2]int, error) { + levelConfigCacheOnce.Do(func() { + levelConfigAppCache = cache.NewAppCache() + }) + + cacheData, err := levelConfigAppCache.Get(levelConfigCacheKey) + if err != nil { + return nil, err + } + + var levelMap map[string][2]int + if err := json.Unmarshal(cacheData, &levelMap); err != nil { + return nil, err + } + + return levelMap, nil +} + +// getLevelConfigFromDB 从数据库获取等级配置并缓存 +func getLevelConfigFromDB(db *gorm.DB) (map[string][2]int, error) { + setting := &dbr.OperationSetting{} + levelConfig, err := setting.GetByKey(db, "level_to_exp") + if err != nil { + // 如果获取失败,返回错误 + return nil, err + } + + var levelMap map[string][2]int + if err := json.Unmarshal([]byte(levelConfig.Value), &levelMap); err != nil { + return nil, err + } + + // 将配置缓存起来 + levelConfigCacheOnce.Do(func() { + levelConfigAppCache = cache.NewAppCache() + }) + cacheData, _ := json.Marshal(levelMap) + levelConfigAppCache.Set(levelConfigCacheKey, cacheData, levelConfigCacheExpire) + + return levelMap, nil +} + +// UpdateLevelConfigCache 更新等级配置缓存(当运营设置更新时调用) +func UpdateLevelConfigCache() error { + levelConfigCacheOnce.Do(func() { + levelConfigAppCache = cache.NewAppCache() + }) + return levelConfigAppCache.Delete(levelConfigCacheKey) +} + +// UpdateOperationSetting 更新运营设置(带缓存清理) +func UpdateOperationSetting(db *gorm.DB, key, value, description string) error { + setting := &dbr.OperationSetting{ + Key: key, + Value: value, + Description: description, + } + + // 更新数据库 + err := setting.Update(db) + if err != nil { + return err + } + + // 如果是等级配置更新,清理缓存 + if key == "level_to_exp" { + UpdateLevelConfigCache() + } + + return nil +} + +// getDefaultLevel 获取默认等级 +func getDefaultLevel(experience int) int { + if experience >= 0 && experience <= 100 { + return 1 + } else if experience >= 101 && experience <= 300 { + return 2 + } else if experience >= 301 && experience <= 600 { + return 3 + } else if experience >= 601 && experience <= 1000 { + return 4 + } else { + return 5 + } +} diff --git a/internal/dao/jinzhu/mute.go.disabled b/internal/dao/jinzhu/mute.go.disabled new file mode 100644 index 00000000..a598f727 --- /dev/null +++ b/internal/dao/jinzhu/mute.go.disabled @@ -0,0 +1,73 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package jinzhu + +import ( + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +var ( + _ core.UserMuteManageService = (*userMuteManageSrv)(nil) +) + +type userMuteManageSrv struct { + db *gorm.DB + m *dbr.UserMute + u *dbr.User +} + +func newUserMuteManageService(db *gorm.DB) core.UserMuteManageService { + return &userMuteManageSrv{ + db: db, + m: &dbr.UserMute{}, + u: &dbr.User{}, + } +} + +func (s *userMuteManageSrv) MuteUser(userId int64, targetUserId int64, reason string) error { + if _, err := s.m.GetUserMute(s.db, userId, targetUserId); err != nil { + mute := &dbr.UserMute{ + UserId: userId, + TargetUserId: targetUserId, + Reason: reason, + } + if _, err = mute.Create(s.db); err != nil { + logrus.Errorf("userMuteManageSrv.muteUser create new mute err:%s", err) + return err + } + } + return nil +} + +func (s *userMuteManageSrv) UnmuteUser(userId int64, targetUserId int64) error { + return s.m.DelUserMute(s.db, userId, targetUserId) +} + +func (s *userMuteManageSrv) ListMutes(userId int64, limit, offset int) (*ms.UserMuteList, error) { + mutes, total, err := s.m.ListUserMutes(s.db, userId, limit, offset) + if err != nil { + return nil, err + } + res := &ms.UserMuteList{ + Total: total, + } + for _, mute := range mutes { + res.Mutes = append(res.Mutes, ms.UserMuteItem{ + UserId: mute.UserId, + TargetUserId: mute.TargetUserId, + Reason: mute.Reason, + CreatedOn: mute.CreatedOn, + }) + } + return res, nil +} + +func (s *userMuteManageSrv) IsMuted(userId int64, targetUserId int64) bool { + return s.m.IsMuted(s.db, userId, targetUserId) +} diff --git a/internal/dao/jinzhu/topics.go b/internal/dao/jinzhu/topics.go index 68b59f42..273cb379 100644 --- a/internal/dao/jinzhu/topics.go +++ b/internal/dao/jinzhu/topics.go @@ -183,7 +183,15 @@ func (s *topicSrv) listTags(conditions *ms.ConditionsT, limit int, offset int) ( } for _, userInfo := range userInfos { for _, item = range tagMap[userInfo.ID] { - item.User = userInfo + item.User = &cs.UserInfo{ + ID: userInfo.ID, + Nickname: userInfo.Nickname, + Username: userInfo.Username, + Status: userInfo.Status, + Avatar: userInfo.Avatar, + IsAdmin: userInfo.IsAdmin, + CreatedOn: userInfo.CreatedOn, + } } } } @@ -231,7 +239,15 @@ func (s *topicSrv) tagsFormatB(userTopicsMap map[int64]*topicInfo, tags cs.TagIn tagFormated := tag.Format() for _, user := range users { if user.ID == tagFormated.UserID { - tagFormated.User = user + tagFormated.User = &cs.UserInfo{ + ID: user.ID, + Nickname: user.Nickname, + Username: user.Username, + Status: user.Status, + Avatar: user.Avatar, + IsAdmin: user.IsAdmin, + CreatedOn: user.CreatedOn, + } } } tagList = append(tagList, tagFormated) @@ -342,7 +358,15 @@ func (s *topicSrvA) ListTags(typ cs.TagType, offset, limit int) (res cs.TagList, } for _, userInfo := range userInfos { for _, item = range tagMap[userInfo.ID] { - item.User = userInfo + item.User = &cs.UserInfo{ + ID: userInfo.ID, + Nickname: userInfo.Nickname, + Username: userInfo.Username, + Status: userInfo.Status, + Avatar: userInfo.Avatar, + IsAdmin: userInfo.IsAdmin, + CreatedOn: userInfo.CreatedOn, + } } } } diff --git a/internal/dao/jinzhu/tweets.go b/internal/dao/jinzhu/tweets.go index 8139b1c1..1f2b42a9 100644 --- a/internal/dao/jinzhu/tweets.go +++ b/internal/dao/jinzhu/tweets.go @@ -535,10 +535,10 @@ func (s *tweetSrv) GetUserPostStars(userID int64, limit int, offset int) ([]*ms. } return star.List(s.db, &dbr.ConditionsT{ "ORDER": s.db.NamingStrategy.TableName("PostStar") + ".id DESC", - }, cs.RelationSelf, limit, offset) + }, dbr.RelationTyp(cs.RelationSelf), limit, offset) } -func (s *tweetSrv) ListUserStarTweets(user *cs.VistUser, limit int, offset int) (res []*ms.PostStar, total int64, err error) { +func (s *tweetSrv) ListUserStarTweets(user *dbr.VistUser, limit int, offset int) (res []*ms.PostStar, total int64, err error) { star := &dbr.PostStar{ UserID: user.UserId, } @@ -551,14 +551,14 @@ func (s *tweetSrv) ListUserStarTweets(user *cs.VistUser, limit int, offset int) return } -func (s *tweetSrv) getUserTweets(db *gorm.DB, user *cs.VistUser, limit int, offset int) (res []*ms.Post, total int64, err error) { +func (s *tweetSrv) getUserTweets(db *gorm.DB, user *dbr.VistUser, limit int, offset int) (res []*ms.Post, total int64, err error) { visibilities := []core.PostVisibleT{core.PostVisitPublic} switch user.RelTyp { - case cs.RelationAdmin, cs.RelationSelf: + case dbr.RelationAdmin, dbr.RelationSelf: visibilities = append(visibilities, core.PostVisitPrivate, core.PostVisitFriend) - case cs.RelationFriend: + case dbr.RelationFriend: visibilities = append(visibilities, core.PostVisitFriend) - case cs.RelationGuest: + case dbr.RelationGuest: fallthrough default: // nothing @@ -575,12 +575,12 @@ func (s *tweetSrv) getUserTweets(db *gorm.DB, user *cs.VistUser, limit int, offs return } -func (s *tweetSrv) ListUserMediaTweets(user *cs.VistUser, limit int, offset int) ([]*ms.Post, int64, error) { +func (s *tweetSrv) ListUserMediaTweets(user *dbr.VistUser, limit int, offset int) ([]*ms.Post, int64, error) { db := s.db.Table(_post_by_media_).Where("user_id=?", user.UserId) return s.getUserTweets(db, user, limit, offset) } -func (s *tweetSrv) ListUserCommentTweets(user *cs.VistUser, limit int, offset int) ([]*ms.Post, int64, error) { +func (s *tweetSrv) ListUserCommentTweets(user *dbr.VistUser, limit int, offset int) ([]*ms.Post, int64, error) { db := s.db.Table(_post_by_comment_).Where("comment_user_id=?", user.UserId) return s.getUserTweets(db, user, limit, offset) } @@ -589,7 +589,7 @@ func (s *tweetSrv) GetUserPostStarCount(userID int64) (int64, error) { star := &dbr.PostStar{ UserID: userID, } - return star.Count(s.db, cs.RelationSelf, &dbr.ConditionsT{}) + return star.Count(s.db, dbr.RelationTyp(cs.RelationSelf), &dbr.ConditionsT{}) } func (s *tweetSrv) GetUserPostCollection(postID, userID int64) (*ms.PostCollection, error) { diff --git a/internal/dao/jinzhu/user.go b/internal/dao/jinzhu/user.go index 380a01ef..a908150a 100644 --- a/internal/dao/jinzhu/user.go +++ b/internal/dao/jinzhu/user.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/rocboss/paopao-ce/internal/core" - "github.com/rocboss/paopao-ce/internal/core/cs" "github.com/rocboss/paopao-ce/internal/core/ms" "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" "gorm.io/gorm" @@ -22,7 +21,6 @@ var ( type userManageSrv struct { db *gorm.DB ums core.UserMetricServantA - uls core.UserLevelService _userProfileJoins string _userProfileWhere string @@ -33,11 +31,10 @@ type userRelationSrv struct { db *gorm.DB } -func newUserManageService(db *gorm.DB, ums core.UserMetricServantA, uls core.UserLevelService) core.UserManageService { +func newUserManageService(db *gorm.DB, ums core.UserMetricServantA) core.UserManageService { return &userManageSrv{ db: db, ums: ums, - uls: uls, _userProfileJoins: fmt.Sprintf("LEFT JOIN %s m ON %s.id=m.user_id", _userMetric_, _user_), _userProfileWhere: fmt.Sprintf("%s.username=? AND %s.is_del=0", _user_, _user_), _userProfileColumns: []string{ @@ -50,8 +47,8 @@ func newUserManageService(db *gorm.DB, ums core.UserMetricServantA, uls core.Use fmt.Sprintf("%s.balance", _user_), fmt.Sprintf("%s.is_admin", _user_), fmt.Sprintf("%s.created_on", _user_), + fmt.Sprintf("%s.experience", _user_), "m.tweets_count", - "m.experience", }, } } @@ -69,20 +66,12 @@ func (s *userManageSrv) GetUserByID(id int64) (*ms.User, error) { }, } user, err := user.Get(s.db) - metric, err := s.ums.GetUserMetric(id) if err != nil { return nil, err } - user.Experience = metric.Experience - // 获取用户等级 - level, err := s.uls.GetLevelByExperience(user.Experience) - if err == nil { - user.Level = level.Level - } else { - // 默认等级为1 - user.Level = 1 - } + // 默认等级为1 + user.Level = 1 return user, nil } @@ -95,25 +84,14 @@ func (s *userManageSrv) GetUserByUsername(username string) (*ms.User, error) { if err != nil { return nil, err } - metric, err := s.ums.GetUserMetric(user.ID) - if err != nil { - return user, nil - } - user.Experience = metric.Experience - // 获取用户等级 - level, err := s.uls.GetLevelByExperience(user.Experience) - if err == nil { - user.Level = level.Level - } else { - // 默认等级为1 - user.Level = 1 - } + // 默认等级为1 + user.Level = 1 return user, nil } -func (s *userManageSrv) UserProfileByName(username string) (res *cs.UserProfile, err error) { +func (s *userManageSrv) UserProfileByName(username string) (res *dbr.UserProfile, err error) { err = s.db.Table(_user_).Joins(s. _userProfileJoins). Where(s._userProfileWhere, username). @@ -121,14 +99,8 @@ func (s *userManageSrv) UserProfileByName(username string) (res *cs.UserProfile, First(&res).Error if err == nil && res != nil { - // 获取用户等级 - level, err := s.uls.GetLevelByExperience(res.Experience) - if err == nil { - res.Level = level.Level - } else { - // 默认等级为1 - res.Level = 1 - } + // 默认等级为1 + res.Level = 1 } return @@ -145,21 +117,10 @@ func (s *userManageSrv) GetUserByPhone(phone string) ([]*ms.User, error) { return nil, err } - // 为每个用户设置经验值 + // 为每个用户设置等级 for _, u := range users { - metric, err := s.ums.GetUserMetric(u.ID) - if err == nil { - u.Experience = metric.Experience - - // 获取用户等级 - level, err := s.uls.GetLevelByExperience(u.Experience) - if err == nil { - u.Level = level.Level - } else { - // 默认等级为1 - u.Level = 1 - } - } + // 默认等级为1 + u.Level = 1 } return users, nil @@ -174,21 +135,10 @@ func (s *userManageSrv) GetUsersByIDs(ids []int64) ([]*ms.User, error) { return nil, err } - // 为每个用户设置经验值 + // 为每个用户设置等级 for _, u := range users { - metric, err := s.ums.GetUserMetric(u.ID) - if err == nil { - u.Experience = metric.Experience - - // 获取用户等级 - level, err := s.uls.GetLevelByExperience(u.Experience) - if err == nil { - u.Level = level.Level - } else { - // 默认等级为1 - u.Level = 1 - } - } + // 默认等级为1 + u.Level = 1 } return users, nil @@ -213,21 +163,10 @@ func (s *userManageSrv) GetUsersByKeyword(keyword string) ([]*ms.User, error) { return nil, err } - // 为每个用户设置经验值 + // 为每个用户设置等级 for _, u := range users { - metric, err := s.ums.GetUserMetric(u.ID) - if err == nil { - u.Experience = metric.Experience - - // 获取用户等级 - level, err := s.uls.GetLevelByExperience(u.Experience) - if err == nil { - u.Level = level.Level - } else { - // 默认等级为1 - u.Level = 1 - } - } + // 默认等级为1 + u.Level = 1 } return users, nil diff --git a/internal/dao/search/search.go b/internal/dao/search/search.go index d8a058f8..da590e06 100644 --- a/internal/dao/search/search.go +++ b/internal/dao/search/search.go @@ -97,3 +97,4 @@ func NewBridgeTweetSearchService(ts core.TweetSearchService) core.TweetSearchSer return bts } + diff --git a/internal/model/joint/json.go b/internal/model/joint/json.go index 0b4da5c6..cc3b15d2 100644 --- a/internal/model/joint/json.go +++ b/internal/model/joint/json.go @@ -12,6 +12,23 @@ import ( "github.com/rocboss/paopao-ce/pkg/json" ) +type CacheResp struct { + Data any + JsonResp stdJson.RawMessage +} + +func (r *CacheResp) Render(c *gin.Context) { + if len(r.JsonResp) != 0 { + c.JSON(http.StatusOK, r.JsonResp) + } else { + c.JSON(http.StatusOK, &JsonResp{ + Code: 0, + Msg: "success", + Data: r.Data, + }) + } +} + type CachePageResp struct { Data *PageResp JsonResp stdJson.RawMessage diff --git a/internal/model/web/activities.go b/internal/model/web/activities.go index 6125576a..bee76de3 100644 --- a/internal/model/web/activities.go +++ b/internal/model/web/activities.go @@ -1,8 +1,60 @@ package web +import ( + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/pkg/xerror" +) + +// UserCheckInReq 用户签到请求 +type UserCheckInReq struct { + UserId int64 `json:"-"` // 用户ID,从JWT中获取 +} + +// UserCheckInResp 用户签到响应 type UserCheckInResp struct { - // CheckInCount 签到次数 - CheckInExp int `json:"check_in_exp"` - // CheckInDays 签到天数 - CheckInDays int `json:"check_in_days"` + CheckInExp int `json:"check_in_exp"` // 签到获得的经验值 + CheckInDays int `json:"check_in_days"` // 用户总签到天数 + UserLevel int `json:"user_level"` // 用户当前等级 + UserExp int `json:"user_exp"` // 用户当前经验值 +} + +// GetCheckInRankReq 获取签到排行榜请求 +type GetCheckInRankReq struct { + UserId int64 `json:"-"` // 用户ID,从JWT中获取 +} + +// GetCheckInRankResp 获取签到排行榜响应 +type GetCheckInRankResp struct { + Ranks []CheckInRankItem `json:"ranks"` +} + +// CheckInRankItem 签到排行榜项 +type CheckInRankItem struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + CheckInExp int `json:"check_in_exp"` // 今日签到获得的经验值 + CheckInTime int64 `json:"check_in_time"` // 签到时间戳 +} + +// Bind 绑定请求参数 +func (r *UserCheckInReq) Bind(c *gin.Context) error { + userId, exist := base.UserIdFrom(c) + if !exist { + return xerror.UnauthorizedAuthNotExist + } + r.UserId = userId + return nil +} + +// Bind 绑定请求参数 +func (r *GetCheckInRankReq) Bind(c *gin.Context) error { + userId, exist := base.UserIdFrom(c) + if !exist { + return xerror.UnauthorizedAuthNotExist + } + r.UserId = userId + return nil } diff --git a/internal/model/web/articles.go b/internal/model/web/articles.go new file mode 100644 index 00000000..a6f060dd --- /dev/null +++ b/internal/model/web/articles.go @@ -0,0 +1,310 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/model/joint" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/pkg/app" + "github.com/rocboss/paopao-ce/pkg/convert" + "github.com/rocboss/paopao-ce/pkg/xerror" +) + +// 长文章相关请求和响应结构 + +type ArticleContentItem struct { + Type ms.ArticleContentT `json:"type" binding:"required"` + Content string `json:"content" binding:"required"` + Sort int64 `json:"sort"` +} + +type CreateArticleReq struct { + BaseInfo `json:"-" binding:"-"` + Title string `json:"title" binding:"required,max=200"` + Summary string `json:"summary" binding:"max=500"` + SeriesID *int64 `json:"series_id"` + Contents []ArticleContentItem `json:"contents" binding:"required,dive"` + Visibility ms.ArticleVisibleT `json:"visibility"` + Price int64 `json:"price"` + Tags []string `json:"tags"` +} + +type CreateArticleResp struct { + ID int64 `json:"id"` +} + +type UpdateArticleReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` + Title string `json:"title" binding:"required,max=200"` + Summary string `json:"summary" binding:"max=500"` + SeriesID *int64 `json:"series_id"` + Contents []ArticleContentItem `json:"contents" binding:"required,dive"` + Visibility ms.ArticleVisibleT `json:"visibility"` + Price int64 `json:"price"` + Tags []string `json:"tags"` +} + +type DeleteArticleReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` +} + +type GetArticleReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `form:"id" binding:"required"` +} + +type GetArticleResp struct { + joint.CacheResp +} + +type ListArticlesReq struct { + BaseInfo `json:"-" binding:"-"` + Query string `form:"query"` + Visibility []ms.ArticleVisibleT `form:"visibility"` + Type string `form:"type"` + Style string `form:"style"` + Page int `form:"-" binding:"-"` + PageSize int `form:"-" binding:"-"` +} + +type ListArticlesResp struct { + joint.CachePageResp +} + +type ArticleStarReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` +} + +type ArticleStarResp struct { + Status bool `json:"status"` +} + +type ArticleCollectionReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` +} + +type ArticleCollectionResp struct { + Status bool `json:"status"` +} + +type CreateArticleCommentReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` + Content string `json:"content" binding:"required,max=1000"` +} + +type CreateArticleCommentResp struct { + ID int64 `json:"id"` +} + +type DeleteArticleCommentReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` +} + +type ArticleCommentsReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `form:"id" binding:"required"` + Page int `form:"-" binding:"-"` + PageSize int `form:"-" binding:"-"` +} + +type ArticleCommentsResp struct { + joint.CachePageResp +} + +type CreateArticleCommentReplyReq struct { + BaseInfo `json:"-" binding:"-"` + CommentID int64 `json:"comment_id" binding:"required"` + ReplyToID *int64 `json:"reply_to_id"` + Content string `json:"content" binding:"required,max=1000"` +} + +type CreateArticleCommentReplyResp struct { + ID int64 `json:"id"` +} + +type DeleteArticleCommentReplyReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` +} + +type CreateArticleSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + Title string `json:"title" binding:"required,max=200"` + Description string `json:"description" binding:"max=1000"` + Cover string `json:"cover"` + Visibility ms.ArticleVisibleT `json:"visibility"` + Tags []string `json:"tags"` +} + +type CreateArticleSeriesResp struct { + ID int64 `json:"id"` +} + +type UpdateArticleSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` + Title string `json:"title" binding:"required,max=200"` + Description string `json:"description" binding:"max=1000"` + Cover string `json:"cover"` + Visibility ms.ArticleVisibleT `json:"visibility"` + Tags []string `json:"tags"` +} + +type DeleteArticleSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` +} + +type GetArticleSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `form:"id" binding:"required"` +} + +type GetArticleSeriesResp struct { + joint.CacheResp +} + +type ListArticleSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + Query string `form:"query"` + Visibility []ms.ArticleVisibleT `form:"visibility"` + Page int `form:"-" binding:"-"` + PageSize int `form:"-" binding:"-"` +} + +type ListArticleSeriesResp struct { + joint.CachePageResp +} + +type AddToSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + SeriesID int64 `json:"series_id" binding:"required"` + ArticleID int64 `json:"article_id" binding:"required"` + Sort int `json:"sort"` +} + +type RemoveFromSeriesReq struct { + BaseInfo `json:"-" binding:"-"` + SeriesID int64 `json:"series_id" binding:"required"` + ArticleID int64 `json:"article_id" binding:"required"` +} + +type GetUserArticlesReq struct { + BaseInfo `json:"-" binding:"-"` + Username string `form:"username" binding:"required"` + Style string `form:"style"` + Page int `form:"-" binding:"-"` + PageSize int `form:"-" binding:"-"` +} + +type GetUserArticlesResp struct { + joint.CachePageResp +} + +type GetUserArticleCollectionsReq BasePageReq +type GetUserArticleCollectionsResp base.PageResp + +type GetUserArticleStarsReq BasePageReq +type GetUserArticleStarsResp base.PageResp + +type ArticleStarStatusReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `form:"id" binding:"required"` +} + +type ArticleStarStatusResp struct { + Status bool `json:"status"` +} + +type ArticleCollectionStatusReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `form:"id" binding:"required"` +} + +type ArticleCollectionStatusResp struct { + Status bool `json:"status"` +} + +type AdminDeleteArticleReq struct { + BaseInfo `json:"-" binding:"-"` + ID int64 `json:"id" binding:"required"` + Reason string `json:"reason" binding:"required"` +} + +// Bind methods for request validation and binding + +func (r *ListArticlesReq) Bind(c *gin.Context) error { + r.Page, r.PageSize = app.GetPageInfo(c) + return nil +} + +func (r *ArticleCommentsReq) Bind(c *gin.Context) error { + r.Page, r.PageSize = app.GetPageInfo(c) + return nil +} + +func (r *ListArticleSeriesReq) Bind(c *gin.Context) error { + r.Page, r.PageSize = app.GetPageInfo(c) + return nil +} + +func (r *GetUserArticlesReq) Bind(c *gin.Context) error { + r.Page, r.PageSize = app.GetPageInfo(c) + return nil +} + +func (r *GetUserArticleCollectionsReq) Bind(c *gin.Context) error { + return (*BasePageReq)(r).Bind(c) +} + +func (r *GetUserArticleStarsReq) Bind(c *gin.Context) error { + return (*BasePageReq)(r).Bind(c) +} + +func (r *ArticleStarStatusReq) Bind(c *gin.Context) error { + userId, exist := base.UserIdFrom(c) + if !exist { + return xerror.UnauthorizedAuthNotExist + } + r.BaseInfo = BaseInfo{ + User: &ms.User{Model: &ms.Model{ID: userId}}, + } + r.ID = convert.StrTo(c.Query("id")).MustInt64() + return nil +} + +func (r *ArticleCollectionStatusReq) Bind(c *gin.Context) error { + userId, exist := base.UserIdFrom(c) + if !exist { + return xerror.UnauthorizedAuthNotExist + } + r.BaseInfo = BaseInfo{ + User: &ms.User{Model: &ms.Model{ID: userId}}, + } + r.ID = convert.StrTo(c.Query("id")).MustInt64() + return nil +} + +func (r *CreateArticleReq) ValidateContents() error { + totalWords := 0 + for _, content := range r.Contents { + if content.Type == ms.ArticleContentTypeText { + // 计算字数,这里简化为字符数,实际应该用中文分词库 + totalWords += len([]rune(content.Content)) + } + } + if totalWords > 50000 { + return xerror.NewError(400, "文章字数超过50000字限制") + } + return nil +} diff --git a/internal/model/web/block.go b/internal/model/web/block.go new file mode 100644 index 00000000..eacb0492 --- /dev/null +++ b/internal/model/web/block.go @@ -0,0 +1,28 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/rocboss/paopao-ce/internal/model/joint" + "github.com/rocboss/paopao-ce/internal/servants/base" +) + +type BlockUserReq struct { + BaseInfo `json:"-" binding:"-"` + UserId int64 `json:"user_id" binding:"required"` + Reason string `json:"reason"` +} + +type UnblockUserReq struct { + BaseInfo `json:"-" binding:"-"` + UserId int64 `json:"user_id" binding:"required"` +} + +type ListBlocksReq struct { + BaseInfo `json:"-" binding:"-"` + joint.BasePageInfo +} + +type ListBlocksResp base.PageResp diff --git a/internal/model/web/core.go b/internal/model/web/core.go index 686b904d..a0b46db1 100644 --- a/internal/model/web/core.go +++ b/internal/model/web/core.go @@ -178,3 +178,7 @@ func (r *TweetStarStatusReq) Bind(c *gin.Context) error { r.TweetId = convert.StrTo(c.Query("id")).MustInt64() return nil } + +type StreamMessagesReq struct { + SimpleInfo `json:"-" binding:"-"` +} diff --git a/internal/model/web/loose.go b/internal/model/web/loose.go index 4e9dae62..cebece57 100644 --- a/internal/model/web/loose.go +++ b/internal/model/web/loose.go @@ -95,6 +95,8 @@ type GetUserProfileResp struct { Follows int64 `json:"follows"` Followings int64 `json:"followings"` TweetsCount int `json:"tweets_count"` + Experience int `json:"experience"` + Level int `json:"level"` } type TopicListReq struct { diff --git a/internal/model/web/mute.go b/internal/model/web/mute.go new file mode 100644 index 00000000..3e679d32 --- /dev/null +++ b/internal/model/web/mute.go @@ -0,0 +1,28 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/rocboss/paopao-ce/internal/model/joint" + "github.com/rocboss/paopao-ce/internal/servants/base" +) + +type MuteUserReq struct { + BaseInfo `json:"-" binding:"-"` + UserId int64 `json:"user_id" binding:"required"` + Reason string `json:"reason"` +} + +type UnmuteUserReq struct { + BaseInfo `json:"-" binding:"-"` + UserId int64 `json:"user_id" binding:"required"` +} + +type ListMutesReq struct { + BaseInfo `json:"-" binding:"-"` + joint.BasePageInfo +} + +type ListMutesResp base.PageResp diff --git a/internal/model/web/xerror.go b/internal/model/web/xerror.go index cd27c905..0bbdd0fc 100644 --- a/internal/model/web/xerror.go +++ b/internal/model/web/xerror.go @@ -89,6 +89,16 @@ var ( ErrGetFollowCountFailed = xerror.NewError(80104, "获取关注计数信息失败") ErrNotAllowFollowSelf = xerror.NewError(80105, "不能关注自己") ErrNotAllowUnfollowSelf = xerror.NewError(80106, "不能取消关注自己") + ErrBlockUserFailed = xerror.NewError(80107, "拉黑用户失败") + ErrUnblockUserFailed = xerror.NewError(80108, "取消拉黑失败") + ErrListBlocksFailed = xerror.NewError(80109, "获取拉黑列表失败") + ErrNotAllowBlockSelf = xerror.NewError(80110, "不能拉黑自己") + ErrAlreadyBlocked = xerror.NewError(80111, "该用户已被拉黑") + ErrMuteUserFailed = xerror.NewError(80112, "屏蔽用户失败") + ErrUnmuteUserFailed = xerror.NewError(80113, "取消屏蔽失败") + ErrListMutesFailed = xerror.NewError(80114, "获取屏蔽列表失败") + ErrNotAllowMuteSelf = xerror.NewError(80115, "不能屏蔽自己") + ErrAlreadyMuted = xerror.NewError(80116, "该用户已被屏蔽") ErrGetIndexTrendsFailed = xerror.NewError(802001, "获取动态条栏信息失败") @@ -105,5 +115,27 @@ var ( ErrFileInvalidExt = xerror.NewError(10201, "文件类型不合法") ErrFileInvalidSize = xerror.NewError(10202, "文件大小超限") + // 长文章相关错误 + ErrCreateArticle = xerror.NewError(11001, "长文章创建失败") + ErrUpdateArticle = xerror.NewError(11002, "长文章更新失败") + ErrDeleteArticle = xerror.NewError(11003, "长文章删除失败") + ErrArticleNotExist = xerror.NewError(11004, "长文章不存在") + ErrArticleWordLimit = xerror.NewError(11005, "长文章字数超过限制") + ErrStarArticle = xerror.NewError(11006, "长文章点赞操作失败") + ErrCollectionArticle = xerror.NewError(11007, "长文章收藏操作失败") + ErrCreateComment = xerror.NewError(11008, "评论发布失败") + ErrDeleteComment = xerror.NewError(11009, "评论删除失败") + ErrCommentNotExist = xerror.NewError(11010, "评论不存在") + ErrCommentContentEmpty = xerror.NewError(11011, "评论内容不能为空") + ErrCreateCommentReply = xerror.NewError(11012, "评论回复失败") + ErrDeleteCommentReply = xerror.NewError(11013, "评论回复删除失败") + ErrCommentReplyNotExist = xerror.NewError(11014, "评论回复不存在") + ErrCreateArticleSeries = xerror.NewError(11015, "长文章系列创建失败") + ErrUpdateArticleSeries = xerror.NewError(11016, "长文章系列更新失败") + ErrDeleteArticleSeries = xerror.NewError(11017, "长文章系列删除失败") + ErrArticleSeriesNotExist = xerror.NewError(11018, "长文章系列不存在") + ErrAddToSeries = xerror.NewError(11019, "添加到系列失败") + ErrRemoveFromSeries = xerror.NewError(11020, "从系列移除失败") + ErrNotImplemented = xerror.NewError(10501, "功能未实现") ) diff --git a/internal/servants/base/base.go b/internal/servants/base/base.go index 3c381487..c0dfa3ec 100644 --- a/internal/servants/base/base.go +++ b/internal/servants/base/base.go @@ -20,6 +20,7 @@ import ( "github.com/rocboss/paopao-ce/internal/core/cs" "github.com/rocboss/paopao-ce/internal/core/ms" "github.com/rocboss/paopao-ce/internal/dao" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" "github.com/rocboss/paopao-ce/internal/dao/cache" "github.com/rocboss/paopao-ce/internal/infra/events" "github.com/rocboss/paopao-ce/internal/model/joint" @@ -388,9 +389,9 @@ func (s *DaoServant) DeleteSearchPost(post *ms.Post) error { return s.Ts.DeleteDocuments([]string{fmt.Sprintf("%d", post.ID)}) } -func (s *DaoServant) RelationTypFrom(me *ms.User, username string) (res *cs.VistUser, err error) { - res = &cs.VistUser{ - RelTyp: cs.RelationSelf, +func (s *DaoServant) RelationTypFrom(me *ms.User, username string) (res *dbr.VistUser, err error) { + res = &dbr.VistUser{ + RelTyp: dbr.RelationTyp(cs.RelationSelf), Username: username, } // visit by self @@ -405,16 +406,16 @@ func (s *DaoServant) RelationTypFrom(me *ms.User, username string) (res *cs.Vist res.UserId = he.ID // visit by guest if me == nil { - res.RelTyp = cs.RelationGuest + res.RelTyp = dbr.RelationGuest return } // visit by admin/friend/other if me.IsAdmin { - res.RelTyp = cs.RelationAdmin + res.RelTyp = dbr.RelationAdmin } else if s.Ds.IsFriend(me.ID, he.ID) { - res.RelTyp = cs.RelationFriend + res.RelTyp = dbr.RelationFriend } else { - res.RelTyp = cs.RelationGuest + res.RelTyp = dbr.RelationGuest } return } diff --git a/internal/servants/web/block.go.disabled b/internal/servants/web/block.go.disabled new file mode 100644 index 00000000..d2b2f9d7 --- /dev/null +++ b/internal/servants/web/block.go.disabled @@ -0,0 +1,109 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/gin-gonic/gin" + api "github.com/rocboss/paopao-ce/auto/api/v1" + "github.com/rocboss/paopao-ce/internal/dao/cache" + "github.com/rocboss/paopao-ce/internal/model/web" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/pkg/xerror" + "github.com/sirupsen/logrus" +) + +var ( + _ api.Blockship = (*blockshipSrv)(nil) +) + +type blockshipSrv struct { + api.UnimplementedBlockshipServant + *base.DaoServant +} + +func (s *blockshipSrv) Chain() gin.HandlersChain { + return gin.HandlersChain{chain.Jwt()} +} + +func (s *blockshipSrv) BlockUser(r *web.BlockUserReq) error { + if r.User == nil { + return xerror.UnauthorizedTokenError + } else if r.User.ID == r.UserId { + return web.ErrNotAllowBlockSelf + } + + // 验证reason长度,防止过长输入 + if len(r.Reason) > 500 { + r.Reason = r.Reason[:500] + } + + // 防止重复拉黑 + if s.Ds.IsBlocked(r.User.ID, r.UserId) { + return web.ErrAlreadyBlocked + } + + // 拉黑前先取消关注关系 + if s.Ds.IsFollow(r.User.ID, r.UserId) { + if err := s.Ds.UnfollowUser(r.User.ID, r.UserId); err != nil { + logrus.Errorf("Ds.UnfollowUser err when blocking: %s", err) + } + } + if s.Ds.IsFollow(r.UserId, r.User.ID) { + if err := s.Ds.UnfollowUser(r.UserId, r.User.ID); err != nil { + logrus.Errorf("Ds.UnfollowUser err when blocking: %s", err) + } + } + + if err := s.Ds.BlockUser(r.User.ID, r.UserId, r.Reason); err != nil { + logrus.Errorf("Ds.BlockUser err: %s userId: %d targetId: %d", err, r.User.ID, r.UserId) + return web.ErrBlockUserFailed + } + + // 触发缓存更新事件 + cache.OnCacheMyFollowIdsEvent(s.Ds, r.User.ID) + cache.OnExpireIndexTweetEvent(r.User.ID) + onMessageActionEvent(_messageActionBlock, r.User.ID) + return nil +} + +func (s *blockshipSrv) UnblockUser(r *web.UnblockUserReq) error { + if r.User == nil { + return xerror.UnauthorizedTokenError + } else if r.User.ID == r.UserId { + return web.ErrNotAllowUnblockSelf + } + + if err := s.Ds.UnblockUser(r.User.ID, r.UserId); err != nil { + logrus.Errorf("Ds.UnblockUser err: %s userId: %d targetId: %d", err, r.User.ID, r.UserId) + return web.ErrUnblockUserFailed + } + + // 触发缓存更新事件 + cache.OnCacheMyFollowIdsEvent(s.Ds, r.User.ID) + cache.OnExpireIndexTweetEvent(r.User.ID) + onMessageActionEvent(_messageActionUnblock, r.User.ID) + return nil +} + +func (s *blockshipSrv) ListBlocks(r *web.ListBlocksReq) (*web.ListBlocksResp, error) { + if r.User == nil { + return nil, xerror.UnauthorizedTokenError + } + + res, err := s.Ds.ListBlocks(r.User.ID, r.PageSize, (r.Page-1)*r.PageSize) + if err != nil { + logrus.Errorf("Ds.ListBlocks err: %s", err) + return nil, web.ErrListBlocksFailed + } + + resp := base.PageRespFrom(res.Blocks, r.Page, r.PageSize, res.Total) + return (*web.ListBlocksResp)(resp), nil +} + +func newBlockshipSrv(s *base.DaoServant) api.Blockship { + return &blockshipSrv{ + DaoServant: s, + } +} diff --git a/internal/servants/web/core.go b/internal/servants/web/core.go index 13a78fdd..783d394d 100644 --- a/internal/servants/web/core.go +++ b/internal/servants/web/core.go @@ -6,8 +6,9 @@ package web import ( "context" + "encoding/json" "fmt" - + "net/http" "time" "unicode/utf8" @@ -15,6 +16,7 @@ import ( api "github.com/rocboss/paopao-ce/auto/api/v1" "github.com/rocboss/paopao-ce/internal/conf" "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/core/cs" "github.com/rocboss/paopao-ce/internal/core/ms" "github.com/rocboss/paopao-ce/internal/model/joint" "github.com/rocboss/paopao-ce/internal/model/web" @@ -418,6 +420,113 @@ func (s *coreSrv) TweetStarStatus(req *web.TweetStarStatusReq) (*web.TweetStarSt return resp, nil } +func (s *coreSrv) StreamMessages(req *web.StreamMessagesReq, c *gin.Context) error { + // 设置SSE响应头 + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("Access-Control-Allow-Origin", "*") + + // 获取用户ID + userId := req.Uid + if userId <= 0 { + s.sendSSEEvent(c, "error", "Unauthorized") + return nil + } + + // 创建上下文用于控制连接 + ctx, cancel := context.WithCancel(c.Request.Context()) + defer cancel() + + // 记录已发送的消息ID,避免重复发送 + sentMessageIDs := make(map[int64]bool) + + // 发送初始连接确认 + s.sendSSEEvent(c, "connected", fmt.Sprintf(`{"user_id": %d, "timestamp": %d}`, userId, time.Now().Unix())) + + // 定期检查新消息 + ticker := time.NewTicker(5 * time.Second) // 每5秒检查一次 + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + // 连接断开 + return nil + case <-ticker.C: + // 获取未读消息 + messages, _, err := s.Ds.GetMessages(userId, cs.StyleMsgUnread, 50, 0) + if err != nil { + logrus.Errorf("StreamMessages: failed to get messages for user %d: %v", userId, err) + continue + } + + // 过滤并发送新消息 + for _, msg := range messages { + if sentMessageIDs[msg.ID] { + continue // 已发送过,跳过 + } + + // 格式化消息数据 + messageData := map[string]interface{}{ + "id": msg.ID, + "type": msg.Type, + "brief": msg.Brief, + "content": msg.Content, + "sender_user_id": msg.SenderUserID, + "receiver_user_id": msg.ReceiverUserID, + "post_id": msg.PostID, + "comment_id": msg.CommentID, + "reply_id": msg.ReplyID, + "is_read": msg.IsRead, + "created_on": msg.CreatedOn, + } + + // 发送消息事件 + if err := s.sendSSEEvent(c, "message", messageData); err != nil { + logrus.Errorf("StreamMessages: failed to send SSE event: %v", err) + return nil + } + + // 标记为已发送 + sentMessageIDs[msg.ID] = true + } + + // 手动刷新响应,确保消息立即发送 + s.flushSSE(c) + } + } +} + +// sendSSEEvent 发送SSE事件 +func (s *coreSrv) sendSSEEvent(c *gin.Context, event string, data interface{}) error { + var dataStr string + switch v := data.(type) { + case string: + dataStr = v + default: + jsonBytes, err := json.Marshal(data) + if err != nil { + return err + } + dataStr = string(jsonBytes) + } + + // 按照SSE格式写入数据 + if event != "" { + fmt.Fprintf(c.Writer, "event: %s\n", event) + } + fmt.Fprintf(c.Writer, "data: %s\n\n", dataStr) + return nil +} + +// flushSSE 刷新SSE响应 +func (s *coreSrv) flushSSE(c *gin.Context) { + if flusher, ok := c.Writer.(http.Flusher); ok { + flusher.Flush() + } +} + func (s *coreSrv) messagesFromCache(req *web.GetMessagesReq, limit int, offset int) (res *web.GetMessagesResp, key string, ok bool) { key = fmt.Sprintf("%s%d:%s:%d:%d", s.prefixMessages, req.Uid, req.Style, limit, offset) if data, err := s.wc.Get(key); err == nil { diff --git a/internal/servants/web/followship.go b/internal/servants/web/followship.go index a66bac0d..72ca895a 100644 --- a/internal/servants/web/followship.go +++ b/internal/servants/web/followship.go @@ -99,9 +99,12 @@ func (s *followshipSrv) FollowUser(r *web.FollowUserReq) error { } else if r.User.ID == r.UserId { return web.ErrNotAllowFollowSelf } + + // TODO: 检查是否已被对方拉黑 (暂时跳过) + if err := s.Ds.FollowUser(r.User.ID, r.UserId); err != nil { logrus.Errorf("Ds.FollowUser err: %s userId: %d followId: %d", err, r.User.ID, r.UserId) - return web.ErrUnfollowUserFailed + return web.ErrFolloUserFailed } // 触发缓存更新事件 // TODO: 合并成一个事件 diff --git a/internal/servants/web/loose.go b/internal/servants/web/loose.go index e8ca4b63..4fd32283 100644 --- a/internal/servants/web/loose.go +++ b/internal/servants/web/loose.go @@ -201,13 +201,13 @@ func (s *looseSrv) GetUserTweets(req *web.GetUserTweetsReq) (res *web.GetUserTwe return } -func (s *looseSrv) userTweetsFromCache(req *web.GetUserTweetsReq, user *cs.VistUser) (res *web.GetUserTweetsResp, key string, ok bool) { +func (s *looseSrv) userTweetsFromCache(req *web.GetUserTweetsReq, user *dbr.VistUser) (res *web.GetUserTweetsResp, key string, ok bool) { switch req.Style { case web.UserPostsStylePost, web.UserPostsStyleHighlight, web.UserPostsStyleMedia: key = fmt.Sprintf("%s%d:%s:%s:%d:%d", s.prefixUserTweets, user.UserId, req.Style, user.RelTyp, req.Page, req.PageSize) default: meName := "_" - if user.RelTyp != cs.RelationGuest { + if user.RelTyp != dbr.RelationTyp(cs.RelationGuest) { meName = req.User.Username } key = fmt.Sprintf("%s%d:%s:%s:%d:%d", s.prefixUserTweets, user.UserId, req.Style, meName, req.Page, req.PageSize) @@ -222,7 +222,7 @@ func (s *looseSrv) userTweetsFromCache(req *web.GetUserTweetsReq, user *cs.VistU return } -func (s *looseSrv) getUserStarTweets(req *web.GetUserTweetsReq, user *cs.VistUser) (*web.GetUserTweetsResp, error) { +func (s *looseSrv) getUserStarTweets(req *web.GetUserTweetsReq, user *dbr.VistUser) (*web.GetUserTweetsResp, error) { stars, totalRows, err := s.Ds.ListUserStarTweets(user, req.PageSize, (req.Page-1)*req.PageSize) if err != nil { logrus.Errorf("getUserStarTweets err[1]: %s", err) @@ -255,7 +255,7 @@ func (s *looseSrv) getUserStarTweets(req *web.GetUserTweetsReq, user *cs.VistUse }, nil } -func (s *looseSrv) listUserTweets(req *web.GetUserTweetsReq, user *cs.VistUser) (*web.GetUserTweetsResp, error) { +func (s *looseSrv) listUserTweets(req *web.GetUserTweetsReq, user *dbr.VistUser) (*web.GetUserTweetsResp, error) { var ( tweets []*ms.Post total int64 @@ -294,18 +294,18 @@ func (s *looseSrv) listUserTweets(req *web.GetUserTweetsReq, user *cs.VistUser) }, nil } -func (s *looseSrv) getUserPostTweets(req *web.GetUserTweetsReq, user *cs.VistUser, isHighlight bool) (*web.GetUserTweetsResp, error) { +func (s *looseSrv) getUserPostTweets(req *web.GetUserTweetsReq, user *dbr.VistUser, isHighlight bool) (*web.GetUserTweetsResp, error) { style := cs.StyleUserTweetsGuest switch user.RelTyp { - case cs.RelationAdmin: + case dbr.RelationAdmin: style = cs.StyleUserTweetsAdmin - case cs.RelationSelf: + case dbr.RelationSelf: style = cs.StyleUserTweetsSelf - case cs.RelationFriend: + case dbr.RelationFriend: style = cs.StyleUserTweetsFriend - case cs.RelationFollowing: + case dbr.RelationFollowing: style = cs.StyleUserTweetsFollowing - case cs.RelationGuest: + case dbr.RelationGuest: fallthrough default: // nothing @@ -368,6 +368,8 @@ func (s *looseSrv) GetUserProfile(req *web.GetUserProfileReq) (*web.GetUserProfi Follows: follows, Followings: followings, TweetsCount: he.TweetsCount, + Experience: he.Experience, + Level: he.Level, }, nil } diff --git a/internal/servants/web/mute.go.disabled b/internal/servants/web/mute.go.disabled new file mode 100644 index 00000000..04a0c47a --- /dev/null +++ b/internal/servants/web/mute.go.disabled @@ -0,0 +1,97 @@ +// Copyright 2024 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/gin-gonic/gin" + api "github.com/rocboss/paopao-ce/auto/api/v1" + "github.com/rocboss/paopao-ce/internal/dao/cache" + "github.com/rocboss/paopao-ce/internal/model/web" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/pkg/xerror" + "github.com/sirupsen/logrus" +) + +var ( + _ api.Muteship = (*muteshipSrv)(nil) +) + +type muteshipSrv struct { + api.UnimplementedMuteshipServant + *base.DaoServant +} + +func (s *muteshipSrv) Chain() gin.HandlersChain { + return gin.HandlersChain{chain.Jwt()} +} + +func (s *muteshipSrv) MuteUser(r *web.MuteUserReq) error { + if r.User == nil { + return xerror.UnauthorizedTokenError + } else if r.User.ID == r.UserId { + return web.ErrNotAllowMuteSelf + } + + // 验证reason长度,防止过长输入 + if len(r.Reason) > 500 { + r.Reason = r.Reason[:500] + } + + // 防止重复屏蔽 + if s.Ds.IsMuted(r.User.ID, r.UserId) { + return web.ErrAlreadyMuted + } + + if err := s.Ds.MuteUser(r.User.ID, r.UserId, r.Reason); err != nil { + logrus.Errorf("Ds.MuteUser err: %s userId: %d targetId: %d", err, r.User.ID, r.UserId) + return web.ErrMuteUserFailed + } + + // 触发缓存更新事件 + cache.OnCacheMyFollowIdsEvent(s.Ds, r.User.ID) + cache.OnExpireIndexTweetEvent(r.User.ID) + onMessageActionEvent(_messageActionMute, r.User.ID) + return nil +} + +func (s *muteshipSrv) UnmuteUser(r *web.UnmuteUserReq) error { + if r.User == nil { + return xerror.UnauthorizedTokenError + } else if r.User.ID == r.UserId { + return web.ErrNotAllowUnmuteSelf + } + + if err := s.Ds.UnmuteUser(r.User.ID, r.UserId); err != nil { + logrus.Errorf("Ds.UnmuteUser err: %s userId: %d targetId: %d", err, r.User.ID, r.UserId) + return web.ErrUnmuteUserFailed + } + + // 触发缓存更新事件 + cache.OnCacheMyFollowIdsEvent(s.Ds, r.User.ID) + cache.OnExpireIndexTweetEvent(r.User.ID) + onMessageActionEvent(_messageActionUnmute, r.User.ID) + return nil +} + +func (s *muteshipSrv) ListMutes(r *web.ListMutesReq) (*web.ListMutesResp, error) { + if r.User == nil { + return nil, xerror.UnauthorizedTokenError + } + + res, err := s.Ds.ListMutes(r.User.ID, r.PageSize, (r.Page-1)*r.PageSize) + if err != nil { + logrus.Errorf("Ds.ListMutes err: %s", err) + return nil, web.ErrListMutesFailed + } + + resp := base.PageRespFrom(res.Mutes, r.Page, r.PageSize, res.Total) + return (*web.ListMutesResp)(resp), nil +} + +func newMuteshipSrv(s *base.DaoServant) api.Muteship { + return &muteshipSrv{ + DaoServant: s, + } +} diff --git a/mirc/web/v1/activities.go b/mirc/web/v1/activities.go index 378a3b22..4dfeff04 100644 --- a/mirc/web/v1/activities.go +++ b/mirc/web/v1/activities.go @@ -1,4 +1,18 @@ package v1 +import ( + . "github.com/alimy/mir/v5" + + "github.com/rocboss/paopao-ce/internal/model/web" +) + +// CheckInActivitity 签到活动 type CheckInActivitity struct { + Schema `mir:"v1,chain"` + + // UserCheckIn 用户签到 + UserCheckIn func(Post, web.UserCheckInReq) web.UserCheckInResp `mir:"checkin"` + + // GetCheckInRank 获取签到排行榜 + GetCheckInRank func(Get, web.GetCheckInRankReq) web.GetCheckInRankResp `mir:"checkin/rank"` } diff --git a/mirc/web/v1/block.go b/mirc/web/v1/block.go new file mode 100644 index 00000000..dc21e163 --- /dev/null +++ b/mirc/web/v1/block.go @@ -0,0 +1,21 @@ +package v1 + +import ( + . "github.com/alimy/mir/v5" + + "github.com/rocboss/paopao-ce/internal/model/web" +) + +// Blockship 拉黑模式 服务 +type Blockship struct { + Schema `mir:"v1"` + + // BlockUser 拉黑用户 + BlockUser func(Post, web.BlockUserReq) `mir:"user/block"` + + // UnblockUser 取消拉黑用户 + UnblockUser func(Post, web.UnblockUserReq) `mir:"user/unblock"` + + // ListBlocks 获取用户的拉黑列表 + ListBlocks func(Get, web.ListBlocksReq) web.ListBlocksResp `mir:"user/blocks"` +} diff --git a/mirc/web/v1/core.go b/mirc/web/v1/core.go index ec4bfd9a..c02dd477 100644 --- a/mirc/web/v1/core.go +++ b/mirc/web/v1/core.go @@ -57,4 +57,13 @@ type Core struct { // TweetCollectionStatus 获取动态收藏状态 TweetCollectionStatus func(Get, web.TweetCollectionStatusReq) web.TweetCollectionStatusResp `mir:"post/collection"` + + // GetUserArticleCollections 获取用户长文章收藏列表 + GetUserArticleCollections func(Get, web.GetUserArticleCollectionsReq) web.GetUserArticleCollectionsResp `mir:"user/articles/collections"` + + // GetUserArticleStars 获取用户长文章点赞列表 + GetUserArticleStars func(Get, web.GetUserArticleStarsReq) web.GetUserArticleStarsResp `mir:"user/articles/stars"` + + // StreamMessages SSE获取消息列表 + StreamMessages func(Get, web.StreamMessagesReq) `mir:"user/messages/stream"` } diff --git a/mirc/web/v1/loose.go b/mirc/web/v1/loose.go index 19b65a0a..8b0e6ec7 100644 --- a/mirc/web/v1/loose.go +++ b/mirc/web/v1/loose.go @@ -27,4 +27,28 @@ type Loose struct { // TweetDetail 获取动态详情 TweetDetail func(Get, web.TweetDetailReq) web.TweetDetailResp `mir:"post"` + + // GetArticle 获取长文章详情 + GetArticle func(Get, web.GetArticleReq) web.GetArticleResp `mir:"articles/detail"` + + // ListArticles 获取长文章列表 + ListArticles func(Get, web.ListArticlesReq) web.ListArticlesResp `mir:"articles"` + + // ArticleComments 获取长文章评论 + ArticleComments func(Get, web.ArticleCommentsReq) web.ArticleCommentsResp `mir:"articles/comments"` + + // GetArticleSeries 获取长文章系列详情 + GetArticleSeries func(Get, web.GetArticleSeriesReq) web.GetArticleSeriesResp `mir:"articles/series/detail"` + + // ListArticleSeries 获取长文章系列列表 + ListArticleSeries func(Get, web.ListArticleSeriesReq) web.ListArticleSeriesResp `mir:"articles/series"` + + // GetUserArticles 获取用户长文章列表 + GetUserArticles func(Get, web.GetUserArticlesReq) web.GetUserArticlesResp `mir:"user/articles"` + + // ArticleStarStatus 获取长文章点赞状态 + ArticleStarStatus func(Get, web.ArticleStarStatusReq) web.ArticleStarStatusResp `mir:"articles/star"` + + // ArticleCollectionStatus 获取长文章收藏状态 + ArticleCollectionStatus func(Get, web.ArticleCollectionStatusReq) web.ArticleCollectionStatusResp `mir:"articles/collection"` } diff --git a/mirc/web/v1/mute.go b/mirc/web/v1/mute.go new file mode 100644 index 00000000..3de28cf3 --- /dev/null +++ b/mirc/web/v1/mute.go @@ -0,0 +1,21 @@ +package v1 + +import ( + . "github.com/alimy/mir/v5" + + "github.com/rocboss/paopao-ce/internal/model/web" +) + +// Muteship 屏蔽模式 服务 +type Muteship struct { + Schema `mir:"v1"` + + // MuteUser 屏蔽用户 + MuteUser func(Post, web.MuteUserReq) `mir:"user/mute"` + + // UnmuteUser 取消屏蔽用户 + UnmuteUser func(Post, web.UnmuteUserReq) `mir:"user/unmute"` + + // ListMutes 获取用户的屏蔽列表 + ListMutes func(Get, web.ListMutesReq) web.ListMutesResp `mir:"user/mutes"` +} diff --git a/mirc/web/v1/priv.go b/mirc/web/v1/priv.go index 958bf6a0..eb6d3221 100644 --- a/mirc/web/v1/priv.go +++ b/mirc/web/v1/priv.go @@ -81,4 +81,44 @@ type Priv struct { // UnfollowTopic 取消关注话题 UnfollowTopic func(Post, web.UnfollowTopicReq) `mir:"topic/unfollow"` + + // 长文章相关API + // CreateArticle 创建长文章 + CreateArticle func(Post, Chain, web.CreateArticleReq) web.CreateArticleResp `mir:"articles"` + + // UpdateArticle 更新长文章 + UpdateArticle func(Put, Chain, web.UpdateArticleReq) `mir:"articles"` + + // DeleteArticle 删除长文章 + DeleteArticle func(Delete, web.DeleteArticleReq) `mir:"articles"` + + // StarArticle 长文章点赞操作 + StarArticle func(Post, web.ArticleStarReq) web.ArticleStarResp `mir:"articles/star"` + + // CollectionArticle 长文章收藏操作 + CollectionArticle func(Post, web.ArticleCollectionReq) web.ArticleCollectionResp `mir:"articles/collection"` + + // CreateArticleComment 发布长文章评论 + CreateArticleComment func(Post, web.CreateArticleCommentReq) web.CreateArticleCommentResp `mir:"articles/comment"` + + // DeleteArticleComment 删除长文章评论 + DeleteArticleComment func(Delete, web.DeleteArticleCommentReq) `mir:"articles/comment"` + + // CreateArticleCommentReply 发布评论回复 + CreateArticleCommentReply func(Post, web.CreateArticleCommentReplyReq) web.CreateArticleCommentReplyResp `mir:"articles/comment/reply"` + + // DeleteArticleCommentReply 删除评论回复 + DeleteArticleCommentReply func(Delete, web.DeleteArticleCommentReplyReq) `mir:"articles/comment/reply"` + + // CreateArticleSeries 创建长文章系列 + CreateArticleSeries func(Post, web.CreateArticleSeriesReq) web.CreateArticleSeriesResp `mir:"articles/series"` + + // UpdateArticleSeries 更新长文章系列 + UpdateArticleSeries func(Put, web.UpdateArticleSeriesReq) `mir:"articles/series"` + + // DeleteArticleSeries 删除长文章系列 + DeleteArticleSeries func(Delete, web.DeleteArticleSeriesReq) `mir:"articles/series"` + + // AdminDeleteArticle 管理员删除长文章 + AdminDeleteArticle func(Delete, web.AdminDeleteArticleReq) `mir:"admin/articles"` } diff --git a/scripts/migration/mysql/0017_add_user_exp_and_operation_tables.down.sql b/scripts/migration/mysql/0017_add_user_exp_and_operation_tables.down.sql new file mode 100644 index 00000000..728f5229 --- /dev/null +++ b/scripts/migration/mysql/0017_add_user_exp_and_operation_tables.down.sql @@ -0,0 +1,12 @@ +-- 删除用户签到记录表 +DROP TABLE IF EXISTS `p_user_checkin`; + +-- 删除运营设置表 +DROP TABLE IF EXISTS `p_operation_settings`; + +-- 删除用户表的经验值字段 +ALTER TABLE `p_user` DROP COLUMN `experience`; + + + + diff --git a/scripts/migration/mysql/0017_add_user_exp_and_operation_tables.up.sql b/scripts/migration/mysql/0017_add_user_exp_and_operation_tables.up.sql new file mode 100644 index 00000000..cf694392 --- /dev/null +++ b/scripts/migration/mysql/0017_add_user_exp_and_operation_tables.up.sql @@ -0,0 +1,40 @@ +-- 为用户表添加经验值字段 +ALTER TABLE `p_user` ADD COLUMN `experience` int(11) NOT NULL DEFAULT 0 COMMENT '用户经验值' AFTER `balance`; + +-- 创建运营设置表 +CREATE TABLE IF NOT EXISTS `p_operation_settings` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `key` varchar(255) NOT NULL COMMENT '设置键', + `value` text NOT NULL COMMENT '设置值', + `description` varchar(500) DEFAULT NULL COMMENT '设置描述', + `created_on` int(11) NOT NULL DEFAULT 0, + `updated_on` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_key` (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运营设置表'; + +-- 创建用户签到记录表 +CREATE TABLE IF NOT EXISTS `p_user_checkin` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `checkin_date` date NOT NULL COMMENT '签到日期', + `experience_gained` int(11) NOT NULL DEFAULT 0 COMMENT '获得的经验值', + `created_on` int(11) NOT NULL DEFAULT 0, + `updated_on` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_user_date` (`user_id`, `checkin_date`), + KEY `idx_user_id` (`user_id`), + KEY `idx_checkin_date` (`checkin_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户签到记录表'; + +-- 插入默认运营设置 +INSERT INTO `p_operation_settings` (`key`, `value`, `description`, `created_on`, `updated_on`) VALUES +('checkin_enabled', 'true', '签到功能是否启用', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('checkin_exp_range_min', '5', '签到最小经验值', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('checkin_exp_range_max', '15', '签到最大经验值', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('checkin_rank_length', '10', '签到排行榜显示人数', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('level_to_exp', '{"lv1":[0,100],"lv2":[101,300],"lv3":[301,600],"lv4":[601,1000],"lv5":[1001,1500]}', '经验-等级对应关系', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + + + + diff --git a/scripts/migration/mysql/0018_drop_user_level_table.down.sql b/scripts/migration/mysql/0018_drop_user_level_table.down.sql new file mode 100644 index 00000000..e7c2bdcc --- /dev/null +++ b/scripts/migration/mysql/0018_drop_user_level_table.down.sql @@ -0,0 +1,23 @@ +-- 重新创建用户等级表 +CREATE TABLE IF NOT EXISTS `p_user_level` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `level` int(11) NOT NULL COMMENT '等级', + `min_experience` int(11) NOT NULL COMMENT '最小经验值', + `max_experience` int(11) NOT NULL COMMENT '最大经验值', + `created_on` int(11) NOT NULL DEFAULT 0, + `updated_on` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_level` (`level`), + UNIQUE KEY `idx_min_experience` (`min_experience`), + UNIQUE KEY `idx_max_experience` (`max_experience`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户等级配置表'; + +-- 插入初始等级数据 +INSERT INTO `p_user_level` (`level`, `min_experience`, `max_experience`, `created_on`, `updated_on`) VALUES +(1, 0, 100, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(2, 101, 300, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(3, 301, 600, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(4, 601, 1000, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(5, 1001, 1500, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + + diff --git a/scripts/migration/mysql/0018_drop_user_level_table.up.sql b/scripts/migration/mysql/0018_drop_user_level_table.up.sql new file mode 100644 index 00000000..e703f3a9 --- /dev/null +++ b/scripts/migration/mysql/0018_drop_user_level_table.up.sql @@ -0,0 +1,4 @@ +-- 删除用户等级表 +DROP TABLE IF EXISTS `p_user_level`; + + diff --git a/scripts/migration/mysql/0019_create_articles.down.sql b/scripts/migration/mysql/0019_create_articles.down.sql new file mode 100644 index 00000000..87120057 --- /dev/null +++ b/scripts/migration/mysql/0019_create_articles.down.sql @@ -0,0 +1,10 @@ +-- 删除长文章相关表 + +DROP TABLE IF EXISTS `p_article_comment_replies`; +DROP TABLE IF EXISTS `p_article_comments`; +DROP TABLE IF EXISTS `p_article_stars`; +DROP TABLE IF EXISTS `p_article_collections`; +DROP TABLE IF EXISTS `p_article_series_items`; +DROP TABLE IF EXISTS `p_article_series`; +DROP TABLE IF EXISTS `p_article_contents`; +DROP TABLE IF EXISTS `p_articles`; diff --git a/scripts/migration/mysql/0019_create_articles.up.sql b/scripts/migration/mysql/0019_create_articles.up.sql new file mode 100644 index 00000000..9c7f5057 --- /dev/null +++ b/scripts/migration/mysql/0019_create_articles.up.sql @@ -0,0 +1,153 @@ +-- 创建长文章相关表 + +-- 长文章主表 +CREATE TABLE IF NOT EXISTS `p_articles` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `series_id` bigint(20) DEFAULT NULL COMMENT '系列ID,如果属于某个系列', + `title` varchar(200) NOT NULL COMMENT '文章标题', + `summary` text COMMENT '文章摘要', + `word_count` int(11) NOT NULL DEFAULT 0 COMMENT '字数统计', + `visibility` tinyint(4) NOT NULL DEFAULT 0 COMMENT '可见性: 0私密 10充电可见 20订阅可见 30保留 40保留 50好友可见 60关注可见 70保留 80保留 90公开', + `price` bigint(20) DEFAULT 0 COMMENT '付费价格(分)', + `is_top` int(11) NOT NULL DEFAULT 0 COMMENT '是否置顶', + `is_essence` int(11) NOT NULL DEFAULT 0 COMMENT '是否精华', + `is_lock` int(11) NOT NULL DEFAULT 0 COMMENT '是否锁定', + `comment_count` bigint(20) NOT NULL DEFAULT 0 COMMENT '评论数', + `collection_count` bigint(20) NOT NULL DEFAULT 0 COMMENT '收藏数', + `share_count` bigint(20) NOT NULL DEFAULT 0 COMMENT '分享数', + `upvote_count` bigint(20) NOT NULL DEFAULT 0 COMMENT '点赞数', + `view_count` bigint(20) NOT NULL DEFAULT 0 COMMENT '阅读数', + `latest_replied_on` int(11) NOT NULL DEFAULT 0 COMMENT '最后回复时间', + `tags` varchar(500) DEFAULT '' COMMENT '标签,逗号分隔', + `ip` varchar(64) DEFAULT '' COMMENT 'IP地址', + `ip_loc` varchar(64) DEFAULT '' COMMENT 'IP地理位置', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_series_id` (`series_id`), + KEY `idx_visibility` (`visibility`), + KEY `idx_is_top` (`is_top`), + KEY `idx_is_essence` (`is_essence`), + KEY `idx_created_on` (`created_on`), + KEY `idx_modified_on` (`modified_on`), + KEY `idx_tags` (`tags`(191)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章表'; + +-- 长文章内容表 +CREATE TABLE IF NOT EXISTS `p_article_contents` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `article_id` bigint(20) NOT NULL COMMENT '文章ID', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `content` longtext NOT NULL COMMENT '内容', + `content_type` int(11) NOT NULL DEFAULT 1 COMMENT '内容类型: 1标题 2文字段落 3图片地址 4视频地址 5语音地址 6链接地址 7附件资源 8收费附件', + `sort` bigint(20) NOT NULL DEFAULT 0 COMMENT '排序', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_content_type` (`content_type`), + KEY `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章内容表'; + +-- 长文章系列表 +CREATE TABLE IF NOT EXISTS `p_article_series` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `title` varchar(200) NOT NULL COMMENT '系列标题', + `description` text COMMENT '系列描述', + `cover` varchar(500) DEFAULT '' COMMENT '封面图片', + `article_count` int(11) NOT NULL DEFAULT 0 COMMENT '文章数量', + `total_word_count` int(11) NOT NULL DEFAULT 0 COMMENT '总字数', + `is_completed` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否完结', + `visibility` tinyint(4) NOT NULL DEFAULT 0 COMMENT '可见性', + `tags` varchar(500) DEFAULT '' COMMENT '标签,逗号分隔', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_visibility` (`visibility`), + KEY `idx_is_completed` (`is_completed`), + KEY `idx_created_on` (`created_on`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章系列表'; + +-- 系列文章关联表 +CREATE TABLE IF NOT EXISTS `p_article_series_items` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `series_id` bigint(20) NOT NULL COMMENT '系列ID', + `article_id` bigint(20) NOT NULL COMMENT '文章ID', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `created_on` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_series_article` (`series_id`, `article_id`), + KEY `idx_series_id` (`series_id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系列文章关联表'; + +-- 长文章收藏表 +CREATE TABLE IF NOT EXISTS `p_article_collections` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `article_id` bigint(20) NOT NULL COMMENT '文章ID', + `created_on` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_user_article` (`user_id`, `article_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_article_id` (`article_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章收藏表'; + +-- 长文章点赞表 +CREATE TABLE IF NOT EXISTS `p_article_stars` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `article_id` bigint(20) NOT NULL COMMENT '文章ID', + `created_on` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_user_article` (`user_id`, `article_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_article_id` (`article_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章点赞表'; + +-- 长文章评论表 +CREATE TABLE IF NOT EXISTS `p_article_comments` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `article_id` bigint(20) NOT NULL COMMENT '文章ID', + `content` text NOT NULL COMMENT '评论内容', + `ip` varchar(64) DEFAULT '' COMMENT 'IP地址', + `ip_loc` varchar(64) DEFAULT '' COMMENT 'IP地理位置', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_created_on` (`created_on`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章评论表'; + +-- 长文章评论回复表 +CREATE TABLE IF NOT EXISTS `p_article_comment_replies` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `article_id` bigint(20) NOT NULL COMMENT '文章ID', + `comment_id` bigint(20) NOT NULL COMMENT '评论ID', + `reply_to_id` bigint(20) DEFAULT NULL COMMENT '回复目标ID', + `content` text NOT NULL COMMENT '回复内容', + `ip` varchar(64) DEFAULT '' COMMENT 'IP地址', + `ip_loc` varchar(64) DEFAULT '' COMMENT 'IP地理位置', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_article_id` (`article_id`), + KEY `idx_comment_id` (`comment_id`), + KEY `idx_reply_to_id` (`reply_to_id`), + KEY `idx_created_on` (`created_on`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='长文章评论回复表'; diff --git a/scripts/migration/mysql/0020_add_block_mute_tables.down.sql b/scripts/migration/mysql/0020_add_block_mute_tables.down.sql new file mode 100644 index 00000000..3d3497da --- /dev/null +++ b/scripts/migration/mysql/0020_add_block_mute_tables.down.sql @@ -0,0 +1,4 @@ +-- 撤销用户拉黑和屏蔽功能相关表 + +DROP TABLE IF EXISTS `p_user_mutes`; +DROP TABLE IF EXISTS `p_user_blocks`; diff --git a/scripts/migration/mysql/0020_add_block_mute_tables.up.sql b/scripts/migration/mysql/0020_add_block_mute_tables.up.sql new file mode 100644 index 00000000..8f2149a0 --- /dev/null +++ b/scripts/migration/mysql/0020_add_block_mute_tables.up.sql @@ -0,0 +1,33 @@ +-- 添加用户拉黑和屏蔽功能相关表 + +-- 用户拉黑表 +CREATE TABLE IF NOT EXISTS `p_user_blocks` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '发起拉黑的用户ID', + `target_user_id` bigint(20) NOT NULL COMMENT '被拉黑的用户ID', + `reason` varchar(500) DEFAULT '' COMMENT '拉黑原因', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_target` (`user_id`, `target_user_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_target_user_id` (`target_user_id`), + KEY `idx_created_on` (`created_on`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户拉黑表'; + +-- 用户屏蔽表 +CREATE TABLE IF NOT EXISTS `p_user_mutes` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL COMMENT '发起屏蔽的用户ID', + `target_user_id` bigint(20) NOT NULL COMMENT '被屏蔽的用户ID', + `reason` varchar(500) DEFAULT '' COMMENT '屏蔽原因', + `created_on` int(11) NOT NULL DEFAULT 0, + `modified_on` int(11) NOT NULL DEFAULT 0, + `is_del` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_target` (`user_id`, `target_user_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_target_user_id` (`target_user_id`), + KEY `idx_created_on` (`created_on`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户屏蔽表'; diff --git a/scripts/migration/postgres/0017_add_user_exp_and_operation_tables.down.sql b/scripts/migration/postgres/0017_add_user_exp_and_operation_tables.down.sql new file mode 100644 index 00000000..e9bd3e59 --- /dev/null +++ b/scripts/migration/postgres/0017_add_user_exp_and_operation_tables.down.sql @@ -0,0 +1,12 @@ +-- 删除用户签到记录表 +DROP TABLE IF EXISTS "p_user_checkin"; + +-- 删除运营设置表 +DROP TABLE IF EXISTS "p_operation_settings"; + +-- 删除用户表的经验值字段 +ALTER TABLE "p_user" DROP COLUMN "experience"; + + + + diff --git a/scripts/migration/postgres/0017_add_user_exp_and_operation_tables.up.sql b/scripts/migration/postgres/0017_add_user_exp_and_operation_tables.up.sql new file mode 100644 index 00000000..a0f2e835 --- /dev/null +++ b/scripts/migration/postgres/0017_add_user_exp_and_operation_tables.up.sql @@ -0,0 +1,39 @@ +-- 为用户表添加经验值字段 +ALTER TABLE "p_user" ADD COLUMN "experience" integer NOT NULL DEFAULT 0; + +-- 创建运营设置表 +CREATE TABLE IF NOT EXISTS "p_operation_settings" ( + "id" bigserial PRIMARY KEY, + "key" varchar(255) NOT NULL UNIQUE, + "value" text NOT NULL, + "description" varchar(500), + "created_on" integer NOT NULL DEFAULT 0, + "updated_on" integer NOT NULL DEFAULT 0 +); + +-- 创建用户签到记录表 +CREATE TABLE IF NOT EXISTS "p_user_checkin" ( + "id" bigserial PRIMARY KEY, + "user_id" bigint NOT NULL, + "checkin_date" date NOT NULL, + "experience_gained" integer NOT NULL DEFAULT 0, + "created_on" integer NOT NULL DEFAULT 0, + "updated_on" integer NOT NULL DEFAULT 0, + UNIQUE ("user_id", "checkin_date") +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS "idx_user_checkin_user_id" ON "p_user_checkin" ("user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_checkin_checkin_date" ON "p_user_checkin" ("checkin_date"); + +-- 插入默认运营设置 +INSERT INTO "p_operation_settings" ("key", "value", "description", "created_on", "updated_on") VALUES +('checkin_enabled', 'true', '签到功能是否启用', extract(epoch from now())::integer, extract(epoch from now())::integer), +('checkin_exp_range_min', '5', '签到最小经验值', extract(epoch from now())::integer, extract(epoch from now())::integer), +('checkin_exp_range_max', '15', '签到最大经验值', extract(epoch from now())::integer, extract(epoch from now())::integer), +('checkin_rank_length', '10', '签到排行榜显示人数', extract(epoch from now())::integer, extract(epoch from now())::integer), +('level_to_exp', '{"lv1":[0,100],"lv2":[101,300],"lv3":[301,600],"lv4":[601,1000],"lv5":[1001,1500]}', '经验-等级对应关系', extract(epoch from now())::integer, extract(epoch from now())::integer); + + + + diff --git a/scripts/migration/postgres/0018_drop_user_level_table.down.sql b/scripts/migration/postgres/0018_drop_user_level_table.down.sql new file mode 100644 index 00000000..86bf5f80 --- /dev/null +++ b/scripts/migration/postgres/0018_drop_user_level_table.down.sql @@ -0,0 +1,24 @@ +-- 重新创建用户等级表 +CREATE TABLE IF NOT EXISTS "p_user_level" ( + "id" bigserial PRIMARY KEY, + "level" integer NOT NULL, + "min_experience" integer NOT NULL, + "max_experience" integer NOT NULL, + "created_on" integer NOT NULL DEFAULT 0, + "updated_on" integer NOT NULL DEFAULT 0 +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS "idx_user_level_level" ON "p_user_level" ("level"); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_user_level_min_experience" ON "p_user_level" ("min_experience"); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_user_level_max_experience" ON "p_user_level" ("max_experience"); + +-- 插入初始等级数据 +INSERT INTO "p_user_level" ("level", "min_experience", "max_experience", "created_on", "updated_on") VALUES +(1, 0, 100, extract(epoch from now())::integer, extract(epoch from now())::integer), +(2, 101, 300, extract(epoch from now())::integer, extract(epoch from now())::integer), +(3, 301, 600, extract(epoch from now())::integer, extract(epoch from now())::integer), +(4, 601, 1000, extract(epoch from now())::integer, extract(epoch from now())::integer), +(5, 1001, 1500, extract(epoch from now())::integer, extract(epoch from now())::integer); + + diff --git a/scripts/migration/postgres/0018_drop_user_level_table.up.sql b/scripts/migration/postgres/0018_drop_user_level_table.up.sql new file mode 100644 index 00000000..c0f80d76 --- /dev/null +++ b/scripts/migration/postgres/0018_drop_user_level_table.up.sql @@ -0,0 +1,4 @@ +-- 删除用户等级表 +DROP TABLE IF EXISTS "p_user_level"; + + diff --git a/scripts/migration/postgres/0020_add_block_mute_tables.down.sql b/scripts/migration/postgres/0020_add_block_mute_tables.down.sql new file mode 100644 index 00000000..2b2fe658 --- /dev/null +++ b/scripts/migration/postgres/0020_add_block_mute_tables.down.sql @@ -0,0 +1,4 @@ +-- 撤销用户拉黑和屏蔽功能相关表 + +DROP TABLE IF EXISTS "p_user_mutes"; +DROP TABLE IF EXISTS "p_user_blocks"; diff --git a/scripts/migration/postgres/0020_add_block_mute_tables.up.sql b/scripts/migration/postgres/0020_add_block_mute_tables.up.sql new file mode 100644 index 00000000..e5ae8463 --- /dev/null +++ b/scripts/migration/postgres/0020_add_block_mute_tables.up.sql @@ -0,0 +1,47 @@ +-- 添加用户拉黑和屏蔽功能相关表 + +-- 用户拉黑表 +CREATE TABLE IF NOT EXISTS "p_user_blocks" ( + "id" bigserial NOT NULL PRIMARY KEY, + "user_id" bigint NOT NULL, + "target_user_id" bigint NOT NULL, + "reason" varchar(500) DEFAULT '', + "created_on" integer NOT NULL DEFAULT 0, + "modified_on" integer NOT NULL DEFAULT 0, + "is_del" integer NOT NULL DEFAULT 0 +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS "uk_user_blocks_user_target" ON "p_user_blocks" ("user_id", "target_user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_blocks_user_id" ON "p_user_blocks" ("user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_blocks_target_user_id" ON "p_user_blocks" ("target_user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_blocks_created_on" ON "p_user_blocks" ("created_on"); + +-- 添加注释 +COMMENT ON TABLE "p_user_blocks" IS '用户拉黑表'; +COMMENT ON COLUMN "p_user_blocks"."user_id" IS '发起拉黑的用户ID'; +COMMENT ON COLUMN "p_user_blocks"."target_user_id" IS '被拉黑的用户ID'; +COMMENT ON COLUMN "p_user_blocks"."reason" IS '拉黑原因'; + +-- 用户屏蔽表 +CREATE TABLE IF NOT EXISTS "p_user_mutes" ( + "id" bigserial NOT NULL PRIMARY KEY, + "user_id" bigint NOT NULL, + "target_user_id" bigint NOT NULL, + "reason" varchar(500) DEFAULT '', + "created_on" integer NOT NULL DEFAULT 0, + "modified_on" integer NOT NULL DEFAULT 0, + "is_del" integer NOT NULL DEFAULT 0 +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS "uk_user_mutes_user_target" ON "p_user_mutes" ("user_id", "target_user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_mutes_user_id" ON "p_user_mutes" ("user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_mutes_target_user_id" ON "p_user_mutes" ("target_user_id"); +CREATE INDEX IF NOT EXISTS "idx_user_mutes_created_on" ON "p_user_mutes" ("created_on"); + +-- 添加注释 +COMMENT ON TABLE "p_user_mutes" IS '用户屏蔽表'; +COMMENT ON COLUMN "p_user_mutes"."user_id" IS '发起屏蔽的用户ID'; +COMMENT ON COLUMN "p_user_mutes"."target_user_id" IS '被屏蔽的用户ID'; +COMMENT ON COLUMN "p_user_mutes"."reason" IS '屏蔽原因'; diff --git a/scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.down.sql b/scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.down.sql new file mode 100644 index 00000000..960b10d6 --- /dev/null +++ b/scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.down.sql @@ -0,0 +1,11 @@ +-- 删除用户签到记录表 +DROP TABLE IF EXISTS p_user_checkin; + +-- 删除运营设置表 +DROP TABLE IF EXISTS p_operation_settings; + +-- 删除用户表的经验值字段 (SQLite不支持直接删除列,这里只是为了保持一致性) + + + + diff --git a/scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.up.sql b/scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.up.sql new file mode 100644 index 00000000..5d6b17e7 --- /dev/null +++ b/scripts/migration/sqlite3/0017_add_user_exp_and_operation_tables.up.sql @@ -0,0 +1,39 @@ +-- 为用户表添加经验值字段 +ALTER TABLE p_user ADD COLUMN experience INTEGER NOT NULL DEFAULT 0; + +-- 创建运营设置表 +CREATE TABLE IF NOT EXISTS p_operation_settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + value TEXT NOT NULL, + description TEXT, + created_on INTEGER NOT NULL DEFAULT 0, + updated_on INTEGER NOT NULL DEFAULT 0 +); + +-- 创建用户签到记录表 +CREATE TABLE IF NOT EXISTS p_user_checkin ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + checkin_date TEXT NOT NULL, + experience_gained INTEGER NOT NULL DEFAULT 0, + created_on INTEGER NOT NULL DEFAULT 0, + updated_on INTEGER NOT NULL DEFAULT 0, + UNIQUE(user_id, checkin_date) +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_user_checkin_user_id ON p_user_checkin(user_id); +CREATE INDEX IF NOT EXISTS idx_user_checkin_checkin_date ON p_user_checkin(checkin_date); + +-- 插入默认运营设置 +INSERT INTO p_operation_settings (key, value, description, created_on, updated_on) VALUES +('checkin_enabled', 'true', '签到功能是否启用', strftime('%s', 'now'), strftime('%s', 'now')), +('checkin_exp_range_min', '5', '签到最小经验值', strftime('%s', 'now'), strftime('%s', 'now')), +('checkin_exp_range_max', '15', '签到最大经验值', strftime('%s', 'now'), strftime('%s', 'now')), +('checkin_rank_length', '10', '签到排行榜显示人数', strftime('%s', 'now'), strftime('%s', 'now')), +('level_to_exp', '{"lv1":[0,100],"lv2":[101,300],"lv3":[301,600],"lv4":[601,1000],"lv5":[1001,1500]}', '经验-等级对应关系', strftime('%s', 'now'), strftime('%s', 'now')); + + + + diff --git a/scripts/migration/sqlite3/0018_drop_user_level_table.down.sql b/scripts/migration/sqlite3/0018_drop_user_level_table.down.sql new file mode 100644 index 00000000..04309412 --- /dev/null +++ b/scripts/migration/sqlite3/0018_drop_user_level_table.down.sql @@ -0,0 +1,24 @@ +-- 重新创建用户等级表 +CREATE TABLE IF NOT EXISTS p_user_level ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level INTEGER NOT NULL, + min_experience INTEGER NOT NULL, + max_experience INTEGER NOT NULL, + created_on INTEGER NOT NULL DEFAULT 0, + updated_on INTEGER NOT NULL DEFAULT 0 +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_level_level ON p_user_level(level); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_level_min_experience ON p_user_level(min_experience); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_level_max_experience ON p_user_level(max_experience); + +-- 插入初始等级数据 +INSERT INTO p_user_level (level, min_experience, max_experience, created_on, updated_on) VALUES +(1, 0, 100, strftime('%s', 'now'), strftime('%s', 'now')), +(2, 101, 300, strftime('%s', 'now'), strftime('%s', 'now')), +(3, 301, 600, strftime('%s', 'now'), strftime('%s', 'now')), +(4, 601, 1000, strftime('%s', 'now'), strftime('%s', 'now')), +(5, 1001, 1500, strftime('%s', 'now'), strftime('%s', 'now')); + + diff --git a/scripts/migration/sqlite3/0018_drop_user_level_table.up.sql b/scripts/migration/sqlite3/0018_drop_user_level_table.up.sql new file mode 100644 index 00000000..2c29278f --- /dev/null +++ b/scripts/migration/sqlite3/0018_drop_user_level_table.up.sql @@ -0,0 +1,4 @@ +-- 删除用户等级表 +DROP TABLE IF EXISTS p_user_level; + + diff --git a/scripts/migration/sqlite3/0020_add_block_mute_tables.down.sql b/scripts/migration/sqlite3/0020_add_block_mute_tables.down.sql new file mode 100644 index 00000000..3d3497da --- /dev/null +++ b/scripts/migration/sqlite3/0020_add_block_mute_tables.down.sql @@ -0,0 +1,4 @@ +-- 撤销用户拉黑和屏蔽功能相关表 + +DROP TABLE IF EXISTS `p_user_mutes`; +DROP TABLE IF EXISTS `p_user_blocks`; diff --git a/scripts/migration/sqlite3/0020_add_block_mute_tables.up.sql b/scripts/migration/sqlite3/0020_add_block_mute_tables.up.sql new file mode 100644 index 00000000..ebf5237f --- /dev/null +++ b/scripts/migration/sqlite3/0020_add_block_mute_tables.up.sql @@ -0,0 +1,35 @@ +-- 添加用户拉黑和屏蔽功能相关表 + +-- 用户拉黑表 +CREATE TABLE IF NOT EXISTS `p_user_blocks` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `user_id` INTEGER NOT NULL, + `target_user_id` INTEGER NOT NULL, + `reason` TEXT DEFAULT '', + `created_on` INTEGER NOT NULL DEFAULT 0, + `modified_on` INTEGER NOT NULL DEFAULT 0, + `is_del` INTEGER NOT NULL DEFAULT 0 +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS `uk_user_blocks_user_target` ON `p_user_blocks` (`user_id`, `target_user_id`); +CREATE INDEX IF NOT EXISTS `idx_user_blocks_user_id` ON `p_user_blocks` (`user_id`); +CREATE INDEX IF NOT EXISTS `idx_user_blocks_target_user_id` ON `p_user_blocks` (`target_user_id`); +CREATE INDEX IF NOT EXISTS `idx_user_blocks_created_on` ON `p_user_blocks` (`created_on`); + +-- 用户屏蔽表 +CREATE TABLE IF NOT EXISTS `p_user_mutes` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `user_id` INTEGER NOT NULL, + `target_user_id` INTEGER NOT NULL, + `reason` TEXT DEFAULT '', + `created_on` INTEGER NOT NULL DEFAULT 0, + `modified_on` INTEGER NOT NULL DEFAULT 0, + `is_del` INTEGER NOT NULL DEFAULT 0 +); + +-- 创建索引 +CREATE UNIQUE INDEX IF NOT EXISTS `uk_user_mutes_user_target` ON `p_user_mutes` (`user_id`, `target_user_id`); +CREATE INDEX IF NOT EXISTS `idx_user_mutes_user_id` ON `p_user_mutes` (`user_id`); +CREATE INDEX IF NOT EXISTS `idx_user_mutes_target_user_id` ON `p_user_mutes` (`target_user_id`); +CREATE INDEX IF NOT EXISTS `idx_user_mutes_created_on` ON `p_user_mutes` (`created_on`);