This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/answer.git

commit ab04d85d7eadf792bb2049fd2dea8dd121d0adac
Author: hgaol <dhan...@hotmail.com>
AuthorDate: Mon Apr 21 21:52:33 2025 +0800

    feat: implement migration of followers and tag objects during tag merge
---
 internal/repo/activity_common/follow.go   | 110 +++++++++++++++++++++++-------
 internal/repo/tag/tag_rel_repo.go         |  18 +++++
 internal/service/tag/tag_service.go       |  14 +++-
 internal/service/tag_common/tag_common.go |  12 ++++
 pkg/obj/obj.go                            |  12 ++++
 5 files changed, 141 insertions(+), 25 deletions(-)

diff --git a/internal/repo/activity_common/follow.go 
b/internal/repo/activity_common/follow.go
index b7057227..e1765278 100644
--- a/internal/repo/activity_common/follow.go
+++ b/internal/repo/activity_common/follow.go
@@ -32,6 +32,7 @@ import (
        "github.com/segmentfault/pacman/errors"
        "github.com/segmentfault/pacman/log"
        "xorm.io/builder"
+       "xorm.io/xorm"
 )
 
 // FollowRepo follow repository
@@ -159,6 +160,7 @@ func (ar *FollowRepo) IsFollowed(ctx context.Context, 
userID, objectID string) (
        }
 }
 
+// MigrateFollowers migrate followers from source object to target object
 func (ar *FollowRepo) MigrateFollowers(ctx context.Context, sourceObjectID, 
targetObjectID, action string) error {
        // if source object id and target object id are same type
        sourceObjectTypeStr, err := 
obj.GetObjectTypeStrByObjectID(sourceObjectID)
@@ -177,30 +179,92 @@ func (ar *FollowRepo) MigrateFollowers(ctx 
context.Context, sourceObjectID, targ
                return err
        }
 
-       // 1. Construct the subquery using builder
-       subQueryBuilder := 
builder.Select("user_id").From(entity.Activity{}.TableName()).
-               Where(builder.Eq{
-                       "object_id":     targetObjectID,
-                       "activity_type": activityType,
-                       "cancelled":     entity.ActivityAvailable, // Ensure 
only active follows are considered
-               })
-
-       // 2. Use the subquery builder in the main query's Where clause
-       _, err = ar.data.DB.Context(ctx).Table(entity.Activity{}.TableName()).
-               Where(builder.Eq{
-                       "object_id":     sourceObjectID,
-                       "activity_type": activityType,
-               }).
-               And(builder.NotIn("user_id", subQueryBuilder)). // Pass the 
builder here
-               Update(&entity.Activity{
-                       ObjectID:  targetObjectID,
-                       UpdatedAt: time.Now(),
-               })
-
+       // 1. get all user ids who follow the source object
+       userIDs, err := ar.GetFollowUserIDs(ctx, sourceObjectID)
        if err != nil {
-               log.Errorf("MigrateFollowers: failed to update followers from 
%s to %s: %v", sourceObjectID, targetObjectID, err)
-               return 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+               log.Errorf("MigrateFollowers: failed to get user ids who follow 
%s: %v", sourceObjectID, err)
+               return err
        }
 
-       return nil
+       _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result 
any, err error) {
+               session = session.Context(ctx)
+               // 1. cancel all follows of the source object
+               _, err = session.Table(entity.Activity{}.TableName()).
+                       Where(builder.Eq{
+                               "object_id":     sourceObjectID,
+                               "activity_type": activityType,
+                       }).
+                       Cols("cancelled", "cancelled_at").
+                       Update(&entity.Activity{
+                               Cancelled:   entity.ActivityCancelled,
+                               CancelledAt: time.Now(),
+                       })
+               if err != nil {
+                       return nil, 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+               }
+
+               // 2. update cancel status to active for target tag if source 
tag followers is active
+               _, err = session.Table(entity.Activity{}.TableName()).
+                       Where(builder.Eq{
+                               "object_id":     targetObjectID,
+                               "activity_type": activityType,
+                       }).
+                       And(builder.In("user_id", userIDs)).
+                       Cols("cancelled", "cancelled_at").
+                       Update(&entity.Activity{
+                               Cancelled:   entity.ActivityAvailable,
+                               CancelledAt: time.Now(),
+                       })
+               if err != nil {
+                       return nil, 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+               }
+
+               // 3. get existing follows of the target object
+               targetFollowers := make([]string, 0)
+               err = session.Table(entity.Activity{}.TableName()).
+                       Where(builder.Eq{
+                               "object_id":     targetObjectID,
+                               "activity_type": activityType,
+                               "cancelled":     entity.ActivityAvailable,
+                       }).
+                       Cols("user_id").
+                       Find(&targetFollowers)
+               if err != nil {
+                       return nil, 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+               }
+
+               // 4. filter out user ids that already follow the target object 
and create new activity
+               // Create a map for faster lookup of existing followers
+               existingFollowers := make(map[string]bool)
+               for _, uid := range targetFollowers {
+                       existingFollowers[uid] = true
+               }
+
+               // Filter out users who already follow the target
+               newFollowers := make([]string, 0)
+               for _, uid := range userIDs {
+                       if !existingFollowers[uid] {
+                               newFollowers = append(newFollowers, uid)
+                       }
+               }
+
+               // Create new activities for the filtered users
+               for _, uid := range newFollowers {
+                       activity := &entity.Activity{
+                               UserID:           uid,
+                               ObjectID:         targetObjectID,
+                               OriginalObjectID: targetObjectID,
+                               ActivityType:     activityType,
+                               CreatedAt:        time.Now(),
+                               UpdatedAt:        time.Now(),
+                               Cancelled:        entity.ActivityAvailable,
+                       }
+                       if _, err = session.Insert(activity); err != nil {
+                               return nil, 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+                       }
+               }
+               return nil, nil
+       })
+
+       return err
 }
