fix(health): 修复 5 角色深度测试发现的 8 个问题
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

P0 修复:
- 告警状态机新增 active 合法状态 + 转换规则 (active→acknowledged/dismissed)
- 前端路由守卫改为默认拒绝,未注册路由返回 403

P1 修复:
- 侧边栏菜单根据用户权限码过滤,非 admin 隐藏无权限菜单项
- Critical-alerts handler 增加详细错误日志 + div_ceil 安全防护
- 仪表盘统计 API 调用使用 silent 模式避免 500 触发全局 toast

P2 修复:
- 随访类型映射新增 visit → 上门 (前后端同步)
- 随访 fallback 选项新增 visit 类型

排除的假 BUG (代码已正确):
- 患者性别/血型: MCP fill() 不兼容 Select 组件,正常交互正确
- 随访筛选/对话框关闭: 代码逻辑验证正确

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-07 08:24:12 +08:00
parent 0acf901893
commit 85a7dacd16
8 changed files with 292 additions and 32 deletions

View File

@@ -34,14 +34,20 @@ where
let (items, total) = critical_alert_service::list_pending_alerts(
&state, ctx.tenant_id, page, page_size,
)
.await?;
.await
.map_err(|e| {
tracing::error!(error = %e, tenant_id = %ctx.tenant_id, "查询危急值告警列表失败");
e
})?;
let total_pages = if page_size > 0 { total.div_ceil(page_size) } else { 0 };
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
data: items,
total,
page,
page_size,
total_pages: total.div_ceil(page_size.max(1)),
total_pages,
})))
}

View File

@@ -82,7 +82,7 @@ pub fn validate_schedule_status(value: &str) -> HealthResult<()> {
/// follow_up_task.follow_up_type
pub fn validate_follow_up_type(value: &str) -> HealthResult<()> {
validate_enum!(value, "follow_up_type", [
"phone", "outpatient", "home_visit", "online", "wechat",
"phone", "outpatient", "home_visit", "visit", "online", "wechat",
]);
Ok(())
}
@@ -231,18 +231,18 @@ pub fn validate_alert_severity(value: &str) -> HealthResult<()> {
/// alert.status
pub fn validate_alert_status(value: &str) -> HealthResult<()> {
validate_enum!(value, "alert_status", [
"pending", "acknowledged", "resolved", "dismissed",
"pending", "active", "acknowledged", "resolved", "dismissed",
]);
Ok(())
}
/// 告警状态转换校验: pending→acknowledged/dismissed, acknowledged→resolved/dismissed
/// 告警状态转换校验: pending/active→acknowledged/dismissed, acknowledged→resolved/dismissed
pub fn validate_alert_status_transition(current: &str, next: &str) -> HealthResult<()> {
if current == next {
return Ok(());
}
let allowed = match current {
"pending" => matches!(next, "acknowledged" | "dismissed"),
"pending" | "active" => matches!(next, "acknowledged" | "dismissed"),
"acknowledged" => matches!(next, "resolved" | "dismissed"),
_ => false,
};
@@ -477,6 +477,8 @@ mod tests {
#[test]
fn alert_status_pending() { assert!(validate_alert_status("pending").is_ok()); }
#[test]
fn alert_status_active() { assert!(validate_alert_status("active").is_ok()); }
#[test]
fn alert_status_resolved() { assert!(validate_alert_status("resolved").is_ok()); }
#[test]
fn alert_status_invalid() { assert!(validate_alert_status("open").is_err()); }
@@ -487,6 +489,10 @@ mod tests {
#[test]
fn alert_pending_to_dismissed() { assert!(validate_alert_status_transition("pending", "dismissed").is_ok()); }
#[test]
fn alert_active_to_acknowledged() { assert!(validate_alert_status_transition("active", "acknowledged").is_ok()); }
#[test]
fn alert_active_to_dismissed() { assert!(validate_alert_status_transition("active", "dismissed").is_ok()); }
#[test]
fn alert_pending_to_resolved_fails() { assert!(validate_alert_status_transition("pending", "resolved").is_err()); }
#[test]
fn alert_acknowledged_to_resolved() { assert!(validate_alert_status_transition("acknowledged", "resolved").is_ok()); }