feat(health): 菜单方案B重组 — 患者中心+随访关怀+配置归入系统管理+文章标签合并
方案B业务流程导向菜单优化: - "患者管理" → "患者中心",吸收日常监测/诊断/知情同意/咨询 - "诊疗服务" → "随访关怀",只保留随访相关 - 告警规则/危急值阈值 → 系统管理 - 文章分类/标签菜单软删除,合并为文章管理页内 Tab 变更文件: - 迁移 164: 重命名目录+移动叶子菜单+重建 menu_roles - ArticleManageList.tsx: 分类/标签管理合并为页内 Tab - 讨论记录 + 可视化原型 HTML Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
//! 方案 B:业务流程导向菜单重组
|
||||
//!
|
||||
//! 在迁移 163 基础上进一步优化:
|
||||
//! 1. "患者管理" → "患者中心",吸收日常监测/诊断/知情同意/咨询
|
||||
//! 2. "诊疗服务" → "随访关怀",只保留随访相关
|
||||
//! 3. 告警规则/危急值阈值 → 系统管理(配置项归入)
|
||||
//! 4. 文章分类/文章标签软删除(降级为文章管理页内 Tab)
|
||||
//!
|
||||
//! 注意: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";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// ================================================================
|
||||
// Part 1: 重命名目录
|
||||
// ================================================================
|
||||
|
||||
// "患者管理" → "患者中心",图标改用 HeartOutlined(代表以患者为中心)
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET title = '患者中心', icon = 'HeartOutlined', version = version + 1 \
|
||||
WHERE id = '{DIR_PATIENT_MGMT}' AND menu_type = 'directory' AND deleted_at IS NULL"
|
||||
))
|
||||
.await?;
|
||||
|
||||
// "诊疗服务" → "随访关怀",图标改用 FormOutlined(代表关怀表单)
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET title = '随访关怀', icon = 'FormOutlined', version = version + 1 \
|
||||
WHERE id = '{DIR_CLINICAL}' AND menu_type = 'directory' AND deleted_at IS NULL"
|
||||
))
|
||||
.await?;
|
||||
|
||||
// ================================================================
|
||||
// Part 2: 移动叶子菜单
|
||||
// ================================================================
|
||||
|
||||
// 患者中心:患者(0) + 日常监测(1) + 诊断(2) + 知情同意(3) + 咨询(4) + 标签(5) + 医护(6)
|
||||
for &(path, sort) in &[
|
||||
("/health/patients", 0),
|
||||
("/health/daily-monitoring", 1),
|
||||
("/health/diagnoses", 2),
|
||||
("/health/consents", 3),
|
||||
("/health/consultations", 4),
|
||||
("/health/tags", 5),
|
||||
("/health/doctors", 6),
|
||||
] {
|
||||
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?;
|
||||
}
|
||||
|
||||
// 随访关怀:行动收件箱(0) + 随访任务(1) + 随访模板(2) + 排班(3,冻结) + 预约(4,冻结)
|
||||
for &(path, sort) in &[
|
||||
("/health/action-inbox", 0),
|
||||
("/health/follow-up-tasks", 1),
|
||||
("/health/follow-up-templates", 2),
|
||||
("/health/schedules", 3),
|
||||
("/health/appointments", 4),
|
||||
] {
|
||||
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/devices", 3),
|
||||
("/health/ble-gateways", 4),
|
||||
] {
|
||||
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?;
|
||||
}
|
||||
|
||||
// 告警规则 + 危急值阈值 → 系统管理
|
||||
let sys_dir = "(SELECT id FROM menus WHERE title = '系统管理' AND menu_type = 'directory' AND deleted_at IS NULL LIMIT 1)";
|
||||
for &(path, sort) in &[
|
||||
("/health/alert-rules", 8),
|
||||
("/health/critical-value-thresholds", 9),
|
||||
] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET parent_id = {sys_dir}, sort_order = {sort}, version = version + 1 \
|
||||
WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Part 3: 软删除文章分类/文章标签菜单(降级为文章管理页内 Tab)
|
||||
// ================================================================
|
||||
|
||||
for path in &["/health/article-categories", "/health/article-tags"] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET deleted_at = NOW(), version = version + 1 \
|
||||
WHERE path = '{path}' AND deleted_at IS NULL"
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Part 4: 重建 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?;
|
||||
}
|
||||
|
||||
// 医生:工作台 + 患者中心(患者/日常监测/诊断/知情同意/咨询) + 随访关怀(行动收件箱/随访任务/随访模板) + 监测告警(仪表盘/列表) + AI(客服/分析/用量) + 消息
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"doctor",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/daily-monitoring",
|
||||
"/health/diagnoses",
|
||||
"/health/consents",
|
||||
"/health/consultations",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/follow-up-templates",
|
||||
"/health/action-inbox",
|
||||
"/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/daily-monitoring",
|
||||
"/health/diagnoses",
|
||||
"/health/consents",
|
||||
"/health/consultations",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/action-inbox",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/messages",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 健康管理师:几乎所有健康业务菜单
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"health_manager",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/daily-monitoring",
|
||||
"/health/diagnoses",
|
||||
"/health/consents",
|
||||
"/health/consultations",
|
||||
"/health/tags",
|
||||
"/health/doctors",
|
||||
"/health/follow-up-tasks",
|
||||
"/health/follow-up-templates",
|
||||
"/health/action-inbox",
|
||||
"/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?;
|
||||
|
||||
// 运营:工作台 + 患者中心(患者/标签) + 监测告警(仪表盘/列表/设备) + 运营管理(全) + AI用量 + 消息
|
||||
assign_menus_for_role(
|
||||
db,
|
||||
"operator",
|
||||
&[
|
||||
"/",
|
||||
"/health/statistics",
|
||||
"/health/patients",
|
||||
"/health/tags",
|
||||
"/health/devices",
|
||||
"/health/alert-dashboard",
|
||||
"/health/alerts",
|
||||
"/health/articles",
|
||||
"/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 title = '患者管理', icon = 'TeamOutlined', version = version + 1 \
|
||||
WHERE id::text LIKE 'a0000000-0000-0000-0000-000000000005%' \
|
||||
AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
)
|
||||
.await?;
|
||||
db.execute_unprepared(
|
||||
"UPDATE menus SET title = '诊疗服务', icon = 'MedicineBoxOutlined', version = version + 1 \
|
||||
WHERE id::text LIKE 'a0000000-0000-0000-0000-000000000006%' \
|
||||
AND menu_type = 'directory' AND deleted_at IS NULL",
|
||||
).await?;
|
||||
|
||||
// 恢复患者管理原始 3 项
|
||||
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?;
|
||||
}
|
||||
|
||||
// 恢复诊疗服务原始 9 项
|
||||
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?;
|
||||
}
|
||||
|
||||
// 恢复健康监测原始 7 项
|
||||
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 in &["/health/article-categories", "/health/article-tags"] {
|
||||
db.execute_unprepared(&format!(
|
||||
"UPDATE menus SET deleted_at = NULL, version = version + 1 \
|
||||
WHERE path = '{path}'"
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 告警规则/阈值移回健康监测
|
||||
for &(path, sort) in &[
|
||||
("/health/alert-rules", 3),
|
||||
("/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?;
|
||||
}
|
||||
|
||||
// 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?;
|
||||
|
||||
// 重建 menu_roles(用迁移 163 的角色分配)
|
||||
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 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?;
|
||||
|
||||
// 绑定目录(directory 类型)
|
||||
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