feat: add timeline api interface

This commit is contained in:
LinkinStar 2022-11-22 19:48:27 +08:00
parent d36f758fba
commit cd961eafc2
30 changed files with 472 additions and 124 deletions

View File

@ -46,6 +46,7 @@ import (
auth2 "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/collection_common"
comment2 "github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/dashboard"
export2 "github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/follow"
@ -186,7 +187,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
dashboardController := controller.NewDashboardController(dashboardService)
uploadController := controller.NewUploadController(uploaderService)
activityCommon := activity_common2.NewActivityCommon(activityRepo)
activityService := activity2.NewActivityService()
activityActivityRepo := activity.NewActivityRepo(dataData)
commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService)
activityController := controller.NewActivityController(activityCommon, activityService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)

View File

@ -1,20 +1,5 @@
package constant
// | 问题 回答 标签 | undeleted | 操作者 | | 恢复删除的内容 |
// | 问题 回答 标签 | deleted | 操作者 | | 删除内容 |
// | 问题 回答 标签 | rollback | 编辑者 | 显示编辑理由 | 回滚版本编辑记录; 点击 Type 显示最近的版本比较 |
// | 问题 回答 标签 | edit | 编辑者 | 显示编辑理由 | 编辑记录; 点击 Type 显示最近的版本比较 |
// | 问题 回答 | downvote | 投票者 or N/A | | 内容点踩,名字仅管理员可见; 取消时显示已取消和取消时间 |
// | 问题 回答 | upvote | 投票者 | | 内容点赞; 取消时显示已取消和取消时间 |
// | 问题 回答 | accept | 提问者 | | 采纳答案Type 链接到对应的回答; 取消时显示已取消和取消时间 |
// | 问题 回答 | commented | 评论者 | 显示评论内容 | 添加评论Type 链接到对应的评论 |
// | 问题 | answered | 回答者 | | 添加回答Type 链接到对应的回答 |
// | 问题 | reopened | 操作者 | | 重新开启问题 |
// | 问题 | closed | 操作者 | 显示关闭理由 | 关闭问题 |
// | 问题 | asked | 提问者 | | 初始提问版本,点击展开无需比较 |
// | 回答 | answered | 回答者 | | 初始回答版本,点击展开无需比较 |
// | 标签 | created | 创建者 | | 初始标签版本,点击展开无需比较 |
// question activity
type ActivityTypeKey string

View File

