2baf29b3ae
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>
129 lines
4.0 KiB
JavaScript
129 lines
4.0 KiB
JavaScript
import { inferBloodPressureContext } from './health-observation-validate.mjs';
|
|
import { HEALTH_DRAFT_TTL_MS, createDraftKey } from './health-observation-draft-store.mjs';
|
|
|
|
function extractionToCommitBody(extracted = {}, { observedAt = Date.now(), source = 'photo', sourceRef = null } = {}) {
|
|
const metricSet = extracted.metricSet ?? extracted.metric_set;
|
|
if (metricSet === 'blood_pressure') {
|
|
return {
|
|
confirmed: true,
|
|
metricSet: 'blood_pressure',
|
|
systolic: extracted.values?.systolic ?? extracted.systolic,
|
|
diastolic: extracted.values?.diastolic ?? extracted.diastolic,
|
|
context: extracted.context ?? inferBloodPressureContext(observedAt),
|
|
observedAt,
|
|
source,
|
|
sourceRef,
|
|
qualityFlag: extracted.lowConfidence ? 'suspect' : 'ok',
|
|
rawPayload: extracted.raw ?? extracted,
|
|
};
|
|
}
|
|
if (metricSet === 'symptom') {
|
|
return {
|
|
confirmed: true,
|
|
metricSet: 'symptom',
|
|
symptomCode: extracted.symptomCode ?? extracted.code,
|
|
severity: extracted.severity,
|
|
observedAt,
|
|
source,
|
|
sourceRef,
|
|
};
|
|
}
|
|
const valueNum =
|
|
extracted.values?.valueNum
|
|
?? extracted.values?.hr
|
|
?? extracted.values?.weight
|
|
?? extracted.values?.spo2
|
|
?? extracted.values?.temperature
|
|
?? extracted.valueNum;
|
|
return {
|
|
confirmed: true,
|
|
metricSet,
|
|
valueNum,
|
|
observedAt,
|
|
source,
|
|
sourceRef,
|
|
context: 'any',
|
|
qualityFlag: extracted.lowConfidence ? 'suspect' : 'ok',
|
|
rawPayload: extracted.raw ?? extracted,
|
|
};
|
|
}
|
|
|
|
export function createHealthObservationDraftService({ draftStore, observationService } = {}) {
|
|
if (!draftStore || !observationService) {
|
|
throw new Error('createHealthObservationDraftService requires draftStore and observationService');
|
|
}
|
|
|
|
return {
|
|
draftStore,
|
|
observationService,
|
|
async saveExtractionDraft(userId, {
|
|
extraction,
|
|
channel = 'h5',
|
|
sourceRef = null,
|
|
now = Date.now(),
|
|
} = {}) {
|
|
if (!extraction?.ok) {
|
|
const error = Object.assign(new Error('抽取结果无效,无法创建草稿'), { code: 'invalid_extraction' });
|
|
throw error;
|
|
}
|
|
if (sourceRef) {
|
|
const existing = await draftStore.findPendingBySourceRef(userId, sourceRef);
|
|
if (existing) return existing;
|
|
}
|
|
const draftKey = createDraftKey({
|
|
userId,
|
|
sourceRef: sourceRef ?? `manual-${now}`,
|
|
metricSet: extraction.metricSet,
|
|
now,
|
|
});
|
|
return draftStore.insert(userId, {
|
|
draftKey,
|
|
metricSet: extraction.metricSet,
|
|
sourceRef,
|
|
channel,
|
|
expiresAt: now + HEALTH_DRAFT_TTL_MS,
|
|
extracted: {
|
|
metricSet: extraction.metricSet,
|
|
values: extraction.values,
|
|
lowConfidence: extraction.lowConfidence ?? false,
|
|
raw: extraction.raw ?? null,
|
|
sourceRef,
|
|
},
|
|
validation: {
|
|
requireConfirm: true,
|
|
ok: true,
|
|
},
|
|
});
|
|
},
|
|
async getDraft(userId, draftKey) {
|
|
const draft = await draftStore.findByKey(userId, draftKey);
|
|
if (!draft) return null;
|
|
if (draft.expiresAt <= Date.now()) return null;
|
|
return draft;
|
|
},
|
|
async commitDraft(userId, draftKey, { source = 'photo' } = {}) {
|
|
const draft = await this.getDraft(userId, draftKey);
|
|
if (!draft) {
|
|
const error = Object.assign(new Error('草稿不存在或已过期'), { code: 'draft_not_found' });
|
|
throw error;
|
|
}
|
|
const body = extractionToCommitBody(draft.extracted, {
|
|
observedAt: Date.now(),
|
|
source,
|
|
sourceRef: draft.sourceRef,
|
|
});
|
|
const result = await observationService.commit(userId, body);
|
|
await draftStore.update(userId, draftKey, {
|
|
status: 'committed',
|
|
committedAt: Date.now(),
|
|
});
|
|
return { draft, ...result };
|
|
},
|
|
async discardDraft(userId, draftKey) {
|
|
const draft = await draftStore.findByKey(userId, draftKey);
|
|
if (!draft) return null;
|
|
return draftStore.update(userId, draftKey, { status: 'discarded' });
|
|
},
|
|
};
|
|
}
|