feat(health): 家庭成员健康代理 — 同意追踪 + 健康摘要查看
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

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:
iven
2026-05-04 20:57:24 +08:00
parent 0a9272bcf6
commit 95fa09c383
11 changed files with 675 additions and 1 deletions

View File

@@ -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>,
}

View File

@@ -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")]

View File

@@ -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()),
}
}

View 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)))
}

View File

@@ -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;

View File

@@ -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(),
},
]
}

View 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,
})))
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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),
]
}
}

View File

@@ -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(())
}
}