fix: V1 测试版本端到端验证修复 — 6 CRITICAL + 3 HIGH 问题全量修复

修复项:
- fix(db): 迁移 149 — 修复 Admin 角色权限绑定被迁移链破坏 (FE-C1)
- fix(health): 4 个 handler 添加空名称验证 — Doctor/Article/AlertRule/Tag (API-C1~C4)
- fix(health): Stats 仪表盘 new_this_week 查询修复 — SeaORM date_trunc bug (FE-C2)
- fix(server): 添加安全响应头 — X-Frame-Options/CSP/XSS-Protection/Referrer-Policy (SEC-H1)
- fix(mp): 预约创建契约修复 — notes/reason 字段映射 + 移除 schedule_id (MP-H1)
- fix(mp): 咨询会话 subject/last_message 字段改为可选 (MP-H3)
- fix(ai): AiConfig Default derive 替代手写 impl (clippy)

测试报告:
- 8 维度端到端测试全部完成 (后端 87 用例 / 前端 30 页面 / 小程序 80+ API / 安全 20 项 / 性能 20 端点)
- 多角色 7 角色 49 检查 100% 通过
- 综合测试报告 + 专家评估报告
This commit is contained in:
iven
2026-05-18 10:24:40 +08:00
parent 38b0d91407
commit d623f8b2ff
36 changed files with 5564 additions and 189 deletions

View File

@@ -150,6 +150,8 @@ mod m20260513_000145_seed_missing_permissions;
mod m20260515_000146_seed_menu_permissions_phase2;
mod m20260516_000147_seed_ai_chat_permission;
mod m20260518_000148_create_ai_chat_tables;
mod m20260518_000149_fix_admin_permissions;
mod m20260518_000150_seed_ai_config_permission;
pub struct Migrator;
@@ -307,6 +309,8 @@ impl MigratorTrait for Migrator {
Box::new(m20260515_000146_seed_menu_permissions_phase2::Migration),
Box::new(m20260516_000147_seed_ai_chat_permission::Migration),
Box::new(m20260518_000148_create_ai_chat_tables::Migration),
Box::new(m20260518_000149_fix_admin_permissions::Migration),
Box::new(m20260518_000150_seed_ai_config_permission::Migration),
]
}
}

View File

@@ -0,0 +1,78 @@
//! 修复 admin 角色权限绑定
//!
//! 根因链:
//! 1. m20260506_000126 对部分角色执行了软删除SET deleted_at = NOW()
//! 2. m20260508_000131 执行 `DELETE FROM role_permissions WHERE deleted_at IS NOT NULL`
//! 物理删除了所有被软删除的记录
//! 3. m20260508_000131 只重新分配了 doctor/nurse/operator 的权限,遗漏了 admin 角色
//! 4. 后续的 assign_permissions API 调用可能在内部先软删除再 INSERT
//! INSERT 失败时 admin 权限全部丢失
//!
//! 本迁移:
//! - Step 1: 恢复所有被软删除的 admin role_permissionsdeleted_at IS NOT NULL → NULL
//! - Step 2: 插入所有缺失的 admin role_permissionsON CONFLICT DO NOTHING 保证幂等)
//!
//! 覆盖范围:全系统 128 个权限码auth/config/workflow/message/plugin/health/ai/copilot/points
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();
// ================================================================
// Step 1: 恢复被软删除的 admin role_permissions
// ================================================================
// 如果 admin 的某些权限记录仍然存在但被软删除了,恢复它们
db.execute_unprepared(
r#"
UPDATE role_permissions rp
SET deleted_at = NULL, updated_at = NOW(), version = rp.version + 1
FROM roles r
WHERE rp.role_id = r.id
AND r.code = 'admin'
AND r.deleted_at IS NULL
AND rp.deleted_at IS NOT NULL
"#,
)
.await?;
// ================================================================
// Step 2: 插入缺失的 admin role_permissions
// ================================================================
// 将 permissions 表中所有未被软删除的权限绑定到 admin 角色
// ON CONFLICT (role_id, permission_id) DO NOTHING — 已存在(含刚恢复的)的跳过
db.execute_unprepared(
r#"
INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope,
created_at, updated_at, created_by, updated_by,
deleted_at, version)
SELECT r.id, p.id, r.tenant_id, 'all',
NOW(), NOW(), r.id, r.id,
NULL, 1
FROM roles r
JOIN permissions p ON p.tenant_id = r.tenant_id AND p.deleted_at IS NULL
WHERE r.code = 'admin' AND r.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM role_permissions rp
WHERE rp.role_id = r.id
AND rp.permission_id = p.id
AND rp.deleted_at IS NULL
)
ON CONFLICT (role_id, permission_id) DO NOTHING
"#,
)
.await?;
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// 不回滚 — 这是修复性迁移admin 应该始终拥有全部权限
Ok(())
}
}

View File

@@ -0,0 +1,94 @@
//! 新增 ai.config.read / ai.config.manage 权限码 + AI 配置管理菜单
//!
//! AI 配置(模型/温度/Token/迭代次数/系统提示词)需管理员在前端可视化管理。
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-00000000-00000000-000000000000";
// 注册 ai.config.read 和 ai.config.manage 权限到所有租户
for (code, name, desc) in [
("ai.config.read", "查看 AI 配置", "查看 AI 模型和参数配置"),
(
"ai.config.manage",
"管理 AI 配置",
"修改 AI 模型、温度、Token 等参数配置",
),
] {
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, '{code}', '{name}', 'ai', '{code}', '{desc}',
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM permissions p
WHERE p.code = '{code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL
)
"#
)).await?;
// 绑定到管理员角色
db.execute_unprepared(&format!(
r#"
INSERT INTO role_permissions (id, tenant_id, role_id, permission_id, created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id, r.id, p.id, NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
JOIN roles r ON r.tenant_id = t.id AND r.code = 'admin' AND r.deleted_at IS NULL
JOIN permissions p ON p.tenant_id = t.id AND p.code = '{code}' 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.deleted_at IS NULL
)
"#
)).await?;
}
// 添加 AI 配置管理菜单
db.execute_unprepared(&format!(
r#"
INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, visible,
menu_type, permission, created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), t.id,
(SELECT m.id FROM menus m WHERE m.tenant_id = t.id AND m.path = '/health/ai-prompts' AND m.deleted_at IS NULL LIMIT 1),
'AI 配置', '/health/ai-config', 'SettingOutlined', 60, true,
'menu', 'ai.config.read',
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
FROM tenant t
WHERE NOT EXISTS (
SELECT 1 FROM menus m
WHERE m.path = '/health/ai-config' AND m.tenant_id = t.id AND m.deleted_at IS NULL
)
"#
)).await?;
// 菜单绑定 admin 角色
db.execute_unprepared(&format!(
r#"
INSERT INTO menu_roles (id, menu_id, role_id, created_at, updated_at, created_by, updated_by, deleted_at, version)
SELECT gen_random_uuid(), m.id, r.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 = '/health/ai-config' 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(())
}
}