VYPR
Moderate severityNVD Advisory· Published Aug 3, 2023· Updated Oct 10, 2024

Race Condition within a Thread in answerdev/answer

CVE-2023-4127

Description

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

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Race condition in answerdev/answer before v1.1.1 allows thread-based exploitation due to improper synchronization in vote handling.

CVE-2023-4127 describes a race condition within a thread in the answerdev/answer Q&A platform, affecting versions prior to v1.1.1. The vulnerability resides in the vote handling logic, where concurrent access to shared resources without proper locking can lead to inconsistent state. A commit addressing the issue refactors the user vote repository to eliminate the race condition [1].

Exploitation of this race condition requires an attacker to trigger multiple simultaneous vote operations, possibly through rapid-fire requests. The attack surface is accessible to any authenticated user who can cast votes, as the race occurs during the vote update process. No special network position is needed beyond normal client access.

Successful exploitation could allow an attacker to manipulate vote counts or trigger unauthorized rank changes, compromising the integrity of the Q&A platform's scoring mechanisms. The vulnerability was reported via the huntr bug bounty platform [4].

Mitigation is straightforward: users should upgrade to answerdev/answer v1.1.1 or later, which includes the fix in commit 47661dc. The project recommends immediate patching to prevent potential vote tampering.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/answerdev/answerGo
< 1.1.11.1.1

Affected products

2

Patches

1
47661dc8a356

refactor(votes): refactor user vote repo