diff --git a/internal/repo/tag/tag_rel_repo.go 
b/internal/repo/tag/tag_rel_repo.go
index 565c6386..9b08177a 100644
--- a/internal/repo/tag/tag_rel_repo.go
+++ b/internal/repo/tag/tag_rel_repo.go
@@ -30,6 +30,7 @@ import (
        "github.com/apache/answer/internal/service/unique"
        "github.com/apache/answer/pkg/uid"
        "github.com/segmentfault/pacman/errors"
+       "xorm.io/builder"
 )
 
 // tagRelRepo tag rel repository
@@ -203,3 +204,20 @@ func (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx 
context.Context, obje
        }
        return entity.TagRelStatusAvailable, nil
 }
+
+// MigrateTagObjects migrate tag objects
+func (tr *tagRelRepo) MigrateTagObjects(ctx context.Context, sourceTagId, 
targetTagId, objectTypePrefix string) error {
+       _, err := tr.data.DB.Context(ctx).
+               Where("tag_id = ?", sourceTagId).
+               And("object_id LIKE ?", objectTypePrefix+"%").
+               And(builder.NotIn(
+                       "object_id", 
builder.Select("object_id").From(entity.TagRel{}.TableName()).
+                               Where(builder.Eq{"tag_id": targetTagId}).
+                               And(builder.Like{"object_id", objectTypePrefix 
+ "%"}),
+               )).
+               Update(&entity.TagRel{TagID: targetTagId})
+       if err != nil {
+               return 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+       }
+       return nil
+}
diff --git a/internal/service/tag/tag_service.go 
b/internal/service/tag/tag_service.go
index a5df5fe8..577199ec 100644
--- a/internal/service/tag/tag_service.go
+++ b/internal/service/tag/tag_service.go
@@ -476,10 +476,20 @@ func (ts *TagService) MergeTag(ctx context.Context, req 
*schema.MergeTagReq) (er
        }
 
        // 4. update tag followers
