feat(health): 告警降噪集成 alert_engine + OAuth service 编译修复

- alert_engine: create_alert_and_notify 调用 noise_reducer,升级严重度+suppressed标记
- oauth/service: 修复 OsRng import + ActiveModel move 问题
- fhir/handler: linter 补全完整实现
This commit is contained in:
iven
2026-05-04 02:43:32 +08:00
parent 62c02e0f15
commit 975d699e42
3 changed files with 953 additions and 112 deletions

View File

@@ -1,13 +1,21 @@
use axum::extract::{FromRef, Path, Query, State};
use axum::response::IntoResponse;
use axum::Extension;
use axum::Json;
use sea_orm::*;
use serde::Deserialize;
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::TenantContext;
use crate::fhir::converter;
use crate::fhir::types::{category_to_device_types, loinc_to_device_type};
use crate::state::HealthState;
/// GET /fhir/R4/metadata — FHIR CapabilityStatement
pub async fn capability_statement<S>() -> Result<impl IntoResponse, erp_core::error::AppError>
pub async fn capability_statement<S>() -> Result<impl IntoResponse, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
@@ -42,187 +50,603 @@ where
}
#[derive(Debug, Deserialize)]
pub struct SearchParams {
pub struct FhirSearchParams {
#[serde(rename = "_id")]
pub id: Option<String>,
#[serde(rename = "_count")]
pub count: Option<u64>,
#[serde(rename = "_offset")]
pub offset: Option<u64>,
pub patient: Option<String>,
pub category: Option<String>,
#[serde(rename = "_count")]
pub count: Option<u32>,
#[serde(rename = "_offset")]
pub offset: Option<u32>,
pub code: Option<String>,
pub date: Option<String>,
pub name: Option<String>,
pub identifier: Option<String>,
pub status: Option<String>,
}
// ── Patient ────────────────────────────────────────────────────────────
pub async fn search_patients(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.patient.list")?;
let mut query = crate::entity::patient::Entity::find()
.filter(crate::entity::patient::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::patient::Column::DeletedAt.is_null());
if let Some(ref id) = params.id {
let uid = Uuid::parse_str(id).map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::patient::Column::Id.eq(uid));
}
if let Some(ref name) = params.name {
query = query.filter(crate::entity::patient::Column::Name.contains(name));
}
if let Some(ref identifier) = params.identifier {
query = query.filter(crate::entity::patient::Column::IdNumber.contains(identifier));
}
let limit = params.count.unwrap_or(20).min(100);
let offset = params.offset.unwrap_or(0);
let patients = query
.limit(limit)
.offset(offset)
.all(&state.db)
.await?;
let entries: Vec<serde_json::Value> = patients.iter()
.map(|p| serde_json::json!({"resource": converter::patient_to_fhir(p)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_patient(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "Patient not implemented yet"}]
})))
}
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.patient.list")?;
pub async fn patient_everything(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
})))
let patient = crate::entity::patient::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Patient not found".into()))?;
if patient.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::patient_to_fhir(&patient)))
}
// ── Observation ────────────────────────────────────────────────────────
pub async fn search_observations(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
})))
}
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.device-readings.list")?;
let mut query = crate::entity::device_readings::Entity::find()
.filter(crate::entity::device_readings::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::device_readings::Column::DeletedAt.is_null());
if let Some(ref patient_id) = params.patient {
let uid = Uuid::parse_str(patient_id)
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::device_readings::Column::PatientId.eq(uid));
}
if let Some(ref code) = params.code {
if let Some(dt) = loinc_to_device_type(code) {
query = query.filter(crate::entity::device_readings::Column::DeviceType.eq(dt));
}
}
if let Some(ref category) = params.category {
let types = category_to_device_types(category);
if !types.is_empty() {
query = query.filter(
crate::entity::device_readings::Column::DeviceType.is_in(types)
);
}
}
if let Some(ref date) = params.date {
if let Some(after) = date.strip_prefix("gt") {
if let Ok(dt) = after.parse::<chrono::DateTime<chrono::Utc>>() {
query = query.filter(
crate::entity::device_readings::Column::MeasuredAt.gt(dt)
);
}
} else if let Some(before) = date.strip_prefix("lt") {
if let Ok(dt) = before.parse::<chrono::DateTime<chrono::Utc>>() {
query = query.filter(
crate::entity::device_readings::Column::MeasuredAt.lt(dt)
);
}
} else if let Ok(dt) = date.parse::<chrono::DateTime<chrono::Utc>>() {
let start = dt.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
let end = dt.date_naive().and_hms_opt(23, 59, 59).unwrap().and_utc();
query = query.filter(
crate::entity::device_readings::Column::MeasuredAt.between(start, end)
);
}
}
let limit = params.count.unwrap_or(50).min(200);
let readings = query
.order_by_desc(crate::entity::device_readings::Column::MeasuredAt)
.limit(limit)
.all(&state.db)
.await?;
let mut entries = Vec::new();
for reading in &readings {
for obs in converter::device_reading_to_fhir_observations(reading) {
entries.push(serde_json::json!({"resource": obs}));
}
}
pub async fn observation_lastn(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
// ── Device ─────────────────────────────────────────────────────────────
pub async fn search_devices(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.devices.list")?;
let mut query = crate::entity::patient_devices::Entity::find()
.filter(crate::entity::patient_devices::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::patient_devices::Column::DeletedAt.is_null());
if let Some(ref patient_id) = params.patient {
let uid = Uuid::parse_str(patient_id)
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::patient_devices::Column::PatientId.eq(uid));
}
let limit = params.count.unwrap_or(50).min(200);
let devices = query.limit(limit).all(&state.db).await?;
let entries: Vec<serde_json::Value> = devices.iter()
.map(|d| serde_json::json!({"resource": converter::patient_device_to_fhir(d)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_device(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "Device not implemented yet"}]
})))
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.devices.list")?;
let device = crate::entity::patient_devices::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Device not found".into()))?;
if device.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::patient_device_to_fhir(&device)))
}
// ── Practitioner ───────────────────────────────────────────────────────
pub async fn search_practitioners(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.doctor.list")?;
let mut query = crate::entity::doctor_profile::Entity::find()
.filter(crate::entity::doctor_profile::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::doctor_profile::Column::DeletedAt.is_null());
if let Some(ref name) = params.name {
query = query.filter(crate::entity::doctor_profile::Column::Name.contains(name));
}
let limit = params.count.unwrap_or(50).min(200);
let doctors = query.limit(limit).all(&state.db).await?;
let entries: Vec<serde_json::Value> = doctors.iter()
.map(|d| serde_json::json!({"resource": converter::doctor_to_fhir(d)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_practitioner(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "Practitioner not implemented yet"}]
})))
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.doctor.list")?;
let doctor = crate::entity::doctor_profile::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Practitioner not found".into()))?;
if doctor.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::doctor_to_fhir(&doctor)))
}
// ── Appointment ────────────────────────────────────────────────────────
pub async fn search_appointments(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.appointment.list")?;
let mut query = crate::entity::appointment::Entity::find()
.filter(crate::entity::appointment::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::appointment::Column::DeletedAt.is_null());
if let Some(ref patient_id) = params.patient {
let uid = Uuid::parse_str(patient_id)
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::appointment::Column::PatientId.eq(uid));
}
if let Some(ref status) = params.status {
query = query.filter(crate::entity::appointment::Column::Status.eq(status));
}
let limit = params.count.unwrap_or(50).min(200);
let appointments = query
.order_by_desc(crate::entity::appointment::Column::AppointmentDate)
.limit(limit)
.all(&state.db)
.await?;
let entries: Vec<serde_json::Value> = appointments.iter()
.map(|a| serde_json::json!({"resource": converter::appointment_to_fhir(a)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_appointment(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "Appointment not implemented yet"}]
})))
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.appointment.list")?;
let appointment = crate::entity::appointment::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Appointment not found".into()))?;
if appointment.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::appointment_to_fhir(&appointment)))
}
// ── DiagnosticReport ───────────────────────────────────────────────────
pub async fn search_diagnostic_reports(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.health-data.list")?;
let mut query = crate::entity::lab_report::Entity::find()
.filter(crate::entity::lab_report::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::lab_report::Column::DeletedAt.is_null());
if let Some(ref patient_id) = params.patient {
let uid = Uuid::parse_str(patient_id)
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::lab_report::Column::PatientId.eq(uid));
}
if let Some(ref code) = params.code {
query = query.filter(crate::entity::lab_report::Column::ReportType.eq(code));
}
if let Some(ref status) = params.status {
query = query.filter(crate::entity::lab_report::Column::Status.eq(status));
}
let limit = params.count.unwrap_or(50).min(200);
let reports = query
.order_by_desc(crate::entity::lab_report::Column::ReportDate)
.limit(limit)
.all(&state.db)
.await?;
let entries: Vec<serde_json::Value> = reports.iter()
.map(|r| serde_json::json!({"resource": converter::lab_report_to_fhir(r)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_diagnostic_report(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "DiagnosticReport not implemented yet"}]
})))
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.health-data.list")?;
let report = crate::entity::lab_report::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("DiagnosticReport not found".into()))?;
if report.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::lab_report_to_fhir(&report)))
}
// ── Encounter ──────────────────────────────────────────────────────────
pub async fn search_encounters(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.consultation.list")?;
let mut query = crate::entity::consultation_session::Entity::find()
.filter(crate::entity::consultation_session::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::consultation_session::Column::DeletedAt.is_null());
if let Some(ref patient_id) = params.patient {
let uid = Uuid::parse_str(patient_id)
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::consultation_session::Column::PatientId.eq(uid));
}
if let Some(ref status) = params.status {
query = query.filter(crate::entity::consultation_session::Column::Status.eq(status));
}
let limit = params.count.unwrap_or(50).min(200);
let sessions = query
.order_by_desc(crate::entity::consultation_session::Column::CreatedAt)
.limit(limit)
.all(&state.db)
.await?;
let entries: Vec<serde_json::Value> = sessions.iter()
.map(|s| serde_json::json!({"resource": converter::consultation_to_fhir(s)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_encounter(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "Encounter not implemented yet"}]
})))
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.consultation.list")?;
let session = crate::entity::consultation_session::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Encounter not found".into()))?;
if session.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::consultation_to_fhir(&session)))
}
// ── Task ───────────────────────────────────────────────────────────────
pub async fn search_tasks(
State(_state): State<HealthState>,
Query(_params): Query<SearchParams>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<FhirSearchParams>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.follow-up.list")?;
let mut query = crate::entity::follow_up_task::Entity::find()
.filter(crate::entity::follow_up_task::Column::TenantId.eq(ctx.tenant_id))
.filter(crate::entity::follow_up_task::Column::DeletedAt.is_null());
if let Some(ref patient_id) = params.patient {
let uid = Uuid::parse_str(patient_id)
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
query = query.filter(crate::entity::follow_up_task::Column::PatientId.eq(uid));
}
if let Some(ref status) = params.status {
query = query.filter(crate::entity::follow_up_task::Column::Status.eq(status));
}
let limit = params.count.unwrap_or(50).min(200);
let tasks = query
.order_by_desc(crate::entity::follow_up_task::Column::PlannedDate)
.limit(limit)
.all(&state.db)
.await?;
let entries: Vec<serde_json::Value> = tasks.iter()
.map(|t| serde_json::json!({"resource": converter::follow_up_to_fhir(t)}))
.collect();
Ok(Json(serde_json::json!({
"resourceType": "Bundle", "type": "searchset", "total": 0, "entry": []
"resourceType": "Bundle",
"type": "searchset",
"total": entries.len(),
"entry": entries,
})))
}
pub async fn get_task(
State(_state): State<HealthState>,
Path(_id): Path<Uuid>,
) -> Result<impl IntoResponse, erp_core::error::AppError> {
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.follow-up.list")?;
let task = crate::entity::follow_up_task::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Task not found".into()))?;
if task.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
Ok(Json(converter::follow_up_to_fhir(&task)))
}
// ── $everything ────────────────────────────────────────────────────────
/// GET /fhir/R4/Patient/{id}/$everything
pub async fn patient_everything(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
require_permission(&ctx, "health.patient.list")?;
let patient = crate::entity::patient::Entity::find_by_id(id)
.one(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Patient not found".into()))?;
if patient.tenant_id != ctx.tenant_id {
return Err(AppError::Forbidden("Access denied".into()));
}
let mut entries = Vec::new();
// 1. Patient
entries.push(serde_json::json!({
"resource": converter::patient_to_fhir(&patient),
"fullUrl": format!("https://hms.local/fhir/R4/Patient/{}", id),
}));
// 2. Observations设备读数
let readings = crate::entity::device_readings::Entity::find()
.filter(crate::entity::device_readings::Column::PatientId.eq(id))
.filter(crate::entity::device_readings::Column::DeletedAt.is_null())
.limit(200)
.all(&state.db)
.await?;
for r in &readings {
for obs in converter::device_reading_to_fhir_observations(r) {
entries.push(serde_json::json!({"resource": obs}));
}
}
// 3. Devices
let devices = crate::entity::patient_devices::Entity::find()
.filter(crate::entity::patient_devices::Column::PatientId.eq(id))
.filter(crate::entity::patient_devices::Column::DeletedAt.is_null())
.all(&state.db)
.await?;
for d in &devices {
entries.push(serde_json::json!({"resource": converter::patient_device_to_fhir(d)}));
}
// 4. Encounters咨询会话
let consultations = crate::entity::consultation_session::Entity::find()
.filter(crate::entity::consultation_session::Column::PatientId.eq(id))
.filter(crate::entity::consultation_session::Column::DeletedAt.is_null())
.all(&state.db)
.await?;
for c in &consultations {
entries.push(serde_json::json!({"resource": converter::consultation_to_fhir(c)}));
}
// 5. Appointments
let appointments = crate::entity::appointment::Entity::find()
.filter(crate::entity::appointment::Column::PatientId.eq(id))
.filter(crate::entity::appointment::Column::DeletedAt.is_null())
.all(&state.db)
.await?;
for a in &appointments {
entries.push(serde_json::json!({"resource": converter::appointment_to_fhir(a)}));
}
// 6. Tasks随访任务
let tasks = crate::entity::follow_up_task::Entity::find()
.filter(crate::entity::follow_up_task::Column::PatientId.eq(id))
.filter(crate::entity::follow_up_task::Column::DeletedAt.is_null())
.limit(50)
.all(&state.db)
.await?;
for t in &tasks {
entries.push(serde_json::json!({"resource": converter::follow_up_to_fhir(t)}));
}
// 7. DiagnosticReports化验报告
let reports = crate::entity::lab_report::Entity::find()
.filter(crate::entity::lab_report::Column::PatientId.eq(id))
.filter(crate::entity::lab_report::Column::DeletedAt.is_null())
.limit(50)
.all(&state.db)
.await?;
for r in &reports {
entries.push(serde_json::json!({"resource": converter::lab_report_to_fhir(r)}));
}
Ok(Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{"severity": "information", "code": "not-found", "diagnostics": "Task not implemented yet"}]
"resourceType": "Bundle",
"type": "collection",
"total": entries.len(),
"entry": entries,
})))
}