CVE-2023-4127: Race Condition within a Thread (CWE-366)
value: Race Condition within a Thread in GitHub repository answerdev/answer prior to v1.1.1.
CWE description: If two threads of execution use a resource simultaneously, there exists the possibility that resources may be used while invalid, in turn making the state of execution undefined.
CVSS v3 base score: 5.9
Published: 2023-08-03T04:15Z
Repository: https://github.com/answerdev/answer
Language: Go
Files changed: cmd/wire_gen.go, internal/controller/vote_controller.go, internal/repo/activity/answer_repo.go, internal/repo/activity/follow_repo.go, internal/repo/activity/user_active_repo.go, internal/repo/activity/vote_repo.go, internal/repo/activity_common/activity_repo.go, internal/repo/activity_common/follow.go, internal/repo/provider.go, internal/repo/rank/user_rank_repo.go, internal/repo/user/user_repo.go, internal/schema/vote_schema.go, internal/service/activity/answer_activity.go, internal/service/activity_common/activity.go, internal/service/rank/rank_service.go, internal/service/vote_service.go
Commit date: 2023-06-30 18:18:22 +0800
Commit message: refactor(votes): refactor user vote repo

Diff: diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 0439c6385..70f316418 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -151,12 +151,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
 	rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService)
 	commentController := controller.NewCommentController(commentService, rankService)
 	reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
 	reportService := report2.NewReportService(reportRepo, objService)
 	reportController := controller.NewReportController(reportService, rankService)
-	serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configService, activityRepo, userRankRepo, voteRepo, notificationQueueService)
-	voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
+	serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
+	voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
 	voteController := controller.NewVoteController(voteService, rankService)
 	followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
 	tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService)
 	tagController := controller.NewTagController(tagService, tagCommonService, rankService)
 	followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
@@ -170,12 +170,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
 	metaService := meta2.NewMetaService(metaRepo)
 	questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, dataData)
 	collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon)
 	collectionController := controller.NewCollectionController(collectionService)
 	answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
-	questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
-	answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
+	answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo)
 	questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, activityQueueService, siteInfoCommonService)
 	answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, activityQueueService)
 	questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService)
 	answerController := controller.NewAnswerController(answerService, rankService)
 	searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go
index 00162c02f..3548e053e 100644
--- a/internal/controller/vote_controller.go
+++ b/internal/controller/vote_controller.go
@@ -8,11 +8,10 @@ import (
 	"github.com/answerdev/answer/internal/schema"
 	"github.com/answerdev/answer/internal/service"
 	"github.com/answerdev/answer/internal/service/rank"
 	"github.com/answerdev/answer/pkg/uid"
 	"github.com/gin-gonic/gin"
-	"github.com/jinzhu/copier"
 	"github.com/segmentfault/pacman/errors"
 )
 
 // VoteController activity controller
 type VoteController struct {
@@ -52,13 +51,11 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
 		msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank})
 		handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)
 		return
 	}
 
-	dto := &schema.VoteDTO{}
-	_ = copier.Copy(dto, req)
-	resp, err := vc.VoteService.VoteUp(ctx, dto)
+	resp, err := vc.VoteService.VoteUp(ctx, req)
 	if err != nil {
 		handler.HandleResponse(ctx, err, schema.ErrTypeToast)
 	} else {
 		handler.HandleResponse(ctx, err, resp)
 	}
@@ -91,13 +88,11 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
 		msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank})
 		handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil)
 		return
 	}
 
-	dto := &schema.VoteDTO{}
-	_ = copier.Copy(dto, req)
-	resp, err := vc.VoteService.VoteDown(ctx, dto)
+	resp, err := vc.VoteService.VoteDown(ctx, req)
 	if err != nil {
 		handler.HandleResponse(ctx, err, schema.ErrTypeToast)
 	} else {
 		handler.HandleResponse(ctx, err, resp)
 	}
diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go
index ae2441752..1e9bb643a 100644
--- a/internal/repo/activity/answer_repo.go
+++ b/internal/repo/activity/answer_repo.go
@@ -13,11 +13,10 @@ import (
 	"github.com/answerdev/answer/internal/service/activity_common"
 	"github.com/answerdev/answer/internal/service/notice_queue"
 	"github.com/answerdev/answer/internal/service/rank"
 	"github.com/answerdev/answer/pkg/converter"
 	"github.com/segmentfault/pacman/errors"
-	"github.com/segmentfault/pacman/log"
 	"xorm.io/xorm"
 )
 
 var (
 	acceptActionList = []string{constant.ActAccept, constant.ActAccepted}
@@ -44,83 +43,10 @@ func NewAnswerActivityRepo(
 		userRankRepo:             userRankRepo,
 		notificationQueueService: notificationQueueService,
 	}
 }
 
-// NewQuestionActivityRepo new repository
-func NewQuestionActivityRepo(
-	data *data.Data,
-	activityRepo activity_common.ActivityRepo,
-	userRankRepo rank.UserRankRepo,
-) activity.QuestionActivityRepo {
-	return &AnswerActivityRepo{
-		data:         data,
-		activityRepo: activityRepo,
-		userRankRepo: userRankRepo,
-	}
-}
-
-func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID string) (err error) {
-	questionInfo := &entity.Question{}
-	exist, err := ar.data.DB.Context(ctx).Where("id = ?", questionID).Get(questionInfo)
-	if err != nil {
-		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
-	}
-	if !exist {
-		return nil
-	}
-
-	// get all this object activity
-	activityList := make([]*entity.Activity, 0)
-	session := ar.data.DB.Context(ctx).Where("has_rank = 1")
-	session.Where("cancelled = ?", entity.ActivityAvailable)
-	err = session.Find(&activityList, &entity.Activity{ObjectID: questionID})
-	if err != nil {
-		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
-	}
-	if len(activityList) == 0 {
-		return nil
-	}
-
-	log.Infof("questionInfo %s deleted will rollback activity %d", questionID, len(activityList))
-
-	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
-		session = session.Context(ctx)
-		for _, act := range activityList {
-			log.Infof("user %s rollback rank %d", act.UserID, -act.Rank)
-			_, e := ar.userRankRepo.TriggerUserRank(
-				ctx, session, act.UserID, -act.Rank, act.ActivityType)
-			if e != nil {
-				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
-			}
-
-			if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
-				Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
-				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
-			}
-		}
-		return nil, nil
-	})
-	if err != nil {
-		return err
-	}
-
-	// get all answers
-	answerList := make([]*entity.Answer, 0)
-	err = ar.data.DB.Context(ctx).Find(&answerList, &entity.Answer{QuestionID: questionID})
-	if err != nil {
-		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
-	}
-	for _, answerInfo := range answerList {
-		err = ar.DeleteAnswer(ctx, answerInfo.ID)
-		if err != nil {
-			log.Error(err)
-		}
-	}
-	return
-}
-
 // AcceptAnswer accept other answer
 func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
 	answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool,
 ) (err error) {
 	addActivityList := make([]*entity.Activity, 0)
@@ -304,52 +230,5 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
 			ar.notificationQueueService.Send(ctx, msg)
 		}
 	}
 	return err
 }
-
-func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) (err error) {
-	answerInfo := &entity.Answer{}
-	exist, err := ar.data.DB.Context(ctx).Where("id = ?", answerID).Get(answerInfo)
-	if err != nil {
-		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
-	}
-	if !exist {
-		return nil
-	}
-
-	// get all this object activity
-	activityList := make([]*entity.Activity, 0)
-	session := ar.data.DB.Context(ctx).Where("has_rank = 1")
-	session.Where("cancelled = ?", entity.ActivityAvailable)
-	err = session.Find(&activityList, &entity.Activity{ObjectID: answerID})
-	if err != nil {
-		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
-	}
-	if len(activityList) == 0 {
-		return nil
-	}
-
-	log.Infof("answerInfo %s deleted will rollback activity %d", answerID, len(activityList))
-
-	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
-		session = session.Context(ctx)
-		for _, act := range activityList {
-			log.Infof("user %s rollback rank %d", act.UserID, -act.Rank)
-			_, e := ar.userRankRepo.TriggerUserRank(
-				ctx, session, act.UserID, -act.Rank, act.ActivityType)
-			if e != nil {
-				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
-			}
-
-			if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
-				Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
-				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
-			}
-		}
-		return nil, nil
-	})
-	if err != nil {
-		return err
-	}
-	return
-}
diff --git a/internal/repo/activity/follow_repo.go b/internal/repo/activity/follow_repo.go
index 8f9ade892..02f025cd7 100644
--- a/internal/repo/activity/follow_repo.go
+++ b/internal/repo/activity/follow_repo.go
@@ -41,11 +41,11 @@ func NewFollowRepo(
 func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error {
 	objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
 	if err != nil {
 		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 	}
-	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
+	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
 	if err != nil {
 		return err
 	}
 
 	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
@@ -108,11 +108,11 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
 func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) error {
 	objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
 	if err != nil {
 		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 	}
-	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
+	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
 	if err != nil {
 		return err
 	}
 
 	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
diff --git a/internal/repo/activity/user_active_repo.go b/internal/repo/activity/user_active_repo.go
index 0f6099d15..06c587c3d 100644
--- a/internal/repo/activity/user_active_repo.go
+++ b/internal/repo/activity/user_active_repo.go
@@ -1,9 +1,11 @@
 package activity
 
 import (
 	"context"
+	"fmt"
+	"xorm.io/builder"
 
 	"github.com/answerdev/answer/internal/base/data"
 	"github.com/answerdev/answer/internal/base/reason"
 	"github.com/answerdev/answer/internal/entity"
 	"github.com/answerdev/answer/internal/service/activity"
@@ -39,45 +41,60 @@ func NewUserActiveActivityRepo(
 		userRankRepo:  userRankRepo,
 		configService: configService,
 	}
 }
 
-// UserActive accept other answer
+// UserActive user active
 func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) {
 	cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated)
 	if err != nil {
 		return err
 	}
-	activityType := cfg.ID
-	deltaRank := cfg.GetIntValue()
 	addActivity := &entity.Activity{
 		UserID:           userID,
 		ObjectID:         "0",
 		OriginalObjectID: "0",
-		ActivityType:     activityType,
-		Rank:             deltaRank,
+		ActivityType:     cfg.ID,
+		Rank:             cfg.GetIntValue(),
 		HasRank:          1,
 	}
 
 	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
 		session = session.Context(ctx)
 
-		_, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType)
+		user := &entity.User{}
+		exist, err := session.ID(userID).ForUpdate().Get(user)
 		if err != nil {
-			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+			return nil, err
 		}
