chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,819 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/service/eventqueue"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/activity"
|
||||
"github.com/apache/answer/internal/service/activity_common"
|
||||
"github.com/apache/answer/internal/service/activityqueue"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
collectioncommon "github.com/apache/answer/internal/service/collection_common"
|
||||
"github.com/apache/answer/internal/service/export"
|
||||
"github.com/apache/answer/internal/service/noticequeue"
|
||||
"github.com/apache/answer/internal/service/permission"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
"github.com/apache/answer/internal/service/review"
|
||||
"github.com/apache/answer/internal/service/revision_common"
|
||||
"github.com/apache/answer/internal/service/role"
|
||||
usercommon "github.com/apache/answer/internal/service/user_common"
|
||||
"github.com/apache/answer/internal/service/vector_sync"
|
||||
"github.com/apache/answer/pkg/converter"
|
||||
"github.com/apache/answer/pkg/htmltext"
|
||||
"github.com/apache/answer/pkg/token"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// AnswerService user service
|
||||
type AnswerService struct {
|
||||
answerRepo answercommon.AnswerRepo
|
||||
questionRepo questioncommon.QuestionRepo
|
||||
questionCommon *questioncommon.QuestionCommon
|
||||
answerActivityService *activity.AnswerActivityService
|
||||
userCommon *usercommon.UserCommon
|
||||
collectionCommon *collectioncommon.CollectionCommon
|
||||
userRepo usercommon.UserRepo
|
||||
revisionService *revision_common.RevisionService
|
||||
AnswerCommon *answercommon.AnswerCommon
|
||||
voteRepo activity_common.VoteRepo
|
||||
emailService *export.EmailService
|
||||
roleService *role.UserRoleRelService
|
||||
notificationQueueService noticequeue.Service
|
||||
externalNotificationQueueService noticequeue.ExternalService
|
||||
activityQueueService activityqueue.Service
|
||||
reviewService *review.ReviewService
|
||||
eventQueueService eventqueue.Service
|
||||
vectorSyncService vector_sync.Service
|
||||
}
|
||||
|
||||
func NewAnswerService(
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
questionRepo questioncommon.QuestionRepo,
|
||||
questionCommon *questioncommon.QuestionCommon,
|
||||
userCommon *usercommon.UserCommon,
|
||||
collectionCommon *collectioncommon.CollectionCommon,
|
||||
userRepo usercommon.UserRepo,
|
||||
revisionService *revision_common.RevisionService,
|
||||
answerAcceptActivityRepo *activity.AnswerActivityService,
|
||||
answerCommon *answercommon.AnswerCommon,
|
||||
voteRepo activity_common.VoteRepo,
|
||||
emailService *export.EmailService,
|
||||
roleService *role.UserRoleRelService,
|
||||
notificationQueueService noticequeue.Service,
|
||||
externalNotificationQueueService noticequeue.ExternalService,
|
||||
activityQueueService activityqueue.Service,
|
||||
reviewService *review.ReviewService,
|
||||
eventQueueService eventqueue.Service,
|
||||
vectorSyncService vector_sync.Service,
|
||||
) *AnswerService {
|
||||
return &AnswerService{
|
||||
answerRepo: answerRepo,
|
||||
questionRepo: questionRepo,
|
||||
userCommon: userCommon,
|
||||
collectionCommon: collectionCommon,
|
||||
questionCommon: questionCommon,
|
||||
userRepo: userRepo,
|
||||
revisionService: revisionService,
|
||||
answerActivityService: answerAcceptActivityRepo,
|
||||
AnswerCommon: answerCommon,
|
||||
voteRepo: voteRepo,
|
||||
emailService: emailService,
|
||||
roleService: roleService,
|
||||
notificationQueueService: notificationQueueService,
|
||||
externalNotificationQueueService: externalNotificationQueueService,
|
||||
activityQueueService: activityQueueService,
|
||||
reviewService: reviewService,
|
||||
eventQueueService: eventQueueService,
|
||||
vectorSyncService: vectorSyncService,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAnswer delete answer
|
||||
func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAnswerReq) (err error) {
|
||||
answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return nil
|
||||
}
|
||||
// if the status is deleted, return directly
|
||||
if answerInfo.Status == entity.AnswerStatusDeleted {
|
||||
return nil
|
||||
}
|
||||
roleID, err := as.roleService.GetUserRole(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if roleID != role.RoleAdminID && roleID != role.RoleModeratorID {
|
||||
if answerInfo.UserID != req.UserID {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
if answerInfo.VoteCount > 0 {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
if answerInfo.Accepted == schema.AnswerAcceptedEnable {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
_, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
|
||||
if err != nil {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.AnswerCannotDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
err = as.answerRepo.RemoveAnswer(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// user add question count
|
||||
err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID)
|
||||
if err != nil {
|
||||
log.Error("IncreaseAnswerCount error", err.Error())
|
||||
}
|
||||
userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("GetCountByUserID error", err.Error())
|
||||
}
|
||||
err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))
|
||||
if err != nil {
|
||||
log.Error("user IncreaseAnswerCount error", err.Error())
|
||||
}
|
||||
err = as.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: answerInfo.QuestionID,
|
||||
FromAnswerID: answerInfo.ID,
|
||||
}, &entity.QuestionLink{
|
||||
ToQuestionID: answerInfo.QuestionID,
|
||||
ToAnswerID: answerInfo.ID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("RemoveQuestionLink error", err.Error())
|
||||
}
|
||||
|
||||
// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
|
||||
// facing the problem of recovery.
|
||||
// err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
|
||||
// if err != nil {
|
||||
// log.Errorf("delete answer activity change failed: %s", err.Error())
|
||||
// }
|
||||
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
TriggerUserID: converter.StringToInt64(req.UserID),
|
||||
ObjectID: answerInfo.ID,
|
||||
OriginalObjectID: answerInfo.ID,
|
||||
ActivityTypeKey: constant.ActAnswerDeleted,
|
||||
})
|
||||
as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID).TID(answerInfo.ID).
|
||||
AID(answerInfo.ID, answerInfo.UserID))
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
|
||||
return
|
||||
}
|
||||
|
||||
// RecoverAnswer recover deleted answer
|
||||
func (as *AnswerService) RecoverAnswer(ctx context.Context, req *schema.RecoverAnswerReq) (err error) {
|
||||
answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.AnswerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.AnswerNotFound)
|
||||
}
|
||||
if answerInfo.Status != entity.AnswerStatusDeleted {
|
||||
return nil
|
||||
}
|
||||
if err = as.answerRepo.RecoverAnswer(ctx, req.AnswerID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = as.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: answerInfo.QuestionID,
|
||||
FromAnswerID: answerInfo.ID,
|
||||
}, &entity.QuestionLink{
|
||||
ToQuestionID: answerInfo.QuestionID,
|
||||
ToAnswerID: answerInfo.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil {
|
||||
log.Errorf("update answer count failed: %s", err.Error())
|
||||
}
|
||||
userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID)
|
||||
if err != nil {
|
||||
log.Errorf("get user answer count failed: %s", err.Error())
|
||||
} else {
|
||||
err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount))
|
||||
if err != nil {
|
||||
log.Errorf("update user answer count failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
TriggerUserID: converter.StringToInt64(req.UserID),
|
||||
ObjectID: answerInfo.ID,
|
||||
OriginalObjectID: answerInfo.ID,
|
||||
ActivityTypeKey: constant.ActAnswerUndeleted,
|
||||
})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) {
|
||||
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exist {
|
||||
return "", errors.BadRequest(reason.QuestionNotFound)
|
||||
}
|
||||
if questionInfo.Status == entity.QuestionStatusClosed || questionInfo.Status == entity.QuestionStatusDeleted {
|
||||
err = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion)
|
||||
return "", err
|
||||
}
|
||||
insertData := &entity.Answer{}
|
||||
insertData.UserID = req.UserID
|
||||
insertData.OriginalText = req.Content
|
||||
insertData.ParsedText = req.HTML
|
||||
insertData.Accepted = schema.AnswerAcceptedFailed
|
||||
insertData.QuestionID = req.QuestionID
|
||||
insertData.RevisionID = "0"
|
||||
insertData.LastEditUserID = "0"
|
||||
insertData.Status = entity.AnswerStatusPending
|
||||
// insertData.UpdatedAt = now
|
||||
if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
insertData.Status = as.reviewService.AddAnswerReview(ctx, insertData, req.IP, req.UserAgent)
|
||||
if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, insertData.Status); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if insertData.Status == entity.AnswerStatusAvailable {
|
||||
insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"parsed_text"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
log.Error("IncreaseAnswerCount error", err.Error())
|
||||
}
|
||||
err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, uid.DeShortID(insertData.ID))
|
||||
if err != nil {
|
||||
log.Error("UpdateLastAnswer error", err.Error())
|
||||
}
|
||||
err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
log.Error("GetCountByUserID error", err.Error())
|
||||
}
|
||||
err = as.userCommon.UpdateAnswerCount(ctx, req.UserID, int(userAnswerCount))
|
||||
if err != nil {
|
||||
log.Error("user IncreaseAnswerCount error", err.Error())
|
||||
}
|
||||
|
||||
revisionDTO := &schema.AddRevisionDTO{
|
||||
UserID: insertData.UserID,
|
||||
ObjectID: insertData.ID,
|
||||
Title: "",
|
||||
}
|
||||
infoJSON, _ := json.Marshal(insertData)
|
||||
revisionDTO.Content = string(infoJSON)
|
||||
revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
|
||||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
if insertData.Status == entity.AnswerStatusAvailable {
|
||||
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title,
|
||||
htmltext.FetchExcerpt(insertData.ParsedText, "...", 240))
|
||||
}
|
||||
|
||||
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: insertData.UserID,
|
||||
ObjectID: insertData.ID,
|
||||
OriginalObjectID: insertData.ID,
|
||||
ActivityTypeKey: constant.ActAnswerAnswered,
|
||||
RevisionID: revisionID,
|
||||
})
|
||||
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: insertData.UserID,
|
||||
ObjectID: insertData.ID,
|
||||
OriginalObjectID: questionInfo.ID,
|
||||
ActivityTypeKey: constant.ActQuestionAnswered,
|
||||
})
|
||||
as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID).TID(insertData.ID).
|
||||
AID(insertData.ID, insertData.UserID))
|
||||
if insertData.Status == entity.AnswerStatusAvailable {
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: insertData.ID})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: insertData.QuestionID})
|
||||
}
|
||||
return insertData.ID, nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) {
|
||||
var canUpdate bool
|
||||
_, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if existUnreviewed {
|
||||
return "", errors.BadRequest(reason.AnswerCannotUpdate)
|
||||
}
|
||||
|
||||
answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exist {
|
||||
return "", errors.BadRequest(reason.AnswerNotFound)
|
||||
}
|
||||
if answerInfo.Status == entity.AnswerStatusDeleted {
|
||||
return "", errors.BadRequest(reason.AnswerCannotUpdate)
|
||||
}
|
||||
|
||||
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exist {
|
||||
return "", errors.BadRequest(reason.QuestionNotFound)
|
||||
}
|
||||
|
||||
// If the content is the same, ignore it
|
||||
if answerInfo.OriginalText == req.Content {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
insertData := &entity.Answer{}
|
||||
insertData.ID = req.ID
|
||||
insertData.UserID = answerInfo.UserID
|
||||
insertData.QuestionID = questionInfo.ID
|
||||
insertData.OriginalText = req.Content
|
||||
insertData.ParsedText = req.HTML
|
||||
insertData.UpdatedAt = time.Now()
|
||||
insertData.LastEditUserID = "0"
|
||||
if answerInfo.UserID != req.UserID {
|
||||
insertData.LastEditUserID = req.UserID
|
||||
}
|
||||
|
||||
revisionDTO := &schema.AddRevisionDTO{
|
||||
UserID: req.UserID,
|
||||
ObjectID: req.ID,
|
||||
Log: req.EditSummary,
|
||||
}
|
||||
|
||||
if req.NoNeedReview || answerInfo.UserID == req.UserID {
|
||||
canUpdate = true
|
||||
}
|
||||
|
||||
if !canUpdate {
|
||||
revisionDTO.Status = entity.RevisionUnreviewedStatus
|
||||
} else {
|
||||
insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = as.questionCommon.UpdatePostTime(ctx, questionInfo.ID)
|
||||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID)
|
||||
revisionDTO.Status = entity.RevisionReviewPassStatus
|
||||
}
|
||||
|
||||
infoJSON, _ := json.Marshal(insertData)
|
||||
revisionDTO.Content = string(infoJSON)
|
||||
revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
|
||||
if err != nil {
|
||||
return insertData.ID, err
|
||||
}
|
||||
if canUpdate {
|
||||
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: insertData.ID,
|
||||
OriginalObjectID: insertData.ID,
|
||||
ActivityTypeKey: constant.ActAnswerEdited,
|
||||
RevisionID: revisionID,
|
||||
})
|
||||
as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID).TID(insertData.ID).
|
||||
AID(insertData.ID, insertData.UserID))
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: insertData.ID})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: insertData.QuestionID})
|
||||
}
|
||||
|
||||
return insertData.ID, nil
|
||||
}
|
||||
|
||||
// AcceptAnswer accept answer
|
||||
func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAnswerReq) (err error) {
|
||||
// find question
|
||||
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.QuestionNotFound)
|
||||
}
|
||||
questionInfo.ID = uid.DeShortID(questionInfo.ID)
|
||||
if questionInfo.AcceptedAnswerID == req.AnswerID {
|
||||
return nil
|
||||
}
|
||||
|
||||
// find answer
|
||||
var acceptedAnswerInfo *entity.Answer
|
||||
if len(req.AnswerID) > 1 {
|
||||
acceptedAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, req.AnswerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.AnswerNotFound)
|
||||
}
|
||||
|
||||
// check answer belong to question
|
||||
if !sameObjectID(acceptedAnswerInfo.QuestionID, req.QuestionID) {
|
||||
return errors.BadRequest(reason.AnswerNotFound)
|
||||
}
|
||||
acceptedAnswerInfo.ID = uid.DeShortID(acceptedAnswerInfo.ID)
|
||||
}
|
||||
|
||||
// update answers status
|
||||
if err = as.answerRepo.UpdateAcceptedStatus(ctx, req.AnswerID, req.QuestionID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update question status
|
||||
err = as.questionCommon.UpdateAccepted(ctx, req.QuestionID, req.AnswerID)
|
||||
if err != nil {
|
||||
log.Error("UpdateLastAnswer error", err.Error())
|
||||
}
|
||||
|
||||
var oldAnswerInfo *entity.Answer
|
||||
if len(questionInfo.AcceptedAnswerID) > 1 {
|
||||
oldAnswerInfo, _, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID)
|
||||
}
|
||||
|
||||
if acceptedAnswerInfo != nil {
|
||||
as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID).TID(acceptedAnswerInfo.ID).
|
||||
QID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID))
|
||||
}
|
||||
|
||||
as.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo)
|
||||
if acceptedAnswerInfo != nil {
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: acceptedAnswerInfo.ID})
|
||||
}
|
||||
if oldAnswerInfo != nil {
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: oldAnswerInfo.ID})
|
||||
}
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: questionInfo.ID})
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameObjectID reports whether two object ids refer to the same row,
|
||||
// regardless of whether each is in short-id or long-id form.
|
||||
func sameObjectID(a, b string) bool {
|
||||
return uid.DeShortID(a) == uid.DeShortID(b)
|
||||
}
|
||||
|
||||
func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,
|
||||
questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer,
|
||||
) {
|
||||
// if this question is already been answered, should cancel old answer rank
|
||||
if oldAnswerInfo != nil {
|
||||
err := as.answerActivityService.CancelAcceptAnswer(ctx, userID,
|
||||
questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
if newAnswerInfo != nil {
|
||||
err := as.answerActivityService.AcceptAnswer(ctx, userID, newAnswerInfo.ID,
|
||||
questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == questionInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string, isAdminModerator bool) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) {
|
||||
answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil, false, nil
|
||||
}
|
||||
question, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
|
||||
}
|
||||
|
||||
if (question.Status == entity.QuestionStatusDeleted ||
|
||||
question.Status == entity.QuestionStatusPending ||
|
||||
question.Show == entity.QuestionHide) &&
|
||||
!isAdminModerator && question.UserID != loginUserID {
|
||||
return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
|
||||
}
|
||||
if (answerInfo.Status == entity.AnswerStatusDeleted ||
|
||||
answerInfo.Status == entity.AnswerStatusPending) &&
|
||||
!isAdminModerator && answerInfo.UserID != loginUserID {
|
||||
return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
|
||||
}
|
||||
info := as.ShowFormat(ctx, answerInfo)
|
||||
// todo questionFunc
|
||||
questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID)
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
// todo UserFunc
|
||||
|
||||
userIds := make([]string, 0)
|
||||
userIds = append(userIds, answerInfo.UserID)
|
||||
userIds = append(userIds, answerInfo.LastEditUserID)
|
||||
userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds)
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
|
||||
_, ok := userInfoMap[answerInfo.UserID]
|
||||
if ok {
|
||||
info.UserInfo = userInfoMap[answerInfo.UserID]
|
||||
}
|
||||
_, ok = userInfoMap[answerInfo.LastEditUserID]
|
||||
if ok {
|
||||
info.UpdateUserInfo = userInfoMap[answerInfo.LastEditUserID]
|
||||
}
|
||||
|
||||
if loginUserID == "" {
|
||||
return info, questionInfo, has, nil
|
||||
}
|
||||
|
||||
info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserID)
|
||||
|
||||
collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{answerInfo.ID})
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
if len(collectedMap) > 0 {
|
||||
info.Collected = true
|
||||
}
|
||||
|
||||
return info, questionInfo, has, nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) GetDetail(ctx context.Context, answerID string) (*schema.AnswerInfo, error) {
|
||||
answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, errors.BadRequest(reason.AnswerNotFound)
|
||||
}
|
||||
info := as.ShowFormat(ctx, answerInfo)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) GetCountByUserIDQuestionID(ctx context.Context, userId string, questionId string) (ids []string, err error) {
|
||||
return as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userId, questionId)
|
||||
}
|
||||
|
||||
func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminUpdateAnswerStatusReq) error {
|
||||
setStatus, ok := entity.AdminAnswerSearchStatus[req.Status]
|
||||
if !ok {
|
||||
return errors.BadRequest(reason.RequestFormatError)
|
||||
}
|
||||
answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.AnswerNotFound)
|
||||
}
|
||||
|
||||
if setStatus == entity.AnswerStatusDeleted {
|
||||
if err := as.RemoveAnswer(ctx, &schema.RemoveAnswerReq{
|
||||
ID: req.AnswerID,
|
||||
UserID: req.UserID,
|
||||
CanDelete: true,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := &schema.NotificationMsg{}
|
||||
msg.ObjectID = answerInfo.ID
|
||||
msg.Type = schema.NotificationTypeInbox
|
||||
msg.ReceiverUserID = answerInfo.UserID
|
||||
msg.TriggerUserID = answerInfo.UserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
|
||||
as.notificationQueueService.Send(ctx, msg)
|
||||
}
|
||||
|
||||
// recover
|
||||
if setStatus == entity.QuestionStatusAvailable && answerInfo.Status == entity.QuestionStatusDeleted {
|
||||
if err := as.RecoverAnswer(ctx, &schema.RecoverAnswerReq{
|
||||
AnswerID: req.AnswerID,
|
||||
UserID: req.UserID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch setStatus {
|
||||
case entity.AnswerStatusDeleted:
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionDelete, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
|
||||
case entity.AnswerStatusAvailable:
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeAnswer, ObjectID: answerInfo.ID})
|
||||
as.vectorSyncService.Send(ctx, &vector_sync.Task{Action: vector_sync.ActionUpsert, ObjectType: vector_sync.ObjectTypeQuestion, ObjectID: answerInfo.QuestionID})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
|
||||
list := make([]*schema.AnswerInfo, 0)
|
||||
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
}
|
||||
if !exist {
|
||||
return list, 0, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
if (questionInfo.Status == entity.QuestionStatusDeleted ||
|
||||
questionInfo.Status == entity.QuestionStatusPending ||
|
||||
questionInfo.Show == entity.QuestionHide) &&
|
||||
!req.IsAdminModerator && questionInfo.UserID != req.UserID {
|
||||
return list, 0, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
dbSearch := entity.AnswerSearch{}
|
||||
dbSearch.QuestionID = req.QuestionID
|
||||
dbSearch.Page = req.Page
|
||||
dbSearch.PageSize = req.PageSize
|
||||
dbSearch.Order = req.Order
|
||||
dbSearch.IncludeDeleted = req.CanDelete
|
||||
dbSearch.LoginUserID = req.UserID
|
||||
answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
}
|
||||
answerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req)
|
||||
if err != nil {
|
||||
return answerList, count, err
|
||||
}
|
||||
return answerList, count, nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) (
|
||||
[]*schema.AnswerInfo, error) {
|
||||
list := make([]*schema.AnswerInfo, 0)
|
||||
objectIDs := make([]string, 0)
|
||||
userIDs := make([]string, 0)
|
||||
for _, info := range answers {
|
||||
item := as.ShowFormat(ctx, info)
|
||||
list = append(list, item)
|
||||
objectIDs = append(objectIDs, info.ID)
|
||||
userIDs = append(userIDs, info.UserID, info.LastEditUserID)
|
||||
}
|
||||
|
||||
userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
for _, item := range list {
|
||||
item.UserInfo = userInfoMap[item.UserID]
|
||||
item.UpdateUserInfo = userInfoMap[item.UpdateUserID]
|
||||
}
|
||||
if len(req.UserID) == 0 {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID)
|
||||
item.Collected = collectedMap[item.ID]
|
||||
item.MemberActions = permission.GetAnswerPermission(ctx,
|
||||
req.UserID,
|
||||
item.UserID,
|
||||
item.Status,
|
||||
req.CanEdit,
|
||||
req.CanDelete,
|
||||
req.CanRecover)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo {
|
||||
return as.AnswerCommon.ShowFormat(ctx, data)
|
||||
}
|
||||
|
||||
func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) {
|
||||
// If the answer is updated by me, there is no notification for myself.
|
||||
// equivalent behaviour as AnswerService.notificationAnswerTheQuestion
|
||||
if questionUserID == answerUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: answerUserID,
|
||||
ReceiverUserID: questionUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: answerID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.NotificationUpdateAnswer
|
||||
as.notificationQueueService.Send(ctx, msg)
|
||||
}
|
||||
|
||||
func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
|
||||
questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) {
|
||||
// If the question is answered by me, there is no notification for myself.
|
||||
if questionUserID == answerUserID {
|
||||
return
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: answerUserID,
|
||||
ReceiverUserID: questionUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: answerID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.NotificationAnswerTheQuestion
|
||||
as.notificationQueueService.Send(ctx, msg)
|
||||
|
||||
receiverUserInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", questionUserID)
|
||||
return
|
||||
}
|
||||
|
||||
externalNotificationMsg := &schema.ExternalNotificationMsg{
|
||||
ReceiverUserID: receiverUserInfo.ID,
|
||||
ReceiverEmail: receiverUserInfo.EMail,
|
||||
ReceiverLang: receiverUserInfo.Language,
|
||||
}
|
||||
rawData := &schema.NewAnswerTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
AnswerSummary: answerSummary,
|
||||
UnsubscribeCode: token.GenerateToken(),
|
||||
}
|
||||
answerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID)
|
||||
if answerUser != nil {
|
||||
rawData.AnswerUserDisplayName = answerUser.DisplayName
|
||||
}
|
||||
externalNotificationMsg.NewAnswerTemplateRawData = rawData
|
||||
as.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
)
|
||||
|
||||
// TestSameObjectID guards the AcceptAnswer ownership check (issue #1541).
|
||||
//
|
||||
// When short links are enabled, answerRepo.GetByID re-encodes the answer's
|
||||
// QuestionID to its short form while the controller de-shorts req.QuestionID
|
||||
// to its long form. The two encodings of the same question must be treated as
|
||||
// equal, or accepting any answer fails with "Answer do not found".
|
||||
func TestSameObjectID(t *testing.T) {
|
||||
const longQID = "10010000000000001"
|
||||
shortQID := uid.EnShortID(longQID) // e.g. "D1D1"
|
||||
if shortQID == "" || shortQID == longQID {
|
||||
t.Fatalf("precondition failed: EnShortID(%q)=%q, want a distinct short id", longQID, shortQID)
|
||||
}
|
||||
otherLongQID := "10010000000000002"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "short answer-side id vs long request-side id, same question (the bug)",
|
||||
a: shortQID,
|
||||
b: longQID,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "both long, same question (default permalink)",
|
||||
a: longQID,
|
||||
b: longQID,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "both short, same question",
|
||||
a: shortQID,
|
||||
b: shortQID,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different questions must stay rejected (privilege-escalation guard)",
|
||||
a: shortQID,
|
||||
b: otherLongQID,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sameObjectID(tt.a, tt.b); got != tt.want {
|
||||
t.Errorf("sameObjectID(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
func (q *QuestionService) RefreshHottestCron(ctx context.Context) {
|
||||
var (
|
||||
page = 1
|
||||
pageSize = 100
|
||||
)
|
||||
|
||||
for {
|
||||
questionList, _, err := q.questionRepo.GetQuestionPage(
|
||||
ctx,
|
||||
page, pageSize,
|
||||
[]string{},
|
||||
"", "newest",
|
||||
schema.HotInDays,
|
||||
false, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, question := range questionList {
|
||||
updatedAt := question.UpdatedAt.Unix()
|
||||
if updatedAt < 0 {
|
||||
updatedAt = question.CreatedAt.Unix()
|
||||
}
|
||||
|
||||
qAgeInHours := (time.Now().Unix() - question.CreatedAt.Unix()) / 3600
|
||||
qUpdated := (time.Now().Unix() - updatedAt) / 3600
|
||||
|
||||
aScores, err := q.answerRepo.SumVotesByQuestionID(ctx, question.ID)
|
||||
if err != nil {
|
||||
aScores = 0
|
||||
}
|
||||
|
||||
score := q.getScore(float64(question.ViewCount), float64(question.AnswerCount), float64(question.VoteCount), aScores, float64(qAgeInHours), float64(qUpdated))
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
questioninfo := &entity.Question{}
|
||||
questioninfo.ID = question.ID
|
||||
questioninfo.HotScore = int(math.Ceil(score * 10000))
|
||||
err = q.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"hot_score"})
|
||||
if err != nil {
|
||||
log.Error("update question hot score error,question ID:", question.ID, " error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(questionList) < pageSize {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
func (q *QuestionService) getScore(qViews, qAnswers, qScore, aScores, qAgeInHours, qUpdated float64) (score float64) {
|
||||
score = ((math.Log(qViews) * 4) + ((qAnswers * qScore) / 5) + aScores) /
|
||||
math.Pow(((qAgeInHours+1)-((qAgeInHours-qUpdated)/2)), 1.5)
|
||||
return score
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/activity"
|
||||
"github.com/apache/answer/internal/service/activityqueue"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
"github.com/apache/answer/internal/service/noticequeue"
|
||||
"github.com/apache/answer/internal/service/object_info"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
"github.com/apache/answer/internal/service/report_common"
|
||||
"github.com/apache/answer/internal/service/review"
|
||||
"github.com/apache/answer/internal/service/revision"
|
||||
"github.com/apache/answer/internal/service/tag_common"
|
||||
usercommon "github.com/apache/answer/internal/service/user_common"
|
||||
"github.com/apache/answer/pkg/converter"
|
||||
"github.com/apache/answer/pkg/htmltext"
|
||||
"github.com/apache/answer/pkg/obj"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// RevisionService user service
|
||||
type RevisionService struct {
|
||||
revisionRepo revision.RevisionRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
questionCommon *questioncommon.QuestionCommon
|
||||
answerService *AnswerService
|
||||
objectInfoService *object_info.ObjService
|
||||
questionRepo questioncommon.QuestionRepo
|
||||
answerRepo answercommon.AnswerRepo
|
||||
tagRepo tag_common.TagRepo
|
||||
tagCommon *tag_common.TagCommonService
|
||||
notificationQueueService noticequeue.Service
|
||||
activityQueueService activityqueue.Service
|
||||
reportRepo report_common.ReportRepo
|
||||
reviewService *review.ReviewService
|
||||
reviewActivity activity.ReviewActivityRepo
|
||||
}
|
||||
|
||||
func NewRevisionService(
|
||||
revisionRepo revision.RevisionRepo,
|
||||
userCommon *usercommon.UserCommon,
|
||||
questionCommon *questioncommon.QuestionCommon,
|
||||
answerService *AnswerService,
|
||||
objectInfoService *object_info.ObjService,
|
||||
questionRepo questioncommon.QuestionRepo,
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
tagRepo tag_common.TagRepo,
|
||||
tagCommon *tag_common.TagCommonService,
|
||||
notificationQueueService noticequeue.Service,
|
||||
activityQueueService activityqueue.Service,
|
||||
reportRepo report_common.ReportRepo,
|
||||
reviewService *review.ReviewService,
|
||||
reviewActivity activity.ReviewActivityRepo,
|
||||
) *RevisionService {
|
||||
return &RevisionService{
|
||||
revisionRepo: revisionRepo,
|
||||
userCommon: userCommon,
|
||||
questionCommon: questionCommon,
|
||||
answerService: answerService,
|
||||
objectInfoService: objectInfoService,
|
||||
questionRepo: questionRepo,
|
||||
answerRepo: answerRepo,
|
||||
tagRepo: tagRepo,
|
||||
tagCommon: tagCommon,
|
||||
notificationQueueService: notificationQueueService,
|
||||
activityQueueService: activityQueueService,
|
||||
reportRepo: reportRepo,
|
||||
reviewService: reviewService,
|
||||
reviewActivity: reviewActivity,
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) {
|
||||
revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
if revisioninfo.Status != entity.RevisionUnreviewedStatus {
|
||||
return
|
||||
}
|
||||
objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID)
|
||||
if objectTypeerr != nil {
|
||||
return objectTypeerr
|
||||
}
|
||||
if req.Operation == schema.RevisionAuditReject {
|
||||
if err = checkRevisionAuditPermission(req, objectType); err != nil {
|
||||
return err
|
||||
}
|
||||
err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID)
|
||||
return
|
||||
}
|
||||
if req.Operation == schema.RevisionAuditApprove {
|
||||
if err = checkRevisionAuditPermission(req, objectType); err != nil {
|
||||
return err
|
||||
}
|
||||
revisionitem := &schema.GetRevisionResp{}
|
||||
_ = copier.Copy(revisionitem, revisioninfo)
|
||||
rs.parseItem(ctx, revisionitem)
|
||||
var saveErr error
|
||||
switch objectType {
|
||||
case constant.QuestionObjectType:
|
||||
saveErr = rs.revisionAuditQuestion(ctx, revisionitem)
|
||||
case constant.AnswerObjectType:
|
||||
saveErr = rs.revisionAuditAnswer(ctx, revisionitem)
|
||||
case constant.TagObjectType:
|
||||
saveErr = rs.revisionAuditTag(ctx, revisionitem)
|
||||
}
|
||||
if saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rs.reviewActivity.Review(ctx, &schema.PassReviewActivity{
|
||||
UserID: revisioninfo.UserID,
|
||||
TriggerUserID: req.UserID,
|
||||
ObjectID: revisioninfo.ObjectID,
|
||||
OriginalObjectID: "0",
|
||||
RevisionID: revisioninfo.ID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("add review activity failed: %v", err)
|
||||
}
|
||||
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: req.UserID,
|
||||
ReceiverUserID: revisioninfo.UserID,
|
||||
Type: schema.NotificationTypeAchievement,
|
||||
ObjectID: revisioninfo.ObjectID,
|
||||
ObjectType: objectType,
|
||||
}
|
||||
rs.notificationQueueService.Send(ctx, msg)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkRevisionAuditPermission(req *schema.RevisionAuditReq, objectType string) error {
|
||||
switch objectType {
|
||||
case constant.QuestionObjectType:
|
||||
if !req.CanReviewQuestion {
|
||||
return errors.BadRequest(reason.RevisionNoPermission)
|
||||
}
|
||||
case constant.AnswerObjectType:
|
||||
if !req.CanReviewAnswer {
|
||||
return errors.BadRequest(reason.RevisionNoPermission)
|
||||
}
|
||||
case constant.TagObjectType:
|
||||
if !req.CanReviewTag {
|
||||
return errors.BadRequest(reason.RevisionNoPermission)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
|
||||
questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp)
|
||||
if ok {
|
||||
var PostUpdateTime time.Time
|
||||
dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID)
|
||||
if dberr != nil || !exist {
|
||||
return
|
||||
}
|
||||
|
||||
PostUpdateTime = time.Unix(questioninfo.UpdateTime, 0)
|
||||
if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {
|
||||
PostUpdateTime = dbquestion.PostUpdateTime
|
||||
}
|
||||
question := &entity.Question{}
|
||||
question.ID = questioninfo.ID
|
||||
question.Title = questioninfo.Title
|
||||
question.OriginalText = questioninfo.Content
|
||||
question.ParsedText = questioninfo.HTML
|
||||
question.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0)
|
||||
question.PostUpdateTime = PostUpdateTime
|
||||
question.LastEditUserID = revisionitem.UserID
|
||||
saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"})
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
objectTagTags := make([]*schema.TagItem, 0)
|
||||
for _, tag := range questioninfo.Tags {
|
||||
item := &schema.TagItem{}
|
||||
item.SlugName = tag.SlugName
|
||||
objectTagTags = append(objectTagTags, item)
|
||||
}
|
||||
objectTagData := schema.TagChange{}
|
||||
objectTagData.ObjectID = question.ID
|
||||
objectTagData.Tags = objectTagTags
|
||||
minimumTags, err := rs.tagCommon.GetMinimumTags(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData, minimumTags)
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: revisionitem.UserID,
|
||||
ObjectID: revisionitem.ObjectID,
|
||||
ActivityTypeKey: constant.ActQuestionEdited,
|
||||
RevisionID: revisionitem.ID,
|
||||
OriginalObjectID: revisionitem.ObjectID,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
|
||||
answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo)
|
||||
if ok {
|
||||
var PostUpdateTime time.Time
|
||||
dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)
|
||||
if dberr != nil || !exist {
|
||||
return
|
||||
}
|
||||
|
||||
PostUpdateTime = time.Unix(answerinfo.UpdateTime, 0)
|
||||
if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() {
|
||||
PostUpdateTime = dbquestion.PostUpdateTime
|
||||
}
|
||||
|
||||
insertData := new(entity.Answer)
|
||||
insertData.ID = answerinfo.ID
|
||||
insertData.OriginalText = answerinfo.Content
|
||||
insertData.ParsedText = answerinfo.HTML
|
||||
insertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0)
|
||||
insertData.LastEditUserID = revisionitem.UserID
|
||||
saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"})
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime)
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.QuestionNotFound)
|
||||
}
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: revisionitem.UserID,
|
||||
ReceiverUserID: questionInfo.UserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: answerinfo.ID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.NotificationUpdateAnswer
|
||||
rs.notificationQueueService.Send(ctx, msg)
|
||||
|
||||
rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: revisionitem.UserID,
|
||||
ObjectID: insertData.ID,
|
||||
OriginalObjectID: insertData.ID,
|
||||
ActivityTypeKey: constant.ActAnswerEdited,
|
||||
RevisionID: revisionitem.ID,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) {
|
||||
taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp)
|
||||
if ok {
|
||||
tag := &entity.Tag{}
|
||||
tag.ID = taginfo.TagID
|
||||
tag.OriginalText = taginfo.OriginalText
|
||||
tag.ParsedText = taginfo.ParsedText
|
||||
saveerr := rs.tagRepo.UpdateTag(ctx, tag)
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
|
||||
tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.TagNotFound)
|
||||
}
|
||||
if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 {
|
||||
log.Debugf("tag %s update slug_name", tagInfo.SlugName)
|
||||
tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateTagSlugNames := make([]string, 0)
|
||||
for _, tag := range tagList {
|
||||
updateTagSlugNames = append(updateTagSlugNames, tag.SlugName)
|
||||
}
|
||||
err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: revisionitem.UserID,
|
||||
ObjectID: taginfo.TagID,
|
||||
OriginalObjectID: taginfo.TagID,
|
||||
ActivityTypeKey: constant.ActTagEdited,
|
||||
RevisionID: revisionitem.ID,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnreviewedRevisionPage get unreviewed list
|
||||
func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) (
|
||||
resp *pager.PageModel, err error) {
|
||||
revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0)
|
||||
if len(req.GetCanReviewObjectTypes()) == 0 {
|
||||
return pager.NewPageModel(0, revisionResp), nil
|
||||
}
|
||||
revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage(
|
||||
ctx, req.Page, 1, req.GetCanReviewObjectTypes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, rev := range revisionPage {
|
||||
item := &schema.GetUnreviewedRevisionResp{}
|
||||
_, ok := constant.ObjectTypeNumberMapping[rev.ObjectType]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType]
|
||||
info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Info = info
|
||||
revisionitem := &schema.GetRevisionResp{}
|
||||
_ = copier.Copy(revisionitem, rev)
|
||||
rs.parseItem(ctx, revisionitem)
|
||||
item.UnreviewedInfo = revisionitem
|
||||
|
||||
// get user info
|
||||
userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if exists {
|
||||
var uinfo schema.UserBasicInfo
|
||||
_ = copier.Copy(&uinfo, userInfo)
|
||||
item.UnreviewedInfo.UserInfo = uinfo
|
||||
}
|
||||
item.Info.UrlTitle = htmltext.UrlTitle(item.Info.Title)
|
||||
item.UnreviewedInfo.UrlTitle = htmltext.UrlTitle(item.UnreviewedInfo.Title)
|
||||
revisionResp = append(revisionResp, item)
|
||||
}
|
||||
return pager.NewPageModel(total, revisionResp), nil
|
||||
}
|
||||
|
||||
// GetRevisionList get revision list all
|
||||
func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) {
|
||||
var (
|
||||
rev entity.Revision
|
||||
revs []entity.Revision
|
||||
)
|
||||
|
||||
resp = []schema.GetRevisionResp{}
|
||||
objInfo, infoErr := rs.objectInfoService.GetInfo(ctx, req.ObjectID)
|
||||
if infoErr != nil {
|
||||
return nil, infoErr
|
||||
}
|
||||
if err := objInfo.CheckVisibility(req.UserID, req.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = copier.Copy(&rev, req)
|
||||
|
||||
revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range revs {
|
||||
var (
|
||||
uinfo schema.UserBasicInfo
|
||||
item schema.GetRevisionResp
|
||||
)
|
||||
|
||||
_ = copier.Copy(&item, r)
|
||||
rs.parseItem(ctx, &item)
|
||||
|
||||
// get user info
|
||||
userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if exists {
|
||||
err = copier.Copy(&uinfo, userInfo)
|
||||
item.UserInfo = uinfo
|
||||
}
|
||||
resp = append(resp, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) {
|
||||
var (
|
||||
err error
|
||||
question entity.QuestionWithTagsRevision
|
||||
questionInfo *schema.QuestionInfoResp
|
||||
answer entity.Answer
|
||||
answerInfo *schema.AnswerInfo
|
||||
tag entity.Tag
|
||||
tagInfo *schema.GetTagResp
|
||||
)
|
||||
|
||||
shortID := handler.GetEnableShortID(ctx)
|
||||
if shortID {
|
||||
item.ObjectID = uid.EnShortID(item.ObjectID)
|
||||
}
|
||||
switch item.ObjectType {
|
||||
case constant.ObjectTypeStrMapping["question"]:
|
||||
err = json.Unmarshal([]byte(item.Content), &question)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question)
|
||||
if shortID {
|
||||
questionInfo.ID = uid.EnShortID(questionInfo.ID)
|
||||
}
|
||||
item.ContentParsed = questionInfo
|
||||
case constant.ObjectTypeStrMapping["answer"]:
|
||||
err = json.Unmarshal([]byte(item.Content), &answer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
answerInfo = rs.answerService.ShowFormat(ctx, &answer)
|
||||
if shortID {
|
||||
answerInfo.ID = uid.EnShortID(answerInfo.ID)
|
||||
answerInfo.QuestionID = uid.EnShortID(answerInfo.QuestionID)
|
||||
}
|
||||
item.ContentParsed = answerInfo
|
||||
case constant.ObjectTypeStrMapping["tag"]:
|
||||
err = json.Unmarshal([]byte(item.Content), &tag)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
tagInfo = &schema.GetTagResp{
|
||||
TagID: tag.ID,
|
||||
CreatedAt: tag.CreatedAt.Unix(),
|
||||
UpdatedAt: tag.UpdatedAt.Unix(),
|
||||
SlugName: tag.SlugName,
|
||||
DisplayName: tag.DisplayName,
|
||||
OriginalText: tag.OriginalText,
|
||||
ParsedText: tag.ParsedText,
|
||||
FollowCount: tag.FollowCount,
|
||||
QuestionCount: tag.QuestionCount,
|
||||
Recommend: tag.Recommend,
|
||||
Reserved: tag.Reserved,
|
||||
}
|
||||
tagInfo.GetExcerpt()
|
||||
item.ContentParsed = tagInfo
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
item.ContentParsed = item.Content
|
||||
}
|
||||
item.CreatedAtParsed = item.CreatedAt.Unix()
|
||||
}
|
||||
|
||||
// CheckCanUpdateRevision can check revision
|
||||
func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) (
|
||||
resp *schema.ErrTypeData, err error) {
|
||||
_, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if exist {
|
||||
return &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetReviewingType get reviewing type
|
||||
func (rs *RevisionService) GetReviewingType(ctx context.Context, req *schema.GetReviewingTypeReq) (resp []*schema.GetReviewingTypeResp, err error) {
|
||||
resp = make([]*schema.GetReviewingTypeResp, 0)
|
||||
|
||||
// get queue amount
|
||||
if req.IsAdmin {
|
||||
reviewCount, err := rs.reviewService.GetReviewPendingCount(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get report count failed: %v", err)
|
||||
} else {
|
||||
resp = append(resp, &schema.GetReviewingTypeResp{
|
||||
Name: string(constant.QueuedPost),
|
||||
Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewQueuedPostLabel),
|
||||
TodoAmount: reviewCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// get flag amount
|
||||
if req.IsAdmin {
|
||||
reportCount, err := rs.reportRepo.GetReportCount(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get report count failed: %v", err)
|
||||
} else {
|
||||
resp = append(resp, &schema.GetReviewingTypeResp{
|
||||
Name: string(constant.FlaggedPost),
|
||||
Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewFlaggedPostLabel),
|
||||
TodoAmount: reportCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// get suggestion amount
|
||||
countUnreviewedRevision, err := rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes())
|
||||
if err != nil {
|
||||
log.Errorf("get unreviewed revision count failed: %v", err)
|
||||
} else {
|
||||
resp = append(resp, &schema.GetReviewingTypeResp{
|
||||
Name: string(constant.SuggestedPostEdit),
|
||||
Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewSuggestedPostEditLabel),
|
||||
TodoAmount: countUnreviewedRevision,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/internal/service/search_common"
|
||||
"github.com/apache/answer/internal/service/search_parser"
|
||||
"github.com/apache/answer/plugin"
|
||||
)
|
||||
|
||||
type SearchService struct {
|
||||
searchParser *search_parser.SearchParser
|
||||
searchRepo search_common.SearchRepo
|
||||
}
|
||||
|
||||
func NewSearchService(
|
||||
searchParser *search_parser.SearchParser,
|
||||
searchRepo search_common.SearchRepo,
|
||||
) *SearchService {
|
||||
return &SearchService{
|
||||
searchParser: searchParser,
|
||||
searchRepo: searchRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Search search contents
|
||||
func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) {
|
||||
if dto.Page < 1 {
|
||||
dto.Page = 1
|
||||
}
|
||||
if len(dto.Query) == 0 {
|
||||
return &schema.SearchResp{
|
||||
Total: 0,
|
||||
SearchResults: make([]*schema.SearchResult, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// search type
|
||||
cond := ss.searchParser.ParseStructure(ctx, dto)
|
||||
|
||||
// check search plugin
|
||||
var finder plugin.Search
|
||||
_ = plugin.CallSearch(func(search plugin.Search) error {
|
||||
finder = search
|
||||
return nil
|
||||
})
|
||||
|
||||
resp = &schema.SearchResp{}
|
||||
// search plugin is not found, call system search
|
||||
if finder == nil {
|
||||
switch {
|
||||
case cond.SearchAll():
|
||||
resp.SearchResults, resp.Total, err =
|
||||
ss.searchRepo.SearchContents(ctx, cond.Words, cond.Tags, cond.UserID, cond.VoteAmount, dto.Page, dto.Size, dto.Order)
|
||||
case cond.SearchQuestion():
|
||||
resp.SearchResults, resp.Total, err =
|
||||
ss.searchRepo.SearchQuestions(ctx, cond.Words, cond.Tags, cond.NotAccepted, cond.Views, cond.AnswerAmount, dto.Page, dto.Size, dto.Order)
|
||||
case cond.SearchAnswer():
|
||||
resp.SearchResults, resp.Total, err =
|
||||
ss.searchRepo.SearchAnswers(ctx, cond.Words, cond.Tags, cond.Accepted, cond.QuestionID, dto.Page, dto.Size, dto.Order)
|
||||
}
|
||||
return
|
||||
}
|
||||
return ss.searchByPlugin(ctx, finder, cond, dto)
|
||||
}
|
||||
|
||||
func (ss *SearchService) searchByPlugin(ctx context.Context, finder plugin.Search, cond *schema.SearchCondition, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) {
|
||||
var res []plugin.SearchResult
|
||||
resp = &schema.SearchResp{}
|
||||
switch {
|
||||
case cond.SearchAll():
|
||||
res, resp.Total, err = finder.SearchContents(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order))
|
||||
case cond.SearchQuestion():
|
||||
res, resp.Total, err = finder.SearchQuestions(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order))
|
||||
case cond.SearchAnswer():
|
||||
res, resp.Total, err = finder.SearchAnswers(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order))
|
||||
}
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp.SearchResults, err = ss.searchRepo.ParseSearchPluginResult(ctx, res, cond.Words)
|
||||
return resp, err
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplyRegistrationVerification(t *testing.T) {
|
||||
t.Run("required sends activation email and leaves email pending", func(t *testing.T) {
|
||||
userInfo := &entity.User{}
|
||||
calls := map[string]int{}
|
||||
|
||||
err := applyRegistrationVerification(userInfo, true, registrationVerificationActions{
|
||||
sendActivationEmail: func() error {
|
||||
calls["sendActivationEmail"]++
|
||||
return nil
|
||||
},
|
||||
activateUser: func() error {
|
||||
calls["activateUser"]++
|
||||
return nil
|
||||
},
|
||||
markEmailAvailable: func() error {
|
||||
calls["markEmailAvailable"]++
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.EmailStatusToBeVerified, userInfo.MailStatus)
|
||||
assert.Equal(t, 1, calls["sendActivationEmail"])
|
||||
assert.Zero(t, calls["activateUser"])
|
||||
assert.Zero(t, calls["markEmailAvailable"])
|
||||
})
|
||||
|
||||
t.Run("not required activates once and marks email available", func(t *testing.T) {
|
||||
userInfo := &entity.User{}
|
||||
calls := map[string]int{}
|
||||
|
||||
err := applyRegistrationVerification(userInfo, false, registrationVerificationActions{
|
||||
sendActivationEmail: func() error {
|
||||
calls["sendActivationEmail"]++
|
||||
return nil
|
||||
},
|
||||
activateUser: func() error {
|
||||
calls["activateUser"]++
|
||||
return nil
|
||||
},
|
||||
markEmailAvailable: func() error {
|
||||
calls["markEmailAvailable"]++
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.EmailStatusAvailable, userInfo.MailStatus)
|
||||
assert.Zero(t, calls["sendActivationEmail"])
|
||||
assert.Equal(t, 1, calls["activateUser"])
|
||||
assert.Equal(t, 1, calls["markEmailAvailable"])
|
||||
})
|
||||
|
||||
t.Run("not required user activation failure falls back to email verification", func(t *testing.T) {
|
||||
userInfo := &entity.User{}
|
||||
calls := map[string]int{}
|
||||
|
||||
err := applyRegistrationVerification(userInfo, false, registrationVerificationActions{
|
||||
sendActivationEmail: func() error {
|
||||
calls["sendActivationEmail"]++
|
||||
return nil
|
||||
},
|
||||
activateUser: func() error {
|
||||
calls["activateUser"]++
|
||||
return errors.New("activate failed")
|
||||
},
|
||||
markEmailAvailable: func() error {
|
||||
calls["markEmailAvailable"]++
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.EmailStatusToBeVerified, userInfo.MailStatus)
|
||||
assert.Equal(t, 1, calls["sendActivationEmail"])
|
||||
assert.Equal(t, 1, calls["activateUser"])
|
||||
assert.Zero(t, calls["markEmailAvailable"])
|
||||
})
|
||||
|
||||
t.Run("not required email status failure falls back to email verification", func(t *testing.T) {
|
||||
userInfo := &entity.User{}
|
||||
calls := map[string]int{}
|
||||
|
||||
err := applyRegistrationVerification(userInfo, false, registrationVerificationActions{
|
||||
sendActivationEmail: func() error {
|
||||
calls["sendActivationEmail"]++
|
||||
return nil
|
||||
},
|
||||
activateUser: func() error {
|
||||
calls["activateUser"]++
|
||||
return nil
|
||||
},
|
||||
markEmailAvailable: func() error {
|
||||
calls["markEmailAvailable"]++
|
||||
return errors.New("update failed")
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.EmailStatusToBeVerified, userInfo.MailStatus)
|
||||
assert.Equal(t, 1, calls["sendActivationEmail"])
|
||||
assert.Equal(t, 1, calls["activateUser"])
|
||||
assert.Equal(t, 1, calls["markEmailAvailable"])
|
||||
})
|
||||
|
||||
t.Run("fallback email failure returns before available email status", func(t *testing.T) {
|
||||
userInfo := &entity.User{}
|
||||
expectedErr := errors.New("email failed")
|
||||
|
||||
err := applyRegistrationVerification(userInfo, false, registrationVerificationActions{
|
||||
sendActivationEmail: func() error {
|
||||
return expectedErr
|
||||
},
|
||||
activateUser: func() error {
|
||||
return errors.New("activate failed")
|
||||
},
|
||||
markEmailAvailable: func() error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
assert.Equal(t, entity.EmailStatusToBeVerified, userInfo.MailStatus)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/apache/answer/internal/service/eventqueue"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/pager"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/service/activity_type"
|
||||
"github.com/apache/answer/internal/service/comment_common"
|
||||
"github.com/apache/answer/internal/service/config"
|
||||
"github.com/apache/answer/internal/service/object_info"
|
||||
"github.com/apache/answer/pkg/htmltext"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// VoteRepo activity repository
|
||||
type VoteRepo interface {
|
||||
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)
|
||||
}
|
||||
|
||||
// VoteService user service
|
||||
type VoteService struct {
|
||||
voteRepo VoteRepo
|
||||
configService *config.ConfigService
|
||||
questionRepo questioncommon.QuestionRepo
|
||||
answerRepo answercommon.AnswerRepo
|
||||
commentCommonRepo comment_common.CommentCommonRepo
|
||||
objectService *object_info.ObjService
|
||||
eventQueueService eventqueue.Service
|
||||
}
|
||||
|
||||
func NewVoteService(
|
||||
voteRepo VoteRepo,
|
||||
configService *config.ConfigService,
|
||||
questionRepo questioncommon.QuestionRepo,
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
commentCommonRepo comment_common.CommentCommonRepo,
|
||||
objectService *object_info.ObjService,
|
||||
eventQueueService eventqueue.Service,
|
||||
) *VoteService {
|
||||
return &VoteService{
|
||||
voteRepo: voteRepo,
|
||||
configService: configService,
|
||||
questionRepo: questionRepo,
|
||||
answerRepo: answerRepo,
|
||||
commentCommonRepo: commentCommonRepo,
|
||||
objectService: objectService,
|
||||
eventQueueService: eventQueueService,
|
||||
}
|
||||
}
|
||||
|
||||
// VoteUp vote up
|
||||
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 nil, err
|
||||
}
|
||||
if objectInfo.IsDeleted() {
|
||||
return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)
|
||||
}
|
||||
// make object id must be decoded
|
||||
objectInfo.ObjectID = req.ObjectID
|
||||
|
||||
// check user is voting self or not
|
||||
if objectInfo.ObjectCreatorUserID == req.UserID {
|
||||
return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
|
||||
}
|
||||
|
||||
voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)
|
||||
|
||||
// vote operation
|
||||
if req.IsCancel {
|
||||
err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
|
||||
} else {
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
vs.sendEvent(ctx, req, objectInfo, resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// VoteDown vote down
|
||||
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 nil, err
|
||||
}
|
||||
if objectInfo.IsDeleted() {
|
||||
return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted)
|
||||
}
|
||||
// make object id must be decoded
|
||||
objectInfo.ObjectID = req.ObjectID
|
||||
|
||||
// check user is voting self or not
|
||||
if objectInfo.ObjectCreatorUserID == req.UserID {
|
||||
return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
|
||||
}
|
||||
|
||||
// vote operation
|
||||
voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
|
||||
if req.IsCancel {
|
||||
err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
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.ActVoteDown
|
||||
vs.sendEvent(ctx, req, objectInfo, resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListUserVotes list user's votes
|
||||
func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) {
|
||||
typeKeys := []string{
|
||||
activity_type.QuestionVoteUp,
|
||||
activity_type.QuestionVoteDown,
|
||||
activity_type.AnswerVoteUp,
|
||||
activity_type.AnswerVoteDown,
|
||||
}
|
||||
activityTypes := make([]int, 0)
|
||||
activityTypeMapping := make(map[int]string, 0)
|
||||
|
||||
for _, typeKey := range typeKeys {
|
||||
cfg, err := vs.configService.GetConfigByKey(ctx, typeKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
activityTypes = append(activityTypes, cfg.ID)
|
||||
activityTypeMapping[cfg.ID] = typeKey
|
||||
}
|
||||
|
||||
voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
|
||||
votes := make([]*schema.GetVoteWithPageResp, 0)
|
||||
for _, voteInfo := range voteList {
|
||||
objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
item := &schema.GetVoteWithPageResp{
|
||||
CreatedAt: voteInfo.CreatedAt.Unix(),
|
||||
ObjectID: objInfo.ObjectID,
|
||||
QuestionID: objInfo.QuestionID,
|
||||
AnswerID: objInfo.AnswerID,
|
||||
ObjectType: objInfo.ObjectType,
|
||||
Title: objInfo.Title,
|
||||
UrlTitle: htmltext.UrlTitle(objInfo.Title),
|
||||
Content: objInfo.Content,
|
||||
}
|
||||
item.VoteType = translator.Tr(lang,
|
||||
activity_type.ActivityTypeFlagMapping[activityTypeMapping[voteInfo.ActivityType]])
|
||||
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
|
||||
item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey)
|
||||
}
|
||||
votes = append(votes, item)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (vs *VoteService) sendEvent(ctx context.Context,
|
||||
req *schema.VoteReq, objectInfo *schema.SimpleObjectInfo, resp *schema.VoteResp) {
|
||||
var event *schema.EventMsg
|
||||
switch objectInfo.ObjectType {
|
||||
case constant.QuestionObjectType:
|
||||
event = schema.NewEvent(constant.EventQuestionVote, req.UserID).TID(objectInfo.QuestionID).
|
||||
QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID)
|
||||
case constant.AnswerObjectType:
|
||||
event = schema.NewEvent(constant.EventAnswerVote, req.UserID).TID(objectInfo.AnswerID).
|
||||
AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID)
|
||||
case constant.CommentObjectType:
|
||||
event = schema.NewEvent(constant.EventCommentVote, req.UserID).TID(objectInfo.CommentID).
|
||||
CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID)
|
||||
default:
|
||||
return
|
||||
}
|
||||
event.AddExtra("vote_up_amount", fmt.Sprintf("%d", resp.UpVotes))
|
||||
event.AddExtra("vote_down_amount", fmt.Sprintf("%d", resp.DownVotes))
|
||||
vs.eventQueueService.Send(ctx, event)
|
||||
}
|
||||
Reference in New Issue
Block a user