Commit aa911a69 by luoqi

feat(ops): 每日健康报告召回改 进(净新+翻版)/出(失效+完成) diff

原'加了多少计划'看不懂;改成进出双向统计:进=净新(version=1)+翻版(version>1),出=失效(superseded 无在跑计划)+完成(completed)。persona 重算补 status 明细(含 noop)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 5b44e310
......@@ -46,7 +46,7 @@ export class DailyHealthReportService {
const errorH = Number(process.env.PAC_LAG_ERROR_HOURS ?? '48');
// ── 数据量 + 近 24h 增量 ──
const [patientsTotal, patients24h, factsTotal, facts24h, plansActive, plansAssigned, plans24h] =
const [patientsTotal, patients24h, factsTotal, facts24h, plansActive, plansAssigned] =
await Promise.all([
this.prisma.patient.count(),
this.prisma.patient.count({ where: { createdAt: { gte: since24h } } }),
......@@ -54,7 +54,6 @@ export class DailyHealthReportService {
this.prisma.patientFact.count({ where: { createdAt: { gte: since24h } } }),
this.prisma.followupPlan.count({ where: { status: 'active', supersededAt: null } }),
this.prisma.followupPlan.count({ where: { status: 'assigned', supersededAt: null } }),
this.prisma.followupPlan.count({ where: { createdAt: { gte: since24h } } }),
]);
// ── 摄入:最近一次成功增量 + DW 延迟 + 近 7 日失败 ──
......@@ -91,6 +90,47 @@ export class DailyHealthReportService {
const personaPatients = await this.prisma.persona.count({ where: { supersededAt: null } });
const coveragePct = patientsTotal > 0 ? Math.round((personaPatients / patientsTotal) * 100) : 0;
// ── 变动 diff(近 24h)──
// 计划:每次重算把旧 plan 置 superseded + 写新版本,故"createdAt 计数"里大头是**翻版**,
// 必须拆:净新召回(version=1,首次入池患者) vs 重算翻版(version>1,旧版本被替换)。
// 画像:按 personaRecomputeLog.status 统计当日重算次数(成功/部分/失败)。
// 流出(原有召回没了)有两种:
// 完成 = status='completed'(客服闭环做成),按 updatedAt 计入当日;
// 失效 = 24h 内被 superseded 且该患者**已无任何 open plan**(重算发现信号没了 → 自然掉出,
// 非翻版)。这是 plan_reasons.closedAt 未被填充时唯一能反查的"召回消失",按患者去重。
const [planNetNew24h, planRevised24h, planCompleted24h, churnedRows, personaRuns] =
await Promise.all([
this.prisma.followupPlan.count({ where: { version: 1, createdAt: { gte: since24h } } }),
this.prisma.followupPlan.count({ where: { version: { gt: 1 }, createdAt: { gte: since24h } } }),
this.prisma.followupPlan.count({ where: { status: 'completed', updatedAt: { gte: since24h } } }),
this.prisma.$queryRaw<Array<{ n: number }>>`
SELECT count(DISTINCT fp.patient_id)::int AS n
FROM followup_plans fp
WHERE fp.superseded_at >= ${since24h}
AND NOT EXISTS (
SELECT 1 FROM followup_plans o
WHERE o.patient_id = fp.patient_id
AND o.superseded_at IS NULL
AND o.status IN ('active', 'assigned')
)`,
this.prisma.personaRecomputeLog.groupBy({
by: ['status'],
where: { startedAt: { gte: since24h } },
_count: { _all: true },
}),
]);
const planChurnedOut24h = Number(churnedRows[0]?.n ?? 0);
const prMap: Record<string, number> = Object.fromEntries(
personaRuns.map((g) => [g.status, g._count._all]),
);
const personaRuns24h = personaRuns.reduce((s, g) => s + g._count._all, 0);
// 各状态都列,保证 breakdown 之和 = 总次数(noop = 重算了但无变化的空跑,健康)
const personaRunsZh =
`成功 ${prMap.success ?? 0}` +
(prMap.noop ? ` / 空跑 ${prMap.noop}` : '') +
(prMap.partial ? ` / 部分 ${prMap.partial}` : '') +
(prMap.failed ? ` / 失败 ${prMap.failed}` : '');
// ── AI 用量(近 24h)──
const [inv24h, costAgg] = await Promise.all([
this.prisma.agentInvocation.count({ where: { startedAt: { gte: since24h } } }),
......@@ -110,11 +150,12 @@ export class DailyHealthReportService {
const body = [
`📥 摄入 ${lagTag} 上次 ${fmtTime(lastSync?.startedAt)} · DW延迟 ${lagHours == null ? '—' : lagHours.toFixed(1) + 'h'} · 7日失败 ${fail7d}`,
`🔄 重算 画像 ${fmtTime(lastPersona?.startedAt)}${lastPersona?.status && lastPersona.status !== 'success' ? `(${lastPersona.status})` : ''} · 召回 ${fmtTime(lastPlanGen?.startedAt)}(+${lastPlanGen?.plansCreated ?? 0}计划)`,
`📈 数据(24h) 患者 ${fmtNum(patientsTotal)}(+${patients24h}) · 事实 ${fmtWan(factsTotal)}(+${fmtNum(facts24h)}) · 新计划 +${plans24h}`,
`🔄 重算 画像 ${fmtTime(lastPersona?.startedAt)}${lastPersona?.status && lastPersona.status !== 'success' ? `(${lastPersona.status})` : ''} · 召回 ${fmtTime(lastPlanGen?.startedAt)}`,
`📈 数据(24h) 患者 ${fmtNum(patientsTotal)}(+${patients24h}) · 事实 ${fmtWan(factsTotal)}(+${fmtNum(facts24h)})`,
`🖼 画像(24h) 重算 ${fmtNum(personaRuns24h)} (${personaRunsZh}) · 覆盖 ${fmtNum(personaPatients)}/${fmtNum(patientsTotal)}(${coveragePct}%)`,
`📋 召回(24h) 进:净新 +${planNetNew24h} / 翻版 ~${fmtNum(planRevised24h)} · 出:失效 -${planChurnedOut24h} / 完成 -${planCompleted24h}`,
`📋 召回池 待办 ${fmtNum(plansActive)} · 进行中 ${fmtNum(plansAssigned)}`,
`📞 客服执行(24h) ${execTotal}${execBreakdown ? ` · ${execBreakdown}` : ''}`,
`🖼 画像覆盖 ${fmtNum(personaPatients)} / ${fmtNum(patientsTotal)}(${coveragePct}%)`,
`🤖 AI(24h) ${inv24h} 次 · ¥${cost24h.toFixed(2)}`,
].join('\n');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment