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 6abf8222d7c0c41dd13fffe8645f4c4e1514b4d9
Author: hgaol <dhan...@hotmail.com>
AuthorDate: Fri Apr 18 21:20:54 2025 +0800

    feat: add MergeTag functionality to merge tags and update followers
---
 internal/controller/tag_controller.go          | 28 +++++++++++++++
 internal/repo/activity_common/follow.go        | 48 ++++++++++++++++++++++++++
 internal/router/answer_api_router.go           |  1 +
 internal/schema/tag_schema.go                  | 14 ++++++++
 internal/service/activity_common/follow.go     |  1 +
 internal/service/permission/permission_name.go |  1 +
 internal/service/tag/tag_service.go            | 48 ++++++++++++++++++++++++++
 7 files changed, 141 insertions(+)

diff --git a/internal/controller/tag_controller.go 
b/internal/controller/tag_controller.go
index 8326da1c..765497bc 100644
--- a/internal/controller/tag_controller.go
+++ b/internal/controller/tag_controller.go
@@ -355,3 +355,31 @@ func (tc *TagController) UpdateTagSynonym(ctx 
*gin.Context) {
        err = tc.tagService.UpdateTagSynonym(ctx, req)
        handler.HandleResponse(ctx, err, nil)
 }
+
+// MergeTag merge tag
+// @Summary merge tag
+// @Description merge tag
+// @Security ApiKeyAuth
+// @Tags Tag
+// @Accept json
+// @Produce json
+// @Param data body schema.AddTagReq true "tag"
+// @Success 200 {object} handler.RespBody
+// @Router /answer/api/v1/tag/merge [post]
+func (tc *TagController) MergeTag(ctx *gin.Context) {
+       req := &schema.MergeTagReq{}
+       if handler.BindAndCheck(ctx, req) {
+               return
+       }
+
+       isAdminModerator := middleware.GetUserIsAdminModerator(ctx)
+       if !isAdminModerator {
+               handler.HandleResponse(ctx, 
errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
+               return
+       }
+
+       req.UserID = middleware.GetLoginUserIDFromContext(ctx)
+       err := tc.tagService.MergeTag(ctx, req)
+
+       handler.HandleResponse(ctx, err, nil)
+}
diff --git a/internal/repo/activity_common/follow.go 
b/internal/repo/activity_common/follow.go
index ed364f89..b7057227 100644
--- a/internal/repo/activity_common/follow.go
+++ b/internal/repo/activity_common/follow.go
@@ -21,6 +21,7 @@ package activity_common
 
 import (
        "context"
+       "time"
 
        "github.com/apache/answer/internal/base/data"
        "github.com/apache/answer/internal/base/reason"
@@ -30,6 +31,7 @@ import (
        "github.com/apache/answer/pkg/obj"
        "github.com/segmentfault/pacman/errors"
        "github.com/segmentfault/pacman/log"
+       "xorm.io/builder"
 )
 
 // FollowRepo follow repository
@@ -156,3 +158,49 @@ func (ar *FollowRepo) IsFollowed(ctx context.Context, 
userID, objectID string) (
                return true, nil
        }
 }
+
+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)
+       if err != nil {
+               return 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+       }
+       targetObjectTypeStr, err := 
obj.GetObjectTypeStrByObjectID(targetObjectID)
+       if err != nil {
+               return 
errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+       }
+       if sourceObjectTypeStr != targetObjectTypeStr {
+               return 
errors.InternalServer(reason.DisallowFollow).WithMsg("not same object type")
+       }
+       activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, 
sourceObjectTypeStr, action)
+       if err != nil {
+               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(),
+               })
+
+       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()
+       }
+
+       return nil
+}
diff --git a/internal/router/answer_api_router.go 
b/internal/router/answer_api_router.go
index 7c6ab1f3..e01bd9a9 100644
--- a/internal/router/answer_api_router.go
+++ b/internal/router/answer_api_router.go
@@ -246,6 +246,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r 
*gin.RouterGroup) {
        r.POST("/tag/recover", a.tagController.RecoverTag)
        r.DELETE("/tag", a.tagController.RemoveTag)
        r.PUT("/tag/synonym", a.tagController.UpdateTagSynonym)
+       r.POST("/tag/merge", a.tagController.MergeTag)
 
        // collection
        r.POST("/collection/switch", a.collectionController.CollectionSwitch)
diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go
index c9c10d9c..b5a5835b 100644
--- a/internal/schema/tag_schema.go
+++ b/internal/schema/tag_schema.go
@@ -305,3 +305,17 @@ type GetTagBasicResp struct {
        Recommend   bool   `json:"recommend"`
        Reserved    bool   `json:"reserved"`
 }
+
+// MergeTagReq merge tag request
+type MergeTagReq struct {
+       // source tag id
+       SourceTagID string `validate:"required" json:"source_tag_id"`
+       // target tag id
+       TargetTagID string `validate:"required" json:"target_tag_id"`
+       // user id
+       UserID string `json:"-"`
+}
+
+// MergeTagResp merge tag response
+type MergeTagResp struct {
+}
diff --git a/internal/service/activity_common/follow.go 
b/internal/service/activity_common/follow.go
index 867b3990..6b031473 100644
--- a/internal/service/activity_common/follow.go
+++ b/internal/service/activity_common/follow.go
@@ -26,4 +26,5 @@ type FollowRepo interface {
        GetFollowAmount(ctx context.Context, objectID string) (followAmount 
int, err error)
        GetFollowUserIDs(ctx context.Context, objectID string) (userIDs 
[]string, err error)
        IsFollowed(ctx context.Context, userId, objectId string) (bool, error)
+       MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, 
action string) error
 }