-		if exists {
+		if !exist {
+			return nil, fmt.Errorf("user not exist")
+		}
+
+		existsActivity := &entity.Activity{}
+		exist, err = session.
+			And(builder.Eq{"user_id": addActivity.UserID}).
+			And(builder.Eq{"activity_type": addActivity.ActivityType}).
+			Get(existsActivity)
+		if err != nil {
+			return nil, err
+		}
+		if exist {
 			return nil, nil
 		}
 
-		_, err = ar.userRankRepo.TriggerUserRank(ctx, session, addActivity.UserID, addActivity.Rank, activityType)
+		err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)
 		if err != nil {
-			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+			return nil, err
 		}
+
 		_, err = session.Insert(addActivity)
 		if err != nil {
-			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+			return nil, err
 		}
 		return nil, nil
 	})
-	return err
+	if err != nil {
+		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+	}
+	return nil
 }
diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go
index 22355235e..120d392b9 100644
--- a/internal/repo/activity/vote_repo.go
+++ b/internal/repo/activity/vote_repo.go
@@ -1,445 +1,422 @@
 package activity
 
 import (
 	"context"
-	"strings"
+	"fmt"
+	"github.com/segmentfault/pacman/log"
 	"time"
 
 	"github.com/answerdev/answer/internal/base/constant"
 	"github.com/answerdev/answer/internal/service/notice_queue"
 	"github.com/answerdev/answer/pkg/converter"
 
 	"github.com/answerdev/answer/internal/base/pager"
-	"github.com/answerdev/answer/internal/service/config"
 	"github.com/answerdev/answer/internal/service/rank"
 	"github.com/answerdev/answer/pkg/obj"
 
 	"xorm.io/builder"
 
-	"github.com/answerdev/answer/internal/service/activity_common"
-	"github.com/answerdev/answer/internal/service/unique"
-
 	"github.com/answerdev/answer/internal/base/data"
 	"github.com/answerdev/answer/internal/base/reason"
 	"github.com/answerdev/answer/internal/entity"
 	"github.com/answerdev/answer/internal/schema"
 	"github.com/answerdev/answer/internal/service"
+	"github.com/answerdev/answer/internal/service/activity_common"
 	"github.com/segmentfault/pacman/errors"
 	"xorm.io/xorm"
 )
 
 // VoteRepo activity repository
 type VoteRepo struct {
 	data                     *data.Data
-	uniqueIDRepo             unique.UniqueIDRepo
-	configService            *config.ConfigService
 	activityRepo             activity_common.ActivityRepo
 	userRankRepo             rank.UserRankRepo
-	voteCommon               activity_common.VoteRepo
 	notificationQueueService notice_queue.NotificationQueueService
 }
 
 // NewVoteRepo new repository
 func NewVoteRepo(
 	data *data.Data,
-	uniqueIDRepo unique.UniqueIDRepo,
-	configService *config.ConfigService,
 	activityRepo activity_common.ActivityRepo,
 	userRankRepo rank.UserRankRepo,
-	voteCommon activity_common.VoteRepo,
 	notificationQueueService notice_queue.NotificationQueueService,
 ) service.VoteRepo {
 	return &VoteRepo{
 		data:                     data,
-		uniqueIDRepo:             uniqueIDRepo,
-		configService:            configService,
 		activityRepo:             activityRepo,
 		userRankRepo:             userRankRepo,
-		voteCommon:               voteCommon,
 		notificationQueueService: notificationQueueService,
 	}
 }
 
-var LimitUpActions = map[string][]string{
-	"question": {"vote_up", "voted_up"},
-	"answer":   {"vote_up", "voted_up"},
-	"comment":  {"vote_up"},
-}
-
-var LimitDownActions = map[string][]string{
-	"question": {"vote_down", "voted_down"},
-	"answer":   {"vote_down", "voted_down"},
-	"comment":  {"vote_down"},
-}
+func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
+	noNeedToVote, err := vr.votePreCheck(ctx, op)
+	if err != nil {
+		return err
+	}
+	if noNeedToVote {
+		return nil
+	}
 
-func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
-	resp = &schema.VoteResp{}
-	achievementNotificationUserIDs := make([]string, 0)
 	sendInboxNotification := false
-	upVote := false
+	maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx)
+	if err != nil {
+		return err
+	}
+	var userIDs []string
+	for _, activity := range op.Activities {
+		userIDs = append(userIDs, activity.ActivityUserID)
+	}
+
 	_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
 		session = session.Context(ctx)
-		result = nil
-		for _, action := range actions {
-			var (
-				existsActivity entity.Activity
-				insertActivity entity.Activity
-				has            bool
-				triggerUserID,
-				activityUserID string
-				activityType, deltaRank, hasRank int
-			)
-
-			activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
-			if err != nil {
-				return
-			}
 
-			triggerUserID = userID
-			if userID == activityUserID {
-				triggerUserID = "0"
-			}
-
-			// check is voted up
-			has, _ = session.
-				Where(builder.Eq{"object_id": objectID}).
-				And(builder.Eq{"user_id": activityUserID}).
-				And(builder.Eq{"trigger_user_id": triggerUserID}).
-				And(builder.Eq{"activity_type": activityType}).
-				Get(&existsActivity)
-
-			// is is voted,return
-			if has && existsActivity.Cancelled == entity.ActivityAvailable {
-				return
-			}
-
-			insertActivity = entity.Activity{
-				ObjectID:         objectID,
-				OriginalObjectID: objectID,
-				UserID:           activityUserID,
-				TriggerUserID:    converter.StringToInt64(triggerUserID),
-				ActivityType:     activityType,
-				Rank:             deltaRank,
-				HasRank:          hasRank,
-				Cancelled:        entity.ActivityAvailable,
-			}
+		userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
+		if err != nil {
+			return nil, err
+		}
 
-			// trigger user rank and send notification
-			if hasRank != 0 {
-				var isReachStandard bool
-				isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType)
-				if err != nil {
-					return nil, err
-				}
-				if isReachStandard {
-					insertActivity.Rank = 0
-				}
-				achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
-			}
+		err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, maxDailyRank)
+		if err != nil {
+			return nil, err
+		}
 
-			if has {
-				if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
-					Update(&entity.Activity{
-						Cancelled: entity.ActivityAvailable,
-					}); err != nil {
-					return
-				}
-			} else {
-				_, err = session.Insert(&insertActivity)
-				if err != nil {
-					return nil, err
-				}
-				sendInboxNotification = true
-			}
+		sendInboxNotification, err = vr.saveActivitiesAvailable(session, op)
+		if err != nil {
+			return nil, err
+		}
 
-			// update votes
-			if action == constant.ActVoteDown || action == constant.ActVoteUp {
-				votes := 1
-				if action == constant.ActVoteDown {
-					upVote = false
-					votes = -1
-				} else {
-					upVote = true
-				}
-				err = vr.updateVotes(ctx, session, objectID, votes)
-				if err != nil {
-					return
-				}
-			}
+		err = vr.changeUserRank(ctx, session, op, userInfoMapping)
+		if err != nil {
+			return nil, err
 		}
-		return
+		return nil, nil
 	})
 	if err != nil {
-		return
+		return err
 	}
 
-	resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
-	resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
-
-	for _, activityUserID := range achievementNotificationUserIDs {
-		vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
+	for _, activity := range op.Activities {
+		if activity.Rank == 0 {
+			continue
+		}
+		vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
 	}
 	if sendInboxNotification {
-		vr.sendVoteInboxNotification(ctx, userID, objectUserID, objectID, upVote)
+		vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp)
 	}
-	return
+	return nil
 }
 
-func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
-	resp = &schema.VoteResp{}
-	notificationUserIDs := make([]string, 0)
+func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
+	// Pre-Check
+	// 1. check if the activity exist
+	// 2. check if the activity is not cancelled
+	// 3. if all activities are cancelled, return directly
+	activities, err := vr.getExistActivity(ctx, op)
+	if err != nil {
+		return err
+	}
+	var userIDs []string
+	for _, activity := range activities {
+		if activity.Cancelled == entity.ActivityCancelled {
+			continue
+		}
+		userIDs = append(userIDs, activity.UserID)
+	}
+	if len(userIDs) == 0 {
+		return nil
+	}
+
 	_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
 		session = session.Context(ctx)
-		for _, action := range actions {
-			var (
-				existsActivity entity.Activity
-				has            bool
-				triggerUserID,
-				activityUserID string
-				activityType,
-				deltaRank, hasRank int
-			)
-			result = nil
-
-			activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
-			if err != nil {
-				return
-			}
-
-			triggerUserID = userID
-			if userID == activityUserID {
-				triggerUserID = "0"
-			}
-
-			has, err = session.
-				Where(builder.Eq{"user_id": activityUserID}).
-				And(builder.Eq{"trigger_user_id": triggerUserID}).
-				And(builder.Eq{"activity_type": activityType}).
-				And(builder.Eq{"object_id": objectID}).
-				Get(&existsActivity)
-
-			if !has {
-				return
-			}
-
-			if existsActivity.Cancelled == entity.ActivityCancelled {
-				return
-			}
-
-			if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
-				Update(&entity.Activity{
-					Cancelled:   entity.ActivityCancelled,
-					CancelledAt: time.Now(),
-				}); err != nil {
-				return
-			}
 
-			// trigger user rank and send notification
-			if hasRank != 0 && existsActivity.Rank != 0 {
-				_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
-				if err != nil {
-					return
-				}
-				notificationUserIDs = append(notificationUserIDs, activityUserID)
-			}
+		userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
+		if err != nil {
+			return nil, err
+		}
 
-			// update votes
-			if action == "vote_down" || action == "vote_up" {
-				votes := -1
-				if action == "vote_down" {
-					votes = 1
-				}
-				err = vr.updateVotes(ctx, session, objectID, votes)
-				if err != nil {
-					return
-				}
-			}
+		err = vr.cancelActivities(session, activities)
+		if err != nil {
+			return nil, err
 		}
 
-		return
+		err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping)
+		if err != nil {
+			return nil, err
+		}
+		return nil, nil
 	})
 	if err != nil {
-		return
+		return err
 	}
