From 9f28a747358a0ec0608b324dd77946b040ae041a Mon Sep 17 00:00:00 2001 From: bianjiajie Date: Fri, 27 Jan 2023 11:41:30 +0800 Subject: [PATCH] =?UTF-8?q?[feature]=20=E5=A2=9E=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E9=9A=8F=E6=9C=BA=E5=9B=BE=E7=89=87=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/api/main.go | 8 +- internal/app/api/handler/bilbil_picture.go | 11 +++ internal/app/api/idl/bilibili_picture.go | 5 + internal/app/api/router/router.go | 1 + internal/app/api/service/bilbil_picture.go | 107 +++++++++++++++++++++ internal/app/provide.go | 1 + internal/repository/bilibili_picture.go | 20 ++++ 7 files changed, 152 insertions(+), 1 deletion(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 53cdc03..4e02d92 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -4,6 +4,7 @@ import ( "context" "git.vtb.link/eoefans/internal/app" + "git.vtb.link/eoefans/internal/app/api/service" "git.vtb.link/eoefans/internal/launcher" "git.vtb.link/eoefans/internal/pkg/database" "git.vtb.link/eoefans/internal/pkg/httpserver" @@ -23,7 +24,7 @@ func newAPI() fx.Option { ) } -func lc(lifecycle fx.Lifecycle, ginServer *httpserver.Server) { +func lc(lifecycle fx.Lifecycle, ginServer *httpserver.Server, randPicsCache *service.RandomPicsCache) { lifecycle.Append(fx.Hook{ OnStart: func(ctx context.Context) error { return ginServer.Start() @@ -32,4 +33,9 @@ func lc(lifecycle fx.Lifecycle, ginServer *httpserver.Server) { return ginServer.Stop() }, }) + lifecycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return randPicsCache.Run() + }, + }) } diff --git a/internal/app/api/handler/bilbil_picture.go b/internal/app/api/handler/bilbil_picture.go index 73aecd1..3127ddc 100644 --- a/internal/app/api/handler/bilbil_picture.go +++ b/internal/app/api/handler/bilbil_picture.go @@ -41,3 +41,14 @@ func BilibiliRecommendPics(s *service.BilbilPicture) func(ctx *gin.Context) { } } } + +func BilibiliRandomPic(s *service.BilbilPicture) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + if resp, err := s.Random(ctx); err != nil { + _ = ctx.Error(err) + return + } else { + ctx.JSON(http.StatusOK, help.SuccessJson(resp)) + } + } +} diff --git a/internal/app/api/idl/bilibili_picture.go b/internal/app/api/idl/bilibili_picture.go index 8394b1a..fc04f2c 100644 --- a/internal/app/api/idl/bilibili_picture.go +++ b/internal/app/api/idl/bilibili_picture.go @@ -65,6 +65,10 @@ type BilibiliPicturesRecommendResp struct { Page int `json:"page"` Total int `json:"total"` } + +type BilibiliPictureRandomResp struct { + BilibiliDynamicPicture +} type BilibiliDynamicDTO struct { DynamicID uint64 `json:"dynamic_id"` Pictures BilibiliDynamicPictures `json:"pictures"` @@ -90,4 +94,5 @@ type BilibiliPictureRepository interface { FindAllByPubDate(from, to time.Time, page, size int64) (list []*BilibiliDynamic, err error) Latest(page, size, topicID int) (list []*BilibiliDynamic, err error) Recommend(from, to time.Time, page, size, topicID int) (list []*BilibiliDynamic, err error) + Random(rand float64) (list []*BilibiliDynamic, err error) } diff --git a/internal/app/api/router/router.go b/internal/app/api/router/router.go index 7b70798..f6bcdc6 100644 --- a/internal/app/api/router/router.go +++ b/internal/app/api/router/router.go @@ -33,6 +33,7 @@ func InitRouters( { picApi.GET("/latest", handler.BilibiliLatestPics(picService)) picApi.GET("/recommend", handler.BilibiliRecommendPics(picService)) + picApi.GET("/random", handler.BilibiliRandomPic(picService)) } // Auth相关 authApi := r.Group("/v1/auth") diff --git a/internal/app/api/service/bilbil_picture.go b/internal/app/api/service/bilbil_picture.go index e788f85..6db2d74 100644 --- a/internal/app/api/service/bilbil_picture.go +++ b/internal/app/api/service/bilbil_picture.go @@ -2,10 +2,14 @@ package service import ( "context" + "errors" + "math/rand" + "sync" "time" "git.vtb.link/eoefans/internal/app/api/idl" "git.vtb.link/eoefans/internal/repository" + "go.uber.org/zap" "gorm.io/gorm" ) @@ -13,6 +17,73 @@ const ( picRecommendDefaultSize = 20 ) +type RandomPicKey string + +const ( + overall RandomPicKey = "overall" +) + +var ( + errRandomPictureNotFound = errors.New("random picture was not found") +) +var ( + randomPicsCache *RandomPicsCache = &RandomPicsCache{ + syncMap: &sync.Map{}, + } +) + +type RandomPicsCache struct { + syncMap *sync.Map + success bool + db *gorm.DB + log *zap.Logger +} + +func NewRandomPicsCache(db *gorm.DB, log *zap.Logger) *RandomPicsCache { + randomPicsCache.db = db + randomPicsCache.log = log + return randomPicsCache +} + +func (c *RandomPicsCache) Run() error { + tk := time.NewTicker(5 * time.Second) + go func() { + c.flush() + }() + go func(_tk *time.Ticker) { + for { + select { + case <-_tk.C: + c.flush() + default: + continue + } + } + }(tk) + return nil +} + +func (c *RandomPicsCache) flush() { + now := time.Now() + rand.Seed(now.UnixNano()) + tx := c.db.WithContext(context.Background()) + picRepository := repository.NewBilibiliPicture(tx) + list, err := picRepository.Random(rand.Float64()) + if err != nil || len(list) == 0 { + c.success = false + c.log.Error("randomPicsCache flush error ", zap.Error(err), zap.Time("time", now), zap.Int("list", len(list))) + return + } + pictures := make(idl.BilibiliDynamicPictures, 0) + for i := range list { + for j := range list[i].Pictures { + pictures = append(pictures, list[i].Pictures[j]) + } + } + c.syncMap.Store(overall, pictures) + c.success = true +} + type BilbilPicture struct { db *gorm.DB } @@ -63,3 +134,39 @@ func (service *BilbilPicture) Recommend(ctx context.Context, req idl.BilibiliPic } return &resp, nil } + +func (service *BilbilPicture) Random(ctx context.Context) (*idl.BilibiliPictureRandomResp, error) { + if randomPicsCache.success { + value, ok := randomPicsCache.syncMap.Load(overall) + if ok { + pictures, ok := value.(idl.BilibiliDynamicPictures) + if ok && len(pictures) != 0 { + idx := rand.Intn(len(pictures)) + resp := &idl.BilibiliPictureRandomResp{} + resp.BilibiliDynamicPicture = pictures[idx] + return resp, nil + } + } + } + now := time.Now() + rand.Seed(now.UnixNano()) + tx := service.db.WithContext(ctx) + picRepository := repository.NewBilibiliPicture(tx) + list, err := picRepository.Random(rand.Float64()) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, errRandomPictureNotFound + } + pictures := make(idl.BilibiliDynamicPictures, 0) + for i := range list { + for j := range list[i].Pictures { + pictures = append(pictures, list[i].Pictures[j]) + } + } + idx := rand.Intn(len(pictures)) + resp := &idl.BilibiliPictureRandomResp{} + resp.BilibiliDynamicPicture = pictures[idx] + return resp, nil +} diff --git a/internal/app/provide.go b/internal/app/provide.go index 0c4be2b..770818d 100644 --- a/internal/app/provide.go +++ b/internal/app/provide.go @@ -37,5 +37,6 @@ func ServiceProvider() fx.Option { service.NewAuth, service.NewUser, service.NewTool, + service.NewRandomPicsCache, ) } diff --git a/internal/repository/bilibili_picture.go b/internal/repository/bilibili_picture.go index e526771..f572c93 100644 --- a/internal/repository/bilibili_picture.go +++ b/internal/repository/bilibili_picture.go @@ -1,6 +1,7 @@ package repository import ( + "math" "time" "git.vtb.link/eoefans/internal/app/api/idl" @@ -90,3 +91,22 @@ func (impl *BilibiliPictureMysqlImpl) Recommend(from, to time.Time, page, size, } return list, nil } + +func (impl *BilibiliPictureMysqlImpl) Random(rand float64) (list []*idl.BilibiliDynamic, err error) { + conn := impl.tx.Table(idl.BilibiliDynamic{}.TableName()) + type Pair struct { + MaxId float64 + MinId float64 + } + var pair Pair + err = conn.Select("max(id) as max_id,min(id) as min_id").Scan(&pair).Error + if err != nil { + return nil, err + } + offset := math.Floor((pair.MaxId - pair.MinId) * rand) + err = conn.Where("id >= ?", uint64(pair.MinId+offset)).Select("pictures").Limit(5).Find(&list).Error + if err != nil { + return nil, err + } + return list, nil +}