feat: 新增用户信息获取功能及相关模型

- 在APIEndpoints.swift中新增getUserInfo端点以支持获取用户信息。
- 在APIModels.swift中实现获取用户信息请求和响应模型,处理用户信息的请求与解析。
- 在UserInfoManager中新增方法以从服务器获取用户信息,并在登录成功后自动获取用户信息。
- 在SettingFeature中新增用户信息刷新状态管理,支持用户信息的刷新操作。
- 在SettingView中集成用户信息刷新按钮,提升用户体验。
- 在SplashFeature中实现自动获取用户信息的逻辑,优化用户登录流程。
- 在yanaAPITests中添加用户信息相关的单元测试,确保功能的正确性。
This commit is contained in:
edwinQQQ
2025-07-23 11:46:46 +08:00
parent 8362142c49
commit 0fe3b6cb7a
10 changed files with 358 additions and 14 deletions

View File

@@ -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"

View File

@@ -722,3 +722,124 @@ struct TcTokenData: Codable, Equatable {
}
}
// MARK: - User Info API Management
extension UserInfoManager {
///
/// - Parameters:
/// - uid: IDnil使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)
}
}

View File

@@ -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)")
}
}

View File

@@ -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

View File

@@ -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)):

View File

@@ -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 为空"

View File

@@ -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")
// }
// }

View File

@@ -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

View File

@@ -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)

View File

@@ -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创建的请求应该正确")
}
}