backend/pkg/notification/usecase/notification.go

604 lines
16 KiB
Go
Raw Normal View History

2025-11-17 09:31:58 +00:00
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),
}
}