fix(health): 四次审计修复 — 6 CRITICAL + 8 HIGH + 4 MEDIUM
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

CRITICAL:
- C-1: consultation sender_id 改为从 JWT ctx.user_id 注入,防伪造
- C-2: consultation session 更新改为 CAS 原子操作,防并发丢失
- C-3: 随访记录创建包裹在事务中,保证记录/任务/后续任务一致性
- C-4/C-5/C-6: 唯一索引改为 partial index WHERE deleted_at IS NULL

HIGH:
- H-1: manage_patient_tags 添加 tag_ids 租户归属校验
- H-2: assign_doctor 添加重复关联检查
- H-3: calendar_view 限制日期范围最多 90 天
- H-4: export_sessions 添加 10000 条上限
- H-5: patient_tag_relation/patient_doctor_relation 添加 version 字段
- H-6: create_schedule 添加医生存在性检查
- H-7: 预约取消排班释放错误改为日志记录
- H-8: follow_up_task.related_appointment_id 添加 FK 约束

MEDIUM:
- M-2: 修复 search LIKE 双重 % 包裹问题
- M-3: article_service 错误类型改为 ArticleNotFound
- M-4: patient.created 事件移除 PII(姓名)
- M-6: lab_report 添加 (tenant_id, report_type) 索引
This commit is contained in:
iven
2026-04-24 07:50:14 +08:00
parent 7b7677dfec
commit 4867202437
11 changed files with 191 additions and 29 deletions

View File

@@ -45,6 +45,7 @@ mod m20260423_000042_create_health_tables;
mod m20260423_000043_create_wechat_users;
mod m20260423_000044_create_articles;
mod m20260424_000045_health_indexes;
mod m20260424_000046_health_constraints_fix;
pub struct Migrator;
@@ -97,6 +98,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260423_000043_create_wechat_users::Migration),
Box::new(m20260423_000044_create_articles::Migration),
Box::new(m20260424_000045_health_indexes::Migration),
Box::new(m20260424_000046_health_constraints_fix::Migration),
]
}
}

View File

@@ -0,0 +1,94 @@
use sea_orm_migration::prelude::*;
/// 迁移 000046: 修复唯一索引软删除兼容 + 关联表添加 version + 补充索引/FK
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
// C-4: patient.id_number 唯一索引 — 重建为 partial index WHERE deleted_at IS NULL
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number").await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tenant_id_number ON patient (tenant_id, id_number) WHERE deleted_at IS NULL AND id_number IS NOT NULL"
).await?;
// C-5: patient_tag.name 唯一索引 — 重建为 partial index WHERE deleted_at IS NULL
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tag_tenant_name_unique").await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tag_tenant_name_unique ON patient_tag (tenant_id, name) WHERE deleted_at IS NULL"
).await?;
// C-6: doctor_schedule 唯一索引 — 重建为 partial index修正列选择为 (tenant_id, doctor_id, schedule_date, period_type)
conn.execute_unprepared("DROP INDEX IF EXISTS idx_doctor_schedule_unique_slot").await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_doctor_schedule_unique_slot ON doctor_schedule (tenant_id, doctor_id, schedule_date, period_type) WHERE deleted_at IS NULL"
).await?;
// H-5: patient_tag_relation 添加 version 列
conn.execute_unprepared(
"ALTER TABLE patient_tag_relation ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 1"
).await?;
// H-5: patient_doctor_relation 添加 version 列
conn.execute_unprepared(
"ALTER TABLE patient_doctor_relation ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 1"
).await?;
// H-8: follow_up_task.related_appointment_id 添加 FK 约束
conn.execute_unprepared(
"ALTER TABLE follow_up_task DROP CONSTRAINT IF EXISTS fk_follow_up_task_appointment"
).await?;
conn.execute_unprepared(
"ALTER TABLE follow_up_task ADD CONSTRAINT fk_follow_up_task_appointment \
FOREIGN KEY (related_appointment_id) REFERENCES appointment(id) ON DELETE SET NULL"
).await?;
// M-6: lab_report 添加 (tenant_id, report_type) 索引
conn.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_lab_report_tenant_type ON lab_report (tenant_id, report_type)"
).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let conn = manager.get_connection();
// 恢复原始索引(非 partial
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tenant_id_number").await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tenant_id_number ON patient (tenant_id, id_number)"
).await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_patient_tag_tenant_name_unique").await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_patient_tag_tenant_name_unique ON patient_tag (tenant_id, name)"
).await?;
conn.execute_unprepared("DROP INDEX IF EXISTS idx_doctor_schedule_unique_slot").await?;
conn.execute_unprepared(
"CREATE UNIQUE INDEX idx_doctor_schedule_unique_slot ON doctor_schedule (tenant_id, doctor_id, schedule_date, start_time)"
).await?;
conn.execute_unprepared(
"ALTER TABLE patient_tag_relation DROP COLUMN IF EXISTS version"
).await?;
conn.execute_unprepared(
"ALTER TABLE patient_doctor_relation DROP COLUMN IF EXISTS version"
).await?;
conn.execute_unprepared(
"ALTER TABLE follow_up_task DROP CONSTRAINT IF EXISTS fk_follow_up_task_appointment"
).await?;
conn.execute_unprepared(
"DROP INDEX IF EXISTS idx_lab_report_tenant_type"
).await?;
Ok(())
}
}