fix(ios): APIClient 401 错误码传递到 AuthManager,完成错误码分支闭环

- notifyTokenExpired 接受 errorCode 参数,通过 Notification userInfo 传递
- 401 响应提取 errorCode 后传给 notifyTokenExpired
- AuthManager 观察者从 userInfo 读取 errorCode,调用 applyErrorCode
- handleUnauthorized 根据 errorCode 切换精确状态(disabled/deleted/expired)
- restoreSession 冷启动也检查 errorCode,禁用/删除用户不尝试 refresh

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-27 21:13:15 +08:00
parent 539b9a7d2b
commit 49bebad402
2 changed files with 27 additions and 8 deletions

View File

@ -30,9 +30,10 @@ final class AuthManager: ObservableObject {
init() { init() {
tokenExpiredObserver = NotificationCenter.default.addObserver( tokenExpiredObserver = NotificationCenter.default.addObserver(
forName: .tokenExpired, object: nil, queue: .main forName: .tokenExpired, object: nil, queue: .main
) { [weak self] _ in ) { [weak self] notification in
let errorCode = notification.userInfo?["errorCode"] as? String
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
self?.handleUnauthorized() self?.handleUnauthorized(errorCode: errorCode)
} }
} }
} }
@ -59,6 +60,14 @@ final class AuthManager: ObservableObject {
let _: UserProfileResponse = try await APIClient.shared.request("/users/me") let _: UserProfileResponse = try await APIClient.shared.request("/users/me")
session = .authenticated session = .authenticated
} catch { } catch {
// /users/me / refresh
if let apiError = error as? APIError, let code = apiError.errorCode {
await APIClient.shared.setToken(nil)
KeychainHelper.clear()
applyErrorCode(code)
return
}
session = .refreshing session = .refreshing
if let refreshed = await tryRefresh() { if let refreshed = await tryRefresh() {
await APIClient.shared.setToken(refreshed.accessToken) await APIClient.shared.setToken(refreshed.accessToken)
@ -120,11 +129,15 @@ final class AuthManager: ObservableObject {
// MARK: - Private // MARK: - Private
private func handleUnauthorized() { private func handleUnauthorized(errorCode: String? = nil) {
Task { Task {
await APIClient.shared.setToken(nil) await APIClient.shared.setToken(nil)
KeychainHelper.clear() KeychainHelper.clear()
session = .unauthenticated if let code = errorCode {
applyErrorCode(code)
} else {
session = .unauthenticated
}
} }
} }

View File

@ -69,10 +69,12 @@ actor APIClient {
self.token = newToken self.token = newToken
return try await performRequest(path, method: method, body: body, queryItems: queryItems, isRetry: true) return try await performRequest(path, method: method, body: body, queryItems: queryItems, isRetry: true)
} }
await notifyTokenExpired() let info = extractErrorInfo(data)
await notifyTokenExpired(errorCode: info.errorCode)
throw decodeServerError(data, fallback: APIError.unauthorized) throw decodeServerError(data, fallback: APIError.unauthorized)
case 401: case 401:
throw decodeServerError(data, fallback: APIError.unauthorized) let info = extractErrorInfo(data)
throw APIError.serverError(info.message ?? "未授权", code: info.errorCode)
case 400..<500: case 400..<500:
let msg = String(data: data, encoding: .utf8) ?? "" let msg = String(data: data, encoding: .utf8) ?? ""
throw APIError.serverError(msg) throw APIError.serverError(msg)
@ -99,9 +101,13 @@ actor APIClient {
} }
} }
private func notifyTokenExpired() async { private func notifyTokenExpired(errorCode: String? = nil) async {
var userInfo: [AnyHashable: Any]? = nil
if let code = errorCode {
userInfo = ["errorCode": code]
}
await MainActor.run { await MainActor.run {
NotificationCenter.default.post(name: .tokenExpired, object: nil) NotificationCenter.default.post(name: .tokenExpired, object: nil, userInfo: userInfo)
} }
} }