Race Condition within a Thread in answerdev/answer
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/answerdev/answerGo | < 1.1.1 | 1.1.1 |
Affected products
2- answerdev/answerdev/answerv5Range: unspecified
Patches
147661dc8a356refactor(votes): refactor user vote repo
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
4News mentions
0No linked articles in our index yet.