Files
hms/apps/miniprogram/src/services/health.ts
iven 8f353946e1 fix(mp): T40 UI 审计修复 — 28 项设计系统合规 + 安全加固 + 讨论记录
T40 UI 审计修复(60 页面全覆盖):
- 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码
- 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件)
- 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页)
- 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加)
- statusTag 移除硬编码布局值,改用 SCSS mixin 控制
- 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect)
- 移除 action-inbox 未使用 import

安全 P0 修复:
- JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化
- 速率限制增强:滑动窗口 + 暴力破解防护
- analytics handler 错误处理完善

文档:
- T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK)
- 5 份 DevTools/性能审计讨论记录
- wiki 症状导航 + 小程序章节更新
2026-05-14 23:12:54 +08:00

187 lines
6.4 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 Taro from '@tarojs/taro';
import { api } from './request';
export interface VitalSignInput {
indicator_type: string;
value: number;
measured_at?: string;
note?: string;
extra?: Record<string, number>;
}
export interface TodaySummary {
blood_pressure?: { systolic: number; diastolic: number; status: string; reference_range?: string };
heart_rate?: { value: number; status: string; reference_range?: string };
blood_sugar?: { value: number; status: string; reference_range?: string };
weight?: { value: number; status: string; reference_range?: string };
}
export async function getTodaySummary(patientId?: string) {
const params: Record<string, string> = {};
if (patientId) params.patient_id = patientId;
return api.get<TodaySummary>('/health/vital-signs/today', params);
}
/**
* 提交生命体征数据。
* 小程序使用简单的 indicator_type + value 模型,
* 后端 CreateVitalSignsReq 期望结构化字段systolic_bp_morning 等)。
* 此函数负责将指示器类型映射到后端结构化格式。
*/
export async function inputVitalSign(patientId: string, data: VitalSignInput) {
const today = new Date().toISOString().slice(0, 10);
const body: Record<string, unknown> = { record_date: today };
switch (data.indicator_type) {
case 'blood_pressure':
if (data.extra?.systolic) body.systolic_bp_morning = data.extra.systolic;
if (data.extra?.diastolic) body.diastolic_bp_morning = data.extra.diastolic;
break;
case 'blood_pressure_evening':
if (data.extra?.systolic) body.systolic_bp_evening = data.extra.systolic;
if (data.extra?.diastolic) body.diastolic_bp_evening = data.extra.diastolic;
break;
case 'heart_rate':
body.heart_rate = Math.round(data.value);
break;
case 'weight':
body.weight = data.value;
break;
case 'blood_sugar':
body.blood_sugar = data.value;
break;
case 'body_temperature':
body.body_temperature = data.value;
break;
case 'spo2':
body.spo2 = Math.round(data.value);
break;
case 'water_intake':
body.water_intake_ml = Math.round(data.value);
break;
case 'urine_output':
body.urine_output_ml = Math.round(data.value);
break;
default:
console.warn(`[inputVitalSign] 未知的 indicator_type: ${data.indicator_type}`);
break;
}
if (data.note) body.notes = data.note;
return api.post(`/health/patients/${patientId}/vital-signs`, body);
}
export async function getTrend(indicator: string, range: string) {
return api.get<{ indicator: string; data_points: { date: string; value: number }[] }>(
'/health/vital-signs/trend',
{ indicator, range },
);
}
// ---- Daily Monitoring (日常监测) ----
export interface DailyMonitoring {
id: string;
patient_id: string;
record_date: string;
morning_bp_systolic: number | null;
morning_bp_diastolic: number | null;
evening_bp_systolic: number | null;
evening_bp_diastolic: number | null;
weight: number | null;
blood_sugar: number | null;
fluid_intake: number | null;
urine_output: number | null;
notes: string | null;
created_at: string;
updated_at: string;
version: number;
}
export interface CreateDailyMonitoringReq {
patient_id: string;
record_date: string;
morning_bp_systolic?: number;
morning_bp_diastolic?: number;
evening_bp_systolic?: number;
evening_bp_diastolic?: number;
weight?: number;
blood_sugar?: number;
fluid_intake?: number;
urine_output?: number;
notes?: string;
}
export async function createDailyMonitoring(data: CreateDailyMonitoringReq) {
return api.post<DailyMonitoring>('/health/daily-monitoring', data);
}
export async function listDailyMonitoring(
patientId: string,
params?: { page?: number; page_size?: number },
) {
return api.get<{ data: DailyMonitoring[]; total: number }>(
`/health/patients/${patientId}/daily-monitoring`,
params,
);
}
// ---- Health Thresholds (健康阈值) ----
export interface HealthThreshold {
id: string;
indicator: string;
direction: string;
threshold_value: number;
level: string;
department: string | null;
age_min: number | null;
age_max: number | null;
is_active: boolean;
}
const THRESHOLD_CACHE_KEY = 'health_thresholds';
const THRESHOLD_TTL = 24 * 60 * 60 * 1000; // 24h
/** 从缓存或 API 获取健康阈值列表 */
export async function getHealthThresholds(): Promise<HealthThreshold[]> {
try {
const cached = Taro.getStorageSync(THRESHOLD_CACHE_KEY) as
| { data: HealthThreshold[]; ts: number }
| undefined;
if (cached && Date.now() - cached.ts < THRESHOLD_TTL) {
return cached.data;
}
} catch { /* cache miss */ }
try {
const data = await api.get<HealthThreshold[]>('/health/critical-value-thresholds/public');
Taro.setStorageSync(THRESHOLD_CACHE_KEY, { data, ts: Date.now() });
return data;
} catch {
return [];
}
}
/** 查找匹配的阈值,缓存未命中时返回 undefined */
export function findThreshold(
thresholds: HealthThreshold[],
indicator: string,
direction: string,
level = 'warning',
): HealthThreshold | undefined {
return thresholds.find(
(t) => t.indicator === indicator && t.direction === direction && t.level === level && t.is_active,
);
}
/** 内置默认阈值API 不可用时的降级方案) */
export const DEFAULT_THRESHOLDS: HealthThreshold[] = [
{ id: '_bp_sys_high', indicator: 'systolic_bp', direction: 'high', threshold_value: 140, level: 'warning', department: null, age_min: null, age_max: null, is_active: true },
{ id: '_bp_dia_high', indicator: 'diastolic_bp', direction: 'high', threshold_value: 90, level: 'warning', department: null, age_min: null, age_max: null, is_active: true },
{ id: '_hr_high', indicator: 'heart_rate', direction: 'high', threshold_value: 100, level: 'warning', department: null, age_min: null, age_max: null, is_active: true },
{ id: '_hr_low', indicator: 'heart_rate', direction: 'low', threshold_value: 60, level: 'warning', department: null, age_min: null, age_max: null, is_active: true },
{ id: '_bs_fasting_high', indicator: 'blood_sugar_fasting', direction: 'high', threshold_value: 6.1, level: 'warning', department: null, age_min: null, age_max: null, is_active: true },
{ id: '_bs_pp_high', indicator: 'blood_sugar_postprandial', direction: 'high', threshold_value: 7.8, level: 'warning', department: null, age_min: null, age_max: null, is_active: true },
];