feat(health+miniprogram): 健康数据录入 + 趋势图
后端: - 新增 GET /health/vital-signs/trend 小程序趋势查询 API - 通过 JWT user_id 自动关联 patient,支持 range 参数 (7d/30d/90d) - 新增 MiniTrendQueryParams, MiniTrendResp, DataPoint DTO 前端: - 实现健康数据首页(今日概览 + 趋势入口 + 录入按钮) - 实现健康数据录入页(指标选择 + 数值输入 + 提交) - 实现趋势图页(时间范围切换 + 柱状图 + 数据列表) - 新增 health service 和 store(趋势缓存 + 今日摘要) - 修复所有页面相对路径引用问题
This commit is contained in:
@@ -121,3 +121,34 @@ pub struct IndicatorTimeseriesResp {
|
||||
pub indicator: String,
|
||||
pub data: Vec<(NaiveDate, f64)>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 小程序趋势查询(通过当前用户关联 patient)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 小程序趋势查询参数
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MiniTrendQueryParams {
|
||||
/// 指标名称,如 "blood_pressure_systolic", "heart_rate" 等
|
||||
pub indicator: String,
|
||||
/// 时间范围:"7d"(默认), "30d", "90d"
|
||||
pub range: Option<String>,
|
||||
}
|
||||
|
||||
/// 小程序趋势数据点
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DataPoint {
|
||||
/// 日期,格式 YYYY-MM-DD
|
||||
pub date: String,
|
||||
/// 指标数值
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
/// 小程序趋势响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MiniTrendResp {
|
||||
/// 指标名称
|
||||
pub indicator: String,
|
||||
/// 数据点列表(按日期升序)
|
||||
pub data_points: Vec<DataPoint>,
|
||||
}
|
||||
|
||||
@@ -322,6 +322,27 @@ where
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 小程序趋势查询(通过当前用户关联 patient,无需传 patient_id)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn get_mini_trend<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Query(params): Query<MiniTrendQueryParams>,
|
||||
) -> Result<Json<ApiResponse<MiniTrendResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
let result = health_data_service::get_mini_trend(
|
||||
&state, ctx.tenant_id, ctx.user_id, params.indicator, params.range,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 带版本号的更新请求包装
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -112,6 +112,11 @@ impl HealthModule {
|
||||
"/health/patients/{id}/trends/{indicator}",
|
||||
axum::routing::get(health_data_handler::get_indicator_timeseries),
|
||||
)
|
||||
// 小程序趋势查询(通过 JWT user_id 关联 patient,无需传 patient_id)
|
||||
.route(
|
||||
"/health/vital-signs/trend",
|
||||
axum::routing::get(health_data_handler::get_mini_trend),
|
||||
)
|
||||
// 预约排班
|
||||
.route(
|
||||
"/health/appointments",
|
||||
|
||||
@@ -591,3 +591,94 @@ pub async fn get_indicator_timeseries(
|
||||
|
||||
Ok(IndicatorTimeseriesResp { indicator, data })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 小程序趋势查询(通过 user_id 关联 patient)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 根据 user_id 查找关联的 patient_id。
|
||||
/// patient 表的 user_id 字段关联 erp-auth 的用户。
|
||||
/// 如果未关联则返回 Ok(None)。
|
||||
async fn find_patient_by_user_id(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> HealthResult<Option<Uuid>> {
|
||||
let patient_model = patient::Entity::find()
|
||||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient::Column::UserId.eq(user_id))
|
||||
.filter(patient::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?;
|
||||
|
||||
Ok(patient_model.map(|p| p.id))
|
||||
}
|
||||
|
||||
/// 解析 range 参数为天数,默认 7 天。
|
||||
/// 支持 "7d", "30d", "90d" 格式。
|
||||
fn parse_range_days(range: &Option<String>) -> i64 {
|
||||
match range.as_deref() {
|
||||
Some("30d") => 30,
|
||||
Some("90d") => 90,
|
||||
// 默认 7 天(包括 "7d" 和 None)
|
||||
_ => 7,
|
||||
}
|
||||
}
|
||||
|
||||
/// 小程序趋势查询:通过当前用户的 user_id 关联 patient,查询指定指标的时间序列。
|
||||
///
|
||||
/// 逻辑流程:
|
||||
/// 1. 解析 range 参数计算 start_date/end_date
|
||||
/// 2. 通过 user_id 查找关联的 patient(patient.user_id 字段)
|
||||
/// 3. 复用 get_indicator_timeseries 的查询逻辑
|
||||
/// 4. 转换为 DataPoint 格式返回
|
||||
pub async fn get_mini_trend(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
indicator: String,
|
||||
range: Option<String>,
|
||||
) -> HealthResult<MiniTrendResp> {
|
||||
// 1. 通过 user_id 查找关联的 patient
|
||||
let patient_id = find_patient_by_user_id(state, tenant_id, user_id).await?;
|
||||
|
||||
// 如果用户未关联 patient,返回空数据
|
||||
let Some(patient_id) = patient_id else {
|
||||
return Ok(MiniTrendResp {
|
||||
indicator,
|
||||
data_points: vec![],
|
||||
});
|
||||
};
|
||||
|
||||
// 2. 根据 range 计算日期范围
|
||||
let days = parse_range_days(&range);
|
||||
let today = chrono::Local::now().date_naive();
|
||||
let start_date = today - chrono::Duration::days(days);
|
||||
let end_date = today;
|
||||
|
||||
// 3. 复用已有逻辑查询时间序列数据
|
||||
let timeseries = get_indicator_timeseries(
|
||||
state,
|
||||
tenant_id,
|
||||
patient_id,
|
||||
indicator.clone(),
|
||||
Some(start_date),
|
||||
Some(end_date),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 4. 转换为 DataPoint 格式
|
||||
let data_points = timeseries
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|(date, value)| DataPoint {
|
||||
date: date.to_string(),
|
||||
value,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(MiniTrendResp {
|
||||
indicator,
|
||||
data_points,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user