feat(health): 业务链路打通 — 告警自动随访 + 健康数据积分激励
- 迁移 000157: follow_up_task 新增 source_type/source_id 字段追踪任务来源 - 迁移 000157: points_rule 新增 health_data_report/lab_report_upload/streak_7_days 种子规则 - P0-2: follow_up 事件处理器新增 health_data.critical_alert 消费,告警触发时自动创建随访任务 - 自动查找管床医生分配,critical 级别 1 天内、warning 级别 3 天内 - 新增 alert_auto 随访类型,source_type 标记来源为 critical_alert - P1-1: points 事件处理器新增 daily_monitoring.created 消费,日常监测上报自动获取积分 - P1-1: points 事件处理器新增 lab_report.uploaded 消费,化验报告上传自动获取积分 - 更新 HMS 系统设计思路文档 v2.0(实体数/链路图/业务链路章节全面更新) - 新增业务链路打通讨论记录 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,10 @@ pub struct CreateFollowUpTaskReq {
|
||||
pub planned_date: NaiveDate,
|
||||
pub content_template: Option<String>,
|
||||
pub related_appointment_id: Option<Uuid>,
|
||||
#[serde(default)]
|
||||
pub source_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl CreateFollowUpTaskReq {
|
||||
@@ -57,6 +61,10 @@ pub struct FollowUpTaskResp {
|
||||
pub status: String,
|
||||
pub content_template: Option<String>,
|
||||
pub related_appointment_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_id: Option<Uuid>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub version: i32,
|
||||
|
||||
@@ -17,6 +17,10 @@ pub struct Model {
|
||||
pub content_template: Option<String>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub related_appointment_id: Option<Uuid>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub source_type: Option<String>,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
pub source_id: Option<Uuid>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/// follow_up.overdue → 升级通知 + follow_up.created → 通知执行人
|
||||
/// health_data.critical_alert → 自动创建告警随访任务
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
|
||||
let mut handles = Vec::new();
|
||||
|
||||
@@ -139,5 +142,134 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
|
||||
}
|
||||
});
|
||||
|
||||
// health_data.critical_alert → 自动创建告警随访任务
|
||||
let (mut critical_rx, critical_handle) = state
|
||||
.event_bus
|
||||
.subscribe_filtered("health_data.".to_string());
|
||||
handles.push(critical_handle);
|
||||
let critical_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match critical_rx.recv().await {
|
||||
Some(event) if event.event_type == super::HEALTH_DATA_CRITICAL_ALERT => {
|
||||
if erp_core::events::is_event_processed(
|
||||
&critical_state.db,
|
||||
event.id,
|
||||
"critical_alert_followup_creator",
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let patient_id = event
|
||||
.payload
|
||||
.get("patient_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||
|
||||
let Some(pid) = patient_id else {
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&critical_state.db,
|
||||
event.id,
|
||||
"critical_alert_followup_creator",
|
||||
)
|
||||
.await;
|
||||
continue;
|
||||
};
|
||||
|
||||
let tenant_id = event.tenant_id;
|
||||
let alert_obj = event.payload.get("alert");
|
||||
let metric_name = alert_obj
|
||||
.and_then(|a| a.get("indicator"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
let metric_value = alert_obj
|
||||
.and_then(|a| a.get("value"))
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default();
|
||||
let severity = alert_obj
|
||||
.and_then(|a| a.get("level"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("critical");
|
||||
|
||||
// 查找管床医生
|
||||
let assigned_doctor: Option<uuid::Uuid> =
|
||||
crate::entity::patient_doctor_relation::Entity::find()
|
||||
.filter(
|
||||
crate::entity::patient_doctor_relation::Column::PatientId.eq(pid),
|
||||
)
|
||||
.filter(
|
||||
crate::entity::patient_doctor_relation::Column::TenantId
|
||||
.eq(tenant_id),
|
||||
)
|
||||
.filter(
|
||||
crate::entity::patient_doctor_relation::Column::DeletedAt.is_null(),
|
||||
)
|
||||
.one(&critical_state.db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|r| r.doctor_id);
|
||||
|
||||
let planned_date = chrono::Local::now().date_naive()
|
||||
+ chrono::Duration::days(if severity == "critical" { 1 } else { 3 });
|
||||
|
||||
let content = format!(
|
||||
"系统自动创建:患者健康数据异常告警({}: {},严重程度: {})",
|
||||
metric_name, metric_value, severity
|
||||
);
|
||||
|
||||
let req = crate::dto::follow_up_dto::CreateFollowUpTaskReq {
|
||||
patient_id: pid,
|
||||
assigned_to: assigned_doctor,
|
||||
follow_up_type: "alert_auto".to_string(),
|
||||
planned_date,
|
||||
content_template: Some(content),
|
||||
related_appointment_id: None,
|
||||
source_type: Some("critical_alert".to_string()),
|
||||
source_id: Some(event.id),
|
||||
};
|
||||
|
||||
match crate::service::follow_up_service::create_task(
|
||||
&critical_state,
|
||||
tenant_id,
|
||||
None,
|
||||
req,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(task) => {
|
||||
tracing::info!(
|
||||
patient_id = %pid,
|
||||
task_id = %task.id,
|
||||
metric = %metric_name,
|
||||
"告警自动随访任务已创建"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
patient_id = %pid,
|
||||
error = %e,
|
||||
"告警自动随访任务创建失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&critical_state.db,
|
||||
event.id,
|
||||
"critical_alert_followup_creator",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/// points.earned/exchanged/expired → 积分变动通知
|
||||
/// daily_monitoring.created → 健康数据上报积分
|
||||
/// lab_report.uploaded → 化验报告上传积分
|
||||
pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::SubscriptionHandle> {
|
||||
let mut handles = Vec::new();
|
||||
|
||||
@@ -120,5 +122,146 @@ pub fn spawn(state: &crate::state::HealthState) -> Vec<erp_core::events::Subscri
|
||||
}
|
||||
});
|
||||
|
||||
// daily_monitoring.created → 健康数据上报积分
|
||||
let (mut dm_rx, dm_handle) = state
|
||||
.event_bus
|
||||
.subscribe_filtered("daily_monitoring.".to_string());
|
||||
handles.push(dm_handle);
|
||||
let dm_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match dm_rx.recv().await {
|
||||
Some(event) if event.event_type == super::DAILY_MONITORING_CREATED => {
|
||||
if erp_core::events::is_event_processed(
|
||||
&dm_state.db,
|
||||
event.id,
|
||||
"daily_monitoring_points",
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let patient_id = event
|
||||
.payload
|
||||
.get("patient_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||
|
||||
if let Some(pid) = patient_id {
|
||||
match crate::service::points_service::earn_points(
|
||||
&dm_state,
|
||||
event.tenant_id,
|
||||
pid,
|
||||
"health_data_report",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => {
|
||||
tracing::info!(
|
||||
patient_id = %pid,
|
||||
points = tx.amount,
|
||||
"健康数据上报积分已发放"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// 无匹配规则时不告警(租户可能未配置该规则)
|
||||
let err_str = e.to_string();
|
||||
if !err_str.contains("无匹配的积分规则") {
|
||||
tracing::warn!(
|
||||
patient_id = %pid,
|
||||
error = %e,
|
||||
"健康数据上报积分发放失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&dm_state.db,
|
||||
event.id,
|
||||
"daily_monitoring_points",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// lab_report.uploaded → 化验报告上传积分
|
||||
let (mut lr_rx, lr_handle) = state
|
||||
.event_bus
|
||||
.subscribe_filtered("lab_report.".to_string());
|
||||
handles.push(lr_handle);
|
||||
let lr_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match lr_rx.recv().await {
|
||||
Some(event) if event.event_type == super::LAB_REPORT_UPLOADED => {
|
||||
if erp_core::events::is_event_processed(
|
||||
&lr_state.db,
|
||||
event.id,
|
||||
"lab_report_points",
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let patient_id = event
|
||||
.payload
|
||||
.get("patient_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||
|
||||
if let Some(pid) = patient_id {
|
||||
match crate::service::points_service::earn_points(
|
||||
&lr_state,
|
||||
event.tenant_id,
|
||||
pid,
|
||||
"lab_report_upload",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => {
|
||||
tracing::info!(
|
||||
patient_id = %pid,
|
||||
points = tx.amount,
|
||||
"化验报告上传积分已发放"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if !err_str.contains("无匹配的积分规则") {
|
||||
tracing::warn!(
|
||||
patient_id = %pid,
|
||||
error = %e,
|
||||
"化验报告上传积分发放失败"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = erp_core::events::mark_event_processed(
|
||||
&lr_state.db,
|
||||
event.id,
|
||||
"lab_report_points",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ pub async fn list_tasks(
|
||||
status: m.status,
|
||||
content_template: m.content_template,
|
||||
related_appointment_id: m.related_appointment_id,
|
||||
source_type: m.source_type,
|
||||
source_id: m.source_id,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
@@ -169,6 +171,8 @@ pub async fn get_task(
|
||||
status: m.status,
|
||||
content_template: m.content_template,
|
||||
related_appointment_id: m.related_appointment_id,
|
||||
source_type: m.source_type,
|
||||
source_id: m.source_id,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
@@ -205,6 +209,8 @@ pub async fn create_task(
|
||||
status: Set("pending".to_string()),
|
||||
content_template: Set(req.content_template),
|
||||
related_appointment_id: Set(req.related_appointment_id),
|
||||
source_type: Set(req.source_type),
|
||||
source_id: Set(req.source_id),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
@@ -249,6 +255,8 @@ pub async fn create_task(
|
||||
status: m.status,
|
||||
content_template: m.content_template,
|
||||
related_appointment_id: m.related_appointment_id,
|
||||
source_type: m.source_type,
|
||||
source_id: m.source_id,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
@@ -346,6 +354,8 @@ pub async fn update_task(
|
||||
status: m.status,
|
||||
content_template: m.content_template,
|
||||
related_appointment_id: m.related_appointment_id,
|
||||
source_type: m.source_type,
|
||||
source_id: m.source_id,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
version: m.version,
|
||||
@@ -444,6 +454,8 @@ pub async fn batch_create_tasks(
|
||||
status: Set("pending".to_string()),
|
||||
content_template: Set(req.content_template.clone()),
|
||||
related_appointment_id: Set(None),
|
||||
source_type: Set(None),
|
||||
source_id: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
@@ -736,6 +748,8 @@ pub async fn create_record(
|
||||
status: Set("pending".to_string()),
|
||||
content_template: Set(None),
|
||||
related_appointment_id: Set(None),
|
||||
source_type: Set(None),
|
||||
source_id: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
|
||||
@@ -104,6 +104,7 @@ pub fn validate_follow_up_type(value: &str) -> HealthResult<()> {
|
||||
"visit",
|
||||
"online",
|
||||
"wechat",
|
||||
"alert_auto",
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -158,6 +158,7 @@ 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;
|
||||
mod m20260520_000157_follow_up_source_and_points_rules;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -323,6 +324,7 @@ impl MigratorTrait for Migrator {
|
||||
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),
|
||||
Box::new(m20260520_000157_follow_up_source_and_points_rules::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
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> {
|
||||
// 1. follow_up_task 新增 source_type 和 source_id 字段,追踪任务来源
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("follow_up_task"))
|
||||
.add_column(ColumnDef::new(Alias::new("source_type")).string().null())
|
||||
.add_column(ColumnDef::new(Alias::new("source_id")).uuid().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. 新增积分规则种子:健康数据上报
|
||||
let insert_sql = r#"
|
||||
INSERT INTO points_rule (id, tenant_id, name, event_type, points, daily_cap, is_active, created_at, updated_at, version)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
t.id,
|
||||
'健康数据上报',
|
||||
'health_data_report',
|
||||
5,
|
||||
10,
|
||||
true,
|
||||
NOW(),
|
||||
NOW(),
|
||||
1
|
||||
FROM tenant t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM points_rule pr
|
||||
WHERE pr.event_type = 'health_data_report' AND pr.tenant_id = t.id
|
||||
);
|
||||
|
||||
INSERT INTO points_rule (id, tenant_id, name, event_type, points, daily_cap, is_active, created_at, updated_at, version)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
t.id,
|
||||
'化验报告上传',
|
||||
'lab_report_upload',
|
||||
20,
|
||||
40,
|
||||
true,
|
||||
NOW(),
|
||||
NOW(),
|
||||
1
|
||||
FROM tenant t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM points_rule pr
|
||||
WHERE pr.event_type = 'lab_report_upload' AND pr.tenant_id = t.id
|
||||
);
|
||||
|
||||
INSERT INTO points_rule (id, tenant_id, name, event_type, points, daily_cap, is_active, created_at, updated_at, version)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
t.id,
|
||||
'连续上报奖励(7天)',
|
||||
'streak_7_days',
|
||||
50,
|
||||
50,
|
||||
true,
|
||||
NOW(),
|
||||
NOW(),
|
||||
1
|
||||
FROM tenant t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM points_rule pr
|
||||
WHERE pr.event_type = 'streak_7_days' AND pr.tenant_id = t.id
|
||||
);
|
||||
"#;
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(insert_sql)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("follow_up_task"))
|
||||
.drop_column(Alias::new("source_type"))
|
||||
.drop_column(Alias::new("source_id"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
"DELETE FROM points_rule WHERE event_type IN ('health_data_report', 'lab_report_upload', 'streak_7_days');",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ fn default_create_task(patient_id: uuid::Uuid) -> CreateFollowUpTaskReq {
|
||||
planned_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 15).unwrap(),
|
||||
content_template: Some("请询问血压情况".to_string()),
|
||||
related_appointment_id: None,
|
||||
source_type: None,
|
||||
source_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -374,6 +374,8 @@ async fn test_follow_up_record_fields_encrypted() {
|
||||
planned_date: NaiveDate::from_ymd_opt(2026, 5, 1).unwrap(),
|
||||
content_template: None,
|
||||
related_appointment_id: None,
|
||||
source_type: None,
|
||||
source_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
71
docs/discussions/2026-05-20-business-chain-optimization.md
Normal file
71
docs/discussions/2026-05-20-business-chain-optimization.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 业务链路打通方案讨论
|
||||
|
||||
> 日期: 2026-05-20 | 参与者: 产品 + 技术
|
||||
|
||||
## 背景
|
||||
|
||||
系统核心功能已开发完成(59 实体 / 376+ 路由 / 990+ 测试),但各业务模块之间的联动存在 5 个关键断点。用户要求全部打通,实现真正的"数据驱动健康管理闭环"。
|
||||
|
||||
## 讨论要点
|
||||
|
||||
### 断点 1: AI 分析被动触发 → 缺少主动分析能力
|
||||
|
||||
**现状:** AI 只在用户发消息时被动工作,不会主动巡检患者数据。
|
||||
|
||||
**打通方案:**
|
||||
- 新增定时任务:每日自动巡检高风险患者(基于阈值 + 趋势)
|
||||
- 新增事件:`health.ai_alert.sent`(已有常量但未使用)→ 触发医护端推送
|
||||
- 趋势预测告警:连续 N 天数据异常 → 发布 `health_data.critical_alert` 增强版
|
||||
- AI 分析结果回写:更新患者风险评估标签
|
||||
|
||||
### 断点 2: 告警只推医护 → 患者端无反馈
|
||||
|
||||
**现状:** 告警触发后只有医护收到,患者不知道自己数据异常。
|
||||
|
||||
**打通方案:**
|
||||
- 告警事件消费:`alert.triggered` → 向患者发微信模板消息/站内通知
|
||||
- 小程序端新增"健康提醒"入口(消息 Tab 下)
|
||||
- 轻量 AI 建议:告警触发时附带一句话建议(如"您的收缩压偏高,建议低盐饮食并按时服药")
|
||||
|
||||
### 断点 3: 随访手动创建 → 缺少数据驱动自动触发
|
||||
|
||||
**现状:** 随访任务主要靠手动或定时,没有根据数据异常自动生成。
|
||||
|
||||
**打通方案:**
|
||||
- 告警 → 随访:`health_data.critical_alert` 事件被随访模块消费 → 自动创建随访任务
|
||||
- 护理计划 → 随访:`care_plan.activated` / `care_plan_item.completed` → 自动创建对应随访
|
||||
- AI 巡检 → 随访:AI 发现高风险 → 建议创建随访(需医护确认)
|
||||
|
||||
### 断点 4: 积分激励面窄 → 健康行为无积分
|
||||
|
||||
**现状:** 积分只来自签到和活动,健康数据上报没有激励。
|
||||
|
||||
**打通方案:**
|
||||
- 健康数据上报 → 积分:`daily_monitoring.created` 消费 → 调用 `earn_points("health_data_report")`
|
||||
- 连续上报奖励:连续 7/14/30 天上报额外奖励(类似签到阶梯)
|
||||
- 积分过期预警:过期前 3 天发通知 → `points.expiring_soon` 事件
|
||||
- 化验报告上传 → 积分:`lab_report.uploaded` 消费 → 积分
|
||||
|
||||
### 断点 5: 咨询孤立 → 与随访/AI 无联动
|
||||
|
||||
**现状:** 咨询是独立闭环,不能直接跳转随访或触发 AI。
|
||||
|
||||
**打通方案:**
|
||||
- 咨询 → 随访:医生端新增"创建随访"按钮 → 调用 createTask API(关联 consultation_session_id)
|
||||
- 咨询 → AI:医生可对咨询上下文触发 AI 分析(传 session_id 作为上下文)
|
||||
- 告警 → 咨询:`alert.triggered` 消费 → 患者端展示"在线咨询"快捷入口
|
||||
- AI 分析结果主动推送:`ai_analysis.completed` → SSE 推送给关注该患者的医护
|
||||
|
||||
## 优先级排序
|
||||
|
||||
| 优先级 | 断点 | 价值 | 复杂度 | 预估工时 |
|
||||
|--------|------|------|--------|---------|
|
||||
| P0 | 断点 2: 告警推患者 | 高(直接影响患者体验) | 低 | 1 天 |
|
||||
| P0 | 断点 3: 告警→自动随访 | 高(闭环核心) | 中 | 2 天 |
|
||||
| P1 | 断点 4: 健康行为积分 | 中(提升活跃度) | 低 | 1.5 天 |
|
||||
| P1 | 断点 1: AI 主动巡检 | 高(差异化竞争力) | 高 | 3 天 |
|
||||
| P2 | 断点 5: 咨询联动 | 中(效率提升) | 中 | 2 天 |
|
||||
|
||||
## 结论
|
||||
|
||||
全部 5 个断点需要打通,按 P0 → P1 → P2 分三批实施。总计约 9.5 天。
|
||||
@@ -1,6 +1,6 @@
|
||||
# HMS 健康管理平台 — 系统设计思路
|
||||
|
||||
> 版本: v1.0 | 日期: 2026-04-29 | 面向: 客户技术评审
|
||||
> 版本: v2.0 | 日期: 2026-05-20 | 面向: 客户技术评审
|
||||
|
||||
---
|
||||
|
||||
@@ -15,18 +15,19 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
```
|
||||
患者上报数据 ──→ 实时展示(趋势图/仪表盘)
|
||||
──→ 智能告警(异常指标自动通知医护)
|
||||
──→ AI 分析(趋势预测、健康报告)
|
||||
──→ AI 分析(ReAct Agent + 工具调用,趋势预测、健康报告)
|
||||
──→ 随访触发(逾期未上报自动催办)
|
||||
──→ 统计报表(运营决策依据)
|
||||
──→ 护理计划(数据驱动的个性化护理路径)
|
||||
```
|
||||
|
||||
### 1.2 三个设计原则
|
||||
|
||||
| 原则 | 含义 | 体现 |
|
||||
|------|------|------|
|
||||
| **安全先行** | 医疗数据是敏感个人信息,安全不是"加一层",而是架构的内建能力 | PII 字段加密存储、操作审计、知情同意、多租户隔离 |
|
||||
| **安全先行** | 医疗数据是敏感个人信息,安全不是"加一层",而是架构的内建能力 | PII 字段加密存储、操作审计、知情同意、多租户隔离、DTO 输入校验全覆盖 |
|
||||
| **渐进演进** | 不追求一步到位,但每一步都要为下一步打好基础 | 模块化单体架构,可按需拆分为微服务 |
|
||||
| **开放集成** | 平台不封闭,能接入外部设备和系统 | 蓝牙设备协议、OpenAPI 文档、微信生态集成 |
|
||||
| **开放集成** | 平台不封闭,能接入外部设备和系统 | BLE 蓝牙网关协议、FHIR R4 接口、OpenAPI 文档、微信生态集成 |
|
||||
|
||||
---
|
||||
|
||||
@@ -35,25 +36,33 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
### 2.1 三端覆盖
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ HMS 健康管理平台 │
|
||||
├──────────────┬──────────────┬───────────────────┤
|
||||
│ 患者端 │ 医护端 │ 管理后台 │
|
||||
│ 微信小程序 │ 微信小程序 │ PC Web │
|
||||
│ │ │ │
|
||||
│ · 健康数据 │ · 患者管理 │ · 工作台仪表盘 │
|
||||
│ · 预约服务 │ · 咨询回复 │ · 患者管理 │
|
||||
│ · 在线咨询 │ · 随访管理 │ · 健康数据中心 │
|
||||
│ · 积分商城 │ · 告警处理 │ · 内容管理 │
|
||||
│ · AI 报告 │ · 报告解读 │ · 统计报表 │
|
||||
│ · 健康资讯 │ │ · 系统管理 │
|
||||
└──────────────┴──────────────┴───────────────────┘
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ HMS 健康管理平台 │
|
||||
├──────────────┬──────────────┬────────────────────────┤
|
||||
│ 患者端 │ 医护端 │ 管理后台 │
|
||||
│ 微信小程序 │ 微信小程序 │ PC Web (React 19) │
|
||||
│ (Taro 4.2) │ (Taro 4.2) │ │
|
||||
│ │ │ │
|
||||
│ · 健康数据 │ · 患者管理 │ · 工作台仪表盘 │
|
||||
│ · 日常监测 │ · 咨询回复 │ · 患者管理 + 标签 │
|
||||
│ · 预约服务 │ · 随访管理 │ · 健康数据中心 │
|
||||
│ · 在线咨询 │ · 告警处理 │ · 预约排班 │
|
||||
│ · 积分商城 │ · 报告解读 │ · 随访模板 + 任务 │
|
||||
│ · AI 报告 │ · 透析管理 │ · 咨询管理 │
|
||||
│ · 健康资讯 │ · 护理计划 │ · 告警中心 + 规则 │
|
||||
│ · 轮播图 │ · 用药管理 │ · 内容管理 + 媒体库 │
|
||||
│ · 设备同步 │ │ · 积分商城 │
|
||||
│ · 长者模式 │ │ · 统计报表 │
|
||||
│ │ │ · AI 对话 + Copilot │
|
||||
│ │ │ · 系统管理 │
|
||||
└──────────────┴──────────────┴────────────────────────┘
|
||||
│ │ │
|
||||
└──────────────┼───────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ 统一 API 网关 │
|
||||
│ /api/v1/* │
|
||||
│ + /api/v1/fhir/* │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
@@ -61,10 +70,14 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
┌────┴────┐ ┌─────┴─────┐ ┌────┴────┐
|
||||
│ 基础模块 │ │ 核心业务 │ │ AI 模块 │
|
||||
│ │ │ │ │ │
|
||||
│ 认证权限 │ │ 健康管理 │ │ 化验解读 │
|
||||
│ 工作流 │ │ 预约排班 │ │ 趋势分析 │
|
||||
│ 消息中心 │ │ 随访管理 │ │ 报告摘要 │
|
||||
│ 系统配置 │ │ 透析管理 │ │ │
|
||||
│ 认证权限 │ │ 健康管理 │ │ AI 对话 │
|
||||
│ 工作流 │ │ 预约排班 │ │ 化验解读 │
|
||||
│ 消息中心 │ │ 随访管理 │ │ 趋势分析 │
|
||||
│ 系统配置 │ │ 咨询管理 │ │ 报告摘要 │
|
||||
│ 插件框架 │ │ 透析管理 │ │ Copilot │
|
||||
│ │ │ 内容管理 │ │ │
|
||||
│ │ │ 告警系统 │ │ │
|
||||
│ │ │ 积分商城 │ │ │
|
||||
└────┬────┘ └─────┬─────┘ └────┬────┘
|
||||
│ │ │
|
||||
└───────────────┼───────────────┘
|
||||
@@ -80,8 +93,8 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
| 层级 | 职责 | 特点 |
|
||||
|------|------|------|
|
||||
| **L1 基础层(erp-core)** | 错误体系、事件总线、模块 trait、共享类型 | 零业务依赖,所有模块的基础 |
|
||||
| **L2 业务层** | 认证、配置、工作流、消息、健康管理、AI | 各模块独立,互不依赖,通过事件总线通信 |
|
||||
| **L3 组装层(erp-server)** | Axum 入口,注册所有模块,统一路由 | 唯一的组装点,模块可插拔 |
|
||||
| **L2 业务层** | 认证、配置、工作流、消息、插件、健康管理、AI、透析 | 各模块独立,互不依赖,通过事件总线通信 |
|
||||
| **L3 组装层(erp-server)** | Axum 入口,注册 8+ 模块,统一路由,后台定时任务 | 唯一的组装点,模块可插拔 |
|
||||
|
||||
这种分层的设计意图是:**每个业务模块可以独立开发、独立测试、未来按需拆分为独立服务。** 模块之间不存在直接调用关系,所有跨模块通信都通过事件总线异步完成。
|
||||
|
||||
@@ -106,11 +119,11 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
```
|
||||
erp-core(基础层)
|
||||
|
|
||||
+------+-------+-------+------+-------+
|
||||
| | | | | |
|
||||
auth config workflow message health ai
|
||||
| | | | | |
|
||||
+------+-------+-------+------+-------+
|
||||
+------+-------+-------+------+-------+-------+
|
||||
| | | | | | |
|
||||
auth config workflow message health ai dialysis
|
||||
| | | | | | |
|
||||
+------+-------+-------+------+-------+-------+
|
||||
|
|
||||
erp-server(组装层)
|
||||
```
|
||||
@@ -158,9 +171,20 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
└──→ 统计模块:更新预约完成率
|
||||
```
|
||||
|
||||
**事件系统规模:**
|
||||
|
||||
| 指标 | 数量 |
|
||||
|------|------|
|
||||
| 事件类型 | 31 个(health 模块) |
|
||||
| 全系统事件 | 51 个 |
|
||||
| 事件发布点 | 82 个 |
|
||||
| 事件消费模块 | 12 个 |
|
||||
|
||||
**可靠性保障:**
|
||||
- 所有事件持久化到 `domain_events` 表(Outbox 模式)
|
||||
- PostgreSQL LISTEN/NOTIFY 实时投递
|
||||
- 事件处理失败自动进入 Dead Letter 存储,不丢失
|
||||
- 消费端幂等保护(`processed_events` 表去重)
|
||||
- 支持事件重放,服务重启后可恢复未处理事件
|
||||
|
||||
### 3.4 安全合规 — 内建而非外挂
|
||||
@@ -172,34 +196,40 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
| 安全能力 | 实现方式 |
|
||||
|----------|---------|
|
||||
| **身份认证** | JWT + Access Token 15 分钟 + Refresh Token 7 天轮换 |
|
||||
| **权限控制** | RBAC 角色权限 + 行级数据权限 + 按钮级控制 |
|
||||
| **数据加密** | PII 字段(身份证、手机号)AES-256-GCM 加密存储 |
|
||||
| **数据脱敏** | API 返回时自动脱敏(手机号 138****1234、身份证 ****1234) |
|
||||
| **权限控制** | RBAC 角色权限 + 行级数据权限 + 按钮级控制,140 个细粒度权限码 |
|
||||
| **数据加密** | PII 字段(身份证、手机号、咨询内容、随访记录)AES-256-GCM 加密存储 |
|
||||
| **数据脱敏** | API 返回时自动脱敏(手机号 138\*\*\*\*1234、身份证 \*\*\*\*\*1234) |
|
||||
| **操作审计** | 关键操作记录变更前后状态,哈希链防篡改 |
|
||||
| **输入防护** | SQL 注入防护(参数化查询)、XSS 防护、限流保护 |
|
||||
| **输入防护** | SQL 注入防护(参数化查询)、XSS 防护、限流保护、SSRF 防护、DTO 全量校验 |
|
||||
| **知情同意** | 患者数据处理前获取明确同意,可随时撤回 |
|
||||
| **接口安全** | SSE 支持 URL token 认证,FHIR OAuth client_credentials |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据架构
|
||||
|
||||
### 4.1 实体模型(44 个业务实体)
|
||||
### 4.1 实体模型(59 个业务实体 + 20 个 AI 实体)
|
||||
|
||||
平台围绕健康管理场景建模,覆盖从患者建档到长期随访的完整数据链路。
|
||||
|
||||
| 业务域 | 实体数 | 核心实体 |
|
||||
|--------|--------|---------|
|
||||
| 患者管理 | 8 | 患者、家属、标签、医患关系、设备绑定、知情同意 |
|
||||
| 医护管理 | 2 | 医生档案、排班 |
|
||||
| 健康数据 | 9 | 健康档案、体征记录、体征小时聚合、日常监测、化验报告、趋势、诊断、用药记录、设备读数 |
|
||||
| 患者管理 | 10 | 患者、家属、标签、标签关联、医患关系、患者分配、设备绑定、知情同意、盲索引、交接日志 |
|
||||
| 医护管理 | 3 | 医生档案、排班、班次 |
|
||||
| 健康数据 | 10 | 健康档案、体征记录、体征小时聚合、体征日聚合、日常监测、化验报告、健康趋势、诊断、用药记录、药物提醒 |
|
||||
| 透析管理 | 1 | 透析记录(独立 crate,可扩展为血透专科模块) |
|
||||
| 预约排班 | 1 | 预约(原子 CAS 并发控制,防止超额) |
|
||||
| 预约排班 | 2 | 预约、医生排班(原子 CAS 并发控制,防止超额) |
|
||||
| 随访管理 | 4 | 随访任务、随访记录、随访模板、模板字段 |
|
||||
| 咨询管理 | 2 | 咨询会话、咨询消息 |
|
||||
| 内容管理 | 5 | 文章、分类、标签、文章标签关联、文章修订 |
|
||||
| 告警系统 | 5 | 告警、告警规则、危急值告警、危急值响应、危急值阈值 |
|
||||
| 积分商城 | 6 | 积分账户、积分规则、商品、订单、积分流水、签到 |
|
||||
| 线下活动 | 2 | 活动、活动报名 |
|
||||
| 护理计划 | 3 | 护理计划、计划项目、计划结果 |
|
||||
| 媒体内容 | 3 | 媒体文件、文件夹、轮播图 |
|
||||
| 设备接入 | 3 | BLE 网关、设备读数、网关患者绑定 |
|
||||
| API 接入 | 1 | API 客户端(OAuth) |
|
||||
| **合计** | **59** | |
|
||||
|
||||
### 4.2 数据安全策略
|
||||
|
||||
@@ -210,6 +240,7 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
│ 应用层 │
|
||||
│ · JWT 认证 + RBAC 权限 + 行级数据范围 │
|
||||
│ · API 统一入口,无直接数据库访问 │
|
||||
│ · DTO 输入校验全覆盖(44 处缺失已修复) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 数据层 │
|
||||
│ · PII 字段加密(AES-256-GCM + KEK/DEK) │
|
||||
@@ -235,13 +266,142 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术选型
|
||||
## 5. 核心业务链路
|
||||
|
||||
### 5.1 患者建档链路
|
||||
|
||||
```
|
||||
患者创建请求
|
||||
│
|
||||
├── handler 层:输入校验(姓名非空、出生日期 ≤ 今天)
|
||||
├── service 层:业务校验(性别/血型枚举、身份证号 HMAC 去重)
|
||||
├── PII 加密:身份证号 + 紧急联系人电话 → AES 加密 + HMAC 盲索引
|
||||
├── 写入 patient 表 + blind_indexes 表
|
||||
├── 发布 patient.created 事件
|
||||
└── 记录审计日志
|
||||
```
|
||||
|
||||
**关键设计点:** 盲索引(HMAC-SHA256)确保加密存储后仍可按身份证号精确查重,避免重复建档。
|
||||
|
||||
### 5.2 预约排班链路(并发安全)
|
||||
|
||||
```
|
||||
预约创建请求
|
||||
│
|
||||
├── 校验患者 + 医生存在
|
||||
├── 事务内原子 CAS:
|
||||
│ UPDATE doctor_schedule
|
||||
│ SET current_appointments = current_appointments + 1
|
||||
│ WHERE current_appointments < max_appointments
|
||||
│ AND doctor_id = ? AND schedule_date = ? ...
|
||||
├── CAS 成功 → 插入 appointment 记录
|
||||
├── CAS 失败 → 返回"排班已满"
|
||||
├── 发布 appointment.created 事件
|
||||
└── 定时任务:明日预约提醒(每小时检查)
|
||||
```
|
||||
|
||||
**关键设计点:** 事务内 CAS(Compare-And-Swap)确保并发预约不会超额。取消时反向释放名额。
|
||||
|
||||
### 5.3 健康数据 → 告警链路
|
||||
|
||||
```
|
||||
体征数据上报(血压/心率/血糖...)
|
||||
│
|
||||
├── 写入 vital_signs 表
|
||||
├── 调用 check_vital_signs_alert
|
||||
│ ├── 加载 critical_value_threshold 阈值配置
|
||||
│ ├── 逐项检查收缩压/舒张压/心率/血糖
|
||||
│ └── 匹配阈值 → 发布 health_data.critical_alert 事件
|
||||
├── 告警事件消费:
|
||||
│ ├── 创建 alerts 记录
|
||||
│ ├── 发送站内通知(按严重级别分模板)
|
||||
│ ├── SSE 实时推送给管床医生
|
||||
│ └── 告警聚合(抑制重复告警)
|
||||
└── 设备数据同步 → 告警规则引擎评估(5 种设备类型)
|
||||
```
|
||||
|
||||
**关键设计点:** 三级告警(阈值告警 → 规则告警 → 告警聚合抑制),SSE 实时推送确保医护第一时间收到。
|
||||
|
||||
### 5.4 AI 智能分析链路
|
||||
|
||||
```
|
||||
用户发送消息(POST /ai/chat)
|
||||
│
|
||||
├── 配额前置检查(租户/患者维度)
|
||||
├── AI 配置加载(动态 settings 表)
|
||||
├── Provider 解析:default_provider → fallback chain(claude → openai → ollama)
|
||||
├── Tool Registry 注册 9 个工具:
|
||||
│ ├── 查询患者体征、化验报告、预约、用药
|
||||
│ ├── 分析化验报告、健康趋势
|
||||
│ ├── 获取健康洞察、搜索医学知识、查询患者档案
|
||||
├── 角色沙箱:doctor/patient/admin 限制可用工具集
|
||||
├── ReAct Agent 循环(FC provider 多轮 tool call)
|
||||
├── 非 FC provider 降级为普通对话
|
||||
├── 会话持久化(session_id 模式,50 条历史)
|
||||
└── SSE 流式输出
|
||||
```
|
||||
|
||||
**关键设计点:** ReAct Agent 架构使 AI 能够自主调用后端工具获取实时数据,不只是"聊天"而是真正的"分析"。
|
||||
|
||||
### 5.5 咨询管理链路
|
||||
|
||||
```
|
||||
患者发起咨询 → 创建 consultation_session (status=waiting)
|
||||
│
|
||||
├── 发送首条消息 → 自动切换 waiting → active
|
||||
├── handler 层通过 doctor_profile 推导 sender_role(不信任客户端)
|
||||
├── 消息内容 PII 加密后存储
|
||||
├── CAS 更新会话 unread_count + last_message_at
|
||||
├── 发布 consultation.new_message 事件
|
||||
├── 医生端:长轮询(先查 DB → 无消息则等事件总线通知)
|
||||
└── 关闭会话 → consultation.closed 事件
|
||||
```
|
||||
|
||||
**关键设计点:** 长轮询 + 事件总线混合模式,兼顾实时性和可靠性。
|
||||
|
||||
### 5.6 随访管理链路
|
||||
|
||||
```
|
||||
创建随访任务 (status=pending)
|
||||
│
|
||||
├── 发布 follow_up.created 事件
|
||||
├── 定时检查(每 6 小时):过期任务标记 overdue → 发布 follow_up.overdue
|
||||
├── 执行随访:
|
||||
│ ├── 随访结果 PII 加密后存储
|
||||
│ ├── 更新任务 status → completed
|
||||
│ ├── 如指定 next_follow_up_date → 自动创建后续任务
|
||||
│ └── 发布 follow_up.completed 事件
|
||||
└── 批量操作:批量创建(≤100)、批量分配、批量完成
|
||||
```
|
||||
|
||||
**关键设计点:** 随访闭环(创建 → 执行 → 自动创建后续 → 过期催办),结合事件驱动确保不遗漏。
|
||||
|
||||
### 5.7 积分商城链路
|
||||
|
||||
```
|
||||
签到 → 获取积分 → 兑换商品
|
||||
│
|
||||
├── 每日签到:连续天数 + 阶梯奖励(7/14/30 天)
|
||||
├── 积分获取:按规则表查找分值 → 检查 daily_cap → CAS 更新余额
|
||||
├── 积分兑换:
|
||||
│ ├── 校验商品上架 + 库存
|
||||
│ ├── FIFO 消费:按 expires_at ASC 扣减最早的 earn 交易
|
||||
│ └── 创建订单 + CAS 更新余额
|
||||
├── 线下活动签到 → 自动发放活动积分
|
||||
└── 定时清理(每 24 小时):过期积分 → 扣减余额 → 发布 points.expired
|
||||
```
|
||||
|
||||
**关键设计点:** FIFO 积分消费确保先到期的积分优先使用,CAS 原子操作保障并发安全。
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术选型
|
||||
|
||||
每一项技术选择都经过实际验证,不是纸上谈兵。
|
||||
|
||||
| 层次 | 选择 | 选择理由 |
|
||||
|------|------|---------|
|
||||
| **后端语言** | Rust | 内存安全 + 高性能,无 GC 停顿,适合医疗系统长期稳定运行 |
|
||||
| **后端语言** | Rust (edition 2024) | 内存安全 + 高性能,无 GC 停顿,适合医疗系统长期稳定运行 |
|
||||
| **Web 框架** | Axum 0.8 | Tokio 官方维护,类型安全路由,tower 中间件生态 |
|
||||
| **ORM** | SeaORM 1.1 | 异步、编译期类型检查、迁移工具链完善,SQL 错误在编译期暴露 |
|
||||
| **数据库** | PostgreSQL 18 | 企业级可靠性,JSON 支持,丰富的索引类型,医疗场景的稳妥选择 |
|
||||
@@ -249,7 +409,8 @@ HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生
|
||||
| **前端** | React 19 + Ant Design 6 | 企业后台 UI 标配,组件丰富,开发效率高 |
|
||||
| **状态管理** | Zustand 5 | 极简 API,无 boilerplate,适合中等复杂度应用 |
|
||||
| **小程序** | Taro 4.2 + React 18 | 跨端兼容,React 开发体验一致,微信生态集成成熟 |
|
||||
| **AI 集成** | SSE 流式 + 大模型 API | 化验解读、趋势分析、报告摘要,流式输出实时反馈 |
|
||||
| **AI 集成** | ReAct Agent + SSE 流式 + FC | 化验解读、趋势分析、报告摘要,Agent 自主调用后端工具 |
|
||||
| **外部标准** | FHIR R4 | 医疗数据互操作标准,8 种资源类型只读接口 |
|
||||
|
||||
**为什么选择 Rust?**
|
||||
|
||||
@@ -262,18 +423,32 @@ Rust 在医疗系统中的独特价值:
|
||||
|
||||
---
|
||||
|
||||
## 6. 质量保障
|
||||
## 7. 质量保障
|
||||
|
||||
### 6.1 测试金字塔
|
||||
### 7.1 测试体系
|
||||
|
||||
| 测试类型 | 数量 | 覆盖目标 |
|
||||
|----------|------|---------|
|
||||
| 单元测试 | 225+ | 每个 service 函数、验证逻辑、脱敏逻辑 |
|
||||
| 集成测试 | 159+ | API 端点 → 数据库完整链路,使用真实 PostgreSQL |
|
||||
| 后端测试函数 | 990+ 个(802 同步 + 188 异步) | 每个 service 函数、验证逻辑、脱敏逻辑 |
|
||||
| 后端测试文件 | 110 个 | 覆盖全部业务模块 |
|
||||
| Web 前端单元测试 | 62 文件(~693 断言) | 组件、hooks、API 层 |
|
||||
| E2E 测试 | 17 套 spec(~64 断言) | Web 13 套 + 小程序 4 套 |
|
||||
| 多租户测试 | 独立测试 crate | 数据隔离验证,确保跨租户不可见 |
|
||||
| E2E 测试 | 5 套 | 前端关键用户流程(Playwright) |
|
||||
| 角色权限测试 | R01-R05 五角色全覆盖 | 86.5% 通过率,5 个 BUG 已修复 |
|
||||
| 小程序多角色测试 | MP 多角色验证 | 96.2% 通过率 |
|
||||
|
||||
### 6.2 验证机制
|
||||
### 7.2 代码质量
|
||||
|
||||
| 指标 | 状态 |
|
||||
|------|------|
|
||||
| Clippy 警告 | 全 workspace 0 警告 |
|
||||
| utoipa 注解 | 94 个文件含 OpenAPI 注解 |
|
||||
| 权限码 | 140 个(health 57 + ai 21 + auth 24 + 其他 38) |
|
||||
| 依赖版本 | 全部最新主版本线 |
|
||||
| UI 合规审计 | 60 页面全覆盖,评分 95/100 |
|
||||
| 长者模式 | 58/58 页面 100% 覆盖 |
|
||||
|
||||
### 7.3 验证机制
|
||||
|
||||
每个功能交付前必须通过:
|
||||
|
||||
@@ -283,7 +458,7 @@ Rust 在医疗系统中的独特价值:
|
||||
- 生产构建 — `pnpm build` 前端生产构建通过
|
||||
- 数据库验证 — 迁移可正向/反向执行
|
||||
|
||||
### 6.3 关键数据保障
|
||||
### 7.4 关键数据保障
|
||||
|
||||
| 保障项 | 机制 |
|
||||
|--------|------|
|
||||
@@ -291,6 +466,41 @@ Rust 在医疗系统中的独特价值:
|
||||
| 数据不丢失 | 软删除 + 事件 Outbox 持久化 + Dead Letter |
|
||||
| 数据不篡改 | 乐观锁(version 字段)+ 审计日志哈希链 |
|
||||
| 接口一致性 | 统一 `ApiResponse<T>` 包装,OpenAPI 文档自动生成 |
|
||||
| 输入安全 | DTO 全量 Validate derive + handler 层 validate() 调用 + SSRF 防护 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 项目规模
|
||||
|
||||
### 8.1 关键数字
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| Rust crate | 17 个 |
|
||||
| Rust 源文件 | ~694 个(~125,000 行) |
|
||||
| 数据库表 | 95 个(30 基础 + 49 健康 + 13 AI + 3 媒体) |
|
||||
| 数据库迁移 | 156 个 |
|
||||
| 后端路由 | 376+ 个(11 公开 + 14 FHIR + 2 网关 + ~350 受保护) |
|
||||
| 后端测试 | 990+ 个函数 |
|
||||
| Web 前端 | 316 个 TS/TSX 文件,36 活跃路由,161 页面 |
|
||||
| 微信小程序 | 167 个 TS/TSX 文件,59 页面(12 主包 + 47 子包) |
|
||||
| 事件类型 | 51 个全系统 / 82 发布点 / 12 消费模块 |
|
||||
| 权限码 | 140 个 |
|
||||
| Git 提交 | 927+ 次 |
|
||||
|
||||
### 8.2 开发进度
|
||||
|
||||
| 阶段 | 状态 |
|
||||
|------|------|
|
||||
| 基础层(auth/config/workflow/message) | 已完成 |
|
||||
| 健康管理核心模块(erp-health) | 已完成(59 实体 / 31 handler / 37 service) |
|
||||
| AI 智能分析模块(erp-ai) | 已完成(20 实体 / ReAct Agent / 4 Provider) |
|
||||
| 透析管理模块(erp-dialysis) | 已完成 |
|
||||
| Web 管理后台 | 已完成(36 活跃路由) |
|
||||
| 患者端小程序 | 已完成(59 页面 + 长者模式) |
|
||||
| 医护端小程序 | 已完成(分包,独立导航栏) |
|
||||
| 全系统审计 V2 | 已完成(85%,P0 安全修复已完成) |
|
||||
| 系统评分 | 6.3/10 (B-),V1 CONDITIONAL GO |
|
||||
|
||||
---
|
||||
|
||||
@@ -298,16 +508,9 @@ Rust 在医疗系统中的独特价值:
|
||||
|
||||
HMS 平台的设计围绕四个关键词展开:
|
||||
|
||||
1. **模块化** — 每个业务模块独立自治,通过事件总线协作,可按需拆分演进
|
||||
2. **安全合规** — 加密存储、操作审计、知情同意、多租户隔离,从架构内建安全能力
|
||||
3. **数据驱动** — 每条数据都有明确用途:展示、告警、分析、触发、统计
|
||||
1. **模块化** — 17 个 crate、8 个独立业务模块,通过事件总线协作,可按需拆分演进
|
||||
2. **安全合规** — PII 加密、操作审计、知情同意、多租户隔离、DTO 全量校验,从架构内建安全能力
|
||||
3. **数据驱动** — 59 个业务实体覆盖全链路,每条数据都有明确用途:展示、告警、分析、触发、统计
|
||||
4. **渐进演进** — 当前是模块化单体,未来可按模块独立拆分为微服务,无需重写
|
||||
|
||||
平台已完成核心功能开发,包括 44 个业务实体、25+ 管理后台页面、40 个小程序页面、225+ 单元测试。具备投入试运行的基础条件。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
平台已完成核心功能开发:59 个业务实体、376+ 个 API 端点、36 个管理后台路由、59 个小程序页面、990+ 个后端测试。系统通过 V2 审计(85% 完成度),具备投入试运行的基础条件。
|
||||
|
||||
Reference in New Issue
Block a user