-       ts.followCommon.MigrateFollowers(ctx, sourceTag.ID, targetTagInfo.ID, 
"follow")
+       err = ts.followCommon.MigrateFollowers(ctx, sourceTag.ID, 
targetTagInfo.ID, "follow")
+       if err != nil {
+               return err
+       }
 
        // 5. update question tags
-       // todo, confirm whether transfer questions
+       err = ts.tagCommonService.MigrateTagQuestions(ctx, sourceTag.ID, 
targetTagInfo.ID)
+       if err != nil {
+               return err
+       }
+       err = ts.tagCommonService.RefreshTagQuestionCount(ctx, 
[]string{targetTagInfo.ID, sourceTag.ID})
+       if err != nil {
+               return err
+       }
 
        return nil
 }
diff --git a/internal/service/tag_common/tag_common.go 
b/internal/service/tag_common/tag_common.go
index df7c7ef2..aecfba42 100644
--- a/internal/service/tag_common/tag_common.go
+++ b/internal/service/tag_common/tag_common.go
@@ -35,6 +35,7 @@ import (
        "github.com/apache/answer/internal/service/revision_common"
        "github.com/apache/answer/internal/service/siteinfo_common"
        "github.com/apache/answer/pkg/converter"
+       "github.com/apache/answer/pkg/obj"
        "github.com/segmentfault/pacman/errors"
        "github.com/segmentfault/pacman/log"
 )
@@ -77,6 +78,7 @@ type TagRelRepo interface {
        BatchGetObjectTagRelList(ctx context.Context, objectIds []string) 
(tagListList []*entity.TagRel, err error)
        CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err 
error)
        GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) 
(status int, err error)
+       MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId, 
objectTypePrefix string) error
 }
 
 // TagCommonService user service
@@ -930,3 +932,13 @@ func (ts *TagCommonService) UpdateTag(ctx context.Context, 
req *schema.UpdateTag
 
        return
 }
+
+// MigrateTagQuestions migrate tag question
+func (ts *TagCommonService) MigrateTagQuestions(ctx context.Context, 
sourceTagID, targetTagID string) (err error) {
+       questionPrefix, err := 
obj.GetObjectIDPrefixByObjectType(constant.QuestionObjectType)
+       if err != nil {
+               return err
+       }
+
+       return ts.tagRelRepo.MigrateTagObjects(ctx, sourceTagID, targetTagID, 
questionPrefix)
+}
diff --git a/pkg/obj/obj.go b/pkg/obj/obj.go
index 0dc62819..37737d6d 100644
--- a/pkg/obj/obj.go
+++ b/pkg/obj/obj.go
@@ -20,6 +20,8 @@
 package obj
 
 import (
+       "strconv"
+
        "github.com/apache/answer/internal/base/constant"
        "github.com/apache/answer/internal/base/reason"
        "github.com/apache/answer/pkg/converter"
@@ -47,6 +49,16 @@ func GetObjectTypeNumberByObjectID(objectID string) 
(objectTypeNumber int, err e
        return converter.StringToInt(objectID[1:4]), nil
 }
 
+// GetObjectIDPrefixByObjectType get object id prefix by object type
+func GetObjectIDPrefixByObjectType(objectType string) (objectIDPrefix string, 
err error) {
+       objectTypeNumber, ok := constant.ObjectTypeStrMapping[objectType]
+       if !ok {
+               return "", errors.BadRequest(reason.ObjectNotFound)
+       }
+       objectIDPrefixNum := 0b1000 | objectTypeNumber
+       return strconv.FormatUint(uint64(objectIDPrefixNum), 2), nil
+}
+
 func checkObjectID(objectID string) (err error) {
        if len(objectID) < 5 {
                return errors.BadRequest(reason.ObjectNotFound)

Reply via email to