fix(health): 精准审计修复 6 个真实问题 — 安全/一致性/性能
P0: consultation handler sender_role 从请求体移除,改为服务端推导(防伪造) P1: 所有软删除操作统一使用 check_version 乐观锁(6个函数) P1: 修复 health_trend 索引缺少 tenant_id 前导列 + follow_up_record 补 (tenant_id, executed_date) 索引 P2: Decimal->f64 使用 ToPrimitive::to_f64 替代脆弱的 to_string().parse() P2: 预约取消释放槽位+状态更新包裹进同一事务
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1281,6 +1281,7 @@ dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"erp-core",
|
||||
"num-traits",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -17,3 +17,4 @@ thiserror.workspace = true
|
||||
validator.workspace = true
|
||||
utoipa.workspace = true
|
||||
async-trait.workspace = true
|
||||
num-traits = "0.2.19"
|
||||
|
||||
@@ -35,7 +35,6 @@ pub struct CloseSessionReq {
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateConsultationMessageReq {
|
||||
pub session_id: Uuid,
|
||||
pub sender_role: String,
|
||||
pub content_type: Option<String>,
|
||||
pub content: String,
|
||||
}
|
||||
@@ -135,7 +134,7 @@ where
|
||||
let msg_req = CreateMessageReq {
|
||||
session_id: req.session_id,
|
||||
sender_id: ctx.user_id,
|
||||
sender_role: req.sender_role,
|
||||
sender_role: "doctor".to_string(),
|
||||
content_type: req.content_type,
|
||||
content: req.content,
|
||||
};
|
||||
|
||||
@@ -28,6 +28,11 @@ pub struct UpdateDoctorWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
pub async fn list_doctors<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -100,12 +105,13 @@ pub async fn delete_doctor<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.doctor.manage")?;
|
||||
doctor_service::delete_doctor(&state, ctx.tenant_id, id, Some(ctx.user_id)).await?;
|
||||
doctor_service::delete_doctor(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ pub struct UpdateFollowUpTaskWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
pub async fn list_tasks<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -95,13 +100,14 @@ pub async fn delete_task<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.follow-up.manage")?;
|
||||
follow_up_service::delete_task(&state, ctx.tenant_id, id, Some(ctx.user_id)).await?;
|
||||
follow_up_service::delete_task(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ pub struct GenerateTrendReq {
|
||||
pub period_end: chrono::NaiveDate,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateWithVersion<T> {
|
||||
pub data: T,
|
||||
@@ -104,13 +109,14 @@ pub async fn delete_vital_signs<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((_patient_id, vid)): Path<(Uuid, Uuid)>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
health_data_service::delete_vital_signs(&state, ctx.tenant_id, vid, Some(ctx.user_id)).await?;
|
||||
health_data_service::delete_vital_signs(&state, ctx.tenant_id, vid, Some(ctx.user_id), req.version).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -178,13 +184,14 @@ pub async fn delete_lab_report<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((_patient_id, rid)): Path<(Uuid, Uuid)>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
health_data_service::delete_lab_report(&state, ctx.tenant_id, rid, Some(ctx.user_id)).await?;
|
||||
health_data_service::delete_lab_report(&state, ctx.tenant_id, rid, Some(ctx.user_id), req.version).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -252,13 +259,14 @@ pub async fn delete_health_record<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((_patient_id, rid)): Path<(Uuid, Uuid)>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
health_data_service::delete_health_record(&state, ctx.tenant_id, rid, Some(ctx.user_id)).await?;
|
||||
health_data_service::delete_health_record(&state, ctx.tenant_id, rid, Some(ctx.user_id), req.version).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ pub struct AssignDoctorReq {
|
||||
pub relationship_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteWithVersion {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
pub async fn list_patients<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -118,13 +123,14 @@ pub async fn delete_patient<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::delete_patient(&state, ctx.tenant_id, id, Some(ctx.user_id)).await?;
|
||||
patient_service::delete_patient(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -219,6 +225,7 @@ pub async fn delete_family_member<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((patient_id, member_id)): Path<(Uuid, Uuid)>,
|
||||
Json(req): Json<DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
@@ -226,7 +233,7 @@ where
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
patient_service::delete_family_member(
|
||||
&state, ctx.tenant_id, patient_id, member_id, Some(ctx.user_id),
|
||||
&state, ctx.tenant_id, patient_id, member_id, Some(ctx.user_id), req.version,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
|
||||
@@ -184,6 +184,8 @@ pub async fn update_appointment_status(
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let txn = state.db.begin().await?;
|
||||
|
||||
// 取消时释放排班名额(带下限保护)
|
||||
if req.status == "cancelled" {
|
||||
if let Some(did) = model.doctor_id {
|
||||
@@ -198,7 +200,7 @@ pub async fn update_appointment_status(
|
||||
.filter(doctor_schedule::Column::ScheduleDate.eq(model.appointment_date))
|
||||
.filter(doctor_schedule::Column::DeletedAt.is_null())
|
||||
.filter(Expr::col(doctor_schedule::Column::CurrentAppointments).gt(0))
|
||||
.exec(&state.db)
|
||||
.exec(&txn)
|
||||
.await;
|
||||
if let Err(e) = release_result {
|
||||
tracing::error!(error = %e, "取消预约时释放排班名额失败");
|
||||
@@ -213,7 +215,9 @@ pub async fn update_appointment_status(
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
|
||||
let m = active.update(&state.db).await?;
|
||||
let m = active.update(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
let event_type = format!("appointment.{}", m.status);
|
||||
let event = DomainEvent::new(
|
||||
|
||||
@@ -152,14 +152,17 @@ pub async fn delete_doctor(
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = find_doctor(&state.db, tenant_id, id).await?;
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: doctor_profile::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -164,6 +164,7 @@ pub async fn delete_task(
|
||||
tenant_id: Uuid,
|
||||
task_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = follow_up_task::Entity::find()
|
||||
.filter(follow_up_task::Column::Id.eq(task_id))
|
||||
@@ -173,11 +174,14 @@ pub async fn delete_task(
|
||||
.await?
|
||||
.ok_or(HealthError::FollowUpTaskNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: follow_up_task::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use chrono::Utc;
|
||||
use erp_core::events::DomainEvent;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
@@ -52,8 +53,8 @@ pub async fn list_vital_signs(
|
||||
systolic_bp_evening: m.systolic_bp_evening,
|
||||
diastolic_bp_evening: m.diastolic_bp_evening,
|
||||
heart_rate: m.heart_rate,
|
||||
weight: m.weight.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
blood_sugar: m.blood_sugar.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
weight: m.weight.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
blood_sugar: m.blood_sugar.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
water_intake_ml: m.water_intake_ml,
|
||||
urine_output_ml: m.urine_output_ml,
|
||||
notes: m.notes,
|
||||
@@ -110,8 +111,8 @@ pub async fn create_vital_signs(
|
||||
systolic_bp_morning: m.systolic_bp_morning, diastolic_bp_morning: m.diastolic_bp_morning,
|
||||
systolic_bp_evening: m.systolic_bp_evening, diastolic_bp_evening: m.diastolic_bp_evening,
|
||||
heart_rate: m.heart_rate,
|
||||
weight: m.weight.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
blood_sugar: m.blood_sugar.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
weight: m.weight.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
blood_sugar: m.blood_sugar.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
water_intake_ml: m.water_intake_ml, urine_output_ml: m.urine_output_ml,
|
||||
notes: m.notes, created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
@@ -159,8 +160,8 @@ pub async fn update_vital_signs(
|
||||
systolic_bp_morning: m.systolic_bp_morning, diastolic_bp_morning: m.diastolic_bp_morning,
|
||||
systolic_bp_evening: m.systolic_bp_evening, diastolic_bp_evening: m.diastolic_bp_evening,
|
||||
heart_rate: m.heart_rate,
|
||||
weight: m.weight.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
blood_sugar: m.blood_sugar.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
weight: m.weight.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
blood_sugar: m.blood_sugar.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
water_intake_ml: m.water_intake_ml, urine_output_ml: m.urine_output_ml,
|
||||
notes: m.notes, created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
@@ -171,6 +172,7 @@ pub async fn delete_vital_signs(
|
||||
tenant_id: Uuid,
|
||||
vital_signs_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = vital_signs::Entity::find()
|
||||
.filter(vital_signs::Column::Id.eq(vital_signs_id))
|
||||
@@ -180,11 +182,14 @@ pub async fn delete_vital_signs(
|
||||
.await?
|
||||
.ok_or(HealthError::VitalSignsNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: vital_signs::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -321,6 +326,7 @@ pub async fn delete_lab_report(
|
||||
tenant_id: Uuid,
|
||||
report_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = lab_report::Entity::find()
|
||||
.filter(lab_report::Column::Id.eq(report_id))
|
||||
@@ -330,11 +336,14 @@ pub async fn delete_lab_report(
|
||||
.await?
|
||||
.ok_or(HealthError::LabReportNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: lab_report::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -468,6 +477,7 @@ pub async fn delete_health_record(
|
||||
tenant_id: Uuid,
|
||||
record_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = health_record::Entity::find()
|
||||
.filter(health_record::Column::Id.eq(record_id))
|
||||
@@ -477,11 +487,14 @@ pub async fn delete_health_record(
|
||||
.await?
|
||||
.ok_or(HealthError::HealthRecordNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: health_record::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -605,8 +618,8 @@ pub async fn get_indicator_timeseries(
|
||||
let data: Vec<(chrono::NaiveDate, f64)> = vitals.into_iter().filter_map(|v| {
|
||||
let val = match indicator.as_str() {
|
||||
"heart_rate" => v.heart_rate.map(|x| x as f64),
|
||||
"weight" => v.weight.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
"blood_sugar" => v.blood_sugar.map(|d| d.to_string().parse().unwrap_or(0.0)),
|
||||
"weight" => v.weight.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
"blood_sugar" => v.blood_sugar.map(|d| d.to_f64().unwrap_or(0.0)),
|
||||
"systolic_bp_morning" => v.systolic_bp_morning.map(|x| x as f64),
|
||||
"diastolic_bp_morning" => v.diastolic_bp_morning.map(|x| x as f64),
|
||||
"systolic_bp_evening" => v.systolic_bp_evening.map(|x| x as f64),
|
||||
|
||||
@@ -229,14 +229,17 @@ pub async fn delete_patient(
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = find_patient(&state.db, tenant_id, id).await?;
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: patient::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -498,6 +501,7 @@ pub async fn delete_family_member(
|
||||
patient_id: Uuid,
|
||||
family_member_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = patient_family_member::Entity::find()
|
||||
.filter(patient_family_member::Column::Id.eq(family_member_id))
|
||||
@@ -508,11 +512,14 @@ pub async fn delete_family_member(
|
||||
.await?
|
||||
.ok_or(HealthError::FamilyMemberNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)
|
||||
.map_err(|_| HealthError::VersionMismatch)?;
|
||||
|
||||
let mut active: patient_family_member::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
active.version = Set(next_ver);
|
||||
active.update(&state.db).await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -46,6 +46,7 @@ mod m20260423_000043_create_wechat_users;
|
||||
mod m20260423_000044_create_articles;
|
||||
mod m20260424_000045_health_indexes;
|
||||
mod m20260424_000046_health_constraints_fix;
|
||||
mod m20260424_000047_health_index_fix;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -99,6 +100,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260423_000044_create_articles::Migration),
|
||||
Box::new(m20260424_000045_health_indexes::Migration),
|
||||
Box::new(m20260424_000046_health_constraints_fix::Migration),
|
||||
Box::new(m20260424_000047_health_index_fix::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// 删除旧索引(缺少 tenant_id 前导列)
|
||||
db.execute_unprepared(
|
||||
"DROP INDEX IF EXISTS idx_health_trend_patient_period"
|
||||
).await?;
|
||||
|
||||
// 重建为包含 tenant_id 的正确索引
|
||||
db.execute_unprepared(
|
||||
"CREATE INDEX IF NOT EXISTS idx_health_trend_tenant_patient_period \
|
||||
ON health_trend (tenant_id, patient_id, period_start DESC)"
|
||||
).await?;
|
||||
|
||||
// 添加 follow_up_record 缺失的 (tenant_id, executed_date) 索引
|
||||
db.execute_unprepared(
|
||||
"CREATE INDEX IF NOT EXISTS idx_follow_up_record_tenant_executed_date \
|
||||
ON follow_up_record (tenant_id, executed_date DESC)"
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
"DROP INDEX IF EXISTS idx_health_trend_tenant_patient_period"
|
||||
).await?;
|
||||
|
||||
db.execute_unprepared(
|
||||
"DROP INDEX IF EXISTS idx_follow_up_record_tenant_executed_date"
|
||||
).await?;
|
||||
|
||||
db.execute_unprepared(
|
||||
"CREATE INDEX IF NOT EXISTS idx_health_trend_patient_period \
|
||||
ON health_trend (patient_id, period_start)"
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user