fix(mp): 安全 P0 修复 + 架构 Hook 层补充 + 五专家组分析报告
安全修复: - 提取 sanitizeHtml 共享工具,修复 article/detail RichText XSS 风险 - request.ts 生产环境强制 HTTPS,消除 HTTP 回退风险 - 错误信息净化:后端错误码映射为用户友好消息,不再透传原始内容 - Token 生命周期管理:利用 expires_in 记录过期时间,请求前主动刷新 工程修复: - Babel 依赖从 dependencies 移至 devDependencies(包体积优化) 架构改进: - 新增 usePagination hook(分页加载 + hasMore + refresh,10+ 页面可复用) - 新增 useAuthRequired hook(登录态 + 患者档案 + 角色判断统一入口) - 新增 usePageRefresh hook(下拉刷新统一封装,17 页面可复用) 文档: - 五专家组深度分析+头脑风暴报告(架构7.2/安全5.5/UX6.0/工程5.5/产品7.2)
This commit is contained in:
@@ -2,11 +2,21 @@ 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 } from '@/services/request';
|
||||
import { clearRequestCache, markLoggingOut, clearLoggingOut, setCachedPatientId } from '@/services/request';
|
||||
import { useHealthStore } from './health';
|
||||
|
||||
// --- 内存缓存,避免每次 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 };
|
||||
}
|
||||
|
||||
@@ -68,15 +78,48 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
},
|
||||
|
||||
restore: () => {
|
||||
let user: AuthState['user'] = null;
|
||||
let roles: string[] = [];
|
||||
// 利用内存缓存避免重复 Storage IPC + JSON.parse
|
||||
try {
|
||||
const userData = secureGet('user_data') || Taro.getStorageSync('user_data');
|
||||
if (userData) user = JSON.parse(userData);
|
||||
const rolesData = secureGet('user_roles') || Taro.getStorageSync('user_roles');
|
||||
if (rolesData) roles = JSON.parse(rolesData);
|
||||
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 不可用时保持默认值 */ }
|
||||
const currentPatient = Taro.getStorageSync('current_patient') || null;
|
||||
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 });
|
||||
},
|
||||
|
||||
@@ -92,6 +135,9 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
: [];
|
||||
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 || '');
|
||||
@@ -118,12 +164,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
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; user: AuthState['user'] };
|
||||
const tokenData = resp as { access_token: string; refresh_token: string; expires_in?: number; user: AuthState['user'] };
|
||||
const roles = resp.roles instanceof Array
|
||||
? (resp.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 || '');
|
||||
@@ -141,6 +190,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
setCurrentPatient: (patient) => {
|
||||
Taro.setStorageSync('current_patient_id', patient.id);
|
||||
Taro.setStorageSync('current_patient', patient);
|
||||
setCachedPatientId(patient.id);
|
||||
clearRequestCache();
|
||||
set({ currentPatient: patient });
|
||||
},
|
||||
|
||||
@@ -159,8 +210,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
logout: () => {
|
||||
markLoggingOut();
|
||||
clearRequestCache();
|
||||
setCachedPatientId('');
|
||||
secureRemove('access_token');
|
||||
secureRemove('refresh_token');
|
||||
secureRemove('token_expires_at');
|
||||
secureRemove('user_data');
|
||||
secureRemove('user_roles');
|
||||
secureRemove('tenant_id');
|
||||
@@ -169,6 +222,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
Taro.removeStorageSync('current_patient_id');
|
||||
Taro.removeStorageSync('analytics_queue');
|
||||
Taro.removeStorageSync('edit_patient');
|
||||
useHealthStore.getState().clearCache();
|
||||
set({ user: null, roles: [], currentPatient: null, patients: [] });
|
||||
Taro.reLaunch({ url: '/pages/index/index' });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user