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:
iven
2026-05-19 21:36:01 +08:00
parent 8fbe1543cb
commit c6d4e76b62
12 changed files with 514 additions and 122 deletions

View File

@@ -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),
]
}
}

View File

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

View File

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