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 {