diff --git a/app/src/main/java/com/rainy/token/data/repository/RetryHelper.kt b/app/src/main/java/com/rainy/token/data/repository/RetryHelper.kt new file mode 100644 index 0000000..f6b73e4 --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/repository/RetryHelper.kt @@ -0,0 +1,39 @@ +package com.rainy.token.data.repository + +import kotlinx.coroutines.delay +import kotlin.math.pow + +/** + * 通用重试包装。 + * + * 对 [RepositoryError.Network] 和 [RepositoryError.ServerError] 执行指数退避重试。 + * 401/403(InvalidCredential)、429(RateLimited)、ParseError 等不重试。 + * + * @param maxRetries 最大重试次数(不含首次),默认 2 + * @param baseDelayMs 退避基数,默认 1000ms(第二次 = 2000ms) + * @param block 实际请求逻辑 + */ +suspend fun retryOnTransientError( + maxRetries: Int = 2, + baseDelayMs: Long = 1000L, + block: suspend () -> Result +): Result { + var lastResult = block() + if (lastResult.isSuccess) return lastResult + + repeat(maxRetries) { attempt -> + val error = lastResult.exceptionOrNull() + val shouldRetry = when (error) { + is RepositoryError.Network -> true + is RepositoryError.ServerError -> error.code in 500..599 + else -> false + } + if (!shouldRetry) return lastResult + + val backoffMs = baseDelayMs * (2.0.pow(attempt).toLong()) + delay(backoffMs) + lastResult = block() + if (lastResult.isSuccess) return lastResult + } + return lastResult +} \ No newline at end of file diff --git a/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt b/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt index c6dfba3..e558678 100644 --- a/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt +++ b/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt @@ -5,6 +5,7 @@ import com.rainy.token.data.repository.CommandCodeGoRepository import com.rainy.token.data.repository.DeepSeekRepository import com.rainy.token.data.repository.OllamaRepository import com.rainy.token.data.repository.OpenCodeGoRepository +import com.rainy.token.data.repository.retryOnTransientError import com.rainy.token.domain.model.ServiceBalance import com.rainy.token.domain.service.ServiceType import javax.inject.Inject @@ -19,6 +20,8 @@ import javax.inject.Provider * 注入同一 UseCase 时的"could not be resolved"误报(KSP 已知 issue)。 * Provider 让 Hilt 推迟创建 Repository 实例到第一次 .get() 时,KSP 不需要在 * 编译期解析所有构造器签名。 + * - **重试策略**:对 Network 和 5xx ServerError 自动指数退避重试(最多 2 次), + * 401/403/429/ParseError 不重试。 */ class RefreshBalanceUseCase @Inject constructor( private val deepSeekRepositoryProvider: Provider, @@ -28,10 +31,10 @@ class RefreshBalanceUseCase @Inject constructor( private val ollamaRepositoryProvider: Provider ) { suspend operator fun invoke(service: ServiceType): Result = when (service) { - ServiceType.DEEPSEEK -> deepSeekRepositoryProvider.get().fetchBalance() - ServiceType.OPENCODE_GO -> openCodeGoRepositoryProvider.get().fetchBalance() - ServiceType.COMMANDCODE_GO -> commandCodeGoRepositoryProvider.get().fetchBalance() - ServiceType.CODEX -> codexRepositoryProvider.get().fetchBalance() - ServiceType.OLLAMA -> ollamaRepositoryProvider.get().fetchBalance() + ServiceType.DEEPSEEK -> retryOnTransientError { deepSeekRepositoryProvider.get().fetchBalance() } + ServiceType.OPENCODE_GO -> retryOnTransientError { openCodeGoRepositoryProvider.get().fetchBalance() } + ServiceType.COMMANDCODE_GO -> retryOnTransientError { commandCodeGoRepositoryProvider.get().fetchBalance() } + ServiceType.CODEX -> retryOnTransientError { codexRepositoryProvider.get().fetchBalance() } + ServiceType.OLLAMA -> retryOnTransientError { ollamaRepositoryProvider.get().fetchBalance() } } } \ No newline at end of file