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>
131 lines
3.8 KiB
JavaScript
131 lines
3.8 KiB
JavaScript
import { computeHealthBaselines } from './health-baseline-engine.mjs';
|
|
import { evaluateHealthEvents } from './health-event-engine.mjs';
|
|
import { baselineToApiShape } from './health-baseline-serialize.mjs';
|
|
import { isMemindHealthEnabled } from './health-feature.mjs';
|
|
|
|
export async function recomputeUserHealthBaselines(
|
|
userId,
|
|
{
|
|
observationStore,
|
|
baselineStore,
|
|
eventStore,
|
|
eventNotificationService = null,
|
|
now = Date.now(),
|
|
observationLimit = 500,
|
|
eventMode = 'active',
|
|
} = {},
|
|
) {
|
|
if (!userId || !observationStore || !baselineStore) {
|
|
return { ok: false, skipped: true, reason: 'missing_dependencies' };
|
|
}
|
|
|
|
const observations = await observationStore.list(userId, { limit: observationLimit });
|
|
if (!observations.length) {
|
|
return { ok: true, skipped: true, reason: 'no_observations', userId };
|
|
}
|
|
|
|
const computed = computeHealthBaselines(observations, { now });
|
|
const persisted = await baselineStore.upsertMany(userId, computed, { computedAt: now });
|
|
|
|
let newEvents = 0;
|
|
let notificationsSent = 0;
|
|
if (eventStore) {
|
|
const detected = evaluateHealthEvents(observations, computed, { now, mode: eventMode });
|
|
for (const event of detected) {
|
|
const result = await eventStore.insertIfNew(userId, event, { now });
|
|
if (result.created) {
|
|
newEvents += 1;
|
|
if (eventNotificationService) {
|
|
const notifyResult = await eventNotificationService.notifyNewEvent(userId, event, {
|
|
eventId: result.row?.id ?? null,
|
|
});
|
|
if (notifyResult?.webCreated || notifyResult?.wechatSent) {
|
|
notificationsSent += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
skipped: false,
|
|
userId,
|
|
observationCount: observations.length,
|
|
baselineCount: persisted.length,
|
|
newEvents,
|
|
notificationsSent,
|
|
baselines: persisted.map(baselineToApiShape),
|
|
};
|
|
}
|
|
|
|
export async function listHealthBaselineJobUserIds({
|
|
observationStore,
|
|
pool = null,
|
|
limit = 500,
|
|
} = {}) {
|
|
if (typeof observationStore?.listUserIds === 'function') {
|
|
return observationStore.listUserIds().slice(0, limit);
|
|
}
|
|
if (!pool) return [];
|
|
try {
|
|
const [rows] = await pool.query(
|
|
`SELECT id FROM h5_users WHERE status = 'active' ORDER BY updated_at DESC LIMIT ?`,
|
|
[Math.min(Math.max(limit, 1), 2000)],
|
|
);
|
|
return rows.map((row) => String(row.id));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function runHealthBaselineJob({
|
|
userIds = [],
|
|
observationStore,
|
|
baselineStore,
|
|
eventStore,
|
|
eventNotificationService = null,
|
|
now = Date.now(),
|
|
env = process.env,
|
|
logger = console,
|
|
} = {}) {
|
|
if (!isMemindHealthEnabled(env)) {
|
|
return { ok: false, skipped: true, reason: 'health_disabled', results: [] };
|
|
}
|
|
if (!observationStore || !baselineStore) {
|
|
return { ok: false, skipped: true, reason: 'missing_dependencies', results: [] };
|
|
}
|
|
|
|
const targets = [...new Set((userIds ?? []).map((id) => String(id)).filter(Boolean))];
|
|
const results = [];
|
|
for (const userId of targets) {
|
|
try {
|
|
const result = await recomputeUserHealthBaselines(userId, {
|
|
observationStore,
|
|
baselineStore,
|
|
eventStore,
|
|
eventNotificationService,
|
|
now,
|
|
});
|
|
results.push(result);
|
|
} catch (error) {
|
|
logger.warn?.(`Health baseline job failed for ${userId}:`, error);
|
|
results.push({
|
|
ok: false,
|
|
skipped: false,
|
|
userId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
processed: results.length,
|
|
updated: results.filter((item) => item.ok && !item.skipped).length,
|
|
newEvents: results.reduce((sum, item) => sum + Number(item.newEvents ?? 0), 0),
|
|
notificationsSent: results.reduce((sum, item) => sum + Number(item.notificationsSent ?? 0), 0),
|
|
results,
|
|
};
|
|
}
|