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) }