diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index d8e6c744b..801c5a776 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -177,7 +177,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, answerCommon := answercommon.NewAnswerCommon(answerRepo) metaRepo := meta.NewMetaRepo(dataData) metaCommonService := metacommon.NewMetaCommonService(metaRepo) - questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, activityQueueService, revisionRepo, dataData) + questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, activityQueueService, revisionRepo, siteInfoCommonService, dataData) eventQueueService := event_queue.NewEventQueueService() userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon, eventQueueService) captchaRepo := captcha.NewCaptchaRepo(dataData) diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index 7cfd39a09..51f0da9b5 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -174,6 +174,9 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ if err != nil { return err } + if cf.Key == constant.ReasonADuplicate { + qs.questioncommon.AddQuestionLinkForCloseReason(ctx, questionInfo, req.CloseMsg) + } qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, @@ -199,6 +202,7 @@ func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.Reope if err != nil { return err } + qs.questioncommon.RemoveQuestionLinkForReopen(ctx, questionInfo) qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ UserID: req.UserID, ObjectID: questionInfo.ID, diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 63c8f5a2a..13df33896 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -23,6 +23,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/apache/incubator-answer/internal/service/siteinfo_common" "math" "strings" "time" @@ -98,6 +99,7 @@ type QuestionCommon struct { configService *config.ConfigService activityQueueService activity_queue.ActivityQueueService revisionRepo revision.RevisionRepo + siteInfoService siteinfo_common.SiteInfoCommonService data *data.Data } @@ -113,6 +115,7 @@ func NewQuestionCommon(questionRepo QuestionRepo, configService *config.ConfigService, activityQueueService activity_queue.ActivityQueueService, revisionRepo revision.RevisionRepo, + siteInfoService siteinfo_common.SiteInfoCommonService, data *data.Data, ) *QuestionCommon { return &QuestionCommon{ @@ -128,6 +131,7 @@ func NewQuestionCommon(questionRepo QuestionRepo, configService: configService, activityQueueService: activityQueueService, revisionRepo: revisionRepo, + siteInfoService: siteInfoService, data: data, } } @@ -795,3 +799,75 @@ func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, an 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 +} diff --git a/plugin/importer.go b/plugin/importer.go index 3893d54a1..bfd7d36a5 100644 --- a/plugin/importer.go +++ b/plugin/importer.go @@ -1,62 +1,62 @@ -/* - * 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 plugin - -import ( - "context" -) - -type QuestionImporterInfo struct { - Title string `json:"title"` - Content string `json:"content"` - Tags []string `json:"tags"` - UserEmail string `json:"user_email"` -} - -type Importer interface { - Base - RegisterImporterFunc(ctx context.Context, importer ImporterFunc) -} - -type ImporterFunc interface { - AddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error) -} - -var ( - // CallImporter is a function that calls all registered parsers - CallImporter, - registerImporter = MakePlugin[Importer](false) -) - -func ImporterEnabled() (enabled bool) { - _ = CallImporter(func(fn Importer) error { - enabled = true - return nil - }) - return -} -func GetImporter() (ip Importer, ok bool) { - _ = CallImporter(func(fn Importer) error { - ip = fn - ok = true - return nil - }) - return -} +/* + * 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 plugin + +import ( + "context" +) + +type QuestionImporterInfo struct { + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + UserEmail string `json:"user_email"` +} + +type Importer interface { + Base + RegisterImporterFunc(ctx context.Context, importer ImporterFunc) +} + +type ImporterFunc interface { + AddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error) +} + +var ( + // CallImporter is a function that calls all registered parsers + CallImporter, + registerImporter = MakePlugin[Importer](false) +) + +func ImporterEnabled() (enabled bool) { + _ = CallImporter(func(fn Importer) error { + enabled = true + return nil + }) + return +} +func GetImporter() (ip Importer, ok bool) { + _ = CallImporter(func(fn Importer) error { + ip = fn + ok = true + return nil + }) + return +}