/* * 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 answer import ( "context" "time" "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/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity_common" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/rank" "github.com/apache/answer/internal/service/unique" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // answerRepo answer repository type answerRepo struct { data *data.Data uniqueIDRepo unique.UniqueIDRepo userRankRepo rank.UserRankRepo activityRepo activity_common.ActivityRepo } // NewAnswerRepo new repository func NewAnswerRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userRankRepo rank.UserRankRepo, activityRepo activity_common.ActivityRepo, ) answercommon.AnswerRepo { return &answerRepo{ data: data, uniqueIDRepo: uniqueIDRepo, userRankRepo: userRankRepo, activityRepo: activityRepo, } } // AddAnswer add answer func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) { answer.QuestionID = uid.DeShortID(answer.QuestionID) ID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName()) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } answer.ID = ID _, err = ar.data.DB.Context(ctx).Insert(answer) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { answer.ID = uid.EnShortID(answer.ID) answer.QuestionID = uid.EnShortID(answer.QuestionID) } _ = ar.updateSearch(ctx, answer.ID) return nil } // RemoveAnswer delete answer func (ar *answerRepo) RemoveAnswer(ctx context.Context, answerID string) (err error) { answerID = uid.DeShortID(answerID) _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{ Status: entity.AnswerStatusDeleted, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answerID) return nil } // RecoverAnswer recover answer func (ar *answerRepo) RecoverAnswer(ctx context.Context, answerID string) (err error) { answerID = uid.DeShortID(answerID) _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{ Status: entity.AnswerStatusAvailable, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answerID) return nil } // RemoveAllUserAnswer remove all user answer func (ar *answerRepo) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) { // find all answer id that need to be deleted answerIDs := make([]string, 0) session := ar.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.AnswerStatusDeleted) err = session.Select("id").Table("answer").Find(&answerIDs) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(answerIDs) == 0 { return nil } log.Infof("find %d answers need to be deleted for user %s", len(answerIDs), userID) // delete all question session = ar.data.DB.Context(ctx).Where("user_id = ?", userID) session.Where("status != ?", entity.AnswerStatusDeleted) _, err = session.Cols("status", "updated_at").Update(&entity.Answer{ UpdatedAt: time.Now(), Status: entity.AnswerStatusDeleted, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // update search content for _, id := range answerIDs { _ = ar.updateSearch(ctx, id) } return nil } // UpdateAnswer update answer func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) { answer.ID = uid.DeShortID(answer.ID) answer.QuestionID = uid.DeShortID(answer.QuestionID) _, err = ar.data.DB.Context(ctx).ID(answer.ID).Cols(cols...).Update(answer) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answer.ID) return err } func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) { answerID = uid.DeShortID(answerID) _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{Status: status}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _ = ar.updateSearch(ctx, answerID) return } // GetAnswer get answer one func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( answer *entity.Answer, exist bool, err error, ) { id = uid.DeShortID(id) answer = &entity.Answer{} exist, err = ar.data.DB.Context(ctx).ID(id).Get(answer) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { answer.ID = uid.EnShortID(answer.ID) answer.QuestionID = uid.EnShortID(answer.QuestionID) } return } // GetAnswerCount count answer func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) { var resp = new(entity.Answer) count, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusAvailable).Count(resp) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // GetAnswerList get answer list all func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { answerList = make([]*entity.Answer, 0) if len(answer.ID) > 0 { answer.ID = uid.DeShortID(answer.ID) } if len(answer.QuestionID) > 0 { answer.QuestionID = uid.DeShortID(answer.QuestionID) } err = ar.data.DB.Context(ctx).Find(&answerList, answer) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range answerList { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return } // GetAnswerPage get answer page func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) { answer.ID = uid.DeShortID(answer.ID) answer.QuestionID = uid.DeShortID(answer.QuestionID) answerList = make([]*entity.Answer, 0) total, err = pager.Help(page, pageSize, &answerList, answer, ar.data.DB.Context(ctx)) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range answerList { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return } // UpdateAcceptedStatus update all accepted status of this question's answers func (ar *answerRepo) UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error { acceptedAnswerID = uid.DeShortID(acceptedAnswerID) questionID = uid.DeShortID(questionID) // update all this question's answer accepted status to false _, err := ar.data.DB.Context(ctx).Where("question_id = ?", questionID).Cols("adopted").Update(&entity.Answer{ Accepted: schema.AnswerAcceptedFailed, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // if acceptedAnswerID is not empty, update accepted status to true if len(acceptedAnswerID) > 0 && acceptedAnswerID != "0" { _, err = ar.data.DB.Context(ctx).Where("id = ?", acceptedAnswerID).Cols("adopted").Update(&entity.Answer{ Accepted: schema.AnswerAcceptedEnable, }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } _ = ar.updateSearch(ctx, acceptedAnswerID) return nil } // GetByID func (ar *answerRepo) GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) { var resp entity.Answer answerID = uid.DeShortID(answerID) has, err := ar.data.DB.Context(ctx).ID(answerID).Get(&resp) if err != nil { return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { resp.ID = uid.EnShortID(resp.ID) resp.QuestionID = uid.EnShortID(resp.QuestionID) } return &resp, has, nil } func (ar *answerRepo) GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) { for idx, answerID := range answerIDs { answerIDs[idx] = uid.DeShortID(answerID) } var resp = make([]*entity.Answer, 0) err := ar.data.DB.Context(ctx).In("id", answerIDs).Find(&resp) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range resp { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return resp, nil } func (ar *answerRepo) GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) { questionID = uid.DeShortID(questionID) var resp = new(entity.Answer) count, err := ar.data.DB.Context(ctx).Where("question_id =? and status = ?", questionID, entity.AnswerStatusAvailable).Count(resp) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (ar *answerRepo) GetCountByUserID(ctx context.Context, userID string) (int64, error) { var resp = new(entity.Answer) count, err := ar.data.DB.Context(ctx).Where(" user_id = ? and status = ?", userID, entity.AnswerStatusAvailable).Count(resp) if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } func (ar *answerRepo) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) { questionID = uid.DeShortID(questionID) var ids []string resp := make([]string, 0) err := ar.data.DB.Context(ctx).Table(entity.Answer{}.TableName()).Where("question_id =? and user_id = ? and status = ?", questionID, userID, entity.AnswerStatusAvailable).OrderBy("created_at ASC").Cols("id").Find(&ids) if err != nil { return resp, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, id := range ids { resp = append(resp, uid.EnShortID(id)) } } else { resp = ids } return resp, nil } // SearchList func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { if search.QuestionID != "" { search.QuestionID = uid.DeShortID(search.QuestionID) } search.ID = uid.DeShortID(search.ID) var count int64 var err error rows := make([]*entity.Answer, 0) if search.Page > 0 { search.Page-- } else { search.Page = 0 } if search.PageSize == 0 { search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := ar.data.DB.Context(ctx) if search.QuestionID != "" { session = session.And("question_id = ?", search.QuestionID) } if len(search.UserID) > 0 { session = session.And("user_id = ?", search.UserID) } switch search.Order { case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") case entity.AnswerSearchOrderByTimeAsc: session = session.OrderBy("created_at asc") case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") default: session = session.OrderBy("adopted desc,vote_count desc,created_at asc") } if !search.IncludeDeleted { if search.LoginUserID == "" { session = session.And("status = ? ", entity.AnswerStatusAvailable) } else { session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID) } } session = session.Limit(search.PageSize, offset) count, err = session.FindAndCount(&rows) if err != nil { return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range rows { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return rows, count, nil } // GetPersonalAnswerPage personal answer page func (ar *answerRepo) GetPersonalAnswerPage(ctx context.Context, req *entity.PersonalAnswerPageQueryCond) ( resp []*entity.Answer, total int64, err error) { cond := &entity.Answer{ UserID: req.UserID, } session := ar.data.DB.Context(ctx) switch req.Order { case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") case entity.AnswerSearchOrderByTimeAsc: session = session.OrderBy("created_at asc") case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") default: session = session.OrderBy("adopted desc,vote_count desc,created_at asc") } if req.ShowPending { session = session.And("status != ?", entity.AnswerStatusDeleted) } else { session = session.And("status = ?", entity.AnswerStatusAvailable) } resp = make([]*entity.Answer, 0) total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) if err != nil { return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if handler.GetEnableShortID(ctx) { for _, item := range resp { item.ID = uid.EnShortID(item.ID) item.QuestionID = uid.EnShortID(item.QuestionID) } } return resp, total, nil } func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( resp []*entity.Answer, total int64, err error) { cond := &entity.Answer{} session := ar.data.DB.Context(ctx) if len(req.QuestionID) == 0 && len(req.AnswerID) == 0 { session.Join("INNER", "question", "answer.question_id = question.id") if len(req.QuestionTitle) > 0 { session.Where("question.title like ?", "%"+req.QuestionTitle+"%") } } if len(req.AnswerID) > 0 { cond.ID = req.AnswerID } if len(req.QuestionID) > 0 { session.Where("answer.question_id = ?", req.QuestionID) } if req.Status > 0 { cond.Status = req.Status } session.Desc("answer.created_at") resp = make([]*entity.Answer, 0) total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) if err != nil { return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return resp, total, nil } // SumVotesByQuestionID sum votes by question id func (ar *answerRepo) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) { questionID = uid.DeShortID(questionID) var resp entity.Answer count, err := ar.data.DB.Context(ctx).Where("question_id = ? and status = ?", questionID, entity.AnswerStatusAvailable).Sum(&resp, "vote_count") if err != nil { return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return count, nil } // updateSearch update search, if search plugin not enable, do nothing func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) { answerID = uid.DeShortID(answerID) // check search plugin var ( s plugin.Search ) _ = plugin.CallSearch(func(search plugin.Search) error { s = search return nil }) if s == nil { return } answer, exist, err := ar.GetAnswer(ctx, answerID) if !exist { return } if err != nil { return err } // get question var ( question = new(entity.Question) ) exist, err = ar.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(&question) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if !exist { return } // get tags var ( tagListList = make([]*entity.TagRel, 0) tags = make([]string, 0) ) st := ar.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)) st.Where("status = ?", entity.TagRelStatusAvailable) err = st.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } for _, tag := range tagListList { tags = append(tags, tag.TagID) } content := &plugin.SearchContent{ ObjectID: answerID, Title: question.Title, Type: constant.AnswerObjectType, Content: answer.OriginalText, Answers: 0, Status: plugin.SearchContentStatus(answer.Status), Tags: tags, QuestionID: answer.QuestionID, UserID: answer.UserID, Views: int64(question.ViewCount), Created: answer.CreatedAt.Unix(), Active: answer.UpdatedAt.Unix(), Score: int64(answer.VoteCount), HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, } err = s.UpdateContent(ctx, content) return } func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error { // get all deleted answers ids ids := make([]string, 0) err := ar.data.DB.Context(ctx).Select("id").Table(new(entity.Answer).TableName()). Where("status = ?", entity.AnswerStatusDeleted).Find(&ids) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if len(ids) == 0 { return nil } // delete all revisions permanently _, err = ar.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } _, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil }