
- 在APIEndpoints.swift中新增tcToken端点以支持腾讯云COS Token获取。 - 在APIModels.swift中新增TcTokenRequest和TcTokenResponse模型,处理Token请求和响应。 - 在COSManager.swift中实现Token的获取、缓存和过期管理逻辑,提升API请求的安全性。 - 在LanguageSettingsView中添加调试功能,允许测试COS Token获取。 - 在多个视图中更新状态管理和导航逻辑,确保用户体验一致性。 - 在FeedFeature和HomeFeature中优化状态管理,简化视图逻辑。
183 lines
8.0 KiB
Swift
183 lines
8.0 KiB
Swift
import Foundation
|
||
import ComposableArchitecture
|
||
|
||
@Reducer
|
||
struct IDLoginFeature {
|
||
@ObservableState
|
||
struct State: Equatable {
|
||
var userID: String = ""
|
||
var password: String = ""
|
||
var isPasswordVisible = false
|
||
var isLoading = false
|
||
var errorMessage: String?
|
||
|
||
// 新增:Account Model 和 Ticket 相关状态
|
||
var accountModel: AccountModel?
|
||
var isTicketLoading = false
|
||
var ticketError: String?
|
||
var loginStep: LoginStep = .initial
|
||
|
||
enum LoginStep: Equatable {
|
||
case initial // 初始状态
|
||
case authenticating // 正在进行 OAuth 认证
|
||
case gettingTicket // 正在获取 Ticket
|
||
case completed // 认证完成
|
||
case failed // 认证失败
|
||
}
|
||
|
||
#if DEBUG
|
||
init() {
|
||
self.userID = "2356814"
|
||
self.password = "a123456"
|
||
}
|
||
#endif
|
||
}
|
||
|
||
enum Action: Equatable {
|
||
case togglePasswordVisibility
|
||
case loginButtonTapped(userID: String, password: String)
|
||
case forgotPasswordTapped
|
||
case backButtonTapped
|
||
case loginResponse(TaskResult<IDLoginResponse>)
|
||
|
||
// 新增:Ticket 相关 actions
|
||
case requestTicket(accessToken: String)
|
||
case ticketResponse(TaskResult<TicketResponse>)
|
||
case clearTicketError
|
||
case resetLogin
|
||
}
|
||
|
||
@Dependency(\.apiService) var apiService
|
||
|
||
var body: some ReducerOf<Self> {
|
||
Reduce { state, action in
|
||
switch action {
|
||
case .togglePasswordVisibility:
|
||
state.isPasswordVisible.toggle()
|
||
return .none
|
||
case let .loginButtonTapped(userID, password):
|
||
state.userID = userID
|
||
state.password = password
|
||
state.isLoading = true
|
||
state.errorMessage = nil
|
||
state.ticketError = nil
|
||
state.loginStep = .authenticating
|
||
// 真实登录 API 调用 Effect
|
||
return .run { send in
|
||
do {
|
||
guard let loginRequest = await LoginHelper.createIDLoginRequest(userID: userID, password: password) else {
|
||
await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
|
||
return
|
||
}
|
||
let response = try await apiService.request(loginRequest)
|
||
await send(.loginResponse(.success(response)))
|
||
} catch {
|
||
if let apiError = error as? APIError {
|
||
await send(.loginResponse(.failure(apiError)))
|
||
} else {
|
||
await send(.loginResponse(.failure(APIError.unknown(error.localizedDescription))))
|
||
}
|
||
}
|
||
}
|
||
case .forgotPasswordTapped:
|
||
return .none
|
||
case .backButtonTapped:
|
||
return .none
|
||
case let .loginResponse(.success(response)):
|
||
state.isLoading = false
|
||
if response.isSuccess {
|
||
state.errorMessage = nil
|
||
if let loginData = response.data,
|
||
let accountModel = AccountModel.from(loginData: loginData) {
|
||
state.accountModel = accountModel
|
||
// 触发 Effect 保存 userInfo(如有)
|
||
if let userInfo = loginData.userInfo {
|
||
return .run { _ in await UserInfoManager.saveUserInfo(userInfo) }
|
||
}
|
||
// 自动获取 ticket
|
||
return .send(.requestTicket(accessToken: accountModel.accessToken!))
|
||
} else {
|
||
state.errorMessage = "登录数据格式错误"
|
||
state.loginStep = .failed
|
||
}
|
||
} else {
|
||
state.errorMessage = response.errorMessage
|
||
state.loginStep = .failed
|
||
}
|
||
return .none
|
||
case let .loginResponse(.failure(error)):
|
||
state.isLoading = false
|
||
state.errorMessage = error.localizedDescription
|
||
state.loginStep = .failed
|
||
return .none
|
||
case let .requestTicket(accessToken):
|
||
state.isTicketLoading = true
|
||
state.ticketError = nil
|
||
state.loginStep = .gettingTicket
|
||
// 先拷贝所需字段,避免并发捕获
|
||
let uid: Int? = {
|
||
if let am = state.accountModel, let uidStr = am.uid { return Int(uidStr) } else { return nil }
|
||
}()
|
||
return .run { send in
|
||
do {
|
||
let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid)
|
||
let response = try await apiService.request(ticketRequest)
|
||
await send(.ticketResponse(.success(response)))
|
||
} catch {
|
||
debugErrorSync("❌ ID登录 Ticket 获取失败: \(error)")
|
||
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
|
||
}
|
||
}
|
||
case let .ticketResponse(.success(response)):
|
||
state.isTicketLoading = false
|
||
if response.isSuccess {
|
||
state.ticketError = nil
|
||
state.loginStep = .completed
|
||
debugInfoSync("✅ ID 登录完整流程成功")
|
||
debugInfoSync("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
|
||
// --- 并发安全修正:彻底避免 Effect 闭包捕获 state/accountModel ---
|
||
if let ticket = response.ticket, let oldAccountModel = state.accountModel {
|
||
// 用 withTicket 生成新 struct,闭包只捕获 newAccountModel
|
||
let newAccountModel = oldAccountModel.withTicket(ticket)
|
||
state.accountModel = newAccountModel
|
||
// 只捕获 newAccountModel,绝不捕获 state
|
||
return .run { _ in
|
||
// 这里不能捕获 state/accountModel,否则 Swift 并发会报错
|
||
await UserInfoManager.saveAccountModel(newAccountModel)
|
||
}
|
||
} else if response.ticket == nil {
|
||
state.ticketError = "Ticket 为空"
|
||
state.loginStep = .failed
|
||
} else {
|
||
debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
|
||
state.ticketError = "内部错误:账户信息丢失"
|
||
state.loginStep = .failed
|
||
}
|
||
} else {
|
||
state.ticketError = response.errorMessage
|
||
state.loginStep = .failed
|
||
}
|
||
return .none
|
||
case let .ticketResponse(.failure(error)):
|
||
state.isTicketLoading = false
|
||
state.ticketError = error.localizedDescription
|
||
state.loginStep = .failed
|
||
debugErrorSync("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)")
|
||
return .none
|
||
case .clearTicketError:
|
||
state.ticketError = nil
|
||
return .none
|
||
case .resetLogin:
|
||
state.isLoading = false
|
||
state.isTicketLoading = false
|
||
state.errorMessage = nil
|
||
state.ticketError = nil
|
||
state.accountModel = nil
|
||
state.loginStep = .initial
|
||
// Effect 清除认证信息
|
||
return .run { _ in await UserInfoManager.clearAllAuthenticationData() }
|
||
}
|
||
}
|
||
}
|
||
}
|