diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 0439c6385..70f316418 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -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) diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index 00162c02f..3548e053e 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -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 { diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go index ae2441752..1e9bb643a 100644 --- a/internal/repo/activity/answer_repo.go +++ b/internal/repo/activity/answer_repo.go @@ -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 -} diff --git a/internal/repo/activity/follow_repo.go b/internal/repo/activity/follow_repo.go index 8f9ade892..02f025cd7 100644 --- a/internal/repo/activity/follow_repo.go +++ b/internal/repo/activity/follow_repo.go @@ -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 } diff --git a/internal/repo/activity/user_active_repo.go b/internal/repo/activity/user_active_repo.go index 0f6099d15..06c587c3d 100644 --- a/internal/repo/activity/user_active_repo.go +++ b/internal/repo/activity/user_active_repo.go @@ -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 } diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index 22355235e..120d392b9 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -2,7 +2,8 @@ package activity import ( "context" - "strings" + "fmt" + "github.com/segmentfault/pacman/log" "time" "github.com/answerdev/answer/internal/base/constant" @@ -10,20 +11,17 @@ import ( "github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/pkg/obj" "xorm.io/builder" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/unique" - "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/activity_common" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) @@ -31,413 +29,392 @@ import ( // VoteRepo activity repository type VoteRepo struct { data *data.Data - uniqueIDRepo unique.UniqueIDRepo - configService *config.ConfigService activityRepo activity_common.ActivityRepo userRankRepo rank.UserRankRepo - voteCommon activity_common.VoteRepo notificationQueueService notice_queue.NotificationQueueService } // NewVoteRepo new repository func NewVoteRepo( data *data.Data, - uniqueIDRepo unique.UniqueIDRepo, - configService *config.ConfigService, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, - voteCommon activity_common.VoteRepo, notificationQueueService notice_queue.NotificationQueueService, ) service.VoteRepo { return &VoteRepo{ data: data, - uniqueIDRepo: uniqueIDRepo, - configService: configService, activityRepo: activityRepo, userRankRepo: userRankRepo, - voteCommon: voteCommon, notificationQueueService: notificationQueueService, } } -var LimitUpActions = map[string][]string{ - "question": {"vote_up", "voted_up"}, - "answer": {"vote_up", "voted_up"}, - "comment": {"vote_up"}, -} - -var LimitDownActions = map[string][]string{ - "question": {"vote_down", "voted_down"}, - "answer": {"vote_down", "voted_down"}, - "comment": {"vote_down"}, -} +func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { + noNeedToVote, err := vr.votePreCheck(ctx, op) + if err != nil { + return err + } + if noNeedToVote { + return nil + } -func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - achievementNotificationUserIDs := make([]string, 0) sendInboxNotification := false - upVote := false + maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx) + if err != nil { + return err + } + var userIDs []string + for _, activity := range op.Activities { + userIDs = append(userIDs, activity.ActivityUserID) + } + _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) - result = nil - for _, action := range actions { - var ( - existsActivity entity.Activity - insertActivity entity.Activity - has bool - triggerUserID, - activityUserID string - activityType, deltaRank, hasRank int - ) - - activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action) - if err != nil { - return - } - triggerUserID = userID - if userID == activityUserID { - triggerUserID = "0" - } - - // check is voted up - has, _ = session. - Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"user_id": activityUserID}). - And(builder.Eq{"trigger_user_id": triggerUserID}). - And(builder.Eq{"activity_type": activityType}). - Get(&existsActivity) - - // is is voted,return - if has && existsActivity.Cancelled == entity.ActivityAvailable { - return - } - - insertActivity = entity.Activity{ - ObjectID: objectID, - OriginalObjectID: objectID, - UserID: activityUserID, - TriggerUserID: converter.StringToInt64(triggerUserID), - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, - Cancelled: entity.ActivityAvailable, - } + userInfoMapping, err := vr.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err + } - // trigger user rank and send notification - if hasRank != 0 { - var isReachStandard bool - isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType) - if err != nil { - return nil, err - } - if isReachStandard { - insertActivity.Rank = 0 - } - achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID) - } + err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, maxDailyRank) + if err != nil { + return nil, err + } - if has { - if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{ - Cancelled: entity.ActivityAvailable, - }); err != nil { - return - } - } else { - _, err = session.Insert(&insertActivity) - if err != nil { - return nil, err - } - sendInboxNotification = true - } + sendInboxNotification, err = vr.saveActivitiesAvailable(session, op) + if err != nil { + return nil, err + } - // update votes - if action == constant.ActVoteDown || action == constant.ActVoteUp { - votes := 1 - if action == constant.ActVoteDown { - upVote = false - votes = -1 - } else { - upVote = true - } - err = vr.updateVotes(ctx, session, objectID, votes) - if err != nil { - return - } - } + err = vr.changeUserRank(ctx, session, op, userInfoMapping) + if err != nil { + return nil, err } - return + return nil, nil }) if err != nil { - return + return err } - resp, err = vr.GetVoteResultByObjectId(ctx, objectID) - resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) - - for _, activityUserID := range achievementNotificationUserIDs { - vr.sendNotification(ctx, activityUserID, objectUserID, objectID) + for _, activity := range op.Activities { + if activity.Rank == 0 { + continue + } + vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID) } if sendInboxNotification { - vr.sendVoteInboxNotification(ctx, userID, objectUserID, objectID, upVote) + vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp) } - return + return nil } -func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - notificationUserIDs := make([]string, 0) +func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { + // Pre-Check + // 1. check if the activity exist + // 2. check if the activity is not cancelled + // 3. if all activities are cancelled, return directly + activities, err := vr.getExistActivity(ctx, op) + if err != nil { + return err + } + var userIDs []string + for _, activity := range activities { + if activity.Cancelled == entity.ActivityCancelled { + continue + } + userIDs = append(userIDs, activity.UserID) + } + if len(userIDs) == 0 { + return nil + } + _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { session = session.Context(ctx) - for _, action := range actions { - var ( - existsActivity entity.Activity - has bool - triggerUserID, - activityUserID string - activityType, - deltaRank, hasRank int - ) - result = nil - - activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action) - if err != nil { - return - } - - triggerUserID = userID - if userID == activityUserID { - triggerUserID = "0" - } - - has, err = session. - Where(builder.Eq{"user_id": activityUserID}). - And(builder.Eq{"trigger_user_id": triggerUserID}). - And(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"object_id": objectID}). - Get(&existsActivity) - - if !has { - return - } - - if existsActivity.Cancelled == entity.ActivityCancelled { - return - } - - if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at"). - Update(&entity.Activity{ - Cancelled: entity.ActivityCancelled, - CancelledAt: time.Now(), - }); err != nil { - return - } - // trigger user rank and send notification - if hasRank != 0 && existsActivity.Rank != 0 { - _, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType) - if err != nil { - return - } - notificationUserIDs = append(notificationUserIDs, activityUserID) - } + userInfoMapping, err := vr.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err + } - // update votes - if action == "vote_down" || action == "vote_up" { - votes := -1 - if action == "vote_down" { - votes = 1 - } - err = vr.updateVotes(ctx, session, objectID, votes) - if err != nil { - return - } - } + err = vr.cancelActivities(session, activities) + if err != nil { + return nil, err } - return + err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping) + if err != nil { + return nil, err + } + return nil, nil }) if err != nil { - return + return err } - resp, err = vr.GetVoteResultByObjectId(ctx, objectID) - resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) - for _, activityUserID := range notificationUserIDs { - vr.sendNotification(ctx, activityUserID, objectUserID, objectID) + for _, activity := range activities { + if activity.Rank == 0 { + continue + } + vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID) } - return + return nil } -func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - objectType, err := obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } +func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) ( + up, down int64, err error) { + up = vr.countVoteUp(ctx, objectID, objectType) + down = vr.countVoteDown(ctx, objectID, objectType) + err = vr.updateVotes(ctx, objectID, objectType, int(up-down)) + return +} - actions, ok := LimitUpActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return - } +func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string, + page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) { + session := vr.data.DB.Context(ctx) + cond := builder. + And( + builder.Eq{"user_id": userID}, + builder.Eq{"cancelled": 0}, + builder.In("activity_type", activityTypes), + ) - _, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID) - return vr.vote(ctx, objectID, userID, objectUserID, actions) -} + session.Where(cond).Desc("updated_at") -func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - objectType, err := obj.GetObjectTypeStrByObjectID(objectID) + total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session) if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitDownActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - - _, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID) - return vr.vote(ctx, objectID, userID, objectUserID, actions) + return } -func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - var objectType string - resp = &schema.VoteResp{} - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) +func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) { + activities, err := vr.getExistActivity(ctx, op) if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return + return false, err } - actions, ok := LimitUpActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return + done := 0 + for _, activity := range activities { + if activity.Cancelled == entity.ActivityAvailable { + done++ + } } - - return vr.voteCancel(ctx, objectID, userID, objectUserID, actions) + return done == len(op.Activities), nil } -func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { - var objectType string - resp = &schema.VoteResp{} - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) +func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { + us := make([]*entity.User, 0) + err := session.In("id", userIDs).ForUpdate().Find(&us) if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitDownActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return + log.Error(err) + return nil, err } - return vr.voteCancel(ctx, objectID, userID, objectUserID, actions) + users := make(map[string]*entity.User, 0) + for _, u := range us { + users[u.ID] = u + } + return users, nil } -func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) { - activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) - - if err != nil { - return +func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session, + op *schema.VoteOperationInfo, maxDailyRank int) (err error) { + // check if user reach daily rank limit + for _, activity := range op.Activities { + reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank) + if err != nil { + log.Error(err) + return err + } + if reach { + activity.Rank = 0 + } } + return nil +} - activityUserID = userID - if strings.Contains(action, "voted") { - activityUserID = objectUserID +func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session, + op *schema.VoteOperationInfo, + userInfoMapping map[string]*entity.User) (err error) { + for _, activity := range op.Activities { + if activity.Rank == 0 { + continue + } + user := userInfoMapping[activity.ActivityUserID] + if user == nil { + continue + } + if err = vr.userRankRepo.ChangeUserRank(ctx, session, + activity.ActivityUserID, user.Rank, activity.Rank); err != nil { + log.Error(err) + return err + } } - - return activityUserID, activityType, rank, hasRank, nil + return nil } -func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - for _, action := range []string{"vote_up", "vote_down"} { - var ( - activity entity.Activity - votes int64 - activityType int - ) - - activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) - - votes, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"cancelled": 0}). - Count(&activity) +func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, + activities []*entity.Activity, + userInfoMapping map[string]*entity.User) (err error) { + for _, activity := range activities { + if activity.Rank == 0 { + continue + } + user := userInfoMapping[activity.UserID] + if user == nil { + continue + } + if err = vr.userRankRepo.ChangeUserRank(ctx, session, + activity.UserID, user.Rank, -activity.Rank); err != nil { + log.Error(err) + return err + } + } + return nil +} +// saveActivitiesAvailable save activities +// If activity not exist it will be created or else will be updated +// If this activity is already exist, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) { + for _, activity := range op.Activities { + existsActivity := &entity.Activity{} + exist, err := session. + Where(builder.Eq{"object_id": op.ObjectID}). + And(builder.Eq{"user_id": activity.ActivityUserID}). + And(builder.Eq{"trigger_user_id": activity.TriggerUserID}). + And(builder.Eq{"activity_type": activity.ActivityType}). + Get(existsActivity) if err != nil { - return + return false, err } - - if action == "vote_up" { - resp.UpVotes = int(votes) + if exist && existsActivity.Cancelled == entity.ActivityAvailable { + activity.Rank = 0 + continue + } + if exist { + if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). + Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); err != nil { + return false, err + } } else { - resp.DownVotes = int(votes) + insertActivity := entity.Activity{ + ObjectID: op.ObjectID, + OriginalObjectID: op.ObjectID, + UserID: activity.ActivityUserID, + TriggerUserID: converter.StringToInt64(activity.TriggerUserID), + ActivityType: activity.ActivityType, + Rank: activity.Rank, + HasRank: activity.HasRank(), + Cancelled: entity.ActivityAvailable, + } + _, err = session.Insert(&insertActivity) + if err != nil { + return false, err + } + newAct = true } } + return newAct, nil +} - resp.Votes = resp.UpVotes - resp.DownVotes +// cancelActivities cancel activities +// If this activity is already cancelled, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { + for _, activity := range activities { + t := &entity.Activity{} + exist, err := session.ID(activity.ID).Get(t) + if err != nil { + log.Error(err) + return err + } + if !exist { + log.Error(fmt.Errorf("%s activity not exist", activity.ID)) + return fmt.Errorf("%s activity not exist", activity.ID) + } + // If this activity is already cancelled, set activity rank to 0 + if t.Cancelled == entity.ActivityCancelled { + activity.Rank = 0 + } + if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at"). + Update(&entity.Activity{ + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), + }); err != nil { + log.Error(err) + return err + } + } + return nil +} - return resp, nil +func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) { + var activities []*entity.Activity + for _, action := range op.Activities { + t := &entity.Activity{} + exist, err := vr.data.DB.Context(ctx). + Where(builder.Eq{"user_id": action.ActivityUserID}). + And(builder.Eq{"trigger_user_id": action.TriggerUserID}). + And(builder.Eq{"activity_type": action.ActivityType}). + And(builder.Eq{"object_id": op.ObjectID}). + Get(t) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + activities = append(activities, t) + } + } + return activities, nil } -func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string, - page int, pageSize int, activityTypes []int) (voteList []entity.Activity, total int64, err error) { - session := vr.data.DB.Context(ctx) - cond := builder. - And( - builder.Eq{"user_id": userID}, - builder.Eq{"cancelled": 0}, - builder.In("activity_type", activityTypes), - ) +func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) { + count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp) + if err != nil { + log.Errorf("get vote up count error: %v", err) + } + return count +} - session.Where(cond).Desc("updated_at") +func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) { + count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown) + if err != nil { + log.Errorf("get vote down count error: %v", err) + } + return count +} - total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session) +func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) { + activity := &entity.Activity{} + activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action) + count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}). + And(builder.Eq{"activity_type": activityType}). + And(builder.Eq{"cancelled": 0}). + Count(activity) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return + return count, err } -// updateVotes -// if votes < 0 Decr object vote_count,otherwise Incr object vote_count -func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) { - var ( - objectType string - e error - ) - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) +func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) { + session := vr.data.DB.Context(ctx) switch objectType { - case "question": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{}) - case "answer": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{}) - case "comment": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{}) - default: - e = errors.BadRequest(reason.DisallowVote) + case constant.QuestionObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount}) + case constant.AnswerObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount}) + case constant.CommentObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount}) } - - if e != nil { - err = e - } else if err != nil { - err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack() + if err != nil { + log.Error(err) } - return } -// sendNotification send rank triggered notification -func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { +func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go index db1af3d16..ca16e482d 100644 --- a/internal/repo/activity_common/activity_repo.go +++ b/internal/repo/activity_common/activity_repo.go @@ -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() diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go index 050982772..1c3f08a2c 100644 --- a/internal/repo/activity_common/follow.go +++ b/internal/repo/activity_common/follow.go @@ -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 } diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 10722cecd..34fe1c1aa 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet( activity.NewVoteRepo, activity.NewFollowRepo, activity.NewAnswerActivityRepo, - activity.NewQuestionActivityRepo, activity.NewUserActiveActivityRepo, activity.NewActivityRepo, tag.NewTagRepo, diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go index 872769db8..257dec8e3 100644 --- a/internal/repo/rank/user_rank_repo.go +++ b/internal/repo/rank/user_rank_repo.go @@ -31,6 +31,56 @@ func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank. } } +func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) { + maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit") + if err != nil { + return 0, err + } + return maxDailyRank, nil +} + +func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session, + userID string, maxDailyRank int) ( + reach bool, err error) { + session.Where(builder.Eq{"user_id": userID}) + session.Where(builder.Eq{"cancelled": 0}) + session.Where(builder.Between{ + Col: "updated_at", + LessVal: now.BeginningOfDay(), + MoreVal: now.EndOfDay(), + }) + + earned, err := session.Sum(&entity.Activity{}, "`rank`") + if err != nil { + return false, err + } + if int(earned) <= maxDailyRank { + return false, nil + } + log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank) + return true, nil +} + +// ChangeUserRank change user rank +func (ur *UserRankRepo) ChangeUserRank( + ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) { + // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. + if plugin.RankAgentEnabled() || deltaRank == 0 { + return nil + } + + // If user rank is lower than 1 after this action, then user rank will be set to 1 only. + if deltaRank < 0 && userCurrentScore+deltaRank < 1 { + deltaRank = 1 - userCurrentScore + } + + _, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{}) + if err != nil { + return err + } + return nil +} + // TriggerUserRank trigger user rank change // session is need provider, it means this action must be success or failure // if outer action is failed then this action is need rollback @@ -38,10 +88,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, session *xorm.Session, userID string, deltaRank int, activityType int, ) (isReachStandard bool, err error) { // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. - if plugin.RankAgentEnabled() { - return false, nil - } - if deltaRank == 0 { + if plugin.RankAgentEnabled() || deltaRank == 0 { return false, nil } diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index 2ff22a5f7..9bf2f2eab 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -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 diff --git a/internal/schema/vote_schema.go b/internal/schema/vote_schema.go index 93775c29f..e41f542fa 100644 --- a/internal/schema/vote_schema.go +++ b/internal/schema/vote_schema.go @@ -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"` diff --git a/internal/service/activity/answer_activity.go b/internal/service/activity/answer_activity.go index 2a8e51d8e..2f44ea455 100644 --- a/internal/service/activity/answer_activity.go +++ b/internal/service/activity/answer_activity.go @@ -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) -} diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go index 69e57482b..adfb55ad3 100644 --- a/internal/service/activity_common/activity.go +++ b/internal/service/activity_common/activity.go @@ -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) diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go index 307bc5915..2bf222a48 100644 --- a/internal/service/rank/rank_service.go +++ b/internal/service/rank/rank_service.go @@ -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) } diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index 1d39ff991..61f7e31d2 100644 --- a/internal/service/vote_service.go +++ b/internal/service/vote_service.go @@ -2,6 +2,8 @@ package service import ( "context" + "github.com/answerdev/answer/internal/service/activity_common" + "strings" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/handler" @@ -13,42 +15,37 @@ import ( "github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/object_info" "github.com/answerdev/answer/pkg/htmltext" - "github.com/answerdev/answer/pkg/obj" "github.com/segmentfault/pacman/log" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" answercommon "github.com/answerdev/answer/internal/service/answer_common" questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) // VoteRepo activity repository type VoteRepo interface { - VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) + Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) + CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) + GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error) ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) ( - voteList []entity.Activity, total int64, err error) + voteList []*entity.Activity, total int64, err error) } // VoteService user service type VoteService struct { voteRepo VoteRepo - UniqueIDRepo unique.UniqueIDRepo configService *config.ConfigService questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo commentCommonRepo comment_common.CommentCommonRepo objectService *object_info.ObjService + activityRepo activity_common.ActivityRepo } func NewVoteService( - VoteRepo VoteRepo, - uniqueIDRepo unique.UniqueIDRepo, + voteRepo VoteRepo, configService *config.ConfigService, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, @@ -56,8 +53,7 @@ func NewVoteService( objectService *object_info.ObjService, ) *VoteService { return &VoteService{ - voteRepo: VoteRepo, - UniqueIDRepo: uniqueIDRepo, + voteRepo: voteRepo, configService: configService, questionRepo: questionRepo, answerRepo: answerRepo, @@ -67,90 +63,83 @@ func NewVoteService( } // VoteUp vote up -func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { - voteResp = &schema.VoteResp{} - - var objectUserID string - - objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID) +func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { + objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) if err != nil { - return + return nil, err } + // make object id must be decoded + objectInfo.ObjectID = req.ObjectID // check user is voting self or not - if objectUserID == dto.UserID { - err = errors.BadRequest(reason.DisallowVoteYourSelf) - return + if objectInfo.ObjectCreatorUserID == req.UserID { + return nil, errors.BadRequest(reason.DisallowVoteYourSelf) } - if dto.IsCancel { - return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) + voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo) + + // vote operation + if req.IsCancel { + err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo) } else { - return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID) + // cancel vote down if exist + voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) + err = vs.voteRepo.CancelVote(ctx, voteOperationInfo) + if err != nil { + return nil, err + } + err = vs.voteRepo.Vote(ctx, voteUpOperationInfo) + } + + resp = &schema.VoteResp{} + resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) + if err != nil { + log.Error(err) + } + resp.Votes = resp.UpVotes - resp.DownVotes + if !req.IsCancel { + resp.VoteStatus = constant.ActVoteUp } + return resp, nil } // VoteDown vote down -func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { - voteResp = &schema.VoteResp{} - - var objectUserID string - - objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID) +func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { + objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) if err != nil { - return + return nil, err } + // make object id must be decoded + objectInfo.ObjectID = req.ObjectID // check user is voting self or not - if objectUserID == dto.UserID { - err = errors.BadRequest(reason.DisallowVoteYourSelf) - return + if objectInfo.ObjectCreatorUserID == req.UserID { + return nil, errors.BadRequest(reason.DisallowVoteYourSelf) } - if dto.IsCancel { - return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) + // vote operation + voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) + if req.IsCancel { + err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo) } else { - return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID) + // cancel vote up if exist + err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)) + if err != nil { + return nil, err + } + err = vs.voteRepo.Vote(ctx, voteDownOperationInfo) } -} - -func (vs *VoteService) GetObjectUserID(ctx context.Context, objectID string) (userID string, err error) { - var objectKey string - objectKey, err = obj.GetObjectTypeStrByObjectID(objectID) + resp = &schema.VoteResp{} + resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) if err != nil { - err = nil - return + log.Error(err) } - - switch objectKey { - case "question": - object, has, e := vs.questionRepo.GetQuestion(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - case "answer": - object, has, e := vs.answerRepo.GetAnswer(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - case "comment": - object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - default: - err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack() - return + resp.Votes = resp.UpVotes - resp.DownVotes + if !req.IsCancel { + resp.VoteStatus = constant.ActVoteDown } - - return + return resp, nil } // ListUserVotes list user's votes @@ -207,3 +196,61 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith } return pager.NewPageModel(total, votes), err } + +func (vs *VoteService) createVoteOperationInfo(ctx context.Context, + userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo { + // warp vote operation + voteOperationInfo := &schema.VoteOperationInfo{ + ObjectID: objectInfo.ObjectID, + ObjectType: objectInfo.ObjectType, + ObjectCreatorUserID: objectInfo.ObjectCreatorUserID, + OperatingUserID: userID, + VoteUp: voteUp, + VoteDown: !voteUp, + } + voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo) + return voteOperationInfo +} + +func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) ( + activities []*schema.VoteActivity) { + activities = make([]*schema.VoteActivity, 0) + + var actions []string + switch op.ObjectType { + case constant.QuestionObjectType: + if op.VoteUp { + actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp} + } else { + actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown} + } + case constant.AnswerObjectType: + if op.VoteUp { + actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp} + } else { + actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown} + } + case constant.CommentObjectType: + actions = []string{activity_type.CommentVoteUp} + } + + for _, action := range actions { + t := &schema.VoteActivity{} + cfg, err := vs.configService.GetConfigByKey(ctx, action) + if err != nil { + log.Warnf("get config by key error: %v", err) + continue + } + t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() + + if strings.Contains(action, "voted") { + t.ActivityUserID = op.ObjectCreatorUserID + t.TriggerUserID = op.OperatingUserID + } else { + t.ActivityUserID = op.OperatingUserID + t.TriggerUserID = "0" + } + activities = append(activities, t) + } + return activities +}