-	resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
-	resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
 
-	for _, activityUserID := range notificationUserIDs {
-		vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
+	for _, activity := range activities {
+		if activity.Rank == 0 {
+			continue
+		}
+		vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID)
 	}
-	return
+	return nil
 }
 
-func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
-	resp = &schema.VoteResp{}
-	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
-	if err != nil {
-		err = errors.BadRequest(reason.ObjectNotFound)
-		return
-	}
+func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (
+	up, down int64, err error) {
+	up = vr.countVoteUp(ctx, objectID, objectType)
+	down = vr.countVoteDown(ctx, objectID, objectType)
+	err = vr.updateVotes(ctx, objectID, objectType, int(up-down))
+	return
+}
 
-	actions, ok := LimitUpActions[objectType]
-	if !ok {
-		err = errors.BadRequest(reason.DisallowVote)
-		return
-	}
+func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
+	page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {
+	session := vr.data.DB.Context(ctx)
+	cond := builder.
+		And(
+			builder.Eq{"user_id": userID},
+			builder.Eq{"cancelled": 0},
+			builder.In("activity_type", activityTypes),
+		)
 
-	_, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID)
-	return vr.vote(ctx, objectID, userID, objectUserID, actions)
-}
+	session.Where(cond).Desc("updated_at")
 
-func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
-	resp = &schema.VoteResp{}
-	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
+	total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
 	if err != nil {
-		err = errors.BadRequest(reason.ObjectNotFound)
-		return
-	}
-	actions, ok := LimitDownActions[objectType]
-	if !ok {
-		err = errors.BadRequest(reason.DisallowVote)
-		return
+		err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 	}
-
-	_, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID)
-	return vr.vote(ctx, objectID, userID, objectUserID, actions)
+	return
 }
 
-func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
-	var objectType string
-	resp = &schema.VoteResp{}
-
-	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
+func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
+	activities, err := vr.getExistActivity(ctx, op)
 	if err != nil {
-		err = errors.BadRequest(reason.ObjectNotFound)
-		return
+		return false, err
 	}
-	actions, ok := LimitUpActions[objectType]
-	if !ok {
-		err = errors.BadRequest(reason.DisallowVote)
-		return
+	done := 0
+	for _, activity := range activities {
+		if activity.Cancelled == entity.ActivityAvailable {
+			done++
+		}
 	}
-
-	return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
+	return done == len(op.Activities), nil
 }
 
-func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
-	var objectType string
-	resp = &schema.VoteResp{}
-
-	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
+func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {
+	us := make([]*entity.User, 0)
+	err := session.In("id", userIDs).ForUpdate().Find(&us)
 	if err != nil {
-		err = errors.BadRequest(reason.ObjectNotFound)
-		return
-	}
-	actions, ok := LimitDownActions[objectType]
-	if !ok {
-		err = errors.BadRequest(reason.DisallowVote)
-		return
+		log.Error(err)
+		return nil, err
 	}
 
-	return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
+	users := make(map[string]*entity.User, 0)
+	for _, u := range us {
+		users[u.ID] = u
+	}
+	return users, nil
 }
 
-func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) {
-	activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
-
-	if err != nil {
-		return
+func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session,
+	op *schema.VoteOperationInfo, maxDailyRank int) (err error) {
+	// check if user reach daily rank limit
+	for _, activity := range op.Activities {
+		reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)
+		if err != nil {
+			log.Error(err)
+			return err
+		}
+		if reach {
+			activity.Rank = 0
+		}
 	}
+	return nil
+}
 
-	activityUserID = userID
-	if strings.Contains(action, "voted") {
-		activityUserID = objectUserID
+func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session,
+	op *schema.VoteOperationInfo,
+	userInfoMapping map[string]*entity.User) (err error) {
+	for _, activity := range op.Activities {
+		if activity.Rank == 0 {
+			continue
+		}
+		user := userInfoMapping[activity.ActivityUserID]
+		if user == nil {
+			continue
+		}
+		if err = vr.userRankRepo.ChangeUserRank(ctx, session,
+			activity.ActivityUserID, user.Rank, activity.Rank); err != nil {
+			log.Error(err)
+			return err
+		}
 	}
-
-	return activityUserID, activityType, rank, hasRank, nil
+	return nil
 }
 
-func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) {
-	resp = &schema.VoteResp{}
-	for _, action := range []string{"vote_up", "vote_down"} {
-		var (
-			activity     entity.Activity
-			votes        int64
-			activityType int
-		)
-
-		activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
-
-		votes, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
-			And(builder.Eq{"activity_type": activityType}).
-			And(builder.Eq{"cancelled": 0}).
-			Count(&activity)
+func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
+	activities []*entity.Activity,
+	userInfoMapping map[string]*entity.User) (err error) {
+	for _, activity := range activities {
+		if activity.Rank == 0 {
+			continue
+		}
+		user := userInfoMapping[activity.UserID]
+		if user == nil {
+			continue
+		}
+		if err = vr.userRankRepo.ChangeUserRank(ctx, session,
+			activity.UserID, user.Rank, -activity.Rank); err != nil {
+			log.Error(err)
+			return err
+		}
+	}
+	return nil
+}
 
+// saveActivitiesAvailable save activities
+// If activity not exist it will be created or else will be updated
+// If this activity is already exist, set activity rank to 0
+// So after this function, the activity rank will be correct for update user rank
+func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) {
+	for _, activity := range op.Activities {
+		existsActivity := &entity.Activity{}
+		exist, err := session.
+			Where(builder.Eq{"object_id": op.ObjectID}).
+			And(builder.Eq{"user_id": activity.ActivityUserID}).
+			And(builder.Eq{"trigger_user_id": activity.TriggerUserID}).
+			And(builder.Eq{"activity_type": activity.ActivityType}).
+			Get(existsActivity)
 		if err != nil {
-			return
+			return false, err
 		}
-
-		if action == "vote_up" {
-			resp.UpVotes = int(votes)
+		if exist && existsActivity.Cancelled == entity.ActivityAvailable {
+			activity.Rank = 0
+			continue
+		}
+		if exist {
+			if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
+				Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); err != nil {
+				return false, err
+			}
 		} else {
-			resp.DownVotes = int(votes)
+			insertActivity := entity.Activity{
+				ObjectID:         op.ObjectID,
+				OriginalObjectID: op.ObjectID,
+				UserID:           activity.ActivityUserID,
+				TriggerUserID:    converter.StringToInt64(activity.TriggerUserID),
+				ActivityType:     activity.ActivityType,
+				Rank:             activity.Rank,
+				HasRank:          activity.HasRank(),
+				Cancelled:        entity.ActivityAvailable,
+			}
+			_, err = session.Insert(&insertActivity)
+			if err != nil {
+				return false, err
+			}
+			newAct = true
 		}
 	}
+	return newAct, nil
+}
 
-	resp.Votes = resp.UpVotes - resp.DownVotes
+// cancelActivities cancel activities
+// If this activity is already cancelled, set activity rank to 0
+// So after this function, the activity rank will be correct for update user rank
+func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {
+	for _, activity := range activities {
+		t := &entity.Activity{}
+		exist, err := session.ID(activity.ID).Get(t)
+		if err != nil {
+			log.Error(err)
+			return err
+		}
+		if !exist {
+			log.Error(fmt.Errorf("%s activity not exist", activity.ID))
+			return fmt.Errorf("%s activity not exist", activity.ID)
+		}
+		//  If this activity is already cancelled, set activity rank to 0
+		if t.Cancelled == entity.ActivityCancelled {
+			activity.Rank = 0
+		}
+		if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at").
+			Update(&entity.Activity{
+				Cancelled:   entity.ActivityCancelled,
+				CancelledAt: time.Now(),
+			}); err != nil {
+			log.Error(err)
+			return err
+		}
+	}
+	return nil
+}
 
-	return resp, nil
+func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) {
+	var activities []*entity.Activity
+	for _, action := range op.Activities {
+		t := &entity.Activity{}
+		exist, err := vr.data.DB.Context(ctx).
+			Where(builder.Eq{"user_id": action.ActivityUserID}).
+			And(builder.Eq{"trigger_user_id": action.TriggerUserID}).
+			And(builder.Eq{"activity_type": action.ActivityType}).
+			And(builder.Eq{"object_id": op.ObjectID}).
+			Get(t)
+		if err != nil {
+			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+		}
+		if exist {
+			activities = append(activities, t)
+		}
+	}
+	return activities, nil
 }
 
-func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
-	page int, pageSize int, activityTypes []int) (voteList []entity.Activity, total int64, err error) {
-	session := vr.data.DB.Context(ctx)
-	cond := builder.
-		And(
-			builder.Eq{"user_id": userID},
-			builder.Eq{"cancelled": 0},
-			builder.In("activity_type", activityTypes),
-		)
+func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) {
+	count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp)
+	if err != nil {
+		log.Errorf("get vote up count error: %v", err)
+	}
+	return count
+}
 
-	session.Where(cond).Desc("updated_at")
+func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) {
+	count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown)
+	if err != nil {
+		log.Errorf("get vote down count error: %v", err)
+	}
+	return count
+}
 
-	total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
+func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) {
+	activity := &entity.Activity{}
+	activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action)
+	count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
+		And(builder.Eq{"activity_type": activityType}).
+		And(builder.Eq{"cancelled": 0}).
+		Count(activity)
 	if err != nil {
 		err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 	}
-	return
+	return count, err
 }
 
-// updateVotes
-// if votes < 0 Decr object vote_count,otherwise Incr object vote_count
-func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) {
-	var (
-		objectType string
-		e          error
-	)
-
-	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
+func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) {
+	session := vr.data.DB.Context(ctx)
 	switch objectType {
-	case "question":
-		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{})
-	case "answer":
-		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{})
-	case "comment":
-		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{})
-	default:
-		e = errors.BadRequest(reason.DisallowVote)
+	case constant.QuestionObjectType:
+		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount})
+	case constant.AnswerObjectType:
+		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount})
+	case constant.CommentObjectType:
+		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount})
 	}
