package usecase import ( "context" "fmt" "strings" "apps/backend/internal/module/appnotif/domain" "github.com/google/uuid" ) type Service struct { Repo domain.Repository } func New(repo domain.Repository) *Service { return &Service{Repo: repo} } func (s *Service) List(ctx context.Context, ownerUID int64) ([]*domain.Notification, error) { return s.Repo.ListByOwner(ctx, ownerUID) } func (s *Service) UnreadCount(ctx context.Context, ownerUID int64) (int64, error) { return s.Repo.UnreadCount(ctx, ownerUID) } func (s *Service) MarkRead(ctx context.Context, ownerUID int64, id string) error { return s.Repo.MarkRead(ctx, ownerUID, id) } func (s *Service) MarkAllRead(ctx context.Context, ownerUID int64) error { return s.Repo.MarkAllRead(ctx, ownerUID) } // NotifyJobTerminal — 相容舊介面;改走 NotifyJobState(upsert 同一 job 一則) func (s *Service) NotifyJobTerminal(ctx context.Context, ownerUID int64, jobID, status, summary string) error { return s.NotifyJobState(ctx, ownerUID, jobID, "", status, summary, 100) } // NotifyJobState — 同一 job 只保留一則通知,進度更新時改寫 title/body(鈴鐺即時有進度) func (s *Service) NotifyJobState(ctx context.Context, ownerUID int64, jobID, templateType, status, summary string, percent int) error { if ownerUID <= 0 || jobID == "" { return nil } title := jobNotifyTitle(templateType, status, percent) body := strings.TrimSpace(summary) if body == "" { body = fmt.Sprintf("%s · %s", title, jobID) } // 進度摘要裡若已含 % 就不再重複;否則附上 percent if percent > 0 && percent < 100 && !strings.Contains(body, "%") { body = fmt.Sprintf("%s · %d%%", body, percent) } existing, err := s.Repo.FindLatestByJobRef(ctx, ownerUID, jobID) now := domain.NowNano() if err == nil && existing != nil { existing.Title = title existing.Body = body existing.Kind = domain.KindJob existing.RefType = domain.RefJob existing.RefID = jobID // 每次有進度/終態都標未讀,鈴鐺才會跳 existing.ReadAt = 0 // 用 created_at 排序時把最新活動頂到前面 existing.CreatedAt = now return s.Repo.Replace(ctx, existing) } n := &domain.Notification{ ID: uuid.NewString(), OwnerUID: ownerUID, Title: title, Body: body, Kind: domain.KindJob, RefType: domain.RefJob, RefID: jobID, CreatedAt: now, } return s.Repo.Insert(ctx, n) } func jobNotifyTitle(templateType, status string, percent int) string { name := templateLabelZH(templateType) switch status { case "succeeded": return name + " · 已完成" case "failed": return name + " · 失敗" case "cancelled": return name + " · 已取消" case "running": if percent > 0 { return fmt.Sprintf("%s · 進行中 %d%%", name, percent) } return name + " · 進行中" case "queued", "pending": return name + " · 已排程" default: if percent > 0 { return fmt.Sprintf("%s · %d%%", name, percent) } return name + " · 更新" } } func templateLabelZH(templateType string) string { switch templateType { case "persona_analyze_account": return "人設分析(爬公開貼文)" case "persona_analyze_text": return "人設分析(文字)" case "compose_mimic": return "仿寫貼文" case "threads_token_renew": return "Threads Token 延長" case "demo": return "Demo 測試任務" default: if strings.TrimSpace(templateType) == "" { return "背景任務" } return "背景任務" } }