package usecase import ( "backend/pkg/notification/domain/entity" "backend/pkg/notification/domain/notification" "backend/pkg/notification/domain/repository" "backend/pkg/notification/domain/usecase" "context" "errors" "fmt" "time" errs "backend/pkg/library/errors" "github.com/gocql/gocql" ) // NotificationUseCaseParam 通知服務參數配置 type NotificationUseCaseParam struct { Repo repository.NotificationRepository Logger errs.Logger } // NotificationUseCase 通知服務實現 type NotificationUseCase struct { param NotificationUseCaseParam } // MustNotificationUseCase 創建通知服務實例 func MustNotificationUseCase(param NotificationUseCaseParam) usecase.NotificationUseCase { return &NotificationUseCase{ param: param, } } // ==================== EventUseCase 實現 ==================== // CreateEvent 創建新的通知事件 func (uc *NotificationUseCase) CreateEvent(ctx context.Context, e *usecase.NotificationEvent) error { // 驗證輸入 if err := uc.validateNotificationEvent(e); err != nil { return err } // 轉換 priority priority, err := uc.parsePriority(e.Priority) if err != nil { return errs.InputInvalidRangeError(fmt.Sprintf("invalid priority: %s", e.Priority)).Wrap(err) } // 創建 entity event := &entity.NotificationEvent{ EventID: gocql.TimeUUID(), EventType: e.EventType, ActorUID: e.ActorUID, ObjectType: e.ObjectType, ObjectID: e.ObjectID, Title: e.Title, Body: e.Body, Payload: e.Payload, Priority: priority, CreatedAt: time.Now().UTC(), } // 保存到資料庫 if err := uc.param.Repo.Create(ctx, event); err != nil { return errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "event_type", Val: e.EventType}, {Key: "actor_uid", Val: e.ActorUID}, {Key: "func", Val: "NotificationRepository.Create"}, {Key: "error", Val: err.Error()}, }, "failed to create notification event", ).Wrap(err) } return nil } // GetEventByID 根據 ID 獲取事件 func (uc *NotificationUseCase) GetEventByID(ctx context.Context, id string) (*usecase.NotificationEventResp, error) { // 驗證 UUID 格式 if _, err := gocql.ParseUUID(id); err != nil { return nil, errs.InputInvalidRangeError(fmt.Sprintf("invalid event ID format: %s", id)).Wrap(err) } // 從資料庫獲取 event, err := uc.param.Repo.GetByID(ctx, id) if err != nil { return nil, errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "event_id", Val: id}, {Key: "func", Val: "NotificationRepository.GetByID"}, {Key: "error", Val: err.Error()}, }, "failed to get notification event by ID", ).Wrap(err) } // 轉換為響應格式 return uc.entityToEventResp(event), nil } // ListEventsByObject 根據物件查詢事件列表 func (uc *NotificationUseCase) ListEventsByObject(ctx context.Context, param usecase.QueryNotificationEventParam) ([]*usecase.NotificationEventResp, error) { // 驗證參數 if param.ObjectID == nil || param.ObjectType == nil || param.Limit == nil { return nil, errs.InputInvalidRangeError("object_id and object_type are required") } // 構建查詢參數 repoParam := repository.QueryNotificationEventParam{ ObjectID: param.ObjectID, ObjectType: param.ObjectType, Limit: param.Limit, } // 從資料庫查詢 events, err := uc.param.Repo.ListByObject(ctx, repoParam) if err != nil { return nil, errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "object_id", Val: *param.ObjectID}, {Key: "object_type", Val: *param.ObjectType}, {Key: "func", Val: "NotificationRepository.ListByObject"}, {Key: "error", Val: err.Error()}, }, "failed to list notification events by object", ).Wrap(err) } // 轉換為響應格式 result := make([]*usecase.NotificationEventResp, 0, len(events)) for _, event := range events { result = append(result, uc.entityToEvent(event)) } return result, nil } // ==================== UserNotificationUseCase 實現 ==================== // CreateUserNotification 為單個用戶創建通知 func (uc *NotificationUseCase) CreateUserNotification(ctx context.Context, n *usecase.UserNotification) error { // 驗證輸入 if err := uc.validateUserNotification(n); err != nil { return err } // 生成 bucket bucket := uc.generateBucket(time.Now().UTC()) // 解析 EventID eventID, err := gocql.ParseUUID(n.EventID) if err != nil { return errs.InputInvalidRangeError(fmt.Sprintf("invalid event ID format: %s", n.EventID)).Wrap(err) } // 創建 entity userNotif := &entity.UserNotification{ UserID: n.UserID, Bucket: bucket, TS: gocql.TimeUUID(), EventID: eventID, Status: notification.UNREAD, ReadAt: time.Time{}, } // 計算 TTL(如果未提供,使用默認值) ttlSeconds := n.TTL if ttlSeconds == 0 { ttlSeconds = uc.calculateDefaultTTL() } // 保存到資料庫 if err := uc.param.Repo.CreateUserNotification(ctx, userNotif, ttlSeconds); err != nil { return errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "user_id", Val: n.UserID}, {Key: "event_id", Val: n.EventID}, {Key: "func", Val: "NotificationRepository.CreateUserNotification"}, {Key: "error", Val: err.Error()}, }, "failed to create user notification", ).Wrap(err) } return nil } // BulkCreateNotifications 批量創建通知 func (uc *NotificationUseCase) BulkCreateNotifications(ctx context.Context, list []*usecase.UserNotification) error { if len(list) == 0 { return errs.InputInvalidRangeError("notification list cannot be empty") } // 生成 bucket bucket := uc.generateBucket(time.Now().UTC()) // 轉換為 entity 列表 entities := make([]*entity.UserNotification, 0, len(list)) for _, n := range list { // 驗證輸入 if err := uc.validateUserNotification(n); err != nil { return err } // 解析 EventID eventID, err := gocql.ParseUUID(n.EventID) if err != nil { return errs.InputInvalidRangeError(fmt.Sprintf("invalid event ID format: %s", n.EventID)).Wrap(err) } // 計算 TTL ttlSeconds := n.TTL if ttlSeconds == 0 { ttlSeconds = uc.calculateDefaultTTL() } e := &entity.UserNotification{ UserID: n.UserID, Bucket: bucket, TS: gocql.TimeUUID(), EventID: eventID, Status: notification.UNREAD, ReadAt: time.Time{}, } entities = append(entities, e) } // 使用第一個通知的 TTL(假設批量通知使用相同的 TTL) ttlSeconds := list[0].TTL if ttlSeconds == 0 { ttlSeconds = uc.calculateDefaultTTL() } // 批量保存 if err := uc.param.Repo.BulkCreate(ctx, entities, ttlSeconds); err != nil { return errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "count", Val: len(list)}, {Key: "func", Val: "NotificationRepository.BulkCreate"}, {Key: "error", Val: err.Error()}, }, "failed to bulk create user notifications", ).Wrap(err) } return nil } // ListLatestNotifications 獲取用戶最新的通知列表 func (uc *NotificationUseCase) ListLatestNotifications(ctx context.Context, opt usecase.ListLatestOptions) ([]*usecase.UserNotificationResponse, error) { // 驗證參數 if opt.UserID == "" { return nil, errs.InputInvalidRangeError("user_id is required") } // 限制 Limit 最大值 if opt.Limit <= 0 { opt.Limit = 20 // 默認值 } // 如果未提供 buckets,生成默認的 buckets(最近 3 個月) if len(opt.Buckets) == 0 { opt.Buckets = uc.generateDefaultBuckets() } // 構建查詢參數 repoOpt := repository.ListLatestOptions{ UserID: opt.UserID, Buckets: opt.Buckets, Limit: opt.Limit, } // 從資料庫查詢 notifications, err := uc.param.Repo.ListLatest(ctx, repoOpt) if err != nil { return nil, errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "user_id", Val: opt.UserID}, {Key: "buckets", Val: opt.Buckets}, {Key: "func", Val: "NotificationRepository.ListLatest"}, {Key: "error", Val: err.Error()}, }, "failed to list latest notifications", ).Wrap(err) } // 轉換為響應格式 result := make([]*usecase.UserNotificationResponse, 0, len(notifications)) for _, n := range notifications { result = append(result, uc.entityToUserNotificationResp(n)) } return result, nil } // MarkAsRead 標記單個通知為已讀 func (uc *NotificationUseCase) MarkAsRead(ctx context.Context, userID, bucket string, ts string) error { // 驗證參數 if userID == "" || bucket == "" || ts == "" { return errs.InputInvalidRangeError("user_id, bucket, and ts are required") } // 解析 TimeUUID timeUUID, err := gocql.ParseUUID(ts) if err != nil { return errs.InputInvalidRangeError(fmt.Sprintf("invalid ts format: %s", ts)).Wrap(err) } // 更新資料庫 if err := uc.param.Repo.MarkRead(ctx, userID, bucket, timeUUID); err != nil { return errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "user_id", Val: userID}, {Key: "bucket", Val: bucket}, {Key: "ts", Val: ts}, {Key: "func", Val: "NotificationRepository.MarkRead"}, {Key: "error", Val: err.Error()}, }, "failed to mark notification as read", ).Wrap(err) } return nil } // MarkAllAsRead 標記指定 buckets 範圍內的所有通知為已讀 func (uc *NotificationUseCase) MarkAllAsRead(ctx context.Context, userID string, buckets []string) error { // 驗證參數 if userID == "" { return errs.InputInvalidRangeError("user_id is required") } // 如果未提供 buckets,使用默認的 buckets if len(buckets) == 0 { buckets = uc.generateDefaultBuckets() } // 更新資料庫 if err := uc.param.Repo.MarkAllRead(ctx, userID, buckets); err != nil { return errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "user_id", Val: userID}, {Key: "buckets", Val: buckets}, {Key: "func", Val: "NotificationRepository.MarkAllRead"}, {Key: "error", Val: err.Error()}, }, "failed to mark all notifications as read", ).Wrap(err) } return nil } // CountUnread 計算未讀通知數量(近似值) func (uc *NotificationUseCase) CountUnread(ctx context.Context, userID string, buckets []string) (int64, error) { // 驗證參數 if userID == "" { return 0, errs.InputInvalidRangeError("user_id is required") } // 如果未提供 buckets,使用默認的 buckets if len(buckets) == 0 { buckets = uc.generateDefaultBuckets() } // 從資料庫查詢 count, err := uc.param.Repo.CountUnreadApprox(ctx, userID, buckets) if err != nil { return 0, errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "user_id", Val: userID}, {Key: "buckets", Val: buckets}, {Key: "func", Val: "NotificationRepository.CountUnreadApprox"}, {Key: "error", Val: err.Error()}, }, "failed to count unread notifications", ).Wrap(err) } return count, nil } // ==================== CursorUseCase 實現 ==================== // GetCursor 獲取用戶的通知光標 func (uc *NotificationUseCase) GetCursor(ctx context.Context, userID string) (*usecase.NotificationCursor, error) { // 驗證參數 if userID == "" { return nil, errs.InputInvalidRangeError("user_id is required") } // 從資料庫查詢 cursor, err := uc.param.Repo.GetCursor(ctx, userID) if err != nil { return nil, errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "user_id", Val: userID}, {Key: "func", Val: "NotificationRepository.GetCursor"}, {Key: "error", Val: err.Error()}, }, "failed to get notification cursor", ).Wrap(err) } // 如果不存在,返回 nil if cursor == nil { return nil, nil } // 轉換為響應格式 return uc.entityToCursor(cursor), nil } // UpdateCursor 更新或插入通知光標 func (uc *NotificationUseCase) UpdateCursor(ctx context.Context, param *usecase.UpdateNotificationCursorParam) error { // 驗證參數 if param == nil { return errs.InputInvalidRangeError("cursor param is required") } if param.UID == "" { return errs.InputInvalidRangeError("uid is required") } if param.LastSeenTS == "" { return errs.InputInvalidRangeError("last_seen_ts is required") } // 解析 TimeUUID lastSeenTS, err := gocql.ParseUUID(param.LastSeenTS) if err != nil { return errs.InputInvalidRangeError(fmt.Sprintf("invalid last_seen_ts format: %s", param.LastSeenTS)).Wrap(err) } // 創建 entity cursor := &entity.NotificationCursor{ UID: param.UID, LastSeenTS: lastSeenTS, UpdatedAt: time.Now(), } // 更新資料庫 if err := uc.param.Repo.UpsertCursor(ctx, cursor); err != nil { return errs.DBErrorErrorL( uc.param.Logger, []errs.LogField{ {Key: "uid", Val: param.UID}, {Key: "last_seen_ts", Val: param.LastSeenTS}, {Key: "func", Val: "NotificationRepository.UpsertCursor"}, {Key: "error", Val: err.Error()}, }, "failed to update notification cursor", ).Wrap(err) } return nil } // ==================== 輔助函數 ==================== // validateNotificationEvent 驗證通知事件 func (uc *NotificationUseCase) validateNotificationEvent(e *usecase.NotificationEvent) error { if e == nil { return errs.InputInvalidRangeError("notification event is required") } if e.EventType == "" { return errs.InputInvalidRangeError("event_type is required") } if e.ActorUID == "" { return errs.InputInvalidRangeError("actor_uid is required") } if e.ObjectType == "" { return errs.InputInvalidRangeError("object_type is required") } if e.ObjectID == "" { return errs.InputInvalidRangeError("object_id is required") } return nil } // validateUserNotification 驗證用戶通知 func (uc *NotificationUseCase) validateUserNotification(n *usecase.UserNotification) error { if n == nil { return errs.InputInvalidRangeError("user notification is required") } if n.UserID == "" { return errs.InputInvalidRangeError("user_id is required") } if n.EventID == "" { return errs.InputInvalidRangeError("event_id is required") } return nil } // parsePriority 解析優先級字符串 func (uc *NotificationUseCase) parsePriority(priorityStr string) (notification.NotifyPriority, error) { switch priorityStr { case "critical": return notification.Critical, nil case "high": return notification.High, nil case "normal": return notification.Normal, nil case "low": return notification.Low, nil default: return notification.Normal, errors.New("invalid priority value") } } // generateBucket 生成 bucket 字符串(格式:YYYYMM) func (uc *NotificationUseCase) generateBucket(t time.Time) string { return t.Format("200601") } // generateDefaultBuckets 生成默認的 buckets(最近 3 個月) func (uc *NotificationUseCase) generateDefaultBuckets() []string { now := time.Now() buckets := make([]string, 0, 3) for i := 0; i < 3; i++ { month := now.AddDate(0, -i, 0) buckets = append(buckets, month.Format("200601")) } return buckets } // calculateDefaultTTL 計算默認 TTL(90 天) func (uc *NotificationUseCase) calculateDefaultTTL() int { return 90 * 24 * 60 * 60 // 90 天,單位:秒 } // entityToEventResp 將 entity 轉換為 EventResp func (uc *NotificationUseCase) entityToEventResp(e *entity.NotificationEvent) *usecase.NotificationEventResp { return &usecase.NotificationEventResp{ EventID: e.EventID.String(), EventType: e.EventType, ActorUID: e.ActorUID, ObjectType: e.ObjectType, ObjectID: e.ObjectID, Title: e.Title, Body: e.Body, Payload: e.Payload, Priority: e.Priority.ToString(), CreatedAt: e.CreatedAt.UTC().Format(time.RFC3339), } } // entityToEvent 將 entity 轉換為 Event func (uc *NotificationUseCase) entityToEvent(e *entity.NotificationEvent) *usecase.NotificationEventResp { return &usecase.NotificationEventResp{ EventID: e.EventID.String(), EventType: e.EventType, ActorUID: e.ActorUID, ObjectType: e.ObjectType, ObjectID: e.ObjectID, Title: e.Title, Body: e.Body, Payload: e.Payload, Priority: e.Priority.ToString(), CreatedAt: e.CreatedAt.UTC().Format(time.RFC3339), } } // entityToUserNotificationResp 將 entity 轉換為 UserNotificationResponse func (uc *NotificationUseCase) entityToUserNotificationResp(n *entity.UserNotification) *usecase.UserNotificationResponse { resp := &usecase.UserNotificationResponse{ UserID: n.UserID, Bucket: n.Bucket, TS: n.TS.String(), EventID: n.EventID.String(), Status: n.Status.ToString(), } // 如果 ReadAt 不是零值,設置為字符串 if !n.ReadAt.IsZero() { readAtStr := n.ReadAt.UTC().Format(time.RFC3339) resp.ReadAt = &readAtStr } return resp } // entityToCursor 將 entity 轉換為 Cursor func (uc *NotificationUseCase) entityToCursor(c *entity.NotificationCursor) *usecase.NotificationCursor { return &usecase.NotificationCursor{ UID: c.UID, LastSeenTS: c.LastSeenTS.String(), UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339), } }