fix(ai): AI 对话全链路修复 + 菜单配置 + 会话消息持久化
- 修复 ai_tenant_config Entity 表名错误(复数→单数)导致 budget_status 500
- 修复 ai_usage 表 SQL 引用不存在的 deleted_at 列
- 修复 risk_service SQL 列名/表名与实际数据库 schema 不匹配
- chat_handler provider 选择改为配置优先(default_provider→fallback chain)
- 新增 Ollama 非 FC provider 的 generate() 降级路径
- 新增 GET /ai/chat/sessions/{id}/messages 端点
- 前端 ChatPage 切换会话时从后端加载历史消息
- AiConfigPage 新增 default_provider 和 system_prompt 配置字段
- 迁移 000155-000156:AI 菜单调整 + AI 客服菜单 + 角色绑定
- 配额检查错误处理区分配额耗尽和 DB 异常
This commit is contained in:
@@ -156,6 +156,8 @@ mod m20260518_000151_fix_ai_config_menu_parent;
|
||||
mod m20260518_000152_seed_ai_provider_permission;
|
||||
mod m20260518_000153_ai_health_butler_v2;
|
||||
mod m20260519_000154_seed_ai_knowledge_permissions;
|
||||
mod m20260519_000155_fix_ai_menus_and_add_chat;
|
||||
mod m20260519_000156_fix_ai_menus_round2;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -319,6 +321,8 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260518_000152_seed_ai_provider_permission::Migration),
|
||||
Box::new(m20260518_000153_ai_health_butler_v2::Migration),
|
||||
Box::new(m20260519_000154_seed_ai_knowledge_permissions::Migration),
|
||||
Box::new(m20260519_000155_fix_ai_menus_and_add_chat::Migration),
|
||||
Box::new(m20260519_000156_fix_ai_menus_round2::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! 修复 AI 知识库菜单层级 + 新增 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";
|
||||
|
||||
// 1. 修复 AI 知识库:从 AI 配置子级移到 AI 分析分组下(与 AI 配置同级)
|
||||
// 必须递增 version 以通过乐观锁触发器
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
UPDATE menus mk
|
||||
SET parent_id = mp.parent_id, version = mk.version + 1
|
||||
FROM menus mp
|
||||
WHERE mp.path = '/health/ai-config' AND mp.deleted_at IS NULL
|
||||
AND mk.path = '/health/ai-knowledge' AND mk.deleted_at IS NULL
|
||||
AND mk.parent_id = mp.id
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. 新增 AI 客服菜单(与 AI 配置、AI 知识库同级)
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order,
|
||||
permission, menu_type, visible,
|
||||
created_at, updated_at, created_by, updated_by, deleted_at, version)
|
||||
SELECT gen_random_uuid(), t.id,
|
||||
(SELECT m.parent_id FROM menus m WHERE m.path = '/health/ai-config' AND m.tenant_id = t.id AND m.deleted_at IS NULL LIMIT 1),
|
||||
'AI 客服', '/ai/chat', 'MessageOutlined', 58,
|
||||
'ai.chat.session.list', 'menu', true,
|
||||
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||
FROM tenant t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM menus m
|
||||
WHERE m.path = '/ai/chat' AND m.tenant_id = t.id AND m.deleted_at IS NULL
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
|
||||
// 3. 菜单绑定 admin 角色
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
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, m.tenant_id, NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||
FROM menus m
|
||||
JOIN roles r ON r.tenant_id = m.tenant_id AND r.code = 'admin' AND r.deleted_at IS NULL
|
||||
WHERE m.path = '/ai/chat' AND m.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM menu_roles mr
|
||||
WHERE mr.menu_id = m.id AND mr.role_id = r.id AND mr.deleted_at IS NULL
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! 修复 AI 知识库菜单层级 + 新增 AI 客服菜单(补充 000155 未生效的部分)
|
||||
|
||||
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";
|
||||
|
||||
// 1. 修复 AI 知识库:从 AI 配置子级移到 AI 分析分组下(与 AI 配置同级)
|
||||
// 递增 version 以通过乐观锁触发器
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
UPDATE menus mk
|
||||
SET parent_id = mp.parent_id, version = mk.version + 1
|
||||
FROM menus mp
|
||||
WHERE mp.path = '/health/ai-config' AND mp.deleted_at IS NULL
|
||||
AND mk.path = '/health/ai-knowledge' AND mk.deleted_at IS NULL
|
||||
AND mk.parent_id = mp.id
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. 新增 AI 客服菜单(与 AI 配置同级)
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order,
|
||||
permission, menu_type, visible,
|
||||
created_at, updated_at, created_by, updated_by, deleted_at, version)
|
||||
SELECT gen_random_uuid(), t.id,
|
||||
(SELECT m.parent_id FROM menus m WHERE m.path = '/health/ai-config' AND m.tenant_id = t.id AND m.deleted_at IS NULL LIMIT 1),
|
||||
'AI 客服', '/ai/chat', 'MessageOutlined', 58,
|
||||
'ai.chat.session.list', 'menu', true,
|
||||
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||
FROM tenant t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM menus m
|
||||
WHERE m.path = '/ai/chat' AND m.tenant_id = t.id AND m.deleted_at IS NULL
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
|
||||
// 3. 菜单绑定 admin 角色
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
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, m.tenant_id, NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||
FROM menus m
|
||||
JOIN roles r ON r.tenant_id = m.tenant_id AND r.code = 'admin' AND r.deleted_at IS NULL
|
||||
WHERE m.path = '/ai/chat' AND m.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM menu_roles mr
|
||||
WHERE mr.menu_id = m.id AND mr.role_id = r.id AND mr.deleted_at IS NULL
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
|
||||
// 4. 为 doctor / health_manager 角色也绑定 AI 客服菜单
|
||||
for role in ["doctor", "health_manager"] {
|
||||
db.execute_unprepared(&format!(
|
||||
r#"
|
||||
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, m.tenant_id, NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||
FROM menus m
|
||||
JOIN roles r ON r.tenant_id = m.tenant_id AND r.code = '{role}' AND r.deleted_at IS NULL
|
||||
WHERE m.path = '/ai/chat' AND m.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM menu_roles mr
|
||||
WHERE mr.menu_id = m.id AND mr.role_id = r.id AND mr.deleted_at IS NULL
|
||||
)
|
||||
"#
|
||||
)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user