fix: 多角色业务链路测试发现并修复 3 类问题
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

1. 角色权限修复(CRITICAL):
   - operator 角色权限为空(迁移 name/code 不匹配 + 软删除冲突)
   - doctor 角色权限被误清(API assign_permissions 失败导致全部软删除)
   - nurse 缺 devices 权限 + doctor/nurse 缺 appointment 权限
   - 新增 3 个迁移 000130-000132 修复所有角色权限

2. 趋势指标映射修复(HIGH):
   - 前端 blood_pressure_systolic → systolic_bp_morning
   - 前端 blood_sugar_fasting → blood_sugar
   - 同步修复首页、健康页、趋势页的 indicator 参数

3. 咨询页错误处理优化(MEDIUM):
   - 403/401 时显示空列表而非"加载失败"错误提示
This commit is contained in:
iven
2026-05-08 22:00:43 +08:00
parent 81c174a902
commit 28dafa9bea
8 changed files with 376 additions and 77 deletions

View File

@@ -131,6 +131,9 @@ mod m20260506_000126_fix_role_permissions_cleanup;
mod m20260507_000127_fix_doctor_extra_permissions;
mod m20260507_000128_fix_alert_status_and_menu_perms;
mod m20260507_000129_fix_nurse_operator_points_permissions;
mod m20260508_000130_fix_operator_permissions_and_nurse_devices;
mod m20260508_000131_fix_all_role_permissions;
mod m20260508_000132_fix_doctor_permissions_restore;
pub struct Migrator;
@@ -269,6 +272,9 @@ impl MigratorTrait for Migrator {
Box::new(m20260507_000127_fix_doctor_extra_permissions::Migration),
Box::new(m20260507_000128_fix_alert_status_and_menu_perms::Migration),
Box::new(m20260507_000129_fix_nurse_operator_points_permissions::Migration),
Box::new(m20260508_000130_fix_operator_permissions_and_nurse_devices::Migration),
Box::new(m20260508_000131_fix_all_role_permissions::Migration),
Box::new(m20260508_000132_fix_doctor_permissions_restore::Migration),
]
}
}

View File

@@ -0,0 +1,18 @@
//! 迁移桩文件 — 000130 的实际逻辑已合并到 000131
//! 保留此文件以避免 "missing migration file" 错误
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> {
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Ok(())
}
}

View File

