Files
e-party-iOS/yana/Features/IDLoginFeature.swift
edwinQQQ 0fe3b6cb7a feat: 新增用户信息获取功能及相关模型
- 在APIEndpoints.swift中新增getUserInfo端点以支持获取用户信息。
- 在APIModels.swift中实现获取用户信息请求和响应模型,处理用户信息的请求与解析。
- 在UserInfoManager中新增方法以从服务器获取用户信息,并在登录成功后自动获取用户信息。
- 在SettingFeature中新增用户信息刷新状态管理,支持用户信息的刷新操作。
- 在SettingView中集成用户信息刷新按钮,提升用户体验。
- 在SplashFeature中实现自动获取用户信息的逻辑,优化用户登录流程。
- 在yanaAPITests中添加用户信息相关的单元测试,确保功能的正确性。
2025-07-23 11:46:46 +08:00

192 lines
8.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")")
if let ticket = response.ticket, let oldAccountModel = state.accountModel {
let newAccountModel = oldAccountModel.withTicket(ticket)
state.accountModel = newAccountModel
return .run { _ in
await UserInfoManager.saveAccountModel(newAccountModel)
//
debugInfoSync("🔄 登录成功,开始获取用户信息")
if let _ = await UserInfoManager.fetchUserInfoFromServer(
uid: newAccountModel.uid,
apiService: apiService
) {
debugInfoSync("✅ 用户信息获取成功")
} else {
debugErrorSync("❌ 用户信息获取失败,但不影响登录流程")
}
}
} 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() }
}
}
}
}