CVE-2023-4127 (published 2023-08-03T04:15Z)

Race Condition within a Thread in GitHub repository answerdev/answer prior to v1.1.1.

CVSS v3 base score: 5.9, MEDIUM (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-366
cwe_name: Race Condition within a Thread
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.
extended_description: 
            
             
			 
         
         

Fix information:
repo_url: https://github.com/answerdev/answer
commit_details: repo_url: https://github.com/answerdev/answer
author: LinkinStars
author_date: 2023-06-30 18:18:22+08:00
msg: refactor(votes): refactor user vote repo
repository: repo_url: https://github.com/answerdev/answer
repo_name: apache/incubator-answer
description: A Q&A platform software for teams at any scales. Whether it's a community forum, help center, or knowledge management platform, you can always count on Apache Answer.
repo_language: Go
owner: answerdev
file_changes: filename: wire_gen.go
change_type: ModificationType.MODIFY
diff: @@ -153,8 +153,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
 	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)
@@ -172,8 +172,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
 	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)

programming_language: Go
filename: vote_controller.go
change_type: ModificationType.MODIFY
diff: @@ -10,7 +10,6 @@ import (
 	"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"
 )
 
@@ -54,9 +53,7 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
 		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 {
@@ -93,9 +90,7 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
 		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 {

programming_language: Go
filename: answer_repo.go
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,6 @@ import (
 	"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"
 )
 
@@ -46,79 +45,6 @@ func NewAnswerActivityRepo(
 	}
 }
 
-// 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,
@@ -306,50 +232,3 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
 	}
 	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
-}

programming_language: Go
filename: follow_repo.go
change_type: ModificationType.MODIFY
diff: @@ -43,7 +43,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
 	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
 	}
@@ -110,7 +110,7 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string)
 	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
 	}

programming_language: Go
filename: user_active_repo.go
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,8 @@ package activity
 
 import (
 	"context"
+	"fmt"
+	"xorm.io/builder"
 
 	"github.com/answerdev/answer/internal/base/data"
 	"github.com/answerdev/answer/internal/base/reason"
@@ -41,43 +43,58 @@ func NewUserActiveActivityRepo(
 	}
 }
 
-// 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
 }

programming_language: Go
filename: vote_repo.go
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,8 @@ package activity
 
 import (
 	"context"
-	"strings"
+	"fmt"
+	"github.com/segmentfault/pacman/log"
 	"time"
 
 	"github.com/answerdev/answer/internal/base/constant"
@@ -10,20 +11,17 @@ import (
 	"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"
 )
@@ -31,413 +29,392 @@ import (
 // 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

programming_language: Go
filename: activity_repo.go
change_type: ModificationType.MODIFY
diff: @@ -41,12 +41,12 @@ 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
@@ -59,8 +59,8 @@ func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID str
 	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()

programming_language: Go
filename: follow.go
change_type: ModificationType.MODIFY
diff: @@ -74,7 +74,7 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us
 	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
@@ -96,7 +96,7 @@ 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()
 	}
@@ -118,7 +118,7 @@ func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (
 		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
 	}

programming_language: Go
filename: provider.go
change_type: ModificationType.MODIFY
diff: @@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet(
 	activity.NewVoteRepo,
 	activity.NewFollowRepo,
 	activity.NewAnswerActivityRepo,
-	activity.NewQuestionActivityRepo,
 	activity.NewUserActiveActivityRepo,
 	activity.NewActivityRepo,
 	tag.NewTagRepo,

programming_language: Go
filename: user_rank_repo.go
change_type: ModificationType.MODIFY
diff: @@ -31,6 +31,56 @@ func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.
 	}
 }
 
+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
@@ -38,10 +88,7 @@ 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
 	}
 

programming_language: Go
filename: user_repo.go
change_type: ModificationType.MODIFY
diff: @@ -195,7 +195,7 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
 
 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

programming_language: Go
filename: vote_schema.go
change_type: ModificationType.MODIFY
diff: @@ -6,20 +6,44 @@ type VoteReq struct {
 	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 {
@@ -31,22 +55,6 @@ type GetVoteWithPageReq struct {
 	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"`

programming_language: Go
filename: answer_activity.go
change_type: ModificationType.MODIFY
diff: @@ -2,9 +2,6 @@ package activity
 
 import (
 	"context"
-	"time"
-
-	"github.com/segmentfault/pacman/log"
 )
 
 // AnswerActivityRepo answer activity
@@ -13,26 +10,18 @@ type AnswerActivityRepo interface {
 		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,
 	}
 }
 
@@ -47,31 +36,3 @@ 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)
-}

programming_language: Go
filename: activity.go
change_type: ModificationType.MODIFY
diff: @@ -15,7 +15,7 @@ import (
 
 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)

programming_language: Go
filename: rank_service.go
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,10 @@ const (
 )
 
 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)
 }

programming_language: Go
filename: vote_service.go
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,8 @@ 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"
@@ -13,42 +15,37 @@ import (
 	"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,
@@ -56,8 +53,7 @@ func NewVoteService(
 	objectService *object_info.ObjService,
 ) *VoteService {
 	return &VoteService{
-		voteRepo:          VoteRepo,
-		UniqueIDRepo:      uniqueIDRepo,
+		voteRepo:          voteRepo,
 		configService:     configService,
 		questionRepo:      questionRepo,
 		answerRepo:        answerRepo,
@@ -67,90 +63,83 @@ func NewVoteService(
 }
 
 // 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
@@ -207,3 +196,61 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
 	}
 	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
+}

programming_language: Go