Files
memind/health-observation-service.mjs
T
john 2baf29b3ae feat(health): complete P0 health channel — baseline engine, page-data, MindSpace UI
Deliver encrypted health zone, observation API, baseline maturity pipeline,
page-data bindings, and H5/WeChat channel integration for health P0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:04:57 +08:00

142 lines
4.6 KiB
JavaScript

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,
});
},
};
}