refactor(votes): refactor user vote repo

This commit is contained in:
LinkinStars 2023-06-30 18:18:22 +08:00
parent 9a31d7e76d
commit 47661dc8a3
16 changed files with 582 additions and 649 deletions

View File

@ -153,8 +153,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
reportService := report2.NewReportService(reportRepo, objService) reportService := report2.NewReportService(reportRepo, objService)
reportController := controller.NewReportController(reportService, rankService) reportController := controller.NewReportController(reportService, rankService)
serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configService, activityRepo, userRankRepo, voteRepo, notificationQueueService) serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
voteController := controller.NewVoteController(voteService, rankService) voteController := controller.NewVoteController(voteService, rankService)
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) 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) collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon)
collectionController := controller.NewCollectionController(collectionService) collectionController := controller.NewCollectionController(collectionService)
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, activityQueueService, siteInfoCommonService) 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) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, activityQueueService)
questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService) questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService)

View File

@ -10,7 +10,6 @@ import (
"github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/uid" "github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
) )
@ -54,9 +53,7 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
return return
} }
dto := &schema.VoteDTO{} resp, err := vc.VoteService.VoteUp(ctx, req)
_ = copier.Copy(dto, req)
resp, err := vc.VoteService.VoteUp(ctx, dto)
if err != nil { if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast) handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else { } else {
@ -93,9 +90,7 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
return return
} }
dto := &schema.VoteDTO{} resp, err := vc.VoteService.VoteDown(ctx, req)
_ = copier.Copy(dto, req)
resp, err := vc.VoteService.VoteDown(ctx, dto)
if err != nil { if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast) handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else { } else {

View File

@ -15,7 +15,6 @@ import (
"github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm" "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 // AcceptAnswer accept other answer
func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool, answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool,
@ -306,50 +232,3 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
} }
return err 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
}

View File

@ -43,7 +43,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
if err != nil { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 { if err != nil {
return err return err
} }
@ -110,7 +110,7 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string)
if err != nil { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 { if err != nil {
return err return err
} }

View File

@ -2,6 +2,8 @@ package activity
import ( import (
"context" "context"
"fmt"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason" "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) { func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated) cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated)
if err != nil { if err != nil {
return err return err
} }
activityType := cfg.ID
deltaRank := cfg.GetIntValue()
addActivity := &entity.Activity{ addActivity := &entity.Activity{
UserID: userID, UserID: userID,
ObjectID: "0", ObjectID: "0",
OriginalObjectID: "0", OriginalObjectID: "0",
ActivityType: activityType, ActivityType: cfg.ID,
Rank: deltaRank, Rank: cfg.GetIntValue(),
HasRank: 1, HasRank: 1,
} }
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx) 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 { 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 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 { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, err
} }
_, err = session.Insert(addActivity) _, err = session.Insert(addActivity)
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, err
} }
return nil, nil return nil, nil
}) })
return err if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
} }

View File

