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)