internal/service/question_common/question.go (785 lines of code) (raw):
/*
* 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/activity_queue"
"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 activity_queue.ActivityQueueService
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 activity_queue.ActivityQueueService,
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 interface{}) 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
}