- 修复 stores/auth.ts 三种登录方式从错误路径提取 roles(resp.roles → resp.user.roles) - 首页添加医护人员自动跳转医生端(useDidShow + isMedicalStaff) - services/auth.ts credentialLogin 返回类型补全 roles 字段 - Web 前端 healthData.ts 字段对齐后端 DTO(indicators→items, content→overall_assessment) - Web 前端 medicationReminders.ts 字段对齐(time_slots→reminder_times) - 小程序 report.ts / reports 页面字段对齐后端(indicators→items, doctor_interpretation→doctor_notes) - 小程序 patient.ts / followup.ts / alert.ts 补全缺失字段 - 后端 stats_handler.rs 权限码修正(health.patient.list→health.dashboard.manage) - 新增 V1 E2E 测试报告和五专家组评审报告
260 lines
9.1 KiB
TypeScript
260 lines
9.1 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, markLoggingOut, clearLoggingOut, setCachedPatientId } from '@/services/request';
|
|
import { resetAllStores } from './index';
|
|
|
|
// --- 内存缓存,避免每次 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 BindPhoneResp {
|
|
access_token: string;
|
|
refresh_token: string;
|
|
expires_in?: number;
|
|
user: { id: string; username: string; display_name?: string; phone?: string; tenant_id?: string };
|
|
}
|
|
|
|
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 = secureGet('user_data') || Taro.getStorageSync('user_data') || '';
|
|
if (userData !== cachedUserJson) {
|
|
cachedUserJson = userData;
|
|
cachedUserObj = userData ? JSON.parse(userData) : null;
|
|
}
|
|
const rolesData = secureGet('user_roles') || Taro.getStorageSync('user_roles') || '';
|
|
if (rolesData !== cachedRolesJson) {
|
|
cachedRolesJson = rolesData;
|
|
cachedRolesObj = rolesData ? JSON.parse(rolesData) : [];
|
|
}
|
|
} catch { /* secure storage 不可用时保持默认值 */ }
|
|
try {
|
|
let patientRaw = Taro.getStorageSync('current_patient');
|
|
// 防御双重序列化:如果 Storage 写入了 JSON 字符串而非对象,尝试解析
|
|
if (typeof patientRaw === 'string') {
|
|
try { patientRaw = JSON.parse(patientRaw); } catch { patientRaw = 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;
|
|
|
|
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();
|
|
return true;
|
|
}
|
|
secureSet('wechat_openid', resp.openid);
|
|
set({ loading: false });
|
|
return false;
|
|
} catch {
|
|
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();
|
|
return true;
|
|
} catch {
|
|
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();
|
|
return true;
|
|
} catch (err: any) {
|
|
secureRemove('wechat_openid');
|
|
set({ loading: false });
|
|
throw err;
|
|
}
|
|
},
|
|
|
|
setCurrentPatient: (patient) => {
|
|
Taro.setStorageSync('current_patient_id', patient.id);
|
|
Taro.setStorageSync('current_patient', patient);
|
|
setCachedPatientId(patient.id);
|
|
clearRequestCache();
|
|
set({ currentPatient: patient });
|
|
},
|
|
|
|
loadPatients: async () => {
|
|
try {
|
|
const patients = await authApi.getPatients();
|
|
set({ patients });
|
|
if (patients.length > 0 && !get().currentPatient) {
|
|
get().setCurrentPatient(patients[0]);
|
|
}
|
|
} catch {
|
|
// 患者列表加载失败不阻塞流程
|
|
}
|
|
},
|
|
|
|
logout: () => {
|
|
markLoggingOut();
|
|
clearRequestCache();
|
|
setCachedPatientId('');
|
|
secureRemove('access_token');
|
|
secureRemove('refresh_token');
|
|
secureRemove('token_expires_at');
|
|
secureRemove('user_data');
|
|
secureRemove('user_roles');
|
|
secureRemove('tenant_id');
|
|
secureRemove('wechat_openid');
|
|
Taro.removeStorageSync('current_patient');
|
|
Taro.removeStorageSync('current_patient_id');
|
|
Taro.removeStorageSync('analytics_queue');
|
|
Taro.removeStorageSync('edit_patient');
|
|
resetAllStores();
|
|
set({ user: null, roles: [], currentPatient: null, patients: [] });
|
|
Taro.reLaunch({ url: '/pages/index/index' });
|
|
},
|
|
}));
|