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>
115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
|
import { ensureHealthPageDataForUser } from './health-page-data-bootstrap.mjs';
|
|
import { HEALTH_OBSERVATION_DRAFTS_DATASET } from './health-page-data-schema.mjs';
|
|
|
|
const DATASET_NAME = HEALTH_OBSERVATION_DRAFTS_DATASET.name;
|
|
|
|
function parseJsonField(value, fallback = {}) {
|
|
if (value == null) return fallback;
|
|
if (typeof value === 'object') return value;
|
|
try {
|
|
return JSON.parse(String(value));
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function mapPgDraftRow(row, userId, sourceRef = null) {
|
|
return {
|
|
id: Number(row.id),
|
|
userId: String(userId),
|
|
draftKey: row.draft_key,
|
|
metricSet: row.metric_set,
|
|
extracted: parseJsonField(row.extracted),
|
|
validation: parseJsonField(row.validation),
|
|
status: row.status,
|
|
channel: row.channel,
|
|
sourceRef: sourceRef ?? parseJsonField(row.extracted)?.sourceRef ?? null,
|
|
createdAt: row.created_at ? new Date(row.created_at).getTime() : Date.now(),
|
|
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : Date.now(),
|
|
committedAt: row.committed_at ? new Date(row.committed_at).getTime() : null,
|
|
};
|
|
}
|
|
|
|
export function createPageDataHealthObservationDraftStore({ resolveWorkspaceRoot, logger = console }) {
|
|
if (typeof resolveWorkspaceRoot !== 'function') {
|
|
throw new Error('createPageDataHealthObservationDraftStore requires resolveWorkspaceRoot');
|
|
}
|
|
|
|
async function getUserDataSpace(userId) {
|
|
const workspaceRoot = await resolveWorkspaceRoot(userId);
|
|
if (!workspaceRoot) return null;
|
|
const service = createUserDataSpaceService({ workspaceRoot, userId: String(userId) });
|
|
await ensureHealthPageDataForUser(service, userId);
|
|
return service;
|
|
}
|
|
|
|
async function listPending(userId, { limit = 100 } = {}) {
|
|
const service = await getUserDataSpace(userId);
|
|
if (!service) return [];
|
|
try {
|
|
const result = await service.readDatasetRows(DATASET_NAME, {
|
|
limit,
|
|
orderBy: 'created_at',
|
|
orderDir: 'desc',
|
|
});
|
|
const now = Date.now();
|
|
return (result.rows ?? [])
|
|
.map((row) => mapPgDraftRow(row, userId))
|
|
.filter((row) => row.status === 'pending' && row.expiresAt > now);
|
|
} catch (error) {
|
|
if (error?.code === 'dataset_not_found') return [];
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return {
|
|
backend: 'page_data',
|
|
async findByKey(userId, draftKey) {
|
|
const pending = await listPending(userId);
|
|
return pending.find((row) => row.draftKey === draftKey) ?? null;
|
|
},
|
|
async findPendingBySourceRef(userId, sourceRef) {
|
|
if (!sourceRef) return null;
|
|
const pending = await listPending(userId);
|
|
return pending.find((row) => row.sourceRef === sourceRef) ?? null;
|
|
},
|
|
async insert(userId, draft) {
|
|
const service = await getUserDataSpace(userId);
|
|
if (!service) {
|
|
const error = Object.assign(new Error('用户健康草稿空间不可用'), { code: 'health_storage_unavailable' });
|
|
throw error;
|
|
}
|
|
const extracted = { ...draft.extracted, sourceRef: draft.sourceRef ?? null };
|
|
try {
|
|
const { row } = await service.insertDatasetRow(DATASET_NAME, {
|
|
draft_key: draft.draftKey,
|
|
metric_set: draft.metricSet,
|
|
extracted: JSON.stringify(extracted),
|
|
validation: JSON.stringify(draft.validation ?? {}),
|
|
status: 'pending',
|
|
channel: draft.channel ?? 'h5',
|
|
expires_at: new Date(draft.expiresAt ?? Date.now() + 86400000).toISOString(),
|
|
});
|
|
return mapPgDraftRow(row, userId, draft.sourceRef ?? null);
|
|
} catch (error) {
|
|
logger.warn?.('Page Data health draft insert failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
async update(userId, draftKey, patch) {
|
|
const service = await getUserDataSpace(userId);
|
|
if (!service) return null;
|
|
const existing = await this.findByKey(userId, draftKey);
|
|
if (!existing?.id) return null;
|
|
const payload = {};
|
|
if (patch.extracted != null) payload.extracted = JSON.stringify(patch.extracted);
|
|
if (patch.validation != null) payload.validation = JSON.stringify(patch.validation);
|
|
if (patch.status != null) payload.status = patch.status;
|
|
if (patch.committedAt != null) payload.committed_at = new Date(patch.committedAt).toISOString();
|
|
const { row } = await service.updateDatasetRow(DATASET_NAME, existing.id, payload);
|
|
return mapPgDraftRow(row, userId, existing.sourceRef);
|
|
},
|
|
};
|
|
}
|