@ -2,7 +2,8 @@ package activity
import ( import (
"context" "context"
"strings" "fmt"
"github.com/segmentfault/pacman/log"
"time" "time"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
@ -10,20 +11,17 @@ import (
"github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/internal/base/pager" "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/internal/service/rank"
"github.com/answerdev/answer/pkg/obj" "github.com/answerdev/answer/pkg/obj"
"xorm.io/builder" "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/data"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -31,365 +29,147 @@ import (
// VoteRepo activity repository // VoteRepo activity repository
type VoteRepo struct { type VoteRepo struct {
data *data.Data data *data.Data
uniqueIDRepo unique.UniqueIDRepo
configService *config.ConfigService
activityRepo activity_common.ActivityRepo activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo userRankRepo rank.UserRankRepo
voteCommon activity_common.VoteRepo
notificationQueueService notice_queue.NotificationQueueService notificationQueueService notice_queue.NotificationQueueService
} }
// NewVoteRepo new repository // NewVoteRepo new repository
func NewVoteRepo( func NewVoteRepo(
data *data.Data, data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
configService *config.ConfigService,
activityRepo activity_common.ActivityRepo, activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo, userRankRepo rank.UserRankRepo,
voteCommon activity_common.VoteRepo,
notificationQueueService notice_queue.NotificationQueueService, notificationQueueService notice_queue.NotificationQueueService,
) service.VoteRepo { ) service.VoteRepo {
return &VoteRepo{ return &VoteRepo{
data: data, data: data,
uniqueIDRepo: uniqueIDRepo,
configService: configService,
activityRepo: activityRepo, activityRepo: activityRepo,
userRankRepo: userRankRepo, userRankRepo: userRankRepo,
voteCommon: voteCommon,
notificationQueueService: notificationQueueService, notificationQueueService: notificationQueueService,
} }
} }
var LimitUpActions = map[string][]string{ func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
"question": {"vote_up", "voted_up"}, noNeedToVote, err := vr.votePreCheck(ctx, op)
"answer": {"vote_up", "voted_up"},
"comment": {"vote_up"},
}
var LimitDownActions = map[string][]string{
"question": {"vote_down", "voted_down"},
"answer": {"vote_down", "voted_down"},
"comment": {"vote_down"},
}
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
achievementNotificationUserIDs := make([]string, 0)
sendInboxNotification := false
upVote := false
_, 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,
}
// trigger user rank and send notification
if hasRank != 0 {
var isReachStandard bool
isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType)
if err != nil {
return nil, err
}
if isReachStandard {
insertActivity.Rank = 0
}
achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
}
if has {
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{
Cancelled: entity.ActivityAvailable,
}); err != nil {
return
}
} else {
_, err = session.Insert(&insertActivity)
if err != nil {
return nil, err
}
sendInboxNotification = true
}
// update votes
if action == constant.ActVoteDown || action == constant.ActVoteUp {
votes := 1
if action == constant.ActVoteDown {
upVote = false
votes = -1
} else {
upVote = true
}
err = vr.updateVotes(ctx, session, objectID, votes)
if err != nil {
return
}
}
}
return
})
if err != nil { if err != nil {
return return err
}
if noNeedToVote {
return nil
} }
resp, err = vr.GetVoteResultByObjectId(ctx, objectID) sendInboxNotification := false
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx)
if err != nil {
return err
}
var userIDs []string
for _, activity := range op.Activities {
userIDs = append(userIDs, activity.ActivityUserID)
}
for _, activityUserID := range achievementNotificationUserIDs { _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID) session = session.Context(ctx)
userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, maxDailyRank)
if err != nil {
return nil, err
}
sendInboxNotification, err = vr.saveActivitiesAvailable(session, op)
if err != nil {
return nil, err
}
err = vr.changeUserRank(ctx, session, op, userInfoMapping)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return err
}
for _, activity := range op.Activities {
if activity.Rank == 0 {
continue
}
vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
} }
if sendInboxNotification { 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) { func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
resp = &schema.VoteResp{} // Pre-Check
notificationUserIDs := make([]string, 0) // 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) { _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx) 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) userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil { if err != nil {
return return nil, err
}
triggerUserID = userID
if userID == activityUserID {
triggerUserID = "0"
}
has, err = session.
Where(builder.Eq{"user_id": activityUserID}).
And(builder.Eq{"trigger_user_id": triggerUserID}).
And(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"object_id": objectID}).
Get(&existsActivity)
if !has {
return
}
if existsActivity.Cancelled == entity.ActivityCancelled {
return
}
if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
return
}
// trigger user rank and send notification
if hasRank != 0 && existsActivity.Rank != 0 {
_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
if err != nil {
return
}
notificationUserIDs = append(notificationUserIDs, activityUserID)
}
// update votes
if action == "vote_down" || action == "vote_up" {
votes := -1
if action == "vote_down" {
votes = 1
}
err = vr.updateVotes(ctx, session, objectID, votes)
if err != nil {
return
}
}
} }
return err = vr.cancelActivities(session, activities)
if err != nil {
return nil, err
}
err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping)
if err != nil {
return nil, err
}
return nil, nil
}) })
if err != 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 { for _, activity := range activities {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID) if activity.Rank == 0 {
continue
}
vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID)
} }
return nil
}
func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (
up, down int64, err error) {
up = vr.countVoteUp(ctx, objectID, objectType)
down = vr.countVoteDown(ctx, objectID, objectType)
err = vr.updateVotes(ctx, objectID, objectType, int(up-down))
return return
} }
func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitUpActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
_, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID)
return vr.vote(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitDownActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
_, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID)
return vr.vote(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
var objectType string
resp = &schema.VoteResp{}
objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitUpActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
var objectType string
resp = &schema.VoteResp{}
objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitDownActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) {
activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
if err != nil {
return
}
activityUserID = userID
if strings.Contains(action, "voted") {
activityUserID = objectUserID
}
return activityUserID, activityType, rank, hasRank, nil
}
func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
for _, action := range []string{"vote_up", "vote_down"} {
var (
activity entity.Activity
votes int64
activityType int
)
activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
votes, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
And(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"cancelled": 0}).
Count(&activity)
if err != nil {
return
}
if action == "vote_up" {
resp.UpVotes = int(votes)
} else {
resp.DownVotes = int(votes)
}
}
resp.Votes = resp.UpVotes - resp.DownVotes
return resp, nil
}
func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string, func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
page int, pageSize int, activityTypes []int) (voteList []entity.Activity, total int64, err error) { page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {
session := vr.data.DB.Context(ctx) session := vr.data.DB.Context(ctx)
cond := builder. cond := builder.
And( And(
@ -407,37 +187,234 @@ func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
return return
} }
// updateVotes func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
// if votes < 0 Decr object vote_count,otherwise Incr object vote_count activities, err := vr.getExistActivity(ctx, op)
func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) { if err != nil {
var ( return false, err
objectType string }
e error done := 0
) for _, activity := range activities {
if activity.Cancelled == entity.ActivityAvailable {
done++
}
}
return done == len(op.Activities), nil
}
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 {
log.Error(err)
return nil, err
}
users := make(map[string]*entity.User, 0)
for _, u := range us {
users[u.ID] = u
}
return users, nil
}
func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session,
op *schema.VoteOperationInfo, maxDailyRank int) (err error) {
// check if user reach daily rank limit
for _, activity := range op.Activities {
reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)
if err != nil {
log.Error(err)
return err
}
if reach {
activity.Rank = 0
}
}
return nil
}
func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session,
op *schema.VoteOperationInfo,
userInfoMapping map[string]*entity.User) (err error) {
for _, activity := range op.Activities {
if activity.Rank == 0 {
continue
}
user := userInfoMapping[activity.ActivityUserID]
if user == nil {
continue
}
if err = vr.userRankRepo.ChangeUserRank(ctx, session,
activity.ActivityUserID, user.Rank, activity.Rank); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
activities []*entity.Activity,
userInfoMapping map[string]*entity.User) (err error) {
for _, activity := range activities {
if activity.Rank == 0 {
continue
}
user := userInfoMapping[activity.UserID]
if user == nil {
continue
}
if err = vr.userRankRepo.ChangeUserRank(ctx, session,
activity.UserID, user.Rank, -activity.Rank); err != nil {
log.Error(err)
return err
}
}
return nil
}
// saveActivitiesAvailable save activities
// If activity not exist it will be created or else will be updated
// If this activity is already exist, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) {
for _, activity := range op.Activities {
existsActivity := &entity.Activity{}
exist, err := session.
Where(builder.Eq{"object_id": op.ObjectID}).
And(builder.Eq{"user_id": activity.ActivityUserID}).
And(builder.Eq{"trigger_user_id": activity.TriggerUserID}).
And(builder.Eq{"activity_type": activity.ActivityType}).
Get(existsActivity)
if err != nil {
return false, err
}
if exist && existsActivity.Cancelled == entity.ActivityAvailable {
activity.Rank = 0
continue
}
if exist {
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); err != nil {
return false, err
}
} else {
insertActivity := entity.Activity{
ObjectID: op.ObjectID,
OriginalObjectID: op.ObjectID,
UserID: activity.ActivityUserID,
TriggerUserID: converter.StringToInt64(activity.TriggerUserID),
ActivityType: activity.ActivityType,
Rank: activity.Rank,
HasRank: activity.HasRank(),
Cancelled: entity.ActivityAvailable,
}
_, err = session.Insert(&insertActivity)
if err != nil {
return false, err
}
newAct = true
}
}
return newAct, nil
}
// cancelActivities cancel activities
// If this activity is already cancelled, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {
for _, activity := range activities {
t := &entity.Activity{}
exist, err := session.ID(activity.ID).Get(t)
if err != nil {
log.Error(err)
return err
}
if !exist {
log.Error(fmt.Errorf("%s activity not exist", activity.ID))
return fmt.Errorf("%s activity not exist", activity.ID)
}
// If this activity is already cancelled, set activity rank to 0
if t.Cancelled == entity.ActivityCancelled {
activity.Rank = 0
}
if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) {
var activities []*entity.Activity
for _, action := range op.Activities {
t := &entity.Activity{}
exist, err := vr.data.DB.Context(ctx).
Where(builder.Eq{"user_id": action.ActivityUserID}).
And(builder.Eq{"trigger_user_id": action.TriggerUserID}).
And(builder.Eq{"activity_type": action.ActivityType}).
And(builder.Eq{"object_id": op.ObjectID}).
Get(t)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
activities = append(activities, t)
}
}
return activities, nil
}
func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) {
count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp)
if err != nil {
log.Errorf("get vote up count error: %v", err)
}
return count
}
func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) {
count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown)
if err != nil {
log.Errorf("get vote down count error: %v", err)
}
return count
}
func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) {
activity := &entity.Activity{}
activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action)
count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
And(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"cancelled": 0}).
Count(activity)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, err
}
func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) {
session := vr.data.DB.Context(ctx)
switch objectType { switch objectType {
case "question": case constant.QuestionObjectType:
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{}) _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount})
case "answer": case constant.AnswerObjectType:
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{}) _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount})
case "comment": case constant.CommentObjectType:
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{}) _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount})
default:
e = errors.BadRequest(reason.DisallowVote)
} }
if err != nil {
if e != nil { log.Error(err)
err = e
} else if err != nil {
err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack()
} }
return return
} }
// sendNotification send rank triggered notification func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID) objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil { if err != nil {
return return