diff --git a/internal/service/permission/permission_name.go 
b/internal/service/permission/permission_name.go
index 42a0938b..5c93e89e 100644
--- a/internal/service/permission/permission_name.go
+++ b/internal/service/permission/permission_name.go
@@ -53,6 +53,7 @@ const (
        TagEditWithoutReview        = "tag.edit_without_review"
        TagDelete                   = "tag.delete"
        TagSynonym                  = "tag.synonym"
+       TagMerge                    = "tag.merge"
        LinkUrlLimit                = "link.url_limit"
        VoteDetail                  = "vote.detail"
        AnswerAudit                 = "answer.audit"
diff --git a/internal/service/tag/tag_service.go 
b/internal/service/tag/tag_service.go
index 5d626ae2..8b614a7a 100644
--- a/internal/service/tag/tag_service.go
+++ b/internal/service/tag/tag_service.go
@@ -436,6 +436,54 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, 
req *schema.GetTagWith
        return pager.NewPageModel(total, resp), nil
 }
 
+// MergeTag merge tag
+func (ts *TagService) MergeTag(ctx context.Context, req *schema.MergeTagReq) 
(err error) {
+       // 1. get source tag and its synonyms
+       sourceTag, exist, err := ts.tagCommonService.GetTagByID(ctx, 
req.SourceTagID)
+       if err != nil {
+               return err
+       }
+       if !exist {
+               return errors.BadRequest(reason.TagNotFound)
+       }
+
+       sourceTagSynonyms, err := ts.tagRepo.GetTagList(ctx, 
&entity.Tag{MainTagID: converter.StringToInt64(sourceTag.ID)})
+       if err != nil {
+               return err
+       }
+
+       addSynonymTagList := make([]string, 0)
+       addSynonymTagList = append(addSynonymTagList, sourceTag.SlugName)
+       for _, tag := range sourceTagSynonyms {
+               addSynonymTagList = append(addSynonymTagList, tag.SlugName)
+       }
+
+       // 2. get target tag
+       targetTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, 
req.TargetTagID)
+       if err != nil {
+               return err
+       }
+       if !exist {
+               return errors.BadRequest(reason.TagNotFound)
+       }
+
+       // 3. update source tag and its synonyms as synonyms of target tag
+       if len(addSynonymTagList) > 0 {
+               err = ts.tagRepo.UpdateTagSynonym(ctx, addSynonymTagList, 
converter.StringToInt64(targetTagInfo.ID), targetTagInfo.SlugName)
+               if err != nil {
+                       return err
+               }
+       }
+
+       // 4. update tag followers
+       ts.followCommon.MigrateFollowers(ctx, sourceTag.ID, targetTagInfo.ID, 
"follow")
+
+       // 5. update question tags
+       // todo, confirm whether transfer questions
+
+       return nil
+}
+
 // checkTagIsFollow get tag list page
 func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID 
string) bool {
        if len(userID) == 0 {

Reply via email to