import { api, setAuthToken, STORAGE_KEYS } from '@/services/api'; import { updateUser, UpdateUserDto } from '@/services/users'; import AsyncStorage from '@/utils/kvStore'; import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; // 预加载的用户数据存储 let preloadedUserData: { token: string | null; profile: UserProfile; privacyAgreed: boolean; } | null = null; // 预加载用户数据的函数 export async function preloadUserData() { try { const [profileStr, privacyAgreedStr, token] = await Promise.all([ AsyncStorage.getItem(STORAGE_KEYS.userProfile), AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed), AsyncStorage.getItem(STORAGE_KEYS.authToken), ]); let profile: UserProfile = {}; if (profileStr) { try { profile = JSON.parse(profileStr) as UserProfile; } catch { profile = {}; } } const privacyAgreed = privacyAgreedStr === 'true'; // 如果有 token,需要设置到 API 客户端 if (token) { await setAuthToken(token); } preloadedUserData = { token, profile, privacyAgreed }; return preloadedUserData; } catch (error) { console.error('预加载用户数据失败:', error); preloadedUserData = { token: null, profile: {}, privacyAgreed: false }; return preloadedUserData; } } // 获取预加载的用户数据 function getPreloadedUserData() { return preloadedUserData || { token: null, profile: {}, privacyAgreed: false }; } export type Gender = 'male' | 'female' | ''; export type UserProfile = { name?: string; email?: string; gender?: Gender; birthDate?: string; weight?: string; height?: string; initialWeight?: string; // 初始体重 targetWeight?: string; // 目标体重 avatar?: string | null; dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等) dailyCaloriesGoal?: number; // 每日卡路里消耗目标 pilatesPurposes?: string[]; // 普拉提目的(多选) isVip?: boolean; freeUsageCount?: number; maxUsageCount?: number; }; export type WeightHistoryItem = { id: string; // 添加 id 字段用于删除和更新操作 weight: string; source: string; createdAt: string; }; export type ActivityHistoryItem = { date: string; level: number; }; export type UserState = { token: string | null; profile: UserProfile; loading: boolean; error: string | null; privacyAgreed: boolean; weightHistory: WeightHistoryItem[]; activityHistory: ActivityHistoryItem[]; }; export const DEFAULT_MEMBER_NAME = '小海豹'; const getInitialState = (): UserState => { const preloaded = getPreloadedUserData(); return { token: preloaded.token, profile: { name: DEFAULT_MEMBER_NAME, isVip: false, freeUsageCount: 3, maxUsageCount: 5, ...preloaded.profile, // 合并预加载的用户资料 }, loading: false, error: null, privacyAgreed: preloaded.privacyAgreed, weightHistory: [], activityHistory: [], }; }; const initialState: UserState = getInitialState(); export type LoginPayload = Record & { // 可扩展:用户名密码、Apple 身份、短信验证码等 username?: string; password?: string; appleIdentityToken?: string; }; export const login = createAsyncThunk( 'user/login', async (payload: LoginPayload, { rejectWithValue }) => { try { let data: any = null; // 若为 Apple 登录,走新的后端接口 /api/users/auth/apple/login if (payload.appleIdentityToken) { const appleToken = payload.appleIdentityToken; // 智能尝试不同 DTO 键名(identityToken / idToken)以提升兼容性 const candidates = [ { identityToken: appleToken }, { idToken: appleToken }, ]; let lastError: any = null; for (const body of candidates) { try { data = await api.post('/api/users/auth/apple/login', body); lastError = null; break; } catch (err: any) { lastError = err; // 若为 4xx 尝试下一个映射;其他错误直接抛出 const status = (err as any)?.status; if (status && status >= 500) throw err; } } if (!data && lastError) throw lastError; } else { // 其他登录(如游客/用户名密码)保持原有 '/api/login' data = await api.post('/api/login', payload); } // 兼容多种返回结构 const token = (data as any).token ?? (data as any).accessToken ?? (data as any).access_token ?? (data as any).jwt ?? (data as any)?.profile?.token ?? (data as any)?.user?.token ?? null; const profile: UserProfile | null = (data as any).profile ?? (data as any).user ?? (data as any).account ?? (data as any); if (!token) throw new Error('登录响应缺少 token'); await AsyncStorage.setItem(STORAGE_KEYS.authToken, token); await AsyncStorage.setItem(STORAGE_KEYS.userProfile, JSON.stringify(profile ?? {})); await setAuthToken(token); return { token, profile } as { token: string; profile: UserProfile }; } catch (err: any) { const message = err?.message ?? '登录失败'; return rejectWithValue(message); } } ); // 同步重新hydrate用户数据,避免异步状态更新 export const rehydrateUserSync = createAsyncThunk('user/rehydrateSync', async () => { // 立即从预加载的数据获取,如果没有则异步获取 if (preloadedUserData) { return preloadedUserData; } // 如果预加载的数据不存在,则执行正常的异步加载 return await preloadUserData(); }); export const rehydrateUser = createAsyncThunk('user/rehydrate', async () => { const [profileStr, privacyAgreedStr, token] = await Promise.all([ AsyncStorage.getItem(STORAGE_KEYS.userProfile), AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed), AsyncStorage.getItem(STORAGE_KEYS.authToken), ]); let profile: UserProfile = {}; if (profileStr) { try { profile = JSON.parse(profileStr) as UserProfile; } catch { profile = {}; } } const privacyAgreed = privacyAgreedStr === 'true'; // 如果有 token,需要设置到 API 客户端 if (token) { await setAuthToken(token); } return { profile, privacyAgreed, token } as { profile: UserProfile; privacyAgreed: boolean; token: string | null }; }); export const setPrivacyAgreed = createAsyncThunk('user/setPrivacyAgreed', async () => { await AsyncStorage.setItem(STORAGE_KEYS.privacyAgreed, 'true'); return true; }); export const logout = createAsyncThunk('user/logout', async () => { await Promise.all([ AsyncStorage.removeItem(STORAGE_KEYS.authToken), AsyncStorage.removeItem(STORAGE_KEYS.userProfile), AsyncStorage.removeItem(STORAGE_KEYS.privacyAgreed), ]); await setAuthToken(null); return true; }); // 拉取最新用户信息 export const fetchMyProfile = createAsyncThunk('user/fetchMyProfile', async (_, { rejectWithValue }) => { try { // 固定使用后端文档的接口:/api/users/info const data: any = await api.get('/api/users/info'); console.log('fetchMyProfile', data); const profile: UserProfile = (data as any).profile ?? (data as any).user ?? (data as any).account ?? (data as any); await AsyncStorage.setItem(STORAGE_KEYS.userProfile, JSON.stringify(profile ?? {})); return profile; } catch (err: any) { const message = err?.message ?? '获取用户信息失败'; return rejectWithValue(message); } }); // 获取用户体重历史记录 export const fetchWeightHistory = createAsyncThunk('user/fetchWeightHistory', async (_, { rejectWithValue }) => { try { const data: WeightHistoryItem[] = await api.get('/api/users/weight-history'); return data; } catch (err: any) { return rejectWithValue(err?.message ?? '获取用户体重历史记录失败'); } }); // 获取用户活动历史记录 export const fetchActivityHistory = createAsyncThunk('user/fetchActivityHistory', async (_, { rejectWithValue }) => { try { const data: ActivityHistoryItem[] = await api.get('/api/users/activity-history'); return data; } catch (err: any) { return rejectWithValue(err?.message ?? '获取用户活动历史记录失败'); } }); // 更新用户资料(包括体重) export const updateUserProfile = createAsyncThunk( 'user/updateProfile', async (updateDto: UpdateUserDto, { rejectWithValue }) => { try { const data = await updateUser(updateDto); console.log('updateUserProfile', data); return data; } catch (err: any) { return rejectWithValue(err?.message ?? '更新用户资料失败'); } } ); // 删除体重记录 export const deleteWeightRecord = createAsyncThunk( 'user/deleteWeightRecord', async (recordId: string, { rejectWithValue }) => { try { await api.delete(`/api/users/weight-records/${recordId}`); return recordId; } catch (err: any) { return rejectWithValue(err?.message ?? '删除体重记录失败'); } } ); // 更新体重记录 export const updateWeightRecord = createAsyncThunk( 'user/updateWeightRecord', async ({ id, weight }: { id: string; weight: number }, { rejectWithValue }) => { try { const data = await api.put(`/api/users/weight-records/${id}`, { weight }); return data; } catch (err: any) { return rejectWithValue(err?.message ?? '更新体重记录失败'); } } ); const userSlice = createSlice({ name: 'user', initialState, reducers: { updateProfile(state, action: PayloadAction>) { state.profile = { ...(state.profile ?? {}), ...action.payload }; }, setDailyStepsGoal(state, action: PayloadAction) { state.profile = { ...(state.profile ?? {}), dailyStepsGoal: action.payload }; }, setDailyCaloriesGoal(state, action: PayloadAction) { state.profile = { ...(state.profile ?? {}), dailyCaloriesGoal: action.payload }; }, setPilatesPurposes(state, action: PayloadAction) { state.profile = { ...(state.profile ?? {}), pilatesPurposes: action.payload }; }, }, extraReducers: (builder) => { builder .addCase(login.pending, (state) => { state.loading = true; state.error = null; }) .addCase(login.fulfilled, (state, action) => { state.loading = false; state.token = action.payload.token; state.profile = action.payload.profile; if (!state.profile?.name || !state.profile.name.trim()) { state.profile.name = DEFAULT_MEMBER_NAME; } }) .addCase(login.rejected, (state, action) => { state.loading = false; state.error = (action.payload as string) ?? '登录失败'; }) .addCase(rehydrateUser.fulfilled, (state, action) => { state.profile = action.payload.profile; state.privacyAgreed = action.payload.privacyAgreed; state.token = action.payload.token; if (!state.profile?.name || !state.profile.name.trim()) { state.profile.name = DEFAULT_MEMBER_NAME; } }) .addCase(rehydrateUserSync.fulfilled, (state, action) => { state.profile = action.payload.profile; state.privacyAgreed = action.payload.privacyAgreed; state.token = action.payload.token; if (!state.profile?.name || !state.profile.name.trim()) { state.profile.name = DEFAULT_MEMBER_NAME; } }) .addCase(fetchMyProfile.fulfilled, (state, action) => { state.profile = action.payload || {}; if (!state.profile?.name || !state.profile.name.trim()) { state.profile.name = DEFAULT_MEMBER_NAME; } }) .addCase(fetchMyProfile.rejected, (state, action) => { // 不覆盖现有资料,仅记录错误 state.error = (action.payload as string) ?? '获取用户信息失败'; }) .addCase(logout.fulfilled, (state) => { state.token = null; state.profile = {}; state.privacyAgreed = false; }) .addCase(setPrivacyAgreed.fulfilled, (state) => { state.privacyAgreed = true; }) .addCase(fetchWeightHistory.fulfilled, (state, action) => { state.weightHistory = action.payload; }) .addCase(fetchWeightHistory.rejected, (state, action) => { state.error = (action.payload as string) ?? '获取用户体重历史记录失败'; }) .addCase(fetchActivityHistory.fulfilled, (state, action) => { state.activityHistory = action.payload; }) .addCase(fetchActivityHistory.rejected, (state, action) => { state.error = (action.payload as string) ?? '获取用户活动历史记录失败'; }) .addCase(updateUserProfile.fulfilled, (state, action) => { state.profile = { ...state.profile, ...action.payload }; }) .addCase(updateUserProfile.rejected, (state, action) => { state.error = (action.payload as string) ?? '更新用户资料失败'; }) .addCase(deleteWeightRecord.fulfilled, (state, action) => { state.weightHistory = state.weightHistory.filter(record => record.id !== action.payload && record.createdAt !== action.payload ); }) .addCase(deleteWeightRecord.rejected, (state, action) => { state.error = (action.payload as string) ?? '删除体重记录失败'; }) .addCase(updateWeightRecord.fulfilled, (state, action) => { const index = state.weightHistory.findIndex(record => record.id === action.payload.id || record.createdAt === action.payload.id ); if (index !== -1) { state.weightHistory[index] = { ...state.weightHistory[index], ...action.payload }; } }) .addCase(updateWeightRecord.rejected, (state, action) => { state.error = (action.payload as string) ?? '更新体重记录失败'; }); }, }); export const { updateProfile, setDailyStepsGoal, setDailyCaloriesGoal, setPilatesPurposes } = userSlice.actions; // Selectors export const selectUserProfile = (state: { user: UserState }) => state.user.profile; // 计算用户年龄的 selector export const selectUserAge = createSelector( [selectUserProfile], (profile) => { if (!profile?.birthDate) return null; const birthDate = dayjs(profile.birthDate); const today = dayjs(); // 计算精确年龄(考虑月份和日期) let age = today.year() - birthDate.year(); // 如果今年的生日还没到,年龄减1 if (today.month() < birthDate.month() || (today.month() === birthDate.month() && today.date() < birthDate.date())) { age--; } return age; } ); export default userSlice.reducer;