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)
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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,365 +29,147 @@ 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, 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
})
func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
noNeedToVote, err := vr.votePreCheck(ctx, op)
if err != nil {
return
return err
}
if noNeedToVote {
return nil
}
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
sendInboxNotification := 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)
}
for _, activityUserID := range achievementNotificationUserIDs {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
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 {
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)
}
// 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
}
}
userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
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 {
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 nil
}
func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (
up, down int64, err error) {
up = vr.countVoteUp(ctx, objectID, objectType)
down = vr.countVoteDown(ctx, objectID, objectType)
err = vr.updateVotes(ctx, objectID, objectType, int(up-down))
return
}
func (vr *VoteRepo) 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,
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)
cond := builder.
And(
@ -407,37 +187,234 @@ func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
return
}
// updateVotes
// if votes < 0 Decr object vote_count,otherwise Incr object vote_count
func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) {
var (
objectType string
e error
)
func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
activities, err := vr.getExistActivity(ctx, op)
if err != nil {
return false, err
}
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 {
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

View File

@ -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()

View File

@ -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
}

View File

@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet(
activity.NewVoteRepo,
activity.NewFollowRepo,
activity.NewAnswerActivityRepo,
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo,
activity.NewActivityRepo,
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
// 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
}

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) {
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

View File

@ -6,20 +6,44 @@ type VoteReq struct {
UserID string `json:"-"`
}
type VoteDTO struct {
// object TagID
ObjectID string
// is cancel
IsCancel bool
// user TagID
UserID string
type VoteResp struct {
UpVotes int64 `json:"up_votes"`
DownVotes int64 `json:"down_votes"`
Votes int64 `json:"votes"`
VoteStatus string `json:"vote_status"`
}
type VoteResp struct {
UpVotes int `json:"up_votes"`
DownVotes int `json:"down_votes"`
Votes int `json:"votes"`
VoteStatus string `json:"vote_status"`
// VoteOperationInfo vote operation info
type VoteOperationInfo struct {
// operation object id
ObjectID 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
}
// 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"`

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}