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:
iven
2026-05-21 08:13:23 +08:00
parent 2644926fb6
commit b8c84ed9af
4 changed files with 2141 additions and 58 deletions

View File

@@ -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(())
}