package usecase import ( "context" "strings" "time" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/publish_guard/domain/entity" domrepo "haixun-backend/internal/model/publish_guard/domain/repository" domusecase "haixun-backend/internal/model/publish_guard/domain/usecase" threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase" "github.com/google/uuid" ) type guardUseCase struct { repo domrepo.Repository threadsAccount threadsaccountdomain.UseCase } func NewUseCase(repo domrepo.Repository, threadsAccount threadsaccountdomain.UseCase) domusecase.UseCase { return &guardUseCase{repo: repo, threadsAccount: threadsAccount} } func (u *guardUseCase) Get(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.PolicySummary, error) { if err := u.require(ctx, tenantID, ownerUID, accountID); err != nil { return nil, err } item, err := u.repo.Get(ctx, tenantID, ownerUID, accountID) if err != nil { if strings.Contains(err.Error(), "not found") { item = defaultPolicy(tenantID, ownerUID, accountID) } else { return nil, err } } out := toSummary(item) return &out, nil } func (u *guardUseCase) Upsert(ctx context.Context, req domusecase.UpsertRequest) (*domusecase.PolicySummary, error) { if err := u.require(ctx, req.TenantID, req.OwnerUID, req.AccountID); err != nil { return nil, err } item := defaultPolicy(req.TenantID, req.OwnerUID, req.AccountID) item.Enabled = req.Enabled if req.MaxDailyPosts > 0 { item.MaxDailyPosts = req.MaxDailyPosts } if req.MinIntervalMinutes > 0 { item.MinIntervalMinutes = req.MinIntervalMinutes } if req.ConsecutiveFailurePauseLimit > 0 { item.ConsecutiveFailurePauseLimit = req.ConsecutiveFailurePauseLimit } if req.Paused != nil { item.Paused = *req.Paused } item.PausedReason = strings.TrimSpace(req.PausedReason) item.UpdateAt = clock.NowUnixNano() saved, err := u.repo.Upsert(ctx, item) if err != nil { return nil, err } out := toSummary(saved) return &out, nil } func (u *guardUseCase) Resume(ctx context.Context, tenantID, ownerUID, accountID string) (*domusecase.PolicySummary, error) { policy, err := u.Get(ctx, tenantID, ownerUID, accountID) if err != nil { return nil, err } paused := false return u.Upsert(ctx, domusecase.UpsertRequest{ TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, Enabled: policy.Enabled, MaxDailyPosts: policy.MaxDailyPosts, MinIntervalMinutes: policy.MinIntervalMinutes, ConsecutiveFailurePauseLimit: policy.ConsecutiveFailurePauseLimit, Paused: &paused, }) } func (u *guardUseCase) Pause(ctx context.Context, tenantID, ownerUID, accountID, reason string) (*domusecase.PolicySummary, error) { policy, err := u.Get(ctx, tenantID, ownerUID, accountID) if err != nil { return nil, err } paused := true return u.Upsert(ctx, domusecase.UpsertRequest{ TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, Enabled: policy.Enabled, MaxDailyPosts: policy.MaxDailyPosts, MinIntervalMinutes: policy.MinIntervalMinutes, ConsecutiveFailurePauseLimit: policy.ConsecutiveFailurePauseLimit, Paused: &paused, PausedReason: reason, }) } func (u *guardUseCase) Check(ctx context.Context, req domusecase.CheckRequest) (*domusecase.CheckResult, error) { policy, err := u.Get(ctx, req.TenantID, req.OwnerUID, req.AccountID) if err != nil { return nil, err } if !policy.Enabled { return &domusecase.CheckResult{Allowed: true}, nil } now := req.Now if now <= 0 { now = clock.NowUnixNano() } if policy.Paused { return &domusecase.CheckResult{Allowed: false, Reason: firstNonEmpty(policy.PausedReason, "發文已暫停")}, nil } if policy.MaxDailyPosts > 0 && req.PublishedToday >= int64(policy.MaxDailyPosts) { return &domusecase.CheckResult{Allowed: false, DelayUntil: now + int64(time.Hour), Reason: "已達每日發文上限"}, nil } if policy.MinIntervalMinutes > 0 && req.LastPublishedAt > 0 { minNext := req.LastPublishedAt + int64(time.Duration(policy.MinIntervalMinutes)*time.Minute) if minNext > now { return &domusecase.CheckResult{Allowed: false, DelayUntil: minNext, Reason: "未達最小發文間隔"}, nil } } if policy.ConsecutiveFailurePauseLimit > 0 && req.FailureStreak >= policy.ConsecutiveFailurePauseLimit { return &domusecase.CheckResult{Allowed: false, Reason: "連續失敗達上限,已暫停帳號發文", ShouldPause: true}, nil } return &domusecase.CheckResult{Allowed: true}, nil } func (u *guardUseCase) require(ctx context.Context, tenantID, ownerUID, accountID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Auth).AuthUnauthorized("missing actor") } if strings.TrimSpace(accountID) == "" { return app.For(code.ThreadsAccount).InputMissingRequired("account_id is required") } if u.threadsAccount != nil { _, err := u.threadsAccount.Get(ctx, tenantID, ownerUID, accountID) return err } return nil } func defaultPolicy(tenantID, ownerUID, accountID string) *entity.Policy { now := clock.NowUnixNano() return &entity.Policy{ ID: uuid.NewString(), TenantID: strings.TrimSpace(tenantID), OwnerUID: strings.TrimSpace(ownerUID), AccountID: strings.TrimSpace(accountID), Enabled: true, MaxDailyPosts: 6, MinIntervalMinutes: 90, ConsecutiveFailurePauseLimit: 3, CreateAt: now, UpdateAt: now, } } func toSummary(item *entity.Policy) domusecase.PolicySummary { if item == nil { return domusecase.PolicySummary{} } return domusecase.PolicySummary{ AccountID: item.AccountID, Enabled: item.Enabled, MaxDailyPosts: item.MaxDailyPosts, MinIntervalMinutes: item.MinIntervalMinutes, ConsecutiveFailurePauseLimit: item.ConsecutiveFailurePauseLimit, Paused: item.Paused, PausedReason: item.PausedReason, UpdateAt: item.UpdateAt, } } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" }