feature: qSOFA Scoring & Sepsis Bundle Compliance

This commit is contained in:
voltsrage
2026-06-18 22:52:43 +08:00
parent 3c54feb38a
commit 4b46c09f90
34 changed files with 2351 additions and 18 deletions
@@ -47,10 +47,12 @@ public class EsIndexerService : BackgroundService
{
_kafkaOptions.Topics.ObservationRecorded,
_kafkaOptions.Topics.AlertGenerated,
_kafkaOptions.Topics.EncounterStatusChanged
_kafkaOptions.Topics.EncounterStatusChanged,
_kafkaOptions.Topics.SepsisBundleCreated,
_kafkaOptions.Topics.SepsisBundleUpdated
});
_logger.LogInformation("EsIndexerService started. Subscribed to 3 topics.");
_logger.LogInformation("EsIndexerService started. Subscribed to 5 topics.");
try
{
@@ -91,6 +93,10 @@ public class EsIndexerService : BackgroundService
HandleObservationRecordedAsync(payload, ct),
var t when t == _kafkaOptions.Topics.AlertGenerated =>
HandleAlertGeneratedAsync(payload, ct),
var t when t == _kafkaOptions.Topics.SepsisBundleCreated =>
HandleSepsisBundleEventAsync(payload, ct),
var t when t == _kafkaOptions.Topics.SepsisBundleUpdated =>
HandleSepsisBundleEventAsync(payload, ct),
_ => Task.CompletedTask
};
@@ -260,4 +266,63 @@ public class EsIndexerService : BackgroundService
_logger.LogWarning(
"Could not update patient_encounters for alert on encounter {Id}", evt.EncounterId);
}
// --- sepsis.bundle.created / sepsis.bundle.updated ---
// Denormalizes bundle status onto the patient_encounters document so department
// acuity dashboards can filter/sort by sepsis bundle compliance state.
private async Task HandleSepsisBundleEventAsync(string payload, CancellationToken ct)
{
using var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
var encounterId = root.GetProperty("encounterId").GetString()!;
var complianceStatus = root.TryGetProperty("complianceStatus", out var cs)
? cs.GetString() ?? "IN_PROGRESS"
: "IN_PROGRESS";
var deadlineAt = root.TryGetProperty("deadlineAt", out var dl) ? dl.GetString() : null;
var scriptLines = new List<string>
{
"ctx._source.sepsisBundleStatus = params.status",
"ctx._source.sepsisBundleDeadlineAt = params.deadlineAt"
};
var scriptParams = new Dictionary<string, object?>
{
["status"] = complianceStatus,
["deadlineAt"] = deadlineAt
};
if (root.TryGetProperty("elementStatus", out _))
{
scriptLines.Add("ctx._source.sepsisBundleElementsCompleted = params.elementsCompleted");
scriptParams["elementsCompleted"] = root.TryGetProperty("elementsCompleted", out var ec)
? ec.GetInt32()
: (complianceStatus is "COMPLIANT" or "NON_COMPLIANT" ? 4 : 0);
}
else
{
scriptLines.Add("ctx._source.sepsisBundleElementsCompleted = 0");
}
var updateResp = await _elastic.UpdateAsync<PatientEncounterDocument, object>(
_esOptions.Indices.PatientEncounters,
encounterId,
u => u
.Script(new Script(new InlineScript
{
Source = string.Join(";\n", scriptLines),
Language = ScriptLanguage.Painless,
Params = scriptParams!
}))
.RetryOnConflict(3),
ct);
if (!updateResp.IsValidResponse && updateResp.Result != Result.NotFound)
_logger.LogWarning(
"Could not update patient_encounters bundle status for encounter {Id}", encounterId);
_logger.LogDebug("Updated patient_encounters bundle status for encounter {Id} → {Status}",
encounterId, complianceStatus);
}
}