From c9a0926495961308b9ee25d1944ba89331edf142 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Sat, 31 Aug 2024 08:59:08 +0800 Subject: [PATCH 01/11] add timeline service --- ...ost.up.js => 20240829054501_post.up.mongo} | 0 ....up.js => 20240829054502_comment.up.mongo} | 3 -- generate/protobuf/tweeting.proto | 31 ++++++++++++++++ .../add_post_to_timeline_logic.go | 31 ++++++++++++++++ .../timelineservice/get_timeline_logic.go | 31 ++++++++++++++++ .../commentservice/comment_service_server.go | 2 +- .../server/postservice/post_service_server.go | 2 +- .../timeline_service_server.go | 35 +++++++++++++++++++ 8 files changed, 130 insertions(+), 5 deletions(-) rename generate/database/mongodb/{20240829054501_post.up.js => 20240829054501_post.up.mongo} (100%) rename generate/database/mongodb/{20240829054502_comment.up.js => 20240829054502_comment.up.mongo} (53%) create mode 100644 internal/logic/timelineservice/add_post_to_timeline_logic.go create mode 100644 internal/logic/timelineservice/get_timeline_logic.go create mode 100644 internal/server/timelineservice/timeline_service_server.go diff --git a/generate/database/mongodb/20240829054501_post.up.js b/generate/database/mongodb/20240829054501_post.up.mongo similarity index 100% rename from generate/database/mongodb/20240829054501_post.up.js rename to generate/database/mongodb/20240829054501_post.up.mongo diff --git a/generate/database/mongodb/20240829054502_comment.up.js b/generate/database/mongodb/20240829054502_comment.up.mongo similarity index 53% rename from generate/database/mongodb/20240829054502_comment.up.js rename to generate/database/mongodb/20240829054502_comment.up.mongo index 119f0ad..33382ec 100644 --- a/generate/database/mongodb/20240829054502_comment.up.js +++ b/generate/database/mongodb/20240829054502_comment.up.mongo @@ -1,5 +1,2 @@ use digimon_tweeting; db.comment.createIndex({ "post_id": 1,"createAt":1}); - - -// TODO 看是否有要刪除過多的索引,要在測試一下 \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index cbc1b2c..b850ac2 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -181,4 +181,35 @@ service CommentService rpc DeleteComment(DeleteCommentReq) returns (OKResp); // UpdateComment 更新評論 rpc UpdateComment(UpdateCommentReq) returns (OKResp); +} + +// ========== TimeLineService (個人動態時報) ========== + +message GetTimelineReq +{ + string uid = 1; // 用户ID + int32 pageIndex = 2; // 頁碼 + int32 pageSize = 3; // 每一頁大小 +} + +message GetTimelineResp +{ + repeated PostDetailItem posts = 1; // 貼文列表 + Pager page = 2; // 分頁訊息 +} + +message AddPostToTimelineReq +{ + string uid = 1; // key + repeated PostDetailItem posts = 2; +} +// TimelineService 業務邏輯在外面組合 +service TimelineService +{ + // AddPostToTimeline 加入貼文,只管一股腦全塞,這裡會自動判斷 + // 誰要砍誰不砍,處理排序,上限是 1000 筆(超過1000 請他回資料庫拿) + // 只存活躍用戶的,不活躍不浪費快取的空間 + rpc AddPostToTimeline(GetTimelineReq) returns (OKResp); + // GetTimeline 取得這個人的動態時報 + rpc GetTimeline(GetTimelineReq) returns (GetTimelineResp); } \ No newline at end of file diff --git a/internal/logic/timelineservice/add_post_to_timeline_logic.go b/internal/logic/timelineservice/add_post_to_timeline_logic.go new file mode 100644 index 0000000..e041dc2 --- /dev/null +++ b/internal/logic/timelineservice/add_post_to_timeline_logic.go @@ -0,0 +1,31 @@ +package timelineservicelogic + +import ( + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AddPostToTimelineLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewAddPostToTimelineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddPostToTimelineLogic { + return &AddPostToTimelineLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// AddPostToTimeline 加入貼文,只管一股腦全塞,這裡會自動判斷 +func (l *AddPostToTimelineLogic) AddPostToTimeline(in *tweeting.GetTimelineReq) (*tweeting.OKResp, error) { + // todo: add your logic here and delete this line + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/timelineservice/get_timeline_logic.go b/internal/logic/timelineservice/get_timeline_logic.go new file mode 100644 index 0000000..113abc2 --- /dev/null +++ b/internal/logic/timelineservice/get_timeline_logic.go @@ -0,0 +1,31 @@ +package timelineservicelogic + +import ( + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetTimelineLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetTimelineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTimelineLogic { + return &GetTimelineLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetTimeline 取得這個人的動態時報 +func (l *GetTimelineLogic) GetTimeline(in *tweeting.GetTimelineReq) (*tweeting.GetTimelineResp, error) { + // todo: add your logic here and delete this line + + return &tweeting.GetTimelineResp{}, nil +} diff --git a/internal/server/commentservice/comment_service_server.go b/internal/server/commentservice/comment_service_server.go index 9fed63f..f434bad 100644 --- a/internal/server/commentservice/comment_service_server.go +++ b/internal/server/commentservice/comment_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice" + "app-cloudep-tweeting-service/internal/logic/commentservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go index 639c1f4..07d52d3 100644 --- a/internal/server/postservice/post_service_server.go +++ b/internal/server/postservice/post_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice" + "app-cloudep-tweeting-service/internal/logic/postservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/timelineservice/timeline_service_server.go b/internal/server/timelineservice/timeline_service_server.go new file mode 100644 index 0000000..7e641f4 --- /dev/null +++ b/internal/server/timelineservice/timeline_service_server.go @@ -0,0 +1,35 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: tweeting.proto + +package server + +import ( + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/logic/timelineservice" + "app-cloudep-tweeting-service/internal/svc" +) + +type TimelineServiceServer struct { + svcCtx *svc.ServiceContext + tweeting.UnimplementedTimelineServiceServer +} + +func NewTimelineServiceServer(svcCtx *svc.ServiceContext) *TimelineServiceServer { + return &TimelineServiceServer{ + svcCtx: svcCtx, + } +} + +// AddPostToTimeline 加入貼文,只管一股腦全塞,這裡會自動判斷 +func (s *TimelineServiceServer) AddPostToTimeline(ctx context.Context, in *tweeting.GetTimelineReq) (*tweeting.OKResp, error) { + l := timelineservicelogic.NewAddPostToTimelineLogic(ctx, s.svcCtx) + return l.AddPostToTimeline(in) +} + +// GetTimeline 取得這個人的動態時報 +func (s *TimelineServiceServer) GetTimeline(ctx context.Context, in *tweeting.GetTimelineReq) (*tweeting.GetTimelineResp, error) { + l := timelineservicelogic.NewGetTimelineLogic(ctx, s.svcCtx) + return l.GetTimeline(in) +} From d82f5c33370c8f4766f1a0b51800cd235872e076 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Sun, 1 Sep 2024 21:49:28 +0800 Subject: [PATCH 02/11] add timeline service --- Makefile | 6 +- etc/tweeting.yaml | 6 +- generate/protobuf/tweeting.proto | 11 +- go.mod | 3 + internal/config/config.go | 6 + internal/domain/const.go | 4 + internal/domain/errors.go | 4 + internal/domain/redis.go | 19 + internal/domain/redis_test.go | 45 ++ internal/domain/repository/timeline.go | 57 +++ .../repository/timeline_sort_by_timestamp.go | 143 ++++++ .../timeline_sort_by_timestamp_test.go | 439 ++++++++++++++++++ .../commentservice/comment_service_server.go | 2 +- .../server/postservice/post_service_server.go | 2 +- .../timeline_service_server.go | 2 +- 15 files changed, 741 insertions(+), 8 deletions(-) create mode 100644 internal/domain/redis.go create mode 100644 internal/domain/redis_test.go create mode 100644 internal/domain/repository/timeline.go create mode 100644 internal/repository/timeline_sort_by_timestamp.go create mode 100644 internal/repository/timeline_sort_by_timestamp_test.go diff --git a/Makefile b/Makefile index 20dc350..035a97b 100644 --- a/Makefile +++ b/Makefile @@ -51,9 +51,9 @@ gen-mongo-model: # 建立 rpc 資料庫 # 只產生 Model 剩下的要自己撰寫,連欄位名稱也是 goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) -# goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) -# goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) -# goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) + goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) + goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) + goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE) @echo "Generate mongo model files successfully" .PHONY: mock-gen diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml index 279f3cb..0ae0dc7 100644 --- a/etc/tweeting.yaml +++ b/etc/tweeting.yaml @@ -11,4 +11,8 @@ Mongo: User: "" Password: "" Port: "27017" - Database: digimon_tweeting \ No newline at end of file + Database: digimon_tweeting + +TimelineSetting: + Expire: 86400 + MaxLength: 1000 \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index b850ac2..4b8912b 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -201,8 +201,17 @@ message GetTimelineResp message AddPostToTimelineReq { string uid = 1; // key - repeated PostDetailItem posts = 2; + repeated PostTimelineItem posts = 3; } + +// 貼文更新 +message PostTimelineItem +{ + string post_id = 1; // 貼文ID + int64 created_at = 7; // 發佈時間 -> 排序使用 +} + + // TimelineService 業務邏輯在外面組合 service TimelineService { diff --git a/go.mod b/go.mod index ea41a6a..4fad66b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.3 require ( code.30cm.net/digimon/library-go/errs v1.2.4 code.30cm.net/digimon/library-go/validator v1.0.0 + github.com/alicebob/miniredis/v2 v2.33.0 github.com/stretchr/testify v1.9.0 github.com/zeromicro/go-zero v1.7.0 go.mongodb.org/mongo-driver v1.16.0 @@ -14,6 +15,7 @@ require ( ) require ( + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -65,6 +67,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index 73cf966..ac5c588 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import "github.com/zeromicro/go-zero/zrpc" type Config struct { zrpc.RpcServerConf + Mongo struct { Schema string User string @@ -12,4 +13,9 @@ type Config struct { Port string Database string } + + TimelineSetting struct { + Expire int64 // Second + MaxLength int64 // 暫存筆數 + } } diff --git a/internal/domain/const.go b/internal/domain/const.go index 0e7211b..060080a 100644 --- a/internal/domain/const.go +++ b/internal/domain/const.go @@ -11,3 +11,7 @@ const ( AdTypeOnlyAd AdTypeOnlyNotAd ) + +const ( + LastOfTimelineFlag = "NoMoreData" +) diff --git a/internal/domain/errors.go b/internal/domain/errors.go index d16b1ab..8a44bb8 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -33,6 +33,10 @@ const ( CommentListErrorCode ) +const ( + TimeLineErrorCode ErrorCode = iota + 20 +) + func CommentError(ec ErrorCode, s ...string) *ers.LibError { return ers.NewError(code.CloudEPTweeting, code.DBError, ec.ToUint32(), diff --git a/internal/domain/redis.go b/internal/domain/redis.go new file mode 100644 index 0000000..b8ab2a0 --- /dev/null +++ b/internal/domain/redis.go @@ -0,0 +1,19 @@ +package domain + +import "strings" + +type RedisKey string + +func (key RedisKey) ToString() string { + return string(key) +} + +func (key RedisKey) With(s ...string) RedisKey { + parts := append([]string{string(key)}, s...) + + return RedisKey(strings.Join(parts, ":")) +} + +const ( + TimelineRedisKey RedisKey = "timeline" +) diff --git a/internal/domain/redis_test.go b/internal/domain/redis_test.go new file mode 100644 index 0000000..3df7c50 --- /dev/null +++ b/internal/domain/redis_test.go @@ -0,0 +1,45 @@ +package domain + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestRedisKeyToString 測試 ToString 方法 +func TestRedisKeyToString(t *testing.T) { + key := RedisKey("user:timeline") + expected := "user:timeline" + result := key.ToString() + + assert.Equal(t, expected, result, "ToString should return the correct string representation of RedisKey") +} + +// TestRedisKeyWith 測試 With 方法 +func TestRedisKeyWith(t *testing.T) { + key := RedisKey("user:timeline") + subKey := "12345" + expected := "user:timeline:12345" + result := key.With(subKey) + + assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with the provided subKey") +} + +// TestRedisKeyWithMultiple 測試 With 方法與多個參數 +func TestRedisKeyWithMultiple(t *testing.T) { + key := RedisKey("user:timeline") + subKeys := []string{"12345", "posts"} + expected := "user:timeline:12345:posts" + result := key.With(subKeys...) + + assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with multiple provided subKeys") +} + +// TestRedisKeyWithEmpty 測試 With 方法與空參數 +func TestRedisKeyWithEmpty(t *testing.T) { + key := RedisKey("user:timeline") + expected := "user:timeline" + result := key.With() + + assert.Equal(t, RedisKey(expected), result, "With should return the original key when no subKeys are provided") +} diff --git a/internal/domain/repository/timeline.go b/internal/domain/repository/timeline.go new file mode 100644 index 0000000..6beb3af --- /dev/null +++ b/internal/domain/repository/timeline.go @@ -0,0 +1,57 @@ +package repository + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "context" +) + +/* + ---------------------------------------- + | | | | | + | data A| data B |NO Data | .... | + | | | | | + ---------------------------------------- + + 動態時報在發現這個 Queue 有 No Data 的 Flag 時 + 就不再去 Query 資料庫,防止被狂刷。 + 只是要注意在業務上何時要 加入/刪除 這個 Flag +*/ + +// TimelineRepository 定義時間線的存儲接口,可以根據不同的排序策略實現。 +type TimelineRepository interface { + // AddPost 將貼文添加到動態時報,並根據排序策略進行排序。 + AddPost(ctx context.Context, req AddPostRequest) error + // FetchTimeline 獲取指定用戶的動態時報。 + FetchTimeline(ctx context.Context, req FetchTimelineRequest) (FetchTimelineResponse, error) + // SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。 + SetNoMoreDataFlag(ctx context.Context, uid string) error + // HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。 + HasNoMoreData(ctx context.Context, uid string) (bool, error) + // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。 + ClearNoMoreDataFlag(ctx context.Context, uid string) error +} + +// AddPostRequest 用於將貼文添加到時間線的請求結構體。 +type AddPostRequest struct { + UID string + PostItems []TimelineItem +} + +// TimelineItem 表示時間線中的一個元素,排序依據取決於 Score。 +type TimelineItem struct { + PostID string // 貼文ID + Score int64 // 排序使用的分數,根據具體實現可能代表時間、優先級等 +} + +// FetchTimelineRequest 用於獲取時間線的請求結構體。 +type FetchTimelineRequest struct { + UID string + PageSize int64 + PageIndex int64 +} + +// FetchTimelineResponse 表示獲取時間線的回應結構體。 +type FetchTimelineResponse struct { + Items []TimelineItem + Page tweeting.Pager +} diff --git a/internal/repository/timeline_sort_by_timestamp.go b/internal/repository/timeline_sort_by_timestamp.go new file mode 100644 index 0000000..fdef560 --- /dev/null +++ b/internal/repository/timeline_sort_by_timestamp.go @@ -0,0 +1,143 @@ +package repository + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/config" + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/domain/repository" + "context" + "errors" + "time" + + "github.com/zeromicro/go-zero/core/stores/redis" +) + +// TODO 第一版本先使用 Redis 來做,後續如果有效能考量,在考慮使用其他方案 + +type TimelineRepositoryParam struct { + Config config.Config + Redis redis.Redis +} + +type TimelineRepository struct { + cfg config.Config + redis redis.Redis +} + +func MustGenerateUseCase(param TimelineRepositoryParam) repository.TimelineRepository { + return &TimelineRepository{ + cfg: param.Config, + redis: param.Redis, + } +} + +// AddPost 將貼文添加到時間線,並根據 Score 排序 +func (t *TimelineRepository) AddPost(ctx context.Context, req repository.AddPostRequest) error { + key := domain.TimelineRedisKey.With(req.UID).ToString() + + // 準備要插入的元素 + zItems := make([]redis.Pair, len(req.PostItems)) + for i, item := range req.PostItems { + zItems[i] = redis.Pair{ + Score: item.Score, + Key: item.PostID, + } + } + + // 將 ZSet 元素添加到 Redis + _, err := t.redis.ZaddsCtx(ctx, key, zItems...) + if err != nil { + return err + } + + // 檢查 ZSet 長度,並在超過 maxLength 時刪除多餘的元素 + if t.cfg.TimelineSetting.MaxLength > 0 { + // 這裡從 0 到 - (maxLength+1) 代表超過限制的元素範圍 + _, err := t.redis.ZremrangebyrankCtx(ctx, key, 0, -(t.cfg.TimelineSetting.MaxLength + 1)) + if err != nil { + return err + } + } + + // 設置過期時間 + return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire)) +} + +// FetchTimeline 獲取指定用戶的動態時報 +func (t *TimelineRepository) FetchTimeline(ctx context.Context, req repository.FetchTimelineRequest) (repository.FetchTimelineResponse, error) { + key := domain.TimelineRedisKey.With(req.UID).ToString() + + start := (req.PageIndex - 1) * req.PageSize + end := start + req.PageSize - 1 + + // 從 Redis 中按分數由高到低獲取時間線元素 + pair, err := t.redis.ZrevrangeWithScoresCtx(ctx, key, start, end) + if err != nil { + return repository.FetchTimelineResponse{}, err + } + + // 構建返回結果 + items := make([]repository.TimelineItem, len(pair)) + for i, z := range pair { + items[i] = repository.TimelineItem{ + PostID: z.Key, + Score: z.Score, + } + } + + // 計算總數量 + total, err := t.redis.ZcardCtx(ctx, key) + if err != nil { + return repository.FetchTimelineResponse{}, err + } + + return repository.FetchTimelineResponse{ + Items: items, + Page: tweeting.Pager{ + Total: int64(total), + Index: req.PageIndex, + Size: req.PageSize, + }, + }, nil +} + +// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫 +func (t *TimelineRepository) SetNoMoreDataFlag(ctx context.Context, uid string) error { + key := domain.TimelineRedisKey.With(uid).ToString() + + // 添加一個標誌到時間線的 ZSet + _, err := t.redis.ZaddsCtx(ctx, key, redis.Pair{ + Score: time.Now().UTC().Unix(), + Key: domain.LastOfTimelineFlag, + }) + if err != nil { + return err + } + + // 設置過期時間 + return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire)) +} + +// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫 +func (t *TimelineRepository) HasNoMoreData(ctx context.Context, uid string) (bool, error) { + key := domain.TimelineRedisKey.With(uid).ToString() + + // 檢查 "NoMoreData" 標誌是否存在 + score, err := t.redis.ZscoreCtx(ctx, key, domain.LastOfTimelineFlag) + if errors.Is(err, redis.Nil) { + return false, nil // 標誌不存在 + } + if err != nil { + return false, err // 其他錯誤 + } + + return score != 0, nil +} + +// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌 +func (t *TimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string) error { + key := domain.TimelineRedisKey.With(uid).ToString() + // 移除 "NoMoreData" 標誌 + _, err := t.redis.ZremCtx(ctx, key, domain.LastOfTimelineFlag) + return err +} diff --git a/internal/repository/timeline_sort_by_timestamp_test.go b/internal/repository/timeline_sort_by_timestamp_test.go new file mode 100644 index 0000000..0ada538 --- /dev/null +++ b/internal/repository/timeline_sort_by_timestamp_test.go @@ -0,0 +1,439 @@ +package repository + +import ( + "app-cloudep-tweeting-service/internal/config" + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/domain/repository" + "context" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/core/stores/redis" + "testing" + "time" +) + +func NewRepo() (*miniredis.Miniredis, repository.TimelineRepository, error) { + r1, err := miniredis.Run() + if err != nil { + return nil, nil, err + } + + newRedis, err := redis.NewRedis(redis.RedisConf{ + Host: r1.Addr(), + Type: redis.ClusterType, + Pass: "", + }) + if err != nil { + r1.Close() + return nil, nil, err + } + + c := config.Config{ + TimelineSetting: struct { + Expire int64 + MaxLength int64 + }{Expire: 86400, MaxLength: 1000}, + } + + timelineRepo := MustGenerateUseCase(TimelineRepositoryParam{ + Config: c, + Redis: *newRedis, + }) + + return r1, timelineRepo, nil +} + +func TestAddPost(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, repo repository.TimelineRepository) error + expectErr bool + validate func(t *testing.T, r1 *miniredis.Miniredis) + }{ + { + name: "success", + action: func(t *testing.T, repo repository.TimelineRepository) error { + ctx := context.Background() + uid := "OOOOOOKJ" + + return repo.AddPost(ctx, repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post1", Score: 100}, + }, + }) + }, + expectErr: false, + validate: func(t *testing.T, r1 *miniredis.Miniredis) { + uid := "OOOOOOKJ" + key := domain.TimelineRedisKey.With(uid).ToString() + score, err := r1.ZScore(key, "post1") + assert.NoError(t, err) + assert.Equal(t, float64(100), score) + }, + }, + { + name: "timeout", + action: func(t *testing.T, repo repository.TimelineRepository) error { + ctx := context.Background() + timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond) + defer cancel() + time.Sleep(2 * time.Millisecond) + + uid := "OOOOOLK" + return repo.AddPost(timeoutCtx, repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post2", Score: 200}, + }, + }) + }, + expectErr: true, + }, + { + name: "Redis error on Zadd", + action: func(t *testing.T, repo repository.TimelineRepository) error { + r1, repo, err := NewRepo() + assert.NoError(t, err) + r1.Close() // 模拟 Redis 错误 + + ctx := context.Background() + uid := "OOOOOWE" + return repo.AddPost(ctx, repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post3", Score: 300}, + }, + }) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + err = tt.action(t, repo) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, r1) + } + } + }) + } +} + +func TestFetchTimeline(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) + expectErr bool + validate func(t *testing.T, r1 *miniredis.Miniredis, resp *repository.FetchTimelineResponse) + }{ + { + name: "FetchTimeline - success", + action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { + ctx := context.Background() + uid := "user123" + + _ = repo.AddPost(ctx, repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post1", Score: 200}, + {PostID: "post2", Score: 100}, + }, + }) + + return repo.FetchTimeline(ctx, repository.FetchTimelineRequest{ + UID: uid, + PageSize: 10, + PageIndex: 1, + }) + }, + expectErr: false, + validate: func(t *testing.T, r1 *miniredis.Miniredis, resp *repository.FetchTimelineResponse) { + assert.Equal(t, 2, len(resp.Items)) + assert.Equal(t, "post1", resp.Items[0].PostID) + assert.Equal(t, "post2", resp.Items[1].PostID) + }, + }, + { + name: "FetchTimeline - timeout", + action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { + ctx := context.Background() + timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond) + defer cancel() + time.Sleep(2 * time.Millisecond) + + uid := "user123" + return repo.FetchTimeline(timeoutCtx, repository.FetchTimelineRequest{ + UID: uid, + PageSize: 10, + PageIndex: 1, + }) + }, + expectErr: true, + }, + { + name: "FetchTimeline - Redis error on ZrangebyscoreWithScoresCtx", + action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + uid := "user123" + + _ = repo.AddPost(context.Background(), repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post3", Score: 300}, + }, + }) + + r1.Close() + + return repo.FetchTimeline(context.Background(), repository.FetchTimelineRequest{ + UID: uid, + PageSize: 10, + PageIndex: 1, + }) + }, + expectErr: true, + }, + { + name: "FetchTimeline - Redis error on ZcardCtx", + action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + uid := "user123" + + _ = repo.AddPost(context.Background(), repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post4", Score: 400}, + }, + }) + + r1.Close() + + return repo.FetchTimeline(context.Background(), repository.FetchTimelineRequest{ + UID: uid, + PageSize: 10, + PageIndex: 1, + }) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + resp, err := tt.action(t, repo) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, r1, &resp) + } + } + }) + } +} + +func TestSetNoMoreDataFlag(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, repo repository.TimelineRepository) error + expectErr bool + validate func(t *testing.T, r1 *miniredis.Miniredis) + }{ + { + name: "SetNoMoreDataFlag - success", + action: func(t *testing.T, repo repository.TimelineRepository) error { + ctx := context.Background() + uid := "user123" + return repo.SetNoMoreDataFlag(ctx, uid) + }, + expectErr: false, + validate: func(t *testing.T, r1 *miniredis.Miniredis) { + uid := "user123" + key := domain.TimelineRedisKey.With(uid).ToString() + score, _ := r1.ZScore(key, domain.LastOfTimelineFlag) + assert.NotZero(t, score) + + // 驗證是否設定過期時間 + ttl := r1.TTL(key) + assert.Equal(t, time.Duration(86400)*time.Second, ttl) + }, + }, + { + name: "SetNoMoreDataFlag - Redis error on ZaddsCtx", + action: func(t *testing.T, repo repository.TimelineRepository) error { + r1, repo, err := NewRepo() + assert.NoError(t, err) + r1.Close() // 手動關閉,復現錯誤 + + ctx := context.Background() + uid := "user123" + return repo.SetNoMoreDataFlag(ctx, uid) + }, + expectErr: true, + }, + { + name: "SetNoMoreDataFlag - Redis error on ExpireCtx", + action: func(t *testing.T, repo repository.TimelineRepository) error { + r1, repo, err := NewRepo() + assert.NoError(t, err) + + ctx := context.Background() + uid := "user123" + + r1.Close() + + return repo.SetNoMoreDataFlag(ctx, uid) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + err = tt.action(t, repo) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, r1) + } + } + }) + } +} + +func TestHasNoMoreData(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, repo repository.TimelineRepository) (bool, error) + expectErr bool + expected bool + setup func(r1 *miniredis.Miniredis) + }{ + { + name: "HasNoMoreData - 標誌存在", + action: func(t *testing.T, repo repository.TimelineRepository) (bool, error) { + ctx := context.Background() + uid := "user123" + return repo.HasNoMoreData(ctx, uid) + }, + expectErr: false, + expected: true, + setup: func(r1 *miniredis.Miniredis) { + uid := "user123" + key := domain.TimelineRedisKey.With(uid).ToString() + _, _ = r1.ZAdd(key, float64(time.Now().UTC().Unix()), domain.LastOfTimelineFlag) + }, + }, + { + name: "HasNoMoreData - 標誌不存在", + action: func(t *testing.T, repo repository.TimelineRepository) (bool, error) { + ctx := context.Background() + uid := "user123" + return repo.HasNoMoreData(ctx, uid) + }, + expectErr: false, + expected: false, + setup: func(r1 *miniredis.Miniredis) {}, // 不設置標誌 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + tt.setup(r1) + + result, err := tt.action(t, repo) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestClearNoMoreDataFlag(t *testing.T) { + tests := []struct { + name string + action func(t *testing.T, repo repository.TimelineRepository) error + expectErr bool + setup func(r1 *miniredis.Miniredis) + validate func(t *testing.T, r1 *miniredis.Miniredis) + }{ + { + name: "ClearNoMoreDataFlag - 成功清除標誌", + action: func(t *testing.T, repo repository.TimelineRepository) error { + ctx := context.Background() + uid := "user123" + return repo.ClearNoMoreDataFlag(ctx, uid) + }, + expectErr: false, + setup: func(r1 *miniredis.Miniredis) { + uid := "user123" + key := domain.TimelineRedisKey.With(uid).ToString() + _, err := r1.ZAdd(key, 100, domain.LastOfTimelineFlag) // 設置標誌 + assert.NoError(t, err) + }, + validate: func(t *testing.T, r1 *miniredis.Miniredis) { + uid := "user123" + key := domain.TimelineRedisKey.With(uid).ToString() + _, err := r1.ZScore(key, domain.LastOfTimelineFlag) + assert.Error(t, err) // 標誌應該已被移除 + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r1, repo, err := NewRepo() + assert.NoError(t, err) + defer r1.Close() + + tt.setup(r1) + + err = tt.action(t, repo) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + tt.validate(t, r1) + } + }) + } +} diff --git a/internal/server/commentservice/comment_service_server.go b/internal/server/commentservice/comment_service_server.go index f434bad..9fed63f 100644 --- a/internal/server/commentservice/comment_service_server.go +++ b/internal/server/commentservice/comment_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/commentservice" + commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go index 07d52d3..639c1f4 100644 --- a/internal/server/postservice/post_service_server.go +++ b/internal/server/postservice/post_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/postservice" + postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/timelineservice/timeline_service_server.go b/internal/server/timelineservice/timeline_service_server.go index 7e641f4..186fef5 100644 --- a/internal/server/timelineservice/timeline_service_server.go +++ b/internal/server/timelineservice/timeline_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/timelineservice" + timelineservicelogic "app-cloudep-tweeting-service/internal/logic/timelineservice" "app-cloudep-tweeting-service/internal/svc" ) From 1a770b5b41976fa9ba62327799c25afe28032a93 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Sun, 1 Sep 2024 21:50:17 +0800 Subject: [PATCH 03/11] fix format --- internal/repository/timeline_sort_by_timestamp_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/repository/timeline_sort_by_timestamp_test.go b/internal/repository/timeline_sort_by_timestamp_test.go index 0ada538..577c2d1 100644 --- a/internal/repository/timeline_sort_by_timestamp_test.go +++ b/internal/repository/timeline_sort_by_timestamp_test.go @@ -5,11 +5,12 @@ import ( "app-cloudep-tweeting-service/internal/domain" "app-cloudep-tweeting-service/internal/domain/repository" "context" + "testing" + "time" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/zeromicro/go-zero/core/stores/redis" - "testing" - "time" ) func NewRepo() (*miniredis.Miniredis, repository.TimelineRepository, error) { From f1353278059bf2001ab191834d665838f926f37a Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Sun, 1 Sep 2024 22:47:24 +0800 Subject: [PATCH 04/11] fix format --- etc/tweeting.yaml | 6 +- generate/protobuf/tweeting.proto | 43 +++++++--- internal/config/config.go | 8 +- internal/domain/errors.go | 6 +- .../logic/timelineservice/add_post_logic.go | 76 +++++++++++++++++ .../add_post_to_timeline_logic.go | 31 ------- .../clear_no_more_data_flag_logic.go | 59 +++++++++++++ .../timelineservice/fetch_timeline_logic.go | 83 +++++++++++++++++++ .../timelineservice/get_timeline_logic.go | 31 ------- .../timelineservice/has_no_more_data_logic.go | 61 ++++++++++++++ .../set_no_more_data_flag_logic.go | 59 +++++++++++++ .../timeline_sort_by_timestamp_test.go | 34 ++++++++ .../timeline_service_server.go | 34 ++++++-- internal/svc/service_context.go | 15 ++++ tweeting.go | 2 + 15 files changed, 464 insertions(+), 84 deletions(-) create mode 100644 internal/logic/timelineservice/add_post_logic.go delete mode 100644 internal/logic/timelineservice/add_post_to_timeline_logic.go create mode 100644 internal/logic/timelineservice/clear_no_more_data_flag_logic.go create mode 100644 internal/logic/timelineservice/fetch_timeline_logic.go delete mode 100644 internal/logic/timelineservice/get_timeline_logic.go create mode 100644 internal/logic/timelineservice/has_no_more_data_logic.go create mode 100644 internal/logic/timelineservice/set_no_more_data_flag_logic.go diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml index 0ae0dc7..90a1858 100644 --- a/etc/tweeting.yaml +++ b/etc/tweeting.yaml @@ -15,4 +15,8 @@ Mongo: TimelineSetting: Expire: 86400 - MaxLength: 1000 \ No newline at end of file + MaxLength: 1000 + +RedisCluster: + Host: 127.0.0.1:7001 + Type: cluster diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index 4b8912b..7fa65b0 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -188,14 +188,20 @@ service CommentService message GetTimelineReq { string uid = 1; // 用户ID - int32 pageIndex = 2; // 頁碼 - int32 pageSize = 3; // 每一頁大小 + int64 pageIndex = 2; // 頁碼 + int64 pageSize = 3; // 每一頁大小 } -message GetTimelineResp +message FetchTimelineResponse { - repeated PostDetailItem posts = 1; // 貼文列表 - Pager page = 2; // 分頁訊息 + repeated FetchTimelineItem posts = 1; // 貼文列表 + Pager page = 2; // 分頁訊息 +} + +message FetchTimelineItem +{ + string post_id = 1; + int64 score = 2; } message AddPostToTimelineReq @@ -207,18 +213,33 @@ message AddPostToTimelineReq // 貼文更新 message PostTimelineItem { - string post_id = 1; // 貼文ID - int64 created_at = 7; // 發佈時間 -> 排序使用 + string post_id = 1; // 貼文ID + int64 created_at = 7; // 發佈時間 -> 排序使用 } +message DoNoMoreDataReq +{ + string uid = 1; +} + +message HasNoMoreDataResp +{ + bool status = 1; +} // TimelineService 業務邏輯在外面組合 service TimelineService { - // AddPostToTimeline 加入貼文,只管一股腦全塞,這裡會自動判斷 + // AddPost 加入貼文,只管一股腦全塞,這裡會自動判斷 // 誰要砍誰不砍,處理排序,上限是 1000 筆(超過1000 請他回資料庫拿) // 只存活躍用戶的,不活躍不浪費快取的空間 - rpc AddPostToTimeline(GetTimelineReq) returns (OKResp); - // GetTimeline 取得這個人的動態時報 - rpc GetTimeline(GetTimelineReq) returns (GetTimelineResp); + rpc AddPost(AddPostToTimelineReq) returns (OKResp); + // FetchTimeline 取得這個人的動態時報 + rpc FetchTimeline(GetTimelineReq) returns (FetchTimelineResponse); + // SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。 + rpc SetNoMoreDataFlag(DoNoMoreDataReq) returns (OKResp); + // HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。 + rpc HasNoMoreData(DoNoMoreDataReq) returns (HasNoMoreDataResp); + // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。 + rpc ClearNoMoreDataFlag(DoNoMoreDataReq) returns (OKResp); } \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index ac5c588..4fd146d 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,9 @@ package config -import "github.com/zeromicro/go-zero/zrpc" +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) type Config struct { zrpc.RpcServerConf @@ -18,4 +21,7 @@ type Config struct { Expire int64 // Second MaxLength int64 // 暫存筆數 } + + // Redis Cluster + RedisCluster redis.RedisConf } diff --git a/internal/domain/errors.go b/internal/domain/errors.go index 8a44bb8..405a226 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -34,7 +34,11 @@ const ( ) const ( - TimeLineErrorCode ErrorCode = iota + 20 + AddTimeLineErrorCode ErrorCode = iota + 20 + FetchTimeLineErrorCode + ClearNoMoreDataErrorCode + HasNoMoreDataErrorCode + SetNoMoreDataErrorCode ) func CommentError(ec ErrorCode, s ...string) *ers.LibError { diff --git a/internal/logic/timelineservice/add_post_logic.go b/internal/logic/timelineservice/add_post_logic.go new file mode 100644 index 0000000..6a51a13 --- /dev/null +++ b/internal/logic/timelineservice/add_post_logic.go @@ -0,0 +1,76 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/domain/repository" + "context" + + ers "code.30cm.net/digimon/library-go/errs" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AddPostLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewAddPostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddPostLogic { + return &AddPostLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type addPostReq struct { + UID string `json:"uid" validate:"required"` +} + +// AddPost 加入貼文,只管一股腦全塞,這裡會自動判斷 +func (l *AddPostLogic) AddPost(in *tweeting.AddPostToTimelineReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&addPostReq{ + UID: in.GetUid(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + if len(in.GetPosts()) == 0 { + // 沒資料,直接 OK + return &tweeting.OKResp{}, nil + } + + post := make([]repository.TimelineItem, 0, len(in.GetPosts())) + for _, item := range in.GetPosts() { + post = append(post, repository.TimelineItem{ + PostID: item.PostId, + Score: item.CreatedAt, + }) + } + + err := l.svcCtx.TimelineRepo.AddPost(l.ctx, repository.AddPostRequest{ + UID: in.GetUid(), + PostItems: post, + }) + if err != nil { + // 錯誤代碼 05-021-20 + e := domain.CommentErrorL( + domain.AddTimeLineErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "TimelineRepo.AddPost"}, + {Key: "err", Value: err}, + }, + "failed to insert timeline repo :", in.GetUid()).Wrap(err) + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/timelineservice/add_post_to_timeline_logic.go b/internal/logic/timelineservice/add_post_to_timeline_logic.go deleted file mode 100644 index e041dc2..0000000 --- a/internal/logic/timelineservice/add_post_to_timeline_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package timelineservicelogic - -import ( - "context" - - "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type AddPostToTimelineLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewAddPostToTimelineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddPostToTimelineLogic { - return &AddPostToTimelineLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// AddPostToTimeline 加入貼文,只管一股腦全塞,這裡會自動判斷 -func (l *AddPostToTimelineLogic) AddPostToTimeline(in *tweeting.GetTimelineReq) (*tweeting.OKResp, error) { - // todo: add your logic here and delete this line - - return &tweeting.OKResp{}, nil -} diff --git a/internal/logic/timelineservice/clear_no_more_data_flag_logic.go b/internal/logic/timelineservice/clear_no_more_data_flag_logic.go new file mode 100644 index 0000000..050a7e7 --- /dev/null +++ b/internal/logic/timelineservice/clear_no_more_data_flag_logic.go @@ -0,0 +1,59 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "context" + + ers "code.30cm.net/digimon/library-go/errs" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ClearNoMoreDataFlagLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewClearNoMoreDataFlagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ClearNoMoreDataFlagLogic { + return &ClearNoMoreDataFlagLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type clearNoMoreDataFlagReq struct { + UID string `json:"uid" validate:"required"` +} + +// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。 +func (l *ClearNoMoreDataFlagLogic) ClearNoMoreDataFlag(in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&clearNoMoreDataFlagReq{ + UID: in.GetUid(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.TimelineRepo.ClearNoMoreDataFlag(l.ctx, in.GetUid()) + if err != nil { + // 錯誤代碼 05-021-22 + e := domain.CommentErrorL( + domain.ClearNoMoreDataErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "TimelineRepo.ClearNoMoreDataFlag"}, + {Key: "err", Value: err}, + }, + "failed to clear no more data flag timeline repo :", in.GetUid()).Wrap(err) + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/timelineservice/fetch_timeline_logic.go b/internal/logic/timelineservice/fetch_timeline_logic.go new file mode 100644 index 0000000..46ada2e --- /dev/null +++ b/internal/logic/timelineservice/fetch_timeline_logic.go @@ -0,0 +1,83 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/domain/repository" + "context" + + ers "code.30cm.net/digimon/library-go/errs" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type FetchTimelineLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewFetchTimelineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FetchTimelineLogic { + return &FetchTimelineLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type fetchTimelineReq struct { + UID string `json:"uid" validate:"required"` + PageSize int64 `json:"page_size" validate:"required"` + PageIndex int64 `json:"page_index" validate:"required"` +} + +// FetchTimeline 取得這個人的動態時報 +func (l *FetchTimelineLogic) FetchTimeline(in *tweeting.GetTimelineReq) (*tweeting.FetchTimelineResponse, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&fetchTimelineReq{ + UID: in.GetUid(), + PageSize: in.PageSize, + PageIndex: in.PageIndex, + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + resp, err := l.svcCtx.TimelineRepo.FetchTimeline(l.ctx, repository.FetchTimelineRequest{ + UID: in.GetUid(), + PageIndex: in.GetPageIndex(), + PageSize: in.GetPageSize(), + }) + if err != nil { + // 錯誤代碼 05-021-21 + e := domain.CommentErrorL( + domain.FetchTimeLineErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "TimelineRepo.FetchTimeline"}, + {Key: "err", Value: err}, + }, + "failed to fetch timeline repo :", in.GetUid()).Wrap(err) + return nil, e + } + + result := make([]*tweeting.FetchTimelineItem, 0, resp.Page.Size) + for _, item := range resp.Items { + result = append(result, &tweeting.FetchTimelineItem{ + PostId: item.PostID, + Score: item.Score, + }) + } + + return &tweeting.FetchTimelineResponse{ + Posts: result, + Page: &tweeting.Pager{ + Total: resp.Page.Total, + Index: resp.Page.Index, + Size: resp.Page.Size, + }, + }, nil +} diff --git a/internal/logic/timelineservice/get_timeline_logic.go b/internal/logic/timelineservice/get_timeline_logic.go deleted file mode 100644 index 113abc2..0000000 --- a/internal/logic/timelineservice/get_timeline_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package timelineservicelogic - -import ( - "context" - - "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetTimelineLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetTimelineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTimelineLogic { - return &GetTimelineLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// GetTimeline 取得這個人的動態時報 -func (l *GetTimelineLogic) GetTimeline(in *tweeting.GetTimelineReq) (*tweeting.GetTimelineResp, error) { - // todo: add your logic here and delete this line - - return &tweeting.GetTimelineResp{}, nil -} diff --git a/internal/logic/timelineservice/has_no_more_data_logic.go b/internal/logic/timelineservice/has_no_more_data_logic.go new file mode 100644 index 0000000..56d7f90 --- /dev/null +++ b/internal/logic/timelineservice/has_no_more_data_logic.go @@ -0,0 +1,61 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "context" + + ers "code.30cm.net/digimon/library-go/errs" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type HasNoMoreDataLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewHasNoMoreDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HasNoMoreDataLogic { + return &HasNoMoreDataLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type hasNoMoreDataReq struct { + UID string `json:"uid" validate:"required"` +} + +// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。 +func (l *HasNoMoreDataLogic) HasNoMoreData(in *tweeting.DoNoMoreDataReq) (*tweeting.HasNoMoreDataResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&hasNoMoreDataReq{ + UID: in.GetUid(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + res, err := l.svcCtx.TimelineRepo.HasNoMoreData(l.ctx, in.GetUid()) + if err != nil { + // 錯誤代碼 05-021-23 + e := domain.CommentErrorL( + domain.HasNoMoreDataErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "TimelineRepo.HasNoMoreData"}, + {Key: "err", Value: err}, + }, + "failed to get no more data flag:", in.GetUid()).Wrap(err) + return nil, e + } + + return &tweeting.HasNoMoreDataResp{ + Status: res, + }, nil +} diff --git a/internal/logic/timelineservice/set_no_more_data_flag_logic.go b/internal/logic/timelineservice/set_no_more_data_flag_logic.go new file mode 100644 index 0000000..321a0d0 --- /dev/null +++ b/internal/logic/timelineservice/set_no_more_data_flag_logic.go @@ -0,0 +1,59 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "context" + + ers "code.30cm.net/digimon/library-go/errs" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SetNoMoreDataFlagLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewSetNoMoreDataFlagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetNoMoreDataFlagLogic { + return &SetNoMoreDataFlagLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type sasNoMoreDataReq struct { + UID string `json:"uid" validate:"required"` +} + +// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。 +func (l *SetNoMoreDataFlagLogic) SetNoMoreDataFlag(in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&sasNoMoreDataReq{ + UID: in.GetUid(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.TimelineRepo.SetNoMoreDataFlag(l.ctx, in.GetUid()) + if err != nil { + // 錯誤代碼 05-021-24 + e := domain.CommentErrorL( + domain.SetNoMoreDataErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "TimelineRepo.SetNoMoreDataErrorCode"}, + {Key: "err", Value: err}, + }, + "failed to set no more data flag:", in.GetUid()).Wrap(err) + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/repository/timeline_sort_by_timestamp_test.go b/internal/repository/timeline_sort_by_timestamp_test.go index 577c2d1..6388107 100644 --- a/internal/repository/timeline_sort_by_timestamp_test.go +++ b/internal/repository/timeline_sort_by_timestamp_test.go @@ -109,6 +109,40 @@ func TestAddPost(t *testing.T) { }, expectErr: true, }, + { + name: "duplicate Key", + action: func(t *testing.T, repo repository.TimelineRepository) error { + ctx := context.Background() + uid := "OOOOODUP" + + // 第一次插入 + err := repo.AddPost(ctx, repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post1", Score: 100}, + }, + }) + if err != nil { + return err + } + + // 第二次插入,使用相同的 PostID 但不同的 Score + return repo.AddPost(ctx, repository.AddPostRequest{ + UID: uid, + PostItems: []repository.TimelineItem{ + {PostID: "post1", Score: 200}, + }, + }) + }, + expectErr: false, + validate: func(t *testing.T, r1 *miniredis.Miniredis) { + uid := "OOOOODUP" + key := domain.TimelineRedisKey.With(uid).ToString() + score, err := r1.ZScore(key, "post1") + assert.NoError(t, err) + assert.Equal(t, float64(200), score) // 應該是第二次插入的分數 + }, + }, } for _, tt := range tests { diff --git a/internal/server/timelineservice/timeline_service_server.go b/internal/server/timelineservice/timeline_service_server.go index 186fef5..2d2dc03 100644 --- a/internal/server/timelineservice/timeline_service_server.go +++ b/internal/server/timelineservice/timeline_service_server.go @@ -22,14 +22,32 @@ func NewTimelineServiceServer(svcCtx *svc.ServiceContext) *TimelineServiceServer } } -// AddPostToTimeline 加入貼文,只管一股腦全塞,這裡會自動判斷 -func (s *TimelineServiceServer) AddPostToTimeline(ctx context.Context, in *tweeting.GetTimelineReq) (*tweeting.OKResp, error) { - l := timelineservicelogic.NewAddPostToTimelineLogic(ctx, s.svcCtx) - return l.AddPostToTimeline(in) +// AddPost 加入貼文,只管一股腦全塞,這裡會自動判斷 +func (s *TimelineServiceServer) AddPost(ctx context.Context, in *tweeting.AddPostToTimelineReq) (*tweeting.OKResp, error) { + l := timelineservicelogic.NewAddPostLogic(ctx, s.svcCtx) + return l.AddPost(in) } -// GetTimeline 取得這個人的動態時報 -func (s *TimelineServiceServer) GetTimeline(ctx context.Context, in *tweeting.GetTimelineReq) (*tweeting.GetTimelineResp, error) { - l := timelineservicelogic.NewGetTimelineLogic(ctx, s.svcCtx) - return l.GetTimeline(in) +// FetchTimeline 取得這個人的動態時報 +func (s *TimelineServiceServer) FetchTimeline(ctx context.Context, in *tweeting.GetTimelineReq) (*tweeting.FetchTimelineResponse, error) { + l := timelineservicelogic.NewFetchTimelineLogic(ctx, s.svcCtx) + return l.FetchTimeline(in) +} + +// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。 +func (s *TimelineServiceServer) SetNoMoreDataFlag(ctx context.Context, in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { + l := timelineservicelogic.NewSetNoMoreDataFlagLogic(ctx, s.svcCtx) + return l.SetNoMoreDataFlag(in) +} + +// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。 +func (s *TimelineServiceServer) HasNoMoreData(ctx context.Context, in *tweeting.DoNoMoreDataReq) (*tweeting.HasNoMoreDataResp, error) { + l := timelineservicelogic.NewHasNoMoreDataLogic(ctx, s.svcCtx) + return l.HasNoMoreData(in) +} + +// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。 +func (s *TimelineServiceServer) ClearNoMoreDataFlag(ctx context.Context, in *tweeting.DoNoMoreDataReq) (*tweeting.OKResp, error) { + l := timelineservicelogic.NewClearNoMoreDataFlagLogic(ctx, s.svcCtx) + return l.ClearNoMoreDataFlag(in) } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 6dff22c..7af4b3f 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -2,7 +2,11 @@ package svc import ( "app-cloudep-tweeting-service/internal/config" + domainRepo "app-cloudep-tweeting-service/internal/domain/repository" model "app-cloudep-tweeting-service/internal/model/mongo" + "app-cloudep-tweeting-service/internal/repository" + + "github.com/zeromicro/go-zero/core/stores/redis" vi "code.30cm.net/digimon/library-go/validator" ) @@ -13,13 +17,24 @@ type ServiceContext struct { PostModel model.PostModel CommentModel model.CommentModel + + TimelineRepo domainRepo.TimelineRepository } func NewServiceContext(c config.Config) *ServiceContext { + newRedis, err := redis.NewRedis(c.RedisCluster, redis.Cluster()) + if err != nil { + panic(err) + } + return &ServiceContext{ Config: c, Validate: vi.MustValidator(), PostModel: MustPostModel(c), CommentModel: MustCommentModel(c), + TimelineRepo: repository.MustGenerateUseCase(repository.TimelineRepositoryParam{ + Config: c, + Redis: *newRedis, + }), } } diff --git a/tweeting.go b/tweeting.go index c708b77..cde5e9e 100644 --- a/tweeting.go +++ b/tweeting.go @@ -18,6 +18,8 @@ import ( var configFile = flag.String("f", "etc/tweeting.yaml", "the config file") +// TODO 要把每一個錯誤代碼修改的更詳細,目前都資料庫錯誤 + func main() { flag.Parse() From 5599f88787b7bbdb86a2919cba87a22420c2ebf4 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Mon, 2 Sep 2024 21:43:03 +0800 Subject: [PATCH 05/11] add user to nerwork --- etc/tweeting.yaml | 9 +++ generate/database/neo4j/relaction.cypher | 5 ++ generate/protobuf/tweeting.proto | 12 ++++ go.mod | 1 + internal/config/config.go | 13 ++++ internal/domain/repository/social_network.go | 7 +++ internal/lib/neo4j/config.go | 14 +++++ internal/lib/neo4j/neo4j.go | 54 +++++++++++++++++ internal/lib/neo4j/option.go | 60 +++++++++++++++++++ .../add_user_to_network_logic.go | 36 +++++++++++ internal/repository/social_network.go | 53 ++++++++++++++++ .../repository/timeline_sort_by_timestamp.go | 2 +- .../commentservice/comment_service_server.go | 2 +- .../server/postservice/post_service_server.go | 2 +- .../social_network_service_server.go | 28 +++++++++ .../timeline_service_server.go | 2 +- internal/svc/service_context.go | 29 ++++++--- 17 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 generate/database/neo4j/relaction.cypher create mode 100644 internal/domain/repository/social_network.go create mode 100644 internal/lib/neo4j/config.go create mode 100644 internal/lib/neo4j/neo4j.go create mode 100644 internal/lib/neo4j/option.go create mode 100644 internal/logic/socialnetworkservice/add_user_to_network_logic.go create mode 100644 internal/repository/social_network.go create mode 100644 internal/server/socialnetworkservice/social_network_service_server.go diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml index 90a1858..61ad1c3 100644 --- a/etc/tweeting.yaml +++ b/etc/tweeting.yaml @@ -20,3 +20,12 @@ TimelineSetting: RedisCluster: Host: 127.0.0.1:7001 Type: cluster + +Neo4J: + URI: neo4j://localhost:7687 + Username: neo4j + Password: yyyytttt + MaxConnectionPoolSize: 20 + MaxConnectionLifetime: 200s + ConnectionTimeout : 200s + LogLevel : debug \ No newline at end of file diff --git a/generate/database/neo4j/relaction.cypher b/generate/database/neo4j/relaction.cypher new file mode 100644 index 0000000..c1c34da --- /dev/null +++ b/generate/database/neo4j/relaction.cypher @@ -0,0 +1,5 @@ +// 企業版才能用,社群版只能用預設的 +CREATE DATABASE relation; + +// 創建 User 節點 UID 是唯一鍵 +CREATE CONSTRAINT FOR (u:User) REQUIRE u.UID IS UNIQUE \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index 7fa65b0..89f0c97 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -242,4 +242,16 @@ service TimelineService rpc HasNoMoreData(DoNoMoreDataReq) returns (HasNoMoreDataResp); // ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。 rpc ClearNoMoreDataFlag(DoNoMoreDataReq) returns (OKResp); +} + +// ========== Social Network (關係網路) ========== + +message AddUserToNetworkReq +{ + string uid = 1; +} + +service SocialNetworkService +{ + rpc AddUserToNetwork(AddUserToNetworkReq) returns (OKResp); } \ No newline at end of file diff --git a/go.mod b/go.mod index 4fad66b..f1c33fb 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( code.30cm.net/digimon/library-go/errs v1.2.4 code.30cm.net/digimon/library-go/validator v1.0.0 github.com/alicebob/miniredis/v2 v2.33.0 + github.com/neo4j/neo4j-go-driver/v5 v5.24.0 github.com/stretchr/testify v1.9.0 github.com/zeromicro/go-zero v1.7.0 go.mongodb.org/mongo-driver v1.16.0 diff --git a/internal/config/config.go b/internal/config/config.go index 4fd146d..30a88a1 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,8 @@ package config import ( + "time" + "github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/zrpc" ) @@ -24,4 +26,15 @@ type Config struct { // Redis Cluster RedisCluster redis.RedisConf + + // 圖形話資料庫 + Neo4J struct { + URI string + Username string + Password string + MaxConnectionPoolSize int + MaxConnectionLifetime time.Duration + ConnectionTimeout time.Duration + LogLevel string + } } diff --git a/internal/domain/repository/social_network.go b/internal/domain/repository/social_network.go new file mode 100644 index 0000000..d1c07a4 --- /dev/null +++ b/internal/domain/repository/social_network.go @@ -0,0 +1,7 @@ +package repository + +import "context" + +type SocialNetworkRepository interface { + CreateUserNode(ctx context.Context, uid string) error +} diff --git a/internal/lib/neo4j/config.go b/internal/lib/neo4j/config.go new file mode 100644 index 0000000..a90758b --- /dev/null +++ b/internal/lib/neo4j/config.go @@ -0,0 +1,14 @@ +package neo4j + +import "time" + +// Config holds the configuration for Neo4j connection. +type Config struct { + URI string + Username string + Password string + MaxConnectionPoolSize int + MaxConnectionLifetime time.Duration + ConnectionTimeout time.Duration + LogLevel string +} diff --git a/internal/lib/neo4j/neo4j.go b/internal/lib/neo4j/neo4j.go new file mode 100644 index 0000000..736099d --- /dev/null +++ b/internal/lib/neo4j/neo4j.go @@ -0,0 +1,54 @@ +package neo4j + +import ( + "context" + "fmt" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + n4Cfg "github.com/neo4j/neo4j-go-driver/v5/neo4j/config" +) + +// NewNeo4J initializes a Neo4jInit using the provided Config and options. +// If opts is not provided, it will initialize Neo4jInit with default configuration. +func NewNeo4J(conf *Config, opts ...Option) *Client { + driverConfig := &n4Cfg.Config{ + MaxConnectionLifetime: conf.MaxConnectionLifetime, + MaxConnectionPoolSize: conf.MaxConnectionPoolSize, + ConnectionAcquisitionTimeout: conf.ConnectionTimeout, + } + + neo4ji := &Client{ + neo4jConf: driverConfig, + } + + for _, opt := range opts { + opt(neo4ji) + } + + return neo4ji +} + +// Conn initiates connection to the database and returns a Neo4j driver instance. +func (c *Client) Conn() (neo4j.DriverWithContext, error) { + auth := neo4j.BasicAuth(c.serviceConf.Username, c.serviceConf.Password, "") + driver, err := neo4j.NewDriverWithContext(c.serviceConf.URI, auth, func(c *n4Cfg.Config) {}) + if err != nil { + return nil, fmt.Errorf("neo4j driver initialization error: %w", err) + } + defer func(driver neo4j.DriverWithContext, ctx context.Context) { + err := driver.Close(ctx) + if err != nil { + // fmt + + } + }(driver, context.Background()) + + ctx := context.Background() + // Verify the connection to Neo4j. + err = driver.VerifyConnectivity(ctx) + if err != nil { + return nil, fmt.Errorf("neo4j connectivity verification error: %w", err) + } + + return driver, nil +} diff --git a/internal/lib/neo4j/option.go b/internal/lib/neo4j/option.go new file mode 100644 index 0000000..a126618 --- /dev/null +++ b/internal/lib/neo4j/option.go @@ -0,0 +1,60 @@ +package neo4j + +import ( + "strings" + "time" + + n4Cfg "github.com/neo4j/neo4j-go-driver/v5/neo4j/config" + "github.com/neo4j/neo4j-go-driver/v5/neo4j/log" +) + +const ( + defaultMaxConnectionLifetime = 5 * time.Minute + defaultMaxConnectionPoolSize = 25 + defaultConnectionTimeout = 5 * time.Second +) + +// Option configures Neo4jInit behaviour. +type Option func(*Client) + +type Client struct { + neo4jConf *n4Cfg.Config + serviceConf Config +} + +// WithLogLevel sets the log level for the Neo4j driver. +func WithLogLevel(level string) Option { + return func(neo4ji *Client) { + var logger log.Logger + + switch strings.ToLower(level) { + case "panic", "fatal", "error": + logger = log.ToConsole(log.ERROR) + case "warn", "warning": + logger = log.ToConsole(log.WARNING) + case "info", "debug", "trace": + logger = log.ToConsole(log.INFO) + default: + logger = log.ToConsole(log.ERROR) + } + + neo4ji.neo4jConf.Log = logger + } +} + +// WithPerformance configures the Neo4j driver for performance by setting connection pool size and lifetime. +func WithPerformance() Option { + return func(neo4ji *Client) { + if neo4ji.serviceConf.MaxConnectionPoolSize > 0 { + neo4ji.neo4jConf.MaxConnectionPoolSize = neo4ji.serviceConf.MaxConnectionPoolSize + } else { + neo4ji.neo4jConf.MaxConnectionPoolSize = defaultMaxConnectionPoolSize + } + + if neo4ji.serviceConf.MaxConnectionLifetime > 0 { + neo4ji.neo4jConf.MaxConnectionLifetime = neo4ji.serviceConf.MaxConnectionLifetime + } else { + neo4ji.neo4jConf.MaxConnectionLifetime = defaultMaxConnectionLifetime + } + } +} diff --git a/internal/logic/socialnetworkservice/add_user_to_network_logic.go b/internal/logic/socialnetworkservice/add_user_to_network_logic.go new file mode 100644 index 0000000..69f51cb --- /dev/null +++ b/internal/logic/socialnetworkservice/add_user_to_network_logic.go @@ -0,0 +1,36 @@ +package socialnetworkservicelogic + +import ( + "context" + "fmt" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AddUserToNetworkLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewAddUserToNetworkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddUserToNetworkLogic { + return &AddUserToNetworkLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +func (l *AddUserToNetworkLogic) AddUserToNetwork(in *tweeting.AddUserToNetworkReq) (*tweeting.OKResp, error) { + // todo: add your logic here and delete this line + err := l.svcCtx.SocialNetworkRepository.CreateUserNode(l.ctx, in.GetUid()) + if err != nil { + return nil, err + } + fmt.Println(err) + + return &tweeting.OKResp{}, nil +} diff --git a/internal/repository/social_network.go b/internal/repository/social_network.go new file mode 100644 index 0000000..5adb29e --- /dev/null +++ b/internal/repository/social_network.go @@ -0,0 +1,53 @@ +package repository + +import ( + "app-cloudep-tweeting-service/internal/config" + "app-cloudep-tweeting-service/internal/domain/repository" + client4J "app-cloudep-tweeting-service/internal/lib/neo4j" + "context" + "fmt" + + ers "code.30cm.net/digimon/library-go/errs" + n4 "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +type SocialNetworkParam struct { + Config config.Config + Neo4jClient *client4J.Client +} + +type SocialNetworkRepository struct { + cfg config.Config + neo4jClient *client4J.Client +} + +func MustSocialNetworkRepository(param SocialNetworkParam) repository.SocialNetworkRepository { + return &SocialNetworkRepository{ + cfg: param.Config, + neo4jClient: param.Neo4jClient, + } +} + +func (s SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { + // 執行 Cypher + conn, err := s.neo4jClient.Conn() + if err != nil { + return ers.DBError("failed to connect to node4j", err.Error()) + } + + insert := map[string]any{ + "uid": uid, + } + + result, err := conn.NewSession(ctx, n4.SessionConfig{ + AccessMode: n4.AccessModeWrite, + }).Run(ctx, "CREATE (n:Person {name: $name}) RETURN n", insert) + if err != nil { + return err + } + + fmt.Println(result.Keys()) + + return nil + +} diff --git a/internal/repository/timeline_sort_by_timestamp.go b/internal/repository/timeline_sort_by_timestamp.go index fdef560..9599c71 100644 --- a/internal/repository/timeline_sort_by_timestamp.go +++ b/internal/repository/timeline_sort_by_timestamp.go @@ -24,7 +24,7 @@ type TimelineRepository struct { redis redis.Redis } -func MustGenerateUseCase(param TimelineRepositoryParam) repository.TimelineRepository { +func MustGenerateRepository(param TimelineRepositoryParam) repository.TimelineRepository { return &TimelineRepository{ cfg: param.Config, redis: param.Redis, diff --git a/internal/server/commentservice/comment_service_server.go b/internal/server/commentservice/comment_service_server.go index 9fed63f..f434bad 100644 --- a/internal/server/commentservice/comment_service_server.go +++ b/internal/server/commentservice/comment_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice" + "app-cloudep-tweeting-service/internal/logic/commentservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go index 639c1f4..07d52d3 100644 --- a/internal/server/postservice/post_service_server.go +++ b/internal/server/postservice/post_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice" + "app-cloudep-tweeting-service/internal/logic/postservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/socialnetworkservice/social_network_service_server.go b/internal/server/socialnetworkservice/social_network_service_server.go new file mode 100644 index 0000000..3131851 --- /dev/null +++ b/internal/server/socialnetworkservice/social_network_service_server.go @@ -0,0 +1,28 @@ +// Code generated by goctl. DO NOT EDIT. +// Source: tweeting.proto + +package server + +import ( + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/logic/socialnetworkservice" + "app-cloudep-tweeting-service/internal/svc" +) + +type SocialNetworkServiceServer struct { + svcCtx *svc.ServiceContext + tweeting.UnimplementedSocialNetworkServiceServer +} + +func NewSocialNetworkServiceServer(svcCtx *svc.ServiceContext) *SocialNetworkServiceServer { + return &SocialNetworkServiceServer{ + svcCtx: svcCtx, + } +} + +func (s *SocialNetworkServiceServer) AddUserToNetwork(ctx context.Context, in *tweeting.AddUserToNetworkReq) (*tweeting.OKResp, error) { + l := socialnetworkservicelogic.NewAddUserToNetworkLogic(ctx, s.svcCtx) + return l.AddUserToNetwork(in) +} diff --git a/internal/server/timelineservice/timeline_service_server.go b/internal/server/timelineservice/timeline_service_server.go index 2d2dc03..771d6c7 100644 --- a/internal/server/timelineservice/timeline_service_server.go +++ b/internal/server/timelineservice/timeline_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - timelineservicelogic "app-cloudep-tweeting-service/internal/logic/timelineservice" + "app-cloudep-tweeting-service/internal/logic/timelineservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 7af4b3f..0c63516 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -3,6 +3,7 @@ package svc import ( "app-cloudep-tweeting-service/internal/config" domainRepo "app-cloudep-tweeting-service/internal/domain/repository" + "app-cloudep-tweeting-service/internal/lib/neo4j" model "app-cloudep-tweeting-service/internal/model/mongo" "app-cloudep-tweeting-service/internal/repository" @@ -12,13 +13,12 @@ import ( ) type ServiceContext struct { - Config config.Config - Validate vi.Validate - - PostModel model.PostModel - CommentModel model.CommentModel - - TimelineRepo domainRepo.TimelineRepository + Config config.Config + Validate vi.Validate + PostModel model.PostModel + CommentModel model.CommentModel + TimelineRepo domainRepo.TimelineRepository + SocialNetworkRepository domainRepo.SocialNetworkRepository } func NewServiceContext(c config.Config) *ServiceContext { @@ -27,14 +27,27 @@ func NewServiceContext(c config.Config) *ServiceContext { panic(err) } + neoClient := neo4j.NewNeo4J(&neo4j.Config{ + URI: c.Neo4J.URI, + Username: c.Neo4J.Username, + Password: c.Neo4J.Password, + MaxConnectionPoolSize: c.Neo4J.MaxConnectionPoolSize, + MaxConnectionLifetime: c.Neo4J.MaxConnectionLifetime, + ConnectionTimeout: c.Neo4J.ConnectionTimeout, + }, neo4j.WithPerformance(), neo4j.WithLogLevel(c.Neo4J.LogLevel)) + return &ServiceContext{ Config: c, Validate: vi.MustValidator(), PostModel: MustPostModel(c), CommentModel: MustCommentModel(c), - TimelineRepo: repository.MustGenerateUseCase(repository.TimelineRepositoryParam{ + TimelineRepo: repository.MustGenerateRepository(repository.TimelineRepositoryParam{ Config: c, Redis: *newRedis, }), + SocialNetworkRepository: repository.MustSocialNetworkRepository(repository.SocialNetworkParam{ + Config: c, + Neo4jClient: neoClient, + }), } } From 350bbf086cb830756443a468e14ab7107a694c93 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 3 Sep 2024 09:17:50 +0800 Subject: [PATCH 06/11] add social_network repo --- etc/tweeting.yaml | 2 +- internal/lib/neo4j/neo4j.go | 13 +++++----- .../add_user_to_network_logic.go | 1 + internal/repository/social_network.go | 26 +++++++++---------- internal/svc/service_context.go | 1 - tweeting.go | 8 ++++-- 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml index 61ad1c3..8dfae0b 100644 --- a/etc/tweeting.yaml +++ b/etc/tweeting.yaml @@ -22,7 +22,7 @@ RedisCluster: Type: cluster Neo4J: - URI: neo4j://localhost:7687 + URI: bolt://localhost:7687 Username: neo4j Password: yyyytttt MaxConnectionPoolSize: 20 diff --git a/internal/lib/neo4j/neo4j.go b/internal/lib/neo4j/neo4j.go index 736099d..d8178e7 100644 --- a/internal/lib/neo4j/neo4j.go +++ b/internal/lib/neo4j/neo4j.go @@ -19,6 +19,12 @@ func NewNeo4J(conf *Config, opts ...Option) *Client { neo4ji := &Client{ neo4jConf: driverConfig, + serviceConf: Config{ + URI: conf.URI, + Username: conf.Username, + Password: conf.Password, + LogLevel: conf.LogLevel, + }, } for _, opt := range opts { @@ -35,13 +41,6 @@ func (c *Client) Conn() (neo4j.DriverWithContext, error) { if err != nil { return nil, fmt.Errorf("neo4j driver initialization error: %w", err) } - defer func(driver neo4j.DriverWithContext, ctx context.Context) { - err := driver.Close(ctx) - if err != nil { - // fmt - - } - }(driver, context.Background()) ctx := context.Background() // Verify the connection to Neo4j. diff --git a/internal/logic/socialnetworkservice/add_user_to_network_logic.go b/internal/logic/socialnetworkservice/add_user_to_network_logic.go index 69f51cb..1840a3c 100644 --- a/internal/logic/socialnetworkservice/add_user_to_network_logic.go +++ b/internal/logic/socialnetworkservice/add_user_to_network_logic.go @@ -28,6 +28,7 @@ func (l *AddUserToNetworkLogic) AddUserToNetwork(in *tweeting.AddUserToNetworkRe // todo: add your logic here and delete this line err := l.svcCtx.SocialNetworkRepository.CreateUserNode(l.ctx, in.GetUid()) if err != nil { + fmt.Println("gg88g88g8", err) return nil, err } fmt.Println(err) diff --git a/internal/repository/social_network.go b/internal/repository/social_network.go index 5adb29e..45ea0f8 100644 --- a/internal/repository/social_network.go +++ b/internal/repository/social_network.go @@ -5,10 +5,7 @@ import ( "app-cloudep-tweeting-service/internal/domain/repository" client4J "app-cloudep-tweeting-service/internal/lib/neo4j" "context" - "fmt" - - ers "code.30cm.net/digimon/library-go/errs" - n4 "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) type SocialNetworkParam struct { @@ -29,25 +26,28 @@ func MustSocialNetworkRepository(param SocialNetworkParam) repository.SocialNetw } func (s SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { - // 執行 Cypher - conn, err := s.neo4jClient.Conn() + session, err := s.neo4jClient.Conn() if err != nil { - return ers.DBError("failed to connect to node4j", err.Error()) + return err } + defer session.Close(ctx) - insert := map[string]any{ + params := map[string]interface{}{ "uid": uid, } - result, err := conn.NewSession(ctx, n4.SessionConfig{ - AccessMode: n4.AccessModeWrite, - }).Run(ctx, "CREATE (n:Person {name: $name}) RETURN n", insert) + _, err = session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeWrite, + }).Run(ctx, "CREATE (n:User {uid: $uid}) RETURN n", params) if err != nil { return err } - fmt.Println(result.Keys()) + // // 處理結果 + // if run.Next(ctx) { + // node := run.Record().AsMap() + // fmt.Printf("Created Node: %v\n", node) + // } return nil - } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 0c63516..e70b793 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -6,7 +6,6 @@ import ( "app-cloudep-tweeting-service/internal/lib/neo4j" model "app-cloudep-tweeting-service/internal/model/mongo" "app-cloudep-tweeting-service/internal/repository" - "github.com/zeromicro/go-zero/core/stores/redis" vi "code.30cm.net/digimon/library-go/validator" diff --git a/tweeting.go b/tweeting.go index cde5e9e..173ffd1 100644 --- a/tweeting.go +++ b/tweeting.go @@ -6,7 +6,10 @@ import ( "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/config" + commentserviceServer "app-cloudep-tweeting-service/internal/server/commentservice" postserviceServer "app-cloudep-tweeting-service/internal/server/postservice" + socialnetworkserviceServer "app-cloudep-tweeting-service/internal/server/socialnetworkservice" + timelineserviceServer "app-cloudep-tweeting-service/internal/server/timelineservice" "app-cloudep-tweeting-service/internal/svc" "github.com/zeromicro/go-zero/core/conf" @@ -18,8 +21,6 @@ import ( var configFile = flag.String("f", "etc/tweeting.yaml", "the config file") -// TODO 要把每一個錯誤代碼修改的更詳細,目前都資料庫錯誤 - func main() { flag.Parse() @@ -29,6 +30,9 @@ func main() { s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { tweeting.RegisterPostServiceServer(grpcServer, postserviceServer.NewPostServiceServer(ctx)) + tweeting.RegisterCommentServiceServer(grpcServer, commentserviceServer.NewCommentServiceServer(ctx)) + tweeting.RegisterTimelineServiceServer(grpcServer, timelineserviceServer.NewTimelineServiceServer(ctx)) + tweeting.RegisterSocialNetworkServiceServer(grpcServer, socialnetworkserviceServer.NewSocialNetworkServiceServer(ctx)) if c.Mode == service.DevMode || c.Mode == service.TestMode { reflection.Register(grpcServer) From e352f006b0ce6b5446834e4265d9b41f0ac2c8d0 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 3 Sep 2024 17:11:12 +0800 Subject: [PATCH 07/11] fix social network server --- generate/database/neo4j/relaction.cypher | 2 +- generate/protobuf/tweeting.proto | 43 ++- internal/domain/errors.go | 9 + internal/domain/repository/social_network.go | 19 + .../add_user_to_network_logic.go | 37 -- .../get_followee_count_logic.go | 58 +++ .../get_followee_logic.go | 69 ++++ .../get_follower_count_logic.go | 62 +++ .../get_follower_logic.go | 75 ++++ .../mark_follow_relation_logic.go | 62 +++ .../remove_follow_relation_logic.go | 57 +++ internal/repository/social_network.go | 353 +++++++++++++++++- .../social_network_service_server.go | 37 +- 13 files changed, 834 insertions(+), 49 deletions(-) delete mode 100644 internal/logic/socialnetworkservice/add_user_to_network_logic.go create mode 100644 internal/logic/socialnetworkservice/get_followee_count_logic.go create mode 100644 internal/logic/socialnetworkservice/get_followee_logic.go create mode 100644 internal/logic/socialnetworkservice/get_follower_count_logic.go create mode 100644 internal/logic/socialnetworkservice/get_follower_logic.go create mode 100644 internal/logic/socialnetworkservice/mark_follow_relation_logic.go create mode 100644 internal/logic/socialnetworkservice/remove_follow_relation_logic.go diff --git a/generate/database/neo4j/relaction.cypher b/generate/database/neo4j/relaction.cypher index c1c34da..06c9f90 100644 --- a/generate/database/neo4j/relaction.cypher +++ b/generate/database/neo4j/relaction.cypher @@ -2,4 +2,4 @@ CREATE DATABASE relation; // 創建 User 節點 UID 是唯一鍵 -CREATE CONSTRAINT FOR (u:User) REQUIRE u.UID IS UNIQUE \ No newline at end of file +CREATE CONSTRAINT FOR (u:User) REQUIRE u.uid IS UNIQUE \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index 89f0c97..ff19d56 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -251,7 +251,48 @@ message AddUserToNetworkReq string uid = 1; } +message DoFollowerRelationReq +{ + string follower_uid = 1; + string followee_uid = 2; +} + +message FollowReq +{ + string uid = 1; + int64 page_size = 2; + int64 page_index = 3; +} + +message FollowResp +{ + repeated string uid = 1; + Pager page = 2; +} + +message FollowCountReq +{ + string uid = 1; +} + +message FollowCountResp +{ + string uid = 1; + int64 total = 2; +} + service SocialNetworkService { - rpc AddUserToNetwork(AddUserToNetworkReq) returns (OKResp); + // MarkFollowRelation 關注 + rpc MarkFollowRelation(DoFollowerRelationReq) returns (OKResp); + // RemoveFollowRelation 取消關注 + rpc RemoveFollowRelation(DoFollowerRelationReq) returns (OKResp); + // GetFollower 取得跟隨者名單 + rpc GetFollower(FollowReq) returns (FollowResp); + // GetFollowee 取得我跟隨的名單 + rpc GetFollowee(FollowReq) returns (FollowResp); + // GetFollowerCount 取得跟隨者數量 + rpc GetFollowerCount(FollowCountReq) returns (FollowCountResp); + // GetFolloweeCount 取得我跟隨的數量 + rpc GetFolloweeCount(FollowCountReq) returns (FollowCountResp); } \ No newline at end of file diff --git a/internal/domain/errors.go b/internal/domain/errors.go index 405a226..618d6cc 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -41,6 +41,15 @@ const ( SetNoMoreDataErrorCode ) +const ( + MarkRelationErrorCode ErrorCode = iota + 30 + GetFollowerErrorCode + GetFollowerCountErrorCode + GetFolloweeErrorCode + GetFolloweeCountErrorCode + RemoveRelationErrorCode +) + func CommentError(ec ErrorCode, s ...string) *ers.LibError { return ers.NewError(code.CloudEPTweeting, code.DBError, ec.ToUint32(), diff --git a/internal/domain/repository/social_network.go b/internal/domain/repository/social_network.go index d1c07a4..3775e25 100644 --- a/internal/domain/repository/social_network.go +++ b/internal/domain/repository/social_network.go @@ -4,4 +4,23 @@ import "context" type SocialNetworkRepository interface { CreateUserNode(ctx context.Context, uid string) error + MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error + RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error + GetFollower(ctx context.Context, req FollowReq) (FollowResp, error) + GetFollowee(ctx context.Context, req FollowReq) (FollowResp, error) + GetFollowerCount(ctx context.Context, uid string) (int64, error) + GetFolloweeCount(ctx context.Context, uid string) (int64, error) + GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) + GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) +} + +type FollowReq struct { + UID string + PageSize int64 + PageIndex int64 +} + +type FollowResp struct { + UIDs []string + Total int64 } diff --git a/internal/logic/socialnetworkservice/add_user_to_network_logic.go b/internal/logic/socialnetworkservice/add_user_to_network_logic.go deleted file mode 100644 index 1840a3c..0000000 --- a/internal/logic/socialnetworkservice/add_user_to_network_logic.go +++ /dev/null @@ -1,37 +0,0 @@ -package socialnetworkservicelogic - -import ( - "context" - "fmt" - - "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type AddUserToNetworkLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewAddUserToNetworkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddUserToNetworkLogic { - return &AddUserToNetworkLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -func (l *AddUserToNetworkLogic) AddUserToNetwork(in *tweeting.AddUserToNetworkReq) (*tweeting.OKResp, error) { - // todo: add your logic here and delete this line - err := l.svcCtx.SocialNetworkRepository.CreateUserNode(l.ctx, in.GetUid()) - if err != nil { - fmt.Println("gg88g88g8", err) - return nil, err - } - fmt.Println(err) - - return &tweeting.OKResp{}, nil -} diff --git a/internal/logic/socialnetworkservice/get_followee_count_logic.go b/internal/logic/socialnetworkservice/get_followee_count_logic.go new file mode 100644 index 0000000..3cd8227 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_followee_count_logic.go @@ -0,0 +1,58 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + ers "code.30cm.net/digimon/library-go/errs" + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetFolloweeCountLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetFolloweeCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFolloweeCountLogic { + return &GetFolloweeCountLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetFolloweeCount 取得我跟隨的數量 +func (l *GetFolloweeCountLogic) GetFolloweeCount(in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&getFollowCountReq{ + UID: in.Uid, + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + followeeCount, err := l.svcCtx.SocialNetworkRepository.GetFolloweeCount(l.ctx, in.GetUid()) + if err != nil { + // 錯誤代碼 05-021-34 + e := domain.CommentErrorL( + domain.GetFolloweeCountErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "SocialNetworkRepository.GetFolloweeCount"}, + {Key: "err", Value: err}, + }, + "failed to count follower").Wrap(err) + + return nil, e + } + + return &tweeting.FollowCountResp{ + Uid: in.GetUid(), + Total: followeeCount, + }, nil +} diff --git a/internal/logic/socialnetworkservice/get_followee_logic.go b/internal/logic/socialnetworkservice/get_followee_logic.go new file mode 100644 index 0000000..4a6ad59 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_followee_logic.go @@ -0,0 +1,69 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/domain/repository" + ers "code.30cm.net/digimon/library-go/errs" + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetFolloweeLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetFolloweeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFolloweeLogic { + return &GetFolloweeLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// GetFollowee 取得我跟隨的名單 +func (l *GetFolloweeLogic) GetFollowee(in *tweeting.FollowReq) (*tweeting.FollowResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&getFollowReq{ + UID: in.Uid, + PageSize: in.PageSize, + PageIndex: in.PageIndex, + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + followee, err := l.svcCtx.SocialNetworkRepository.GetFollowee(l.ctx, repository.FollowReq{ + UID: in.GetUid(), + PageIndex: in.GetPageIndex(), + PageSize: in.GetPageSize(), + }) + if err != nil { + // 錯誤代碼 05-021-33 + e := domain.CommentErrorL( + domain.GetFolloweeErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "SocialNetworkRepository.GetFollowee"}, + {Key: "err", Value: err}, + }, + "failed to get relation: ", in.GetUid()).Wrap(err) + + return nil, e + } + + return &tweeting.FollowResp{ + Uid: followee.UIDs, + Page: &tweeting.Pager{ + Total: followee.Total, + Index: in.GetPageIndex(), + Size: in.GetPageSize(), + }, + }, nil +} diff --git a/internal/logic/socialnetworkservice/get_follower_count_logic.go b/internal/logic/socialnetworkservice/get_follower_count_logic.go new file mode 100644 index 0000000..7d9b748 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_follower_count_logic.go @@ -0,0 +1,62 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + ers "code.30cm.net/digimon/library-go/errs" + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetFollowerCountLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetFollowerCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFollowerCountLogic { + return &GetFollowerCountLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type getFollowCountReq struct { + UID string `validate:"required"` +} + +// GetFollowerCount 取得跟隨者數量 +func (l *GetFollowerCountLogic) GetFollowerCount(in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&getFollowCountReq{ + UID: in.Uid, + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + followerCount, err := l.svcCtx.SocialNetworkRepository.GetFollowerCount(l.ctx, in.GetUid()) + if err != nil { + // 錯誤代碼 05-021-32 + e := domain.CommentErrorL( + domain.GetFollowerCountErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "SocialNetworkRepository.GetFollowerCount"}, + {Key: "err", Value: err}, + }, + "failed to count follower").Wrap(err) + + return nil, e + } + + return &tweeting.FollowCountResp{ + Uid: in.GetUid(), + Total: followerCount, + }, nil +} diff --git a/internal/logic/socialnetworkservice/get_follower_logic.go b/internal/logic/socialnetworkservice/get_follower_logic.go new file mode 100644 index 0000000..7dd72f7 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_follower_logic.go @@ -0,0 +1,75 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + "app-cloudep-tweeting-service/internal/domain/repository" + ers "code.30cm.net/digimon/library-go/errs" + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetFollowerLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetFollowerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFollowerLogic { + return &GetFollowerLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type getFollowReq struct { + UID string `validate:"required"` + PageSize int64 `validate:"required"` + PageIndex int64 `validate:"required"` +} + +// GetFollower 取得跟隨者名單 +func (l *GetFollowerLogic) GetFollower(in *tweeting.FollowReq) (*tweeting.FollowResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&getFollowReq{ + UID: in.Uid, + PageSize: in.PageSize, + PageIndex: in.PageIndex, + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + follower, err := l.svcCtx.SocialNetworkRepository.GetFollower(l.ctx, repository.FollowReq{ + UID: in.GetUid(), + PageIndex: in.GetPageIndex(), + PageSize: in.GetPageSize(), + }) + if err != nil { + // 錯誤代碼 05-021-31 + e := domain.CommentErrorL( + domain.GetFollowerErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "SocialNetworkRepository.GetFollower"}, + {Key: "err", Value: err}, + }, + "failed to get relation: ", in.GetUid()).Wrap(err) + + return nil, e + } + + return &tweeting.FollowResp{ + Uid: follower.UIDs, + Page: &tweeting.Pager{ + Total: follower.Total, + Index: in.GetPageIndex(), + Size: in.GetPageSize(), + }, + }, nil +} diff --git a/internal/logic/socialnetworkservice/mark_follow_relation_logic.go b/internal/logic/socialnetworkservice/mark_follow_relation_logic.go new file mode 100644 index 0000000..ce133de --- /dev/null +++ b/internal/logic/socialnetworkservice/mark_follow_relation_logic.go @@ -0,0 +1,62 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + ers "code.30cm.net/digimon/library-go/errs" + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MarkFollowRelationLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewMarkFollowRelationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MarkFollowRelationLogic { + return &MarkFollowRelationLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +type doFollowReq struct { + FollowerUID string `json:"follower_uid" validate:"required"` // 追隨者,跟隨你的人(別人關注你) + FolloweeUID string `json:"followee_uid" validate:"required"` // 追蹤者,你跟隨的人(你關注別) +} + +// MarkFollowRelation 關注 +func (l *MarkFollowRelationLogic) MarkFollowRelation(in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&doFollowReq{ + FollowerUID: in.GetFollowerUid(), + FolloweeUID: in.GetFolloweeUid(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + // 這裡要幫建立關係, follower 追蹤 -> followee + err := l.svcCtx.SocialNetworkRepository.MarkFollowerRelation(l.ctx, in.GetFollowerUid(), in.GetFolloweeUid()) + if err != nil { + // 錯誤代碼 05-021-30 + e := domain.CommentErrorL( + domain.MarkRelationErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "SocialNetworkRepository.MarkFollowerRelationBetweenUsers"}, + {Key: "err", Value: err}, + }, + "failed to mark relation form -> to", in.GetFollowerUid(), in.GetFolloweeUid()).Wrap(err) + + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/logic/socialnetworkservice/remove_follow_relation_logic.go b/internal/logic/socialnetworkservice/remove_follow_relation_logic.go new file mode 100644 index 0000000..fa40e79 --- /dev/null +++ b/internal/logic/socialnetworkservice/remove_follow_relation_logic.go @@ -0,0 +1,57 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/internal/domain" + ers "code.30cm.net/digimon/library-go/errs" + "context" + + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RemoveFollowRelationLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewRemoveFollowRelationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RemoveFollowRelationLogic { + return &RemoveFollowRelationLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// RemoveFollowRelation 取消關注 +func (l *RemoveFollowRelationLogic) RemoveFollowRelation(in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&doFollowReq{ + FollowerUID: in.GetFollowerUid(), + FolloweeUID: in.GetFolloweeUid(), + }); err != nil { + // 錯誤代碼 05-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + // 這裡要幫刪除關係, follower 追蹤 -> followee + err := l.svcCtx.SocialNetworkRepository.RemoveFollowerRelation(l.ctx, in.GetFollowerUid(), in.GetFolloweeUid()) + if err != nil { + // 錯誤代碼 05-021-35 + e := domain.CommentErrorL( + domain.RemoveRelationErrorCode, + logx.WithContext(l.ctx), + []logx.LogField{ + {Key: "req", Value: in}, + {Key: "func", Value: "SocialNetworkRepository.RemoveFollowerRelation"}, + {Key: "err", Value: err}, + }, + "failed to remove relation form -> to", in.GetFollowerUid(), in.GetFolloweeUid()).Wrap(err) + + return nil, e + } + + return &tweeting.OKResp{}, nil +} diff --git a/internal/repository/social_network.go b/internal/repository/social_network.go index 45ea0f8..73ef3c4 100644 --- a/internal/repository/social_network.go +++ b/internal/repository/social_network.go @@ -5,6 +5,7 @@ import ( "app-cloudep-tweeting-service/internal/domain/repository" client4J "app-cloudep-tweeting-service/internal/lib/neo4j" "context" + "fmt" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) @@ -25,7 +26,7 @@ func MustSocialNetworkRepository(param SocialNetworkParam) repository.SocialNetw } } -func (s SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { +func (s *SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { session, err := s.neo4jClient.Conn() if err != nil { return err @@ -36,18 +37,356 @@ func (s SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) "uid": uid, } - _, err = session.NewSession(ctx, neo4j.SessionConfig{ + run, err := session.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeWrite, }).Run(ctx, "CREATE (n:User {uid: $uid}) RETURN n", params) if err != nil { return err } - // // 處理結果 - // if run.Next(ctx) { - // node := run.Record().AsMap() - // fmt.Printf("Created Node: %v\n", node) - // } + // 處理結果 + if run.Next(ctx) { + _ = run.Record().AsMap() + } return nil } + +func (s *SocialNetworkRepository) MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error { + session, err := s.neo4jClient.Conn() + if err != nil { + return err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "fromUID": fromUID, + "toUID": toUID, + } + + // 這是有向的關係 form -> to + query := ` + MERGE (from:User {uid: $fromUID}) + MERGE (to:User {uid: $toUID}) + MERGE (from)-[:FRIENDS_WITH]->(to) + RETURN from, to + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeWrite, + }).Run(ctx, query, params) + if err != nil { + return err + } + + // 處理結果 + if run.Next(ctx) { + _ = run.Record().AsMap() + } + + return nil +} + +func (s *SocialNetworkRepository) GetFollower(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return repository.FollowResp{}, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid": req.UID, + "skip": (req.PageIndex - 1) * req.PageSize, + "limit": req.PageSize, + } + + query := ` + MATCH (follower:User)-[:FRIENDS_WITH]->(user:User {uid: $uid}) + RETURN follower.uid AS uid + SKIP $skip LIMIT $limit + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return repository.FollowResp{}, err + } + + var uids []string + for run.Next(ctx) { + record := run.Record() + if uid, ok := record.Get("uid"); ok { + uids = append(uids, uid.(string)) + } + } + + total, err := s.GetFollowerCount(ctx, req.UID) + if err != nil { + return repository.FollowResp{}, err + } + + return repository.FollowResp{ + UIDs: uids, + Total: total, + }, nil +} + +func (s *SocialNetworkRepository) GetFollowee(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return repository.FollowResp{}, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid": req.UID, + "skip": (req.PageIndex - 1) * req.PageSize, + "limit": req.PageSize, + } + + query := ` + MATCH (user:User {uid: $uid})-[:FRIENDS_WITH]->(followee:User) + RETURN followee.uid AS uid + SKIP $skip LIMIT $limit + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return repository.FollowResp{}, err + } + + var uids []string + for run.Next(ctx) { + record := run.Record() + if uid, ok := record.Get("uid"); ok { + uids = append(uids, uid.(string)) + } + } + + total, err := s.GetFolloweeCount(ctx, req.UID) + if err != nil { + return repository.FollowResp{}, err + } + + return repository.FollowResp{ + UIDs: uids, + Total: total, + }, nil +} + +func (s *SocialNetworkRepository) GetFollowerCount(ctx context.Context, uid string) (int64, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return 0, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid": uid, + } + + query := ` + MATCH (:User)-[:FRIENDS_WITH]->(user:User {uid: $uid}) + RETURN count(*) AS followerCount + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return 0, err + } + + var count int64 + if run.Next(ctx) { + record := run.Record() + if followerCount, ok := record.Get("followerCount"); ok { + count = followerCount.(int64) + } + } + + return count, nil +} + +func (s *SocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid string) (int64, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return 0, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid": uid, + } + + query := ` + MATCH (user:User {uid: $uid})-[:FRIENDS_WITH]->(:User) + RETURN count(*) AS followeeCount + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return 0, err + } + + var count int64 + if run.Next(ctx) { + record := run.Record() + if followeeCount, ok := record.Get("followeeCount"); ok { + count = followeeCount.(int64) + } + } + + return count, nil +} + +func (s *SocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error { + session, err := s.neo4jClient.Conn() + if err != nil { + return err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "fromUID": fromUID, + "toUID": toUID, + } + + query := ` + MATCH (from:User {uid: $fromUID})-[r:FRIENDS_WITH]->(to:User {uid: $toUID}) + DELETE r + ` + + _, err = session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeWrite, + }).Run(ctx, query, params) + if err != nil { + return fmt.Errorf("failed to remove follower relation: %w", err) + } + + return nil +} + +// GetDegreeBetweenUsers 取得這兩個點之間的度數 (最短路徑長度) +func (s *SocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return 0, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid1": uid1, + "uid2": uid2, + } + + query := ` + MATCH (user1:User {uid: $uid1}), (user2:User {uid: $uid2}) + MATCH p = shortestPath((user1)-[*]-(user2)) + RETURN length(p) AS degree + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return 0, fmt.Errorf("failed to get degree between users: %w", err) + } + + var degree int64 + if run.Next(ctx) { + record := run.Record() + if deg, ok := record.Get("degree"); ok { + degree = deg.(int64) + } + } + + return degree, nil +} + +// GetUIDsWithinNDegrees 取得某個節點在 n 度內關係所有 UID +func (s *SocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return nil, 0, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid": uid, + "degrees": degrees, + "skip": (pageIndex - 1) * pageSize, + "limit": pageSize, + } + + // 查詢結果帶分頁 + query := ` + MATCH (user:User {uid: $uid})-[:FRIENDS_WITH*1..$degrees]-(related:User) + WITH DISTINCT related.uid AS uid + SKIP $skip LIMIT $limit + RETURN uid + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return nil, 0, fmt.Errorf("failed to get uids within %d degrees of user: %w", degrees, err) + } + + var uids []string + for run.Next(ctx) { + record := run.Record() + if uid, ok := record.Get("uid"); ok { + uids = append(uids, uid.(string)) + } + } + + // 計算總數 + totalCount, err := s.getTotalUIDsWithinNDegrees(ctx, uid, degrees) + if err != nil { + return nil, 0, err + } + + return uids, totalCount, nil +} + +func (s *SocialNetworkRepository) getTotalUIDsWithinNDegrees(ctx context.Context, uid string, degrees int64) (int64, error) { + session, err := s.neo4jClient.Conn() + if err != nil { + return 0, err + } + defer session.Close(ctx) + + params := map[string]interface{}{ + "uid": uid, + "degrees": degrees, + } + + query := ` + MATCH (user:User {uid: $uid})-[:FRIENDS_WITH*1..$degrees]-(related:User) + RETURN count(DISTINCT related.uid) AS totalCount + ` + + run, err := session.NewSession(ctx, neo4j.SessionConfig{ + AccessMode: neo4j.AccessModeRead, + }).Run(ctx, query, params) + if err != nil { + return 0, fmt.Errorf("failed to get total uids within %d degrees of user: %w", degrees, err) + } + + var totalCount int64 + if run.Next(ctx) { + record := run.Record() + if count, ok := record.Get("totalCount"); ok { + totalCount = count.(int64) + } + } + + return totalCount, nil +} diff --git a/internal/server/socialnetworkservice/social_network_service_server.go b/internal/server/socialnetworkservice/social_network_service_server.go index 3131851..a0d8859 100644 --- a/internal/server/socialnetworkservice/social_network_service_server.go +++ b/internal/server/socialnetworkservice/social_network_service_server.go @@ -22,7 +22,38 @@ func NewSocialNetworkServiceServer(svcCtx *svc.ServiceContext) *SocialNetworkSer } } -func (s *SocialNetworkServiceServer) AddUserToNetwork(ctx context.Context, in *tweeting.AddUserToNetworkReq) (*tweeting.OKResp, error) { - l := socialnetworkservicelogic.NewAddUserToNetworkLogic(ctx, s.svcCtx) - return l.AddUserToNetwork(in) +// MarkFollowRelation 關注 +func (s *SocialNetworkServiceServer) MarkFollowRelation(ctx context.Context, in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { + l := socialnetworkservicelogic.NewMarkFollowRelationLogic(ctx, s.svcCtx) + return l.MarkFollowRelation(in) +} + +// RemoveFollowRelation 取消關注 +func (s *SocialNetworkServiceServer) RemoveFollowRelation(ctx context.Context, in *tweeting.DoFollowerRelationReq) (*tweeting.OKResp, error) { + l := socialnetworkservicelogic.NewRemoveFollowRelationLogic(ctx, s.svcCtx) + return l.RemoveFollowRelation(in) +} + +// GetFollower 取得跟隨者名單 +func (s *SocialNetworkServiceServer) GetFollower(ctx context.Context, in *tweeting.FollowReq) (*tweeting.FollowResp, error) { + l := socialnetworkservicelogic.NewGetFollowerLogic(ctx, s.svcCtx) + return l.GetFollower(in) +} + +// GetFollowee 取得我跟隨的名單 +func (s *SocialNetworkServiceServer) GetFollowee(ctx context.Context, in *tweeting.FollowReq) (*tweeting.FollowResp, error) { + l := socialnetworkservicelogic.NewGetFolloweeLogic(ctx, s.svcCtx) + return l.GetFollowee(in) +} + +// GetFollowerCount 取得跟隨者數量 +func (s *SocialNetworkServiceServer) GetFollowerCount(ctx context.Context, in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { + l := socialnetworkservicelogic.NewGetFollowerCountLogic(ctx, s.svcCtx) + return l.GetFollowerCount(in) +} + +// GetFolloweeCount 取得我跟隨的數量 +func (s *SocialNetworkServiceServer) GetFolloweeCount(ctx context.Context, in *tweeting.FollowCountReq) (*tweeting.FollowCountResp, error) { + l := socialnetworkservicelogic.NewGetFolloweeCountLogic(ctx, s.svcCtx) + return l.GetFolloweeCount(in) } From ba437e83529b05c87f930c85492269e23e1174a7 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 3 Sep 2024 17:47:34 +0800 Subject: [PATCH 08/11] fix golint --- .golangci.yaml | 8 ++ Makefile | 1 + internal/domain/errors.go | 5 +- internal/lib/neo4j/neo4j.go | 2 +- .../commentservice/delete_comment_logic.go | 1 + .../commentservice/get_comments_logic.go | 5 +- .../commentservice/update_comment_logic.go | 9 ++- .../logic/postservice/create_post_logic.go | 3 +- .../logic/postservice/delete_post_logic.go | 1 + .../logic/postservice/list_posts_logic.go | 5 +- .../logic/postservice/update_post_logic.go | 7 +- .../get_followee_count_logic.go | 3 +- .../get_followee_logic.go | 3 +- .../get_follower_count_logic.go | 3 +- .../get_follower_logic.go | 3 +- .../mark_follow_relation_logic.go | 3 +- .../remove_follow_relation_logic.go | 3 +- .../logic/timelineservice/add_post_logic.go | 1 + .../clear_no_more_data_flag_logic.go | 1 + .../timelineservice/fetch_timeline_logic.go | 1 + .../timelineservice/has_no_more_data_logic.go | 1 + .../set_no_more_data_flag_logic.go | 1 + internal/repository/social_network.go | 79 +++++++++++++++---- .../repository/timeline_sort_by_timestamp.go | 1 + .../commentservice/comment_service_server.go | 2 +- .../server/postservice/post_service_server.go | 2 +- .../social_network_service_server.go | 2 +- .../timeline_service_server.go | 2 +- internal/svc/init_mongo.go | 8 +- internal/svc/service_context.go | 1 + tweeting.go | 7 +- 31 files changed, 124 insertions(+), 50 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 5518484..c9726ca 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -117,6 +117,14 @@ issues: - gocognit - contextcheck + exclude-dirs: + - internal/model + + exclude-files: + - .*_test.go + + + linters-settings: gci: sections: diff --git a/Makefile b/Makefile index 035a97b..186d9a1 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ test: # 進行測試 fmt: # 格式優化 $(GOFMT) -w $(GOFILES) goimports -w ./ + golangci-lint run .PHONY: gen-rpc gen-rpc: # 建立 rpc code diff --git a/internal/domain/errors.go b/internal/domain/errors.go index 618d6cc..2f18b18 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -1,7 +1,6 @@ package domain import ( - "fmt" "strings" ers "code.30cm.net/digimon/library-go/errs" @@ -51,9 +50,7 @@ const ( ) func CommentError(ec ErrorCode, s ...string) *ers.LibError { - return ers.NewError(code.CloudEPTweeting, code.DBError, - ec.ToUint32(), - fmt.Sprintf("%s", strings.Join(s, " "))) + return ers.NewError(code.CloudEPTweeting, code.DBError, ec.ToUint32(), strings.Join(s, " ")) } func CommentErrorL(ec ErrorCode, diff --git a/internal/lib/neo4j/neo4j.go b/internal/lib/neo4j/neo4j.go index d8178e7..c110b8a 100644 --- a/internal/lib/neo4j/neo4j.go +++ b/internal/lib/neo4j/neo4j.go @@ -37,7 +37,7 @@ func NewNeo4J(conf *Config, opts ...Option) *Client { // Conn initiates connection to the database and returns a Neo4j driver instance. func (c *Client) Conn() (neo4j.DriverWithContext, error) { auth := neo4j.BasicAuth(c.serviceConf.Username, c.serviceConf.Password, "") - driver, err := neo4j.NewDriverWithContext(c.serviceConf.URI, auth, func(c *n4Cfg.Config) {}) + driver, err := neo4j.NewDriverWithContext(c.serviceConf.URI, auth, func(_ *n4Cfg.Config) {}) if err != nil { return nil, fmt.Errorf("neo4j driver initialization error: %w", err) } diff --git a/internal/logic/commentservice/delete_comment_logic.go b/internal/logic/commentservice/delete_comment_logic.go index 467d027..80ec847 100644 --- a/internal/logic/commentservice/delete_comment_logic.go +++ b/internal/logic/commentservice/delete_comment_logic.go @@ -37,6 +37,7 @@ func (l *DeleteCommentLogic) DeleteComment(in *tweeting.DeleteCommentReq) (*twee {Key: "err", Value: err}, }, "failed to del comment").Wrap(err) + return nil, e } diff --git a/internal/logic/commentservice/get_comments_logic.go b/internal/logic/commentservice/get_comments_logic.go index d22a2cf..d369e1e 100644 --- a/internal/logic/commentservice/get_comments_logic.go +++ b/internal/logic/commentservice/get_comments_logic.go @@ -41,8 +41,8 @@ func convertToCommentDetailItem(item *model.Comment) *tweeting.CommentDetail { Uid: item.UID, Content: item.Content, CreatedAt: item.CreateAt, - LikeCount: int64(item.LikeCount), - DislikeCount: int64(item.DisLikeCount), + LikeCount: item.LikeCount, + DislikeCount: item.DisLikeCount, } } @@ -82,6 +82,7 @@ func (l *GetCommentsLogic) GetComments(in *tweeting.GetCommentsReq) (*tweeting.G {Key: "err", Value: err}, }, "failed to find comment").Wrap(err) + return nil, e } diff --git a/internal/logic/commentservice/update_comment_logic.go b/internal/logic/commentservice/update_comment_logic.go index bc24ef0..5002f72 100644 --- a/internal/logic/commentservice/update_comment_logic.go +++ b/internal/logic/commentservice/update_comment_logic.go @@ -27,16 +27,16 @@ func NewUpdateCommentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upd } } -type checkCommentId struct { - CommentId string `validate:"required"` +type checkCommentID struct { + CommentID string `validate:"required"` Content string `json:"content,omitempty" validate:"lte=500"` } // UpdateComment 更新評論 func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*tweeting.OKResp, error) { // 驗證資料 - if err := l.svcCtx.Validate.ValidateAll(&checkCommentId{ - CommentId: in.GetCommentId(), + if err := l.svcCtx.Validate.ValidateAll(&checkCommentID{ + CommentID: in.GetCommentId(), Content: in.GetContent(), }); err != nil { // 錯誤代碼 05-011-00 @@ -75,6 +75,7 @@ func (l *UpdateCommentLogic) UpdateComment(in *tweeting.UpdateCommentReq) (*twee {Key: "err", Value: err}, }, "failed to update comment:", in.CommentId).Wrap(err) + return nil, e } diff --git a/internal/logic/postservice/create_post_logic.go b/internal/logic/postservice/create_post_logic.go index e3fbb0d..ec00a3c 100644 --- a/internal/logic/postservice/create_post_logic.go +++ b/internal/logic/postservice/create_post_logic.go @@ -32,7 +32,7 @@ type newTweetingReq struct { UID string `json:"uid" validate:"required"` Content string `json:"content" validate:"required,lte=500"` // 貼文限制 500 字內 Tags []string `json:"tags"` - MediaUrl []string `json:"media_url"` + MediaURL []string `json:"media_url"` IsAd bool `json:"is_ad"` // default false } @@ -85,6 +85,7 @@ func (l *CreatePostLogic) CreatePost(in *tweeting.NewPostReq) (*tweeting.PostRes {Key: "err", Value: err}, }, "failed to add new post").Wrap(err) + return nil, e } diff --git a/internal/logic/postservice/delete_post_logic.go b/internal/logic/postservice/delete_post_logic.go index ffa6bda..60256f6 100644 --- a/internal/logic/postservice/delete_post_logic.go +++ b/internal/logic/postservice/delete_post_logic.go @@ -37,6 +37,7 @@ func (l *DeletePostLogic) DeletePost(in *tweeting.DeletePostsReq) (*tweeting.OKR {Key: "err", Value: err}, }, "failed to del post").Wrap(err) + return nil, e } diff --git a/internal/logic/postservice/list_posts_logic.go b/internal/logic/postservice/list_posts_logic.go index 544327d..568d801 100644 --- a/internal/logic/postservice/list_posts_logic.go +++ b/internal/logic/postservice/list_posts_logic.go @@ -54,8 +54,8 @@ func convertToPostDetailItem(item *model.Post) *tweeting.PostDetailItem { IsAd: item.IsAd, CreatedAt: item.CreateAt, UpdateAt: item.UpdateAt, - LikeCount: int64(item.Like), - DislikeCount: int64(item.DisLike), + LikeCount: item.Like, + DislikeCount: item.DisLike, } } @@ -102,6 +102,7 @@ func (l *ListPostsLogic) ListPosts(in *tweeting.QueryPostsReq) (*tweeting.ListPo {Key: "err", Value: err}, }, "failed to find posts").Wrap(err) + return nil, e } diff --git a/internal/logic/postservice/update_post_logic.go b/internal/logic/postservice/update_post_logic.go index 2275fcc..f028b6c 100644 --- a/internal/logic/postservice/update_post_logic.go +++ b/internal/logic/postservice/update_post_logic.go @@ -28,7 +28,7 @@ func NewUpdatePostLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Update } } -type checkPostId struct { +type checkPostID struct { PostID string `validate:"required"` Content string `json:"content,omitempty" validate:"lte=500"` } @@ -36,7 +36,7 @@ type checkPostId struct { // UpdatePost 更新貼文 func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKResp, error) { // 驗證資料 - if err := l.svcCtx.Validate.ValidateAll(&checkPostId{ + if err := l.svcCtx.Validate.ValidateAll(&checkPostID{ PostID: in.GetPostId(), Content: in.GetContent(), }); err != nil { @@ -53,7 +53,7 @@ func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKRe update.ID = oid update.Tags = in.GetTags() // 將 Media 存入 - var media []model.Media + media := make([]model.Media, 0, len(in.GetMedia())) for _, item := range in.GetMedia() { media = append(media, model.Media{ Links: item.Url, @@ -88,6 +88,7 @@ func (l *UpdatePostLogic) UpdatePost(in *tweeting.UpdatePostReq) (*tweeting.OKRe {Key: "err", Value: err}, }, "failed to update post", in.PostId).Wrap(err) + return nil, e } diff --git a/internal/logic/socialnetworkservice/get_followee_count_logic.go b/internal/logic/socialnetworkservice/get_followee_count_logic.go index 3cd8227..d5b4385 100644 --- a/internal/logic/socialnetworkservice/get_followee_count_logic.go +++ b/internal/logic/socialnetworkservice/get_followee_count_logic.go @@ -2,9 +2,10 @@ package socialnetworkservicelogic import ( "app-cloudep-tweeting-service/internal/domain" - ers "code.30cm.net/digimon/library-go/errs" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" diff --git a/internal/logic/socialnetworkservice/get_followee_logic.go b/internal/logic/socialnetworkservice/get_followee_logic.go index 4a6ad59..73d28d1 100644 --- a/internal/logic/socialnetworkservice/get_followee_logic.go +++ b/internal/logic/socialnetworkservice/get_followee_logic.go @@ -3,9 +3,10 @@ package socialnetworkservicelogic import ( "app-cloudep-tweeting-service/internal/domain" "app-cloudep-tweeting-service/internal/domain/repository" - ers "code.30cm.net/digimon/library-go/errs" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" diff --git a/internal/logic/socialnetworkservice/get_follower_count_logic.go b/internal/logic/socialnetworkservice/get_follower_count_logic.go index 7d9b748..2cd9bb7 100644 --- a/internal/logic/socialnetworkservice/get_follower_count_logic.go +++ b/internal/logic/socialnetworkservice/get_follower_count_logic.go @@ -2,9 +2,10 @@ package socialnetworkservicelogic import ( "app-cloudep-tweeting-service/internal/domain" - ers "code.30cm.net/digimon/library-go/errs" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" diff --git a/internal/logic/socialnetworkservice/get_follower_logic.go b/internal/logic/socialnetworkservice/get_follower_logic.go index 7dd72f7..f6c4234 100644 --- a/internal/logic/socialnetworkservice/get_follower_logic.go +++ b/internal/logic/socialnetworkservice/get_follower_logic.go @@ -3,9 +3,10 @@ package socialnetworkservicelogic import ( "app-cloudep-tweeting-service/internal/domain" "app-cloudep-tweeting-service/internal/domain/repository" - ers "code.30cm.net/digimon/library-go/errs" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" diff --git a/internal/logic/socialnetworkservice/mark_follow_relation_logic.go b/internal/logic/socialnetworkservice/mark_follow_relation_logic.go index ce133de..0e1465c 100644 --- a/internal/logic/socialnetworkservice/mark_follow_relation_logic.go +++ b/internal/logic/socialnetworkservice/mark_follow_relation_logic.go @@ -2,9 +2,10 @@ package socialnetworkservicelogic import ( "app-cloudep-tweeting-service/internal/domain" - ers "code.30cm.net/digimon/library-go/errs" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" diff --git a/internal/logic/socialnetworkservice/remove_follow_relation_logic.go b/internal/logic/socialnetworkservice/remove_follow_relation_logic.go index fa40e79..eff0d15 100644 --- a/internal/logic/socialnetworkservice/remove_follow_relation_logic.go +++ b/internal/logic/socialnetworkservice/remove_follow_relation_logic.go @@ -2,9 +2,10 @@ package socialnetworkservicelogic import ( "app-cloudep-tweeting-service/internal/domain" - ers "code.30cm.net/digimon/library-go/errs" "context" + ers "code.30cm.net/digimon/library-go/errs" + "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/svc" diff --git a/internal/logic/timelineservice/add_post_logic.go b/internal/logic/timelineservice/add_post_logic.go index 6a51a13..53910e4 100644 --- a/internal/logic/timelineservice/add_post_logic.go +++ b/internal/logic/timelineservice/add_post_logic.go @@ -69,6 +69,7 @@ func (l *AddPostLogic) AddPost(in *tweeting.AddPostToTimelineReq) (*tweeting.OKR {Key: "err", Value: err}, }, "failed to insert timeline repo :", in.GetUid()).Wrap(err) + return nil, e } diff --git a/internal/logic/timelineservice/clear_no_more_data_flag_logic.go b/internal/logic/timelineservice/clear_no_more_data_flag_logic.go index 050a7e7..ca8e961 100644 --- a/internal/logic/timelineservice/clear_no_more_data_flag_logic.go +++ b/internal/logic/timelineservice/clear_no_more_data_flag_logic.go @@ -52,6 +52,7 @@ func (l *ClearNoMoreDataFlagLogic) ClearNoMoreDataFlag(in *tweeting.DoNoMoreData {Key: "err", Value: err}, }, "failed to clear no more data flag timeline repo :", in.GetUid()).Wrap(err) + return nil, e } diff --git a/internal/logic/timelineservice/fetch_timeline_logic.go b/internal/logic/timelineservice/fetch_timeline_logic.go index 46ada2e..74f89d4 100644 --- a/internal/logic/timelineservice/fetch_timeline_logic.go +++ b/internal/logic/timelineservice/fetch_timeline_logic.go @@ -61,6 +61,7 @@ func (l *FetchTimelineLogic) FetchTimeline(in *tweeting.GetTimelineReq) (*tweeti {Key: "err", Value: err}, }, "failed to fetch timeline repo :", in.GetUid()).Wrap(err) + return nil, e } diff --git a/internal/logic/timelineservice/has_no_more_data_logic.go b/internal/logic/timelineservice/has_no_more_data_logic.go index 56d7f90..6146ffe 100644 --- a/internal/logic/timelineservice/has_no_more_data_logic.go +++ b/internal/logic/timelineservice/has_no_more_data_logic.go @@ -52,6 +52,7 @@ func (l *HasNoMoreDataLogic) HasNoMoreData(in *tweeting.DoNoMoreDataReq) (*tweet {Key: "err", Value: err}, }, "failed to get no more data flag:", in.GetUid()).Wrap(err) + return nil, e } diff --git a/internal/logic/timelineservice/set_no_more_data_flag_logic.go b/internal/logic/timelineservice/set_no_more_data_flag_logic.go index 321a0d0..59da4df 100644 --- a/internal/logic/timelineservice/set_no_more_data_flag_logic.go +++ b/internal/logic/timelineservice/set_no_more_data_flag_logic.go @@ -52,6 +52,7 @@ func (l *SetNoMoreDataFlagLogic) SetNoMoreDataFlag(in *tweeting.DoNoMoreDataReq) {Key: "err", Value: err}, }, "failed to set no more data flag:", in.GetUid()).Wrap(err) + return nil, e } diff --git a/internal/repository/social_network.go b/internal/repository/social_network.go index 73ef3c4..978f670 100644 --- a/internal/repository/social_network.go +++ b/internal/repository/social_network.go @@ -6,6 +6,9 @@ import ( client4J "app-cloudep-tweeting-service/internal/lib/neo4j" "context" "fmt" + + "github.com/zeromicro/go-zero/core/logx" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) @@ -27,6 +30,7 @@ func MustSocialNetworkRepository(param SocialNetworkParam) repository.SocialNetw } func (s *SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return err @@ -53,6 +57,7 @@ func (s *SocialNetworkRepository) CreateUserNode(ctx context.Context, uid string } func (s *SocialNetworkRepository) MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return err @@ -88,6 +93,7 @@ func (s *SocialNetworkRepository) MarkFollowerRelation(ctx context.Context, from } func (s *SocialNetworkRepository) GetFollower(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return repository.FollowResp{}, err @@ -113,11 +119,17 @@ func (s *SocialNetworkRepository) GetFollower(ctx context.Context, req repositor return repository.FollowResp{}, err } - var uids []string + var uidList []string for run.Next(ctx) { record := run.Record() - if uid, ok := record.Get("uid"); ok { - uids = append(uids, uid.(string)) + uid, ok := record.Get("uid") + if ok { + if uidStr, ok := uid.(string); ok { + uidList = append(uidList, uidStr) + } else { + // TODO 可以印 log + continue + } } } @@ -127,12 +139,13 @@ func (s *SocialNetworkRepository) GetFollower(ctx context.Context, req repositor } return repository.FollowResp{ - UIDs: uids, + UIDs: uidList, Total: total, }, nil } func (s *SocialNetworkRepository) GetFollowee(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return repository.FollowResp{}, err @@ -158,11 +171,17 @@ func (s *SocialNetworkRepository) GetFollowee(ctx context.Context, req repositor return repository.FollowResp{}, err } - var uids []string + var uidList []string for run.Next(ctx) { record := run.Record() - if uid, ok := record.Get("uid"); ok { - uids = append(uids, uid.(string)) + uid, ok := record.Get("uid") + if ok { + if uidStr, ok := uid.(string); ok { + uidList = append(uidList, uidStr) + } else { + // 可以印 log + continue + } } } @@ -172,12 +191,13 @@ func (s *SocialNetworkRepository) GetFollowee(ctx context.Context, req repositor } return repository.FollowResp{ - UIDs: uids, + UIDs: uidList, Total: total, }, nil } func (s *SocialNetworkRepository) GetFollowerCount(ctx context.Context, uid string) (int64, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return 0, err @@ -204,7 +224,11 @@ func (s *SocialNetworkRepository) GetFollowerCount(ctx context.Context, uid stri if run.Next(ctx) { record := run.Record() if followerCount, ok := record.Get("followerCount"); ok { - count = followerCount.(int64) + if dc, ok := followerCount.(int64); ok { + count = dc + } else { + logx.Info("followerCount error") + } } } @@ -212,6 +236,7 @@ func (s *SocialNetworkRepository) GetFollowerCount(ctx context.Context, uid stri } func (s *SocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid string) (int64, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return 0, err @@ -238,7 +263,11 @@ func (s *SocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid stri if run.Next(ctx) { record := run.Record() if followeeCount, ok := record.Get("followeeCount"); ok { - count = followeeCount.(int64) + if dc, ok := followeeCount.(int64); ok { + count = dc + } else { + logx.Info("followeeCount error") + } } } @@ -246,6 +275,7 @@ func (s *SocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid stri } func (s *SocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return err @@ -274,6 +304,7 @@ func (s *SocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fr // GetDegreeBetweenUsers 取得這兩個點之間的度數 (最短路徑長度) func (s *SocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return 0, err @@ -302,7 +333,11 @@ func (s *SocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid if run.Next(ctx) { record := run.Record() if deg, ok := record.Get("degree"); ok { - degree = deg.(int64) + if degreeValue, ok := deg.(int64); ok { + degree = degreeValue + } else { + logx.Info("degree error") + } } } @@ -311,6 +346,7 @@ func (s *SocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid // GetUIDsWithinNDegrees 取得某個節點在 n 度內關係所有 UID func (s *SocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return nil, 0, err @@ -339,11 +375,17 @@ func (s *SocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid return nil, 0, fmt.Errorf("failed to get uids within %d degrees of user: %w", degrees, err) } - var uids []string + var uidList []string for run.Next(ctx) { record := run.Record() - if uid, ok := record.Get("uid"); ok { - uids = append(uids, uid.(string)) + uid, ok := record.Get("uid") + if ok { + if uidStr, ok := uid.(string); ok { + uidList = append(uidList, uidStr) + } else { + // 可以印 log + continue + } } } @@ -353,10 +395,11 @@ func (s *SocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid return nil, 0, err } - return uids, totalCount, nil + return uidList, totalCount, nil } func (s *SocialNetworkRepository) getTotalUIDsWithinNDegrees(ctx context.Context, uid string, degrees int64) (int64, error) { + //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return 0, err @@ -384,7 +427,11 @@ func (s *SocialNetworkRepository) getTotalUIDsWithinNDegrees(ctx context.Context if run.Next(ctx) { record := run.Record() if count, ok := record.Get("totalCount"); ok { - totalCount = count.(int64) + if countV, ok := count.(int64); ok { + totalCount = countV + } else { + logx.Info("totalCount error") + } } } diff --git a/internal/repository/timeline_sort_by_timestamp.go b/internal/repository/timeline_sort_by_timestamp.go index 9599c71..ebf4c1d 100644 --- a/internal/repository/timeline_sort_by_timestamp.go +++ b/internal/repository/timeline_sort_by_timestamp.go @@ -139,5 +139,6 @@ func (t *TimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string key := domain.TimelineRedisKey.With(uid).ToString() // 移除 "NoMoreData" 標誌 _, err := t.redis.ZremCtx(ctx, key, domain.LastOfTimelineFlag) + return err } diff --git a/internal/server/commentservice/comment_service_server.go b/internal/server/commentservice/comment_service_server.go index f434bad..9fed63f 100644 --- a/internal/server/commentservice/comment_service_server.go +++ b/internal/server/commentservice/comment_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/commentservice" + commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go index 07d52d3..639c1f4 100644 --- a/internal/server/postservice/post_service_server.go +++ b/internal/server/postservice/post_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/postservice" + postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/socialnetworkservice/social_network_service_server.go b/internal/server/socialnetworkservice/social_network_service_server.go index a0d8859..efb9003 100644 --- a/internal/server/socialnetworkservice/social_network_service_server.go +++ b/internal/server/socialnetworkservice/social_network_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/socialnetworkservice" + socialnetworkservicelogic "app-cloudep-tweeting-service/internal/logic/socialnetworkservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/server/timelineservice/timeline_service_server.go b/internal/server/timelineservice/timeline_service_server.go index 771d6c7..2d2dc03 100644 --- a/internal/server/timelineservice/timeline_service_server.go +++ b/internal/server/timelineservice/timeline_service_server.go @@ -7,7 +7,7 @@ import ( "context" "app-cloudep-tweeting-service/gen_result/pb/tweeting" - "app-cloudep-tweeting-service/internal/logic/timelineservice" + timelineservicelogic "app-cloudep-tweeting-service/internal/logic/timelineservice" "app-cloudep-tweeting-service/internal/svc" ) diff --git a/internal/svc/init_mongo.go b/internal/svc/init_mongo.go index d815d98..8f9bf1e 100644 --- a/internal/svc/init_mongo.go +++ b/internal/svc/init_mongo.go @@ -6,7 +6,7 @@ import ( "fmt" ) -func mustMongoConnectUrl(c config.Config) string { +func mustMongoConnectURL(c config.Config) string { return fmt.Sprintf("%s://%s:%s", c.Mongo.Schema, c.Mongo.Host, @@ -18,10 +18,12 @@ func mustMongoConnectUrl(c config.Config) string { func MustPostModel(c config.Config) model.PostModel { postCollection := model.Post{} - return model.NewPostModel(mustMongoConnectUrl(c), c.Mongo.Database, postCollection.CollectionName()) + + return model.NewPostModel(mustMongoConnectURL(c), c.Mongo.Database, postCollection.CollectionName()) } func MustCommentModel(c config.Config) model.CommentModel { m := model.Comment{} - return model.NewCommentModel(mustMongoConnectUrl(c), c.Mongo.Database, m.CollectionName()) + + return model.NewCommentModel(mustMongoConnectURL(c), c.Mongo.Database, m.CollectionName()) } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index e70b793..0c63516 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -6,6 +6,7 @@ import ( "app-cloudep-tweeting-service/internal/lib/neo4j" model "app-cloudep-tweeting-service/internal/model/mongo" "app-cloudep-tweeting-service/internal/repository" + "github.com/zeromicro/go-zero/core/stores/redis" vi "code.30cm.net/digimon/library-go/validator" diff --git a/tweeting.go b/tweeting.go index 173ffd1..45b83b2 100644 --- a/tweeting.go +++ b/tweeting.go @@ -1,9 +1,6 @@ package main import ( - "flag" - "fmt" - "app-cloudep-tweeting-service/gen_result/pb/tweeting" "app-cloudep-tweeting-service/internal/config" commentserviceServer "app-cloudep-tweeting-service/internal/server/commentservice" @@ -11,6 +8,8 @@ import ( socialnetworkserviceServer "app-cloudep-tweeting-service/internal/server/socialnetworkservice" timelineserviceServer "app-cloudep-tweeting-service/internal/server/timelineservice" "app-cloudep-tweeting-service/internal/svc" + "flag" + "log" "github.com/zeromicro/go-zero/core/conf" "github.com/zeromicro/go-zero/core/service" @@ -40,6 +39,6 @@ func main() { }) defer s.Stop() - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + log.Printf("Starting rpc server at %s...\n", c.ListenOn) s.Start() } From 647e645d6192667754105da46b7af75ee7664b20 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 3 Sep 2024 17:52:44 +0800 Subject: [PATCH 09/11] fix test --- internal/repository/timeline_sort_by_timestamp_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/timeline_sort_by_timestamp_test.go b/internal/repository/timeline_sort_by_timestamp_test.go index 6388107..5d81570 100644 --- a/internal/repository/timeline_sort_by_timestamp_test.go +++ b/internal/repository/timeline_sort_by_timestamp_test.go @@ -36,7 +36,7 @@ func NewRepo() (*miniredis.Miniredis, repository.TimelineRepository, error) { }{Expire: 86400, MaxLength: 1000}, } - timelineRepo := MustGenerateUseCase(TimelineRepositoryParam{ + timelineRepo := MustGenerateRepository(TimelineRepositoryParam{ Config: c, Redis: *newRedis, }) From 184586cdffe146335d19208373250d2a5c4a2a96 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 3 Sep 2024 19:20:10 +0800 Subject: [PATCH 10/11] add social network test --- Makefile | 2 + go.mod | 33 +++ internal/config/config.go | 2 +- internal/lib/neo4j/neo4j_test.go | 209 ++++++++++++++++++ .../get_followee_count_logic_test.go | 88 ++++++++ .../get_followee_logic_test.go | 130 +++++++++++ .../get_follower_count_logic_test.go | 108 +++++++++ .../get_follower_logic_test.go | 130 +++++++++++ .../mark_follow_relation_logic_test.go | 108 +++++++++ .../remove_follow_relation_logic_test.go | 108 +++++++++ internal/mock/repository/social_network.go | 174 +++++++++++++++ internal/mock/repository/timeline.go | 113 ++++++++++ internal/repository/social_network_test.go | 1 + 13 files changed, 1205 insertions(+), 1 deletion(-) create mode 100644 internal/lib/neo4j/neo4j_test.go create mode 100644 internal/logic/socialnetworkservice/get_followee_count_logic_test.go create mode 100644 internal/logic/socialnetworkservice/get_followee_logic_test.go create mode 100644 internal/logic/socialnetworkservice/get_follower_count_logic_test.go create mode 100644 internal/logic/socialnetworkservice/get_follower_logic_test.go create mode 100644 internal/logic/socialnetworkservice/mark_follow_relation_logic_test.go create mode 100644 internal/logic/socialnetworkservice/remove_follow_relation_logic_test.go create mode 100644 internal/mock/repository/social_network.go create mode 100644 internal/mock/repository/timeline.go create mode 100644 internal/repository/social_network_test.go diff --git a/Makefile b/Makefile index 186d9a1..d8e2c07 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,8 @@ mock-gen: # 建立 mock 資料 mockgen -source=./internal/model/mongo/post_model.go -destination=./internal/mock/model/post_model.go -package=mock mockgen -source=./internal/model/mongo/comment_model_gen.go -destination=./internal/mock/model/comment_model_gen.go -package=mock mockgen -source=./internal/model/mongo/comment_model.go -destination=./internal/mock/model/comment_model.go -package=mock + mockgen -source=./internal/domain/repository/social_network.go -destination=./internal/mock/repository/social_network.go -package=mock + mockgen -source=./internal/domain/repository/timeline.go -destination=./internal/mock/repository/timeline.go -package=mock @echo "Generate mock files successfully" .PHONY: migrate-database diff --git a/go.mod b/go.mod index f1c33fb..f1f1499 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/alicebob/miniredis/v2 v2.33.0 github.com/neo4j/neo4j-go-driver/v5 v5.24.0 github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.33.0 github.com/zeromicro/go-zero v1.7.0 go.mongodb.org/mongo-driver v1.16.0 go.uber.org/mock v0.4.0 @@ -16,19 +17,32 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect @@ -48,30 +62,49 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/redis/go-redis/v9 v9.6.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index 30a88a1..a028a59 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,7 +27,7 @@ type Config struct { // Redis Cluster RedisCluster redis.RedisConf - // 圖形話資料庫 + // 圖形資料庫 Neo4J struct { URI string Username string diff --git a/internal/lib/neo4j/neo4j_test.go b/internal/lib/neo4j/neo4j_test.go new file mode 100644 index 0000000..5618f69 --- /dev/null +++ b/internal/lib/neo4j/neo4j_test.go @@ -0,0 +1,209 @@ +package neo4j + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestNewNeo4J(t *testing.T) { + tests := []struct { + name string + conf *Config + expected *Config + }{ + { + name: "valid configuration", + conf: &Config{ + URI: "neo4j://localhost:7687", + Username: "neo4j", + Password: "password", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + expected: &Config{ + URI: "neo4j://localhost:7687", + Username: "neo4j", + Password: "password", + LogLevel: "info", + }, + }, + { + name: "empty URI", + conf: &Config{ + URI: "", + Username: "neo4j", + Password: "password", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + expected: &Config{ + URI: "", + Username: "neo4j", + Password: "password", + LogLevel: "info", + }, + }, + { + name: "empty username and password", + conf: &Config{ + URI: "neo4j://localhost:7687", + Username: "", + Password: "", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + expected: &Config{ + URI: "neo4j://localhost:7687", + Username: "", + Password: "", + LogLevel: "info", + }, + }, + { + name: "custom log level", + conf: &Config{ + URI: "neo4j://localhost:7687", + Username: "neo4j", + Password: "password", + LogLevel: "debug", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + expected: &Config{ + URI: "neo4j://localhost:7687", + Username: "neo4j", + Password: "password", + LogLevel: "debug", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewNeo4J(tt.conf) + assert.NotNil(t, client) + assert.Equal(t, tt.expected.URI, client.serviceConf.URI) + assert.Equal(t, tt.expected.Username, client.serviceConf.Username) + assert.Equal(t, tt.expected.Password, client.serviceConf.Password) + assert.Equal(t, tt.expected.LogLevel, client.serviceConf.LogLevel) + }) + } +} + +func TestConn(t *testing.T) { + ctx := context.Background() + + neo4jContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "neo4j:latest", + ExposedPorts: []string{"7687/tcp"}, + Env: map[string]string{ + "NEO4J_AUTH": "neo4j/yyyytttt", + }, + WaitingFor: wait.ForLog("Started"), + }, + Started: true, + }) + if err != nil { + t.Fatal(err) + } + defer neo4jContainer.Terminate(ctx) + + host, _ := neo4jContainer.Host(ctx) + port, _ := neo4jContainer.MappedPort(ctx, "7687") + + uri := fmt.Sprintf("bolt://%s:%s", host, port.Port()) + t.Log("Neo4j running at:", uri) + + tests := []struct { + name string + conf *Config + shouldFail bool + }{ + { + name: "successful connection", + conf: &Config{ + URI: uri, + Username: "neo4j", + Password: "yyyytttt", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + shouldFail: false, + }, + { + name: "failed connection due to invalid URI", + conf: &Config{ + URI: uri, + Username: "neo4j", + Password: "wrongpassword", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + shouldFail: true, + }, + { + name: "failed connection due to missing URI", + conf: &Config{ + URI: "", + Username: "neo4j", + Password: "password", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + shouldFail: true, + }, + { + name: "failed connection due to missing username and password", + conf: &Config{ + URI: uri, + Username: "", + Password: "", + LogLevel: "info", + MaxConnectionLifetime: time.Minute * 5, + MaxConnectionPoolSize: 10, + ConnectionTimeout: time.Second * 5, + }, + shouldFail: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewNeo4J(tt.conf) + driver, err := client.Conn() + if tt.shouldFail { + assert.Error(t, err) + assert.Nil(t, driver) + } else { + assert.NoError(t, err) + assert.NotNil(t, driver) + + // Close the driver after test + defer func() { + err := driver.Close(context.Background()) + assert.NoError(t, err) + }() + } + }) + } +} diff --git a/internal/logic/socialnetworkservice/get_followee_count_logic_test.go b/internal/logic/socialnetworkservice/get_followee_count_logic_test.go new file mode 100644 index 0000000..93d9991 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_followee_count_logic_test.go @@ -0,0 +1,88 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestGetFolloweeCountLogic_GetFolloweeCount(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + svcCtx := &svc.ServiceContext{ + SocialNetworkRepository: mockSocialNetworkRepository, + Validate: mockValidate, + } + + // 测试数据集 + tests := []struct { + name string + input *tweeting.FollowCountReq + prepare func() + expectErr bool + }{ + { + name: "ok", + input: &tweeting.FollowCountReq{ + Uid: "12345", + }, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + mockSocialNetworkRepository.EXPECT().GetFolloweeCount(gomock.Any(), "12345").Return(int64(10), nil).Times(1) + }, + expectErr: false, + }, + { + name: "驗證失敗", + input: &tweeting.FollowCountReq{ + Uid: "", + }, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + }, + { + name: "取得跟隨數量失敗", + input: &tweeting.FollowCountReq{ + Uid: "12345", + }, + prepare: func() { + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + mockSocialNetworkRepository.EXPECT().GetFolloweeCount(gomock.Any(), "12345").Return(int64(0), errors.New("repository error")).Times(1) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.prepare() + + logic := GetFolloweeCountLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + got, err := logic.GetFolloweeCount(tt.input) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.input.Uid, got.Uid) + } + }) + } +} diff --git a/internal/logic/socialnetworkservice/get_followee_logic_test.go b/internal/logic/socialnetworkservice/get_followee_logic_test.go new file mode 100644 index 0000000..d356767 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_followee_logic_test.go @@ -0,0 +1,130 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain/repository" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestGetFolloweeLogic_GetFollowee(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + SocialNetworkRepository: mockSocialNetworkRepository, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.FollowReq + prepare func() + expectErr bool + wantResp *tweeting.FollowResp + }{ + { + name: "成功獲取我跟隨的名單", + input: &tweeting.FollowReq{ + Uid: "12345", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 GetFollowee 返回正確的結果 + mockSocialNetworkRepository.EXPECT().GetFollowee(gomock.Any(), repository.FollowReq{ + UID: "12345", + PageSize: 10, + PageIndex: 1, + }).Return(repository.FollowResp{ + UIDs: []string{"user1", "user2"}, + Total: 2, + }, nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.FollowResp{ + Uid: []string{"user1", "user2"}, + Page: &tweeting.Pager{ + Total: 2, + Index: 1, + Size: 10, + }, + }, + }, + { + name: "驗證失敗", + input: &tweeting.FollowReq{ + Uid: "", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "獲取我跟隨的名單失敗", + input: &tweeting.FollowReq{ + Uid: "12345", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 GetFollowee 返回錯誤 + mockSocialNetworkRepository.EXPECT().GetFollowee(gomock.Any(), repository.FollowReq{ + UID: "12345", + PageSize: 10, + PageIndex: 1, + }).Return(repository.FollowResp{}, errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 GetFolloweeLogic + logic := GetFolloweeLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 GetFollowee + got, err := logic.GetFollowee(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/socialnetworkservice/get_follower_count_logic_test.go b/internal/logic/socialnetworkservice/get_follower_count_logic_test.go new file mode 100644 index 0000000..b6f9924 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_follower_count_logic_test.go @@ -0,0 +1,108 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestGetFollowerCountLogic_GetFollowerCount(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + SocialNetworkRepository: mockSocialNetworkRepository, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.FollowCountReq + prepare func() + expectErr bool + wantResp *tweeting.FollowCountResp + }{ + { + name: "成功獲取跟隨者數量", + input: &tweeting.FollowCountReq{ + Uid: "12345", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 GetFollowerCount 返回正確的結果 + mockSocialNetworkRepository.EXPECT().GetFollowerCount(gomock.Any(), "12345").Return(int64(10), nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.FollowCountResp{ + Uid: "12345", + Total: 10, + }, + }, + { + name: "驗證失敗", + input: &tweeting.FollowCountReq{ + Uid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "獲取跟隨者數量失敗", + input: &tweeting.FollowCountReq{ + Uid: "12345", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 GetFollowerCount 返回錯誤 + mockSocialNetworkRepository.EXPECT().GetFollowerCount(gomock.Any(), "12345").Return(int64(0), errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 GetFollowerCountLogic + logic := GetFollowerCountLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 GetFollowerCount + got, err := logic.GetFollowerCount(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/socialnetworkservice/get_follower_logic_test.go b/internal/logic/socialnetworkservice/get_follower_logic_test.go new file mode 100644 index 0000000..249a7c4 --- /dev/null +++ b/internal/logic/socialnetworkservice/get_follower_logic_test.go @@ -0,0 +1,130 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain/repository" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestGetFollowerLogic_GetFollower(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + SocialNetworkRepository: mockSocialNetworkRepository, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.FollowReq + prepare func() + expectErr bool + wantResp *tweeting.FollowResp + }{ + { + name: "成功獲取跟隨者名單", + input: &tweeting.FollowReq{ + Uid: "12345", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 GetFollower 返回正確的結果 + mockSocialNetworkRepository.EXPECT().GetFollower(gomock.Any(), repository.FollowReq{ + UID: "12345", + PageSize: 10, + PageIndex: 1, + }).Return(repository.FollowResp{ + UIDs: []string{"user1", "user2"}, + Total: 2, + }, nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.FollowResp{ + Uid: []string{"user1", "user2"}, + Page: &tweeting.Pager{ + Total: 2, + Index: 1, + Size: 10, + }, + }, + }, + { + name: "驗證失敗", + input: &tweeting.FollowReq{ + Uid: "", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "獲取跟隨者名單失敗", + input: &tweeting.FollowReq{ + Uid: "12345", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬 GetFollower 返回錯誤 + mockSocialNetworkRepository.EXPECT().GetFollower(gomock.Any(), repository.FollowReq{ + UID: "12345", + PageSize: 10, + PageIndex: 1, + }).Return(repository.FollowResp{}, errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 GetFollowerLogic + logic := GetFollowerLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 GetFollower + got, err := logic.GetFollower(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/socialnetworkservice/mark_follow_relation_logic_test.go b/internal/logic/socialnetworkservice/mark_follow_relation_logic_test.go new file mode 100644 index 0000000..ec6163c --- /dev/null +++ b/internal/logic/socialnetworkservice/mark_follow_relation_logic_test.go @@ -0,0 +1,108 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestMarkFollowRelationLogic_MarkFollowRelation(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + SocialNetworkRepository: mockSocialNetworkRepository, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.DoFollowerRelationReq + prepare func() + expectErr bool + wantResp *tweeting.OKResp + }{ + { + name: "成功建立關注關係", + input: &tweeting.DoFollowerRelationReq{ + FollowerUid: "follower123", + FolloweeUid: "followee456", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功建立關注關係 + mockSocialNetworkRepository.EXPECT().MarkFollowerRelation(gomock.Any(), "follower123", "followee456").Return(nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.OKResp{}, + }, + { + name: "驗證失敗", + input: &tweeting.DoFollowerRelationReq{ + FollowerUid: "", + FolloweeUid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "建立關注關係失敗", + input: &tweeting.DoFollowerRelationReq{ + FollowerUid: "follower123", + FolloweeUid: "followee456", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬建立關注關係失敗 + mockSocialNetworkRepository.EXPECT().MarkFollowerRelation(gomock.Any(), "follower123", "followee456").Return(errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 MarkFollowRelationLogic + logic := MarkFollowRelationLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 MarkFollowRelation + got, err := logic.MarkFollowRelation(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/socialnetworkservice/remove_follow_relation_logic_test.go b/internal/logic/socialnetworkservice/remove_follow_relation_logic_test.go new file mode 100644 index 0000000..17b9484 --- /dev/null +++ b/internal/logic/socialnetworkservice/remove_follow_relation_logic_test.go @@ -0,0 +1,108 @@ +package socialnetworkservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestRemoveFollowRelationLogic_RemoveFollowRelation(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockSocialNetworkRepository := mockRepo.NewMockSocialNetworkRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + SocialNetworkRepository: mockSocialNetworkRepository, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.DoFollowerRelationReq + prepare func() + expectErr bool + wantResp *tweeting.OKResp + }{ + { + name: "成功取消關注", + input: &tweeting.DoFollowerRelationReq{ + FollowerUid: "follower123", + FolloweeUid: "followee456", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功刪除關注關係 + mockSocialNetworkRepository.EXPECT().RemoveFollowerRelation(gomock.Any(), "follower123", "followee456").Return(nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.OKResp{}, + }, + { + name: "驗證失敗", + input: &tweeting.DoFollowerRelationReq{ + FollowerUid: "", + FolloweeUid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "取消關注失敗", + input: &tweeting.DoFollowerRelationReq{ + FollowerUid: "follower123", + FolloweeUid: "followee456", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬刪除關注關係失敗 + mockSocialNetworkRepository.EXPECT().RemoveFollowerRelation(gomock.Any(), "follower123", "followee456").Return(errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 RemoveFollowRelationLogic + logic := RemoveFollowRelationLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 RemoveFollowRelation + got, err := logic.RemoveFollowRelation(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/mock/repository/social_network.go b/internal/mock/repository/social_network.go new file mode 100644 index 0000000..163a03d --- /dev/null +++ b/internal/mock/repository/social_network.go @@ -0,0 +1,174 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/domain/repository/social_network.go +// +// Generated by this command: +// +// mockgen -source=./internal/domain/repository/social_network.go -destination=./internal/mock/repository/social_network.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + repository "app-cloudep-tweeting-service/internal/domain/repository" + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockSocialNetworkRepository is a mock of SocialNetworkRepository interface. +type MockSocialNetworkRepository struct { + ctrl *gomock.Controller + recorder *MockSocialNetworkRepositoryMockRecorder +} + +// MockSocialNetworkRepositoryMockRecorder is the mock recorder for MockSocialNetworkRepository. +type MockSocialNetworkRepositoryMockRecorder struct { + mock *MockSocialNetworkRepository +} + +// NewMockSocialNetworkRepository creates a new mock instance. +func NewMockSocialNetworkRepository(ctrl *gomock.Controller) *MockSocialNetworkRepository { + mock := &MockSocialNetworkRepository{ctrl: ctrl} + mock.recorder = &MockSocialNetworkRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSocialNetworkRepository) EXPECT() *MockSocialNetworkRepositoryMockRecorder { + return m.recorder +} + +// CreateUserNode mocks base method. +func (m *MockSocialNetworkRepository) CreateUserNode(ctx context.Context, uid string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserNode", ctx, uid) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateUserNode indicates an expected call of CreateUserNode. +func (mr *MockSocialNetworkRepositoryMockRecorder) CreateUserNode(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserNode", reflect.TypeOf((*MockSocialNetworkRepository)(nil).CreateUserNode), ctx, uid) +} + +// GetDegreeBetweenUsers mocks base method. +func (m *MockSocialNetworkRepository) GetDegreeBetweenUsers(ctx context.Context, uid1, uid2 string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDegreeBetweenUsers", ctx, uid1, uid2) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDegreeBetweenUsers indicates an expected call of GetDegreeBetweenUsers. +func (mr *MockSocialNetworkRepositoryMockRecorder) GetDegreeBetweenUsers(ctx, uid1, uid2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDegreeBetweenUsers", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetDegreeBetweenUsers), ctx, uid1, uid2) +} + +// GetFollowee mocks base method. +func (m *MockSocialNetworkRepository) GetFollowee(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFollowee", ctx, req) + ret0, _ := ret[0].(repository.FollowResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFollowee indicates an expected call of GetFollowee. +func (mr *MockSocialNetworkRepositoryMockRecorder) GetFollowee(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowee", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFollowee), ctx, req) +} + +// GetFolloweeCount mocks base method. +func (m *MockSocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFolloweeCount", ctx, uid) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFolloweeCount indicates an expected call of GetFolloweeCount. +func (mr *MockSocialNetworkRepositoryMockRecorder) GetFolloweeCount(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFolloweeCount", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFolloweeCount), ctx, uid) +} + +// GetFollower mocks base method. +func (m *MockSocialNetworkRepository) GetFollower(ctx context.Context, req repository.FollowReq) (repository.FollowResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFollower", ctx, req) + ret0, _ := ret[0].(repository.FollowResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFollower indicates an expected call of GetFollower. +func (mr *MockSocialNetworkRepositoryMockRecorder) GetFollower(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollower", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFollower), ctx, req) +} + +// GetFollowerCount mocks base method. +func (m *MockSocialNetworkRepository) GetFollowerCount(ctx context.Context, uid string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFollowerCount", ctx, uid) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFollowerCount indicates an expected call of GetFollowerCount. +func (mr *MockSocialNetworkRepositoryMockRecorder) GetFollowerCount(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFollowerCount", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetFollowerCount), ctx, uid) +} + +// GetUIDsWithinNDegrees mocks base method. +func (m *MockSocialNetworkRepository) GetUIDsWithinNDegrees(ctx context.Context, uid string, degrees, pageSize, pageIndex int64) ([]string, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUIDsWithinNDegrees", ctx, uid, degrees, pageSize, pageIndex) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetUIDsWithinNDegrees indicates an expected call of GetUIDsWithinNDegrees. +func (mr *MockSocialNetworkRepositoryMockRecorder) GetUIDsWithinNDegrees(ctx, uid, degrees, pageSize, pageIndex any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUIDsWithinNDegrees", reflect.TypeOf((*MockSocialNetworkRepository)(nil).GetUIDsWithinNDegrees), ctx, uid, degrees, pageSize, pageIndex) +} + +// MarkFollowerRelation mocks base method. +func (m *MockSocialNetworkRepository) MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MarkFollowerRelation", ctx, fromUID, toUID) + ret0, _ := ret[0].(error) + return ret0 +} + +// MarkFollowerRelation indicates an expected call of MarkFollowerRelation. +func (mr *MockSocialNetworkRepositoryMockRecorder) MarkFollowerRelation(ctx, fromUID, toUID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkFollowerRelation", reflect.TypeOf((*MockSocialNetworkRepository)(nil).MarkFollowerRelation), ctx, fromUID, toUID) +} + +// RemoveFollowerRelation mocks base method. +func (m *MockSocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveFollowerRelation", ctx, fromUID, toUID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveFollowerRelation indicates an expected call of RemoveFollowerRelation. +func (mr *MockSocialNetworkRepositoryMockRecorder) RemoveFollowerRelation(ctx, fromUID, toUID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFollowerRelation", reflect.TypeOf((*MockSocialNetworkRepository)(nil).RemoveFollowerRelation), ctx, fromUID, toUID) +} diff --git a/internal/mock/repository/timeline.go b/internal/mock/repository/timeline.go new file mode 100644 index 0000000..91325f5 --- /dev/null +++ b/internal/mock/repository/timeline.go @@ -0,0 +1,113 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/domain/repository/timeline.go +// +// Generated by this command: +// +// mockgen -source=./internal/domain/repository/timeline.go -destination=./internal/mock/repository/timeline.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + repository "app-cloudep-tweeting-service/internal/domain/repository" + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockTimelineRepository is a mock of TimelineRepository interface. +type MockTimelineRepository struct { + ctrl *gomock.Controller + recorder *MockTimelineRepositoryMockRecorder +} + +// MockTimelineRepositoryMockRecorder is the mock recorder for MockTimelineRepository. +type MockTimelineRepositoryMockRecorder struct { + mock *MockTimelineRepository +} + +// NewMockTimelineRepository creates a new mock instance. +func NewMockTimelineRepository(ctrl *gomock.Controller) *MockTimelineRepository { + mock := &MockTimelineRepository{ctrl: ctrl} + mock.recorder = &MockTimelineRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTimelineRepository) EXPECT() *MockTimelineRepositoryMockRecorder { + return m.recorder +} + +// AddPost mocks base method. +func (m *MockTimelineRepository) AddPost(ctx context.Context, req repository.AddPostRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPost", ctx, req) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddPost indicates an expected call of AddPost. +func (mr *MockTimelineRepositoryMockRecorder) AddPost(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPost", reflect.TypeOf((*MockTimelineRepository)(nil).AddPost), ctx, req) +} + +// ClearNoMoreDataFlag mocks base method. +func (m *MockTimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClearNoMoreDataFlag", ctx, uid) + ret0, _ := ret[0].(error) + return ret0 +} + +// ClearNoMoreDataFlag indicates an expected call of ClearNoMoreDataFlag. +func (mr *MockTimelineRepositoryMockRecorder) ClearNoMoreDataFlag(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearNoMoreDataFlag", reflect.TypeOf((*MockTimelineRepository)(nil).ClearNoMoreDataFlag), ctx, uid) +} + +// FetchTimeline mocks base method. +func (m *MockTimelineRepository) FetchTimeline(ctx context.Context, req repository.FetchTimelineRequest) (repository.FetchTimelineResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchTimeline", ctx, req) + ret0, _ := ret[0].(repository.FetchTimelineResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchTimeline indicates an expected call of FetchTimeline. +func (mr *MockTimelineRepositoryMockRecorder) FetchTimeline(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTimeline", reflect.TypeOf((*MockTimelineRepository)(nil).FetchTimeline), ctx, req) +} + +// HasNoMoreData mocks base method. +func (m *MockTimelineRepository) HasNoMoreData(ctx context.Context, uid string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasNoMoreData", ctx, uid) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HasNoMoreData indicates an expected call of HasNoMoreData. +func (mr *MockTimelineRepositoryMockRecorder) HasNoMoreData(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasNoMoreData", reflect.TypeOf((*MockTimelineRepository)(nil).HasNoMoreData), ctx, uid) +} + +// SetNoMoreDataFlag mocks base method. +func (m *MockTimelineRepository) SetNoMoreDataFlag(ctx context.Context, uid string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetNoMoreDataFlag", ctx, uid) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetNoMoreDataFlag indicates an expected call of SetNoMoreDataFlag. +func (mr *MockTimelineRepositoryMockRecorder) SetNoMoreDataFlag(ctx, uid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNoMoreDataFlag", reflect.TypeOf((*MockTimelineRepository)(nil).SetNoMoreDataFlag), ctx, uid) +} diff --git a/internal/repository/social_network_test.go b/internal/repository/social_network_test.go new file mode 100644 index 0000000..50a4378 --- /dev/null +++ b/internal/repository/social_network_test.go @@ -0,0 +1 @@ +package repository From a72c0139825cc7ee45bc895816c9ef43ae06546b Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Tue, 3 Sep 2024 19:44:07 +0800 Subject: [PATCH 11/11] add timeline test --- .../timelineservice/add_post_logic_test.go | 137 +++++++++++++++++ .../clear_no_more_data_flag_logic_test.go | 105 +++++++++++++ .../fetch_timeline_logic_test.go | 140 ++++++++++++++++++ .../has_no_more_data_logic_test.go | 107 +++++++++++++ .../set_no_more_data_flag_logic_test.go | 105 +++++++++++++ 5 files changed, 594 insertions(+) create mode 100644 internal/logic/timelineservice/add_post_logic_test.go create mode 100644 internal/logic/timelineservice/clear_no_more_data_flag_logic_test.go create mode 100644 internal/logic/timelineservice/fetch_timeline_logic_test.go create mode 100644 internal/logic/timelineservice/has_no_more_data_logic_test.go create mode 100644 internal/logic/timelineservice/set_no_more_data_flag_logic_test.go diff --git a/internal/logic/timelineservice/add_post_logic_test.go b/internal/logic/timelineservice/add_post_logic_test.go new file mode 100644 index 0000000..7b15e24 --- /dev/null +++ b/internal/logic/timelineservice/add_post_logic_test.go @@ -0,0 +1,137 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain/repository" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestAddPostLogic_AddPost(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + TimelineRepo: mockTimelineRepo, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.AddPostToTimelineReq + prepare func() + expectErr bool + wantResp *tweeting.OKResp + }{ + { + name: "成功加入貼文", + input: &tweeting.AddPostToTimelineReq{ + Uid: "user123", + Posts: []*tweeting.PostTimelineItem{ + {PostId: "post1", CreatedAt: 1627890123}, + {PostId: "post2", CreatedAt: 1627890124}, + }, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功加入貼文 + mockTimelineRepo.EXPECT().AddPost(gomock.Any(), repository.AddPostRequest{ + UID: "user123", + PostItems: []repository.TimelineItem{ + {PostID: "post1", Score: 1627890123}, + {PostID: "post2", Score: 1627890124}, + }, + }).Return(nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.OKResp{}, + }, + { + name: "驗證失敗", + input: &tweeting.AddPostToTimelineReq{ + Uid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "沒有貼文資料", + input: &tweeting.AddPostToTimelineReq{ + Uid: "user123", + Posts: []*tweeting.PostTimelineItem{}, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.OKResp{}, + }, + { + name: "加入貼文失敗", + input: &tweeting.AddPostToTimelineReq{ + Uid: "user123", + Posts: []*tweeting.PostTimelineItem{ + {PostId: "post1", CreatedAt: 1627890123}, + }, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬加入貼文失敗 + mockTimelineRepo.EXPECT().AddPost(gomock.Any(), repository.AddPostRequest{ + UID: "user123", + PostItems: []repository.TimelineItem{ + {PostID: "post1", Score: 1627890123}, + }, + }).Return(errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 AddPostLogic + logic := AddPostLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 AddPost + got, err := logic.AddPost(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/timelineservice/clear_no_more_data_flag_logic_test.go b/internal/logic/timelineservice/clear_no_more_data_flag_logic_test.go new file mode 100644 index 0000000..7e18967 --- /dev/null +++ b/internal/logic/timelineservice/clear_no_more_data_flag_logic_test.go @@ -0,0 +1,105 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestClearNoMoreDataFlagLogic_ClearNoMoreDataFlag(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + TimelineRepo: mockTimelineRepo, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.DoNoMoreDataReq + prepare func() + expectErr bool + wantResp *tweeting.OKResp + }{ + { + name: "成功清除 NoMoreData 標誌", + input: &tweeting.DoNoMoreDataReq{ + Uid: "user123", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功清除 NoMoreData 標誌 + mockTimelineRepo.EXPECT().ClearNoMoreDataFlag(gomock.Any(), "user123").Return(nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.OKResp{}, + }, + { + name: "驗證失敗", + input: &tweeting.DoNoMoreDataReq{ + Uid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "清除 NoMoreData 標誌失敗", + input: &tweeting.DoNoMoreDataReq{ + Uid: "user123", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬清除 NoMoreData 標誌失敗 + mockTimelineRepo.EXPECT().ClearNoMoreDataFlag(gomock.Any(), "user123").Return(errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 ClearNoMoreDataFlagLogic + logic := ClearNoMoreDataFlagLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 ClearNoMoreDataFlag + got, err := logic.ClearNoMoreDataFlag(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/timelineservice/fetch_timeline_logic_test.go b/internal/logic/timelineservice/fetch_timeline_logic_test.go new file mode 100644 index 0000000..6e83be7 --- /dev/null +++ b/internal/logic/timelineservice/fetch_timeline_logic_test.go @@ -0,0 +1,140 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + "app-cloudep-tweeting-service/internal/domain/repository" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestFetchTimelineLogic_FetchTimeline(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + TimelineRepo: mockTimelineRepo, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.GetTimelineReq + prepare func() + expectErr bool + wantResp *tweeting.FetchTimelineResponse + }{ + { + name: "成功獲取動態時報", + input: &tweeting.GetTimelineReq{ + Uid: "user123", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功獲取動態時報 + mockTimelineRepo.EXPECT().FetchTimeline(gomock.Any(), repository.FetchTimelineRequest{ + UID: "user123", + PageSize: 10, + PageIndex: 1, + }).Return(repository.FetchTimelineResponse{ + Items: []repository.TimelineItem{ + {PostID: "post1", Score: 100}, + {PostID: "post2", Score: 200}, + }, + Page: tweeting.Pager{ + Total: 2, + Size: 10, + Index: 1, + }, + }, nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.FetchTimelineResponse{ + Posts: []*tweeting.FetchTimelineItem{ + {PostId: "post1", Score: 100}, + {PostId: "post2", Score: 200}, + }, + Page: &tweeting.Pager{ + Total: 2, + Index: 1, + Size: 10, + }, + }, + }, + { + name: "驗證失敗", + input: &tweeting.GetTimelineReq{ + Uid: "", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "獲取動態時報失敗", + input: &tweeting.GetTimelineReq{ + Uid: "user123", + PageSize: 10, + PageIndex: 1, + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬獲取動態時報失敗 + mockTimelineRepo.EXPECT().FetchTimeline(gomock.Any(), repository.FetchTimelineRequest{ + UID: "user123", + PageSize: 10, + PageIndex: 1, + }).Return(repository.FetchTimelineResponse{}, errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 FetchTimelineLogic + logic := FetchTimelineLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 FetchTimeline + got, err := logic.FetchTimeline(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/timelineservice/has_no_more_data_logic_test.go b/internal/logic/timelineservice/has_no_more_data_logic_test.go new file mode 100644 index 0000000..8f8bed7 --- /dev/null +++ b/internal/logic/timelineservice/has_no_more_data_logic_test.go @@ -0,0 +1,107 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestHasNoMoreDataLogic_HasNoMoreData(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + TimelineRepo: mockTimelineRepo, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.DoNoMoreDataReq + prepare func() + expectErr bool + wantResp *tweeting.HasNoMoreDataResp + }{ + { + name: "成功檢查時間線是否完整", + input: &tweeting.DoNoMoreDataReq{ + Uid: "user123", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功檢查時間線狀態 + mockTimelineRepo.EXPECT().HasNoMoreData(gomock.Any(), "user123").Return(true, nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.HasNoMoreDataResp{ + Status: true, + }, + }, + { + name: "驗證失敗", + input: &tweeting.DoNoMoreDataReq{ + Uid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "檢查時間線狀態失敗", + input: &tweeting.DoNoMoreDataReq{ + Uid: "user123", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬檢查時間線狀態失敗 + mockTimelineRepo.EXPECT().HasNoMoreData(gomock.Any(), "user123").Return(false, errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 HasNoMoreDataLogic + logic := HasNoMoreDataLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 HasNoMoreData + got, err := logic.HasNoMoreData(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +} diff --git a/internal/logic/timelineservice/set_no_more_data_flag_logic_test.go b/internal/logic/timelineservice/set_no_more_data_flag_logic_test.go new file mode 100644 index 0000000..d18c804 --- /dev/null +++ b/internal/logic/timelineservice/set_no_more_data_flag_logic_test.go @@ -0,0 +1,105 @@ +package timelineservicelogic + +import ( + "app-cloudep-tweeting-service/gen_result/pb/tweeting" + mocklib "app-cloudep-tweeting-service/internal/mock/lib" + mockRepo "app-cloudep-tweeting-service/internal/mock/repository" + "app-cloudep-tweeting-service/internal/svc" + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestSetNoMoreDataFlagLogic_SetNoMoreDataFlag(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockTimelineRepo := mockRepo.NewMockTimelineRepository(ctrl) + mockValidate := mocklib.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + TimelineRepo: mockTimelineRepo, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *tweeting.DoNoMoreDataReq + prepare func() + expectErr bool + wantResp *tweeting.OKResp + }{ + { + name: "成功標記時間線已完整", + input: &tweeting.DoNoMoreDataReq{ + Uid: "user123", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬成功標記時間線狀態 + mockTimelineRepo.EXPECT().SetNoMoreDataFlag(gomock.Any(), "user123").Return(nil).Times(1) + }, + expectErr: false, + wantResp: &tweeting.OKResp{}, + }, + { + name: "驗證失敗", + input: &tweeting.DoNoMoreDataReq{ + Uid: "", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + { + name: "標記時間線狀態失敗", + input: &tweeting.DoNoMoreDataReq{ + Uid: "user123", + }, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬標記時間線狀態失敗 + mockTimelineRepo.EXPECT().SetNoMoreDataFlag(gomock.Any(), "user123").Return(errors.New("repository error")).Times(1) + }, + expectErr: true, + wantResp: nil, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 SetNoMoreDataFlagLogic + logic := SetNoMoreDataFlagLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 SetNoMoreDataFlag + got, err := logic.SetNoMoreDataFlag(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantResp, got) + } + }) + } +}