backend/pkg/notification/usecase/notification.go

604 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 計算默認 TTL90 天)
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),
}
}