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" "github.com/zeromicro/go-zero/core/logx" "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 { //nolint:contextcheck session, err := s.neo4jClient.Conn() if err != nil { return err } defer session.Close(ctx) params := map[string]interface{}{ "uid": uid, } 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) { _ = run.Record().AsMap() } return nil } func (s *SocialNetworkRepository) MarkFollowerRelation(ctx context.Context, fromUID, toUID string) error { //nolint:contextcheck 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) { //nolint:contextcheck 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 uidList []string for run.Next(ctx) { record := run.Record() uid, ok := record.Get("uid") if ok { if uidStr, ok := uid.(string); ok { uidList = append(uidList, uidStr) } else { // TODO 可以印 log continue } } } total, err := s.GetFollowerCount(ctx, req.UID) if err != nil { return repository.FollowResp{}, err } return repository.FollowResp{ 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 } 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 uidList []string for run.Next(ctx) { record := run.Record() uid, ok := record.Get("uid") if ok { if uidStr, ok := uid.(string); ok { uidList = append(uidList, uidStr) } else { // 可以印 log continue } } } total, err := s.GetFolloweeCount(ctx, req.UID) if err != nil { return repository.FollowResp{}, err } return repository.FollowResp{ 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 } 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 { if dc, ok := followerCount.(int64); ok { count = dc } else { logx.Info("followerCount error") } } } return count, nil } func (s *SocialNetworkRepository) GetFolloweeCount(ctx context.Context, uid string) (int64, error) { //nolint:contextcheck 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 { if dc, ok := followeeCount.(int64); ok { count = dc } else { logx.Info("followeeCount error") } } } return count, nil } func (s *SocialNetworkRepository) RemoveFollowerRelation(ctx context.Context, fromUID, toUID string) error { //nolint:contextcheck 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) { //nolint:contextcheck 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 { if degreeValue, ok := deg.(int64); ok { degree = degreeValue } else { logx.Info("degree error") } } } return degree, nil } // 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 } 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 uidList []string for run.Next(ctx) { record := run.Record() uid, ok := record.Get("uid") if ok { if uidStr, ok := uid.(string); ok { uidList = append(uidList, uidStr) } else { // 可以印 log continue } } } // 計算總數 totalCount, err := s.getTotalUIDsWithinNDegrees(ctx, uid, degrees) if err != nil { return nil, 0, err } 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 } 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 { if countV, ok := count.(int64); ok { totalCount = countV } else { logx.Info("totalCount error") } } } return totalCount, nil }