@ -1,8 +1,12 @@
package controller
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/gin-gonic/gin"
)
type ActivityController struct {
@ -16,3 +20,26 @@ func NewActivityController(
activityService *activity.ActivityService) *ActivityController {
return &ActivityController{activityCommonService: activityCommonService, activityService: activityService}
}
// GetObjectTimeline get object timeline
// @Summary get object timeline
// @Description get object timeline
// @Tags Comment
// @Produce json
// @Param object_id query string false "object id"
// @Param tag_slug_name query string false "tag slug name"
// @Param object_type query string false "object type" Enums(question, answer, tag)
// @Param show_vote bool false "is show vote"
// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp}
// @Router /answer/api/v1/activity/timeline [get]
func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) {
req := &schema.GetObjectTimelineReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := ac.activityService.GetObjectTimeline(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -9,17 +9,19 @@ const (
// Activity activity
type Activity struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
UserID string `xorm:"not null index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
ActivityType int `xorm:"not null INT(11) activity_type"`
Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"`
Rank int `xorm:"not null default 0 INT(11) rank"`
HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"`
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
UserID string `xorm:"not null index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
ActivityType int `xorm:"not null INT(11) activity_type"`
Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"`
Rank int `xorm:"not null default 0 INT(11) rank"`
HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
}
type ActivityRankSum struct {

View File

@ -6,19 +6,19 @@ import (
const (
QuestionStatusAvailable = 1
QuestionStatusclosed = 2
QuestionStatusClosed = 2
QuestionStatusDeleted = 10
)
var CmsQuestionSearchStatus = map[string]int{
"available": QuestionStatusAvailable,
"closed": QuestionStatusclosed,
"closed": QuestionStatusClosed,
"deleted": QuestionStatusDeleted,
}
var CmsQuestionSearchStatusIntToString = map[int]string{
QuestionStatusAvailable: "available",
QuestionStatusclosed: "closed",
QuestionStatusClosed: "closed",
QuestionStatusDeleted: "deleted",
}

View File

@ -23,7 +23,7 @@ type Revision struct {
Content string `xorm:"not null TEXT content"`
Log string `xorm:"VARCHAR(255) log"`
Status int `xorm:"not null default 1 INT(11) status"`
ReviewUserID string `xorm:"not null default 0 BIGINT(20) review_user_id"`
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
// TableName revision table name

View File

@ -163,7 +163,7 @@ func InitBaseInfo(ctx *gin.Context) {
}
if cli.CheckDBTableExist(c.Data.Database) {
log.Warnf("database is already initialized")
log.Warn("database is already initialized")
handler.HandleResponse(ctx, nil, nil)
return
}

View File

@ -242,6 +242,26 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`},
{ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edit", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edit", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edit", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
}
_, err := engine.Insert(defaultConfigTable)
return err

View File

@ -44,6 +44,7 @@ var migrations = []Migration{
NewMigration("this is first version, no operation", noopMigration),
NewMigration("add user language", addUserLanguage),
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
NewMigration("add activity timeline", addActivityTimeline),
}
// GetCurrentDBVersion returns the current db version

19
internal/migrations/v3.go Normal file
View File

@ -0,0 +1,19 @@
package migrations
import (
"time"
"xorm.io/xorm"
)
func addActivityTimeline(x *xorm.Engine) error {
type Reversion struct {
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
type Activity struct {
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
}
return x.Sync(new(Activity), new(Reversion))
}

View File

@ -0,0 +1,35 @@
package activity
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/activity"
"github.com/segmentfault/pacman/errors"
)
// activityRepo activity repository
type activityRepo struct {
data *data.Data
}
// NewActivityRepo new repository
func NewActivityRepo(
data *data.Data,
) activity.ActivityRepo {
return &activityRepo{
data: data,
}
}
func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (
activityList []*entity.Activity, err error) {
activityList = make([]*entity.Activity, 0)
err = ar.data.DB.Find(&activityList, &entity.Activity{OriginalObjectID: objectID})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return activityList, nil
}

View File

@ -2,6 +2,7 @@ package activity
import (
"context"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
@ -96,8 +97,8 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
@ -124,7 +125,7 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str
// AcceptAnswer accept other answer
func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool,
) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
@ -134,10 +135,11 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
ObjectID: answerObjID,
OriginalObjectID: questionObjID,
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
}
if action == acceptAction {
addActivity.UserID = questionUserID
@ -222,7 +224,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
// CancelAcceptAnswer accept other answer
func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string,
answerObjID, questionObjID, questionUserID, answerUserID string,
) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
@ -232,10 +234,11 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
ActivityType: activityType,
Rank: -deltaRank,
HasRank: hasRank,
ObjectID: answerObjID,
OriginalObjectID: questionObjID,
ActivityType: activityType,
Rank: -deltaRank,
HasRank: hasRank,
}
if action == acceptAction {
addActivity.UserID = questionUserID
@ -265,8 +268,8 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
if _, e := session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
@ -326,8 +329,8 @@ func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string)
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}

View File

@ -73,12 +73,13 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
} else {
// update existing activity with new user id and u object id
_, err = session.Insert(&entity.Activity{
UserID: userID,
ObjectID: objectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
Rank: 0,
HasRank: 0,
UserID: userID,
ObjectID: objectID,
OriginalObjectID: objectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
Rank: 0,
HasRank: 0,
})
}

View File

@ -55,11 +55,12 @@ func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string)
}
addActivity := &entity.Activity{
UserID: userID,
ObjectID: "0",
ActivityType: activityType,
Rank: deltaRank,
HasRank: 1,
UserID: userID,
ObjectID: "0",
OriginalObjectID: "0",
ActivityType: activityType,
Rank: deltaRank,
HasRank: 1,
}
_, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType)
if err != nil {

View File

@ -106,13 +106,14 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
}
insertActivity = entity.Activity{
ObjectID: objectID,
UserID: activityUserID,
TriggerUserID: converter.StringToInt64(triggerUserID),
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
Cancelled: entity.ActivityAvailable,
ObjectID: objectID,
OriginalObjectID: objectID,
UserID: activityUserID,
TriggerUserID: converter.StringToInt64(triggerUserID),
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
Cancelled: entity.ActivityAvailable,
}
// trigger user rank and send notification
@ -206,7 +207,7 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
return
}
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),

View File

@ -53,6 +53,7 @@ var ProviderSetRepo = wire.NewSet(
activity.NewAnswerActivityRepo,
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo,
activity.NewActivityRepo,
tag.NewTagRepo,
tag_common.NewTagCommonRepo,
tag.NewTagRelRepo,

View File

@ -166,7 +166,7 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
questionList := make([]*entity.Question, 0)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).FindAndCount(&questionList)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -210,7 +210,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS
session = session.And("question.user_id = ?", search.UserID)
}
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed})
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
// if search.Status > 0 {
// session = session.And("question.status = ?", search.Status)
// }

View File

@ -203,6 +203,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// upload file
r.POST("/file", a.uploadController.UploadFile)
// activity
r.GET("/activity/timeline", a.activityController.GetObjectTimeline)
}
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {

View File

@ -4,8 +4,48 @@ import "github.com/answerdev/answer/internal/base/constant"
// ActivityMsg activity message
type ActivityMsg struct {
UserID string `json:"user_id"`
TriggerUserID int64 `json:"trigger_user_id"`
ObjectID string `json:"object_id"`
ActivityTypeKey constant.ActivityTypeKey `json:"activity_type_key"`
UserID string `json:"user_id"`
TriggerUserID int64 `json:"trigger_user_id"`
ObjectID string `json:"object_id"`
OriginalObjectID string `json:"original_object_id"`
ActivityTypeKey constant.ActivityTypeKey `json:"activity_type_key"`
RevisionID string `json:"revision_id"`
}
// GetObjectTimelineReq get object timeline request
type GetObjectTimelineReq struct {
ObjectId string `validate:"omitempty,gt=0,lte=100" form:"object_id"`
TagSlugName string `validate:"omitempty,gt=0,lte=35" form:"slug_name"`
ObjectType string `validate:"required,oneof=question answer tag" form:"object_type"`
ShowVote bool `validate:"omitempty" form:"show_vote"`
UserID string `json:"-"`
}
// GetObjectTimelineResp get object timeline response
type GetObjectTimelineResp struct {
ObjectInfo *ActObjectInfo `json:"object_info"`
Timeline []*ActObjectTimeline `json:"timeline"`
}
// ActObjectTimeline act object timeline
type ActObjectTimeline struct {
ActivityID string `json:"activity_id"`
RevisionID string `json:"revision_id"`
CreatedAt int64 `json:"created_at"`
ActivityType string `json:"activity_type"`
Username string `json:"username"`
UserDisplayName string `json:"user_display_name"`
Comment string `json:"comment"`
ObjectID string `json:"object_id"`
ObjectType string `json:"object_type"`
Cancelled bool `json:"cancelled"`
CancelledAt int64 `json:"cancelled_at"`
}
// ActObjectInfo act object info
type ActObjectInfo struct {
Title string `json:"title"`
ObjectType string `json:"object_type"`
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
}

View File

@ -1,9 +1,132 @@
package activity
import (
"context"
"strings"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/config"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/log"
)
// ActivityRepo activity repository
type ActivityRepo interface {
GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error)
}
// ActivityService activity service
type ActivityService struct {
activityRepo ActivityRepo
userCommon *usercommon.UserCommon
activityCommonService *activity_common.ActivityCommon
tagCommonService *tag_common.TagCommonService
objectInfoService *object_info.ObjService
commentCommonService *comment_common.CommentCommonService
}
func NewActivityService() *ActivityService {
return &ActivityService{}
// NewActivityService new activity service
func NewActivityService(
activityRepo ActivityRepo,
userCommon *usercommon.UserCommon,
activityCommonService *activity_common.ActivityCommon,
tagCommonService *tag_common.TagCommonService,
objectInfoService *object_info.ObjService,
commentCommonService *comment_common.CommentCommonService,
) *ActivityService {
return &ActivityService{
objectInfoService: objectInfoService,
activityRepo: activityRepo,
userCommon: userCommon,
activityCommonService: activityCommonService,
tagCommonService: tagCommonService,
commentCommonService: commentCommonService,
}
}
// GetObjectTimeline get object timeline
func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) (
resp *schema.GetObjectTimelineResp, err error) {
resp = &schema.GetObjectTimelineResp{
ObjectInfo: &schema.ActObjectInfo{},
Timeline: make([]*schema.ActObjectTimeline, 0),
}
objInfo, err := as.objectInfoService.GetInfo(ctx, req.ObjectId)
if err != nil {
return nil, err
}
resp.ObjectInfo.Title = objInfo.Title
resp.ObjectInfo.ObjectType = objInfo.ObjectType
resp.ObjectInfo.QuestionID = objInfo.QuestionID
resp.ObjectInfo.AnswerID = objInfo.AnswerID
activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectId, req.ShowVote)
if err != nil {
return nil, err
}
for _, act := range activityList {
item := &schema.ActObjectTimeline{
ActivityID: act.ID,
RevisionID: converter.IntToString(act.RevisionID),
CreatedAt: act.CreatedAt.Unix(),
Cancelled: act.Cancelled == entity.ActivityCancelled,
ObjectID: act.ObjectID,
}
if item.Cancelled {
item.CancelledAt = act.CancelledAt.Unix()
}
// database save activity type is number, change to activity type string is like "question.asked".
// so we need to cut the front part of '.'
item.ObjectType, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".")
isHidden, formattedActivityType := formatActivity(item.ActivityType)
if isHidden {
continue
}
item.ActivityType = formattedActivityType
// get user info
userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, act.UserID)
if err != nil {
return nil, err
}
if exist {
item.Username = userBasicInfo.Username
item.UserDisplayName = userBasicInfo.DisplayName
}
if item.ObjectType == constant.CommentObjectType {
comment, err := as.commentCommonService.GetComment(ctx, item.ObjectID)
if err != nil {
log.Error(err)
} else {
item.Comment = comment.ParsedText
}
}
resp.Timeline = append(resp.Timeline, item)
}
return
}
func formatActivity(activityType string) (isHidden bool, formattedActivityType string) {
if activityType == "voted_up" || activityType == "voted_down" || activityType == "accepted" {
return true, ""
}
if activityType == "vote_up" {
return false, "upvote"
}
if activityType == "vote_down" {
return false, "downvote"
}
return false, activityType
}

View File

@ -10,9 +10,9 @@ import (
// AnswerActivityRepo answer activity
type AnswerActivityRepo interface {
AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool) (err error)
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error)
CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string) (err error)
answerObjID, questionObjID, questionUserID, answerUserID string) (err error)
DeleteAnswer(ctx context.Context, answerID string) (err error)
}
@ -38,14 +38,14 @@ func NewAnswerActivityService(
// AcceptAnswer accept answer change activity
func (as *AnswerActivityService) AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionUserID, answerUserID, isSelf)
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID, isSelf)
}
// CancelAcceptAnswer cancel accept answer change activity
func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string) (err error) {
return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionUserID, answerUserID)
answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {
return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID)
}
// DeleteAnswer delete answer change activity

View File

@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
@ -48,15 +49,19 @@ func (ac *ActivityCommon) HandleActivity() {
activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(context.Background(), string(msg.ActivityTypeKey))
if err != nil {
log.Errorf("error getting activity type %s, activity type is %s", err, activityType)
log.Errorf("error getting activity type %s, activity type is %d", err, activityType)
}
act := &entity.Activity{
UserID: msg.UserID,
TriggerUserID: msg.TriggerUserID,
ObjectID: msg.ObjectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
UserID: msg.UserID,
TriggerUserID: msg.TriggerUserID,
ObjectID: msg.ObjectID,
OriginalObjectID: msg.OriginalObjectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
}
if len(msg.RevisionID) > 0 {
act.RevisionID = converter.StringToInt64(msg.RevisionID)
}
if err := ac.activityRepo.AddActivity(context.TODO(), act); err != nil {
log.Error(err)

View File

@ -165,21 +165,25 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
}
infoJSON, _ := json.Marshal(insertData)
revisionDTO.Content = string(infoJSON)
err = as.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return insertData.ID, err
}
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, insertData.ID, req.UserID)
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerAnswered,
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerAnswered,
RevisionID: revisionID,
})
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
ActivityTypeKey: constant.ActQuestionAnswered,
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionAnswered,
RevisionID: revisionID,
})
return insertData.ID, nil
}
@ -215,11 +219,19 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
}
infoJSON, _ := json.Marshal(insertData)
revisionDTO.Content = string(infoJSON)
err = as.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return insertData.ID, err
}
as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID)
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerEdit,
RevisionID: revisionID,
})
return insertData.ID, nil
}
@ -288,14 +300,14 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,
// if this question is already been answered, should cancel old answer rank
if oldAnswerInfo != nil {
err := as.answerActivityService.CancelAcceptAnswer(
ctx, questionInfo.AcceptedAnswerID, questionInfo.UserID, oldAnswerInfo.UserID)
ctx, questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID)
if err != nil {
log.Error(err)
}
}
if newAnswerInfo.ID != "" {
err := as.answerActivityService.AcceptAnswer(
ctx, newAnswerInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID)
ctx, newAnswerInfo.ID, questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID)
if err != nil {
log.Error(err)
}
@ -360,7 +372,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri
}
if setStatus == entity.AnswerStatusDeleted {
err = as.answerActivityService.DeleteQuestion(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
if err != nil {
log.Errorf("admin delete question then rank rollback error %s", err.Error())
}

View File

@ -40,7 +40,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
}
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
if cacheInfo != nil {
log.Infof("user status updated: %+v", cacheInfo)
log.Debugf("user status updated: %+v", cacheInfo)
userCacheInfo.UserStatus = cacheInfo.UserStatus
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
userCacheInfo.IsAdmin = cacheInfo.IsAdmin

View File

@ -151,9 +151,10 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: comment.UserID,
ObjectID: comment.ID,
ActivityTypeKey: constant.ActQuestionCommented,
UserID: comment.UserID,
ObjectID: comment.ID,
OriginalObjectID: req.ObjectID,
ActivityTypeKey: constant.ActQuestionCommented,
})
return resp, nil
}

View File

@ -5,8 +5,10 @@ import (
"encoding/json"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/meta"
"github.com/segmentfault/pacman/errors"
@ -308,7 +310,7 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu
if !has {
return nil
}
questionInfo.Status = entity.QuestionStatusclosed
questionInfo.Status = entity.QuestionStatusClosed
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {
return err
@ -322,6 +324,14 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
RevisionID: questionInfo.RevisionID,
})
return nil
}

View File

@ -72,7 +72,7 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
if !has {
return nil
}
questionInfo.Status = entity.QuestionStatusclosed
questionInfo.Status = entity.QuestionStatusClosed
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {
return err
@ -88,9 +88,11 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
RevisionID: questionInfo.RevisionID,
})
return nil
}
@ -160,7 +162,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
}
infoJSON, _ := json.Marshal(questionWithTagsRevision)
revisionDTO.Content = string(infoJSON)
err = qs.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return
}
@ -172,9 +174,11 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: question.UserID,
ObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionAsked,
UserID: question.UserID,
ObjectID: question.ID,
OriginalObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionAsked,
RevisionID: revisionID,
})
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
@ -217,7 +221,13 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
if err != nil {
log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionDeleted,
RevisionID: questionInfo.RevisionID,
})
return nil
}
@ -285,14 +295,16 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
}
infoJSON, _ := json.Marshal(question)
revisionDTO.Content = string(infoJSON)
err = qs.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionEdit,
UserID: req.UserID,
ObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionEdit,
RevisionID: revisionID,
OriginalObjectID: question.ID,
})
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
@ -602,8 +614,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
if !exist {
return errors.BadRequest(reason.QuestionNotFound)
}
questionInfo.Status = setStatus
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
err = qs.questionRepo.UpdateQuestionStatus(ctx, &entity.Question{ID: questionInfo.ID, Status: setStatus})
if err != nil {
return err
}
@ -614,6 +625,24 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
log.Errorf("admin delete question then rank rollback error %s", err.Error())
}
}
if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionDeleted,
RevisionID: questionInfo.RevisionID,
})
}
if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: constant.ActQuestionClosed,
RevisionID: questionInfo.RevisionID,
})
}
msg := &schema.NotificationMsg{}
msg.ObjectID = questionInfo.ID
msg.Type = schema.NotificationTypeInbox

View File

@ -29,12 +29,13 @@ func NewRevisionService(revisionRepo revision.RevisionRepo, userRepo usercommon.
// autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated,
// if not need auto update object.revision_id, it must be false.
// example: user can edit the object, but need audit, the revision_id will be updated when admin approved
func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (err error) {
func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (
revisionID string, err error) {
rev := &entity.Revision{}
_ = copier.Copy(rev, req)
err = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID)
if err != nil {
return err
return "", err
}
return nil
return rev.ID, nil
}

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/tag_common"
@ -105,10 +107,17 @@ func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (
}
tagInfoJson, _ := json.Marshal(tagInfo)
revisionDTO.Content = string(tagInfoJson)
err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,
ActivityTypeKey: constant.ActTagEdit,
RevisionID: revisionID,
})
return
}
@ -299,10 +308,17 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
}
tagInfoJson, _ := json.Marshal(tag)
revisionDTO.Content = string(tagInfoJson)
err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,
ActivityTypeKey: constant.ActTagCreated,
RevisionID: revisionID,
})
}
}

View File

@ -7,10 +7,12 @@ import (
"sort"
"strings"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/segmentfault/pacman/errors"
@ -480,10 +482,17 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
}
tagInfoJson, _ := json.Marshal(tag)
revisionDTO.Content = string(tagInfoJson)
err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: objectTagData.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,
ActivityTypeKey: constant.ActTagCreated,
RevisionID: revisionID,
})
}
}