chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,908 @@
|
||||
/*
|
||||
* 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 questioncommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/service/siteinfo_common"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/data"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/service/activity_common"
|
||||
"github.com/apache/answer/internal/service/activityqueue"
|
||||
"github.com/apache/answer/internal/service/config"
|
||||
metacommon "github.com/apache/answer/internal/service/meta_common"
|
||||
"github.com/apache/answer/internal/service/revision"
|
||||
"github.com/apache/answer/pkg/checker"
|
||||
"github.com/apache/answer/pkg/htmltext"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
collectioncommon "github.com/apache/answer/internal/service/collection_common"
|
||||
tagcommon "github.com/apache/answer/internal/service/tag_common"
|
||||
usercommon "github.com/apache/answer/internal/service/user_common"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// QuestionRepo question repository
|
||||
type QuestionRepo interface {
|
||||
AddQuestion(ctx context.Context, question *entity.Question) (err error)
|
||||
RemoveQuestion(ctx context.Context, id string) (err error)
|
||||
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
|
||||
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
|
||||
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) (
|
||||
questionList []*entity.Question, total int64, err error)
|
||||
GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error)
|
||||
UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error)
|
||||
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
|
||||
DeletePermanentlyQuestions(ctx context.Context) (err error)
|
||||
RecoverQuestion(ctx context.Context, questionID string) (err error)
|
||||
UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error)
|
||||
GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error)
|
||||
UpdatePvCount(ctx context.Context, questionID string) (err error)
|
||||
UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)
|
||||
UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error)
|
||||
UpdateAccepted(ctx context.Context, question *entity.Question) (err error)
|
||||
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
|
||||
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
|
||||
AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error)
|
||||
GetQuestionCount(ctx context.Context) (count int64, err error)
|
||||
GetUnansweredQuestionCount(ctx context.Context) (count int64, err error)
|
||||
GetResolvedQuestionCount(ctx context.Context) (count int64, err error)
|
||||
GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error)
|
||||
SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
|
||||
RemoveAllUserQuestion(ctx context.Context, userID string) (err error)
|
||||
UpdateSearch(ctx context.Context, questionID string) (err error)
|
||||
LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error)
|
||||
GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (questionIDs []string, err error)
|
||||
UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error)
|
||||
RemoveQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)
|
||||
RecoverQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)
|
||||
UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error)
|
||||
GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questions []*entity.Question, total int64, err error)
|
||||
}
|
||||
|
||||
// QuestionCommon user service
|
||||
type QuestionCommon struct {
|
||||
questionRepo QuestionRepo
|
||||
answerRepo answercommon.AnswerRepo
|
||||
voteRepo activity_common.VoteRepo
|
||||
followCommon activity_common.FollowRepo
|
||||
tagCommon *tagcommon.TagCommonService
|
||||
userCommon *usercommon.UserCommon
|
||||
collectionCommon *collectioncommon.CollectionCommon
|
||||
AnswerCommon *answercommon.AnswerCommon
|
||||
metaCommonService *metacommon.MetaCommonService
|
||||
configService *config.ConfigService
|
||||
activityQueueService activityqueue.Service
|
||||
revisionRepo revision.RevisionRepo
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
data *data.Data
|
||||
}
|
||||
|
||||
func NewQuestionCommon(questionRepo QuestionRepo,
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
voteRepo activity_common.VoteRepo,
|
||||
followCommon activity_common.FollowRepo,
|
||||
tagCommon *tagcommon.TagCommonService,
|
||||
userCommon *usercommon.UserCommon,
|
||||
collectionCommon *collectioncommon.CollectionCommon,
|
||||
answerCommon *answercommon.AnswerCommon,
|
||||
metaCommonService *metacommon.MetaCommonService,
|
||||
configService *config.ConfigService,
|
||||
activityQueueService activityqueue.Service,
|
||||
revisionRepo revision.RevisionRepo,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
data *data.Data,
|
||||
) *QuestionCommon {
|
||||
return &QuestionCommon{
|
||||
questionRepo: questionRepo,
|
||||
answerRepo: answerRepo,
|
||||
voteRepo: voteRepo,
|
||||
followCommon: followCommon,
|
||||
tagCommon: tagCommon,
|
||||
userCommon: userCommon,
|
||||
collectionCommon: collectionCommon,
|
||||
AnswerCommon: answerCommon,
|
||||
metaCommonService: metaCommonService,
|
||||
configService: configService,
|
||||
activityQueueService: activityQueueService,
|
||||
revisionRepo: revisionRepo,
|
||||
siteInfoService: siteInfoService,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
|
||||
return qs.questionRepo.GetUserQuestionCount(ctx, userID, 0)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) GetPersonalUserQuestionCount(ctx context.Context, loginUserID, userID string, isAdmin bool) (count int64, err error) {
|
||||
show := entity.QuestionShow
|
||||
if loginUserID == userID || isAdmin {
|
||||
show = 0
|
||||
}
|
||||
return qs.questionRepo.GetUserQuestionCount(ctx, userID, show)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error {
|
||||
return qs.questionRepo.UpdatePvCount(ctx, questionID)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID string) error {
|
||||
count, err := qs.answerRepo.GetCountByQuestionID(ctx, questionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
err = qs.questionRepo.UpdateLastAnswer(ctx, &entity.Question{
|
||||
ID: questionID,
|
||||
LastAnswerID: "0",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return qs.questionRepo.UpdateAnswerCount(ctx, questionID, int(count))
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) {
|
||||
return qs.questionRepo.UpdateCollectionCount(ctx, questionID)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionID, answerID string) error {
|
||||
question := &entity.Question{}
|
||||
question.ID = questionID
|
||||
question.AcceptedAnswerID = answerID
|
||||
return qs.questionRepo.UpdateAccepted(ctx, question)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, answerID string) error {
|
||||
question := &entity.Question{}
|
||||
question.ID = questionID
|
||||
question.LastAnswerID = answerID
|
||||
return qs.questionRepo.UpdateLastAnswer(ctx, question)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error {
|
||||
questioninfo := &entity.Question{}
|
||||
now := time.Now()
|
||||
questioninfo.ID = questionID
|
||||
questioninfo.PostUpdateTime = now
|
||||
return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
|
||||
}
|
||||
func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error {
|
||||
questioninfo := &entity.Question{}
|
||||
questioninfo.ID = questionID
|
||||
questioninfo.PostUpdateTime = setTime
|
||||
return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"})
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfoResp, error) {
|
||||
list := make(map[string]*schema.QuestionInfoResp)
|
||||
questionList, err := qs.questionRepo.FindByID(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
questions, err := qs.FormatQuestions(ctx, questionList, loginUserID)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
for _, item := range questions {
|
||||
list[item.ID] = item
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
|
||||
InviteUserInfo := make([]*schema.UserBasicInfo, 0)
|
||||
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
return InviteUserInfo, err
|
||||
}
|
||||
if !has {
|
||||
return InviteUserInfo, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
// InviteUser
|
||||
if dbinfo.InviteUserID != "" {
|
||||
InviteUserIDs := make([]string, 0)
|
||||
err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs)
|
||||
if err == nil {
|
||||
inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs)
|
||||
if err == nil {
|
||||
for _, userid := range InviteUserIDs {
|
||||
_, ok := inviteUserInfoMap[userid]
|
||||
if ok {
|
||||
InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return InviteUserInfo, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (resp *schema.QuestionInfoResp, err error) {
|
||||
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
questionInfo.ID = uid.DeShortID(questionInfo.ID)
|
||||
if !has {
|
||||
return resp, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
resp = qs.ShowFormat(ctx, questionInfo)
|
||||
if resp.Status == entity.QuestionStatusClosed {
|
||||
metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
closeMsg := &schema.CloseQuestionMeta{}
|
||||
err = json.Unmarshal([]byte(metaInfo.Value), closeMsg)
|
||||
if err != nil {
|
||||
log.Error("json.Unmarshal CloseQuestionMeta error", err.Error())
|
||||
} else {
|
||||
cfg, err := qs.configService.GetConfigByID(ctx, closeMsg.CloseType)
|
||||
if err != nil {
|
||||
log.Error("json.Unmarshal QuestionCloseJson error", err.Error())
|
||||
} else {
|
||||
reasonItem := &schema.ReasonItem{}
|
||||
_ = json.Unmarshal(cfg.GetByteValue(), reasonItem)
|
||||
reasonItem.Translate(cfg.Key, handler.GetLangByCtx(ctx))
|
||||
operation := &schema.Operation{}
|
||||
operation.Type = reasonItem.Name
|
||||
operation.Description = reasonItem.Description
|
||||
operation.Msg = closeMsg.CloseMsg
|
||||
operation.Time = metaInfo.CreatedAt.Unix()
|
||||
operation.Level = schema.OperationLevelInfo
|
||||
resp.Operation = operation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Status != entity.QuestionStatusDeleted {
|
||||
if resp.Tags, err = qs.tagCommon.GetObjectTag(ctx, questionID); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
} else {
|
||||
revisionInfo, exist, err := qs.revisionRepo.GetLastRevisionByObjectID(ctx, questionID)
|
||||
if err != nil {
|
||||
log.Errorf("get revision error %s", err)
|
||||
}
|
||||
if exist {
|
||||
questionWithTagsRevision := &entity.QuestionWithTagsRevision{}
|
||||
if err = json.Unmarshal([]byte(revisionInfo.Content), questionWithTagsRevision); err != nil {
|
||||
log.Errorf("revision parsing error %s", err)
|
||||
return resp, nil
|
||||
}
|
||||
for _, tag := range questionWithTagsRevision.Tags {
|
||||
resp.Tags = append(resp.Tags, &schema.TagResp{
|
||||
ID: tag.ID,
|
||||
SlugName: tag.SlugName,
|
||||
DisplayName: tag.DisplayName,
|
||||
MainTagSlugName: tag.MainTagSlugName,
|
||||
Recommend: tag.Recommend,
|
||||
Reserved: tag.Reserved,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userIds := make([]string, 0)
|
||||
if checker.IsNotZeroString(questionInfo.UserID) {
|
||||
userIds = append(userIds, questionInfo.UserID)
|
||||
}
|
||||
if checker.IsNotZeroString(questionInfo.LastEditUserID) {
|
||||
userIds = append(userIds, questionInfo.LastEditUserID)
|
||||
}
|
||||
if checker.IsNotZeroString(resp.LastAnsweredUserID) {
|
||||
userIds = append(userIds, resp.LastAnsweredUserID)
|
||||
}
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
resp.UserInfo = userInfoMap[questionInfo.UserID]
|
||||
resp.UpdateUserInfo = userInfoMap[questionInfo.LastEditUserID]
|
||||
resp.LastAnsweredUserInfo = userInfoMap[resp.LastAnsweredUserID]
|
||||
if len(loginUserID) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID)
|
||||
resp.IsFollowed, _ = qs.followCommon.IsFollowed(ctx, loginUserID, questionID)
|
||||
|
||||
ids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, questionInfo.ID)
|
||||
if err != nil {
|
||||
log.Error("AnswerFunc.SearchAnswerIDs", err)
|
||||
}
|
||||
resp.Answered = len(ids) > 0
|
||||
if resp.Answered {
|
||||
resp.FirstAnswerId = ids[0]
|
||||
}
|
||||
|
||||
collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{questionInfo.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(collectedMap) > 0 {
|
||||
resp.Collected = true
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) FormatQuestionsPage(
|
||||
ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) (
|
||||
formattedQuestions []*schema.QuestionPageResp, err error) {
|
||||
formattedQuestions = make([]*schema.QuestionPageResp, 0)
|
||||
questionIDs := make([]string, 0)
|
||||
userIDs := make([]string, 0)
|
||||
for _, questionInfo := range questionList {
|
||||
t := &schema.QuestionPageResp{
|
||||
ID: questionInfo.ID,
|
||||
CreatedAt: questionInfo.CreatedAt.Unix(),
|
||||
Title: questionInfo.Title,
|
||||
UrlTitle: htmltext.UrlTitle(questionInfo.Title),
|
||||
Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240),
|
||||
Status: questionInfo.Status,
|
||||
ViewCount: questionInfo.ViewCount,
|
||||
UniqueViewCount: questionInfo.UniqueViewCount,
|
||||
VoteCount: questionInfo.VoteCount,
|
||||
AnswerCount: questionInfo.AnswerCount,
|
||||
CollectionCount: questionInfo.CollectionCount,
|
||||
FollowCount: questionInfo.FollowCount,
|
||||
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
|
||||
LastAnswerID: questionInfo.LastAnswerID,
|
||||
Pin: questionInfo.Pin,
|
||||
Show: questionInfo.Show,
|
||||
Operator: &schema.QuestionPageRespOperator{ID: questionInfo.UserID},
|
||||
}
|
||||
|
||||
questionIDs = append(questionIDs, questionInfo.ID)
|
||||
userIDs = append(userIDs, questionInfo.UserID)
|
||||
haveEdited, haveAnswered := false, false
|
||||
if checker.IsNotZeroString(questionInfo.LastEditUserID) {
|
||||
haveEdited = true
|
||||
userIDs = append(userIDs, questionInfo.LastEditUserID)
|
||||
}
|
||||
if checker.IsNotZeroString(questionInfo.LastAnswerID) {
|
||||
haveAnswered = true
|
||||
|
||||
answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID)
|
||||
if err == nil && exist {
|
||||
if answerInfo.LastEditUserID != "0" {
|
||||
t.LastAnsweredUserID = answerInfo.LastEditUserID
|
||||
} else {
|
||||
t.LastAnsweredUserID = answerInfo.UserID
|
||||
}
|
||||
t.LastAnsweredAt = answerInfo.CreatedAt
|
||||
userIDs = append(userIDs, t.LastAnsweredUserID)
|
||||
}
|
||||
}
|
||||
|
||||
// The default operation is to ask questions
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeAsked
|
||||
t.OperatedAt = questionInfo.CreatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID}
|
||||
|
||||
// If the order is active, the last operation time is the last edit or answer time if it exists
|
||||
if orderCond == schema.QuestionOrderCondActive {
|
||||
if haveEdited {
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeModified
|
||||
t.OperatedAt = questionInfo.UpdatedAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID}
|
||||
}
|
||||
if haveAnswered {
|
||||
if t.LastAnsweredAt.Unix() > t.OperatedAt {
|
||||
t.OperationType = schema.QuestionPageRespOperationTypeAnswered
|
||||
t.OperatedAt = t.LastAnsweredAt.Unix()
|
||||
t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formattedQuestions = append(formattedQuestions, t)
|
||||
}
|
||||
|
||||
tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return formattedQuestions, err
|
||||
}
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
return formattedQuestions, err
|
||||
}
|
||||
|
||||
for _, item := range formattedQuestions {
|
||||
tags, ok := tagsMap[item.ID]
|
||||
if ok {
|
||||
item.Tags = tags
|
||||
} else {
|
||||
item.Tags = make([]*schema.TagResp, 0)
|
||||
}
|
||||
userInfo, ok := userInfoMap[item.Operator.ID]
|
||||
if ok {
|
||||
if userInfo != nil {
|
||||
item.Operator.DisplayName = userInfo.DisplayName
|
||||
item.Operator.Username = userInfo.Username
|
||||
item.Operator.Rank = userInfo.Rank
|
||||
item.Operator.Status = userInfo.Status
|
||||
item.Operator.Avatar = userInfo.Avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
return formattedQuestions, nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfoResp, error) {
|
||||
list := make([]*schema.QuestionInfoResp, 0)
|
||||
objectIds := make([]string, 0)
|
||||
userIds := make([]string, 0)
|
||||
|
||||
for _, questionInfo := range questionList {
|
||||
item := qs.ShowFormat(ctx, questionInfo)
|
||||
list = append(list, item)
|
||||
objectIds = append(objectIds, item.ID)
|
||||
userIds = append(userIds, item.UserID, item.LastEditUserID, item.LastAnsweredUserID)
|
||||
}
|
||||
tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
item.Tags = tagsMap[item.ID]
|
||||
item.UserInfo = userInfoMap[item.UserID]
|
||||
item.UpdateUserInfo = userInfoMap[item.LastEditUserID]
|
||||
item.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID]
|
||||
}
|
||||
if loginUserID == "" {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
item.Collected = collectedMap[item.ID]
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// RemoveQuestion delete question
|
||||
func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
|
||||
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
|
||||
if questionInfo.Status == entity.QuestionStatusDeleted {
|
||||
return nil
|
||||
}
|
||||
|
||||
questionInfo.Status = entity.QuestionStatusDeleted
|
||||
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userQuestionCount, err := qs.GetUserQuestionCount(ctx, questionInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("user GetUserQuestionCount error", err.Error())
|
||||
} else {
|
||||
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
|
||||
if err != nil {
|
||||
log.Error("user IncreaseQuestionCount error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error {
|
||||
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
questionInfo.Status = entity.QuestionStatusClosed
|
||||
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
closeMeta, _ := json.Marshal(schema.CloseQuestionMeta{
|
||||
CloseType: req.CloseType,
|
||||
CloseMsg: req.CloseMsg,
|
||||
})
|
||||
err = qs.metaCommonService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: questionInfo.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
OriginalObjectID: questionInfo.ID,
|
||||
ActivityTypeKey: constant.ActQuestionClosed,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAnswer delete answer
|
||||
func (qs *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) {
|
||||
answerinfo, has, err := qs.answerRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
|
||||
// user add question count
|
||||
|
||||
err = qs.UpdateAnswerCount(ctx, answerinfo.QuestionID)
|
||||
if err != nil {
|
||||
log.Error("UpdateAnswerCount error", err.Error())
|
||||
}
|
||||
userAnswerCount, err := qs.answerRepo.GetCountByUserID(ctx, answerinfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("GetCountByUserID error", err.Error())
|
||||
}
|
||||
err = qs.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount))
|
||||
if err != nil {
|
||||
log.Error("user UpdateAnswerCount error", err.Error())
|
||||
}
|
||||
|
||||
return qs.answerRepo.RemoveAnswer(ctx, id)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) SitemapCron(ctx context.Context) {
|
||||
questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if questionNum <= constant.SitemapMaxSize {
|
||||
_, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum))
|
||||
if err != nil {
|
||||
log.Errorf("get site map question error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))
|
||||
for i := 1; i <= totalPages; i++ {
|
||||
_, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize)
|
||||
if err != nil {
|
||||
log.Errorf("get site map question error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info any) error {
|
||||
infoStr, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
|
||||
err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp {
|
||||
return qs.ShowFormat(ctx, data)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp {
|
||||
info := schema.QuestionInfoResp{}
|
||||
info.ID = data.ID
|
||||
if handler.GetEnableShortID(ctx) {
|
||||
info.ID = uid.EnShortID(data.ID)
|
||||
}
|
||||
info.Title = data.Title
|
||||
info.UrlTitle = htmltext.UrlTitle(data.Title)
|
||||
info.Content = data.OriginalText
|
||||
info.HTML = data.ParsedText
|
||||
info.ViewCount = data.ViewCount
|
||||
info.UniqueViewCount = data.UniqueViewCount
|
||||
info.VoteCount = data.VoteCount
|
||||
info.AnswerCount = data.AnswerCount
|
||||
info.CollectionCount = data.CollectionCount
|
||||
info.FollowCount = data.FollowCount
|
||||
info.AcceptedAnswerID = data.AcceptedAnswerID
|
||||
info.LastAnswerID = data.LastAnswerID
|
||||
info.CreateTime = data.CreatedAt.Unix()
|
||||
info.UpdateTime = data.UpdatedAt.Unix()
|
||||
info.PostUpdateTime = data.PostUpdateTime.Unix()
|
||||
if data.PostUpdateTime.Unix() < 1 {
|
||||
info.PostUpdateTime = 0
|
||||
}
|
||||
info.QuestionUpdateTime = data.UpdatedAt.Unix()
|
||||
if data.UpdatedAt.Unix() < 1 {
|
||||
info.QuestionUpdateTime = 0
|
||||
}
|
||||
info.Status = data.Status
|
||||
info.Pin = data.Pin
|
||||
info.Show = data.Show
|
||||
info.UserID = data.UserID
|
||||
info.LastEditUserID = data.LastEditUserID
|
||||
if data.LastAnswerID != "0" {
|
||||
answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID)
|
||||
if err == nil && exist {
|
||||
if answerInfo.LastEditUserID != "0" {
|
||||
info.LastAnsweredUserID = answerInfo.LastEditUserID
|
||||
} else {
|
||||
info.LastAnsweredUserID = answerInfo.UserID
|
||||
}
|
||||
}
|
||||
}
|
||||
info.Tags = make([]*schema.TagResp, 0)
|
||||
return &info
|
||||
}
|
||||
func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfoResp {
|
||||
info := qs.ShowFormat(ctx, &data.Question)
|
||||
Tags := make([]*schema.TagResp, 0)
|
||||
for _, tag := range data.Tags {
|
||||
item := &schema.TagResp{}
|
||||
item.SlugName = tag.SlugName
|
||||
item.DisplayName = tag.DisplayName
|
||||
item.Recommend = tag.Recommend
|
||||
item.Reserved = tag.Reserved
|
||||
Tags = append(Tags, item)
|
||||
}
|
||||
info.Tags = Tags
|
||||
return info
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, answerID, parsedText, originalText string) (string, error) {
|
||||
err := qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: uid.DeShortID(questionID),
|
||||
FromAnswerID: uid.DeShortID(answerID),
|
||||
})
|
||||
if err != nil {
|
||||
return parsedText, err
|
||||
}
|
||||
// Update the number of question links that have been removed
|
||||
linkedQuestionIDs, err := qs.questionRepo.GetLinkedQuestionIDs(ctx, uid.DeShortID(questionID), entity.QuestionLinkStatusDeleted)
|
||||
if err != nil {
|
||||
log.Errorf("get linked question ids error %v", err)
|
||||
} else {
|
||||
for _, id := range linkedQuestionIDs {
|
||||
if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, id); err != nil {
|
||||
log.Errorf("update question link count error %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
links := checker.GetQuestionLink(originalText)
|
||||
if len(links) == 0 {
|
||||
return parsedText, nil
|
||||
}
|
||||
|
||||
// get answer ids and question ids
|
||||
answerIDs := make([]string, 0, len(links))
|
||||
questionIDs := make([]string, 0, len(links))
|
||||
for _, link := range links {
|
||||
if link.AnswerID != "" {
|
||||
answerIDs = append(answerIDs, link.AnswerID)
|
||||
}
|
||||
if link.QuestionID != "" {
|
||||
questionIDs = append(questionIDs, link.QuestionID)
|
||||
}
|
||||
}
|
||||
|
||||
// get answer info and build cache
|
||||
answerInfoList, err := qs.answerRepo.GetByIDs(ctx, answerIDs...)
|
||||
if err != nil {
|
||||
return parsedText, err
|
||||
}
|
||||
answerCache := make(map[string]string, len(answerInfoList))
|
||||
for _, ans := range answerInfoList {
|
||||
answerID := uid.DeShortID(ans.ID)
|
||||
questionID := ans.QuestionID
|
||||
answerCache[answerID] = questionID
|
||||
}
|
||||
|
||||
// get question info and build cache
|
||||
questionInfoList, err := qs.questionRepo.FindByID(ctx, questionIDs)
|
||||
if err != nil {
|
||||
return parsedText, err
|
||||
}
|
||||
questionCache := make(map[string]struct{}, len(questionInfoList))
|
||||
for _, q := range questionInfoList {
|
||||
questionID := uid.DeShortID(q.ID)
|
||||
questionCache[questionID] = struct{}{}
|
||||
}
|
||||
|
||||
// process links and generate new QuestionLink
|
||||
validLinks := make([]*entity.QuestionLink, 0, len(links))
|
||||
for _, link := range links {
|
||||
linkQuestionID := uid.DeShortID(link.QuestionID)
|
||||
linkAnswerID := uid.DeShortID(link.AnswerID)
|
||||
// validate question id
|
||||
if _, exists := questionCache[linkQuestionID]; linkQuestionID != "0" && !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// validate answer id
|
||||
if linkAnswerID != "0" {
|
||||
linkedQuestionID, exists := answerCache[linkAnswerID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
// if question id is empty, get it from answer cache
|
||||
if link.QuestionID == "" {
|
||||
link.QuestionID = linkedQuestionID
|
||||
}
|
||||
}
|
||||
|
||||
// build new link
|
||||
newLink := &entity.QuestionLink{
|
||||
FromQuestionID: uid.DeShortID(questionID),
|
||||
FromAnswerID: uid.DeShortID(answerID),
|
||||
ToQuestionID: uid.DeShortID(link.QuestionID),
|
||||
ToAnswerID: uid.DeShortID(link.AnswerID),
|
||||
}
|
||||
// replace link in parsed text
|
||||
if link.QuestionID != "" {
|
||||
htmlLink := fmt.Sprintf("<a href=\"/questions/%s\">#%s</a>", link.QuestionID, link.QuestionID)
|
||||
parsedText = strings.ReplaceAll(parsedText, "#"+link.QuestionID, htmlLink)
|
||||
}
|
||||
if link.AnswerID != "" {
|
||||
linkedQuestionID := answerCache[linkAnswerID]
|
||||
htmlLink := fmt.Sprintf("<a href=\"/questions/%s/%s\">#%s</a>", linkedQuestionID, link.AnswerID, link.AnswerID)
|
||||
parsedText = strings.ReplaceAll(parsedText, "#"+link.AnswerID, htmlLink)
|
||||
newLink.ToQuestionID = uid.DeShortID(linkedQuestionID)
|
||||
}
|
||||
// avoid link to self
|
||||
if newLink.FromQuestionID != newLink.ToQuestionID {
|
||||
validLinks = append(validLinks, newLink)
|
||||
}
|
||||
}
|
||||
|
||||
// add new links to repo
|
||||
if len(validLinks) > 0 {
|
||||
err = qs.questionRepo.LinkQuestion(ctx, validLinks...)
|
||||
if err != nil {
|
||||
return parsedText, err
|
||||
}
|
||||
}
|
||||
|
||||
// update question linked count
|
||||
for _, link := range validLinks {
|
||||
if len(link.ToQuestionID) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, link.ToQuestionID); err != nil {
|
||||
log.Errorf("update question link count error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedText, nil
|
||||
}
|
||||
|
||||
// AddQuestionLinkForCloseReason When the reason about close question is a question link, add the link to the question
|
||||
func (qs *QuestionCommon) AddQuestionLinkForCloseReason(ctx context.Context,
|
||||
questionInfo *entity.Question, closeMsg string) {
|
||||
questionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsg)
|
||||
if len(questionID) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
linkedQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
log.Errorf("get question error %s", err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
err = qs.questionRepo.LinkQuestion(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: questionInfo.ID,
|
||||
ToQuestionID: linkedQuestion.ID,
|
||||
Status: entity.QuestionLinkStatusAvailable,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("link question error %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) RemoveQuestionLinkForReopen(ctx context.Context, questionInfo *entity.Question) {
|
||||
questionInfo.ID = uid.DeShortID(questionInfo.ID)
|
||||
metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
closeMsgMeta := &schema.CloseQuestionMeta{}
|
||||
_ = json.Unmarshal([]byte(metaInfo.Value), closeMsgMeta)
|
||||
|
||||
linkedQuestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsgMeta.CloseMsg)
|
||||
if len(linkedQuestionID) == 0 {
|
||||
return
|
||||
}
|
||||
err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: questionInfo.ID,
|
||||
ToQuestionID: linkedQuestionID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("remove question link error %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMsg string) (questionID string) {
|
||||
siteGeneral, err := qs.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general error %s", err)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(closeMsg, siteGeneral.SiteUrl) {
|
||||
return
|
||||
}
|
||||
// get question id from url
|
||||
// the url may like: https://xxx.com/questions/D1401/xxx
|
||||
// the D1401 is question id
|
||||
questionID = strings.TrimPrefix(closeMsg, siteGeneral.SiteUrl)
|
||||
questionID = strings.TrimPrefix(questionID, "/questions/")
|
||||
t := strings.Split(questionID, "/")
|
||||
if len(t) < 1 {
|
||||
return ""
|
||||
}
|
||||
questionID = t[0]
|
||||
questionID = uid.DeShortID(questionID)
|
||||
return questionID
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) GetMinimumContentLength(ctx context.Context) (int, error) {
|
||||
siteInfo, err := qs.siteInfoService.GetSiteQuestion(ctx)
|
||||
if err != nil {
|
||||
return 6, err
|
||||
}
|
||||
return siteInfo.MinimumContent, nil
|
||||
}
|
||||
Reference in New Issue
Block a user