From d6a34bd30e648f81387fb30845eb87d6086803d0 Mon Sep 17 00:00:00 2001 From: Lansong <62054128+Lansongxx@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:57:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90recommendservice=20(#?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biz/adaptor/server.go | 48 ++++- biz/application/service/recommend.go | 235 ++++++++++++++++++++ biz/infrastructure/config/config.go | 4 + biz/infrastructure/consts/field.go | 7 +- biz/infrastructure/convertor/convertor.go | 21 ++ biz/infrastructure/gorse/client.go | 251 ++++++++++++++++++++++ biz/infrastructure/gorse/model.go | 92 ++++++++ go.mod | 8 +- go.sum | 19 +- provider/provider.go | 3 + provider/wire_gen.go | 23 +- 11 files changed, 688 insertions(+), 23 deletions(-) create mode 100644 biz/application/service/recommend.go create mode 100644 biz/infrastructure/gorse/client.go create mode 100644 biz/infrastructure/gorse/model.go diff --git a/biz/adaptor/server.go b/biz/adaptor/server.go index 9fffd13..da638ed 100644 --- a/biz/adaptor/server.go +++ b/biz/adaptor/server.go @@ -9,13 +9,47 @@ import ( type ContentServerImpl struct { *config.Config - FileService service.IFileService - PostService service.IPostService - ZoneService service.IZoneService - UserService service.IUserService - ProductService service.IProductService - CouponService service.ICouponService - OrderService service.IOrderService + FileService service.IFileService + PostService service.IPostService + ZoneService service.IZoneService + UserService service.IUserService + ProductService service.IProductService + CouponService service.ICouponService + OrderService service.IOrderService + RecommendService service.IRecommendService +} + +func (s *ContentServerImpl) CreateFeedBacks(ctx context.Context, req *content.CreateFeedBacksReq) (res *content.CreateFeedBacksResp, err error) { + return s.RecommendService.CreateFeedBacks(ctx, req) +} + +func (s *ContentServerImpl) GetLatestRecommend(ctx context.Context, req *content.GetLatestRecommendReq) (res *content.GetLatestRecommendResp, err error) { + return s.RecommendService.GetLatestRecommend(ctx, req) +} + +func (s *ContentServerImpl) CreateItems(ctx context.Context, req *content.CreateItemsReq) (res *content.CreateItemsResp, err error) { + return s.RecommendService.CreateItems(ctx, req) +} + +func (s *ContentServerImpl) UpdateItem(ctx context.Context, req *content.UpdateItemReq) (res *content.UpdateItemResp, err error) { + return s.RecommendService.UpdateItem(ctx, req) +} + +func (s *ContentServerImpl) DeleteItem(ctx context.Context, req *content.DeleteItemReq) (res *content.DeleteItemResp, err error) { + return s.RecommendService.DeleteItem(ctx, req) +} + +func (s *ContentServerImpl) GetRecommendByUser(ctx context.Context, req *content.GetRecommendByUserReq) (res *content.GetRecommendByUserResp, err error) { + return s.RecommendService.GetRecommendByUser(ctx, req) +} + +func (s *ContentServerImpl) GetRecommendByItem(ctx context.Context, req *content.GetRecommendByItemReq) (res *content.GetRecommendByItemResp, err error) { + return s.RecommendService.GetRecommendByItem(ctx, req) +} + +func (s *ContentServerImpl) GetPopularRecommend(ctx context.Context, req *content.GetPopularRecommendReq) (res *content.GetPopularRecommendResp, err error) { + return s.RecommendService.GetPopularRecommend(ctx, req) + } func (s *ContentServerImpl) CompletelyRemoveFile(ctx context.Context, req *content.CompletelyRemoveFileReq) (res *content.CompletelyRemoveFileResp, err error) { diff --git a/biz/application/service/recommend.go b/biz/application/service/recommend.go new file mode 100644 index 0000000..b326d9b --- /dev/null +++ b/biz/application/service/recommend.go @@ -0,0 +1,235 @@ +package service + +import ( + "context" + "fmt" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/consts" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/convertor" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/gorse" + gencontent "github.com/CloudStriver/service-idl-gen-go/kitex_gen/cloudmind/content" + "github.com/google/wire" + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/stores/redis" + "strconv" +) + +type IRecommendService interface { + CreateItems(ctx context.Context, req *gencontent.CreateItemsReq) (resp *gencontent.CreateItemsResp, err error) + UpdateItem(ctx context.Context, req *gencontent.UpdateItemReq) (resp *gencontent.UpdateItemResp, err error) + DeleteItem(ctx context.Context, req *gencontent.DeleteItemReq) (resp *gencontent.DeleteItemResp, err error) + GetPopularRecommend(ctx context.Context, req *gencontent.GetPopularRecommendReq) (resp *gencontent.GetPopularRecommendResp, err error) + GetRecommendByItem(ctx context.Context, req *gencontent.GetRecommendByItemReq) (resp *gencontent.GetRecommendByItemResp, err error) + GetRecommendByUser(ctx context.Context, req *gencontent.GetRecommendByUserReq) (resp *gencontent.GetRecommendByUserResp, err error) + GetLatestRecommend(ctx context.Context, req *gencontent.GetLatestRecommendReq) (resp *gencontent.GetLatestRecommendResp, err error) + CreateFeedBacks(ctx context.Context, req *gencontent.CreateFeedBacksReq) (resp *gencontent.CreateFeedBacksResp, err error) +} + +type RecommendService struct { + Redis *redis.Redis + Gorse *gorse.GorseClient +} + +func (s *RecommendService) CreateFeedBacks(ctx context.Context, req *gencontent.CreateFeedBacksReq) (resp *gencontent.CreateFeedBacksResp, err error) { + feedbacks := lo.Map[*gencontent.FeedBack, gorse.Feedback](req.FeedBacks, func(feedback *gencontent.FeedBack, index int) gorse.Feedback { + return convertor.FeedBackToGorseFeedBack(feedback) + }) + if _, err = s.Gorse.InsertFeedback(ctx, feedbacks); err != nil { + return resp, err + } + return resp, nil +} + +func (s *RecommendService) GetLatestRecommend(ctx context.Context, req *gencontent.GetLatestRecommendReq) (resp *gencontent.GetLatestRecommendResp, err error) { + resp = new(gencontent.GetLatestRecommendResp) + if req.Limit == nil { + req.Limit = lo.ToPtr(int64(consts.DefaultLimit)) + } + + var ( + offset int + val string + items []gorse.Score + ) + if val, _ = s.Redis.GetCtx(ctx, fmt.Sprintf("cache:latest:recommend:%s", req.UserId)); val == "" { + offset = 0 + } else { + if offset, err = strconv.Atoi(val); err != nil { + offset = 0 + } + } + + if req.Category != nil { + if items, err = s.Gorse.GetItemLatestWithCategory(ctx, req.UserId, req.GetCategory(), int(req.GetLimit()), offset); err != nil { + return resp, err + } + } else { + if items, err = s.Gorse.GetItemLatest(ctx, req.UserId, int(req.GetLimit()), offset); err != nil { + return resp, err + } + } + if len(resp.ItemIds) < int(req.GetLimit()) { + offset = 0 + } else { + offset += int(req.GetLimit()) + } + + _ = s.Redis.SetexCtx(ctx, fmt.Sprintf("cache:latest:recommend:%s", req.UserId), strconv.Itoa(offset), 3600) + resp.ItemIds = lo.Map[gorse.Score, string](items, func(score gorse.Score, _ int) string { + return score.Id + }) + return resp, nil +} + +func (s *RecommendService) CreateItems(ctx context.Context, req *gencontent.CreateItemsReq) (resp *gencontent.CreateItemsResp, err error) { + items := lo.Map[*gencontent.Item, gorse.Item](req.Items, func(item *gencontent.Item, index int) gorse.Item { + return convertor.ItemToGorseItem(item) + }) + if _, err = s.Gorse.InsertItems(ctx, items); err != nil { + return resp, err + } + return resp, nil +} + +func (s *RecommendService) UpdateItem(ctx context.Context, req *gencontent.UpdateItemReq) (resp *gencontent.UpdateItemResp, err error) { + if _, err = s.Gorse.UpdateItem(ctx, req.ItemId, &gorse.ItemPatch{ + IsHidden: req.IsHidden, + Categories: req.Categories, + Labels: req.Labels, + Comment: req.Comment, + }); err != nil { + return resp, err + } + return resp, nil +} + +func (s *RecommendService) DeleteItem(ctx context.Context, req *gencontent.DeleteItemReq) (resp *gencontent.DeleteItemResp, err error) { + if _, err = s.Gorse.DeleteItem(ctx, req.ItemId); err != nil { + return resp, err + } + return resp, nil +} + +func (s *RecommendService) GetPopularRecommend(ctx context.Context, req *gencontent.GetPopularRecommendReq) (resp *gencontent.GetPopularRecommendResp, err error) { + resp = new(gencontent.GetPopularRecommendResp) + if req.Limit == nil { + req.Limit = lo.ToPtr(int64(consts.DefaultLimit)) + } + + var ( + offset int + items []gorse.Score + val string + ) + if val, _ = s.Redis.GetCtx(ctx, fmt.Sprintf("cache:popular:recommend:%s", req.UserId)); val == "" { + offset = 0 + } else { + if offset, err = strconv.Atoi(val); err != nil { + offset = 0 + } + } + + if req.Category != nil { + if items, err = s.Gorse.GetItemPopularWithCategory(ctx, req.UserId, req.GetCategory(), int(req.GetLimit()), offset); err != nil { + return resp, err + } + } else { + if items, err = s.Gorse.GetItemPopular(ctx, req.UserId, int(req.GetLimit()), offset); err != nil { + return resp, err + } + } + if len(items) < int(req.GetLimit()) { + offset = 0 + } else { + offset += int(req.GetLimit()) + } + + _ = s.Redis.SetexCtx(ctx, fmt.Sprintf("cache:popular:recommend:%s", req.UserId), strconv.Itoa(offset), 3600) + + resp.ItemIds = lo.Map[gorse.Score, string](items, func(score gorse.Score, _ int) string { + return score.Id + }) + return resp, nil +} + +func (s *RecommendService) GetRecommendByItem(ctx context.Context, req *gencontent.GetRecommendByItemReq) (resp *gencontent.GetRecommendByItemResp, err error) { + resp = new(gencontent.GetRecommendByItemResp) + if req.Limit == nil { + req.Limit = lo.ToPtr(int64(consts.DefaultLimit)) + } + + var ( + offset int + val string + items []gorse.Score + ) + if val, _ = s.Redis.GetCtx(ctx, fmt.Sprintf("cache:item:recommend:%s", req.ItemId)); val == "" { + offset = 0 + } else { + if offset, err = strconv.Atoi(val); err != nil { + offset = 0 + } + } + + if req.Category != nil { + if items, err = s.Gorse.GetItemNeighborsWithCategory(ctx, req.ItemId, req.GetCategory(), int(req.GetLimit()), offset); err != nil { + return resp, err + } + } else { + if items, err = s.Gorse.GetItemNeighbors(ctx, req.ItemId, int(req.GetLimit()), offset); err != nil { + return resp, err + } + } + if len(resp.ItemIds) < int(req.GetLimit()) { + offset = 0 + } else { + offset += int(req.GetLimit()) + } + + _ = s.Redis.SetexCtx(ctx, fmt.Sprintf("cache:item:recommend:%s", req.ItemId), strconv.Itoa(offset), 3600) + resp.ItemIds = lo.Map[gorse.Score, string](items, func(score gorse.Score, _ int) string { + return score.Id + }) + return resp, nil +} + +func (s *RecommendService) GetRecommendByUser(ctx context.Context, req *gencontent.GetRecommendByUserReq) (resp *gencontent.GetRecommendByUserResp, err error) { + resp = new(gencontent.GetRecommendByUserResp) + if req.Limit == nil { + req.Limit = lo.ToPtr(int64(consts.DefaultLimit)) + } + + var ( + offset int + val string + ) + if val, _ = s.Redis.GetCtx(ctx, fmt.Sprintf("cache:user:recommend:%s", req.UserId)); val == "" { + offset = 0 + } else { + if offset, err = strconv.Atoi(val); err != nil { + offset = 0 + } + } + + if req.Category != nil { + if resp.ItemIds, err = s.Gorse.GetItemRecommendWithCategory(ctx, req.UserId, req.GetCategory(), "read", "60m", int(req.GetLimit()), offset); err != nil { + return resp, err + } + } else { + if resp.ItemIds, err = s.Gorse.GetItemRecommend(ctx, req.UserId, []string{}, "read", "60m", int(req.GetLimit()), offset); err != nil { + return resp, err + } + } + if len(resp.ItemIds) < int(req.GetLimit()) { + offset = 0 + } else { + offset += int(req.GetLimit()) + } + + _ = s.Redis.SetexCtx(ctx, fmt.Sprintf("cache:user:recommend:%s", req.UserId), strconv.Itoa(offset), 3600) + return resp, nil +} + +var RecommendSet = wire.NewSet( + wire.Struct(new(RecommendService), "*"), + wire.Bind(new(IRecommendService), new(*RecommendService)), +) diff --git a/biz/infrastructure/config/config.go b/biz/infrastructure/config/config.go index b4b2a73..07c65df 100644 --- a/biz/infrastructure/config/config.go +++ b/biz/infrastructure/config/config.go @@ -24,6 +24,10 @@ type Config struct { CacheConf cache.CacheConf Elasticsearch ElasticsearchConf Redis *redis.RedisConf + GorseConf struct { + Url string + ApiKey string + } } func NewConfig() (*Config, error) { diff --git a/biz/infrastructure/consts/field.go b/biz/infrastructure/consts/field.go index 1604f88..117de04 100644 --- a/biz/infrastructure/consts/field.go +++ b/biz/infrastructure/consts/field.go @@ -42,7 +42,8 @@ const ( ) var ( - NotDel int64 = 1 - SoftDel int64 = 2 - HardDel int64 = 3 + NotDel int64 = 1 + SoftDel int64 = 2 + HardDel int64 = 3 + DefaultLimit = 10 ) diff --git a/biz/infrastructure/convertor/convertor.go b/biz/infrastructure/convertor/convertor.go index e6d2eb3..a2d9f3c 100644 --- a/biz/infrastructure/convertor/convertor.go +++ b/biz/infrastructure/convertor/convertor.go @@ -2,6 +2,7 @@ package convertor import ( "github.com/CloudStriver/cloudmind-content/biz/infrastructure/consts" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/gorse" couponmapper "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/coupon" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/file" ordermapper "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/order" @@ -620,3 +621,23 @@ func ConvertUserMultiFieldsSearchQuery(in *gencontent.SearchOptions_MultiFieldsK } return q } + +func ItemToGorseItem(in *gencontent.Item) gorse.Item { + return gorse.Item{ + ItemId: in.ItemId, + IsHidden: in.IsHidden, + Labels: in.Labels, + Categories: in.Categories, + Timestamp: in.Timestamp, + Comment: in.Comment, + } +} + +func FeedBackToGorseFeedBack(in *gencontent.FeedBack) gorse.Feedback { + return gorse.Feedback{ + FeedbackType: in.FeedbackType, + UserId: in.UserId, + ItemId: in.ItemId, + Timestamp: in.Timestamp, + } +} diff --git a/biz/infrastructure/gorse/client.go b/biz/infrastructure/gorse/client.go new file mode 100644 index 0000000..87d95a3 --- /dev/null +++ b/biz/infrastructure/gorse/client.go @@ -0,0 +1,251 @@ +package gorse + +// Copyright 2022 gorse Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ( + "context" + "encoding/json" + "fmt" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/config" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "io" + "net/http" + "strings" +) + +type GorseClient struct { + entryPoint string + apiKey string + httpClient http.Client +} + +func NewGorseClient(c *config.Config) *GorseClient { + return &GorseClient{ + entryPoint: c.GorseConf.Url, + apiKey: c.GorseConf.ApiKey, + httpClient: http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}, + } +} + +func (c *GorseClient) InsertFeedback(ctx context.Context, feedbacks []Feedback) (RowAffected, error) { + return request[RowAffected](ctx, c, "POST", c.entryPoint+"/api/feedback", feedbacks) +} + +func (c *GorseClient) PutFeedback(ctx context.Context, feedbacks []Feedback) (RowAffected, error) { + return request[RowAffected](ctx, c, "PUT", c.entryPoint+"/api/feedback", feedbacks) +} + +func (c *GorseClient) GetFeedback(ctx context.Context, cursor string, n int) (Feedbacks, error) { + return request[Feedbacks, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/feedback?cursor=%s&n=%d", cursor, n), nil) +} + +func (c *GorseClient) GetFeedbacksWithType(ctx context.Context, feedbackType, cursor string, n int) (Feedbacks, error) { + return request[Feedbacks, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/feedback/%s?cursor=%s&n=%d", feedbackType, cursor, n), nil) +} + +func (c *GorseClient) GetFeedbackWithUserItem(ctx context.Context, userId, itemId string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/feedback/%s/%s", userId, itemId), nil) +} + +func (c *GorseClient) GetFeedbackWithTypeUserItem(ctx context.Context, feedbackType, userId, itemId string) (Feedback, error) { + return request[Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/feedback/%s/%s/%s", feedbackType, userId, itemId), nil) +} + +func (c *GorseClient) DelFeedback(ctx context.Context, feedbackType, userId, itemId string) (Feedback, error) { + return request[Feedback, any](ctx, c, "DELETE", c.entryPoint+fmt.Sprintf("/api/feedback/%s/%s/%s", feedbackType, userId, itemId), nil) +} + +func (c *GorseClient) DelFeedbackWithUserItem(ctx context.Context, userId, itemId string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "DELETE", c.entryPoint+fmt.Sprintf("/api/feedback/%s/%s", userId, itemId), nil) +} + +func (c *GorseClient) GetItemFeedbacks(ctx context.Context, itemId string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/item/%s/feedback", itemId), nil) +} + +func (c *GorseClient) GetItemFeedbacksWithType(ctx context.Context, itemId, feedbackType string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/item/%s/feedback/%s", itemId, feedbackType), nil) +} + +func (c *GorseClient) GetUserFeedbacks(ctx context.Context, userId string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/user/%s/feedback", userId), nil) +} + +func (c *GorseClient) GetUserFeedbacksWithType(ctx context.Context, userId, feedbackType string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/user/%s/feedback/%s", userId, feedbackType), nil) +} + +// Deprecated: GetUserFeedbacksWithType instead +func (c *GorseClient) ListFeedbacks(ctx context.Context, feedbackType, userId string) ([]Feedback, error) { + return request[[]Feedback, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/user/%s/feedback/%s", userId, feedbackType), nil) +} + +func (c *GorseClient) GetItemLatest(ctx context.Context, userid string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/latest?user-id=%s&n=%d&offset=%d", userid, n, offset), nil) +} + +func (c *GorseClient) GetItemLatestWithCategory(ctx context.Context, userid, category string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/latest?user-id=%s&category=%s&n=%d&offset=%d", userid, category, n, offset), nil) +} + +func (c *GorseClient) GetItemPopular(ctx context.Context, userid string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/popular?user-id=%s&n=%d&offset=%d", userid, n, offset), nil) +} + +func (c *GorseClient) GetItemPopularWithCategory(ctx context.Context, userid, category string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/popular/%s?user-id=%s&n=%d&offset=%d", category, userid, n, offset), nil) +} + +func (c *GorseClient) GetItemRecommend(ctx context.Context, userId string, categories []string, writeBackType, writeBackDelay string, n, offset int) ([]string, error) { + var queryCategories string + if len(categories) > 0 { + queryCategories = "&category=" + strings.Join(categories, "&category=") + } + return request[[]string, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/recommend/%s?write-back-type=%s&write-back-delay=%s&n=%d&offset=%d%s", userId, writeBackType, writeBackDelay, n, offset, queryCategories), nil) +} + +func (c *GorseClient) GetItemRecommendWithCategory(ctx context.Context, userId, category, writeBackType, writeBackDelay string, n, offset int) ([]string, error) { + return request[[]string, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/recommend/%s/%s?write-back-type=%s&write-back-delay=%s&n=%d&offset=%d", userId, category, writeBackType, writeBackDelay, n, offset), nil) +} + +// Deprecated: GetItemRecommendWithCategory instead +func (c *GorseClient) GetRecommend(ctx context.Context, userId, category string, n int) ([]string, error) { + return request[[]string, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/recommend/%s/%s?n=%d", userId, category, n), nil) +} + +func (c *GorseClient) SessionItemRecommend(ctx context.Context, feedbacks []Feedback, n, offset int) ([]Score, error) { + return request[[]Score](ctx, c, "POST", c.entryPoint+fmt.Sprintf("/api/session/recommend?n=%d&offset=%d", n, offset), feedbacks) +} + +func (c *GorseClient) SessionItemRecommendWithCategory(ctx context.Context, feedbacks []Feedback, category string, n, offset int) ([]Score, error) { + return request[[]Score](ctx, c, "POST", c.entryPoint+fmt.Sprintf("/api/session/recommend/%s?n=%d&offset=%d", category, n, offset), feedbacks) +} + +// Deprecated: SessionItemRecommend instead +func (c *GorseClient) SessionRecommend(ctx context.Context, feedbacks []Feedback, n int) ([]Score, error) { + return request[[]Score](ctx, c, "POST", c.entryPoint+fmt.Sprintf("/api/session/recommend?n=%d", n), feedbacks) +} + +func (c *GorseClient) GetUserNeighbors(ctx context.Context, userId string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/user/%s/neighbors?n=%d&offset=%d", userId, n, offset), nil) +} + +func (c *GorseClient) GetItemNeighbors(ctx context.Context, itemId string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/item/%s/neighbors?n=%d&offset=%d", itemId, n, offset), nil) +} + +func (c *GorseClient) GetItemNeighborsWithCategory(ctx context.Context, itemId, category string, n, offset int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/item/%s/neighbors/%s?n=%d&offset=%d", itemId, category, n, offset), nil) +} + +// Deprecated: GetItemNeighbors instead +func (c *GorseClient) GetNeighbors(ctx context.Context, itemId string, n int) ([]Score, error) { + return request[[]Score, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/item/%s/neighbors?n=%d", itemId, n), nil) +} + +func (c *GorseClient) InsertUser(ctx context.Context, user User) (RowAffected, error) { + return request[RowAffected](ctx, c, "POST", c.entryPoint+"/api/user", user) +} + +func (c *GorseClient) InsertUsers(ctx context.Context, users []User) (RowAffected, error) { + return request[RowAffected, any](ctx, c, "POST", c.entryPoint+"/api/users", users) +} + +func (c *GorseClient) UpdateUser(ctx context.Context, userId string, user UserPatch) (RowAffected, error) { + return request[RowAffected](ctx, c, "PATCH", c.entryPoint+fmt.Sprintf("/api/user/%s", userId), user) +} + +func (c *GorseClient) GetUser(ctx context.Context, userId string) (User, error) { + return request[User, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/user/%s", userId), nil) +} + +func (c *GorseClient) GetUsers(ctx context.Context, cursor string, n int) (Users, error) { + return request[Users, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/users?cursor=%s&n=%d", cursor, n), nil) +} + +func (c *GorseClient) DeleteUser(ctx context.Context, userId string) (RowAffected, error) { + return request[RowAffected, any](ctx, c, "DELETE", c.entryPoint+fmt.Sprintf("/api/user/%s", userId), nil) +} + +func (c *GorseClient) InsertItem(ctx context.Context, item Item) (RowAffected, error) { + return request[RowAffected](ctx, c, "POST", c.entryPoint+"/api/item", item) +} + +func (c *GorseClient) InsertItems(ctx context.Context, items []Item) (RowAffected, error) { + return request[RowAffected](ctx, c, "POST", c.entryPoint+"/api/items", items) +} + +func (c *GorseClient) UpdateItem(ctx context.Context, itemId string, item *ItemPatch) (RowAffected, error) { + return request[RowAffected](ctx, c, "PATCH", c.entryPoint+fmt.Sprintf("/api/item/%s", itemId), item) +} + +func (c *GorseClient) GetItem(ctx context.Context, itemId string) (Item, error) { + return request[Item, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/item/%s", itemId), nil) +} + +func (c *GorseClient) GetItems(ctx context.Context, cursor string, n int) (Items, error) { + return request[Items, any](ctx, c, "GET", c.entryPoint+fmt.Sprintf("/api/items?cursor=%s&n=%d", cursor, n), nil) +} + +func (c *GorseClient) DeleteItem(ctx context.Context, itemId string) (RowAffected, error) { + return request[RowAffected, any](ctx, c, "DELETE", c.entryPoint+fmt.Sprintf("/api/item/%s", itemId), nil) +} + +func (c *GorseClient) PutItemCategory(ctx context.Context, itemId string, category string) (RowAffected, error) { + return request[RowAffected, any](ctx, c, "PUT", c.entryPoint+fmt.Sprintf("/api/item/%s/category/%s", itemId, category), nil) +} + +func (c *GorseClient) DelItemCategory(ctx context.Context, itemId string, category string) (RowAffected, error) { + return request[RowAffected, any](ctx, c, "DELETE", c.entryPoint+fmt.Sprintf("/api/item/%s/category/%s", itemId, category), nil) +} + +func (c *GorseClient) HealthLive(ctx context.Context) (Health, error) { + return request[Health, any](ctx, c, "GET", c.entryPoint+"/api/health/live", nil) +} +func (c *GorseClient) HealthReady(ctx context.Context) (Health, error) { + return request[Health, any](ctx, c, "GET", c.entryPoint+"/api/health/ready", nil) +} + +func request[Response any, Body any](ctx context.Context, c *GorseClient, method, url string, body Body) (result Response, err error) { + bodyByte, marshalErr := json.Marshal(body) + if marshalErr != nil { + return result, marshalErr + } + var req *http.Request + req, err = http.NewRequestWithContext(ctx, method, url, strings.NewReader(string(bodyByte))) + if err != nil { + return result, err + } + req.Header.Set("X-API-Key", c.apiKey) + req.Header.Set("Content-Type", "application/json") + resp, err := c.httpClient.Do(req) + if err != nil { + return result, err + } + defer resp.Body.Close() + buf := new(strings.Builder) + _, err = io.Copy(buf, resp.Body) + if err != nil { + return result, err + } + if resp.StatusCode != http.StatusOK { + return result, ErrorMessage(buf.String()) + } + err = json.Unmarshal([]byte(buf.String()), &result) + if err != nil { + return result, err + } + return result, err +} diff --git a/biz/infrastructure/gorse/model.go b/biz/infrastructure/gorse/model.go new file mode 100644 index 0000000..83a5b69 --- /dev/null +++ b/biz/infrastructure/gorse/model.go @@ -0,0 +1,92 @@ +package gorse + +// Copyright 2022 gorse Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "time" + +type Feedback struct { + FeedbackType string `json:"FeedbackType"` + UserId string `json:"UserId"` + ItemId string `json:"ItemId"` + Timestamp string `json:"Timestamp"` +} + +type Feedbacks struct { + Cursor string `json:"Cursor"` + Feedback []Feedback `json:"Feedback"` +} + +type ErrorMessage string + +func (e ErrorMessage) Error() string { + return string(e) +} + +type RowAffected struct { + RowAffected int `json:"RowAffected"` +} + +type Score struct { + Id string `json:"Id"` + Score float64 `json:"Score"` +} + +type User struct { + UserId string `json:"UserId"` + Labels []string `json:"Labels"` + Subscribe []string `json:"Subscribe"` + Comment string `json:"Comment"` +} + +type Users struct { + Cursor string `json:"Cursor"` + Users []User `json:"Users"` +} + +type UserPatch struct { + Labels []string + Subscribe []string + Comment *string +} + +type Item struct { + ItemId string `json:"ItemId"` + IsHidden bool `json:"IsHidden"` + Labels []string `json:"Labels"` + Categories []string `json:"Categories"` + Timestamp string `json:"Timestamp"` + Comment string `json:"Comment"` +} + +type Items struct { + Cursor string `json:"Cursor"` + Items []Item `json:"Items"` +} + +type ItemPatch struct { + IsHidden *bool + Categories []string + Timestamp *time.Time + Labels []string + Comment *string +} + +type Health struct { + CacheStoreConnected bool `json:"CacheStoreConnected"` + CacheStoreError string `json:"CacheStoreError"` + DataStoreConnected bool `json:"DataStoreConnected"` + DataStoreError string `json:"DataStoreError"` + Ready bool `json:"Ready"` +} diff --git a/go.mod b/go.mod index 7595ed1..8370568 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/CloudStriver/go-pkg v0.0.0-20231229114943-910edcb8788d - github.com/CloudStriver/service-idl-gen-go v0.0.0-20240207103956-4877398ec51e + github.com/CloudStriver/service-idl-gen-go v0.0.0-20240209070137-e8c51d34a88f github.com/bytedance/sonic v1.10.2 github.com/cloudwego/kitex v0.8.0 github.com/elastic/go-elasticsearch/v8 v8.11.1 @@ -14,11 +14,14 @@ require ( github.com/samber/lo v1.38.1 github.com/zeromicro/go-zero v1.6.1 go.mongodb.org/mongo-driver v1.13.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/trace v1.19.0 google.golang.org/grpc v1.60.1 ) +//replace github.com/CloudStriver/service-idl-gen-go => ../service-idl-gen-go + require ( github.com/apache/thrift v0.16.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -42,8 +45,9 @@ require ( github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/fatih/structtag v1.2.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/go.sum b/go.sum index 77b126b..b5a2279 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudStriver/go-pkg v0.0.0-20231229114943-910edcb8788d h1:c5M3637HJJN+pD3erToZAw0vutEMLmC+5I63szm5PYU= github.com/CloudStriver/go-pkg v0.0.0-20231229114943-910edcb8788d/go.mod h1:Oj6+J8ixQxqNunSpl9v64q6tAQ83wY/rRFI+3rhHVCk= -github.com/CloudStriver/service-idl-gen-go v0.0.0-20240207103956-4877398ec51e h1:nO3+GrijkLpFAdk+tt+n/R4nJzkeRYoMuXXEBk2LjV0= github.com/CloudStriver/service-idl-gen-go v0.0.0-20240207103956-4877398ec51e/go.mod h1:chtR82RvfrjUujTGWROSCNAwF9Lh/U959k34bXIDvBI= +github.com/CloudStriver/service-idl-gen-go v0.0.0-20240209070137-e8c51d34a88f h1:wPE1omK/VyPxRq9asRfWc5aeV2mggVM6wEoHL4ialMU= +github.com/CloudStriver/service-idl-gen-go v0.0.0-20240209070137-e8c51d34a88f/go.mod h1:chtR82RvfrjUujTGWROSCNAwF9Lh/U959k34bXIDvBI= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -117,6 +118,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -131,8 +134,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= @@ -336,10 +339,16 @@ github.com/zeromicro/go-zero v1.6.1 h1:E8fRkMPiYODk8+jUIrxQQIEG+MTgWfXKiH7sjc9l6 github.com/zeromicro/go-zero v1.6.1/go.mod h1:slLvzqPP/H/h9ABq9ykNOuX6pYLjA8Uy3Rb8adkXTGw= go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= go.opentelemetry.io/contrib/propagators/b3 v1.20.0 h1:Yty9Vs4F3D6/liF1o6FNt0PvN85h/BJJ6DQKJ3nrcM0= go.opentelemetry.io/contrib/propagators/ot v1.20.0 h1:duH7mgL6VGQH7e7QEAVOFkCQXWpCb4PjTtrhdrYrJRQ= go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= @@ -354,10 +363,14 @@ go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQq go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= diff --git a/provider/provider.go b/provider/provider.go index 2995b50..ecaa5c5 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -3,6 +3,7 @@ package provider import ( "github.com/CloudStriver/cloudmind-content/biz/application/service" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/config" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/gorse" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/coupon" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/file" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/order" @@ -28,11 +29,13 @@ var ApplicationSet = wire.NewSet( service.CouponSet, service.ProductSet, service.OrderSet, + service.RecommendSet, ) var InfrastructureSet = wire.NewSet( config.NewConfig, redis.NewRedis, + gorse.NewGorseClient, MapperSet, ) diff --git a/provider/wire_gen.go b/provider/wire_gen.go index 78b6cf3..c793ec8 100644 --- a/provider/wire_gen.go +++ b/provider/wire_gen.go @@ -10,6 +10,7 @@ import ( "github.com/CloudStriver/cloudmind-content/biz/adaptor" "github.com/CloudStriver/cloudmind-content/biz/application/service" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/config" + "github.com/CloudStriver/cloudmind-content/biz/infrastructure/gorse" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/coupon" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/file" "github.com/CloudStriver/cloudmind-content/biz/infrastructure/mapper/order" @@ -81,15 +82,21 @@ func NewContentServerImpl() (*adaptor.ContentServerImpl, error) { OrderEsMapper: orderIEsMapper, Redis: redisRedis, } + gorseClient := gorse.NewGorseClient(configConfig) + recommendService := &service.RecommendService{ + Redis: redisRedis, + Gorse: gorseClient, + } contentServerImpl := &adaptor.ContentServerImpl{ - Config: configConfig, - FileService: fileService, - PostService: postService, - ZoneService: zoneService, - UserService: userService, - ProductService: productService, - CouponService: couponService, - OrderService: orderService, + Config: configConfig, + FileService: fileService, + PostService: postService, + ZoneService: zoneService, + UserService: userService, + ProductService: productService, + CouponService: couponService, + OrderService: orderService, + RecommendService: recommendService, } return contentServerImpl, nil }