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"

Reply via email to