diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift index ea5c0ae..583b149 100644 --- a/yana/APIs/APIEndpoints.swift +++ b/yana/APIs/APIEndpoints.swift @@ -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" diff --git a/yana/APIs/APIModels.swift b/yana/APIs/APIModels.swift index 8e79ba8..71524bb 100644 --- a/yana/APIs/APIModels.swift +++ b/yana/APIs/APIModels.swift @@ -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) + } +} + diff --git a/yana/APIs/LoginModels.swift b/yana/APIs/LoginModels.swift index 52a96e2..47f3017 100644 --- a/yana/APIs/LoginModels.swift +++ b/yana/APIs/LoginModels.swift @@ -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)") + } +} diff --git a/yana/Features/CreateFeedFeature.swift b/yana/Features/CreateFeedFeature.swift index 079ed6d..fa035b6 100644 --- a/yana/Features/CreateFeedFeature.swift +++ b/yana/Features/CreateFeedFeature.swift @@ -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 diff --git a/yana/Features/EMailLoginFeature.swift b/yana/Features/EMailLoginFeature.swift index 0d5e808..a03e58f 100644 --- a/yana/Features/EMailLoginFeature.swift +++ b/yana/Features/EMailLoginFeature.swift @@ -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)): diff --git a/yana/Features/IDLoginFeature.swift b/yana/Features/IDLoginFeature.swift index 198db8c..e0cac39 100644 --- a/yana/Features/IDLoginFeature.swift +++ b/yana/Features/IDLoginFeature.swift @@ -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 为空" diff --git a/yana/Features/SettingFeature.swift b/yana/Features/SettingFeature.swift index 68525e5..8ff2468 100644 --- a/yana/Features/SettingFeature.swift +++ b/yana/Features/SettingFeature.swift @@ -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) // 新增:刷新用户信息响应 case logoutTapped case logout case dismissTapped } + @Dependency(\.apiService) var apiService // 新增:API服务依赖 + var body: some ReducerOf { 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") -// } \ No newline at end of file +// } diff --git a/yana/Features/SplashFeature.swift b/yana/Features/SplashFeature.swift index 81ac16c..565c73e 100644 --- a/yana/Features/SplashFeature.swift +++ b/yana/Features/SplashFeature.swift @@ -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 { 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 diff --git a/yana/Views/SettingView.swift b/yana/Views/SettingView.swift index c5f4cb2..e0746a4 100644 --- a/yana/Views/SettingView.swift +++ b/yana/Views/SettingView.swift @@ -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) diff --git a/yanaAPITests/yanaAPITests.swift b/yanaAPITests/yanaAPITests.swift index 67c85cc..5b0dd34 100644 --- a/yanaAPITests/yanaAPITests.swift +++ b/yanaAPITests/yanaAPITests.swift @@ -173,3 +173,68 @@ final class yanaAPITests: XCTestCase { } } } + +// MARK: - User Info API Tests +extension yanaAPITests { + + func testGetUserInfoRequest() { + // 测试获取用户信息请求创建 + let uid = "12345" + let request = UserInfoHelper.createGetUserInfoRequest(uid: uid) + + XCTAssertEqual(request.endpoint, "/user/get", "端点应该正确") + XCTAssertEqual(request.method, .GET, "请求方法应该是GET") + XCTAssertEqual(request.queryParameters?["uid"], uid, "UID参数应该正确") + XCTAssertFalse(request.shouldShowLoading, "不应该显示loading") + XCTAssertFalse(request.shouldShowError, "不应该显示错误") + } + + func testGetUserInfoResponse() { + // 测试用户信息响应解析 + let responseData: [String: Any] = [ + "code": 200, + "message": "success", + "timestamp": 1640995200000, + "data": [ + "user_id": "12345", + "username": "testuser", + "nickname": "测试用户", + "avatar": "https://example.com/avatar.jpg", + "email": "test@example.com", + "phone": "13800138000", + "status": "active", + "create_time": "2024-01-01 00:00:00", + "update_time": "2024-01-01 00:00:00" + ] + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: responseData) + let response = try JSONDecoder().decode(GetUserInfoResponse.self, from: jsonData) + + XCTAssertTrue(response.isSuccess, "响应应该成功") + XCTAssertEqual(response.code, 200, "状态码应该正确") + XCTAssertNotNil(response.data, "用户信息数据应该存在") + + if let userInfo = response.data { + XCTAssertEqual(userInfo.userId, "12345", "用户ID应该正确") + XCTAssertEqual(userInfo.username, "testuser", "用户名应该正确") + XCTAssertEqual(userInfo.nickname, "测试用户", "昵称应该正确") + } + + debugInfoSync("✅ 用户信息响应解析测试通过") + + } catch { + XCTFail("解析用户信息响应失败: \(error)") + } + } + + func testUserInfoHelper() { + // 测试UserInfoHelper + let uid = "67890" + UserInfoHelper.debugGetUserInfoRequest(uid: uid) + + let request = UserInfoHelper.createGetUserInfoRequest(uid: uid) + XCTAssertEqual(request.queryParameters?["uid"], uid, "Helper创建的请求应该正确") + } +}