909 lines
31 KiB
Go
909 lines
31 KiB
Go
/*
|
|
* 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
|
|
}
|