2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 认证状态管理
|
|
|
|
|
|
*
|
|
|
|
|
|
* 链路设计:
|
|
|
|
|
|
* authService = 纯 HTTP 层(Token 管理),不碰 SQLite
|
|
|
|
|
|
* authStore = 编排层,负责:API → DB初始化 → 写缓存 → 更新状态
|
|
|
|
|
|
*
|
|
|
|
|
|
* 冷启动问题解法:
|
|
|
|
|
|
* 将 userId 持久化到 AsyncStorage,这样 DB 可在 API 调用前提前初始化,
|
|
|
|
|
|
* 彻底打破"需要用户ID才能初始化DB,但用户ID来自API"的循环依赖。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
|
|
import { create } from 'zustand';
|
|
|
|
|
|
import { User } from '../types';
|
2026-03-10 12:58:23 +08:00
|
|
|
|
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../services';
|
2026-03-26 22:04:46 +08:00
|
|
|
|
import { wsService } from '../services/wsService';
|
2026-03-27 17:12:19 +08:00
|
|
|
|
import { callStore } from './callStore';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
initDatabase,
|
|
|
|
|
|
closeDatabase,
|
|
|
|
|
|
saveUserCache,
|
|
|
|
|
|
saveCurrentUserCache,
|
|
|
|
|
|
clearCurrentUserCache,
|
|
|
|
|
|
} from '../services/database';
|
|
|
|
|
|
|
|
|
|
|
|
// AsyncStorage 中保存已登录用户 ID 的 key
|
|
|
|
|
|
const USER_ID_KEY = 'auth_user_id';
|
|
|
|
|
|
|
|
|
|
|
|
// ── userId 持久化辅助函数 ──
|
|
|
|
|
|
async function saveUserId(userId: string): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await AsyncStorage.setItem(USER_ID_KEY, userId);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[AuthStore] 保存 userId 失败:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadUserId(): Promise<string | null> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await AsyncStorage.getItem(USER_ID_KEY);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function clearUserId(): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await AsyncStorage.removeItem(USER_ID_KEY);
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 写用户缓存(DB 已就绪后调用)──
|
|
|
|
|
|
async function cacheUser(user: User): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await saveUserCache(user);
|
|
|
|
|
|
await saveCurrentUserCache(user);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('[AuthStore] 写用户缓存失败:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 将 API 错误转换为用户可读的中文提示 ──
|
|
|
|
|
|
function resolveLoginError(error: any): string {
|
|
|
|
|
|
const code: number = error?.code ?? 0;
|
|
|
|
|
|
const msg: string = error?.message ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
// 网络层错误(无法连接服务器)
|
|
|
|
|
|
if (
|
|
|
|
|
|
msg.includes('Network request failed') ||
|
|
|
|
|
|
msg.includes('网络请求失败') ||
|
|
|
|
|
|
msg.includes('Failed to fetch') ||
|
|
|
|
|
|
code === 500
|
|
|
|
|
|
) {
|
|
|
|
|
|
return '无法连接到服务器,请检查网络连接后重试';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 账号或密码错误
|
|
|
|
|
|
if (code === 401 || msg.includes('密码') || msg.includes('用户名') || msg.includes('incorrect')) {
|
|
|
|
|
|
return '用户名或密码错误,请重新输入';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 账号被禁用/封禁
|
|
|
|
|
|
if (code === 403 || msg.includes('禁止') || msg.includes('封禁') || msg.includes('forbidden')) {
|
|
|
|
|
|
return '账号已被禁用,请联系管理员';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 服务端明确返回了 message,直接用
|
|
|
|
|
|
if (msg && msg !== '登录失败') {
|
|
|
|
|
|
return msg;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return '登录失败,请稍后重试';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// ── 启动 SSE 实时服务 ──
|
|
|
|
|
|
async function startRealtime(): Promise<void> {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
try {
|
2026-03-26 22:04:46 +08:00
|
|
|
|
await wsService.start();
|
2026-03-27 17:12:19 +08:00
|
|
|
|
callStore.getState().initCall();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} catch (error) {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
console.error('[AuthStore] 启动 SSE 服务失败:', error);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface AuthState {
|
|
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
|
|
currentUser: User | null;
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
error: string | null;
|
|
|
|
|
|
|
|
|
|
|
|
login: (credentials: LoginRequest) => Promise<boolean>;
|
|
|
|
|
|
register: (data: RegisterRequest) => Promise<boolean>;
|
|
|
|
|
|
logout: () => Promise<void>;
|
|
|
|
|
|
updateUser: (user: Partial<User>) => void;
|
|
|
|
|
|
setLoading: (loading: boolean) => void;
|
|
|
|
|
|
setError: (error: string | null) => void;
|
|
|
|
|
|
fetchCurrentUser: () => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const useAuthStore = create<AuthState>((set) => ({
|
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
|
currentUser: null,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 登录 ──
|
|
|
|
|
|
// 流程:API → 保存Token → 初始化DB → 持久化userId → 写缓存 → 设置状态
|
|
|
|
|
|
login: async (credentials: LoginRequest) => {
|
|
|
|
|
|
set({ isLoading: true, error: null });
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. API 登录,authService 只负责 HTTP + Token
|
|
|
|
|
|
const response = await authService.login(credentials);
|
|
|
|
|
|
const user = response.user;
|
|
|
|
|
|
|
|
|
|
|
|
if (!user?.id) {
|
|
|
|
|
|
throw new Error('登录响应中缺少用户信息');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const userId = String(user.id);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 初始化用户专属数据库
|
|
|
|
|
|
await initDatabase(userId);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 持久化 userId(供下次冷启动提前初始化 DB 用)
|
|
|
|
|
|
await saveUserId(userId);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 写用户缓存(DB 已就绪)
|
|
|
|
|
|
await cacheUser(user);
|
|
|
|
|
|
|
|
|
|
|
|
set({
|
|
|
|
|
|
isAuthenticated: true,
|
|
|
|
|
|
currentUser: user,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 5. 启动 SSE
|
|
|
|
|
|
await startRealtime();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
set({
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: resolveLoginError(error),
|
|
|
|
|
|
});
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 注册 ──
|
|
|
|
|
|
register: async (data: RegisterRequest) => {
|
|
|
|
|
|
set({ isLoading: true, error: null });
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await authService.register(data);
|
|
|
|
|
|
const user = response.user;
|
|
|
|
|
|
|
|
|
|
|
|
if (!user?.id) {
|
|
|
|
|
|
throw new Error('注册响应中缺少用户信息');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const userId = String(user.id);
|
|
|
|
|
|
|
|
|
|
|
|
await initDatabase(userId);
|
|
|
|
|
|
await saveUserId(userId);
|
|
|
|
|
|
await cacheUser(user);
|
|
|
|
|
|
|
|
|
|
|
|
set({
|
|
|
|
|
|
isAuthenticated: true,
|
|
|
|
|
|
currentUser: user,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
await startRealtime();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
set({
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: resolveAuthApiError(error, '注册失败,请稍后重试'),
|
|
|
|
|
|
});
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 登出 ──
|
|
|
|
|
|
logout: async () => {
|
|
|
|
|
|
set({ isLoading: true });
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 通知服务端(Token 清理在 authService 内部完成)
|
|
|
|
|
|
await authService.logout();
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 2. 停止 SSE
|
2026-03-26 22:04:46 +08:00
|
|
|
|
wsService.stop();
|
2026-04-01 02:17:36 +08:00
|
|
|
|
// 3. 重置通话状态(清理所有事件订阅和定时器)
|
|
|
|
|
|
callStore.getState().reset();
|
|
|
|
|
|
// 4. 清除 DB 中的用户缓存(DB 此时一定已初始化)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
await clearCurrentUserCache().catch(() => {});
|
2026-04-01 02:17:36 +08:00
|
|
|
|
// 5. 关闭数据库连接
|
2026-03-09 21:29:03 +08:00
|
|
|
|
await closeDatabase();
|
2026-04-01 02:17:36 +08:00
|
|
|
|
// 6. 清除持久化的 userId
|
2026-03-09 21:29:03 +08:00
|
|
|
|
await clearUserId();
|
2026-04-01 02:17:36 +08:00
|
|
|
|
// 7. 清除 userManager 的内存缓存
|
|
|
|
|
|
const { userManager } = await import('./userManager');
|
|
|
|
|
|
userManager.invalidateUsers();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[AuthStore] 登出失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
set({
|
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
|
currentUser: null,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 冷启动校验 ──
|
|
|
|
|
|
// 流程:读取已保存的userId → 提前初始化DB → 纯API验证Token → 更新缓存 → 设置状态
|
|
|
|
|
|
fetchCurrentUser: async () => {
|
|
|
|
|
|
set({ isLoading: true });
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 从 AsyncStorage 读取上次登录保存的 userId
|
|
|
|
|
|
// 这是关键:让 DB 在 API 调用前就能初始化,打破循环依赖
|
|
|
|
|
|
const savedUserId = await loadUserId();
|
|
|
|
|
|
|
|
|
|
|
|
if (savedUserId) {
|
|
|
|
|
|
// 2. 用已知的 userId 提前初始化 DB
|
|
|
|
|
|
try {
|
|
|
|
|
|
await initDatabase(savedUserId);
|
|
|
|
|
|
} catch (dbErr) {
|
|
|
|
|
|
console.warn('[AuthStore] DB 预初始化失败:', dbErr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 纯 API 调用验证 Token 有效性(fetchCurrentUserFromAPI 完全不碰 DB)
|
|
|
|
|
|
const user = await authService.fetchCurrentUserFromAPI();
|
|
|
|
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
const userId = String(user.id);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 若 userId 发生变化(极少数情况),重新初始化 DB 并更新持久化
|
|
|
|
|
|
if (!savedUserId || savedUserId !== userId) {
|
|
|
|
|
|
await initDatabase(userId);
|
|
|
|
|
|
await saveUserId(userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. DB 已就绪,更新用户缓存
|
|
|
|
|
|
await cacheUser(user);
|
|
|
|
|
|
|
|
|
|
|
|
set({
|
|
|
|
|
|
isAuthenticated: true,
|
|
|
|
|
|
currentUser: user,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 6. 启动 SSE
|
|
|
|
|
|
await startRealtime();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// Token 已失效或不存在
|
|
|
|
|
|
await clearUserId();
|
|
|
|
|
|
set({
|
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
|
currentUser: null,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[AuthStore] fetchCurrentUser 异常:', error);
|
|
|
|
|
|
set({
|
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
|
currentUser: null,
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
error: '获取用户信息失败',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
updateUser: (userData: Partial<User>) =>
|
|
|
|
|
|
set((state) => ({
|
|
|
|
|
|
currentUser: state.currentUser ? { ...state.currentUser, ...userData } : null,
|
|
|
|
|
|
})),
|
|
|
|
|
|
|
|
|
|
|
|
setLoading: (loading: boolean) => set({ isLoading: loading }),
|
|
|
|
|
|
|
|
|
|
|
|
setError: (error: string | null) => set({ error }),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 导出 selector hooks 以优化性能
|
|
|
|
|
|
export const useCurrentUser = () => useAuthStore((state) => state.currentUser);
|
|
|
|
|
|
export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated);
|
|
|
|
|
|
export const useAuthLoading = () => useAuthStore((state) => state.isLoading);
|