Files
hms/apps/miniprogram/src/stores/auth.ts
iven 1a376a255d fix(mp): 导航/请求健壮性 — reLaunch 去重 + 失败降级
- navigateToLogin 添加去重 + reLaunch 失败降级 redirectTo
- request.ts safeReLaunch 添加目标页检测 + 失败降级
- 退出登录 reLaunch 失败降级 redirectTo
- DoctorTabBar / 首页医生端跳转 reLaunch 失败降级
- 网络恢复时正确清理 toast 状态和定时器
2026-05-25 13:45:12 +08:00

302 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(() => {});
});
},
}));