feat(health): 家庭成员健康代理 — 同意追踪 + 健康摘要查看
Phase 1 Care Engine MVP 最后一项 (#8): - 迁移: patient_family_member 表新增 user_id/consent_status/access_level/consented_at/consent_revoked_at - 实体: 更新 patient_family_member Model 含新字段 - DTO: FamilyMemberResp 扩展 + 新增 GrantFamilyAccessReq/FamilyPatientSummaryResp/FamilyHealthSummaryResp - Service: 授权/撤销访问、家庭成员查看关联患者列表、查看健康摘要(按 access_level 分级) - Handler: 5 个端点(grant/revoke/list/summary/link-user) - 路由: /health/patients/{id}/family-members/{fid}/grant-access 等 - 权限: health.family-proxy.list/manage - 已有 CRUD 适配新字段(list/create/update 返回 consent 状态)
This commit is contained in:
@@ -112,6 +112,10 @@ pub struct FamilyMemberResp {
|
||||
pub phone: Option<String>,
|
||||
pub birth_date: Option<NaiveDate>,
|
||||
pub notes: Option<String>,
|
||||
pub user_id: Option<Uuid>,
|
||||
pub consent_status: String,
|
||||
pub access_level: String,
|
||||
pub consented_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub version: i32,
|
||||
@@ -138,3 +142,36 @@ pub struct TagResp {
|
||||
pub color: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 家庭成员健康代理 DTO
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 患者授权家庭成员查看健康数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct GrantFamilyAccessReq {
|
||||
pub access_level: String,
|
||||
}
|
||||
|
||||
/// 家庭成员查看关联患者列表
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FamilyPatientSummaryResp {
|
||||
pub family_member_id: Uuid,
|
||||
pub patient_id: Uuid,
|
||||
pub patient_name: String,
|
||||
pub relationship: String,
|
||||
pub consent_status: String,
|
||||
pub access_level: String,
|
||||
pub consented_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
/// 家庭成员查看的患者健康摘要
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct FamilyHealthSummaryResp {
|
||||
pub patient_id: Uuid,
|
||||
pub patient_name: String,
|
||||
pub latest_vital_signs: Option<serde_json::Value>,
|
||||
pub active_care_plan: Option<serde_json::Value>,
|
||||
pub recent_alerts_count: i64,
|
||||
pub next_appointment: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,17 @@ pub struct Model {
|
||||
pub birth_date: Option<chrono::NaiveDate>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub notes: Option<String>,
|
||||
/// 关联系统用户(家庭成员绑定账号后可登录查看)
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<Uuid>,
|
||||
/// none / pending / granted / revoked
|
||||
pub consent_status: String,
|
||||
/// none / summary / full / limited
|
||||
pub access_level: String,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub consented_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub consent_revoked_at: Option<DateTimeUtc>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -109,6 +109,9 @@ pub enum HealthError {
|
||||
|
||||
#[error("数据库操作失败: {0}")]
|
||||
DbError(String),
|
||||
|
||||
#[error("权限不足: {0}")]
|
||||
Forbidden(String),
|
||||
}
|
||||
|
||||
impl From<HealthError> for AppError {
|
||||
@@ -149,6 +152,7 @@ impl From<HealthError> for AppError {
|
||||
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
||||
HealthError::InvalidStatusTransition(s) => AppError::Validation(s),
|
||||
HealthError::VersionMismatch => AppError::VersionMismatch,
|
||||
HealthError::Forbidden(msg) => AppError::Forbidden(msg),
|
||||
HealthError::DbError(_) => AppError::Internal(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
84
crates/erp-health/src/handler/family_proxy_handler.rs
Normal file
84
crates/erp-health/src/handler/family_proxy_handler.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! 家庭成员健康代理 Handler — 同意管理 + 健康摘要查看
|
||||
|
||||
use axum::extract::{Json, Path, Query, State};
|
||||
use axum::Extension;
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::patient_dto::*;
|
||||
use crate::service::family_proxy_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct VersionQuery {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
/// 授权家庭成员访问健康数据
|
||||
pub async fn grant_family_access(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((patient_id, family_member_id)): Path<(Uuid, Uuid)>,
|
||||
Query(params): Query<VersionQuery>,
|
||||
Json(req): Json<GrantFamilyAccessReq>,
|
||||
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
let result = family_proxy_service::grant_family_access(
|
||||
&state, ctx.tenant_id, patient_id, family_member_id,
|
||||
Some(ctx.user_id), req, params.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
/// 撤销家庭成员健康数据访问
|
||||
pub async fn revoke_family_access(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((patient_id, family_member_id)): Path<(Uuid, Uuid)>,
|
||||
Query(params): Query<VersionQuery>,
|
||||
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
let result = family_proxy_service::revoke_family_access(
|
||||
&state, ctx.tenant_id, patient_id, family_member_id,
|
||||
Some(ctx.user_id), params.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
/// 家庭成员查看关联患者列表
|
||||
pub async fn list_my_family_patients(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
) -> Result<Json<ApiResponse<Vec<FamilyPatientSummaryResp>>>, AppError> {
|
||||
let result = family_proxy_service::list_family_patients(
|
||||
&state, ctx.tenant_id, ctx.user_id,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
/// 家庭成员查看患者健康摘要
|
||||
pub async fn get_family_health_summary(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(patient_id): Path<Uuid>,
|
||||
) -> Result<Json<ApiResponse<FamilyHealthSummaryResp>>, AppError> {
|
||||
let result = family_proxy_service::get_family_health_summary(
|
||||
&state, ctx.tenant_id, ctx.user_id, patient_id,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
/// 绑定家庭成员到系统用户(小程序扫码验证后调用)
|
||||
pub async fn link_family_member_user(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(family_member_id): Path<Uuid>,
|
||||
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
||||
let result = family_proxy_service::link_family_member_user(
|
||||
&state, ctx.tenant_id, family_member_id, ctx.user_id,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub mod daily_monitoring_handler;
|
||||
pub mod device_handler;
|
||||
pub mod device_reading_handler;
|
||||
pub mod diagnosis_handler;
|
||||
pub mod family_proxy_handler;
|
||||
pub mod medication_record_handler;
|
||||
pub mod medication_reminder_handler;
|
||||
pub mod doctor_handler;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::handler::{
|
||||
action_inbox_handler,
|
||||
alert_handler, alert_rule_handler,
|
||||
appointment_handler, article_category_handler, article_handler, article_tag_handler,
|
||||
ble_gateway_handler, care_plan_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_handler, device_reading_handler, diagnosis_handler, doctor_handler, follow_up_handler, follow_up_template_handler,
|
||||
ble_gateway_handler, care_plan_handler, consultation_handler, consent_handler, critical_alert_handler, critical_value_threshold_handler, daily_monitoring_handler, device_handler, device_reading_handler, diagnosis_handler, doctor_handler, family_proxy_handler, follow_up_handler, follow_up_template_handler,
|
||||
health_data_handler, medication_record_handler, medication_reminder_handler, patient_handler, points_handler, shift_handler, stats_handler,
|
||||
vital_signs_daily_handler,
|
||||
};
|
||||
@@ -233,6 +233,28 @@ impl HealthModule {
|
||||
"/health/patients/{id}/doctors",
|
||||
axum::routing::post(patient_handler::assign_doctor),
|
||||
)
|
||||
// 家庭成员健康代理 — 管理端
|
||||
.route(
|
||||
"/health/patients/{patient_id}/family-members/{family_member_id}/grant-access",
|
||||
axum::routing::post(family_proxy_handler::grant_family_access),
|
||||
)
|
||||
.route(
|
||||
"/health/patients/{patient_id}/family-members/{family_member_id}/revoke-access",
|
||||
axum::routing::put(family_proxy_handler::revoke_family_access),
|
||||
)
|
||||
// 家庭成员健康代理 — 患者端(小程序)
|
||||
.route(
|
||||
"/health/family/patients",
|
||||
axum::routing::get(family_proxy_handler::list_my_family_patients),
|
||||
)
|
||||
.route(
|
||||
"/health/family/patients/{patient_id}/health-summary",
|
||||
axum::routing::get(family_proxy_handler::get_family_health_summary),
|
||||
)
|
||||
.route(
|
||||
"/health/family/members/{family_member_id}/link-user",
|
||||
axum::routing::post(family_proxy_handler::link_family_member_user),
|
||||
)
|
||||
.route(
|
||||
"/health/patients/{id}/doctors/{did}",
|
||||
axum::routing::delete(patient_handler::remove_doctor),
|
||||
@@ -1384,6 +1406,19 @@ impl ErpModule for HealthModule {
|
||||
description: "注册/编辑/删除 BLE 网关、管理患者绑定".into(),
|
||||
module: "health".into(),
|
||||
},
|
||||
// 家庭成员健康代理
|
||||
PermissionDescriptor {
|
||||
code: "health.family-proxy.list".into(),
|
||||
name: "查看家庭健康代理".into(),
|
||||
description: "家庭成员查看关联患者列表和健康摘要".into(),
|
||||
module: "health".into(),
|
||||
},
|
||||
PermissionDescriptor {
|
||||
code: "health.family-proxy.manage".into(),
|
||||
name: "管理家庭健康代理".into(),
|
||||
description: "授权/撤销家庭成员健康数据访问".into(),
|
||||
module: "health".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
362
crates/erp-health/src/service/family_proxy_service.rs
Normal file
362
crates/erp-health/src/service/family_proxy_service.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
//! 家庭成员健康代理 Service — 同意追踪 + 健康摘要查看
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::patient_dto::*;
|
||||
use crate::entity::patient_family_member;
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
|
||||
/// 授权家庭成员访问健康数据
|
||||
pub async fn grant_family_access(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
family_member_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: GrantFamilyAccessReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<FamilyMemberResp> {
|
||||
validate_access_level(&req.access_level)?;
|
||||
|
||||
let model = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::Id.eq(family_member_id))
|
||||
.filter(patient_family_member::Column::PatientId.eq(patient_id))
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::FamilyMemberNotFound)?;
|
||||
|
||||
let next_ver = erp_core::error::check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: patient_family_member::ActiveModel = model.into();
|
||||
active.consent_status = Set("granted".into());
|
||||
active.access_level = Set(req.access_level);
|
||||
active.consented_at = Set(Some(Utc::now()));
|
||||
active.consent_revoked_at = Set(None);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let updated = active.update(&state.db).await?;
|
||||
|
||||
tracing::info!(
|
||||
family_member_id = %updated.id,
|
||||
patient_id = %patient_id,
|
||||
access_level = %updated.access_level,
|
||||
"家庭成员健康访问已授权"
|
||||
);
|
||||
|
||||
Ok(to_family_member_resp(&state, updated))
|
||||
}
|
||||
|
||||
/// 撤销家庭成员健康数据访问
|
||||
pub async fn revoke_family_access(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
family_member_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<FamilyMemberResp> {
|
||||
let model = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::Id.eq(family_member_id))
|
||||
.filter(patient_family_member::Column::PatientId.eq(patient_id))
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::FamilyMemberNotFound)?;
|
||||
|
||||
let next_ver = erp_core::error::check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: patient_family_member::ActiveModel = model.into();
|
||||
active.consent_status = Set("revoked".into());
|
||||
active.access_level = Set("none".into());
|
||||
active.consent_revoked_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let updated = active.update(&state.db).await?;
|
||||
|
||||
tracing::info!(
|
||||
family_member_id = %updated.id,
|
||||
patient_id = %patient_id,
|
||||
"家庭成员健康访问已撤销"
|
||||
);
|
||||
|
||||
Ok(to_family_member_resp(&state, updated))
|
||||
}
|
||||
|
||||
/// 家庭成员查看关联患者列表(通过 user_id 关联)
|
||||
pub async fn list_family_patients(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> HealthResult<Vec<FamilyPatientSummaryResp>> {
|
||||
let links = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::UserId.eq(user_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.filter(patient_family_member::Column::ConsentStatus.eq("granted"))
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut result = Vec::with_capacity(links.len());
|
||||
for link in links {
|
||||
let patient = match crate::entity::patient::Entity::find_by_id(link.patient_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
{
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
result.push(FamilyPatientSummaryResp {
|
||||
family_member_id: link.id,
|
||||
patient_id: link.patient_id,
|
||||
patient_name: patient.name,
|
||||
relationship: link.relationship,
|
||||
consent_status: link.consent_status,
|
||||
access_level: link.access_level,
|
||||
consented_at: link.consented_at,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 家庭成员查看患者健康摘要
|
||||
pub async fn get_family_health_summary(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
) -> HealthResult<FamilyHealthSummaryResp> {
|
||||
// 验证家庭成员有访问权限
|
||||
let link = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::PatientId.eq(patient_id))
|
||||
.filter(patient_family_member::Column::UserId.eq(user_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.filter(patient_family_member::Column::ConsentStatus.eq("granted"))
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::Forbidden("无权查看该患者健康数据".into()))?;
|
||||
|
||||
let patient = crate::entity::patient::Entity::find_by_id(patient_id)
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PatientNotFound)?;
|
||||
|
||||
let access = link.access_level.as_str();
|
||||
|
||||
// 根据访问级别构建摘要
|
||||
let latest_vital_signs = if matches!(access, "summary" | "full" | "limited") {
|
||||
get_latest_vital_signs_summary(&state.db, tenant_id, patient_id).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let active_care_plan = if matches!(access, "full" | "summary") {
|
||||
get_active_care_plan_summary(&state.db, tenant_id, patient_id).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let recent_alerts_count = if matches!(access, "full" | "limited") {
|
||||
count_recent_alerts(&state.db, tenant_id, patient_id).await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let next_appointment = if matches!(access, "summary" | "full") {
|
||||
get_next_appointment_summary(&state.db, tenant_id, patient_id).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(FamilyHealthSummaryResp {
|
||||
patient_id,
|
||||
patient_name: patient.name,
|
||||
latest_vital_signs,
|
||||
active_care_plan,
|
||||
recent_alerts_count,
|
||||
next_appointment,
|
||||
})
|
||||
}
|
||||
|
||||
/// 绑定家庭成员到系统用户(扫码/短信验证后调用)
|
||||
pub async fn link_family_member_user(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
family_member_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> HealthResult<FamilyMemberResp> {
|
||||
let model = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::Id.eq(family_member_id))
|
||||
.filter(patient_family_member::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_family_member::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::FamilyMemberNotFound)?;
|
||||
|
||||
let mut active: patient_family_member::ActiveModel = model.into();
|
||||
active.user_id = Set(Some(user_id));
|
||||
active.updated_at = Set(Utc::now());
|
||||
|
||||
let updated = active.update(&state.db).await?;
|
||||
|
||||
tracing::info!(
|
||||
family_member_id = %updated.id,
|
||||
user_id = %user_id,
|
||||
"家庭成员已绑定系统用户"
|
||||
);
|
||||
|
||||
Ok(to_family_member_resp(&state, updated))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 私有辅助函数
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn validate_access_level(level: &str) -> HealthResult<()> {
|
||||
match level {
|
||||
"summary" | "full" | "limited" => Ok(()),
|
||||
_ => Err(HealthError::Validation(format!(
|
||||
"无效的访问级别: {},允许值: summary, full, limited", level
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_family_member_resp(
|
||||
state: &HealthState,
|
||||
m: patient_family_member::Model,
|
||||
) -> FamilyMemberResp {
|
||||
let kek = state.crypto.kek();
|
||||
let decrypted_phone = m.phone.as_ref()
|
||||
.map(|p| erp_core::crypto::decrypt(kek, p).unwrap_or_else(|_| p.clone()));
|
||||
|
||||
FamilyMemberResp {
|
||||
id: m.id,
|
||||
patient_id: m.patient_id,
|
||||
name: m.name,
|
||||
relationship: m.relationship,
|
||||
phone: decrypted_phone,
|
||||
birth_date: m.birth_date,
|
||||
notes: m.notes,
|
||||
user_id: m.user_id,
|
||||
consent_status: m.consent_status,
|
||||
access_level: m.access_level,
|
||||
consented_at: m.consented_at,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_latest_vital_signs_summary(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
) -> HealthResult<Option<serde_json::Value>> {
|
||||
use crate::entity::vital_signs;
|
||||
|
||||
let latest = vital_signs::Entity::find()
|
||||
.filter(vital_signs::Column::TenantId.eq(tenant_id))
|
||||
.filter(vital_signs::Column::PatientId.eq(patient_id))
|
||||
.filter(vital_signs::Column::DeletedAt.is_null())
|
||||
.order_by_desc(vital_signs::Column::RecordDate)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(latest.map(|v| serde_json::json!({
|
||||
"record_date": v.record_date,
|
||||
"systolic_bp_morning": v.systolic_bp_morning,
|
||||
"diastolic_bp_morning": v.diastolic_bp_morning,
|
||||
"systolic_bp_evening": v.systolic_bp_evening,
|
||||
"diastolic_bp_evening": v.diastolic_bp_evening,
|
||||
"heart_rate": v.heart_rate,
|
||||
"blood_sugar": v.blood_sugar,
|
||||
"spo2": v.spo2,
|
||||
"body_temperature": v.body_temperature,
|
||||
"weight": v.weight,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn get_active_care_plan_summary(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
) -> HealthResult<Option<serde_json::Value>> {
|
||||
use crate::entity::care_plan;
|
||||
|
||||
let plan = care_plan::Entity::find()
|
||||
.filter(care_plan::Column::TenantId.eq(tenant_id))
|
||||
.filter(care_plan::Column::PatientId.eq(patient_id))
|
||||
.filter(care_plan::Column::Status.eq("active"))
|
||||
.filter(care_plan::Column::DeletedAt.is_null())
|
||||
.order_by_desc(care_plan::Column::CreatedAt)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(plan.map(|p| serde_json::json!({
|
||||
"id": p.id,
|
||||
"title": p.title,
|
||||
"status": p.status,
|
||||
"start_date": p.start_date,
|
||||
"end_date": p.end_date,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn count_recent_alerts(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
) -> HealthResult<i64> {
|
||||
use crate::entity::alerts;
|
||||
|
||||
let count = alerts::Entity::find()
|
||||
.filter(alerts::Column::TenantId.eq(tenant_id))
|
||||
.filter(alerts::Column::PatientId.eq(patient_id))
|
||||
.filter(alerts::Column::Status.eq("active"))
|
||||
.count(db)
|
||||
.await?;
|
||||
|
||||
Ok(count as i64)
|
||||
}
|
||||
|
||||
async fn get_next_appointment_summary(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
patient_id: Uuid,
|
||||
) -> HealthResult<Option<serde_json::Value>> {
|
||||
use crate::entity::appointment;
|
||||
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let apt = appointment::Entity::find()
|
||||
.filter(appointment::Column::TenantId.eq(tenant_id))
|
||||
.filter(appointment::Column::PatientId.eq(patient_id))
|
||||
.filter(appointment::Column::Status.eq("confirmed"))
|
||||
.filter(appointment::Column::AppointmentDate.gte(today))
|
||||
.filter(appointment::Column::DeletedAt.is_null())
|
||||
.order_by_asc(appointment::Column::AppointmentDate)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(apt.map(|a| serde_json::json!({
|
||||
"id": a.id,
|
||||
"appointment_date": a.appointment_date,
|
||||
"start_time": a.start_time,
|
||||
"end_time": a.end_time,
|
||||
"type": a.appointment_type,
|
||||
})))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub mod daily_monitoring_service;
|
||||
pub mod device_reading_service;
|
||||
pub mod device_service;
|
||||
pub mod diagnosis_service;
|
||||
pub mod family_proxy_service;
|
||||
pub mod medication_record_service;
|
||||
pub mod medication_reminder_service;
|
||||
pub mod doctor_service;
|
||||
|
||||
@@ -194,6 +194,10 @@ pub async fn list_family_members(
|
||||
phone,
|
||||
birth_date: m.birth_date,
|
||||
notes: m.notes,
|
||||
user_id: m.user_id,
|
||||
consent_status: m.consent_status,
|
||||
access_level: m.access_level,
|
||||
consented_at: m.consented_at,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
@@ -235,6 +239,11 @@ pub async fn create_family_member(
|
||||
phone_hash: Set(phone_hash),
|
||||
birth_date: Set(req.birth_date),
|
||||
notes: Set(req.notes),
|
||||
user_id: Set(None),
|
||||
consent_status: Set("none".into()),
|
||||
access_level: Set("none".into()),
|
||||
consented_at: Set(None),
|
||||
consent_revoked_at: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
@@ -263,6 +272,10 @@ pub async fn create_family_member(
|
||||
phone: decrypted_phone,
|
||||
birth_date: model.birth_date,
|
||||
notes: model.notes,
|
||||
user_id: model.user_id,
|
||||
consent_status: model.consent_status,
|
||||
access_level: model.access_level,
|
||||
consented_at: model.consented_at,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
version: model.version,
|
||||
@@ -347,6 +360,10 @@ pub async fn update_family_member(
|
||||
phone: decrypted_phone,
|
||||
birth_date: updated.birth_date,
|
||||
notes: updated.notes,
|
||||
user_id: updated.user_id,
|
||||
consent_status: updated.consent_status,
|
||||
access_level: updated.access_level,
|
||||
consented_at: updated.consented_at,
|
||||
created_at: updated.created_at,
|
||||
updated_at: updated.updated_at,
|
||||
version: updated.version,
|
||||
|
||||
@@ -114,6 +114,7 @@ mod m20260505_000111_create_care_plan;
|
||||
mod m20260505_000112_create_shift_management;
|
||||
mod m20260505_000113_create_ble_gateways;
|
||||
mod m20260505_000114_dialysis_record_add_workflow_instance;
|
||||
mod m20260505_000115_family_member_health_proxy;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -235,6 +236,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260505_000112_create_shift_management::Migration),
|
||||
Box::new(m20260505_000113_create_ble_gateways::Migration),
|
||||
Box::new(m20260505_000114_dialysis_record_add_workflow_instance::Migration),
|
||||
Box::new(m20260505_000115_family_member_health_proxy::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("patient_family_member"))
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("user_id"))
|
||||
.uuid()
|
||||
.null()
|
||||
.to_owned(),
|
||||
)
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("consent_status"))
|
||||
.string()
|
||||
.not_null()
|
||||
.default("none")
|
||||
.to_owned(),
|
||||
)
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("access_level"))
|
||||
.string()
|
||||
.not_null()
|
||||
.default("none")
|
||||
.to_owned(),
|
||||
)
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("consented_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.null()
|
||||
.to_owned(),
|
||||
)
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("consent_revoked_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.null()
|
||||
.to_owned(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_family_member_user_id")
|
||||
.table(Alias::new("patient_family_member"))
|
||||
.col(Alias::new("user_id"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_family_member_consent_status")
|
||||
.table(Alias::new("patient_family_member"))
|
||||
.col(Alias::new("consent_status"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_family_member_user")
|
||||
.from(Alias::new("patient_family_member"), Alias::new("user_id"))
|
||||
.to(Alias::new("users"), Alias::new("id"))
|
||||
.on_delete(ForeignKeyAction::SetNull)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_foreign_key(
|
||||
ForeignKey::drop()
|
||||
.name("fk_family_member_user")
|
||||
.table(Alias::new("patient_family_member"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_family_member_consent_status")
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(Index::drop().name("idx_family_member_user_id").to_owned())
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("patient_family_member"))
|
||||
.drop_column(Alias::new("consent_revoked_at"))
|
||||
.drop_column(Alias::new("consented_at"))
|
||||
.drop_column(Alias::new("access_level"))
|
||||
.drop_column(Alias::new("consent_status"))
|
||||
.drop_column(Alias::new("user_id"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user