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>
97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
|
import { ensureHealthPageDataForUser } from './health-page-data-bootstrap.mjs';
|
|
import { HEALTH_EVENTS_DATASET } from './health-page-data-schema.mjs';
|
|
import {
|
|
healthEventFingerprint,
|
|
mapHealthEventToPgPayload,
|
|
mapPgHealthEventRow,
|
|
} from './health-event-serialize.mjs';
|
|
|
|
const DATASET_NAME = HEALTH_EVENTS_DATASET.name;
|
|
|
|
export function createPageDataHealthEventStore({
|
|
resolveWorkspaceRoot,
|
|
logger = console,
|
|
} = {}) {
|
|
if (typeof resolveWorkspaceRoot !== 'function') {
|
|
throw new Error('createPageDataHealthEventStore 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 list(userId, { status = 'open', limit = 100 } = {}) {
|
|
const service = await getUserDataSpace(userId);
|
|
if (!service) return [];
|
|
try {
|
|
const result = await service.readDatasetRows(DATASET_NAME, {
|
|
limit: Math.min(Math.max(limit, 1), 200),
|
|
orderBy: 'detected_at',
|
|
orderDir: 'desc',
|
|
});
|
|
return (result.rows ?? [])
|
|
.map((row) => mapPgHealthEventRow(row, userId))
|
|
.filter((row) => !status || row.status === status);
|
|
} catch (error) {
|
|
if (error?.code === 'dataset_not_found') return [];
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return {
|
|
backend: 'page_data',
|
|
list,
|
|
async listUnreadAlerts(userId, { limit = 20 } = {}) {
|
|
const rows = await list(userId, { status: 'open', limit: 200 });
|
|
return rows
|
|
.filter((row) => row.severity === 'watch' || row.severity === 'alert')
|
|
.slice(0, limit);
|
|
},
|
|
async acknowledge(userId, eventId) {
|
|
const openRows = await list(userId, { status: 'open', limit: 200 });
|
|
const existing = openRows.find((row) => Number(row.id) === Number(eventId));
|
|
if (!existing?.id) return null;
|
|
const service = await getUserDataSpace(userId);
|
|
if (!service) return null;
|
|
const { row } = await service.updateDatasetRow(DATASET_NAME, existing.id, { status: 'acknowledged' });
|
|
return mapPgHealthEventRow(row, userId);
|
|
},
|
|
async acknowledgeAll(userId) {
|
|
const openRows = await listUnreadAlerts(userId, { limit: 200 });
|
|
let count = 0;
|
|
for (const row of openRows) {
|
|
const updated = await this.acknowledge(userId, row.id);
|
|
if (updated) count += 1;
|
|
}
|
|
return count;
|
|
},
|
|
async insertIfNew(userId, event, { now = Date.now() } = {}) {
|
|
const openRows = await list(userId, { status: 'open', limit: 200 });
|
|
const fingerprint = healthEventFingerprint(event);
|
|
const duplicate = openRows.find((row) => healthEventFingerprint(row) === fingerprint);
|
|
if (duplicate) return { row: duplicate, created: false };
|
|
|
|
const service = await getUserDataSpace(userId);
|
|
if (!service) {
|
|
const error = Object.assign(new Error('用户健康事件空间不可用'), { code: 'health_storage_unavailable' });
|
|
throw error;
|
|
}
|
|
try {
|
|
const { row } = await service.insertDatasetRow(
|
|
DATASET_NAME,
|
|
mapHealthEventToPgPayload(event, now),
|
|
);
|
|
return { row: mapPgHealthEventRow(row, userId), created: true };
|
|
} catch (error) {
|
|
logger.warn?.('Page Data health event insert failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
}
|