View File

@ -41,12 +41,12 @@ func NewActivityRepo(
func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) ( func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (
activityType, rank, hasRank int, err error) { activityType, rank, hasRank int, err error) {
objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil { if err != nil {
return return
} }
confKey := fmt.Sprintf("%s.%s", objectKey, action) confKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, confKey) cfg, err := ar.configService.GetConfigByKey(ctx, confKey)
if err != nil { if err != nil {
return return
@ -59,8 +59,8 @@ func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID str
return return
} }
func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) { func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) {
configKey := fmt.Sprintf("%s.%s", objectKey, action) configKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, configKey) cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
if err != nil { if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()

View File

@ -74,7 +74,7 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us
if err != nil { if err != nil {
return nil, err return nil, err
} }
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow") activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil { if err != nil {
log.Errorf("can't get activity type by object key: %s", objectTypeStr) log.Errorf("can't get activity type by object key: %s", objectTypeStr)
return nil, err return nil, err
@ -96,7 +96,7 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us
// GetFollowIDs get all follow id list // GetFollowIDs get all follow id list
func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) {
followIDs = make([]string, 0) followIDs = make([]string, 0)
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 return false, err
} }
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet(
activity.NewVoteRepo, activity.NewVoteRepo,
activity.NewFollowRepo, activity.NewFollowRepo,
activity.NewAnswerActivityRepo, activity.NewAnswerActivityRepo,
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo, activity.NewUserActiveActivityRepo,
activity.NewActivityRepo, activity.NewActivityRepo,
tag.NewTagRepo, tag.NewTagRepo,

View File

@ -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 // TriggerUserRank trigger user rank change
// session is need provider, it means this action must be success or failure // session is need provider, it means this action must be success or failure
// if outer action is failed then this action is need rollback // 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, session *xorm.Session, userID string, deltaRank int, activityType int,
) (isReachStandard bool, err error) { ) (isReachStandard bool, err error) {
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank. // IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
if plugin.RankAgentEnabled() { if plugin.RankAgentEnabled() || deltaRank == 0 {
return false, nil
}
if deltaRank == 0 {
return false, nil return false, nil
} }

View File

@ -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) { func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
list := make([]*entity.User, 0) 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 { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return list, err return list, err

View File

@ -6,20 +6,44 @@ type VoteReq struct {
UserID string `json:"-"` UserID string `json:"-"`
} }
type VoteDTO struct { type VoteResp struct {
// object TagID UpVotes int64 `json:"up_votes"`
ObjectID string DownVotes int64 `json:"down_votes"`
// is cancel Votes int64 `json:"votes"`
IsCancel bool VoteStatus string `json:"vote_status"`
// user TagID
UserID string
} }
type VoteResp struct { // VoteOperationInfo vote operation info
UpVotes int `json:"up_votes"` type VoteOperationInfo struct {
DownVotes int `json:"down_votes"` // operation object id
Votes int `json:"votes"` ObjectID string
VoteStatus string `json:"vote_status"` // question answer comment
ObjectType string
// object owner user id
ObjectCreatorUserID string
// operation user id
OperatingUserID string
// vote up
VoteUp bool
// vote down
VoteDown bool
// vote activity info
Activities []*VoteActivity
}
// VoteActivity vote activity
type VoteActivity struct {
ActivityType int
ActivityUserID string
TriggerUserID string
Rank int
}
func (v *VoteActivity) HasRank() int {
if v.Rank != 0 {
return 1
}
return 0
} }
type GetVoteWithPageReq struct { type GetVoteWithPageReq struct {
@ -31,22 +55,6 @@ type GetVoteWithPageReq struct {
UserID string `json:"-"` 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 { type GetVoteWithPageResp struct {
// create time // create time
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`

View File

@ -2,9 +2,6 @@ package activity
import ( import (
"context" "context"
"time"
"github.com/segmentfault/pacman/log"
) )
// AnswerActivityRepo answer activity // AnswerActivityRepo answer activity
@ -13,26 +10,18 @@ type AnswerActivityRepo interface {
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error)
CancelAcceptAnswer(ctx context.Context, CancelAcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string) (err error) 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 // AnswerActivityService user service
type AnswerActivityService struct { type AnswerActivityService struct {
answerActivityRepo AnswerActivityRepo answerActivityRepo AnswerActivityRepo
questionActivityRepo QuestionActivityRepo
} }
// NewAnswerActivityService new comment service // NewAnswerActivityService new comment service
func NewAnswerActivityService( func NewAnswerActivityService(
answerActivityRepo AnswerActivityRepo, questionActivityRepo QuestionActivityRepo) *AnswerActivityService { answerActivityRepo AnswerActivityRepo) *AnswerActivityService {
return &AnswerActivityService{ return &AnswerActivityService{
answerActivityRepo: answerActivityRepo, answerActivityRepo: answerActivityRepo,
questionActivityRepo: questionActivityRepo,
} }
} }
@ -47,31 +36,3 @@ func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {
return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID) 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)
}

View File

@ -15,7 +15,7 @@ import (
type ActivityRepo interface { type ActivityRepo interface {
GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error) 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) ( GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) (
existsActivity *entity.Activity, exist bool, err error) existsActivity *entity.Activity, exist bool, err error)
GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)

View File

@ -29,6 +29,10 @@ const (
) )
type UserRankRepo interface { 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) 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) UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error)
} }

View File

@ -2,6 +2,8 @@ package service
import ( import (
"context" "context"
"github.com/answerdev/answer/internal/service/activity_common"
"strings"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler" "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/config"
"github.com/answerdev/answer/internal/service/object_info" "github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
answercommon "github.com/answerdev/answer/internal/service/answer_common" answercommon "github.com/answerdev/answer/internal/service/answer_common"
questioncommon "github.com/answerdev/answer/internal/service/question_common" questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
) )
// VoteRepo activity repository // VoteRepo activity repository
type VoteRepo interface { type VoteRepo interface {
VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, 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)
ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) ( 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 // VoteService user service
type VoteService struct { type VoteService struct {
voteRepo VoteRepo voteRepo VoteRepo
UniqueIDRepo unique.UniqueIDRepo
configService *config.ConfigService configService *config.ConfigService
questionRepo questioncommon.QuestionRepo questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo answerRepo answercommon.AnswerRepo
commentCommonRepo comment_common.CommentCommonRepo commentCommonRepo comment_common.CommentCommonRepo
objectService *object_info.ObjService objectService *object_info.ObjService
activityRepo activity_common.ActivityRepo
} }
func NewVoteService( func NewVoteService(
VoteRepo VoteRepo, voteRepo VoteRepo,
uniqueIDRepo unique.UniqueIDRepo,
configService *config.ConfigService, configService *config.ConfigService,
questionRepo questioncommon.QuestionRepo, questionRepo questioncommon.QuestionRepo,
answerRepo answercommon.AnswerRepo, answerRepo answercommon.AnswerRepo,
@ -56,8 +53,7 @@ func NewVoteService(
objectService *object_info.ObjService, objectService *object_info.ObjService,
) *VoteService { ) *VoteService {
return &VoteService{ return &VoteService{
voteRepo: VoteRepo, voteRepo: voteRepo,
UniqueIDRepo: uniqueIDRepo,
configService: configService, configService: configService,
questionRepo: questionRepo, questionRepo: questionRepo,
answerRepo: answerRepo, answerRepo: answerRepo,
@ -67,90 +63,83 @@ func NewVoteService(
} }
// VoteUp vote up // VoteUp vote up
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{} objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
var objectUserID string
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil { if err != nil {
return return nil, err
} }
// make object id must be decoded
objectInfo.ObjectID = req.ObjectID
// check user is voting self or not // check user is voting self or not
if objectUserID == dto.UserID { if objectInfo.ObjectCreatorUserID == req.UserID {
err = errors.BadRequest(reason.DisallowVoteYourSelf) return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
return
} }
if dto.IsCancel { voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
// vote operation
if req.IsCancel {
err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
} else { } 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 // VoteDown vote down
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{} objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
var objectUserID string
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil { if err != nil {
return return nil, err
} }
// make object id must be decoded
objectInfo.ObjectID = req.ObjectID
// check user is voting self or not // check user is voting self or not
if objectUserID == dto.UserID { if objectInfo.ObjectCreatorUserID == req.UserID {
err = errors.BadRequest(reason.DisallowVoteYourSelf) return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
return
} }
if dto.IsCancel { // vote operation
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
if req.IsCancel {
err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
} else { } 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 { if err != nil {
err = nil log.Error(err)
return
} }
resp.Votes = resp.UpVotes - resp.DownVotes
switch objectKey { if !req.IsCancel {
case "question": resp.VoteStatus = constant.ActVoteDown
object, has, e := vs.questionRepo.GetQuestion(ctx, objectID)
if e != nil || !has {
err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack()
return
}
userID = object.UserID
case "answer":
object, has, e := vs.answerRepo.GetAnswer(ctx, objectID)
if e != nil || !has {
err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack()
return
}
userID = object.UserID
case "comment":
object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID)
if e != nil || !has {
err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack()
return
}
userID = object.UserID
default:
err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack()
return
} }
return resp, nil
return
} }
// ListUserVotes list user's votes // 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 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
}