add delta sync for dm-v21

This commit is contained in:
eric 2026-08-09 19:56:10 +08:00
parent 5eb1125840
commit 177c888c84
4 changed files with 72 additions and 24 deletions

View file

@ -22,10 +22,10 @@ data class RefreshRequest(
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RefreshResponse( data class RefreshResponse(
val access_token: String, val access_token: String? = null,
val expires_in: Int, val expires_in: Int? = null,
val refresh_token: String, val refresh_token: String? = null,
val token: String? = null // 💡 設為可空並給預設值,防止後端沒吐時崩潰 val token: String? = null // 💡 有些後端會直接吐 token 欄位
) )
interface TecnologiaApi { interface TecnologiaApi {

View file

@ -56,6 +56,8 @@ object NetworkModule {
tokenManager.currentAccessToken.firstOrNull() tokenManager.currentAccessToken.firstOrNull()
} }
android.util.Log.d("AuthDebug", "Interceptor - 路徑: $path, Token是否存在: ${!token.isNullOrEmpty()}")
val requestBuilder = originalRequest.newBuilder() val requestBuilder = originalRequest.newBuilder()
if (!token.isNullOrEmpty()) { if (!token.isNullOrEmpty()) {
requestBuilder.addHeader("Authorization", "Bearer $token") requestBuilder.addHeader("Authorization", "Bearer $token")

View file

@ -25,7 +25,15 @@ class TokenAuthenticator @Inject constructor(
} }
override fun authenticate(route: Route?, response: Response): Request? { override fun authenticate(route: Route?, response: Response): Request? {
Log.d("AuthDebug", "--- 進入 Authenticator (觸發網址: ${response.request.url}) ---") val url = response.request.url
Log.d("AuthDebug", "--- 進入 Authenticator (觸發網址: $url) ---")
// 🌟 防禦機制:如果刷新 Token 的請求本身又回傳了 401,代表 Refresh Token 已徹底過期
// 必須直接回傳 null,否則會進入無窮迴圈(甚至死鎖,因為 Mutex 不可重入)
if (url.encodedPath.contains("api/auth/refresh")) {
Log.e("AuthDebug", "❌ 刷新 Token 的請求本身又回傳了 401!停止重試。")
return null
}
// 使用 runBlocking 配合 Mutex,讓同時間進來的 401 請求排隊 // 使用 runBlocking 配合 Mutex,讓同時間進來的 401 請求排隊
return runBlocking { return runBlocking {
@ -35,13 +43,24 @@ class TokenAuthenticator @Inject constructor(
val currentTokenInStore = tokenManager.currentAccessToken.first() val currentTokenInStore = tokenManager.currentAccessToken.first()
val oldRequestToken = response.request.header("Authorization")?.removePrefix("Bearer ") val oldRequestToken = response.request.header("Authorization")?.removePrefix("Bearer ")
Log.d("AuthDebug", "DataStore中的Token: ${currentTokenInStore?.take(10)}...")
Log.d("AuthDebug", "原始請求的Token: ${oldRequestToken?.take(10)}...")
// 🌟 防禦機制:如果 DataStore 裡的 Token 已經跟當初發起請求時的舊 Token 不同了 // 🌟 防禦機制:如果 DataStore 裡的 Token 已經跟當初發起請求時的舊 Token 不同了
// 代表排在你前面的請求已經幫忙刷新成功了!這裡直接拿新 Token 重試即可,不用再打一次 API。 // 代表排在你前面的請求已經幫忙刷新成功了!這裡直接拿新 Token 重試即可,不用再打一次 API。
if (currentTokenInStore != oldRequestToken && !currentTokenInStore.isNullOrEmpty()) { if (currentTokenInStore != oldRequestToken && !currentTokenInStore.isNullOrEmpty()) {
Log.d("AuthDebug", "分流防禦:偵測到其他請求已刷新過 Token,直接重複利用新 Token") Log.d("AuthDebug", "分流防禦:偵測到其他請求已刷新過 Token,直接重複利用新 Token")
return@runBlocking response.request.newBuilder() val requestBuilder = response.request.newBuilder()
.header("Authorization", "Bearer $currentTokenInStore") requestBuilder.header("Authorization", "Bearer $currentTokenInStore")
.build()
// 針對 WebSocket 更新 URL 中的 token 參數
if (response.request.url.queryParameter("token") != null) {
val newUrl = response.request.url.newBuilder()
.setQueryParameter("token", currentTokenInStore)
.build()
requestBuilder.url(newUrl)
}
return@runBlocking requestBuilder.build()
} }
// 2. 真正準備去打後端刷新 // 2. 真正準備去打後端刷新
@ -54,33 +73,58 @@ class TokenAuthenticator @Inject constructor(
} }
// 3. 同步發送網路請求 // 3. 同步發送網路請求
Log.d("AuthDebug", "正在向伺服器請求刷新 Token... RefreshToken: ${refreshToken.take(10)}...")
val refreshResponse = try { val refreshResponse = try {
apiService.get().refreshToken(RefreshRequest(refreshToken)) apiService.get().refreshToken(RefreshRequest(refreshToken))
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}") Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}", e)
null null
} }
// 4. 判斷刷新是否成功 // 4. 判斷刷新是否成功
if (refreshResponse != null && refreshResponse.isSuccessful) { if (refreshResponse != null) {
val newAuthData = refreshResponse.body() Log.d("AuthDebug", "刷新回應碼: ${refreshResponse.code()}, 是否成功: ${refreshResponse.isSuccessful}")
if (newAuthData != null) { if (refreshResponse.isSuccessful) {
Log.d("AuthDebug", "✅ Token 刷新成功,準備寫入 DataStore") val newAuthData = refreshResponse.body()
val newAccessToken = newAuthData?.access_token ?: newAuthData?.token
// 導正順序:確實執行完 saveAuthData if (!newAccessToken.isNullOrEmpty()) {
tokenManager.saveAuthData( Log.d("AuthDebug", "✅ Token 刷新成功,新 AccessToken: ${newAccessToken.take(10)}...")
userId = currentUserId,
accessToken = newAuthData.access_token,
refreshToken = newAuthData.refresh_token,
expiresInSec = newAuthData.expires_in
)
Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求") // 如果後端沒給新的 refresh_token,就沿用舊的
val nextRefreshToken = if (!newAuthData?.refresh_token.isNullOrEmpty()) {
newAuthData?.refresh_token!!
} else {
refreshToken
}
return@runBlocking response.request.newBuilder() tokenManager.saveAuthData(
.header("Authorization", "Bearer ${newAuthData.access_token}") userId = currentUserId,
.build() accessToken = newAccessToken,
refreshToken = nextRefreshToken,
expiresInSec = newAuthData?.expires_in ?: 3600
)
Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求")
// 修正:同步更新 Header 與 URL 中的 token 參數 (針對 WebSocket)
val requestBuilder = response.request.newBuilder()
requestBuilder.header("Authorization", "Bearer $newAccessToken")
if (response.request.url.queryParameter("token") != null) {
val newUrl = response.request.url.newBuilder()
.setQueryParameter("token", newAccessToken)
.build()
requestBuilder.url(newUrl)
}
return@runBlocking requestBuilder.build()
} else {
Log.e("AuthDebug", "❌ 刷新成功但無法取得新 Token (Body: $newAuthData)")
}
} else {
Log.e("AuthDebug", "❌ 刷新失敗,錯誤代碼: ${refreshResponse.code()}, 錯誤內容: ${refreshResponse.errorBody()?.string()}")
} }
} else {
Log.e("AuthDebug", "❌ refreshResponse 為 null (網路異常)")
} }
// 5. 失敗:連 Refresh Token 都失效,強制登出 // 5. 失敗:連 Refresh Token 都失效,強制登出

View file

@ -31,12 +31,14 @@ class TokenManager @Inject constructor(
// 儲存資料(維持原樣,這是安全的) // 儲存資料(維持原樣,這是安全的)
suspend fun saveAuthData(userId: String, accessToken: String, refreshToken: String, expiresInSec: Int) { suspend fun saveAuthData(userId: String, accessToken: String, refreshToken: String, expiresInSec: Int) {
android.util.Log.d("TokenManager", "儲存 Token - User: $userId, AccessToken: ${accessToken.take(10)}..., RefreshToken: ${refreshToken.take(10)}...")
dataStore.edit { preferences -> dataStore.edit { preferences ->
preferences[accessTokenKey(userId)] = accessToken preferences[accessTokenKey(userId)] = accessToken
preferences[refreshTokenKey(userId)] = refreshToken preferences[refreshTokenKey(userId)] = refreshToken
val expiresAt = System.currentTimeMillis() + (expiresInSec * 1000L) val expiresAt = System.currentTimeMillis() + (expiresInSec * 1000L)
preferences[expiresAtKey(userId)] = expiresAt preferences[expiresAtKey(userId)] = expiresAt
android.util.Log.d("TokenManager", "Token 將於 ${(expiresInSec / 60)} 分鐘後過期 (timestamp: $expiresAt)")
preferences[CURRENT_USER_ID] = userId preferences[CURRENT_USER_ID] = userId