feat(ai): 新增 AI 客服聊天功能 + 消息页重构为小华助手

- 新增 POST /ai/chat 端点,由 LLM(Ollama qwen3)担任 24h 健康客服"小华"
- 新增 ai.chat.send 权限,绑定管理员/患者/医生/护士/健康管理师角色
- 消息页从咨询列表重构为单窗口 AI 对话(欢迎态 + 聊天态 + 快捷问诊)
- 通知功能迁移到"我的"页面菜单项(带未读角标),独立通知列表页
- 修复气泡文字截断:改用百分比 max-width + block Text + pre-wrap 换行
- 修复权限绑定:迁移 SQL 角色名从英文改为中文(admin→管理员,patient→患者)
This commit is contained in:
iven
2026-05-17 00:49:41 +08:00
parent 4be28de3ce
commit 710b2e2423
14 changed files with 952 additions and 439 deletions

View File

@@ -148,6 +148,7 @@ mod m20260512_000143_seed_copilot_alert_rules;
mod m20260513_000144_enforce_version_optimistic_lock;
mod m20260513_000145_seed_missing_permissions;
mod m20260515_000146_seed_menu_permissions_phase2;
mod m20260516_000147_seed_ai_chat_permission;
pub struct Migrator;
@@ -303,6 +304,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260513_000144_enforce_version_optimistic_lock::Migration),
Box::new(m20260513_000145_seed_missing_permissions::Migration),
Box::new(m20260515_000146_seed_menu_permissions_phase2::Migration),
Box::new(m20260516_000147_seed_ai_chat_permission::Migration),
]
}
}

View File

@@ -0,0 +1,65 @@
//! 新增 ai.chat.send 权限码 — AI 客服聊天
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();
let sys = "00000000-0000-0000-0000-000000000000";
// 注册权限到所有租户
db.execute_unprepared(&format!(
r#"
INSERT INTO permissions (id, tenant_id, code, name, resource, action, description,
created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id, 'ai.chat.send', 'AI 客服聊天', 'ai', 'chat.send', 'AI 客服聊天',
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM permissions p
WHERE p.code = 'ai.chat.send' AND p.tenant_id = t.id AND p.deleted_at IS NULL
)
"#
)).await?;
// 绑定到管理员角色
db.execute_unprepared(
r#"
INSERT INTO role_permissions (role_id, permission_id, tenant_id, created_by, updated_by, version)
SELECT r.id, p.id, t.id, r.id, r.id, 1
FROM tenant t
JOIN roles r ON r.tenant_id = t.id AND r.name = '管理员' AND r.deleted_at IS NULL
JOIN permissions p ON p.tenant_id = t.id AND p.code = 'ai.chat.send' AND p.deleted_at IS NULL
WHERE NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id AND rp.tenant_id = t.id
)
"#,
).await?;
// 绑定到患者角色(患者需要使用 AI 客服)
db.execute_unprepared(
r#"
INSERT INTO role_permissions (role_id, permission_id, tenant_id, created_by, updated_by, version)
SELECT r.id, p.id, t.id, r.id, r.id, 1
FROM tenant t
JOIN roles r ON r.tenant_id = t.id AND r.name = '患者' AND r.deleted_at IS NULL
JOIN permissions p ON p.tenant_id = t.id AND p.code = 'ai.chat.send' AND p.deleted_at IS NULL
WHERE NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id AND rp.tenant_id = t.id
)
"#,
).await?;
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Ok(())
}
}