CVE-2023-4127 (Go, published 2023-08-03T04:15Z)
Description: value: Race Condition within a Thread in GitHub repository answerdev/answer prior to v1.1.1.
Severity: nan (CVSS v3 base score: 5.9)
Weakness: CWE-366 \u2014 Race Condition within a Thread
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.
Repository: https://github.com/answerdev/answer
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 message: refactor(votes): refactor user vote repo


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
}