- navigateToLogin 添加去重 + reLaunch 失败降级 redirectTo - request.ts safeReLaunch 添加目标页检测 + 失败降级 - 退出登录 reLaunch 失败降级 redirectTo - DoctorTabBar / 首页医生端跳转 reLaunch 失败降级 - 网络恢复时正确清理 toast 状态和定时器
302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
import { create } from 'zustand';
|
||
import Taro from '@tarojs/taro';
|
||
import * as authApi from '@/services/auth';
|
||
import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage';
|
||
import { clearRequestCache, invalidateHeadersCache, markLoggingOut, clearLoggingOut, setCachedPatientId } from '@/services/request';
|
||
|
||
// secureGet 已内置明文键 fallback,无需再手动 fallback
|
||
function storageGet(key: string): string {
|
||
return secureGet(key);
|
||
}
|
||
import { resetAllStores } from './index';
|
||
import { resetAnalyticsDisabled } from '@/services/analytics';
|
||
|
||
// --- 内存缓存,避免每次 Tab 切换重复 Storage IPC + JSON.parse ---
|
||
let cachedUserJson = '';
|
||
let cachedUserObj: AuthState['user'] = null;
|
||
let cachedRolesJson = '';
|
||
let cachedRolesObj: string[] = [];
|
||
let cachedPatientJson = '';
|
||
let cachedPatientObj: authApi.PatientInfo | null = null;
|
||
|
||
interface AuthState {
|
||
user: { id: string; username: string; display_name?: string; phone?: string; tenant_id?: string } | null;
|
||
roles: string[];
|
||
currentPatient: authApi.PatientInfo | null;
|
||
patients: authApi.PatientInfo[];
|
||
loading: boolean;
|
||
|
||
login: (code: string) => Promise<boolean>;
|
||
credentialLogin: (username: string, password: string) => Promise<boolean>;
|
||
bindPhone: (encryptedData: string, iv: string) => Promise<boolean>;
|
||
setCurrentPatient: (patient: authApi.PatientInfo) => void;
|
||
loadPatients: () => Promise<void>;
|
||
logout: () => void;
|
||
restore: () => void;
|
||
isMedicalStaff: () => boolean;
|
||
isDoctor: () => boolean;
|
||
isNurse: () => boolean;
|
||
isHealthManager: () => boolean;
|
||
hasRole: (code: string) => boolean;
|
||
hasPatientProfile: () => boolean;
|
||
}
|
||
|
||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||
user: null,
|
||
roles: [],
|
||
currentPatient: null,
|
||
patients: [],
|
||
loading: false,
|
||
|
||
isMedicalStaff: () => {
|
||
const { roles } = get();
|
||
return roles.some((r) => r === 'doctor' || r === 'nurse' || r === 'admin' || r === 'health_manager');
|
||
},
|
||
|
||
isDoctor: () => {
|
||
const { roles } = get();
|
||
return roles.some((r) => r === 'doctor' || r === 'admin');
|
||
},
|
||
|
||
isNurse: () => {
|
||
const { roles } = get();
|
||
return roles.some((r) => r === 'nurse' || r === 'admin');
|
||
},
|
||
|
||
isHealthManager: () => {
|
||
const { roles } = get();
|
||
return roles.some((r) => r === 'health_manager' || r === 'admin');
|
||
},
|
||
|
||
hasRole: (code: string) => {
|
||
const { roles } = get();
|
||
return roles.some((r) => r === code || r === 'admin');
|
||
},
|
||
|
||
hasPatientProfile: () => {
|
||
return !!get().currentPatient;
|
||
},
|
||
|
||
restore: () => {
|
||
// 利用内存缓存避免重复 Storage IPC + JSON.parse
|
||
try {
|
||
const userData = storageGet('user_data');
|
||
if (userData !== cachedUserJson) {
|
||
cachedUserJson = userData;
|
||
cachedUserObj = userData ? JSON.parse(userData) : null;
|
||
}
|
||
const rolesData = storageGet('user_roles');
|
||
if (rolesData !== cachedRolesJson) {
|
||
cachedRolesJson = rolesData;
|
||
cachedRolesObj = rolesData ? JSON.parse(rolesData) : [];
|
||
}
|
||
} catch { /* secure storage 不可用时保持默认值 */ }
|
||
try {
|
||
const patientStr = storageGet('current_patient');
|
||
let patientRaw = patientStr ? JSON.parse(patientStr) : null;
|
||
const patientJson = patientRaw ? JSON.stringify(patientRaw) : '';
|
||
if (patientJson !== cachedPatientJson) {
|
||
cachedPatientJson = patientJson;
|
||
cachedPatientObj = patientRaw || null;
|
||
}
|
||
} catch { cachedPatientObj = null; }
|
||
|
||
const user = cachedUserObj;
|
||
const roles = cachedRolesObj;
|
||
const currentPatient = cachedPatientObj;
|
||
|
||
// 同步 cachedPatientId 到 request.ts
|
||
if (currentPatient?.id) {
|
||
setCachedPatientId(currentPatient.id);
|
||
}
|
||
|
||
// 跳过未变更的 set()
|
||
const cur = get();
|
||
const userChanged = cur.user?.id !== user?.id;
|
||
const rolesChanged = cur.roles.length !== roles.length || cur.roles.some((r, i) => r !== roles[i]);
|
||
const patientChanged = cur.currentPatient?.id !== currentPatient?.id;
|
||
if (!userChanged && !rolesChanged && !patientChanged) return;
|
||
|
||
// 状态有变化时清理请求缓存,避免返回过期数据
|
||
clearRequestCache();
|
||
set({ user, roles, currentPatient });
|
||
},
|
||
|
||
login: async (code: string) => {
|
||
if (get().loading) return false;
|
||
set({ loading: true });
|
||
try {
|
||
const resp = await authApi.wechatLogin(code);
|
||
if (resp.bound && resp.token) {
|
||
const { access_token, refresh_token, user } = resp.token;
|
||
const userObj = user as Record<string, unknown>;
|
||
const roles = Array.isArray(userObj?.roles)
|
||
? (userObj.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
|
||
: [];
|
||
secureSet('access_token', access_token);
|
||
secureSet('refresh_token', refresh_token);
|
||
if (resp.token.expires_in) {
|
||
secureSet('token_expires_at', String(Date.now() + resp.token.expires_in * 1000));
|
||
}
|
||
secureSet('user_data', JSON.stringify(user));
|
||
secureSet('user_roles', JSON.stringify(roles));
|
||
secureSet('tenant_id', user.tenant_id || '');
|
||
set({ user, roles, loading: false });
|
||
clearLoggingOut();
|
||
invalidateHeadersCache();
|
||
resetAnalyticsDisabled();
|
||
get().loadPatients();
|
||
return true;
|
||
}
|
||
secureSet('wechat_openid', resp.openid);
|
||
set({ loading: false });
|
||
return false;
|
||
} catch (err) {
|
||
console.warn('[auth] 微信登录失败:', err);
|
||
set({ loading: false });
|
||
return false;
|
||
}
|
||
},
|
||
|
||
credentialLogin: async (username: string, password: string) => {
|
||
if (get().loading) return false;
|
||
set({ loading: true });
|
||
try {
|
||
const tenantId = Taro.getStorageSync('tenant_id') || process.env.TARO_APP_DEFAULT_TENANT_ID || '';
|
||
const resp = await authApi.credentialLogin(username, password, tenantId);
|
||
const user = resp.user as Record<string, unknown>;
|
||
const roles = Array.isArray(user?.roles)
|
||
? (user.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
|
||
: [];
|
||
secureSet('access_token', resp.access_token);
|
||
secureSet('refresh_token', resp.refresh_token);
|
||
if (resp.expires_in) {
|
||
secureSet('token_expires_at', String(Date.now() + resp.expires_in * 1000));
|
||
}
|
||
secureSet('user_data', JSON.stringify(resp.user));
|
||
secureSet('user_roles', JSON.stringify(roles));
|
||
secureSet('tenant_id', resp.user?.tenant_id || tenantId);
|
||
set({ user: resp.user, roles, loading: false });
|
||
clearLoggingOut();
|
||
invalidateHeadersCache();
|
||
resetAnalyticsDisabled();
|
||
get().loadPatients();
|
||
return true;
|
||
} catch (err) {
|
||
console.warn('[auth] 账号密码登录失败:', err);
|
||
set({ loading: false });
|
||
return false;
|
||
}
|
||
},
|
||
|
||
bindPhone: async (encryptedData: string, iv: string) => {
|
||
if (get().loading) return false;
|
||
set({ loading: true });
|
||
try {
|
||
const openid = secureGet('wechat_openid') || '';
|
||
if (!openid) {
|
||
set({ loading: false });
|
||
throw new Error('登录态丢失,请返回重试');
|
||
}
|
||
const resp = await authApi.wechatBindPhone(openid, encryptedData, iv) as Record<string, unknown>;
|
||
const tokenData = resp as { access_token: string; refresh_token: string; expires_in?: number; user: AuthState['user'] };
|
||
const userObj = tokenData.user as Record<string, unknown>;
|
||
const roles = Array.isArray(userObj?.roles)
|
||
? (userObj.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
|
||
: [];
|
||
secureSet('access_token', tokenData.access_token);
|
||
secureSet('refresh_token', tokenData.refresh_token);
|
||
if (tokenData.expires_in) {
|
||
secureSet('token_expires_at', String(Date.now() + tokenData.expires_in * 1000));
|
||
}
|
||
secureSet('user_data', JSON.stringify(tokenData.user));
|
||
secureSet('user_roles', JSON.stringify(roles));
|
||
secureSet('tenant_id', tokenData.user?.tenant_id || '');
|
||
secureRemove('wechat_openid');
|
||
set({ user: tokenData.user, roles, loading: false });
|
||
clearLoggingOut();
|
||
invalidateHeadersCache();
|
||
resetAnalyticsDisabled();
|
||
get().loadPatients();
|
||
return true;
|
||
} catch (err: unknown) {
|
||
secureRemove('wechat_openid');
|
||
set({ loading: false });
|
||
throw err;
|
||
}
|
||
},
|
||
|
||
setCurrentPatient: (patient) => {
|
||
const safePatient: authApi.PatientInfo = {
|
||
id: patient.id,
|
||
name: patient.name,
|
||
gender: patient.gender,
|
||
birth_date: patient.birth_date,
|
||
relation: patient.relation,
|
||
};
|
||
secureSet('current_patient_id', safePatient.id);
|
||
secureSet('current_patient', JSON.stringify(safePatient));
|
||
setCachedPatientId(safePatient.id);
|
||
clearRequestCache();
|
||
set({ currentPatient: safePatient });
|
||
},
|
||
|
||
loadPatients: async () => {
|
||
try {
|
||
const summaries = await authApi.getPatientSummaries();
|
||
const patients: authApi.PatientInfo[] = summaries.map((p) => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
gender: p.gender,
|
||
birth_date: p.birth_date,
|
||
relation: 'self',
|
||
}));
|
||
set({ patients });
|
||
if (patients.length > 0 && !get().currentPatient) {
|
||
get().setCurrentPatient(patients[0]);
|
||
}
|
||
} catch (err) {
|
||
console.warn('[auth] 患者列表加载失败:', err);
|
||
}
|
||
},
|
||
|
||
logout: () => {
|
||
markLoggingOut();
|
||
clearRequestCache();
|
||
setCachedPatientId('');
|
||
// 清理模块级缓存
|
||
cachedUserJson = '';
|
||
cachedUserObj = null;
|
||
cachedRolesJson = '';
|
||
cachedRolesObj = [];
|
||
cachedPatientJson = '';
|
||
cachedPatientObj = null;
|
||
secureRemove('access_token');
|
||
secureRemove('refresh_token');
|
||
secureRemove('token_expires_at');
|
||
secureRemove('user_data');
|
||
secureRemove('user_roles');
|
||
secureRemove('tenant_id');
|
||
secureRemove('wechat_openid');
|
||
secureRemove('current_patient');
|
||
secureRemove('current_patient_id');
|
||
// analytics_queue 使用明文存储(analytics.ts STORAGE_KEY = 'analytics_queue')
|
||
Taro.removeStorageSync('analytics_queue');
|
||
secureRemove('edit_patient');
|
||
secureRemove('ai_chat_history');
|
||
// 清理 BLE DataBuffer 缓存(key 格式:ble_buffer_{patientId}_{bucket})
|
||
const storageInfo = Taro.getStorageInfoSync();
|
||
storageInfo.keys.forEach((key) => {
|
||
if (key.startsWith('ble_buffer_') || key.startsWith('last_ble_sync')) {
|
||
Taro.removeStorageSync(key);
|
||
}
|
||
});
|
||
resetAllStores();
|
||
set({ user: null, roles: [], currentPatient: null, patients: [] });
|
||
Taro.reLaunch({ url: '/pages/index/index' }).catch((err) => {
|
||
console.warn('[auth] reLaunch after logout failed:', err);
|
||
Taro.redirectTo({ url: '/pages/index/index' }).catch(() => {});
|
||
});
|
||
},
|
||
}));
|