This is an automated email from the ASF dual-hosted git repository. robin0716 pushed a commit to branch develop/robin in repository https://gitbox.apache.org/repos/asf/incubator-answer.git
commit b062d53f42fc12a42c18e5c308fb547a6bc1edb0 Author: LinkinStars <linkins...@foxmail.com> AuthorDate: Thu Aug 15 18:34:13 2024 +0800 feat(badge): add badge notification --- cmd/wire_gen.go | 6 +- i18n/en_US.yaml | 2 + internal/base/constant/cache_key.go | 2 + internal/base/constant/notification.go | 8 ++ internal/controller/notification_controller.go | 6 +- internal/repo/notification/notification_repo.go | 6 +- internal/schema/badge_schema.go | 18 ++++ internal/schema/notification_schema.go | 76 ++++++++++++++-- internal/service/badge/badge_award_service.go | 85 ++++++++++-------- .../service/notification/notification_service.go | 100 ++++++++++++++------- .../service/notification_common/notification.go | 89 ++++++++++++++---- internal/service/object_info/object_info.go | 1 - 12 files changed, 301 insertions(+), 98 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 9acd76ee..cc245aea 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -238,7 +238,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService) notificationRepo := notification2.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService, userExternalLoginRepo, siteInfoCommonService) - notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService) + badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) + notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService, badgeRepo) notificationController := controller.NewNotificationController(notificationService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, reviewService, revisionRepo, dataData) dashboardController := controller.NewDashboardController(dashboardService) @@ -259,11 +260,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reviewController := controller.NewReviewController(reviewService, rankService, captchaService) metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo, eventQueueService) metaController := controller.NewMetaController(metaService) - badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) eventRuleRepo := badge.NewEventRuleRepo(dataData) - badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService) + badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService, notificationQueueService) badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo, badgeAwardService) badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 3a26fc53..8b137d37 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -465,6 +465,8 @@ backend: other: upvoted comment invited_you_to_answer: other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: diff --git a/internal/base/constant/cache_key.go b/internal/base/constant/cache_key.go index 4135b53c..987798d1 100644 --- a/internal/base/constant/cache_key.go +++ b/internal/base/constant/cache_key.go @@ -50,4 +50,6 @@ const ( NewQuestionNotificationLimitMax = 50 RateLimitCacheKeyPrefix = "answer:rate-limit:" RateLimitCacheTime = 5 * time.Minute + RedDotCacheKey = "answer:red-dot:%s:%s" + RedDotCacheTime = 30 * 24 * time.Hour ) diff --git a/internal/base/constant/notification.go b/internal/base/constant/notification.go index ceebe7de..9a7762d8 100644 --- a/internal/base/constant/notification.go +++ b/internal/base/constant/notification.go @@ -56,6 +56,8 @@ const ( NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted" // NotificationInvitedYouToAnswer invited you to answer NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer" + // NotificationEarnedBadge earned badge + NotificationEarnedBadge = "notification.action.earned_badge" ) type NotificationChannelKey string @@ -71,6 +73,12 @@ const ( EmailChannel NotificationChannelKey = "email" ) +const ( + NotificationTypeInbox = "inbox" + NotificationTypeAchievement = "achievement" + NotificationTypeBadgeAchievement = "badge" +) + var ( NotificationMsgTypeMapping = map[string]int{ NotificationUpdateQuestion: 1, diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index 15796b9c..952c262e 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -105,8 +105,8 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) { req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] - RedDot, err := nc.notificationService.ClearRedDot(ctx, req) - handler.HandleResponse(ctx, err, RedDot) + resp, err := nc.notificationService.ClearRedDot(ctx, req) + handler.HandleResponse(ctx, err, resp) } // ClearUnRead @@ -125,7 +125,7 @@ func (nc *NotificationController) ClearUnRead(ctx *gin.Context) { return } userID := middleware.GetLoginUserIDFromContext(ctx) - err := nc.notificationService.ClearUnRead(ctx, userID, req.TypeStr) + err := nc.notificationService.ClearUnRead(ctx, userID, req.NotificationType) handler.HandleResponse(ctx, err, gin.H{}) } diff --git a/internal/repo/notification/notification_repo.go b/internal/repo/notification/notification_repo.go index 6b4f0040..bd325ef2 100644 --- a/internal/repo/notification/notification_repo.go +++ b/internal/repo/notification/notification_repo.go @@ -69,7 +69,7 @@ func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notif func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead - _, err = nr.data.DB.Context(ctx).Where("user_id =?", userID).And("type =?", notificationType).Cols("is_read").Update(info) + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("type = ?", notificationType).Cols("is_read").Update(info) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -79,7 +79,7 @@ func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, noti func (nr *notificationRepo) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead - _, err = nr.data.DB.Context(ctx).Where("user_id =?", userID).And("id =?", id).Cols("is_read").Update(info) + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("id = ?", id).Cols("is_read").Update(info) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -98,7 +98,7 @@ func (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Not func (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) { info := &entity.Notification{} - exist, err := nr.data.DB.Context(ctx).Where("user_id = ? ", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) + exist, err := nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return info, false, err diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index 088193ad..efbcd37a 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -171,3 +171,21 @@ type GetUserBadgeAwardListResp struct { // badge level Level entity.BadgeLevel `json:"level" ` } + +// GetBadgeByIDResp get badge by id response +type GetBadgeByIDResp struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge description + Description string `json:"description" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge is single or multiple + IsSingle bool `json:"is_single" ` + // badge level + Level entity.BadgeLevel `json:"level" ` +} diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index 4e0e9316..8d4b694e 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -19,6 +19,12 @@ package schema +import ( + "encoding/json" + "github.com/apache/incubator-answer/internal/entity" + "sort" +) + const ( NotificationTypeInbox = 1 NotificationTypeAchievement = 2 @@ -95,10 +101,70 @@ type ObjectInfo struct { } type RedDot struct { - Inbox int64 `json:"inbox"` - Achievement int64 `json:"achievement"` - Revision int64 `json:"revision"` - CanRevision bool `json:"can_revision"` + Inbox int64 `json:"inbox"` + Achievement int64 `json:"achievement"` + Revision int64 `json:"revision"` + CanRevision bool `json:"can_revision"` + BadgeAward *RedDotBadgeAward `json:"badge_award"` +} + +type RedDotBadgeAward struct { + NotificationID string `json:"notification_id"` + BadgeID string `json:"badge_id"` + Name string `json:"name"` + Icon string `json:"icon"` + Level entity.BadgeLevel `json:"level"` +} + +type RedDotBadgeAwardCache struct { + BadgeAwardList map[string]*RedDotBadgeAward `json:"badge_award_list"` +} + +// NewRedDotBadgeAwardCache new red dot badge award cache +func NewRedDotBadgeAwardCache() *RedDotBadgeAwardCache { + return &RedDotBadgeAwardCache{ + BadgeAwardList: make(map[string]*RedDotBadgeAward), + } +} + +// GetBadgeAward get badge award +func (r *RedDotBadgeAwardCache) GetBadgeAward() *RedDotBadgeAward { + if len(r.BadgeAwardList) == 0 { + return nil + } + var ids []string + for _, v := range r.BadgeAwardList { + ids = append(ids, v.NotificationID) + } + sort.Strings(ids) + return r.BadgeAwardList[ids[0]] +} + +// FromJSON from json +func (r *RedDotBadgeAwardCache) FromJSON(data string) { + _ = json.Unmarshal([]byte(data), r) +} + +// ToJSON to json +func (r *RedDotBadgeAwardCache) ToJSON() string { + data, _ := json.Marshal(r) + return string(data) +} + +// AddBadgeAward add badge award +func (r *RedDotBadgeAwardCache) AddBadgeAward(badgeAward *RedDotBadgeAward) { + if r.BadgeAwardList == nil { + r.BadgeAwardList = make(map[string]*RedDotBadgeAward) + } + r.BadgeAwardList[badgeAward.NotificationID] = badgeAward +} + +// RemoveBadgeAward remove badge award +func (r *RedDotBadgeAwardCache) RemoveBadgeAward(notificationID string) { + if r.BadgeAwardList == nil { + return + } + delete(r.BadgeAwardList, notificationID) } type NotificationSearch struct { @@ -112,8 +178,8 @@ type NotificationSearch struct { } type NotificationClearRequest struct { + NotificationType string `validate:"required,oneof=inbox achievement" json:"type"` UserID string `json:"-"` - TypeStr string `json:"type" form:"type"` // inbox achievement CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 3883de61..dd78fd02 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -21,11 +21,13 @@ package badge import ( "context" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/object_info" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/uid" @@ -65,10 +67,11 @@ type BadgeAwardRepo interface { } type BadgeAwardService struct { - badgeAwardRepo BadgeAwardRepo - badgeRepo BadgeRepo - userCommon *usercommon.UserCommon - objectInfoService *object_info.ObjService + badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + userCommon *usercommon.UserCommon + objectInfoService *object_info.ObjService + notificationQueueService notice_queue.NotificationQueueService } func NewBadgeAwardService( @@ -76,17 +79,19 @@ func NewBadgeAwardService( badgeRepo BadgeRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, + notificationQueueService notice_queue.NotificationQueueService, ) *BadgeAwardService { return &BadgeAwardService{ - badgeAwardRepo: badgeAwardRepo, - badgeRepo: badgeRepo, - userCommon: userCommon, - objectInfoService: objectInfoService, + badgeAwardRepo: badgeAwardRepo, + badgeRepo: badgeRepo, + userCommon: userCommon, + objectInfoService: objectInfoService, + notificationQueueService: notificationQueueService, } } // GetBadgeAwardList get badge award list -func (b *BadgeAwardService) GetBadgeAwardList( +func (bs *BadgeAwardService) GetBadgeAwardList( ctx context.Context, req *schema.GetBadgeAwardWithPageReq, ) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) { @@ -94,11 +99,11 @@ func (b *BadgeAwardService) GetBadgeAwardList( badgeAwardList []*entity.BadgeAward ) - req.UserID, err = b.validateUserByUsername(ctx, req.Username) + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { - badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) + badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) } else { - badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) + badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) } if err != nil { @@ -113,7 +118,7 @@ func (b *BadgeAwardService) GetBadgeAwardList( ) // if exist object info - objInfo, e := b.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) + objInfo, e := bs.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) if e == nil && !objInfo.IsDeleted() { objectID = objInfo.ObjectID questionID = objInfo.QuestionID @@ -135,7 +140,7 @@ func (b *BadgeAwardService) GetBadgeAwardList( } // get user info - userInfo, exists, e := b.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) + userInfo, exists, e := bs.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) if e != nil { log.Errorf("user not found by id: %s, err: %v", badgeAward.UserID, e) } @@ -150,8 +155,8 @@ func (b *BadgeAwardService) GetBadgeAwardList( } // Award award badge -func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { - badgeData, exists, err := b.badgeRepo.GetByID(ctx, badgeID) +func (bs *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { + badgeData, exists, err := bs.badgeRepo.GetByID(ctx, badgeID) if err != nil { return err } @@ -160,7 +165,7 @@ func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID st return errors.BadRequest(reason.BadgeObjectNotFound) } - alreadyAwarded, err := b.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) + alreadyAwarded, err := bs.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) if err != nil { return err } @@ -175,11 +180,27 @@ func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID st BadgeGroupID: badgeData.BadgeGroupID, IsBadgeDeleted: entity.IsBadgeNotDeleted, } - return b.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) + err = bs.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) + if err != nil { + return err + } + + msg := &schema.NotificationMsg{ + TriggerUserID: badgeAward.UserID, + ReceiverUserID: badgeAward.UserID, + Type: schema.NotificationTypeAchievement, + ObjectID: badgeAward.ID, + ObjectType: constant.BadgeAwardObjectType, + Title: badgeData.Name, + ExtraInfo: map[string]string{"badge_id": badgeData.ID}, + NotificationAction: constant.NotificationEarnedBadge, + } + bs.notificationQueueService.Send(ctx, msg) + return nil } // GetUserBadgeAwardList get user badge award list -func (b *BadgeAwardService) GetUserBadgeAwardList( +func (bs *BadgeAwardService) GetUserBadgeAwardList( ctx *gin.Context, req *schema.GetUserBadgeAwardListReq, ) ( @@ -191,12 +212,12 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( earnedCounts []*entity.BadgeEarnedCount ) - req.UserID, err = b.validateUserByUsername(ctx, req.Username) + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { return } - earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) + earnedCounts, err = bs.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) if err != nil { return } @@ -204,7 +225,7 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { - badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) if e != nil { err = e return @@ -225,24 +246,18 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( } // GetUserRecentBadgeAwardList get user badge award list -func (b *BadgeAwardService) GetUserRecentBadgeAwardList( - ctx *gin.Context, - req *schema.GetUserBadgeAwardListReq, -) ( - resp []*schema.GetUserBadgeAwardListResp, - total int64, - err error, -) { +func (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req *schema.GetUserBadgeAwardListReq) ( + resp []*schema.GetUserBadgeAwardListResp, total int64, err error) { var ( earnedCounts []*entity.BadgeAwardRecent ) - req.UserID, err = b.validateUserByUsername(ctx, req.Username) + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { return } - earnedCounts, err = b.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) + earnedCounts, err = bs.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) if err != nil { return } @@ -251,7 +266,7 @@ func (b *BadgeAwardService) GetUserRecentBadgeAwardList( resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { - badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) if e != nil { err = e return @@ -278,14 +293,14 @@ type userReq struct { Username string } -func (b *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { +func (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { var ( userInfo *schema.UserBasicInfo exist bool ) // validate user exists or not if len(userName) > 0 { - userInfo, exist, err = b.userCommon.GetUserBasicInfoByUserName(ctx, userName) + userInfo, exist, err = bs.userCommon.GetUserBasicInfoByUserName(ctx, userName) if err != nil { return } diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index 71febb67..b73d4fda 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -23,7 +23,7 @@ import ( "context" "encoding/json" "fmt" - + "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/report_common" "github.com/apache/incubator-answer/internal/service/review" usercommon "github.com/apache/incubator-answer/internal/service/user_common" @@ -52,6 +52,7 @@ type NotificationService struct { reportRepo report_common.ReportRepo reviewService *review.ReviewService userRepo usercommon.UserRepo + badgeRepo badge.BadgeRepo } func NewNotificationService( @@ -62,6 +63,7 @@ func NewNotificationService( userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, reviewService *review.ReviewService, + badgeRepo badge.BadgeRepo, ) *NotificationService { return &NotificationService{ data: data, @@ -71,35 +73,60 @@ func NewNotificationService( userRepo: userRepo, reportRepo: reportRepo, reviewService: reviewService, + badgeRepo: badgeRepo, } } func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) { + inboxKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, req.UserID) + achievementKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, req.UserID) + redBot := &schema.RedDot{} - inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID) - achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID) - inboxValue, _, err := ns.data.Cache.GetInt64(ctx, inboxKey) - if err != nil { - redBot.Inbox = 0 - } else { - redBot.Inbox = inboxValue - } - achievementValue, _, err := ns.data.Cache.GetInt64(ctx, achievementKey) - if err != nil { - redBot.Achievement = 0 - } else { - redBot.Achievement = achievementValue - } - revisionCount := &schema.RevisionSearch{} - _ = copier.Copy(revisionCount, req) + redBot.Inbox, _, err = ns.data.Cache.GetInt64(ctx, inboxKey) + redBot.Achievement, _, err = ns.data.Cache.GetInt64(ctx, achievementKey) + + // get review amount if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { redBot.CanRevision = true redBot.Revision = ns.countAllReviewAmount(ctx, req) } + // get badge award + redBot.BadgeAward = ns.getBadgeAward(ctx, req.UserID) return redBot, nil } +func (ns *NotificationService) getBadgeAward(ctx context.Context, userID string) (badgeAward *schema.RedDotBadgeAward) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + log.Errorf("get badge award failed: %v", err) + return nil + } + if !exist { + return nil + } + + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + award := c.GetBadgeAward() + if award == nil { + return nil + } + badgeInfo, exists, err := ns.badgeRepo.GetByID(ctx, award.BadgeID) + if err != nil { + log.Errorf("get badge info failed: %v", err) + return nil + } + if !exists { + return nil + } + award.Name = translator.Tr(handler.GetLangByCtx(ctx), badgeInfo.Name) + award.Icon = badgeInfo.Icon + award.Level = badgeInfo.Level + return award +} + func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) { // get queue amount if req.IsAdmin { @@ -137,21 +164,16 @@ func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *sc } func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { - botType, ok := schema.NotificationType[req.TypeStr] - if ok { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, req.UserID) - err := ns.data.Cache.Del(ctx, key) - if err != nil { - log.Error("ClearRedDot del cache error", err.Error()) - } - } - getRedDotreq := &schema.GetRedDot{} - _ = copier.Copy(getRedDotreq, req) - return ns.GetRedDot(ctx, getRedDotreq) + key := fmt.Sprintf(constant.RedDotCacheKey, req.NotificationType, req.UserID) + _ = ns.data.Cache.Del(ctx, key) + + resp := &schema.GetRedDot{} + _ = copier.Copy(resp, req) + return ns.GetRedDot(ctx, resp) } -func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, botTypeStr string) error { - botType, ok := schema.NotificationType[botTypeStr] +func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, notificationType string) error { + botType, ok := schema.NotificationType[notificationType] if ok { err := ns.notificationRepo.ClearUnRead(ctx, userID, botType) if err != nil { @@ -164,19 +186,23 @@ func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, b func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, id string) error { notificationInfo, exist, err := ns.notificationRepo.GetById(ctx, id) if err != nil { - log.Error("notificationRepo.GetById error", err.Error()) + log.Errorf("get notification failed: %v", err) return nil } - if !exist { + if !exist || notificationInfo.UserID != userID { return nil } - if notificationInfo.UserID == userID && notificationInfo.IsRead == schema.NotificationNotRead { + if notificationInfo.IsRead == schema.NotificationNotRead { err := ns.notificationRepo.ClearIDUnRead(ctx, userID, id) if err != nil { return err } } + err = ns.notificationCommon.RemoveBadgeAwardAlertCache(ctx, userID, id) + if err != nil { + log.Errorf("remove badge award alert cache failed: %v", err) + } return nil } @@ -224,6 +250,14 @@ func (ns *NotificationService) formatNotificationPage(ctx context.Context, notif item.NotificationAction == constant.NotificationDownVotedTheAnswer { item.UserInfo = nil } + // If notification is badge, the user info is not needed and the title need to be translated. + if item.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { + badgeName := translator.Tr(lang, item.ObjectInfo.Title) + item.ObjectInfo.Title = translator.TrWithData(lang, constant.NotificationEarnedBadge, struct { + BadgeName string + }{BadgeName: badgeName}) + item.UserInfo = nil + } item.ID = notificationInfo.ID item.NotificationAction = translator.Tr(lang, item.NotificationAction) diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 319403b2..a3129b3a 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -103,7 +103,7 @@ func NewNotificationCommon( // ObjectInfo.Title // ObjectInfo.ObjectID // ObjectInfo.ObjectType -func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error { +func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) { if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() { return nil } @@ -119,17 +119,25 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N Type: msg.Type, } var questionID string // just for notify all followers - objInfo, err := ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) - if err != nil { - log.Error(err) - } else { - req.ObjectInfo.Title = objInfo.Title - questionID = objInfo.QuestionID + var objInfo *schema.SimpleObjectInfo + if msg.ObjectType == constant.BadgeAwardObjectType { + req.ObjectInfo.Title = msg.Title objectMap := make(map[string]string) - objectMap["question"] = uid.DeShortID(objInfo.QuestionID) - objectMap["answer"] = uid.DeShortID(objInfo.AnswerID) - objectMap["comment"] = objInfo.CommentID + objectMap["badge_id"] = msg.ExtraInfo["badge_id"] req.ObjectInfo.ObjectMap = objectMap + } else { + objInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) + if err != nil { + log.Error(err) + } else { + req.ObjectInfo.Title = objInfo.Title + questionID = objInfo.QuestionID + objectMap := make(map[string]string) + objectMap["question"] = uid.DeShortID(objInfo.QuestionID) + objectMap["answer"] = uid.DeShortID(objInfo.AnswerID) + objectMap["comment"] = objInfo.CommentID + req.ObjectInfo.ObjectMap = objectMap + } } if msg.Type == schema.NotificationTypeAchievement { @@ -188,10 +196,13 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N if err != nil { return fmt.Errorf("add notification error: %w", err) } - err = ns.addRedDot(ctx, info.UserID, info.Type) + err = ns.addRedDot(ctx, info.UserID, msg.Type) if err != nil { log.Error("addRedDot Error", err.Error()) } + if req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { + err = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap["badge_id"]) + } go ns.SendNotificationToAllFollower(ctx, msg, questionID) @@ -201,19 +212,67 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N return nil } -func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, botType int) error { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, userID) - err := ns.data.Cache.SetInt64(ctx, key, 1, 30*24*time.Hour) //Expiration time is one month. +func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error { + var key string + if noticeType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + err := ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } +// AddBadgeAwardAlertCache add badge award alert cache +func (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + c := schema.NewRedDotBadgeAwardCache() + c.AddBadgeAward(&schema.RedDotBadgeAward{ + NotificationID: notificationID, + BadgeID: badgeID, + }) + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) + } + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + c.AddBadgeAward(&schema.RedDotBadgeAward{ + NotificationID: notificationID, + BadgeID: badgeID, + }) + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) +} + +// RemoveBadgeAwardAlertCache remove badge award alert cache +func (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + return nil + } + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + c.RemoveBadgeAward(notificationID) + if len(c.BadgeAwardList) == 0 { + return ns.data.Cache.Del(ctx, key) + } + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) +} + // SendNotificationToAllFollower send notification to all followers func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg, questionID string) { - if msg.NoNeedPushAllFollow { + if msg.NoNeedPushAllFollow || len(questionID) == 0 { return } if msg.NotificationAction != constant.NotificationUpdateQuestion && diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index 6c2e89a9..9a85f07e 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -21,7 +21,6 @@ package object_info import ( "context" - "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/schema"