import { validateBloodPressurePair, validateMetricValue } from './health-observation-validate.mjs'; import { assertSymptomRecord } from './health-symptoms.mjs'; import { observationDedupKey } from './health-observation-validate.mjs'; import { buildHealthTimeline } from './health-timeline.mjs'; const METRIC_SET_TO_TYPE = Object.freeze({ heart_rate: 'hr', weight: 'weight', spo2: 'spo2', temperature: 'temperature', sleep: 'sleep_minutes', }); export function createHealthObservationService({ store } = {}) { if (!store) throw new Error('createHealthObservationService requires store'); async function insertUnique(userId, observation) { const dedup = observationDedupKey({ metricType: observation.metricType, observedAt: observation.observedAt, sourceRef: observation.sourceRef ?? null, }); if (dedup && store.findByDedupKey) { const existing = await store.findByDedupKey(userId, dedup); if (existing) return existing; } return store.insert(userId, { ...observation, confirmed: true, dedupKey: dedup, }); } return { store, async list(userId, options) { return store.list(userId, options); }, async listTimeline(userId, options) { const rows = await store.list(userId, { limit: options?.limit ?? 500 }); return buildHealthTimeline(rows, options); }, async commit(userId, body = {}) { if (body.confirmed !== true) { const error = Object.assign(new Error('必须确认后才能保存'), { code: 'health_unconfirmed' }); throw error; } const observedAt = body.observedAt ?? Date.now(); const source = body.source ?? 'manual'; const context = body.context ?? 'other'; const sourceRef = body.sourceRef ?? null; if (body.metricSet === 'blood_pressure') { const pair = validateBloodPressurePair(body.systolic, body.diastolic); if (!pair.ok) { const error = Object.assign(new Error(pair.error ?? 'invalid_blood_pressure'), { code: pair.error }); throw error; } const systolic = await insertUnique(userId, { confirmed: true, observedAt, metricType: 'bp_systolic', valueNum: pair.systolic, unit: 'mmHg', context, source, sourceRef, qualityFlag: body.qualityFlag ?? 'ok', }); const diastolic = await insertUnique(userId, { confirmed: true, observedAt, metricType: 'bp_diastolic', valueNum: pair.diastolic, unit: 'mmHg', context, source, sourceRef, qualityFlag: body.qualityFlag ?? 'ok', }); return { observations: [systolic, diastolic] }; } if (body.metricSet === 'symptom') { const symptom = assertSymptomRecord({ code: body.symptomCode ?? body.code, severity: body.severity, note: body.note, }); if (!symptom.ok) { const error = Object.assign(new Error(symptom.error ?? 'invalid_symptom'), { code: symptom.error }); throw error; } const row = await insertUnique(userId, { confirmed: true, observedAt, metricType: 'symptom', valueText: symptom.code === 'none' ? 'none' : `${symptom.code}:${symptom.severity}`, unit: null, context, source, sourceRef, qualityFlag: 'ok', note: symptom.note, }); return { observations: [row] }; } const metricType = body.metricType ?? METRIC_SET_TO_TYPE[body.metricSet]; if (!metricType) { const error = Object.assign(new Error('unknown_metric_set'), { code: 'unknown_metric_set' }); throw error; } const checked = validateMetricValue(metricType, body.valueNum ?? body.value); if (!checked.ok) { const error = Object.assign(new Error(checked.error ?? 'invalid_metric'), { code: checked.error }); throw error; } const row = await insertUnique(userId, { confirmed: true, observedAt, metricType, valueNum: checked.value, unit: checked.unit, context: body.context ?? 'any', source, sourceRef, qualityFlag: body.qualityFlag ?? 'ok', }); return { observations: [row] }; }, async commitFromPending(userId, pendingValues = {}, source = 'manual') { if (!pendingValues?.metricSet) return null; return this.commit(userId, { confirmed: true, ...pendingValues, source: pendingValues.source ?? source, }); }, }; }