Initial commit: HealthCarePregnant project documentation and platform
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 端到端场景演练(T-9.2 / PRD §3.2 核心场景 S1–S7、§5.1 闭环、§7 上线判据)。
|
||||
*
|
||||
* 在服务层串联各模块,覆盖 V1 可上线场景的实质闭环;
|
||||
* 标注为 V2/V3 的能力(趋势预测、游戏化)以 it.todo 记录,不在 V1 断言。
|
||||
*
|
||||
* 复用既有内存仓储与服务,构造共享依赖的系统实例(makeSystem)。
|
||||
*/
|
||||
import { PatientService } from '../modules/patient/patient.service';
|
||||
import { InMemoryPatientRepository } from '../modules/patient/patient.repository';
|
||||
import { AnalysisService } from '../modules/analysis/analysis.service';
|
||||
import { InMemoryAlertRepository } from '../modules/analysis/alert.repository';
|
||||
import { CaseflowService } from '../modules/caseflow/caseflow.service';
|
||||
import { InMemoryCaseflowRepository } from '../modules/caseflow/caseflow.repository';
|
||||
import { ObservationService } from '../modules/observation/observation.service';
|
||||
import { InMemoryObservationRepository } from '../modules/observation/observation.repository';
|
||||
import { KnowledgeService } from '../modules/knowledge/knowledge.service';
|
||||
import { InMemoryKnowledgeRepository } from '../modules/knowledge/knowledge.repository';
|
||||
import { AiService } from '../modules/ai/ai.service';
|
||||
import { NotificationService } from '../modules/notification/notification.service';
|
||||
import { RedflagService } from '../modules/redflag/redflag.service';
|
||||
import { ReminderService } from '../modules/reminder/reminder.service';
|
||||
import { FollowupService } from '../modules/followup/followup.service';
|
||||
import { InMemoryFollowupRepository } from '../modules/followup/followup.repository';
|
||||
|
||||
function makeSystem() {
|
||||
const notification = new NotificationService();
|
||||
const followupService = new FollowupService(new InMemoryFollowupRepository());
|
||||
const patientService = new PatientService(new InMemoryPatientRepository());
|
||||
const analysisService = new AnalysisService(new InMemoryAlertRepository());
|
||||
const caseflowService = new CaseflowService(new InMemoryCaseflowRepository());
|
||||
const observationService = new ObservationService(
|
||||
new InMemoryObservationRepository(),
|
||||
patientService,
|
||||
analysisService,
|
||||
caseflowService,
|
||||
followupService,
|
||||
);
|
||||
const knowledgeService = new KnowledgeService(new InMemoryKnowledgeRepository());
|
||||
const aiService = new AiService(knowledgeService, analysisService, caseflowService);
|
||||
const redflagService = new RedflagService(notification, caseflowService);
|
||||
const reminderService = new ReminderService(patientService, notification);
|
||||
return {
|
||||
notification,
|
||||
patientService,
|
||||
analysisService,
|
||||
caseflowService,
|
||||
observationService,
|
||||
knowledgeService,
|
||||
aiService,
|
||||
redflagService,
|
||||
reminderService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('PRD §3.2 核心场景端到端演练(T-9.2)', () => {
|
||||
describe('S1 居家测血糖/血压后自动分析(REQ-1/3)', () => {
|
||||
it('血糖录入 → 质控通过 → 规则分析 → 可追溯预警', async () => {
|
||||
const sys = makeSystem();
|
||||
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
|
||||
|
||||
const rec = await sys.observationService.record(patient.id, {
|
||||
indicator: 'fasting_glucose',
|
||||
value: 5.6,
|
||||
});
|
||||
expect(rec.observation.qcStatus).toBe('accepted');
|
||||
expect(rec.alert).not.toBeNull();
|
||||
expect(rec.alert?.level).toBe('medium');
|
||||
// 可解释 / 可追溯(NFR-3):命中规则 + 关联观测值
|
||||
expect(rec.alert?.ruleIds.length).toBeGreaterThan(0);
|
||||
expect(rec.alert?.observationId).toBe(rec.observation.id);
|
||||
});
|
||||
|
||||
it('血压录入 → 升高生成预警', async () => {
|
||||
const sys = makeSystem();
|
||||
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
|
||||
const rec = await sys.observationService.record(patient.id, {
|
||||
indicator: 'systolic_bp',
|
||||
value: 150,
|
||||
});
|
||||
expect(rec.observation.qcStatus).toBe('accepted');
|
||||
expect(rec.alert?.level).toBe('medium');
|
||||
});
|
||||
|
||||
it('不可信数值 → 质控拦截,不驱动高风险结论(C-5)', async () => {
|
||||
const sys = makeSystem();
|
||||
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
|
||||
const rec = await sys.observationService.record(patient.id, {
|
||||
indicator: 'fasting_glucose',
|
||||
value: 50, // 超出生理合理范围
|
||||
});
|
||||
expect(rec.observation.qcStatus).toBe('rejected');
|
||||
expect(rec.alert).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('S2 GDM 风险孕妇的日常管理闭环(REQ-6/9/10)', () => {
|
||||
it('建档→预警→自动开案→指派→流转→AI建议(需人工确认)→照护计划→提醒', async () => {
|
||||
const sys = makeSystem();
|
||||
// 既往 GDM → 基线 medium
|
||||
const patient = await sys.patientService.create({
|
||||
name: '小雅',
|
||||
age: 31,
|
||||
lmp: '2026-01-01',
|
||||
historyGdm: true,
|
||||
});
|
||||
expect(patient.initialRiskLevel).toBe('medium');
|
||||
|
||||
// 录入偏高血糖 → 预警 → 预警驱动开案
|
||||
const rec = await sys.observationService.record(patient.id, {
|
||||
indicator: 'fasting_glucose',
|
||||
value: 6.2,
|
||||
});
|
||||
expect(rec.alert).not.toBeNull();
|
||||
|
||||
const c = await sys.caseflowService.getCaseByPatient(patient.id);
|
||||
expect(c.status).toBe('open');
|
||||
expect(c.riskLevel).toBe('medium');
|
||||
|
||||
// 指派管理师 + 推进阶段
|
||||
await sys.caseflowService.assignManager(c.id, 'cm-1');
|
||||
const advanced = await sys.caseflowService.advanceStage(c.id, 'assessment', '评估');
|
||||
expect(advanced.stage).toBe('assessment');
|
||||
expect(advanced.caseManagerId).toBe('cm-1');
|
||||
|
||||
// AI 决策建议:中风险 → 必须人工确认(REQ-10.3)
|
||||
const reco = await sys.aiService.recommendForPatient(patient.id);
|
||||
expect(reco.requiresHumanConfirmation).toBe(true);
|
||||
expect(reco.actions.length).toBeGreaterThan(0);
|
||||
|
||||
// 照护计划
|
||||
const plan = await sys.caseflowService.createCarePlan(c.id, {
|
||||
goals: ['空腹血糖 < 5.1 mmol/L'],
|
||||
interventions: [{ kind: 'lifestyle', description: '饮食控制 + 餐后散步' }],
|
||||
followUpFrequency: 'weekly',
|
||||
});
|
||||
expect(plan.goals.length).toBe(1);
|
||||
|
||||
// 提醒下发
|
||||
const reminder = await sys.reminderService.dispatch({ patientId: patient.id, type: 'measurement' });
|
||||
expect(reminder.effectiveType).toBe('measurement');
|
||||
expect(sys.notification.findByRecipient(patient.id).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S3 子痫前期红旗症状旁路(REQ-5)', () => {
|
||||
it('危急组合 → 即时就医提示 + 通知管理师/医生 + 升级高风险', async () => {
|
||||
const sys = makeSystem();
|
||||
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
|
||||
|
||||
const result = await sys.redflagService.evaluate(patient.id, {
|
||||
systolicBp: 165,
|
||||
diastolicBp: 112,
|
||||
symptoms: ['severe_headache'],
|
||||
});
|
||||
expect(result.triggered).toBe(true);
|
||||
expect(result.hits.map((h) => h.ruleId)).toContain('RF-PREECLAMPSIA');
|
||||
expect(result.patientAdvice).toContain('就医');
|
||||
|
||||
// 通知:孕妇 + 管理师 + 医生,均为紧急
|
||||
const msgs = sys.notification.findByRecipient(patient.id);
|
||||
const audiences = msgs.filter((m) => m.urgent).map((m) => m.audience);
|
||||
expect(audiences).toEqual(expect.arrayContaining(['patient', 'case_manager', 'physician']));
|
||||
|
||||
// 个案升级为高风险
|
||||
const c = await sys.caseflowService.getCaseByPatient(patient.id);
|
||||
expect(c.riskLevel).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('S4 深夜疑问"我能吃这个吗"(REQ-7)', () => {
|
||||
it('命中知识 → 带溯源作答;无依据 → 不超纲并建议就医', async () => {
|
||||
const sys = makeSystem();
|
||||
await sys.knowledgeService.create({
|
||||
category: 'guideline',
|
||||
title: '孕期饮食与血糖',
|
||||
content: '控制精制碳水、少量多餐;水果适量并计入总量。',
|
||||
keywords: ['饮食', '血糖', '水果', '能吃'],
|
||||
source: '某权威指南',
|
||||
authority: 'authoritative',
|
||||
});
|
||||
|
||||
const grounded = await sys.aiService.ask('孕期能吃水果吗');
|
||||
expect(grounded.grounded).toBe(true);
|
||||
expect(grounded.citations.length).toBeGreaterThan(0);
|
||||
expect(grounded.citations[0].source).toBe('某权威指南');
|
||||
|
||||
const ungrounded = await sys.aiService.ask('明天会下雨吗');
|
||||
expect(ungrounded.grounded).toBe(false);
|
||||
expect(ungrounded.citations).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S5 趋势预警/预测(REQ-3.5/4)', () => {
|
||||
it('纵向观测按孕周累积,构成趋势分析的数据基础', async () => {
|
||||
const sys = makeSystem();
|
||||
const patient = await sys.patientService.create({ name: '小雅', age: 30, lmp: '2026-01-01' });
|
||||
await sys.observationService.record(patient.id, { indicator: 'fasting_glucose', value: 4.8 });
|
||||
await sys.observationService.record(patient.id, { indicator: 'fasting_glucose', value: 5.0 });
|
||||
const series = await sys.observationService.list(patient.id);
|
||||
expect(series.length).toBe(2);
|
||||
// 每条带孕周上下文,支撑后续时序趋势
|
||||
expect(series.every((o) => typeof o.gestationalWeeks === 'number')).toBe(true);
|
||||
});
|
||||
|
||||
// 趋势异常检测(REQ-3.5,V2)与预测模型(REQ-4,V3,需回顾性+前瞻性验证)超出 V1 范围
|
||||
it.todo('S5 时序趋势异常检测(REQ-3.5)为 V2');
|
||||
it.todo('S5 趋势预测模型(REQ-4)为 V3,需经验证');
|
||||
});
|
||||
|
||||
describe('S6 提升坚持度、缓解焦虑(REQ-9)', () => {
|
||||
it('高风险孕妇运动提醒自动替换为休息(REQ-9.2 安全约束)', async () => {
|
||||
const sys = makeSystem();
|
||||
// 多胎 + 既往GDM → score 4 → 基线高风险
|
||||
const patient = await sys.patientService.create({
|
||||
name: '小雅',
|
||||
age: 31,
|
||||
lmp: '2026-01-01',
|
||||
multipleGestation: true,
|
||||
historyGdm: true,
|
||||
});
|
||||
expect(patient.initialRiskLevel).toBe('high');
|
||||
|
||||
const reminder = await sys.reminderService.dispatch({ patientId: patient.id, type: 'exercise' });
|
||||
expect(reminder.effectiveType).toBe('rest');
|
||||
expect(reminder.adjustedForRisk).toBe(true);
|
||||
});
|
||||
|
||||
it('非高风险孕妇运动提醒保持运动', async () => {
|
||||
const sys = makeSystem();
|
||||
const patient = await sys.patientService.create({ name: '小雅', age: 28, lmp: '2026-01-01' });
|
||||
const reminder = await sys.reminderService.dispatch({ patientId: patient.id, type: 'exercise' });
|
||||
expect(reminder.effectiveType).toBe('exercise');
|
||||
expect(reminder.adjustedForRisk).toBe(false);
|
||||
});
|
||||
|
||||
// 游戏化(积分/徽章/小游戏,REQ-9.3/9.4)为 V2
|
||||
it.todo('S6 游戏化激励(REQ-9.3/9.4)为 V2');
|
||||
});
|
||||
|
||||
describe('S7 管理师 PC 端批量管理(REQ-13.3)', () => {
|
||||
it('多名孕妇入列,提供风险分层作为工作台排序依据', async () => {
|
||||
const sys = makeSystem();
|
||||
await sys.patientService.create({ name: '低风险', age: 28, lmp: '2026-01-01' });
|
||||
await sys.patientService.create({ name: '中风险', age: 31, lmp: '2026-01-01', historyGdm: true });
|
||||
await sys.patientService.create({
|
||||
name: '高风险',
|
||||
age: 36,
|
||||
lmp: '2026-01-01',
|
||||
multipleGestation: true,
|
||||
historyPih: true,
|
||||
});
|
||||
|
||||
const list = await sys.patientService.list();
|
||||
expect(list.length).toBe(3);
|
||||
// 每名孕妇均带初始风险分层(admin-web 工作列表据此按 高→中→低 排序)
|
||||
const levels = list.map((p) => p.initialRiskLevel).sort();
|
||||
expect(levels).toEqual(['high', 'low', 'medium']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user