feat(server): 侧边栏菜单按业务流程重组 — 3 目录 → 7 目录
将"健康业务"(30+ 项扁平列表)拆分为 5 个业务域顶级目录: - 患者管理(患者/标签/医护) - 诊疗服务(随访/咨询/诊断/同意/监测) - 健康监测(实时监控/告警/设备/网关/危急值) - 运营管理(文章/积分/媒体/轮播图) - AI 助手(对话/Prompt/分析/知识库/用量/配置) 系统管理吸收 OAuth 合作方,工作台保持不变。 重建 menu_roles 按 doctor/nurse/health_manager/operator 精确绑定。 新增迁移 163,菜单系统 100% 数据库驱动,前端无需改动。
This commit is contained in:
@@ -164,6 +164,7 @@ mod m20260521_000159_patient_phone_and_consent_seed;
|
||||
mod m20260521_000160_follow_up_task_template_id_and_record_form_data;
|
||||
mod m20260521_000161_consultation_media_id_and_suggestion_references;
|
||||
mod m20260521_000162_consultation_session_rating_feedback;
|
||||
mod m20260521_000163_reorganize_menus_by_business_flow;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -335,6 +336,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260521_000160_follow_up_task_template_id_and_record_form_data::Migration),
|
||||
Box::new(m20260521_000161_consultation_media_id_and_suggestion_references::Migration),
|
||||
Box::new(m20260521_000162_consultation_session_rating_feedback::Migration),
|
||||
Box::new(m20260521_000163_reorganize_menus_by_business_flow::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
//! 侧边栏菜单按业务流程重组
|
||||
//!
|
||||
//! 将"健康业务"及其子目录(30+ 项)重组为 5 个顶级业务域目录:
|
||||
//! 患者管理 / 诊疗服务 / 健康监测 / 运营管理 / AI 助手
|
||||
//!
|
||||
//! 注意:menus 表有 trg_enforce_version 触发器,所有 UPDATE 必须带 version = version + 1
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
const DIR_PATIENT_MGMT: &str = "a0000000-0000-0000-0000-000000000005";
|
||||
const DIR_CLINICAL: &str = "a0000000-0000-0000-0000-000000000006";
|
||||
const DIR_MONITORING: &str = "a0000000-0000-0000-0000-000000000007";
|
||||
const DIR_OPERATIONS: &str = "a0000000-0000-0000-0000-000000000008";
|
||||
const DIR_AI: &str = "a0000000-0000-0000-0000-000000000009";
|
||||
const SYS: &str = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// ================================================================
|
||||
// Part 1: 创建 5 个新顶级 directory(多租户安全 + 幂等)
|
||||
// ================================================================
|
||||
let directories: &[(&str, &str, i32)] = &[
|
||||
(DIR_PATIENT_MGMT, "患者管理", 2),
|
||||
(DIR_CLINICAL, "诊疗服务", 3),
|
||||
(DIR_MONITORING, "健康监测", 4),
|
||||
(DIR_OPERATIONS, "运营管理", 5),
|
||||
(DIR_AI, "AI 助手", 6),
|
||||
];
|
||||
|
||||
for &(id, title, sort) in directories {
|
||||
db.execute_unprepared(&format!(
|
||||
"INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, visible, menu_type, permission, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
SELECT '{id}', t.id, NULL, '{title}', NULL, NULL, {sort}, true, 'directory', NULL, NOW(), NOW(), '{SYS}', '{SYS}', NULL, 1 \
|
||||
FROM tenant t \
|
||||
ON CONFLICT (id) DO NOTHING"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 更新工作台 sort=1, 系统管理 sort=7
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET sort_order = 1, version = version + 1 WHERE title = '工作台' AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
).await?;
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET sort_order = 7, version = version + 1 WHERE title = '系统管理' AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
).await?;
|
||||
|
||||
// ================================================================
|
||||
// Part 2: 移动叶子菜单到新目录(按 path 匹配,无视当前 parent_id)
|
||||
// 所有 UPDATE 必须 version = version + 1(乐观锁触发器要求)
|
||||
// ================================================================
|
||||
|
||||
// 患者管理
|
||||
for &(path, sort) in &[
|
||||
("/health/patients", 0),
|
||||
("/health/tags", 1),
|
||||
("/health/doctors", 2),
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = '{DIR_PATIENT_MGMT}', sort_order = {sort}, version = version + 1 WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 诊疗服务
|
||||
for &(path, sort) in &[
|
||||
("/health/schedules", 0),
|
||||
("/health/appointments", 1),
|
||||
("/health/follow-up-tasks", 2),
|
||||
("/health/follow-up-templates", 3),
|
||||
("/health/consultations", 4),
|
||||
("/health/action-inbox", 5),
|
||||
("/health/diagnoses", 6),
|
||||
("/health/consents", 7),
|
||||
("/health/daily-monitoring", 8),
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = '{DIR_CLINICAL}', sort_order = {sort}, version = version + 1 WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 健康监测
|
||||
for &(path, sort) in &[
|
||||
("/health/realtime-monitor", 0),
|
||||
("/health/alert-dashboard", 1),
|
||||
("/health/alerts", 2),
|
||||
("/health/alert-rules", 3),
|
||||
("/health/devices", 4),
|
||||
("/health/ble-gateways", 5),
|
||||
("/health/critical-value-thresholds", 6),
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = '{DIR_MONITORING}', sort_order = {sort}, version = version + 1 WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 运营管理
|
||||
for &(path, sort) in &[
|
||||
("/health/articles", 0),
|
||||
("/health/article-categories", 1),
|
||||
("/health/article-tags", 2),
|
||||
("/health/points-rules", 3),
|
||||
("/health/points-products", 4),
|
||||
("/health/points-orders", 5),
|
||||
("/health/offline-events", 6),
|
||||
("/health/media-library", 7),
|
||||
("/health/banners", 8),
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = '{DIR_OPERATIONS}', sort_order = {sort}, version = version + 1 WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// AI 助手
|
||||
for &(path, sort) in &[
|
||||
("/ai/chat", 0),
|
||||
("/health/ai-prompts", 1),
|
||||
("/health/ai-analysis", 2),
|
||||
("/health/ai-knowledge", 3),
|
||||
("/health/ai-usage", 4),
|
||||
("/health/ai-config", 5),
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = '{DIR_AI}', sort_order = {sort}, version = version + 1 WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Part 3: 移动 OAuth 到系统管理
|
||||
// ================================================================
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET parent_id = (SELECT id FROM menus WHERE title = '系统管理' AND menu_type = 'directory' AND deleted_at IS NULL LIMIT 1), sort_order = 7, version = version + 1 WHERE path = '/health/oauth-clients' AND deleted_at IS NULL"
|
||||
).await?;
|
||||
|
||||
// ================================================================
|
||||
// Part 4: 清理旧目录
|
||||
// ================================================================
|
||||
|
||||
// 软删除"健康业务"目录及其子目录
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET deleted_at = NOW(), version = version + 1 WHERE title = '健康业务' AND menu_type = 'directory' AND deleted_at IS NULL"
|
||||
).await?;
|
||||
|
||||
// 软删除"健康业务"下的旧子目录(c0000003-xxx)
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET deleted_at = NOW(), version = version + 1 WHERE parent_id = (SELECT id FROM menus WHERE title = '健康业务' AND menu_type = 'directory' LIMIT 1) AND menu_type = 'directory' AND deleted_at IS NULL"
|
||||
).await?;
|
||||
|
||||
// 通过 title 匹配也清理(防止 parent_id 引用失效后遗漏)
|
||||
for old_dir in &["患者医护", "随访咨询", "积分运营", "内容运营", "AI 分析"]
|
||||
{
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET deleted_at = NOW(), version = version + 1 WHERE title = '{old_dir}' AND menu_type = 'directory' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 软删除"配置"目录(如果还存在且已空)
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET deleted_at = NOW(), version = version + 1 WHERE title = '配置' AND menu_type = 'directory' AND deleted_at IS NULL AND NOT EXISTS (SELECT 1 FROM menus c WHERE c.parent_id = menus.id AND c.deleted_at IS NULL AND c.visible = true)"
|
||||
).await?;
|
||||
|
||||
// ================================================================
|
||||
// Part 5: 重建 menu_roles
|
||||
// ================================================================
|
||||
for code in &["doctor", "nurse", "health_manager", "operator", "admin"] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menu_roles SET deleted_at = NOW(), version = version + 1 WHERE role_id IN (SELECT id FROM roles WHERE code = '{code}' AND deleted_at IS NULL) AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"doctor",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/doctors",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/follow-up-templates",
|
||||
"/health/consultations",
|
||||
"/health/action-inbox",
|
||||
"/health/diagnoses",
|
||||
"/health/consents",
|
||||
"/health/daily-monitoring",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/health/ai-analysis",
|
||||
"/health/ai-usage",
|
||||
"/ai/chat",
|
||||
"/messages",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"nurse",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/consultations",
|
||||
"/health/action-inbox",
|
||||
"/health/diagnoses",
|
||||
"/health/consents",
|
||||
"/health/daily-monitoring",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/messages",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"health_manager",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/doctors",
|
||||
"/health/tags",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/follow-up-templates",
|
||||
"/health/consultations",
|
||||
"/health/action-inbox",
|
||||
"/health/diagnoses",
|
||||
"/health/consents",
|
||||
"/health/daily-monitoring",
|
||||
"/health/realtime-monitor",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/health/alert-rules",
|
||||
"/health/devices",
|
||||
"/health/critical-value-thresholds",
|
||||
"/health/ai-prompts",
|
||||
"/health/ai-analysis",
|
||||
"/health/ai-knowledge",
|
||||
"/health/ai-usage",
|
||||
"/health/ai-config",
|
||||
"/ai/chat",
|
||||
"/messages",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"operator",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/tags",
|
||||
"/health/devices",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/health/articles",
|
||||
"/health/article-categories",
|
||||
"/health/article-tags",
|
||||
"/health/points-rules",
|
||||
"/health/points-products",
|
||||
"/health/points-orders",
|
||||
"/health/offline-events",
|
||||
"/health/media-library",
|
||||
"/health/banners",
|
||||
"/health/ai-usage",
|
||||
"/messages",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assign_admin_all_menus(db).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// 恢复"健康业务"目录
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET deleted_at = NULL, version = version + 1 WHERE title = '健康业务' AND menu_type = 'directory'"
|
||||
).await?;
|
||||
|
||||
// 将所有健康相关叶子菜单移回"健康业务"
|
||||
let health_dir = "(SELECT id FROM menus WHERE title = '健康业务' AND menu_type = 'directory' AND deleted_at IS NULL LIMIT 1)";
|
||||
let paths_to_move: &[&str] = &[
|
||||
"/health/patients",
|
||||
"/health/doctors",
|
||||
"/health/tags",
|
||||
"/health/diagnoses",
|
||||
"/health/schedules",
|
||||
"/health/appointments",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/follow-up-templates",
|
||||
"/health/consultations",
|
||||
"/health/action-inbox",
|
||||
"/health/daily-monitoring",
|
||||
"/health/consents",
|
||||
"/health/realtime-monitor",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/health/alert-rules",
|
||||
"/health/devices",
|
||||
"/health/ble-gateways",
|
||||
"/health/critical-value-thresholds",
|
||||
"/health/articles",
|
||||
"/health/article-categories",
|
||||
"/health/article-tags",
|
||||
"/health/points-rules",
|
||||
"/health/points-products",
|
||||
"/health/points-orders",
|
||||
"/health/offline-events",
|
||||
"/health/media-library",
|
||||
"/health/banners",
|
||||
"/health/ai-prompts",
|
||||
"/health/ai-analysis",
|
||||
"/health/ai-knowledge",
|
||||
"/health/ai-usage",
|
||||
"/health/ai-config",
|
||||
"/ai/chat",
|
||||
];
|
||||
let paths_csv: String = paths_to_move
|
||||
.iter()
|
||||
.map(|p| format!("'{p}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = {health_dir}, version = version + 1 WHERE path IN ({paths_csv}) AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
|
||||
// OAuth 移回健康业务
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = {health_dir}, sort_order = 60, version = version + 1 WHERE path = '/health/oauth-clients' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
|
||||
// 删除新建的 5 个目录
|
||||
for id in &[
|
||||
DIR_PATIENT_MGMT,
|
||||
DIR_CLINICAL,
|
||||
DIR_MONITORING,
|
||||
DIR_OPERATIONS,
|
||||
DIR_AI,
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET deleted_at = NOW(), version = version + 1 WHERE id = '{id}'"
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 恢复目录排序
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET sort_order = 1, version = version + 1 WHERE title = '工作台' AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
).await?;
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET sort_order = 2, version = version + 1 WHERE title = '系统管理' AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
).await?;
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET sort_order = 3, version = version + 1 WHERE title = '健康业务' AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn assign_menus_for_role(
|
||||
db: &sea_orm_migration::SchemaManagerConnection<'_>,
|
||||
role_code: &str,
|
||||
menu_paths: &[&str],
|
||||
) -> Result<(), DbErr> {
|
||||
let paths_csv: String = menu_paths
|
||||
.iter()
|
||||
.map(|p| format!("'{p}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
db.execute_unprepared(&format!(
|
||||
"INSERT INTO menu_roles (id, menu_id, role_id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
SELECT gen_random_uuid(), m.id, r.id, r.tenant_id, NOW(), NOW(), r.id, r.id, NULL, 1 \
|
||||
FROM roles r \
|
||||
JOIN menus m ON m.tenant_id = r.tenant_id AND m.path IN ({paths_csv}) AND m.deleted_at IS NULL AND m.visible = true \
|
||||
WHERE r.code = '{role_code}' AND r.deleted_at IS NULL \
|
||||
ON CONFLICT (id) DO NOTHING"
|
||||
)).await?;
|
||||
|
||||
db.execute_unprepared(&format!(
|
||||
"INSERT INTO menu_roles (id, menu_id, role_id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
SELECT gen_random_uuid(), m.id, r.id, r.tenant_id, NOW(), NOW(), r.id, r.id, NULL, 1 \
|
||||
FROM roles r \
|
||||
JOIN menus m ON m.tenant_id = r.tenant_id AND m.menu_type = 'directory' AND m.deleted_at IS NULL \
|
||||
WHERE r.code = '{role_code}' AND r.deleted_at IS NULL \
|
||||
ON CONFLICT (id) DO NOTHING"
|
||||
)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assign_admin_all_menus(
|
||||
db: &sea_orm_migration::SchemaManagerConnection<'_>,
|
||||
) -> Result<(), DbErr> {
|
||||
db.execute_unprepared(
|
||||
"INSERT INTO menu_roles (id, menu_id, role_id, tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
SELECT gen_random_uuid(), m.id, r.id, r.tenant_id, NOW(), NOW(), r.id, r.id, NULL, 1 \
|
||||
FROM roles r \
|
||||
JOIN menus m ON m.tenant_id = r.tenant_id AND m.deleted_at IS NULL AND m.visible = true \
|
||||
WHERE r.code = 'admin' AND r.deleted_at IS NULL \
|
||||
ON CONFLICT (id) DO NOTHING"
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user