604 lines
16 KiB
Go
604 lines
16 KiB
Go
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),
|
||
}
|
||
}
|