-
-	if e != nil {
-		err = e
-	} else if err != nil {
-		err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack()
+	if err != nil {
+		log.Error(err)
 	}
-
 	return
 }
 
-// sendNotification send rank triggered notification
-func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
+func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
 	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
 	if err != nil {
 		return
 	}
 
diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go
index db1af3d16..ca16e482d 100644
--- a/internal/repo/activity_common/activity_repo.go
+++ b/internal/repo/activity_common/activity_repo.go
@@ -39,16 +39,16 @@ func NewActivityRepo(
 	}
 }
 
 func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (
 	activityType, rank, hasRank int, err error) {
-	objectKey, err := obj.GetObjectTypeStrByObjectID(objectID)
+	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
 	if err != nil {
 		return
 	}
 
-	confKey := fmt.Sprintf("%s.%s", objectKey, action)
+	confKey := fmt.Sprintf("%s.%s", objectType, action)
 	cfg, err := ar.configService.GetConfigByKey(ctx, confKey)
 	if err != nil {
 		return
 	}
 	activityType, rank = cfg.ID, cfg.GetIntValue()
@@ -57,12 +57,12 @@ func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID str
 		hasRank = 1
 	}
 	return
 }
 
-func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) {
-	configKey := fmt.Sprintf("%s.%s", objectKey, action)
+func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) {
+	configKey := fmt.Sprintf("%s.%s", objectType, action)
 	cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
 	if err != nil {
 		return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 	}
 	return cfg.ID, nil
diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go
index 050982772..1c3f08a2c 100644
--- a/internal/repo/activity_common/follow.go
+++ b/internal/repo/activity_common/follow.go
@@ -72,11 +72,11 @@ func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (fol
 func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) {
 	objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
 	if err != nil {
 		return nil, err
 	}
-	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
+	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
 	if err != nil {
 		log.Errorf("can't get activity type by object key: %s", objectTypeStr)
 		return nil, err
 	}
 
@@ -94,11 +94,11 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us
 }
 
 // GetFollowIDs get all follow id list
 func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) {
 	followIDs = make([]string, 0)
-	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow")
+	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
 	if err != nil {
 		return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 	}
 	session := ar.data.DB.Context(ctx).Select("object_id")
 	session.Table(entity.Activity{}.TableName())
@@ -116,11 +116,11 @@ func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (
 	objectKey, err := obj.GetObjectTypeStrByObjectID(objectID)
 	if err != nil {
 		return false, err
 	}
 
-	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow")
+	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
 	if err != nil {
 		return false, err
 	}
 
 	at := &entity.Activity{}
diff --git a/internal/repo/provider.go b/internal/repo/provider.go
index 10722cecd..34fe1c1aa 100644
--- a/internal/repo/provider.go
+++ b/internal/repo/provider.go
@@ -50,11 +50,10 @@ var ProviderSetRepo = wire.NewSet(
 	answer.NewAnswerRepo,
 	activity_common.NewActivityRepo,
 	activity.NewVoteRepo,
 	activity.NewFollowRepo,
 	activity.NewAnswerActivityRepo,
-	activity.NewQuestionActivityRepo,
 	activity.NewUserActiveActivityRepo,
 	activity.NewActivityRepo,
 	tag.NewTagRepo,
 	tag_common.NewTagCommonRepo,
 	tag.NewTagRelRepo,
diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go
index 872769db8..257dec8e3 100644
--- a/internal/repo/rank/user_rank_repo.go
+++ b/internal/repo/rank/user_rank_repo.go
@@ -29,21 +29,68 @@ func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.
 		data:          data,
 		configService: configService,
 	}
 }
 
+func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) {
+	maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit")
+	if err != nil {
+		return 0, err
+	}
+	return maxDailyRank, nil
+}
+
+func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session,
+	userID string, maxDailyRank int) (
+	reach bool, err error) {
+	session.Where(builder.Eq{"user_id": userID})
+	session.Where(builder.Eq{"cancelled": 0})
+	session.Where(builder.Between{
+		Col:     "updated_at",
+		LessVal: now.BeginningOfDay(),
+		MoreVal: now.EndOfDay(),
+	})
+
+	earned, err := session.Sum(&entity.Activity{}, "`rank`")
+	if err != nil {
+		return false, err
+	}
+	if int(earned) <= maxDailyRank {
+		return false, nil
+	}
+	log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank)
+	return true, nil
+}
+
+// ChangeUserRank change user rank
+func (ur *UserRankRepo) ChangeUserRank(
+	ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) {
+	// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
+	if plugin.RankAgentEnabled() || deltaRank == 0 {
+		return nil
+	}
+
+	// If user rank is lower than 1 after this action, then user rank will be set to 1 only.
+	if deltaRank < 0 && userCurrentScore+deltaRank < 1 {
+		deltaRank = 1 - userCurrentScore
+	}
+
+	_, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // TriggerUserRank trigger user rank change
 // session is need provider, it means this action must be success or failure
 // if outer action is failed then this action is need rollback
 func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
 	session *xorm.Session, userID string, deltaRank int, activityType int,
 ) (isReachStandard bool, err error) {
 	// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
-	if plugin.RankAgentEnabled() {
-		return false, nil
-	}
-	if deltaRank == 0 {
+	if plugin.RankAgentEnabled() || deltaRank == 0 {
 		return false, nil
 	}
 
 	if deltaRank < 0 {
 		// if user rank is lower than 1 after this action, then user rank will be set to 1 only.
diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go
index 2ff22a5f7..9bf2f2eab 100644
--- a/internal/repo/user/user_repo.go
+++ b/internal/repo/user/user_repo.go
@@ -193,11 +193,11 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
 	return
 }
 
 func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
 	list := make([]*entity.User, 0)
-	err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
+	err := ur.data.DB.Context(ctx).Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
 	if err != nil {
 		err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
 		return list, err
 	}
 	tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
diff --git a/internal/schema/vote_schema.go b/internal/schema/vote_schema.go
index 93775c29f..e41f542fa 100644
--- a/internal/schema/vote_schema.go
+++ b/internal/schema/vote_schema.go
@@ -4,24 +4,48 @@ type VoteReq struct {
 	ObjectID string `validate:"required" form:"object_id" json:"object_id"`  //	 id
 	IsCancel bool   `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel
 	UserID   string `json:"-"`
 }
 
-type VoteDTO struct {
-	// object TagID
+type VoteResp struct {
+	UpVotes    int64  `json:"up_votes"`
+	DownVotes  int64  `json:"down_votes"`
+	Votes      int64  `json:"votes"`
+	VoteStatus string `json:"vote_status"`
+}
+
+// VoteOperationInfo vote operation info
+type VoteOperationInfo struct {
+	// operation object id
 	ObjectID string
-	// is cancel
-	IsCancel bool
-	// user TagID
-	UserID string
+	// question answer comment
+	ObjectType string
+	// object owner user id
+	ObjectCreatorUserID string
+	// operation user id
+	OperatingUserID string
+	// vote up
+	VoteUp bool
+	// vote down
+	VoteDown bool
+	// vote activity info
+	Activities []*VoteActivity
 }
 
-type VoteResp struct {
-	UpVotes    int    `json:"up_votes"`
-	DownVotes  int    `json:"down_votes"`
-	Votes      int    `json:"votes"`
-	VoteStatus string `json:"vote_status"`
+// VoteActivity vote activity
+type VoteActivity struct {
+	ActivityType   int
+	ActivityUserID string
+	TriggerUserID  string
+	Rank           int
+}
+
+func (v *VoteActivity) HasRank() int {
+	if v.Rank != 0 {
+		return 1
+	}
+	return 0
 }
 
 type GetVoteWithPageReq struct {
 	// page
 	Page int `validate:"omitempty,min=1" form:"page"`
@@ -29,26 +53,10 @@ type GetVoteWithPageReq struct {
 	PageSize int `validate:"omitempty,min=1" form:"page_size"`
 	// user id
 	UserID string `json:"-"`
 }
 
-type VoteQuestion struct {
-	// object ID
-	ID string `json:"id"`
-	// title
-	Title string `json:"title"`
-}
-
-type VoteAnswer struct {
-	// object ID
-	ID string `json:"id"`
-	// question ID
-	QuestionID string `json:"question_id"`
-	// title
-	Title string `json:"title"`
-}
-
 type GetVoteWithPageResp struct {
 	// create time
 	CreatedAt int64 `json:"created_at"`
 	// object id
 	ObjectID string `json:"object_id"`
diff --git a/internal/service/activity/answer_activity.go b/internal/service/activity/answer_activity.go
index 2a8e51d8e..2f44ea455 100644
--- a/internal/service/activity/answer_activity.go
+++ b/internal/service/activity/answer_activity.go
@@ -1,40 +1,29 @@
 package activity
 
 import (
 	"context"
-	"time"
-
-	"github.com/segmentfault/pacman/log"
 )
 
 // AnswerActivityRepo answer activity
 type AnswerActivityRepo interface {
 	AcceptAnswer(ctx context.Context,
 		answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error)
 	CancelAcceptAnswer(ctx context.Context,
 		answerObjID, questionObjID, questionUserID, answerUserID string) (err error)
-	DeleteAnswer(ctx context.Context, answerID string) (err error)
-}
-
-// QuestionActivityRepo answer activity
-type QuestionActivityRepo interface {
-	DeleteQuestion(ctx context.Context, questionID string) (err error)
 }
 
 // AnswerActivityService user service
 type AnswerActivityService struct {
-	answerActivityRepo   AnswerActivityRepo
-	questionActivityRepo QuestionActivityRepo
+	answerActivityRepo AnswerActivityRepo
 }
 
 // NewAnswerActivityService new comment service
 func NewAnswerActivityService(
-	answerActivityRepo AnswerActivityRepo, questionActivityRepo QuestionActivityRepo) *AnswerActivityService {
+	answerActivityRepo AnswerActivityRepo) *AnswerActivityService {
 	return &AnswerActivityService{
-		answerActivityRepo:   answerActivityRepo,
-		questionActivityRepo: questionActivityRepo,
+		answerActivityRepo: answerActivityRepo,
 	}
 }
 
 // AcceptAnswer accept answer change activity
 func (as *AnswerActivityService) AcceptAnswer(ctx context.Context,
@@ -45,33 +34,5 @@ func (as *AnswerActivityService) AcceptAnswer(ctx context.Context,
 // CancelAcceptAnswer cancel accept answer change activity
 func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,
 	answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {
 	return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID)
 }
-
-// DeleteAnswer delete answer change activity
-func (as *AnswerActivityService) DeleteAnswer(ctx context.Context, answerID string, createdAt time.Time,
-	voteCount int) (err error) {
-	if voteCount >= 3 {
-		log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", answerID, voteCount)
-		return nil
-	}
-	if createdAt.Before(time.Now().AddDate(0, 0, -60)) {
-		log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", answerID, createdAt.String())
-		return nil
-	}
-	return as.answerActivityRepo.DeleteAnswer(ctx, answerID)
-}
-
-// DeleteQuestion delete question change activity
-func (as *AnswerActivityService) DeleteQuestion(ctx context.Context, questionID string, createdAt time.Time,
-	voteCount int) (err error) {
-	if voteCount >= 3 {
-		log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", questionID, voteCount)
-		return nil
-	}
-	if createdAt.Before(time.Now().AddDate(0, 0, -60)) {
-		log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", questionID, createdAt.String())
-		return nil
-	}
-	return as.questionActivityRepo.DeleteQuestion(ctx, questionID)
-}
diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go
index 69e57482b..adfb55ad3 100644
--- a/internal/service/activity_common/activity.go
+++ b/internal/service/activity_common/activity.go
@@ -13,11 +13,11 @@ import (
 	"xorm.io/xorm"
 )
 
 type ActivityRepo interface {
 	GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error)
-	GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error)
+	GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error)
 	GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) (
 		existsActivity *entity.Activity, exist bool, err error)
 	GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)
 	GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error)
 	AddActivity(ctx context.Context, activity *entity.Activity) (err error)
diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go
index 307bc5915..2bf222a48 100644
--- a/internal/service/rank/rank_service.go
+++ b/internal/service/rank/rank_service.go
@@ -27,10 +27,14 @@ import (
 const (
 	PermissionPrefix = "rank."
 )
 
 type UserRankRepo interface {
+	GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error)
+	CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error)
+	ChangeUserRank(ctx context.Context, session *xorm.Session,
+		userID string, userCurrentScore, deltaRank int) (err error)
 	TriggerUserRank(ctx context.Context, session *xorm.Session, userId string, rank int, activityType int) (isReachStandard bool, err error)
 	UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error)
 }
 
 // RankService rank service
diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go
index 1d39ff991..61f7e31d2 100644
--- a/internal/service/vote_service.go
+++ b/internal/service/vote_service.go
@@ -1,9 +1,11 @@
 package service
 
 import (
 	"context"
+	"github.com/answerdev/answer/internal/service/activity_common"
+	"strings"
 
 	"github.com/answerdev/answer/internal/base/constant"
 	"github.com/answerdev/answer/internal/base/handler"
 	"github.com/answerdev/answer/internal/base/pager"
 	"github.com/answerdev/answer/internal/base/translator"
@@ -11,148 +13,135 @@ import (
 	"github.com/answerdev/answer/internal/service/activity_type"
 	"github.com/answerdev/answer/internal/service/comment_common"
 	"github.com/answerdev/answer/internal/service/config"
 	"github.com/answerdev/answer/internal/service/object_info"
 	"github.com/answerdev/answer/pkg/htmltext"
-	"github.com/answerdev/answer/pkg/obj"
 	"github.com/segmentfault/pacman/log"
 
 	"github.com/answerdev/answer/internal/base/reason"
 	"github.com/answerdev/answer/internal/schema"
 	answercommon "github.com/answerdev/answer/internal/service/answer_common"
 	questioncommon "github.com/answerdev/answer/internal/service/question_common"
-	"github.com/answerdev/answer/internal/service/unique"
 	"github.com/segmentfault/pacman/errors"
 )
 
 // VoteRepo activity repository
 type VoteRepo interface {
-	VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
-	VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
-	VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
-	VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
-	GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error)
+	Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
+	CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
+	GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error)
 	ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) (
-		voteList []entity.Activity, total int64, err error)
+		voteList []*entity.Activity, total int64, err error)
 }
 
 // VoteService user service
 type VoteService struct {
 	voteRepo          VoteRepo
-	UniqueIDRepo      unique.UniqueIDRepo
 	configService     *config.ConfigService
 	questionRepo      questioncommon.QuestionRepo
 	answerRepo        answercommon.AnswerRepo
 	commentCommonRepo comment_common.CommentCommonRepo
 	objectService     *object_info.ObjService
+	activityRepo      activity_common.ActivityRepo
 }
 
 func NewVoteService(
-	VoteRepo VoteRepo,
-	uniqueIDRepo unique.UniqueIDRepo,
+	voteRepo VoteRepo,
 	configService *config.ConfigService,
 	questionRepo questioncommon.QuestionRepo,
 	answerRepo answercommon.AnswerRepo,
 	commentCommonRepo comment_common.CommentCommonRepo,
 	objectService *object_info.ObjService,
 ) *VoteService {
 	return &VoteService{
-		voteRepo:          VoteRepo,
-		UniqueIDRepo:      uniqueIDRepo,
+		voteRepo:          voteRepo,
 		configService:     configService,
 		questionRepo:      questionRepo,
 		answerRepo:        answerRepo,
 		commentCommonRepo: commentCommonRepo,
 		objectService:     objectService,
 	}
 }
 
 // VoteUp vote up
-func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
-	voteResp = &schema.VoteResp{}
-
-	var objectUserID string
-
-	objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
+func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
+	objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
 	if err != nil {
-		return
+		return nil, err
 	}
+	// make object id must be decoded
+	objectInfo.ObjectID = req.ObjectID
 
 	// check user is voting self or not
-	if objectUserID == dto.UserID {
-		err = errors.BadRequest(reason.DisallowVoteYourSelf)
-		return
+	if objectInfo.ObjectCreatorUserID == req.UserID {
+		return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
 	}
 
-	if dto.IsCancel {
-		return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
+	voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)
+
+	// vote operation
+	if req.IsCancel {
+		err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
 	} else {
-		return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
+		// cancel vote down if exist
+		voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
+		err = vs.voteRepo.CancelVote(ctx, voteOperationInfo)
+		if err != nil {
+			return nil, err
+		}
+		err = vs.voteRepo.Vote(ctx, voteUpOperationInfo)
+	}
+
+	resp = &schema.VoteResp{}
+	resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
+	if err != nil {
+		log.Error(err)
+	}
+	resp.Votes = resp.UpVotes - resp.DownVotes
+	if !req.IsCancel {
+		resp.VoteStatus = constant.ActVoteUp
 	}
+	return resp, nil
 }
 
 // VoteDown vote down
-func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
-	voteResp = &schema.VoteResp{}
-
-	var objectUserID string
-
-	objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
+func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
+	objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
 	if err != nil {
-		return
+		return nil, err
 	}
+	// make object id must be decoded
+	objectInfo.ObjectID = req.ObjectID
 
 	// check user is voting self or not
-	if objectUserID == dto.UserID {
-		err = errors.BadRequest(reason.DisallowVoteYourSelf)
-		return
+	if objectInfo.ObjectCreatorUserID == req.UserID {
+		return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
 	}
 
-	if dto.IsCancel {
-		return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
+	// vote operation
+	voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
+	if req.IsCancel {
+		err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
 	} else {
-		return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
+		// cancel vote up if exist
+		err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo))
+		if err != nil {
+			return nil, err
+		}
+		err = vs.voteRepo.Vote(ctx, voteDownOperationInfo)
 	}
-}
-
-func (vs *VoteService) GetObjectUserID(ctx context.Context, objectID string) (userID string, err error) {
-	var objectKey string
-	objectKey, err = obj.GetObjectTypeStrByObjectID(objectID)
 
+	resp = &schema.VoteResp{}
+	resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
 	if err != nil {
-		err = nil
-		return
+		log.Error(err)
 	}
-
-	switch objectKey {
-	case "question":
-		object, has, e := vs.questionRepo.GetQuestion(ctx, objectID)
-		if e != nil || !has {
-			err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack()
-			return
-		}
-		userID = object.UserID
-	case "answer":
-		object, has, e := vs.answerRepo.GetAnswer(ctx, objectID)
-		if e != nil || !has {
-			err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack()
-			return
-		}
-		userID = object.UserID
-	case "comment":
-		object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID)
-		if e != nil || !has {
-			err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack()
-			return
-		}
-		userID = object.UserID
-	default:
-		err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack()
-		return
+	resp.Votes = resp.UpVotes - resp.DownVotes
+	if !req.IsCancel {
+		resp.VoteStatus = constant.ActVoteDown
 	}
-
-	return
+	return resp, nil
 }
 
 // ListUserVotes list user's votes
 func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) {
 	typeKeys := []string{
@@ -205,5 +194,63 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
 		}
 		votes = append(votes, item)
 	}
 	return pager.NewPageModel(total, votes), err
 }
+
+func (vs *VoteService) createVoteOperationInfo(ctx context.Context,
+	userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo {
+	// warp vote operation
+	voteOperationInfo := &schema.VoteOperationInfo{
+		ObjectID:            objectInfo.ObjectID,
+		ObjectType:          objectInfo.ObjectType,
+		ObjectCreatorUserID: objectInfo.ObjectCreatorUserID,
+		OperatingUserID:     userID,
+		VoteUp:              voteUp,
+		VoteDown:            !voteUp,
+	}
+	voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo)
+	return voteOperationInfo
+}
+
+func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) (
+	activities []*schema.VoteActivity) {
+	activities = make([]*schema.VoteActivity, 0)
+
+	var actions []string
+	switch op.ObjectType {
+	case constant.QuestionObjectType:
+		if op.VoteUp {
+			actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp}
+		} else {
+			actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown}
+		}
+	case constant.AnswerObjectType:
+		if op.VoteUp {
+			actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp}
+		} else {
+			actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown}
+		}
+	case constant.CommentObjectType:
+		actions = []string{activity_type.CommentVoteUp}
+	}
+
+	for _, action := range actions {
+		t := &schema.VoteActivity{}
+		cfg, err := vs.configService.GetConfigByKey(ctx, action)
+		if err != nil {
+			log.Warnf("get config by key error: %v", err)
+			continue
+		}
+		t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()
+
+		if strings.Contains(action, "voted") {
+			t.ActivityUserID = op.ObjectCreatorUserID
+			t.TriggerUserID = op.OperatingUserID
+		} else {
+			t.ActivityUserID = op.OperatingUserID
+			t.TriggerUserID = "0"
+		}
+		activities = append(activities, t)
+	}
+	return activities
+}

Vulnerable code: 	serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configService, activityRepo, userRankRepo, voteRepo, notificationQueueService)
	voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
	questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
	answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
	"github.com/jinzhu/copier"
	dto := &schema.VoteDTO{}
	_ = copier.Copy(dto, req)
	resp, err := vc.VoteService.VoteUp(ctx, dto)
	dto := &schema.VoteDTO{}
	_ = copier.Copy(dto, req)
	resp, err := vc.VoteService.VoteDown(ctx, dto)
	"github.com/segmentfault/pacman/log"
// NewQuestionActivityRepo new repository
func NewQuestionActivityRepo(
	data *data.Data,
	activityRepo activity_common.ActivityRepo,
	userRankRepo rank.UserRankRepo,
) activity.QuestionActivityRepo {
	return &AnswerActivityRepo{
		data:         data,
		activityRepo: activityRepo,
		userRankRepo: userRankRepo,
	}
}

func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID string) (err error) {
	questionInfo := &entity.Question{}
	exist, err := ar.data.DB.Context(ctx).Where("id = ?", questionID).Get(questionInfo)
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	if !exist {
		return nil
	}

	// get all this object activity
	activityList := make([]*entity.Activity, 0)
	session := ar.data.DB.Context(ctx).Where("has_rank = 1")
	session.Where("cancelled = ?", entity.ActivityAvailable)
	err = session.Find(&activityList, &entity.Activity{ObjectID: questionID})
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	if len(activityList) == 0 {
		return nil
	}

	log.Infof("questionInfo %s deleted will rollback activity %d", questionID, len(activityList))

	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
		session = session.Context(ctx)
		for _, act := range activityList {
			log.Infof("user %s rollback rank %d", act.UserID, -act.Rank)
			_, e := ar.userRankRepo.TriggerUserRank(
				ctx, session, act.UserID, -act.Rank, act.ActivityType)
			if e != nil {
				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
			}

			if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
				Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
			}
		}
		return nil, nil
	})
	if err != nil {
		return err
	}

	// get all answers
	answerList := make([]*entity.Answer, 0)
	err = ar.data.DB.Context(ctx).Find(&answerList, &entity.Answer{QuestionID: questionID})
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	for _, answerInfo := range answerList {
		err = ar.DeleteAnswer(ctx, answerInfo.ID)
		if err != nil {
			log.Error(err)
		}
	}
	return
}


func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) (err error) {
	answerInfo := &entity.Answer{}
	exist, err := ar.data.DB.Context(ctx).Where("id = ?", answerID).Get(answerInfo)
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	if !exist {
		return nil
	}

	// get all this object activity
	activityList := make([]*entity.Activity, 0)
	session := ar.data.DB.Context(ctx).Where("has_rank = 1")
	session.Where("cancelled = ?", entity.ActivityAvailable)
	err = session.Find(&activityList, &entity.Activity{ObjectID: answerID})
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	if len(activityList) == 0 {
		return nil
	}

	log.Infof("answerInfo %s deleted will rollback activity %d", answerID, len(activityList))

	_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
		session = session.Context(ctx)
		for _, act := range activityList {
			log.Infof("user %s rollback rank %d", act.UserID, -act.Rank)
			_, e := ar.userRankRepo.TriggerUserRank(
				ctx, session, act.UserID, -act.Rank, act.ActivityType)
			if e != nil {
				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
			}

			if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
				Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
				return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
			}
		}
		return nil, nil
	})
	if err != nil {
		return err
	}
	return
}
	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
// UserActive accept other answer
	activityType := cfg.ID
	deltaRank := cfg.GetIntValue()
		ActivityType:     activityType,
		Rank:             deltaRank,
		_, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType)
			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
		if exists {
		_, err = ar.userRankRepo.TriggerUserRank(ctx, session, addActivity.UserID, addActivity.Rank, activityType)
			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	return err
	"strings"
	"github.com/answerdev/answer/internal/service/config"
	"github.com/answerdev/answer/internal/service/activity_common"
	"github.com/answerdev/answer/internal/service/unique"

	uniqueIDRepo             unique.UniqueIDRepo
	configService            *config.ConfigService
	voteCommon               activity_common.VoteRepo
	uniqueIDRepo unique.UniqueIDRepo,
	configService *config.ConfigService,
	voteCommon activity_common.VoteRepo,
		uniqueIDRepo:             uniqueIDRepo,
		configService:            configService,
		voteCommon:               voteCommon,
var LimitUpActions = map[string][]string{
	"question": {"vote_up", "voted_up"},
	"answer":   {"vote_up", "voted_up"},
	"comment":  {"vote_up"},
}

var LimitDownActions = map[string][]string{
	"question": {"vote_down", "voted_down"},
	"answer":   {"vote_down", "voted_down"},
	"comment":  {"vote_down"},
}
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
	resp = &schema.VoteResp{}
	achievementNotificationUserIDs := make([]string, 0)
	upVote := false
		result = nil
		for _, action := range actions {
			var (
				existsActivity entity.Activity
				insertActivity entity.Activity
				has            bool
				triggerUserID,
				activityUserID string
				activityType, deltaRank, hasRank int
			)

			activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
			if err != nil {
				return
			}
			triggerUserID = userID
			if userID == activityUserID {
				triggerUserID = "0"
			}

			// check is voted up
			has, _ = session.
				Where(builder.Eq{"object_id": objectID}).
				And(builder.Eq{"user_id": activityUserID}).
				And(builder.Eq{"trigger_user_id": triggerUserID}).
				And(builder.Eq{"activity_type": activityType}).
				Get(&existsActivity)

			// is is voted,return
			if has && existsActivity.Cancelled == entity.ActivityAvailable {
				return
			}

			insertActivity = entity.Activity{
				ObjectID:         objectID,
				OriginalObjectID: objectID,
				UserID:           activityUserID,
				TriggerUserID:    converter.StringToInt64(triggerUserID),
				ActivityType:     activityType,
				Rank:             deltaRank,
				HasRank:          hasRank,
				Cancelled:        entity.ActivityAvailable,
			}
			// trigger user rank and send notification
			if hasRank != 0 {
				var isReachStandard bool
				isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType)
				if err != nil {
					return nil, err
				}
				if isReachStandard {
					insertActivity.Rank = 0
				}
				achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
			}
			if has {
				if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
					Update(&entity.Activity{
						Cancelled: entity.ActivityAvailable,
					}); err != nil {
					return
				}
			} else {
				_, err = session.Insert(&insertActivity)
				if err != nil {
					return nil, err
				}
				sendInboxNotification = true
			}
			// update votes
			if action == constant.ActVoteDown || action == constant.ActVoteUp {
				votes := 1
				if action == constant.ActVoteDown {
					upVote = false
					votes = -1
				} else {
					upVote = true
				}
				err = vr.updateVotes(ctx, session, objectID, votes)
				if err != nil {
					return
				}
			}
		return
		return
	resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
	resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)

	for _, activityUserID := range achievementNotificationUserIDs {
		vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
		vr.sendVoteInboxNotification(ctx, userID, objectUserID, objectID, upVote)
	return
func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
	resp = &schema.VoteResp{}
	notificationUserIDs := make([]string, 0)
		for _, action := range actions {
			var (
				existsActivity entity.Activity
				has            bool
				triggerUserID,
				activityUserID string
				activityType,
				deltaRank, hasRank int
			)
			result = nil

			activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
			if err != nil {
				return
			}

			triggerUserID = userID
			if userID == activityUserID {
				triggerUserID = "0"
			}

			has, err = session.
				Where(builder.Eq{"user_id": activityUserID}).
				And(builder.Eq{"trigger_user_id": triggerUserID}).
				And(builder.Eq{"activity_type": activityType}).
				And(builder.Eq{"object_id": objectID}).
				Get(&existsActivity)

			if !has {
				return
			}

			if existsActivity.Cancelled == entity.ActivityCancelled {
				return
			}

			if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
				Update(&entity.Activity{
					Cancelled:   entity.ActivityCancelled,
					CancelledAt: time.Now(),
				}); err != nil {
				return
			}
			// trigger user rank and send notification
			if hasRank != 0 && existsActivity.Rank != 0 {
				_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
				if err != nil {
					return
				}
				notificationUserIDs = append(notificationUserIDs, activityUserID)
			}
			// update votes
			if action == "vote_down" || action == "vote_up" {
				votes := -1
				if action == "vote_down" {
					votes = 1
				}
				err = vr.updateVotes(ctx, session, objectID, votes)
				if err != nil {
					return
				}
			}
		return
		return
	resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
	resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
	for _, activityUserID := range notificationUserIDs {
		vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
	return
func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
	resp = &schema.VoteResp{}
	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
	if err != nil {
		err = errors.BadRequest(reason.ObjectNotFound)
		return
	}
	actions, ok := LimitUpActions[objectType]
	if !ok {
		err = errors.BadRequest(reason.DisallowVote)
		return
	}
	_, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID)
	return vr.vote(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
	resp = &schema.VoteResp{}
	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
		err = errors.BadRequest(reason.ObjectNotFound)
		return
	}
	actions, ok := LimitDownActions[objectType]
	if !ok {
		err = errors.BadRequest(reason.DisallowVote)
		return

	_, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID)
	return vr.vote(ctx, objectID, userID, objectUserID, actions)
func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
	var objectType string
	resp = &schema.VoteResp{}

	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
		err = errors.BadRequest(reason.ObjectNotFound)
		return
	actions, ok := LimitUpActions[objectType]
	if !ok {
		err = errors.BadRequest(reason.DisallowVote)
		return

	return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
	var objectType string
	resp = &schema.VoteResp{}

	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
		err = errors.BadRequest(reason.ObjectNotFound)
		return
	}
	actions, ok := LimitDownActions[objectType]
	if !ok {
		err = errors.BadRequest(reason.DisallowVote)
		return
	return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) {
	activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)

	if err != nil {
		return
	activityUserID = userID
	if strings.Contains(action, "voted") {
		activityUserID = objectUserID

	return activityUserID, activityType, rank, hasRank, nil
func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) {
	resp = &schema.VoteResp{}
	for _, action := range []string{"vote_up", "vote_down"} {
		var (
			activity     entity.Activity
			votes        int64
			activityType int
		)

		activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)

		votes, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
			And(builder.Eq{"activity_type": activityType}).
			And(builder.Eq{"cancelled": 0}).
			Count(&activity)
			return

		if action == "vote_up" {
			resp.UpVotes = int(votes)
			resp.DownVotes = int(votes)
	resp.Votes = resp.UpVotes - resp.DownVotes
	return resp, nil
func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
	page int, pageSize int, activityTypes []int) (voteList []entity.Activity, total int64, err error) {
	session := vr.data.DB.Context(ctx)
	cond := builder.
		And(
			builder.Eq{"user_id": userID},
			builder.Eq{"cancelled": 0},
			builder.In("activity_type", activityTypes),
		)
	session.Where(cond).Desc("updated_at")
	total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
	return
// updateVotes
// if votes < 0 Decr object vote_count,otherwise Incr object vote_count
func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) {
	var (
		objectType string
		e          error
	)

	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
	case "question":
		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{})
	case "answer":
		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{})
	case "comment":
		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{})
	default:
		e = errors.BadRequest(reason.DisallowVote)

	if e != nil {
		err = e
	} else if err != nil {
		err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack()

// sendNotification send rank triggered notification
func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
	objectKey, err := obj.GetObjectTypeStrByObjectID(objectID)
	confKey := fmt.Sprintf("%s.%s", objectKey, action)
func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) {
	configKey := fmt.Sprintf("%s.%s", objectKey, action)
	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow")
	activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow")
	activity.NewQuestionActivityRepo,
	if plugin.RankAgentEnabled() {
		return false, nil
	}
	if deltaRank == 0 {
	err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
type VoteDTO struct {
	// object TagID
	// is cancel
	IsCancel bool
	// user TagID
	UserID string
type VoteResp struct {
	UpVotes    int    `json:"up_votes"`
	DownVotes  int    `json:"down_votes"`
	Votes      int    `json:"votes"`
	VoteStatus string `json:"vote_status"`
type VoteQuestion struct {
	// object ID
	ID string `json:"id"`
	// title
	Title string `json:"title"`
}

type VoteAnswer struct {
	// object ID
	ID string `json:"id"`
	// question ID
	QuestionID string `json:"question_id"`
	// title
	Title string `json:"title"`
}

	"time"

	"github.com/segmentfault/pacman/log"
	DeleteAnswer(ctx context.Context, answerID string) (err error)
}

// QuestionActivityRepo answer activity
type QuestionActivityRepo interface {
	DeleteQuestion(ctx context.Context, questionID string) (err error)
	answerActivityRepo   AnswerActivityRepo
	questionActivityRepo QuestionActivityRepo
	answerActivityRepo AnswerActivityRepo, questionActivityRepo QuestionActivityRepo) *AnswerActivityService {
		answerActivityRepo:   answerActivityRepo,
		questionActivityRepo: questionActivityRepo,

// DeleteAnswer delete answer change activity
func (as *AnswerActivityService) DeleteAnswer(ctx context.Context, answerID string, createdAt time.Time,
	voteCount int) (err error) {
	if voteCount >= 3 {
		log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", answerID, voteCount)
		return nil
	}
	if createdAt.Before(time.Now().AddDate(0, 0, -60)) {
		log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", answerID, createdAt.String())
		return nil
	}
	return as.answerActivityRepo.DeleteAnswer(ctx, answerID)
}

// DeleteQuestion delete question change activity
func (as *AnswerActivityService) DeleteQuestion(ctx context.Context, questionID string, createdAt time.Time,
	voteCount int) (err error) {
	if voteCount >= 3 {
		log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", questionID, voteCount)
		return nil
	}
	if createdAt.Before(time.Now().AddDate(0, 0, -60)) {
		log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", questionID, createdAt.String())
		return nil
	}
	return as.questionActivityRepo.DeleteQuestion(ctx, questionID)
}
	GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error)
	"github.com/answerdev/answer/pkg/obj"
	"github.com/answerdev/answer/internal/service/unique"
	VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
	VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
	VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
	VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
	GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error)
		voteList []entity.Activity, total int64, err error)
	UniqueIDRepo      unique.UniqueIDRepo
	VoteRepo VoteRepo,
	uniqueIDRepo unique.UniqueIDRepo,
		voteRepo:          VoteRepo,
		UniqueIDRepo:      uniqueIDRepo,
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
	voteResp = &schema.VoteResp{}

	var objectUserID string

	objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
		return
	if objectUserID == dto.UserID {
		err = errors.BadRequest(reason.DisallowVoteYourSelf)
		return
	if dto.IsCancel {
		return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
		return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
	voteResp = &schema.VoteResp{}

	var objectUserID string

	objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
		return
	if objectUserID == dto.UserID {
		err = errors.BadRequest(reason.DisallowVoteYourSelf)
		return
	if dto.IsCancel {
		return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
		return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
}

func (vs *VoteService) GetObjectUserID(ctx context.Context, objectID string) (userID string, err error) {
	var objectKey string
	objectKey, err = obj.GetObjectTypeStrByObjectID(objectID)
		err = nil
		return

	switch objectKey {
	case "question":
		object, has, e := vs.questionRepo.GetQuestion(ctx, objectID)
		if e != nil || !has {
			err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack()
			return
		}
		userID = object.UserID
	case "answer":
		object, has, e := vs.answerRepo.GetAnswer(ctx, objectID)
		if e != nil || !has {
			err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack()
			return
		}
		userID = object.UserID
	case "comment":
		object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID)
		if e != nil || !has {
			err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack()
			return
		}
		userID = object.UserID
	default:
		err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack()
		return

	return
Fixed code: 	serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
	voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
	answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo)
	resp, err := vc.VoteService.VoteUp(ctx, req)
	resp, err := vc.VoteService.VoteDown(ctx, req)
	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
	"fmt"
	"xorm.io/builder"
// UserActive user active
		ActivityType:     cfg.ID,
		Rank:             cfg.GetIntValue(),
		user := &entity.User{}
		exist, err := session.ID(userID).ForUpdate().Get(user)
			return nil, err
		if !exist {
			return nil, fmt.Errorf("user not exist")
		}

		existsActivity := &entity.Activity{}
		exist, err = session.
			And(builder.Eq{"user_id": addActivity.UserID}).
			And(builder.Eq{"activity_type": addActivity.ActivityType}).
			Get(existsActivity)
		if err != nil {
			return nil, err
		}
		if exist {
		err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)
			return nil, err

			return nil, err
	if err != nil {
		return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	}
	return nil
	"fmt"
	"github.com/segmentfault/pacman/log"
	"github.com/answerdev/answer/internal/service/activity_common"
func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
	noNeedToVote, err := vr.votePreCheck(ctx, op)
	if err != nil {
		return err
	}
	if noNeedToVote {
		return nil
	}
	maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx)
	if err != nil {
		return err
	}
	var userIDs []string
	for _, activity := range op.Activities {
		userIDs = append(userIDs, activity.ActivityUserID)
	}

		userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
		if err != nil {
			return nil, err
		}
		err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, maxDailyRank)
		if err != nil {
			return nil, err
		}
		sendInboxNotification, err = vr.saveActivitiesAvailable(session, op)
		if err != nil {
			return nil, err
		}
		err = vr.changeUserRank(ctx, session, op, userInfoMapping)
		if err != nil {
			return nil, err
		return nil, nil
		return err
	for _, activity := range op.Activities {
		if activity.Rank == 0 {
			continue
		}
		vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
		vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp)
	return nil
func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
	// Pre-Check
	// 1. check if the activity exist
	// 2. check if the activity is not cancelled
	// 3. if all activities are cancelled, return directly
	activities, err := vr.getExistActivity(ctx, op)
	if err != nil {
		return err
	}
	var userIDs []string
	for _, activity := range activities {
		if activity.Cancelled == entity.ActivityCancelled {
			continue
		}
		userIDs = append(userIDs, activity.UserID)
	}
	if len(userIDs) == 0 {
		return nil
	}

		userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
		if err != nil {
			return nil, err
		}
		err = vr.cancelActivities(session, activities)
		if err != nil {
			return nil, err
		err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping)
		if err != nil {
			return nil, err
		}
		return nil, nil
		return err
	for _, activity := range activities {
		if activity.Rank == 0 {
			continue
		}
		vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID)
	return nil
func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (
	up, down int64, err error) {
	up = vr.countVoteUp(ctx, objectID, objectType)
	down = vr.countVoteDown(ctx, objectID, objectType)
	err = vr.updateVotes(ctx, objectID, objectType, int(up-down))
	return
}
func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
	page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {
	session := vr.data.DB.Context(ctx)
	cond := builder.
		And(
			builder.Eq{"user_id": userID},
			builder.Eq{"cancelled": 0},
			builder.In("activity_type", activityTypes),
		)
	session.Where(cond).Desc("updated_at")
	total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
		err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
	return
func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
	activities, err := vr.getExistActivity(ctx, op)
		return false, err
	done := 0
	for _, activity := range activities {
		if activity.Cancelled == entity.ActivityAvailable {
			done++
		}
	return done == len(op.Activities), nil
func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {
	us := make([]*entity.User, 0)
	err := session.In("id", userIDs).ForUpdate().Find(&us)
		log.Error(err)
		return nil, err
	users := make(map[string]*entity.User, 0)
	for _, u := range us {
		users[u.ID] = u
	}
	return users, nil
func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session,
	op *schema.VoteOperationInfo, maxDailyRank int) (err error) {
	// check if user reach daily rank limit
	for _, activity := range op.Activities {
		reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)
		if err != nil {
			log.Error(err)
			return err
		}
		if reach {
			activity.Rank = 0
		}
	return nil
}
func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session,
	op *schema.VoteOperationInfo,
	userInfoMapping map[string]*entity.User) (err error) {
	for _, activity := range op.Activities {
		if activity.Rank == 0 {
			continue
		}
		user := userInfoMapping[activity.ActivityUserID]
		if user == nil {
			continue
		}
		if err = vr.userRankRepo.ChangeUserRank(ctx, session,
			activity.ActivityUserID, user.Rank, activity.Rank); err != nil {
			log.Error(err)
			return err
		}
	return nil
func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
	activities []*entity.Activity,
	userInfoMapping map[string]*entity.User) (err error) {
	for _, activity := range activities {
		if activity.Rank == 0 {
			continue
		}
		user := userInfoMapping[activity.UserID]
		if user == nil {
			continue
		}
		if err = vr.userRankRepo.ChangeUserRank(ctx, session,
			activity.UserID, user.Rank, -activity.Rank); err != nil {
			log.Error(err)
			return err
		}
	}
	return nil
}
// saveActivitiesAvailable save activities
// If activity not exist it will be created or else will be updated
// If this activity is already exist, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) {
	for _, activity := range op.Activities {
		existsActivity := &entity.Activity{}
		exist, err := session.
			Where(builder.Eq{"object_id": op.ObjectID}).
			And(builder.Eq{"user_id": activity.ActivityUserID}).
			And(builder.Eq{"trigger_user_id": activity.TriggerUserID}).
			And(builder.Eq{"activity_type": activity.ActivityType}).
			Get(existsActivity)
			return false, err
		if exist && existsActivity.Cancelled == entity.ActivityAvailable {
			activity.Rank = 0
			continue
		}
		if exist {
			if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
				Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); err != nil {
				return false, err
			}
			insertActivity := entity.Activity{
				ObjectID:         op.ObjectID,
				OriginalObjectID: op.ObjectID,
				UserID:           activity.ActivityUserID,
				TriggerUserID:    converter.StringToInt64(activity.TriggerUserID),
				ActivityType:     activity.ActivityType,
				Rank:             activity.Rank,
				HasRank:          activity.HasRank(),
				Cancelled:        entity.ActivityAvailable,
			}
			_, err = session.Insert(&insertActivity)
			if err != nil {
				return false, err
			}
			newAct = true
	return newAct, nil
}
// cancelActivities cancel activities
// If this activity is already cancelled, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {
	for _, activity := range activities {
		t := &entity.Activity{}
		exist, err := session.ID(activity.ID).Get(t)
		if err != nil {
			log.Error(err)
			return err
		}
		if !exist {
			log.Error(fmt.Errorf("%s activity not exist", activity.ID))
			return fmt.Errorf("%s activity not exist", activity.ID)
		}
		//  If this activity is already cancelled, set activity rank to 0
		if t.Cancelled == entity.ActivityCancelled {
			activity.Rank = 0
		}
		if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at").
			Update(&entity.Activity{
				Cancelled:   entity.ActivityCancelled,
				CancelledAt: time.Now(),
			}); err != nil {
			log.Error(err)
			return err
		}
	}
	return nil
}
func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) {
	var activities []*entity.Activity
	for _, action := range op.Activities {
		t := &entity.Activity{}
		exist, err := vr.data.DB.Context(ctx).
			Where(builder.Eq{"user_id": action.ActivityUserID}).
			And(builder.Eq{"trigger_user_id": action.TriggerUserID}).
			And(builder.Eq{"activity_type": action.ActivityType}).
			And(builder.Eq{"object_id": op.ObjectID}).
			Get(t)
		if err != nil {
			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
		}
		if exist {
			activities = append(activities, t)
		}
	}
	return activities, nil
func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) {
	count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp)
	if err != nil {
		log.Errorf("get vote up count error: %v", err)
	}
	return count
}
func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) {
	count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown)
	if err != nil {
		log.Errorf("get vote down count error: %v", err)
	}
	return count
}
func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) {
	activity := &entity.Activity{}
	activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action)
	count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
		And(builder.Eq{"activity_type": activityType}).
		And(builder.Eq{"cancelled": 0}).
		Count(activity)
	return count, err
func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) {
	session := vr.data.DB.Context(ctx)
	case constant.QuestionObjectType:
		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount})
	case constant.AnswerObjectType:
		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount})
	case constant.CommentObjectType:
		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount})
	if err != nil {
		log.Error(err)
func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
	confKey := fmt.Sprintf("%s.%s", objectType, action)
func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) {
	configKey := fmt.Sprintf("%s.%s", objectType, action)
	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
	activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) {
	maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit")
	if err != nil {
		return 0, err
	}
	return maxDailyRank, nil
}

func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session,
	userID string, maxDailyRank int) (
	reach bool, err error) {
	session.Where(builder.Eq{"user_id": userID})
	session.Where(builder.Eq{"cancelled": 0})
	session.Where(builder.Between{
		Col:     "updated_at",
		LessVal: now.BeginningOfDay(),
		MoreVal: now.EndOfDay(),
	})

	earned, err := session.Sum(&entity.Activity{}, "`rank`")
	if err != nil {
		return false, err
	}
	if int(earned) <= maxDailyRank {
		return false, nil
	}
	log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank)
	return true, nil
}

// ChangeUserRank change user rank
func (ur *UserRankRepo) ChangeUserRank(
	ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) {
	// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
	if plugin.RankAgentEnabled() || deltaRank == 0 {
		return nil
	}

	// If user rank is lower than 1 after this action, then user rank will be set to 1 only.
	if deltaRank < 0 && userCurrentScore+deltaRank < 1 {
		deltaRank = 1 - userCurrentScore
	}

	_, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{})
	if err != nil {
		return err
	}
	return nil
}

	if plugin.RankAgentEnabled() || deltaRank == 0 {
	err := ur.data.DB.Context(ctx).Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
type VoteResp struct {
	UpVotes    int64  `json:"up_votes"`
	DownVotes  int64  `json:"down_votes"`
	Votes      int64  `json:"votes"`
	VoteStatus string `json:"vote_status"`
}

// VoteOperationInfo vote operation info
type VoteOperationInfo struct {
	// operation object id
	// question answer comment
	ObjectType string
	// object owner user id
	ObjectCreatorUserID string
	// operation user id
	OperatingUserID string
	// vote up
	VoteUp bool
	// vote down
	VoteDown bool
	// vote activity info
	Activities []*VoteActivity
// VoteActivity vote activity
type VoteActivity struct {
	ActivityType   int
	ActivityUserID string
	TriggerUserID  string
	Rank           int
}

func (v *VoteActivity) HasRank() int {
	if v.Rank != 0 {
		return 1
	}
	return 0
	answerActivityRepo AnswerActivityRepo
	answerActivityRepo AnswerActivityRepo) *AnswerActivityService {
		answerActivityRepo: answerActivityRepo,
	GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error)
	GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error)
	CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error)
	ChangeUserRank(ctx context.Context, session *xorm.Session,
		userID string, userCurrentScore, deltaRank int) (err error)
	"github.com/answerdev/answer/internal/service/activity_common"
	"strings"
	Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
	CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
	GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error)
		voteList []*entity.Activity, total int64, err error)
	activityRepo      activity_common.ActivityRepo
	voteRepo VoteRepo,
		voteRepo:          voteRepo,
func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
	objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
		return nil, err
	// make object id must be decoded
	objectInfo.ObjectID = req.ObjectID
	if objectInfo.ObjectCreatorUserID == req.UserID {
		return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
	voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)

	// vote operation
	if req.IsCancel {
		err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
		// cancel vote down if exist
		voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
		err = vs.voteRepo.CancelVote(ctx, voteOperationInfo)
		if err != nil {
			return nil, err
		}
		err = vs.voteRepo.Vote(ctx, voteUpOperationInfo)
	}

	resp = &schema.VoteResp{}
	resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
	if err != nil {
		log.Error(err)
	}
	resp.Votes = resp.UpVotes - resp.DownVotes
	if !req.IsCancel {
		resp.VoteStatus = constant.ActVoteUp
	return resp, nil
func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
	objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
		return nil, err
	// make object id must be decoded
	objectInfo.ObjectID = req.ObjectID
	if objectInfo.ObjectCreatorUserID == req.UserID {
		return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
	// vote operation
	voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
	if req.IsCancel {
		err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
		// cancel vote up if exist
		err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo))
		if err != nil {
			return nil, err
		}
		err = vs.voteRepo.Vote(ctx, voteDownOperationInfo)
	resp = &schema.VoteResp{}
	resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
		log.Error(err)
	resp.Votes = resp.UpVotes - resp.DownVotes
	if !req.IsCancel {
		resp.VoteStatus = constant.ActVoteDown
	return resp, nil

func (vs *VoteService) createVoteOperationInfo(ctx context.Context,
	userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo {
	// warp vote operation
	voteOperationInfo := &schema.VoteOperationInfo{
		ObjectID:            objectInfo.ObjectID,
		ObjectType:          objectInfo.ObjectType,
		ObjectCreatorUserID: objectInfo.ObjectCreatorUserID,
		OperatingUserID:     userID,
		VoteUp:              voteUp,
		VoteDown:            !voteUp,
	}
	voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo)
	return voteOperationInfo
}

func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) (
	activities []*schema.VoteActivity) {
	activities = make([]*schema.VoteActivity, 0)

	var actions []string
	switch op.ObjectType {
	case constant.QuestionObjectType:
		if op.VoteUp {
			actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp}
		} else {
			actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown}
		}
	case constant.AnswerObjectType:
		if op.VoteUp {
			actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp}
		} else {
			actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown}
		}
	case constant.CommentObjectType:
		actions = []string{activity_type.CommentVoteUp}
	}

	for _, action := range actions {
		t := &schema.VoteActivity{}
		cfg, err := vs.configService.GetConfigByKey(ctx, action)
		if err != nil {
			log.Warnf("get config by key error: %v", err)
			continue
		}
		t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()

		if strings.Contains(action, "voted") {
			t.ActivityUserID = op.ObjectCreatorUserID
			t.TriggerUserID = op.OperatingUserID
		} else {
			t.ActivityUserID = op.OperatingUserID
			t.TriggerUserID = "0"
		}
		activities = append(activities, t)
	}
	return activities
}