251 lines
10 KiB
Swift
251 lines
10 KiB
Swift
import SwiftUI
|
||
import Kingfisher
|
||
|
||
class UserManager: ObservableObject {
|
||
static let shared = UserManager()
|
||
|
||
@Published var isLoggedIn: Bool = false
|
||
// 将 currentUser 的类型从 AuthModels.UserInfo 扩展为包含统计数据的 UserProfileResponse
|
||
// 或者我们简单点,只存核心数据,统计数据单独存
|
||
@Published var currentUser: UserInfo?
|
||
@Published var studyStats: (time: Int, lessons: Int) = (0, 0)
|
||
/// 头像更新后递增,用于电子卡/笔记头像 URL 缓存破坏,实时显示最新头像
|
||
@Published var avatarCacheBust: Int = 0
|
||
|
||
// ✨ 新增:游客状态(只读计算属性)
|
||
var isGuest: Bool {
|
||
return !isLoggedIn
|
||
}
|
||
|
||
private let apiClient = APIClient.shared
|
||
|
||
init() {
|
||
checkLoginStatus()
|
||
}
|
||
|
||
func checkLoginStatus() {
|
||
if TokenManager.shared.getToken() != nil {
|
||
isLoggedIn = true
|
||
// 启动时静默刷新用户信息
|
||
Task { try? await fetchUserProfile() }
|
||
}
|
||
}
|
||
|
||
// MARK: - F7: Guest Progress Logic
|
||
|
||
/// 🎒 [新增] 1. 游客暂存逻辑(存入 UserDefaults)
|
||
func saveGuestProgress(nodeId: String, totalStudyTime: Int, completedSlides: Int) {
|
||
let data: [String: Any] = [
|
||
"nodeId": nodeId,
|
||
"totalStudyTime": totalStudyTime,
|
||
"completedSlides": completedSlides,
|
||
"timestamp": Date().timeIntervalSince1970
|
||
]
|
||
UserDefaults.standard.set(data, forKey: "pending_guest_record")
|
||
print("📦 [Guest] Progress saved locally for node: \(nodeId)")
|
||
}
|
||
|
||
/// 🔄 [新增] 2. 补单逻辑(登录成功后调用)
|
||
func syncGuestProgress() async {
|
||
guard let data = UserDefaults.standard.dictionary(forKey: "pending_guest_record"),
|
||
let nodeId = data["nodeId"] as? String,
|
||
let totalStudyTime = data["totalStudyTime"] as? Int,
|
||
let completedSlides = data["completedSlides"] as? Int else {
|
||
print("🔄 [Sync] No pending guest record found")
|
||
return
|
||
}
|
||
|
||
print("🔄 [Sync] Found pending guest record for node: \(nodeId), syncing to server...")
|
||
|
||
do {
|
||
// 调用后端补单接口
|
||
let _ = try await LearningService.shared.completeLesson(
|
||
nodeId: nodeId,
|
||
totalStudyTime: totalStudyTime,
|
||
completedSlides: completedSlides
|
||
)
|
||
|
||
print("✅ [Sync] Guest progress synced successfully!")
|
||
// 销毁证据(清空背包)
|
||
UserDefaults.standard.removeObject(forKey: "pending_guest_record")
|
||
} catch {
|
||
print("❌ [Sync] Failed to sync guest progress: \(error)")
|
||
// 失败则保留数据,下次登录时重试
|
||
}
|
||
}
|
||
|
||
// MARK: - Login Handler
|
||
|
||
/// ✨ [修改] 3. 在登录成功处理中插入补单逻辑
|
||
// ⚠️ 重要:此方法必须在主线程调用,因为会更新 @Published 属性
|
||
@MainActor
|
||
func handleLoginSuccess(_ data: LoginData) {
|
||
print("👤 [UserManager] 开始处理登录成功(主线程)")
|
||
print("🔍 [UserManager] handleLoginSuccess: 登录响应 digitalId = \(data.user.digitalId ?? "nil")")
|
||
_ = TokenManager.shared.saveToken(data.token) // ✅ 明确忽略返回值
|
||
self.currentUser = data.user
|
||
self.isLoggedIn = true
|
||
// 用户状态已随 user 数据保存,无需额外处理
|
||
print("👤 [UserManager] 登录状态已更新: isLoggedIn = \(isLoggedIn)")
|
||
print("🔍 [UserManager] handleLoginSuccess: 设置后 currentUser.digitalId = \(self.currentUser?.digitalId ?? "nil")")
|
||
|
||
// ✨ 新增:触发补单逻辑(后台线程,不阻塞登录)
|
||
Task.detached(priority: .utility) {
|
||
await UserManager.shared.syncGuestProgress()
|
||
}
|
||
|
||
// 拉取用户资料
|
||
Task.detached(priority: .utility) {
|
||
do {
|
||
try await UserManager.shared.fetchUserProfile()
|
||
print("👤 [UserManager] 用户资料拉取成功")
|
||
} catch {
|
||
print("👤 [UserManager] ⚠️ 用户资料拉取失败(非阻塞): \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
func logout() {
|
||
TokenManager.shared.deleteToken()
|
||
self.isLoggedIn = false
|
||
self.currentUser = nil
|
||
self.studyStats = (0, 0)
|
||
// 登出时无需额外清理
|
||
}
|
||
|
||
// MARK: - 新增 API 方法
|
||
|
||
// 1. 获取完整用户信息
|
||
@MainActor
|
||
func fetchUserProfile() async throws {
|
||
print("🔍 [UserManager] fetchUserProfile: 开始调用 API...")
|
||
do {
|
||
let response: APIResponse<UserProfileResponse> = try await apiClient.request(
|
||
endpoint: "/api/user/profile",
|
||
method: "GET",
|
||
requiresAuth: true
|
||
)
|
||
|
||
let data = response.data
|
||
// ✅ 调试:打印 API 返回的 digitalId
|
||
print("🔍 [UserManager] fetchUserProfile: API 返回 digitalId = \(data.digitalId ?? "nil")")
|
||
print("🔍 [UserManager] fetchUserProfile: API 返回完整数据 - id=\(data.id), nickname=\(data.nickname), avatar=\(data.avatar ?? "nil")")
|
||
print("🔍 [头像调试] fetchUserProfile 返回 avatar: \(data.avatar ?? "nil")")
|
||
|
||
// 更新用户基本信息(包含 digitalId)
|
||
let previousAvatar = self.currentUser?.avatar
|
||
self.currentUser = UserInfo(
|
||
id: data.id,
|
||
nickname: data.nickname,
|
||
phone: data.phone,
|
||
avatar: data.avatar,
|
||
digitalId: data.digitalId // ✅ 赛博学习证ID
|
||
)
|
||
if data.avatar != previousAvatar { avatarCacheBust += 1 }
|
||
// ✅ 调试:打印更新后的 digitalId
|
||
print("🔍 [UserManager] fetchUserProfile: 更新后 currentUser.digitalId = \(self.currentUser?.digitalId ?? "nil")")
|
||
|
||
// 更新统计数据
|
||
self.studyStats = (data.total_study_time, data.completed_lessons)
|
||
|
||
print("✅ [UserManager] fetchUserProfile: 完成")
|
||
} catch {
|
||
print("❌ [UserManager] fetchUserProfile: 失败 - \(error)")
|
||
print("❌ [UserManager] fetchUserProfile: 错误详情 - \(error.localizedDescription)")
|
||
throw error // 重新抛出错误,让调用者处理
|
||
}
|
||
}
|
||
|
||
// 2. 更新昵称
|
||
@MainActor
|
||
func updateNickname(_ name: String) async throws {
|
||
let response: APIResponse<UpdateProfileResponse> = try await apiClient.request(
|
||
endpoint: "/api/user/profile",
|
||
method: "PUT",
|
||
body: ["nickname": name],
|
||
requiresAuth: true
|
||
)
|
||
// 本地乐观更新(保持现有头像和 digitalId)
|
||
if let current = currentUser {
|
||
self.currentUser = UserInfo(
|
||
id: current.id,
|
||
nickname: response.data.nickname,
|
||
phone: current.phone,
|
||
avatar: response.data.avatar ?? current.avatar, // ✅ 保持现有头像或使用返回的头像
|
||
digitalId: current.digitalId // ✅ 保持 digitalId
|
||
)
|
||
}
|
||
}
|
||
|
||
// ✅ 新增:更新头像
|
||
/// 更新头像
|
||
/// - Parameter imageUrl: 头像URL(从上传接口获取)
|
||
@MainActor
|
||
func updateAvatar(_ imageUrl: String) async throws {
|
||
print("🔍 [头像调试] updateAvatar 入参 imageUrl: \(imageUrl)")
|
||
let response: APIResponse<UpdateProfileResponse> = try await apiClient.request(
|
||
endpoint: "/api/user/profile",
|
||
method: "PUT",
|
||
body: ["avatar": imageUrl],
|
||
requiresAuth: true
|
||
)
|
||
print("🔍 [头像调试] 更新资料接口返回 response.data.avatar: \(response.data.avatar ?? "nil")")
|
||
// 本地乐观更新
|
||
if let current = currentUser {
|
||
let oldAvatar = current.avatar
|
||
self.currentUser = UserInfo(
|
||
id: current.id,
|
||
nickname: current.nickname,
|
||
phone: current.phone,
|
||
avatar: response.data.avatar,
|
||
digitalId: current.digitalId
|
||
)
|
||
print("🔍 [头像调试] 更新后 currentUser.avatar: \(self.currentUser?.avatar ?? "nil")")
|
||
avatarCacheBust += 1
|
||
// 清除头像缓存(用与视图一致的完整 URL 作为 key),确保电子卡/笔记里下次加载到最新图
|
||
[oldAvatar, response.data.avatar].compactMap { $0 }.filter { !$0.isEmpty }.forEach { path in
|
||
if let url = apiClient.getImageURL(path) { ImageCache.default.removeImage(forKey: url.absoluteString) }
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 更新设置 (推送)
|
||
@MainActor
|
||
func updateSettings(pushEnabled: Bool) async throws {
|
||
let _: APIResponse<UserSettingsResponse> = try await apiClient.request(
|
||
endpoint: "/api/user/settings",
|
||
method: "PUT",
|
||
body: ["push_notification": pushEnabled],
|
||
requiresAuth: true
|
||
)
|
||
}
|
||
|
||
// 4. 获取设置
|
||
func fetchSettings() async throws -> Bool {
|
||
let response: APIResponse<UserSettingsResponse> = try await apiClient.request(
|
||
endpoint: "/api/user/settings",
|
||
method: "GET",
|
||
requiresAuth: true
|
||
)
|
||
return response.data.push_notification
|
||
}
|
||
|
||
// 5. 注销账号 (永久删除)
|
||
/// - Throws: 网络错误或后端返回的错误信息
|
||
@MainActor
|
||
func deleteAccount() async throws {
|
||
// ✅ 使用项目中统一的 APIClient
|
||
// Endpoint: DELETE /api/user/account
|
||
// Response: { "success": true, "message": "..." } -> SimpleResponse
|
||
let _: SimpleResponse = try await apiClient.request(
|
||
endpoint: "/api/user/account",
|
||
method: "DELETE",
|
||
requiresAuth: true // 自动添加 Token
|
||
)
|
||
|
||
// 成功后,执行本地登出清理逻辑
|
||
self.logout()
|
||
}
|
||
}
|
||
|