chore: import upstream snapshot with attribution
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:32 +08:00
commit 39dbe3a57d
1131 changed files with 272709 additions and 0 deletions
+819
View File
@@ -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
}
+104
View File
@@ -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)
})
}
+320
View File
@@ -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)
}