https://github.com/answerdev/answerLinkinStarsJun 30, 2023via ghsa
16 files changed · +552 619
  • cmd/wire_gen.go+3 4 modified
    @@ -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)
    
  • internal/controller/vote_controller.go+2 7 modified
    @@ -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 {
    
  • internal/repo/activity/answer_repo.go+0 121 modified
    @@ -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
    -}
    
  • internal/repo/activity_common/activity_repo.go+4 4 modified
    @@ -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()
    
  • internal/repo/activity_common/follow.go+3 3 modified
    @@ -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
     	}
    
  • internal/repo/activity/follow_repo.go+2 2 modified
    @@ -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
     	}
    
  • internal/repo/activity/user_active_repo.go+29 12 modified
    @@ -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
     }
    
  • internal/repo/activity/vote_repo.go+293 316 modified
    @@ -2,442 +2,419 @@ package activity
     
     import (
     	"context"
    -	"strings"
    +	"fmt"
    +	"github.com/segmentfault/pacman/log"
     	"time"
     
     	"github.com/answerdev/answer/internal/base/constant"
     	"github.com/answerdev/answer/internal/service/notice_queue"
     	"github.com/answerdev/answer/pkg/converter"
     
     	"github.com/answerdev/answer/internal/base/pager"
    -	"github.com/answerdev/answer/internal/service/config"
     	"github.com/answerdev/answer/internal/service/rank"
     	"github.com/answerdev/answer/pkg/obj"
     
     	"xorm.io/builder"
     
    -	"github.com/answerdev/answer/internal/service/activity_common"
    -	"github.com/answerdev/answer/internal/service/unique"
    -
     	"github.com/answerdev/answer/internal/base/data"
     	"github.com/answerdev/answer/internal/base/reason"
     	"github.com/answerdev/answer/internal/entity"
     	"github.com/answerdev/answer/internal/schema"
     	"github.com/answerdev/answer/internal/service"
    +	"github.com/answerdev/answer/internal/service/activity_common"
     	"github.com/segmentfault/pacman/errors"
     	"xorm.io/xorm"
     )
     
     // VoteRepo activity repository
     type VoteRepo struct {
     	data                     *data.Data
    -	uniqueIDRepo             unique.UniqueIDRepo
    -	configService            *config.ConfigService
     	activityRepo             activity_common.ActivityRepo
     	userRankRepo             rank.UserRankRepo
    -	voteCommon               activity_common.VoteRepo
     	notificationQueueService notice_queue.NotificationQueueService
     }
     
     // NewVoteRepo new repository
     func NewVoteRepo(
     	data *data.Data,
    -	uniqueIDRepo unique.UniqueIDRepo,
    -	configService *config.ConfigService,
     	activityRepo activity_common.ActivityRepo,
     	userRankRepo rank.UserRankRepo,
    -	voteCommon activity_common.VoteRepo,
     	notificationQueueService notice_queue.NotificationQueueService,
     ) service.VoteRepo {
     	return &VoteRepo{
     		data:                     data,
    -		uniqueIDRepo:             uniqueIDRepo,
    -		configService:            configService,
     		activityRepo:             activityRepo,
     		userRankRepo:             userRankRepo,
    -		voteCommon:               voteCommon,
     		notificationQueueService: notificationQueueService,
     	}
     }
     
    -var LimitUpActions = map[string][]string{
    -	"question": {"vote_up", "voted_up"},
    -	"answer":   {"vote_up", "voted_up"},
    -	"comment":  {"vote_up"},
    -}
    -
    -var LimitDownActions = map[string][]string{
    -	"question": {"vote_down", "voted_down"},
    -	"answer":   {"vote_down", "voted_down"},
    -	"comment":  {"vote_down"},
    -}
    +func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
    +	noNeedToVote, err := vr.votePreCheck(ctx, op)
    +	if err != nil {
    +		return err
    +	}
    +	if noNeedToVote {
    +		return nil
    +	}
     
    -func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
    -	resp = &schema.VoteResp{}
    -	achievementNotificationUserIDs := make([]string, 0)
     	sendInboxNotification := false
    -	upVote := false
    +	maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx)
    +	if err != nil {
    +		return err
    +	}
    +	var userIDs []string
    +	for _, activity := range op.Activities {
    +		userIDs = append(userIDs, activity.ActivityUserID)
    +	}
    +
     	_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
     		session = session.Context(ctx)
    -		result = nil
    -		for _, action := range actions {
    -			var (
    -				existsActivity entity.Activity
    -				insertActivity entity.Activity
    -				has            bool
    -				triggerUserID,
    -				activityUserID string
    -				activityType, deltaRank, hasRank int
    -			)
    -
    -			activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
    -			if err != nil {
    -				return
    -			}
     
    -			triggerUserID = userID
    -			if userID == activityUserID {
    -				triggerUserID = "0"
    -			}
    -
    -			// check is voted up
    -			has, _ = session.
    -				Where(builder.Eq{"object_id": objectID}).
    -				And(builder.Eq{"user_id": activityUserID}).
    -				And(builder.Eq{"trigger_user_id": triggerUserID}).
    -				And(builder.Eq{"activity_type": activityType}).
    -				Get(&existsActivity)
    -
    -			// is is voted,return
    -			if has && existsActivity.Cancelled == entity.ActivityAvailable {
    -				return
    -			}
    -
    -			insertActivity = entity.Activity{
    -				ObjectID:         objectID,
    -				OriginalObjectID: objectID,
    -				UserID:           activityUserID,
    -				TriggerUserID:    converter.StringToInt64(triggerUserID),
    -				ActivityType:     activityType,
    -				Rank:             deltaRank,
    -				HasRank:          hasRank,
    -				Cancelled:        entity.ActivityAvailable,
    -			}
    +		userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
    +		if err != nil {
    +			return nil, err
    +		}
     
    -			// trigger user rank and send notification
    -			if hasRank != 0 {
    -				var isReachStandard bool
    -				isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType)
    -				if err != nil {
    -					return nil, err
    -				}
    -				if isReachStandard {
    -					insertActivity.Rank = 0
    -				}
    -				achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
    -			}
    +		err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, maxDailyRank)
    +		if err != nil {
    +			return nil, err
    +		}
     
    -			if has {
    -				if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
    -					Update(&entity.Activity{
    -						Cancelled: entity.ActivityAvailable,
    -					}); err != nil {
    -					return
    -				}
    -			} else {
    -				_, err = session.Insert(&insertActivity)
    -				if err != nil {
    -					return nil, err
    -				}
    -				sendInboxNotification = true
    -			}
    +		sendInboxNotification, err = vr.saveActivitiesAvailable(session, op)
    +		if err != nil {
    +			return nil, err
    +		}
     
    -			// update votes
    -			if action == constant.ActVoteDown || action == constant.ActVoteUp {
    -				votes := 1
    -				if action == constant.ActVoteDown {
    -					upVote = false
    -					votes = -1
    -				} else {
    -					upVote = true
    -				}
    -				err = vr.updateVotes(ctx, session, objectID, votes)
    -				if err != nil {
    -					return
    -				}
    -			}
    +		err = vr.changeUserRank(ctx, session, op, userInfoMapping)
    +		if err != nil {
    +			return nil, err
     		}
    -		return
    +		return nil, nil
     	})
     	if err != nil {
    -		return
    +		return err
     	}
     
    -	resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
    -	resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
    -
    -	for _, activityUserID := range achievementNotificationUserIDs {
    -		vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
    +	for _, activity := range op.Activities {
    +		if activity.Rank == 0 {
    +			continue
    +		}
    +		vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
     	}
     	if sendInboxNotification {
    -		vr.sendVoteInboxNotification(ctx, userID, objectUserID, objectID, upVote)
    +		vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp)
     	}
    -	return
    +	return nil
     }
     
    -func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
    -	resp = &schema.VoteResp{}
    -	notificationUserIDs := make([]string, 0)
    +func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
    +	// Pre-Check
    +	// 1. check if the activity exist
    +	// 2. check if the activity is not cancelled
    +	// 3. if all activities are cancelled, return directly
    +	activities, err := vr.getExistActivity(ctx, op)
    +	if err != nil {
    +		return err
    +	}
    +	var userIDs []string
    +	for _, activity := range activities {
    +		if activity.Cancelled == entity.ActivityCancelled {
    +			continue
    +		}
    +		userIDs = append(userIDs, activity.UserID)
    +	}
    +	if len(userIDs) == 0 {
    +		return nil
    +	}
    +
     	_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
     		session = session.Context(ctx)
    -		for _, action := range actions {
    -			var (
    -				existsActivity entity.Activity
    -				has            bool
    -				triggerUserID,
    -				activityUserID string
    -				activityType,
    -				deltaRank, hasRank int
    -			)
    -			result = nil
    -
    -			activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
    -			if err != nil {
    -				return
    -			}
    -
    -			triggerUserID = userID
    -			if userID == activityUserID {
    -				triggerUserID = "0"
    -			}
    -
    -			has, err = session.
    -				Where(builder.Eq{"user_id": activityUserID}).
    -				And(builder.Eq{"trigger_user_id": triggerUserID}).
    -				And(builder.Eq{"activity_type": activityType}).
    -				And(builder.Eq{"object_id": objectID}).
    -				Get(&existsActivity)
    -
    -			if !has {
    -				return
    -			}
    -
    -			if existsActivity.Cancelled == entity.ActivityCancelled {
    -				return
    -			}
    -
    -			if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
    -				Update(&entity.Activity{
    -					Cancelled:   entity.ActivityCancelled,
    -					CancelledAt: time.Now(),
    -				}); err != nil {
    -				return
    -			}
     
    -			// trigger user rank and send notification
    -			if hasRank != 0 && existsActivity.Rank != 0 {
    -				_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
    -				if err != nil {
    -					return
    -				}
    -				notificationUserIDs = append(notificationUserIDs, activityUserID)
    -			}
    +		userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
    +		if err != nil {
    +			return nil, err
    +		}
     
    -			// update votes
    -			if action == "vote_down" || action == "vote_up" {
    -				votes := -1
    -				if action == "vote_down" {
    -					votes = 1
    -				}
    -				err = vr.updateVotes(ctx, session, objectID, votes)
    -				if err != nil {
    -					return
    -				}
    -			}
    +		err = vr.cancelActivities(session, activities)
    +		if err != nil {
    +			return nil, err
     		}
     
    -		return
    +		err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping)
    +		if err != nil {
    +			return nil, err
    +		}
    +		return nil, nil
     	})
     	if err != nil {
    -		return
    +		return err
     	}
    -	resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
    -	resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
     
    -	for _, activityUserID := range notificationUserIDs {
    -		vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
    +	for _, activity := range activities {
    +		if activity.Rank == 0 {
    +			continue
    +		}
    +		vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID)
     	}
    -	return
    +	return nil
     }
     
    -func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
    -	resp = &schema.VoteResp{}
    -	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
    -	if err != nil {
    -		err = errors.BadRequest(reason.ObjectNotFound)
    -		return
    -	}
    +func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (
    +	up, down int64, err error) {
    +	up = vr.countVoteUp(ctx, objectID, objectType)
    +	down = vr.countVoteDown(ctx, objectID, objectType)
    +	err = vr.updateVotes(ctx, objectID, objectType, int(up-down))
    +	return
    +}
     
    -	actions, ok := LimitUpActions[objectType]
    -	if !ok {
    -		err = errors.BadRequest(reason.DisallowVote)
    -		return
    -	}
    +func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
    +	page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {
    +	session := vr.data.DB.Context(ctx)
    +	cond := builder.
    +		And(
    +			builder.Eq{"user_id": userID},
    +			builder.Eq{"cancelled": 0},
    +			builder.In("activity_type", activityTypes),
    +		)
     
    -	_, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID)
    -	return vr.vote(ctx, objectID, userID, objectUserID, actions)
    -}
    +	session.Where(cond).Desc("updated_at")
     
    -func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
    -	resp = &schema.VoteResp{}
    -	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
    +	total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
     	if err != nil {
    -		err = errors.BadRequest(reason.ObjectNotFound)
    -		return
    -	}
    -	actions, ok := LimitDownActions[objectType]
    -	if !ok {
    -		err = errors.BadRequest(reason.DisallowVote)
    -		return
    +		err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
     	}
    -
    -	_, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID)
    -	return vr.vote(ctx, objectID, userID, objectUserID, actions)
    +	return
     }
     
    -func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
    -	var objectType string
    -	resp = &schema.VoteResp{}
    -
    -	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
    +func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
    +	activities, err := vr.getExistActivity(ctx, op)
     	if err != nil {
    -		err = errors.BadRequest(reason.ObjectNotFound)
    -		return
    +		return false, err
     	}
    -	actions, ok := LimitUpActions[objectType]
    -	if !ok {
    -		err = errors.BadRequest(reason.DisallowVote)
    -		return
    +	done := 0
    +	for _, activity := range activities {
    +		if activity.Cancelled == entity.ActivityAvailable {
    +			done++
    +		}
     	}
    -
    -	return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
    +	return done == len(op.Activities), nil
     }
     
    -func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
    -	var objectType string
    -	resp = &schema.VoteResp{}
    -
    -	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
    +func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {
    +	us := make([]*entity.User, 0)
    +	err := session.In("id", userIDs).ForUpdate().Find(&us)
     	if err != nil {
    -		err = errors.BadRequest(reason.ObjectNotFound)
    -		return
    -	}
    -	actions, ok := LimitDownActions[objectType]
    -	if !ok {
    -		err = errors.BadRequest(reason.DisallowVote)
    -		return
    +		log.Error(err)
    +		return nil, err
     	}
     
    -	return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
    +	users := make(map[string]*entity.User, 0)
    +	for _, u := range us {
    +		users[u.ID] = u
    +	}
    +	return users, nil
     }
     
    -func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) {
    -	activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
    -
    -	if err != nil {
    -		return
    +func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session,
    +	op *schema.VoteOperationInfo, maxDailyRank int) (err error) {
    +	// check if user reach daily rank limit
    +	for _, activity := range op.Activities {
    +		reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)
    +		if err != nil {
    +			log.Error(err)
    +			return err
    +		}
    +		if reach {
    +			activity.Rank = 0
    +		}
     	}
    +	return nil
    +}
     
    -	activityUserID = userID
    -	if strings.Contains(action, "voted") {
    -		activityUserID = objectUserID
    +func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session,
    +	op *schema.VoteOperationInfo,
    +	userInfoMapping map[string]*entity.User) (err error) {
    +	for _, activity := range op.Activities {
    +		if activity.Rank == 0 {
    +			continue
    +		}
    +		user := userInfoMapping[activity.ActivityUserID]
    +		if user == nil {
    +			continue
    +		}
    +		if err = vr.userRankRepo.ChangeUserRank(ctx, session,
    +			activity.ActivityUserID, user.Rank, activity.Rank); err != nil {
    +			log.Error(err)
    +			return err
    +		}
     	}
    -
    -	return activityUserID, activityType, rank, hasRank, nil
    +	return nil
     }
     
    -func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) {
    -	resp = &schema.VoteResp{}
    -	for _, action := range []string{"vote_up", "vote_down"} {
    -		var (
    -			activity     entity.Activity
    -			votes        int64
    -			activityType int
    -		)
    -
    -		activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
    -
    -		votes, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
    -			And(builder.Eq{"activity_type": activityType}).
    -			And(builder.Eq{"cancelled": 0}).
    -			Count(&activity)
    +func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
    +	activities []*entity.Activity,
    +	userInfoMapping map[string]*entity.User) (err error) {
    +	for _, activity := range activities {
    +		if activity.Rank == 0 {
    +			continue
    +		}
    +		user := userInfoMapping[activity.UserID]
    +		if user == nil {
    +			continue
    +		}
    +		if err = vr.userRankRepo.ChangeUserRank(ctx, session,
    +			activity.UserID, user.Rank, -activity.Rank); err != nil {
    +			log.Error(err)
    +			return err
    +		}
    +	}
    +	return nil
    +}
     
    +// saveActivitiesAvailable save activities
    +// If activity not exist it will be created or else will be updated
    +// If this activity is already exist, set activity rank to 0
    +// So after this function, the activity rank will be correct for update user rank
    +func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) {
    +	for _, activity := range op.Activities {
    +		existsActivity := &entity.Activity{}
    +		exist, err := session.
    +			Where(builder.Eq{"object_id": op.ObjectID}).
    +			And(builder.Eq{"user_id": activity.ActivityUserID}).
    +			And(builder.Eq{"trigger_user_id": activity.TriggerUserID}).
    +			And(builder.Eq{"activity_type": activity.ActivityType}).
    +			Get(existsActivity)
     		if err != nil {
    -			return
    +			return false, err
     		}
    -
    -		if action == "vote_up" {
    -			resp.UpVotes = int(votes)
    +		if exist && existsActivity.Cancelled == entity.ActivityAvailable {
    +			activity.Rank = 0
    +			continue
    +		}
    +		if exist {
    +			if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
    +				Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); err != nil {
    +				return false, err
    +			}
     		} else {
    -			resp.DownVotes = int(votes)
    +			insertActivity := entity.Activity{
    +				ObjectID:         op.ObjectID,
    +				OriginalObjectID: op.ObjectID,
    +				UserID:           activity.ActivityUserID,
    +				TriggerUserID:    converter.StringToInt64(activity.TriggerUserID),
    +				ActivityType:     activity.ActivityType,
    +				Rank:             activity.Rank,
    +				HasRank:          activity.HasRank(),
    +				Cancelled:        entity.ActivityAvailable,
    +			}
    +			_, err = session.Insert(&insertActivity)
    +			if err != nil {
    +				return false, err
    +			}
    +			newAct = true
     		}
     	}
    +	return newAct, nil
    +}
     
    -	resp.Votes = resp.UpVotes - resp.DownVotes
    +// cancelActivities cancel activities
    +// If this activity is already cancelled, set activity rank to 0
    +// So after this function, the activity rank will be correct for update user rank
    +func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {
    +	for _, activity := range activities {
    +		t := &entity.Activity{}
    +		exist, err := session.ID(activity.ID).Get(t)
    +		if err != nil {
    +			log.Error(err)
    +			return err
    +		}
    +		if !exist {
    +			log.Error(fmt.Errorf("%s activity not exist", activity.ID))
    +			return fmt.Errorf("%s activity not exist", activity.ID)
    +		}
    +		//  If this activity is already cancelled, set activity rank to 0
    +		if t.Cancelled == entity.ActivityCancelled {
    +			activity.Rank = 0
    +		}
    +		if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at").
    +			Update(&entity.Activity{
    +				Cancelled:   entity.ActivityCancelled,
    +				CancelledAt: time.Now(),
    +			}); err != nil {
    +			log.Error(err)
    +			return err
    +		}
    +	}
    +	return nil
    +}
     
    -	return resp, nil
    +func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) {
    +	var activities []*entity.Activity
    +	for _, action := range op.Activities {
    +		t := &entity.Activity{}
    +		exist, err := vr.data.DB.Context(ctx).
    +			Where(builder.Eq{"user_id": action.ActivityUserID}).
    +			And(builder.Eq{"trigger_user_id": action.TriggerUserID}).
    +			And(builder.Eq{"activity_type": action.ActivityType}).
    +			And(builder.Eq{"object_id": op.ObjectID}).
    +			Get(t)
    +		if err != nil {
    +			return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
    +		}
    +		if exist {
    +			activities = append(activities, t)
    +		}
    +	}
    +	return activities, nil
     }
     
    -func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
    -	page int, pageSize int, activityTypes []int) (voteList []entity.Activity, total int64, err error) {
    -	session := vr.data.DB.Context(ctx)
    -	cond := builder.
    -		And(
    -			builder.Eq{"user_id": userID},
    -			builder.Eq{"cancelled": 0},
    -			builder.In("activity_type", activityTypes),
    -		)
    +func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) {
    +	count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp)
    +	if err != nil {
    +		log.Errorf("get vote up count error: %v", err)
    +	}
    +	return count
    +}
     
    -	session.Where(cond).Desc("updated_at")
    +func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) {
    +	count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown)
    +	if err != nil {
    +		log.Errorf("get vote down count error: %v", err)
    +	}
    +	return count
    +}
     
    -	total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
    +func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) {
    +	activity := &entity.Activity{}
    +	activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action)
    +	count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
    +		And(builder.Eq{"activity_type": activityType}).
    +		And(builder.Eq{"cancelled": 0}).
    +		Count(activity)
     	if err != nil {
     		err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
     	}
    -	return
    +	return count, err
     }
     
    -// updateVotes
    -// if votes < 0 Decr object vote_count,otherwise Incr object vote_count
    -func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) {
    -	var (
    -		objectType string
    -		e          error
    -	)
    -
    -	objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
    +func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) {
    +	session := vr.data.DB.Context(ctx)
     	switch objectType {
    -	case "question":
    -		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{})
    -	case "answer":
    -		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{})
    -	case "comment":
    -		_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{})
    -	default:
    -		e = errors.BadRequest(reason.DisallowVote)
    +	case constant.QuestionObjectType:
    +		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount})
    +	case constant.AnswerObjectType:
    +		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount})
    +	case constant.CommentObjectType:
    +		_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount})
     	}
    -
    -	if e != nil {
    -		err = e
    -	} else if err != nil {
    -		err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack()
    +	if err != nil {
    +		log.Error(err)
     	}
    -
     	return
     }
     
    -// sendNotification send rank triggered notification
    -func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
    +func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
     	objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
     	if err != nil {
     		return
    
  • internal/repo/provider.go+0 1 modified
    @@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet(
     	activity.NewVoteRepo,
     	activity.NewFollowRepo,
     	activity.NewAnswerActivityRepo,
    -	activity.NewQuestionActivityRepo,
     	activity.NewUserActiveActivityRepo,
     	activity.NewActivityRepo,
     	tag.NewTagRepo,
    
  • internal/repo/rank/user_rank_repo.go+51 4 modified
    @@ -31,17 +31,64 @@ 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
     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
     	}
     
    
  • internal/repo/user/user_repo.go+1 1 modified
    @@ -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
    
  • internal/schema/vote_schema.go+35 27 modified
    @@ -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"`
    
  • internal/service/activity/answer_activity.go+3 42 modified
    @@ -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)
    -}
    
  • internal/service/activity_common/activity.go+1 1 modified
    @@ -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)
    
  • internal/service/rank/rank_service.go+4 0 modified
    @@ -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)
     }
    
  • internal/service/vote_service.go+121 74 modified
    @@ -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,51 +15,45 @@ 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,
     	commentCommonRepo comment_common.CommentCommonRepo,
     	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
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.