feat: 新增用户信息获取功能及相关模型
- 在APIEndpoints.swift中新增getUserInfo端点以支持获取用户信息。 - 在APIModels.swift中实现获取用户信息请求和响应模型,处理用户信息的请求与解析。 - 在UserInfoManager中新增方法以从服务器获取用户信息,并在登录成功后自动获取用户信息。 - 在SettingFeature中新增用户信息刷新状态管理,支持用户信息的刷新操作。 - 在SettingView中集成用户信息刷新按钮,提升用户体验。 - 在SplashFeature中实现自动获取用户信息的逻辑,优化用户登录流程。 - 在yanaAPITests中添加用户信息相关的单元测试,确保功能的正确性。
This commit is contained in:
@@ -22,6 +22,7 @@ enum APIEndpoint: String, CaseIterable {
|
||||
case latestDynamics = "/dynamic/square/latestDynamics" // 新增:获取最新动态列表端点
|
||||
case tcToken = "/tencent/cos/getToken" // 新增:腾讯云 COS Token 获取端点
|
||||
case publishFeed = "/dynamic/square/publish" // 发布动态
|
||||
case getUserInfo = "/user/get" // 新增:获取用户信息端点
|
||||
|
||||
// Web 页面路径
|
||||
case userAgreement = "/modules/rule/protocol.html"
|
||||
|
@@ -722,3 +722,124 @@ struct TcTokenData: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Info API Management
|
||||
extension UserInfoManager {
|
||||
|
||||
/// 从服务器获取用户信息
|
||||
/// - Parameters:
|
||||
/// - uid: 用户ID,如果为nil则使用当前登录用户的ID
|
||||
/// - apiService: API服务依赖
|
||||
/// - Returns: 用户信息,如果获取失败返回nil
|
||||
static func fetchUserInfoFromServer(
|
||||
uid: String? = nil,
|
||||
apiService: APIServiceProtocol
|
||||
) async -> UserInfo? {
|
||||
// 确定要查询的用户ID
|
||||
let targetUid: String
|
||||
if let uid = uid {
|
||||
targetUid = uid
|
||||
} else {
|
||||
// 使用当前登录用户的ID
|
||||
guard let currentUid = await getCurrentUserId() else {
|
||||
debugErrorSync("❌ 无法获取用户信息:当前用户未登录")
|
||||
return nil
|
||||
}
|
||||
targetUid = currentUid
|
||||
}
|
||||
|
||||
debugInfoSync("👤 开始获取用户信息")
|
||||
debugInfoSync(" 目标UID: \(targetUid)")
|
||||
|
||||
do {
|
||||
let request = UserInfoHelper.createGetUserInfoRequest(uid: targetUid)
|
||||
let response = try await apiService.request(request)
|
||||
|
||||
if response.isSuccess {
|
||||
debugInfoSync("✅ 用户信息获取成功")
|
||||
if let userInfo = response.data {
|
||||
// 保存到本地
|
||||
await saveUserInfo(userInfo)
|
||||
debugInfoSync("💾 用户信息已保存到本地")
|
||||
return userInfo
|
||||
} else {
|
||||
debugErrorSync("❌ 用户信息为空")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
debugErrorSync("❌ 获取用户信息失败: \(response.errorMessage)")
|
||||
return nil
|
||||
}
|
||||
} catch {
|
||||
debugErrorSync("❌ 获取用户信息异常: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新当前用户的用户信息
|
||||
/// - Parameter apiService: API服务依赖
|
||||
/// - Returns: 是否刷新成功
|
||||
static func refreshCurrentUserInfo(apiService: APIServiceProtocol) async -> Bool {
|
||||
guard let currentUid = await getCurrentUserId() else {
|
||||
debugErrorSync("❌ 无法刷新用户信息:当前用户未登录")
|
||||
return false
|
||||
}
|
||||
|
||||
debugInfoSync("🔄 开始刷新当前用户信息")
|
||||
debugInfoSync(" 当前UID: \(currentUid)")
|
||||
|
||||
if let userInfo = await fetchUserInfoFromServer(uid: currentUid, apiService: apiService) {
|
||||
debugInfoSync("✅ 用户信息刷新成功")
|
||||
return true
|
||||
} else {
|
||||
debugErrorSync("❌ 用户信息刷新失败")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定用户的用户信息(带缓存)
|
||||
/// - Parameters:
|
||||
/// - uid: 用户ID
|
||||
/// - apiService: API服务依赖
|
||||
/// - forceRefresh: 是否强制刷新,默认false
|
||||
/// - Returns: 用户信息,如果获取失败返回nil
|
||||
static func getUserInfoWithCache(
|
||||
uid: String,
|
||||
apiService: APIServiceProtocol,
|
||||
forceRefresh: Bool = false
|
||||
) async -> UserInfo? {
|
||||
// 如果不强制刷新,先检查本地缓存
|
||||
if !forceRefresh {
|
||||
if let cachedUserInfo = await getUserInfo() {
|
||||
debugInfoSync("📱 使用本地缓存的用户信息")
|
||||
return cachedUserInfo
|
||||
}
|
||||
}
|
||||
|
||||
// 从服务器获取
|
||||
debugInfoSync("🌐 从服务器获取用户信息")
|
||||
return await fetchUserInfoFromServer(uid: uid, apiService: apiService)
|
||||
}
|
||||
|
||||
/// 在APP启动时自动获取用户信息(如果用户已登录)
|
||||
/// - Parameter apiService: API服务依赖
|
||||
/// - Returns: 是否成功获取或已有缓存
|
||||
static func autoFetchUserInfoOnAppLaunch(apiService: APIServiceProtocol) async -> Bool {
|
||||
// 检查用户是否已登录
|
||||
let authStatus = await checkAuthenticationStatus()
|
||||
guard authStatus.canAutoLogin else {
|
||||
debugInfoSync("🔍 APP启动:用户未登录,跳过用户信息获取")
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否已有用户信息缓存
|
||||
if let cachedUserInfo = await getUserInfo() {
|
||||
debugInfoSync("📱 APP启动:使用现有用户信息缓存")
|
||||
return true
|
||||
}
|
||||
|
||||
// 自动获取用户信息
|
||||
debugInfoSync("🔄 APP启动:自动获取用户信息")
|
||||
return await refreshCurrentUserInfo(apiService: apiService)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -413,3 +413,66 @@ extension LoginHelper {
|
||||
return EmailLoginRequest(email: encryptedEmail, code: code)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Info API Models
|
||||
|
||||
/// 获取用户信息请求模型
|
||||
struct GetUserInfoRequest: APIRequestProtocol {
|
||||
typealias Response = GetUserInfoResponse
|
||||
|
||||
let endpoint = APIEndpoint.getUserInfo.path
|
||||
let method: HTTPMethod = .GET
|
||||
let includeBaseParameters = true
|
||||
let queryParameters: [String: String]?
|
||||
var bodyParameters: [String: Any]? { nil }
|
||||
let timeout: TimeInterval = 30.0
|
||||
let shouldShowLoading: Bool = false // 不显示loading,避免影响用户体验
|
||||
let shouldShowError: Bool = false // 不显示错误,静默处理
|
||||
|
||||
/// 初始化获取用户信息请求
|
||||
/// - Parameter uid: 要查询的用户ID
|
||||
init(uid: String) {
|
||||
self.queryParameters = [
|
||||
"uid": uid
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户信息响应模型
|
||||
struct GetUserInfoResponse: Codable, Equatable {
|
||||
let code: Int?
|
||||
let message: String?
|
||||
let timestamp: Int64?
|
||||
let data: UserInfo?
|
||||
|
||||
/// 是否获取成功
|
||||
var isSuccess: Bool {
|
||||
return code == 200
|
||||
}
|
||||
|
||||
/// 错误消息(如果有)
|
||||
var errorMessage: String {
|
||||
return message ?? "获取用户信息失败,请重试"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Info Helper
|
||||
struct UserInfoHelper {
|
||||
|
||||
/// 创建获取用户信息请求
|
||||
/// - Parameter uid: 用户ID
|
||||
/// - Returns: 配置好的API请求
|
||||
static func createGetUserInfoRequest(uid: String) -> GetUserInfoRequest {
|
||||
return GetUserInfoRequest(uid: uid)
|
||||
}
|
||||
|
||||
/// 调试打印获取用户信息请求
|
||||
/// - Parameter uid: 用户ID
|
||||
static func debugGetUserInfoRequest(uid: String) {
|
||||
debugInfoSync("👤 获取用户信息请求调试")
|
||||
debugInfoSync(" UID: \(uid)")
|
||||
debugInfoSync(" Endpoint: /user/get")
|
||||
debugInfoSync(" Method: GET")
|
||||
debugInfoSync(" Parameters: uid=\(uid)")
|
||||
}
|
||||
}
|
||||
|
@@ -145,7 +145,7 @@ extension CreateFeedFeature.Action: Equatable {
|
||||
|
||||
struct PublishDynamicRequest: APIRequestProtocol {
|
||||
typealias Response = PublishDynamicResponse
|
||||
let endpoint: String = "/dynamic/square/publish"
|
||||
let endpoint: String = APIEndpoint.publishFeed.path
|
||||
let method: HTTPMethod = .POST
|
||||
let includeBaseParameters: Bool = true
|
||||
let queryParameters: [String: String]? = nil
|
||||
|
@@ -160,10 +160,20 @@ struct EMailLoginFeature {
|
||||
case .loginResponse(.success(let accountModel)):
|
||||
state.isLoading = false
|
||||
state.loginStep = .completed
|
||||
// Effect 保存AccountModel并发送通知
|
||||
// Effect 保存AccountModel并获取用户信息
|
||||
return .run { _ in
|
||||
await UserInfoManager.saveAccountModel(accountModel)
|
||||
// 移除:NotificationCenter.default.post(name: .ticketSuccess, object: nil)
|
||||
|
||||
// 新增:登录成功后自动获取用户信息
|
||||
debugInfoSync("🔄 邮箱登录成功,开始获取用户信息")
|
||||
if let _ = await UserInfoManager.fetchUserInfoFromServer(
|
||||
uid: accountModel.uid,
|
||||
apiService: apiService
|
||||
) {
|
||||
debugInfoSync("✅ 用户信息获取成功")
|
||||
} else {
|
||||
debugErrorSync("❌ 用户信息获取失败,但不影响登录流程")
|
||||
}
|
||||
}
|
||||
|
||||
case .loginResponse(.failure(let error)):
|
||||
|
@@ -135,15 +135,24 @@ struct IDLoginFeature {
|
||||
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)
|
||||
|
||||
// 新增:登录成功后自动获取用户信息
|
||||
debugInfoSync("🔄 登录成功,开始获取用户信息")
|
||||
if let _ = await UserInfoManager.fetchUserInfoFromServer(
|
||||
uid: newAccountModel.uid,
|
||||
apiService: apiService
|
||||
) {
|
||||
debugInfoSync("✅ 用户信息获取成功")
|
||||
} else {
|
||||
debugErrorSync("❌ 用户信息获取失败,但不影响登录流程")
|
||||
}
|
||||
}
|
||||
} else if response.ticket == nil {
|
||||
state.ticketError = "Ticket 为空"
|
||||
|
@@ -9,6 +9,7 @@ struct SettingFeature {
|
||||
var accountModel: AccountModel?
|
||||
var isLoading = false
|
||||
var error: String?
|
||||
var isRefreshingUserInfo = false // 新增:用户信息刷新状态
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -17,11 +18,15 @@ struct SettingFeature {
|
||||
case userInfoLoaded(UserInfo?)
|
||||
case loadAccountModel
|
||||
case accountModelLoaded(AccountModel?)
|
||||
case refreshUserInfo // 新增:刷新用户信息
|
||||
case refreshUserInfoResponse(TaskResult<UserInfo?>) // 新增:刷新用户信息响应
|
||||
case logoutTapped
|
||||
case logout
|
||||
case dismissTapped
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService // 新增:API服务依赖
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
@@ -51,6 +56,30 @@ struct SettingFeature {
|
||||
state.accountModel = accountModel
|
||||
return .none
|
||||
|
||||
case .refreshUserInfo: // 新增:刷新用户信息
|
||||
return .none
|
||||
// state.isRefreshingUserInfo = true
|
||||
// state.error = nil
|
||||
// return .run { send in
|
||||
// let userInfo = await UserInfoManager.refreshCurrentUserInfo(apiService: apiService)
|
||||
// await send(.refreshUserInfoResponse(.success(userInfo)))
|
||||
// }
|
||||
|
||||
case let .refreshUserInfoResponse(.success(userInfo)): // 新增:处理刷新响应
|
||||
state.isRefreshingUserInfo = false
|
||||
if let userInfo = userInfo {
|
||||
state.userInfo = userInfo
|
||||
state.error = nil
|
||||
} else {
|
||||
state.error = "刷新用户信息失败"
|
||||
}
|
||||
return .none
|
||||
|
||||
case let .refreshUserInfoResponse(.failure(error)): // 新增:处理刷新错误
|
||||
state.isRefreshingUserInfo = false
|
||||
state.error = error.localizedDescription
|
||||
return .none
|
||||
|
||||
case .logoutTapped:
|
||||
return .send(.logout)
|
||||
|
||||
@@ -61,8 +90,6 @@ struct SettingFeature {
|
||||
}
|
||||
|
||||
case .dismissTapped:
|
||||
// 移除:NotificationCenter.default.post(name: .settingsDismiss, object: nil)
|
||||
// 直接通过父级 action 关闭设置页面
|
||||
return .none
|
||||
}
|
||||
}
|
||||
@@ -72,4 +99,4 @@ struct SettingFeature {
|
||||
// 移除:未使用的通知名称定义
|
||||
// extension Notification.Name {
|
||||
// static let settingsDismiss = Notification.Name("settingsDismiss")
|
||||
// }
|
||||
// }
|
||||
|
@@ -26,11 +26,17 @@ struct SplashFeature {
|
||||
case checkAuthentication
|
||||
case authenticationChecked(UserInfoManager.AuthenticationStatus)
|
||||
|
||||
// 新增:用户信息获取 actions
|
||||
case fetchUserInfo
|
||||
case userInfoFetched(Bool)
|
||||
|
||||
// 新增:导航 actions
|
||||
case navigateToLogin
|
||||
case navigateToMain
|
||||
}
|
||||
|
||||
@Dependency(\.apiService) var apiService // 新增:API服务依赖
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
@@ -65,15 +71,31 @@ struct SplashFeature {
|
||||
state.isCheckingAuthentication = false
|
||||
state.authenticationStatus = status
|
||||
|
||||
// 根据认证状态决定导航目标
|
||||
// 根据认证状态决定下一步操作
|
||||
if status.canAutoLogin {
|
||||
debugInfoSync("🎉 自动登录成功,进入主页")
|
||||
return .send(.navigateToMain)
|
||||
debugInfoSync("🎉 自动登录成功,开始获取用户信息")
|
||||
// 新增:认证成功后自动获取用户信息
|
||||
return .send(.fetchUserInfo)
|
||||
} else {
|
||||
debugInfoSync("🔑 需要手动登录")
|
||||
return .send(.navigateToLogin)
|
||||
}
|
||||
|
||||
case .fetchUserInfo:
|
||||
// 新增:获取用户信息
|
||||
return .run { send in
|
||||
let success = await UserInfoManager.autoFetchUserInfoOnAppLaunch(apiService: apiService)
|
||||
await send(.userInfoFetched(success))
|
||||
}
|
||||
|
||||
case let .userInfoFetched(success):
|
||||
if success {
|
||||
debugInfoSync("✅ 用户信息获取成功,进入主页")
|
||||
} else {
|
||||
debugInfoSync("⚠️ 用户信息获取失败,但仍进入主页")
|
||||
}
|
||||
return .send(.navigateToMain)
|
||||
|
||||
case .navigateToLogin:
|
||||
state.navigationDestination = .login
|
||||
return .none
|
||||
|
@@ -48,7 +48,9 @@ struct SettingView: View {
|
||||
// 内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel)
|
||||
UserInfoCardView(userInfo: store.userInfo, accountModel: store.accountModel, isRefreshing: store.isRefreshingUserInfo, onRefresh: {
|
||||
store.send(.refreshUserInfo)
|
||||
})
|
||||
// .padding()
|
||||
.padding(.top, 32)
|
||||
|
||||
@@ -84,6 +86,8 @@ struct SettingView: View {
|
||||
struct UserInfoCardView: View {
|
||||
let userInfo: UserInfo?
|
||||
let accountModel: AccountModel?
|
||||
let isRefreshing: Bool // 新增:刷新状态
|
||||
let onRefresh: () -> Void // 新增:刷新回调
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
@@ -115,6 +119,28 @@ struct UserInfoCardView: View {
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:刷新按钮
|
||||
Button(action: onRefresh) {
|
||||
HStack(spacing: 4) {
|
||||
if isRefreshing {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
} else {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption)
|
||||
}
|
||||
Text(isRefreshing ? "刷新中..." : "刷新")
|
||||
.font(.caption)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.white.opacity(0.2))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.disabled(isRefreshing)
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
.padding(.horizontal, 20)
|
||||
|
Reference in New Issue
Block a user