From c996830e8f07366a2f94cccf9fd24c08b6fe17c2 Mon Sep 17 00:00:00 2001 From: alimy Date: Sat, 25 Jun 2022 21:30:47 +0800 Subject: [PATCH] support search private tweet and display private tweet in index page when user login or is admin --- README.md | 2 +- internal/core/authority.go | 117 +++++++++++++++++++++++++++++ internal/core/index.go | 2 +- internal/core/search.go | 2 +- internal/dao/authority.go | 37 +++++++++ internal/dao/cache.go | 2 +- internal/dao/cache_index_big.go | 34 +++++---- internal/dao/cache_index_simple.go | 4 +- internal/dao/dao.go | 29 ++++--- internal/dao/post_index.go | 55 +++++++++++--- internal/dao/search.go | 22 +++++- internal/dao/search_bridge.go | 5 +- internal/dao/search_filter.go | 46 ++++++++++++ internal/dao/search_meili.go | 94 ++++++++++++++++++----- internal/dao/search_zinc.go | 30 +++++--- internal/model/model.go | 2 + internal/model/post.go | 24 ++++++ internal/routers/api/post.go | 6 +- internal/service/post.go | 52 ++++--------- pkg/types/types.go | 7 ++ 20 files changed, 454 insertions(+), 118 deletions(-) create mode 100644 internal/core/authority.go create mode 100644 internal/dao/authority.go create mode 100644 internal/dao/search_filter.go create mode 100644 pkg/types/types.go diff --git a/README.md b/README.md index e0e69b4d..bc515dbf 100644 --- a/README.md +++ b/README.md @@ -298,7 +298,7 @@ release/paopao-ce --no-default-features --features sqlite3,localoss,loggerfile,r `LocalOSS` 提供使用本地目录文件作为对象存储的功能,仅用于开发调试环境; * 缓存: Redis/SimpleCacheIndex/BigCacheIndex `SimpleCacheIndex` 提供简单的 广场推文列表 的缓存功能; - `BigCacheIndex` 使用[BigCache](https://github.com/allegro/bigcache)缓存 广场推文列表,缓存每个用户每一页,简单做到千人千面; + `BigCacheIndex` 使用[BigCache](https://github.com/allegro/bigcache)缓存 广场推文列表,缓存每个用户每一页,简单做到千人千面(推荐使用); * 搜索: Zinc/Meili `Zinc` 基于[Zinc](https://github.com/zinclabs/zinc)搜索引擎提供推文搜索服务(目前状态: 稳定,推荐使用); `Meili` 基于[Meilisearch](https://github.com/meilisearch/meilisearch)搜索引擎提供推文搜索服务(目前状态: 内测阶段); diff --git a/internal/core/authority.go b/internal/core/authority.go new file mode 100644 index 00000000..b27db923 --- /dev/null +++ b/internal/core/authority.go @@ -0,0 +1,117 @@ +package core + +import ( + "github.com/rocboss/paopao-ce/internal/model" + "github.com/rocboss/paopao-ce/pkg/types" +) + +const ( + ActRegisterUser act = iota + ActCreatePublicTweet + ActCreatePublicAttachment + ActCreatePublicPicture + ActCreatePublicVideo + ActCreatePrivateTweet + ActCreatePrivateAttachment + ActCreatePrivatePicture + ActCreatePrivateVideo + ActCreateFriendTweet + ActCreateFriendAttachment + ActCreateFriendPicture + ActCreateFriendVideo + ActCreatePublicComment + ActCreatePublicPicureComment + ActCreateFriendComment + ActCreateFriendPicureComment + ActCreatePrivateComment + ActCreatePrivatePicureComment + ActStickTweet + ActTopTweet + ActLockTweet + ActVisibleTweet + ActDeleteTweet + ActCreateActivationCode +) + +type act uint8 + +type FriendFilter map[int64]types.Empty + +type Action struct { + Act act + UserId int64 +} + +type AuthorizationManageService interface { + IsAllow(user *model.User, action *Action) bool + GetFriendFilter(userId int64) FriendFilter + GetFriendIds(userId int64) []int64 +} + +func (f FriendFilter) IsFriend(userId int64) bool { + // _, yesno := f[userId] + // return yesno + // so, you are friend with all world now + return true +} + +// IsAllow default true if user is admin +func (a act) IsAllow(user *model.User, userId int64, isFriend bool, isActivation bool) bool { + if user.IsAdmin { + return true + } + if user.ID == userId && isActivation { + switch a { + case ActCreatePublicTweet, + ActCreatePublicAttachment, + ActCreatePublicPicture, + ActCreatePublicVideo, + ActCreatePrivateTweet, + ActCreatePrivateAttachment, + ActCreatePrivatePicture, + ActCreatePrivateVideo, + ActCreateFriendTweet, + ActCreateFriendAttachment, + ActCreateFriendPicture, + ActCreateFriendVideo, + ActCreatePrivateComment, + ActCreatePrivatePicureComment, + ActStickTweet, + ActLockTweet, + ActVisibleTweet, + ActDeleteTweet: + return true + } + } + + if user.ID == userId && !isActivation { + switch a { + case ActCreatePrivateTweet, + ActCreatePrivateComment, + ActStickTweet, + ActLockTweet, + ActDeleteTweet: + return true + } + } + + if isFriend && isActivation { + switch a { + case ActCreatePublicComment, + ActCreatePublicPicureComment, + ActCreateFriendComment, + ActCreateFriendPicureComment: + return true + } + } + + if !isFriend && isActivation { + switch a { + case ActCreatePublicComment, + ActCreatePublicPicureComment: + return true + } + } + + return false +} diff --git a/internal/core/index.go b/internal/core/index.go index 0188e6a2..be83abec 100644 --- a/internal/core/index.go +++ b/internal/core/index.go @@ -5,5 +5,5 @@ import ( ) type IndexPostsService interface { - IndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) + IndexPosts(user *model.User, offset int, limit int) ([]*model.PostFormated, error) } diff --git a/internal/core/search.go b/internal/core/search.go index 36583f68..7e8b1db5 100644 --- a/internal/core/search.go +++ b/internal/core/search.go @@ -31,5 +31,5 @@ type TweetSearchService interface { IndexName() string AddDocuments(documents DocItems, primaryKey ...string) (bool, error) DeleteDocuments(identifiers []string) error - Search(q *QueryReq, offset, limit int) (*QueryResp, error) + Search(user *model.User, q *QueryReq, offset, limit int) (*QueryResp, error) } diff --git a/internal/dao/authority.go b/internal/dao/authority.go new file mode 100644 index 00000000..1bb0a379 --- /dev/null +++ b/internal/dao/authority.go @@ -0,0 +1,37 @@ +package dao + +import ( + "github.com/rocboss/paopao-ce/internal/conf" + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/model" +) + +func newSimpleAuthorizationManageService() *simpleAuthorizationManageService { + return &simpleAuthorizationManageService{ + db: conf.DBEngine, + } +} + +func (s *simpleAuthorizationManageService) IsAllow(user *model.User, action *core.Action) bool { + // user is activation if had bind phone + isActivation := (len(user.Phone) != 0) + isFriend := s.isFriend(action.UserId) + // TODO: just use defaut act authorization chek rule now + return action.Act.IsAllow(user, action.UserId, isFriend, isActivation) +} + +// GetFriendFilter _userId保留未来使用 +func (s *simpleAuthorizationManageService) GetFriendFilter(_userId int64) core.FriendFilter { + // TODO: just return an empty friend fileter now + return core.FriendFilter{} +} + +func (s *simpleAuthorizationManageService) GetFriendIds(_userId int64) []int64 { + // TODO: just retrun empty now + return nil +} + +func (s *simpleAuthorizationManageService) isFriend(_userId int64) bool { + // friend with all world now + return true +} diff --git a/internal/dao/cache.go b/internal/dao/cache.go index 143d0d27..48a06c05 100644 --- a/internal/dao/cache.go +++ b/internal/dao/cache.go @@ -19,7 +19,7 @@ type postsEntry struct { posts []*model.PostFormated } -type indexPostsFunc func(int64, int, int) ([]*model.PostFormated, error) +type indexPostsFunc func(*model.User, int, int) ([]*model.PostFormated, error) type bigCacheIndexServant struct { getIndexPosts indexPostsFunc diff --git a/internal/dao/cache_index_big.go b/internal/dao/cache_index_big.go index d221bdb5..e39cfab1 100644 --- a/internal/dao/cache_index_big.go +++ b/internal/dao/cache_index_big.go @@ -49,18 +49,18 @@ func newBigCacheIndexServant(getIndexPosts indexPostsFunc) *bigCacheIndexServant return cacheIndex } -func (s *bigCacheIndexServant) IndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) { - key := s.keyFrom(userId, offset, limit) +func (s *bigCacheIndexServant) IndexPosts(user *model.User, offset int, limit int) ([]*model.PostFormated, error) { + key := s.keyFrom(user, offset, limit) posts, err := s.getPosts(key) if err == nil { - logrus.Debugf("get index posts from cache by key: %s userId: %d offset:%d limit:%d", key, userId, offset, limit) + logrus.Debugf("bigCacheIndexServant.IndexPosts get index posts from cache by key: %s", key) return posts, nil } - if posts, err = s.getIndexPosts(userId, offset, limit); err != nil { + if posts, err = s.getIndexPosts(user, offset, limit); err != nil { return nil, err } - logrus.Debugf("get index posts from database by userId: %d offset:%d limit:%d", userId, offset, limit) + logrus.Debugf("bigCacheIndexServant.IndexPosts get index posts from database by key: %s", key) s.cachePosts(key, posts) return posts, nil } @@ -68,14 +68,14 @@ func (s *bigCacheIndexServant) IndexPosts(userId int64, offset int, limit int) ( func (s *bigCacheIndexServant) getPosts(key string) ([]*model.PostFormated, error) { data, err := s.cache.Get(key) if err != nil { - logrus.Debugf("get posts by key: %s from cache err: %v", key, err) + logrus.Debugf("bigCacheIndexServant.getPosts get posts by key: %s from cache err: %v", key, err) return nil, err } buf := bytes.NewBuffer(data) dec := gob.NewDecoder(buf) var posts []*model.PostFormated if err := dec.Decode(&posts); err != nil { - logrus.Debugf("get posts from cache in decode err: %v", err) + logrus.Debugf("bigCacheIndexServant.getPosts get posts from cache in decode err: %v", err) return nil, err } return posts, nil @@ -85,10 +85,10 @@ func (s *bigCacheIndexServant) cachePosts(key string, posts []*model.PostFormate entry := &postsEntry{key: key, posts: posts} select { case s.cachePostsCh <- entry: - logrus.Debugf("cachePosts by chan of key: %s", key) + logrus.Debugf("bigCacheIndexServant.cachePosts cachePosts by chan of key: %s", key) default: go func(ch chan<- *postsEntry, entry *postsEntry) { - logrus.Debugf("cachePosts indexAction by goroutine of key: %s", key) + logrus.Debugf("bigCacheIndexServant.cachePosts cachePosts indexAction by goroutine of key: %s", key) ch <- entry }(s.cachePostsCh, entry) } @@ -98,26 +98,30 @@ func (s *bigCacheIndexServant) setPosts(entry *postsEntry) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) if err := enc.Encode(entry.posts); err != nil { - logrus.Debugf("setPosts encode post entry err: %v", err) + logrus.Debugf("bigCacheIndexServant.setPosts setPosts encode post entry err: %v", err) return } if err := s.cache.Set(entry.key, buf.Bytes()); err != nil { - logrus.Debugf("setPosts set cache err: %v", err) + logrus.Debugf("bigCacheIndexServant.setPosts setPosts set cache err: %v", err) } - logrus.Debugf("setPosts set cache by key: %s", entry.key) + logrus.Debugf("bigCacheIndexServant.setPosts setPosts set cache by key: %s", entry.key) } -func (s *bigCacheIndexServant) keyFrom(userId int64, offset int, limit int) string { +func (s *bigCacheIndexServant) keyFrom(user *model.User, offset int, limit int) string { + var userId int64 = -1 + if user != nil { + userId = user.ID + } return fmt.Sprintf("index:%d:%d:%d", userId, offset, limit) } func (s *bigCacheIndexServant) SendAction(act core.IndexActionT) { select { case s.indexActionCh <- act: - logrus.Debugf("send indexAction by chan: %s", act) + logrus.Debugf("bigCacheIndexServant.SendAction send indexAction by chan: %s", act) default: go func(ch chan<- core.IndexActionT, act core.IndexActionT) { - logrus.Debugf("send indexAction by goroutine: %s", act) + logrus.Debugf("bigCacheIndexServant.SendAction send indexAction by goroutine: %s", act) ch <- act }(s.indexActionCh, act) } diff --git a/internal/dao/cache_index_simple.go b/internal/dao/cache_index_simple.go index a5014b7a..456d760a 100644 --- a/internal/dao/cache_index_simple.go +++ b/internal/dao/cache_index_simple.go @@ -49,7 +49,7 @@ func newSimpleCacheIndexServant(getIndexPosts indexPostsFunc) *simpleCacheIndexS return cacheIndex } -func (s *simpleCacheIndexServant) IndexPosts(_userId int64, offset int, limit int) ([]*model.PostFormated, error) { +func (s *simpleCacheIndexServant) IndexPosts(_user *model.User, offset int, limit int) ([]*model.PostFormated, error) { posts := s.atomicIndex.Load().([]*model.PostFormated) end := offset + limit size := len(posts) @@ -79,7 +79,7 @@ func (s *simpleCacheIndexServant) startIndexPosts() { case <-s.checkTick.C: if len(s.indexPosts) == 0 { logrus.Debugf("index posts by checkTick") - if s.indexPosts, err = s.getIndexPosts(0, 0, s.maxIndexSize); err == nil { + if s.indexPosts, err = s.getIndexPosts(nil, 0, s.maxIndexSize); err == nil { s.atomicIndex.Store(s.indexPosts) } else { logrus.Errorf("get index posts err: %v", err) diff --git a/internal/dao/dao.go b/internal/dao/dao.go index 15653765..5d77c6b4 100644 --- a/internal/dao/dao.go +++ b/internal/dao/dao.go @@ -3,22 +3,26 @@ package dao import ( "github.com/rocboss/paopao-ce/internal/conf" "github.com/rocboss/paopao-ce/internal/core" - "github.com/rocboss/paopao-ce/pkg/zinc" "github.com/sirupsen/logrus" "gorm.io/gorm" ) var ( - _ core.DataService = (*dataServant)(nil) - _ core.AttachmentCheckService = (*attachmentCheckServant)(nil) + _ core.DataService = (*dataServant)(nil) + _ core.AttachmentCheckService = (*attachmentCheckServant)(nil) + _ core.AuthorizationManageService = (*simpleAuthorizationManageService)(nil) ) type dataServant struct { - useCacheIndex bool - cacheIndex core.CacheIndexService + useCacheIndex bool + cacheIndex core.CacheIndexService + ams core.AuthorizationManageService + engine *gorm.DB + getIndexPostsFunc indexPostsFunc +} - engine *gorm.DB - zinc *zinc.ZincClient +type simpleAuthorizationManageService struct { + db *gorm.DB } type attachmentCheckServant struct { @@ -26,17 +30,18 @@ type attachmentCheckServant struct { } func NewDataService() core.DataService { - client := zinc.NewClient(conf.ZincSetting) ds := &dataServant{ engine: conf.DBEngine, - zinc: client, + ams: NewAuthorizationManageService(), } // initialize CacheIndex if needed ds.useCacheIndex = true if conf.CfgIf("SimpleCacheIndex") { - ds.cacheIndex = newSimpleCacheIndexServant(ds.getIndexPosts) + ds.getIndexPostsFunc = ds.simpleCacheIndexGetPosts + ds.cacheIndex = newSimpleCacheIndexServant(ds.simpleCacheIndexGetPosts) } else if conf.CfgIf("BigCacheIndex") { + ds.getIndexPostsFunc = ds.getIndexPosts ds.cacheIndex = newBigCacheIndexServant(ds.getIndexPosts) } else { ds.useCacheIndex = false @@ -49,6 +54,10 @@ func NewDataService() core.DataService { return ds } +func NewAuthorizationManageService() core.AuthorizationManageService { + return newSimpleAuthorizationManageService() +} + func NewAttachmentCheckService() core.AttachmentCheckService { return &attachmentCheckServant{ domain: getOssDomain(), diff --git a/internal/dao/post_index.go b/internal/dao/post_index.go index 1f35e164..b3ae8e10 100644 --- a/internal/dao/post_index.go +++ b/internal/dao/post_index.go @@ -3,31 +3,62 @@ package dao import ( "github.com/rocboss/paopao-ce/internal/core" "github.com/rocboss/paopao-ce/internal/model" + "github.com/rocboss/paopao-ce/pkg/types" "github.com/sirupsen/logrus" ) -func (d *dataServant) IndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) { +func (d *dataServant) IndexPosts(user *model.User, offset int, limit int) ([]*model.PostFormated, error) { if d.useCacheIndex { - if posts, err := d.cacheIndex.IndexPosts(userId, offset, limit); err == nil { - logrus.Debugf("get index posts from cached by userId: %d", userId) + if posts, err := d.cacheIndex.IndexPosts(user, offset, limit); err == nil { + logrus.Debugf("get index posts from cached") return posts, nil } } logrus.Debugf("get index posts from database but useCacheIndex: %t", d.useCacheIndex) - return d.getIndexPosts(userId, offset, limit) + return d.getIndexPostsFunc(user, offset, limit) } -// getIndexPosts _userId保留未来使用 -// TODO: 未来可能根据userId查询广场推文列表,简单做到不同用户的主页都是不同的; -func (d *dataServant) getIndexPosts(_userId int64, offset int, limit int) ([]*model.PostFormated, error) { - posts, err := (&model.Post{}).List(d.engine, &model.ConditionsT{ - "visibility IN ?": []model.PostVisibleT{model.PostVisitPublic, model.PostVisitFriend}, - "ORDER": "is_top DESC, latest_replied_on DESC", - }, offset, limit) +// getIndexPosts TODO: 未来可能根据userId查询广场推文列表,简单做到不同用户的主页都是不同的; +func (d *dataServant) getIndexPosts(user *model.User, offset int, limit int) ([]*model.PostFormated, error) { + predicates := model.Predicates{ + "ORDER": types.AnySlice{"is_top DESC, latest_replied_on DESC"}, + } + if user == nil { + predicates["visibility = ?"] = types.AnySlice{model.PostVisitPublic} + } else if !user.IsAdmin { + frienddIds := d.ams.GetFriendIds(user.ID) + if len(frienddIds) > 0 { + args := types.AnySlice{model.PostVisitPublic, model.PostVisitPrivate, user.ID, model.PostVisitFriend, frienddIds} + predicates["visibility = ? OR (visibility = ? AND user_id = ?) OR (visibility = ? AND user_id IN ?"] = args + } else { + // TODO: 目前不处理没朋友情况,默认用户与世界都是朋友,但是目前无从知晓朋友id,所以这里特殊处理,后续完善 + args := types.AnySlice{model.PostVisitPrivate, user.ID, model.PostVisitPublic, model.PostVisitFriend} + predicates["(visibility = ? AND user_id = ?) OR visibility = ? OR visibility = ?"] = args + } + } + + posts, err := (&model.Post{}).Fetch(d.engine, predicates, offset, limit) + if err != nil { + logrus.Debugf("dataServant.getIndexPosts err: %v", err) + return nil, err + } + + return d.MergePosts(posts) +} + +// simpleCacheIndexGetPosts simpleCacheIndex 专属获取广场推文列表函数 +func (d *dataServant) simpleCacheIndexGetPosts(_user *model.User, offset int, limit int) ([]*model.PostFormated, error) { + predicates := model.Predicates{ + "visibility IN ?": types.AnySlice{[]model.PostVisibleT{model.PostVisitPublic, model.PostVisitPublic}}, + "ORDER": types.AnySlice{"is_top DESC, latest_replied_on DESC"}, + } + + posts, err := (&model.Post{}).Fetch(d.engine, predicates, offset, limit) if err != nil { - logrus.Debugf("getIndexPosts err: %v", err) + logrus.Debugf("dataServant.simpleCacheIndexGetPosts err: %v", err) return nil, err } + return d.MergePosts(posts) } diff --git a/internal/dao/search.go b/internal/dao/search.go index 04791618..fb310ffe 100644 --- a/internal/dao/search.go +++ b/internal/dao/search.go @@ -24,14 +24,28 @@ type bridgeTweetSearchServant struct { updateDocsCh chan *documents } +type tweetSearchFilter struct { + ams core.AuthorizationManageService +} + type zincTweetSearchServant struct { - indexName string - client *zinc.ZincClient + tweetSearchFilter + + indexName string + client *zinc.ZincClient + publicFilter string + privateFilter string + friendFilter string } type meiliTweetSearchServant struct { - client *meilisearch.Client - index *meilisearch.Index + tweetSearchFilter + + client *meilisearch.Client + index *meilisearch.Index + publicFilter string + privateFilter string + friendFilter string } func NewTweetSearchService() core.TweetSearchService { diff --git a/internal/dao/search_bridge.go b/internal/dao/search_bridge.go index 26b96f92..f4d1c1b3 100644 --- a/internal/dao/search_bridge.go +++ b/internal/dao/search_bridge.go @@ -3,6 +3,7 @@ package dao import ( "github.com/Masterminds/semver/v3" "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/model" "github.com/sirupsen/logrus" ) @@ -33,8 +34,8 @@ func (s *bridgeTweetSearchServant) DeleteDocuments(identifiers []string) error { return nil } -func (s *bridgeTweetSearchServant) Search(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { - return s.ts.Search(q, offset, limit) +func (s *bridgeTweetSearchServant) Search(user *model.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { + return s.ts.Search(user, q, offset, limit) } func (s *bridgeTweetSearchServant) updateDocs(doc *documents) { diff --git a/internal/dao/search_filter.go b/internal/dao/search_filter.go new file mode 100644 index 00000000..f73bd7d4 --- /dev/null +++ b/internal/dao/search_filter.go @@ -0,0 +1,46 @@ +package dao + +import ( + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/model" +) + +func (s *tweetSearchFilter) filterResp(user *model.User, resp *core.QueryResp) { + // 管理员不过滤 + if user != nil && user.IsAdmin { + return + } + + var item *model.PostFormated + items := resp.Items + latestIndex := len(items) - 1 + if user == nil { + for i := 0; i <= latestIndex; i++ { + item = items[i] + if item.Visibility != model.PostVisitPublic { + items[i] = items[latestIndex] + items = items[:latestIndex] + resp.Total-- + latestIndex-- + i-- + } + } + } else { + var cutFriend, cutPrivate bool + friendFilter := s.ams.GetFriendFilter(user.ID) + for i := 0; i <= latestIndex; i++ { + item = items[i] + cutFriend = (item.Visibility == model.PostVisitFriend && !friendFilter.IsFriend(item.UserID)) + cutPrivate = (item.Visibility == model.PostVisitPrivate && user.ID != item.UserID) + if cutFriend || cutPrivate { + items[i] = items[latestIndex] + items = items[:latestIndex] + resp.Total-- + latestIndex-- + i-- + } + } + } + + resp.Items = items +} diff --git a/internal/dao/search_meili.go b/internal/dao/search_meili.go index 44aca968..32eb4a7a 100644 --- a/internal/dao/search_meili.go +++ b/internal/dao/search_meili.go @@ -1,6 +1,8 @@ package dao import ( + "fmt" + "github.com/Masterminds/semver/v3" "github.com/meilisearch/meilisearch-go" "github.com/rocboss/paopao-ce/internal/conf" @@ -25,7 +27,7 @@ func newMeiliTweetSearchServant() *meiliTweetSearchServant { }) searchableAttributes := []string{"content", "tags"} sortableAttributes := []string{"is_top", "latest_replied_on"} - filterableAttributes := []string{"tags"} + filterableAttributes := []string{"tags", "visibility", "user_id"} index := client.Index(s.Index) index.UpdateSearchableAttributes(&searchableAttributes) @@ -34,8 +36,14 @@ func newMeiliTweetSearchServant() *meiliTweetSearchServant { } return &meiliTweetSearchServant{ - client: client, - index: client.Index(s.Index), + tweetSearchFilter: tweetSearchFilter{ + ams: NewAuthorizationManageService(), + }, + client: client, + index: client.Index(s.Index), + publicFilter: fmt.Sprintf("visibility=%d", model.PostVisitPublic), + privateFilter: fmt.Sprintf("visibility=%d AND user_id=", model.PostVisitPrivate), + friendFilter: fmt.Sprintf("visibility=%d", model.PostVisitFriend), } } @@ -44,7 +52,7 @@ func (s *meiliTweetSearchServant) Name() string { } func (s *meiliTweetSearchServant) Version() *semver.Version { - return semver.MustParse("v0.1.0") + return semver.MustParse("v0.2.0") } func (s *meiliTweetSearchServant) IndexName() string { @@ -69,54 +77,102 @@ func (s *meiliTweetSearchServant) DeleteDocuments(identifiers []string) error { return nil } -func (s *meiliTweetSearchServant) Search(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { +func (s *meiliTweetSearchServant) Search(user *model.User, q *core.QueryReq, offset, limit int) (resp *core.QueryResp, err error) { if q.Type == core.SearchTypeDefault && q.Query != "" { - return s.queryByContent(q, offset, limit) + resp, err = s.queryByContent(user, q, offset, limit) } else if q.Type == core.SearchTypeTag && q.Query != "" { - return s.queryByTag(q, offset, limit) + resp, err = s.queryByTag(user, q, offset, limit) + } else { + resp, err = s.queryAny(user, offset, limit) + } + if err != nil { + logrus.Errorf("meiliTweetSearchServant.search searchType:%s query:%s error:%v", q.Type, q.Query, err) + return } - return s.queryAny(offset, limit) + + logrus.Debugf("meiliTweetSearchServant.Search type:%s query:%s resp Hits:%d NbHits:%d offset: %d limit:%d ", q.Type, q.Query, len(resp.Items), resp.Total, offset, limit) + s.filterResp(user, resp) + return } -func (s *meiliTweetSearchServant) queryByContent(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { - resp, err := s.index.Search(q.Query, &meilisearch.SearchRequest{ +func (s *meiliTweetSearchServant) queryByContent(user *model.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { + request := &meilisearch.SearchRequest{ Offset: int64(offset), Limit: int64(limit), Sort: []string{"is_top:desc", "latest_replied_on:desc"}, - }) + } + + filter := s.filterList(user) + if len(filter) > 0 { + request.Filter = filter + } + + // logrus.Debugf("meiliTweetSearchServant.queryByContent query:%s request%+v", q.Query, request) + resp, err := s.index.Search(q.Query, request) if err != nil { return nil, err } + return s.postsFrom(resp) } -func (s *meiliTweetSearchServant) queryByTag(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { - resp, err := s.index.Search("#"+q.Query, &meilisearch.SearchRequest{ +func (s *meiliTweetSearchServant) queryByTag(user *model.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { + request := &meilisearch.SearchRequest{ Offset: int64(offset), Limit: int64(limit), - Filter: []string{"tags." + q.Query + "=1"}, Sort: []string{"is_top:desc", "latest_replied_on:desc"}, - }) + } + + filter := s.filterList(user) + tagFilter := []string{"tags." + q.Query + "=1"} + if len(filter) > 0 { + request.Filter = [][]string{tagFilter, {filter}} + } else { + request.Filter = tagFilter + } + + // logrus.Debugf("meiliTweetSearchServant.queryByTag query:%s request%+v", q.Query, request) + resp, err := s.index.Search("#"+q.Query, request) if err != nil { return nil, err } + return s.postsFrom(resp) } -func (s *meiliTweetSearchServant) queryAny(offset, limit int) (*core.QueryResp, error) { - resp, err := s.index.Search("", &meilisearch.SearchRequest{ +func (s *meiliTweetSearchServant) queryAny(user *model.User, offset, limit int) (*core.QueryResp, error) { + request := &meilisearch.SearchRequest{ Offset: int64(offset), Limit: int64(limit), Sort: []string{"is_top:desc", "latest_replied_on:desc"}, - }) + } + + filter := s.filterList(user) + if len(filter) > 0 { + request.Filter = filter + } + + resp, err := s.index.Search("", request) if err != nil { return nil, err } + return s.postsFrom(resp) } +func (s *meiliTweetSearchServant) filterList(user *model.User) string { + if user == nil { + return s.publicFilter + } + + if user.IsAdmin { + return "" + } + + return fmt.Sprintf("%s OR %s OR (%s%d)", s.publicFilter, s.friendFilter, s.privateFilter, user.ID) +} + func (s *meiliTweetSearchServant) postsFrom(resp *meilisearch.SearchResponse) (*core.QueryResp, error) { - logrus.Debugf("resp Hits:%d NbHits:%d offset: %d limit:%d ", len(resp.Hits), resp.NbHits, resp.Offset, resp.Limit) posts := make([]*model.PostFormated, 0, len(resp.Hits)) for _, hit := range resp.Hits { item := &model.PostFormated{} diff --git a/internal/dao/search_zinc.go b/internal/dao/search_zinc.go index dc3630a5..134f919a 100644 --- a/internal/dao/search_zinc.go +++ b/internal/dao/search_zinc.go @@ -1,6 +1,8 @@ package dao import ( + "fmt" + "github.com/Masterminds/semver/v3" "github.com/rocboss/paopao-ce/internal/conf" "github.com/rocboss/paopao-ce/internal/core" @@ -12,8 +14,14 @@ import ( func newZincTweetSearchServant() *zincTweetSearchServant { s := conf.ZincSetting zts := &zincTweetSearchServant{ - indexName: s.Index, - client: zinc.NewClient(s), + tweetSearchFilter: tweetSearchFilter{ + ams: NewAuthorizationManageService(), + }, + indexName: s.Index, + client: zinc.NewClient(s), + publicFilter: fmt.Sprintf("visibility:%d", model.PostVisitPublic), + privateFilter: fmt.Sprintf("visibility:%d AND user_id:%%d", model.PostVisitPrivate), + friendFilter: fmt.Sprintf("visibility:%d", model.PostVisitFriend), } zts.createIndex() @@ -25,7 +33,7 @@ func (s *zincTweetSearchServant) Name() string { } func (s *zincTweetSearchServant) Version() *semver.Version { - return semver.MustParse("v0.1.0") + return semver.MustParse("v0.2.0") } func (s *zincTweetSearchServant) IndexName() string { @@ -61,16 +69,18 @@ func (s *zincTweetSearchServant) DeleteDocuments(identifiers []string) error { return nil } -func (s *zincTweetSearchServant) Search(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { +func (s *zincTweetSearchServant) Search(user *model.User, q *core.QueryReq, offset, limit int) (resp *core.QueryResp, err error) { if q.Type == core.SearchTypeDefault && q.Query != "" { - return s.queryByContent(q, offset, limit) + resp, err = s.queryByContent(user, q, offset, limit) } else if q.Type == core.SearchTypeTag && q.Query != "" { - return s.queryByTag(q, offset, limit) + resp, err = s.queryByTag(user, q, offset, limit) } - return s.queryAny(offset, limit) + resp, err = s.queryAny(user, offset, limit) + s.filterResp(user, resp) + return } -func (s *zincTweetSearchServant) queryByContent(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { +func (s *zincTweetSearchServant) queryByContent(user *model.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { resp, err := s.client.EsQuery(s.indexName, map[string]interface{}{ "query": map[string]interface{}{ "match_phrase": map[string]interface{}{ @@ -87,7 +97,7 @@ func (s *zincTweetSearchServant) queryByContent(q *core.QueryReq, offset, limit return s.postsFrom(resp) } -func (s *zincTweetSearchServant) queryByTag(q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { +func (s *zincTweetSearchServant) queryByTag(user *model.User, q *core.QueryReq, offset, limit int) (*core.QueryResp, error) { resp, err := s.client.ApiQuery(s.indexName, map[string]interface{}{ "search_type": "querystring", "query": map[string]interface{}{ @@ -103,7 +113,7 @@ func (s *zincTweetSearchServant) queryByTag(q *core.QueryReq, offset, limit int) return s.postsFrom(resp) } -func (s *zincTweetSearchServant) queryAny(offset, limit int) (*core.QueryResp, error) { +func (s *zincTweetSearchServant) queryAny(user *model.User, offset, limit int) (*core.QueryResp, error) { queryMap := map[string]interface{}{ "query": map[string]interface{}{ "match_all": map[string]string{}, diff --git a/internal/model/model.go b/internal/model/model.go index b4031dae..bdf8ade6 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -3,6 +3,7 @@ package model import ( "time" + "github.com/rocboss/paopao-ce/pkg/types" "gorm.io/gorm" "gorm.io/plugin/soft_delete" ) @@ -17,6 +18,7 @@ type Model struct { } type ConditionsT map[string]interface{} +type Predicates map[string]types.AnySlice func (m *Model) BeforeCreate(tx *gorm.DB) (err error) { nowTime := time.Now().Unix() diff --git a/internal/model/post.go b/internal/model/post.go index c8943464..77cd16f7 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -137,6 +137,30 @@ func (p *Post) List(db *gorm.DB, conditions *ConditionsT, offset, limit int) ([] return posts, nil } +func (p *Post) Fetch(db *gorm.DB, predicates Predicates, offset, limit int) ([]*Post, error) { + var posts []*Post + var err error + if offset >= 0 && limit > 0 { + db = db.Offset(offset).Limit(limit) + } + if p.UserID > 0 { + db = db.Where("user_id = ?", p.UserID) + } + for query, args := range predicates { + if query == "ORDER" { + db = db.Order(args[0]) + } else { + db = db.Where(query, args...) + } + } + + if err = db.Where("is_del = ?", 0).Find(&posts).Error; err != nil { + return nil, err + } + + return posts, nil +} + func (p *Post) Count(db *gorm.DB, conditions *ConditionsT) (int64, error) { var count int64 if p.UserID > 0 { diff --git a/internal/routers/api/post.go b/internal/routers/api/post.go index a779532b..9f3e9c70 100644 --- a/internal/routers/api/post.go +++ b/internal/routers/api/post.go @@ -22,10 +22,10 @@ func GetPostList(c *gin.Context) { q.Type = "tag" } - userId, _ := userIdFrom(c) + user, _ := userFrom(c) offset, limit := app.GetPageOffset(c) if q.Query == "" && q.Type == "search" { - posts, err := service.GetIndexPosts(userId, offset, limit) + posts, err := service.GetIndexPosts(user, offset, limit) if err != nil { logrus.Errorf("service.GetPostList err: %v\n", err) response.ToErrorResponse(errcode.GetPostsFailed) @@ -38,7 +38,7 @@ func GetPostList(c *gin.Context) { response.ToResponseList(posts, totalRows) } else { - posts, totalRows, err := service.GetPostListFromSearch(q, offset, limit) + posts, totalRows, err := service.GetPostListFromSearch(user, q, offset, limit) if err != nil { logrus.Errorf("service.GetPostListFromSearch err: %v\n", err) diff --git a/internal/service/post.go b/internal/service/post.go index 9af4dad4..5bb35832 100644 --- a/internal/service/post.go +++ b/internal/service/post.go @@ -141,8 +141,8 @@ func CreatePost(c *gin.Context, userID int64, param PostCreationReq) (*model.Pos } } - // TODO: 目前非私密文章才能有如下操作,后续再优化 - if post.Visibility != model.PostVisitPrivate { + // 私密推文不创建标签与用户提醒 + if post.Visibility == model.PostVisitPrivate { // 创建标签 for _, t := range tags { tag := &model.Tag{ @@ -153,6 +153,7 @@ func CreatePost(c *gin.Context, userID int64, param PostCreationReq) (*model.Pos return nil, err } } + // 创建用户消息提醒 for _, u := range param.Users { user, err := ds.GetUserByUsername(u) @@ -170,10 +171,11 @@ func CreatePost(c *gin.Context, userID int64, param PostCreationReq) (*model.Pos PostID: post.ID, }) } - // 推送Search - PushPostToSearch(post) } + // 推送Search + PushPostToSearch(post) + return post, nil } @@ -240,28 +242,11 @@ func VisiblePost(user *model.User, postId int64, visibility model.PostVisibleT) return err } - // 相同属性,不需要操作了 - oldVisibility := post.Visibility - if oldVisibility == visibility { - logrus.Infof("sample visibility no need operate postId: %d", postId) - return nil - } - if err = ds.VisiblePost(post, visibility); err != nil { logrus.Warnf("update post failure: %v", err) return errcode.VisblePostFailed } - // 搜索处理 - if oldVisibility == model.PostVisitPrivate { - // 从私密转为非私密需要push - logrus.Debugf("visible post set to re-public to add search index: %d, visibility: %s", post.ID, visibility) - PushPostToSearch(post) - } else if visibility == model.PostVisitPrivate { - // 从非私密转为私密需要删除索引 - logrus.Debugf("visible post set to private to delete search index: %d, visibility: %s", post.ID, visibility) - DeleteSearchPost(post) - } return nil } @@ -413,8 +398,8 @@ func GetPostContentByID(id int64) (*model.PostContent, error) { return ds.GetPostContentByID(id) } -func GetIndexPosts(userId int64, offset int, limit int) ([]*model.PostFormated, error) { - return ds.IndexPosts(userId, offset, limit) +func GetIndexPosts(user *model.User, offset int, limit int) ([]*model.PostFormated, error) { + return ds.IndexPosts(user, offset, limit) } func GetPostList(req *PostListReq) ([]*model.PostFormated, error) { @@ -431,8 +416,8 @@ func GetPostCount(conditions *model.ConditionsT) (int64, error) { return ds.GetPostCount(conditions) } -func GetPostListFromSearch(q *core.QueryReq, offset, limit int) ([]*model.PostFormated, int64, error) { - resp, err := ts.Search(q, offset, limit) +func GetPostListFromSearch(user *model.User, q *core.QueryReq, offset, limit int) ([]*model.PostFormated, int64, error) { + resp, err := ts.Search(user, q, offset, limit) if err != nil { return nil, 0, err } @@ -443,20 +428,15 @@ func GetPostListFromSearch(q *core.QueryReq, offset, limit int) ([]*model.PostFo return posts, resp.Total, nil } -func GetPostListFromSearchByQuery(query string, offset, limit int) ([]*model.PostFormated, int64, error) { +func GetPostListFromSearchByQuery(user *model.User, query string, offset, limit int) ([]*model.PostFormated, int64, error) { q := &core.QueryReq{ Query: query, Type: "search", } - return GetPostListFromSearch(q, offset, limit) + return GetPostListFromSearch(user, q, offset, limit) } func PushPostToSearch(post *model.Post) { - // TODO: 暂时不索引私密文章,后续再完善 - if post.Visibility == model.PostVisitPrivate { - return - } - postFormated := post.Format() postFormated.User = &model.UserFormated{ ID: post.UserID, @@ -516,11 +496,9 @@ func PushPostsToSearch(c *gin.Context) { for i := 0; i < nums; i++ { posts, _ := GetPostList(&PostListReq{ - Conditions: &model.ConditionsT{ - "visibility IN ?": []model.PostVisibleT{model.PostVisitPublic, model.PostVisitFriend}, - }, - Offset: i * splitNum, - Limit: splitNum, + Conditions: &model.ConditionsT{}, + Offset: i * splitNum, + Limit: splitNum, }) for _, post := range posts { diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 00000000..018502b6 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,7 @@ +package types + +type Empty = struct{} + +type Any = interface{} + +type AnySlice = []interface{}