package usecase import ( "context" "strings" "time" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libthreads "haixun-backend/internal/library/threadsapi" domusecase "haixun-backend/internal/model/threads_account/domain/usecase" ) const defaultTokenRefreshLeadTime = 7 * 24 * time.Hour func (u *threadsAccountUseCase) ResolveAccountAPIAccessToken( ctx context.Context, tenantID, ownerUID, accountID string, ) (string, error) { account, err := u.assertOwned(ctx, tenantID, ownerUID, accountID) if err != nil { return "", err } secrets, err := u.secretsRepo.FindByAccountID(ctx, account.ID) if err != nil { return "", err } if secrets == nil || !isValidAPIToken(secrets) { return "", app.For(code.ThreadsAccount).InputMissingRequired("此帳號尚未完成 Threads API 連線或 token 已過期") } return strings.TrimSpace(secrets.APIAccessToken), nil } func (u *threadsAccountUseCase) RefreshAccountAPIAccessToken( ctx context.Context, tenantID, ownerUID, accountID string, ) (*domusecase.SecretsData, error) { token, err := u.ResolveAccountAPIAccessToken(ctx, tenantID, ownerUID, accountID) if err != nil { return nil, err } result, err := libthreads.RefreshAccessToken(ctx, token) if err != nil { return nil, app.For(code.ThreadsAccount).SvcThirdParty("Threads token refresh 失敗").WithCause(err) } expiresAt := int64(0) if result.ExpiresIn > 0 { expiresAt = libthreads.NsFromNow(result.ExpiresIn) } return u.SaveAPIAccessToken(ctx, accountID, result.AccessToken, expiresAt) } func (u *threadsAccountUseCase) ListTokenRefreshCandidates( ctx context.Context, leadTime time.Duration, ) ([]domusecase.TokenRefreshCandidate, error) { if leadTime <= 0 { leadTime = defaultTokenRefreshLeadTime } deadline := clock.NowUnixNano() + leadTime.Nanoseconds() secretsList, err := u.secretsRepo.ListAPIAccessTokensExpiringBefore(ctx, deadline) if err != nil { return nil, err } out := make([]domusecase.TokenRefreshCandidate, 0, len(secretsList)) for _, secrets := range secretsList { if secrets == nil || strings.TrimSpace(secrets.APIAccessToken) == "" { continue } account, err := u.repo.FindByIDGlobal(ctx, secrets.AccountID) if err != nil || account == nil { continue } out = append(out, domusecase.TokenRefreshCandidate{ AccountID: account.ID, TenantID: account.TenantID, OwnerUID: account.OwnerUID, ExpiresAt: secrets.APITokenExpiresAt, }) } return out, nil }