- Replaced SSEClient with WSClient for handling real-time messaging. - Updated ProcessMessageUseCase to initialize WebSocket listeners. - Refactored messageService to prioritize WebSocket for sending messages, with fallback to HTTP. - Removed sseService and adjusted imports across the application to utilize wsService. - Enhanced message handling and connection management for improved performance and reliability.
310 lines
8.5 KiB
TypeScript
310 lines
8.5 KiB
TypeScript
/**
|
||
* 认证状态管理
|
||
*
|
||
* 链路设计:
|
||
* 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';
|
||
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../services';
|
||
import { wsService } from '../services/wsService';
|
||
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 '登录失败,请稍后重试';
|
||
}
|
||
|
||
// ── 启动 SSE 实时服务 ──
|
||
async function startRealtime(): Promise<void> {
|
||
try {
|
||
await wsService.start();
|
||
} catch (error) {
|
||
console.error('[AuthStore] 启动 SSE 服务失败:', error);
|
||
}
|
||
}
|
||
|
||
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,
|
||
});
|
||
|
||
// 5. 启动 SSE
|
||
await startRealtime();
|
||
|
||
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,
|
||
});
|
||
|
||
await startRealtime();
|
||
|
||
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();
|
||
// 2. 停止 SSE
|
||
wsService.stop();
|
||
// 3. 清除 DB 中的用户缓存(DB 此时一定已初始化)
|
||
await clearCurrentUserCache().catch(() => {});
|
||
// 4. 关闭数据库连接
|
||
await closeDatabase();
|
||
// 5. 清除持久化的 userId
|
||
await clearUserId();
|
||
} 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,
|
||
});
|
||
|
||
// 6. 启动 SSE
|
||
await startRealtime();
|
||
} 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);
|