@@ -0,0 +1,140 @@
//! 修复所有角色权限operator 为空 + doctor 被误清 + nurse 缺 devices + 补 appointment
//!
//! 根因链:
//! 1. m20260507_000129 SQL 使用 `r.name = 'nurse'`(中文),导致 WHERE 不匹配
//! 2. assign_permissions API 先软删除再 INSERTINSERT 失败(唯一约束)导致权限全部丢失
//! 3. 000130 迁移的 `DELETE WHERE deleted_at IS NOT NULL` 物理删除了被软删除的记录
//!
//! 本迁移:物理清理后,对所有受影响角色重新分配完整权限。
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
async fn assign_perms(
db: &sea_orm_migration::SchemaManagerConnection<'_>,
role_code: &str,
perm_codes: &[&str],
) -> Result<(), DbErr> {
for code in perm_codes {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
format!(
"INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope, created_at, updated_at, created_by, updated_by, deleted_at, version) \
SELECT r.id, p.id, r.tenant_id, 'all', NOW(), NOW(), r.id, r.id, NULL, 1 \
FROM roles r \
JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code = '{code}' AND p.deleted_at IS NULL \
WHERE r.code = '{role_code}' AND r.deleted_at IS NULL \
ON CONFLICT (role_id, permission_id) DO UPDATE SET \
data_scope = 'all', deleted_at = NULL, updated_at = NOW(), version = role_permissions.version + 1"
),
)).await?;
}
Ok(())
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
// === 1. 物理删除所有被软删除的 role_permissions 记录 ===
db.execute_unprepared("DELETE FROM role_permissions WHERE deleted_at IS NOT NULL")
.await?;
// === 2. Doctor 角色:完整权限重新分配 ===
let doctor_perms = vec![
"health.patient.list",
"health.patient.manage",
"health.health-data.list",
"health.follow-up.list",
"health.follow-up.manage",
"health.consultation.list",
"health.consultation.manage",
"health.doctor.list",
"health.doctor.manage",
"health.action-inbox.list",
"health.action-inbox.manage",
"health.daily-monitoring.list",
"health.daily-monitoring.manage",
"health.alerts.list",
"health.alerts.manage",
"health.alert-rules.list",
"health.critical-alerts.list",
"health.diagnosis.list",
"health.diagnosis.manage",
"health.consent.list",
"health.consent.manage",
"health.follow-up-templates.list",
"health.follow-up-templates.manage",
"health.appointment.list",
"health.appointment.manage",
"health.care-plan.list",
"health.care-plan.manage",
"health.dialysis.list",
"health.dialysis.manage",
"health.dialysis-prescription.list",
"health.dialysis-prescription.manage",
"health.dialysis.stats",
"ai.analysis.list",
"ai.suggestion.list",
"ai.prompt.list",
"ai.usage.list",
"message.list",
"workflow.list",
"workflow.read",
];
assign_perms(db, "doctor", &doctor_perms).await?;
// === 3. Nurse 角色:完整权限重新分配 ===
let nurse_perms = vec![
"health.patient.list",
"health.patient.manage",
"health.health-data.list",
"health.follow-up.list",
"health.follow-up.manage",
"health.consultation.list",
"health.action-inbox.list",
"health.action-inbox.manage",
"health.daily-monitoring.list",
"health.daily-monitoring.manage",
"health.alerts.list",
"health.critical-alerts.list",
"health.diagnosis.list",
"health.consent.list",
"health.consent.manage",
"health.device-readings.list",
"health.devices.list",
"health.appointment.list",
"health.appointment.manage",
"message.list",
];
assign_perms(db, "nurse", &nurse_perms).await?;
// === 4. Operator 角色:完整权限重新分配 ===
let operator_perms = vec![
"health.patient.list",
"health.tags.list",
"health.tags.manage",
"health.articles.list",
"health.articles.manage",
"health.articles.review",
"health.points.list",
"health.points.manage",
"health.devices.list",
"health.alerts.list",
"health.dashboard.manage",
"ai.usage.list",
"message.list",
];
assign_perms(db, "operator", &operator_perms).await?;
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// No down — this is a corrective migration
Ok(())
}
}

View File

@@ -0,0 +1,81 @@
//! 紧急修复doctor 角色权限被误清,重新分配完整权限
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();
// 先清理 doctor 角色被软删除的记录
db.execute_unprepared(
"DELETE FROM role_permissions WHERE role_id IN (SELECT id FROM roles WHERE code = 'doctor') AND deleted_at IS NOT NULL"
).await?;
let doctor_perms = vec![
"health.patient.list",
"health.patient.manage",
"health.health-data.list",
"health.follow-up.list",
"health.follow-up.manage",
"health.consultation.list",
"health.consultation.manage",
"health.doctor.list",
"health.doctor.manage",
"health.action-inbox.list",
"health.action-inbox.manage",
"health.daily-monitoring.list",
"health.daily-monitoring.manage",
"health.alerts.list",
"health.alerts.manage",
"health.alert-rules.list",
"health.critical-alerts.list",
"health.diagnosis.list",
"health.diagnosis.manage",
"health.consent.list",
"health.consent.manage",
"health.follow-up-templates.list",
"health.follow-up-templates.manage",
"health.appointment.list",
"health.appointment.manage",
"health.care-plan.list",
"health.care-plan.manage",
"health.dialysis.list",
"health.dialysis.manage",
"health.dialysis-prescription.list",
"health.dialysis-prescription.manage",
"health.dialysis.stats",
"ai.analysis.list",
"ai.suggestion.list",
"ai.prompt.list",
"ai.usage.list",
"message.list",
"workflow.list",
"workflow.read",
];
for code in &doctor_perms {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
format!(
"INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope, created_at, updated_at, created_by, updated_by, deleted_at, version) \
SELECT r.id, p.id, r.tenant_id, 'all', NOW(), NOW(), r.id, r.id, NULL, 1 \
FROM roles r \
JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code = '{code}' AND p.deleted_at IS NULL \
WHERE r.code = 'doctor' AND r.deleted_at IS NULL \
ON CONFLICT (role_id, permission_id) DO UPDATE SET \
data_scope = 'all', deleted_at = NULL, updated_at = NOW(), version = role_permissions.version + 1"
),
)).await?;
}
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Ok(())
}
}