Files
hms/crates/erp-health/src/handler/health_data_handler.rs
iven 07f4ba41ba
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(health): 穷尽审计修复 — 权限同步/编译错误/前端bug/审计日志
审计发现并修复的问题:

HIGH:
- H1: ConsultationDetail 使用 getSession(id) 替代错误的列表搜索
- H2: SessionResp 添加 version/updated_at 字段
- H3: 移除 FollowUpRecordList 调用不存在的导出端点
- H4: 新增 articles.ts 前端 API 模块

MEDIUM:
- M1: article delete 添加乐观锁 (expected_version)
- M2: 取消预约排班释放传播错误 (log::warn -> ?)
- M3: FollowUpTaskList 日期格式 Dayjs -> string
- M4: 补充 15 个缺失审计日志

LOW:
- L1: 替换 follow_up_service 中的 .unwrap()
- L2: PatientListItem 添加 version 字段

CRITICAL (新发现):
- 权限未同步: 健康模块 14 个权限从未写入数据库,添加启动时自动同步
- migration 表名错误: patients -> patient
- 编译错误: health_trend entity 未导入, ToPrimitive trait 未导入
- HealthError 缺少 From<AppError> 实现
2026-04-25 08:58:58 +08:00

407 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::Deserialize;
use utoipa::IntoParams;
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::dto::health_data_dto::*;
use crate::dto::DeleteWithVersion;
use crate::service::health_data_service;
use crate::service::trend_service;
use crate::state::HealthState;
// ---------------------------------------------------------------------------
// 查询参数
// ---------------------------------------------------------------------------
#[derive(Debug, Deserialize, IntoParams)]
pub struct PaginationParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct IndicatorTimeseriesParams {
pub start_date: Option<chrono::NaiveDate>,
pub end_date: Option<chrono::NaiveDate>,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct GenerateTrendReq {
pub period_start: chrono::NaiveDate,
pub period_end: chrono::NaiveDate,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateWithVersion<T> {
pub data: T,
pub version: i32,
}
// ---------------------------------------------------------------------------
// 生命体征
// ---------------------------------------------------------------------------
pub async fn list_vital_signs<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<VitalSignsResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = health_data_service::list_vital_signs(
&state, ctx.tenant_id, patient_id, page, page_size,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn create_vital_signs<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Json(req): Json<CreateVitalSignsReq>,
) -> Result<Json<ApiResponse<VitalSignsResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let mut req = req;
req.sanitize();
let result = health_data_service::create_vital_signs(
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn update_vital_signs<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((patient_id, vid)): Path<(Uuid, Uuid)>,
Json(req): Json<UpdateVitalSignsWithVersion>,
) -> Result<Json<ApiResponse<VitalSignsResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let mut data = req.data;
data.sanitize();
let result = health_data_service::update_vital_signs(
&state, ctx.tenant_id, patient_id, vid, Some(ctx.user_id), data, req.version,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn delete_vital_signs<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((_patient_id, vid)): Path<(Uuid, Uuid)>,
Json(req): Json<DeleteWithVersion>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
health_data_service::delete_vital_signs(&state, ctx.tenant_id, vid, Some(ctx.user_id), req.version).await?;
Ok(Json(ApiResponse::ok(())))
}
// ---------------------------------------------------------------------------
// 化验报告
// ---------------------------------------------------------------------------
pub async fn list_lab_reports<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<LabReportResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = health_data_service::list_lab_reports(
&state, ctx.tenant_id, patient_id, page, page_size,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn create_lab_report<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Json(req): Json<CreateLabReportReq>,
) -> Result<Json<ApiResponse<LabReportResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let mut req = req;
req.sanitize();
let result = health_data_service::create_lab_report(
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn update_lab_report<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((_patient_id, rid)): Path<(Uuid, Uuid)>,
Json(req): Json<UpdateLabReportWithVersion>,
) -> Result<Json<ApiResponse<LabReportResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let mut data = req.data;
data.sanitize();
let result = health_data_service::update_lab_report(
&state, ctx.tenant_id, _patient_id, rid, Some(ctx.user_id), data, req.version,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn delete_lab_report<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((_patient_id, rid)): Path<(Uuid, Uuid)>,
Json(req): Json<DeleteWithVersion>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
health_data_service::delete_lab_report(&state, ctx.tenant_id, rid, Some(ctx.user_id), req.version).await?;
Ok(Json(ApiResponse::ok(())))
}
// ---------------------------------------------------------------------------
// 健康档案
// ---------------------------------------------------------------------------
pub async fn list_health_records<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<HealthRecordResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = health_data_service::list_health_records(
&state, ctx.tenant_id, patient_id, page, page_size,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn create_health_record<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Json(req): Json<CreateHealthRecordReq>,
) -> Result<Json<ApiResponse<HealthRecordResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let mut req = req;
req.sanitize();
let result = health_data_service::create_health_record(
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn update_health_record<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((patient_id, rid)): Path<(Uuid, Uuid)>,
Json(req): Json<UpdateHealthRecordWithVersion>,
) -> Result<Json<ApiResponse<HealthRecordResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let mut data = req.data;
data.sanitize();
let result = health_data_service::update_health_record(
&state, ctx.tenant_id, patient_id, rid, Some(ctx.user_id), data, req.version,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn delete_health_record<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((_patient_id, rid)): Path<(Uuid, Uuid)>,
Json(req): Json<DeleteWithVersion>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
health_data_service::delete_health_record(&state, ctx.tenant_id, rid, Some(ctx.user_id), req.version).await?;
Ok(Json(ApiResponse::ok(())))
}
// ---------------------------------------------------------------------------
// 趋势分析
// ---------------------------------------------------------------------------
pub async fn list_trends<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<TrendResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = trend_service::list_trends(
&state, ctx.tenant_id, patient_id, page, page_size,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn generate_trend<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Json(req): Json<GenerateTrendReq>,
) -> Result<Json<ApiResponse<TrendResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let result = trend_service::generate_trend(
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req.period_start, req.period_end,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn get_indicator_timeseries<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((patient_id, indicator)): Path<(Uuid, String)>,
Query(params): Query<IndicatorTimeseriesParams>,
) -> Result<Json<ApiResponse<IndicatorTimeseriesResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let result = trend_service::get_indicator_timeseries(
&state, ctx.tenant_id, patient_id, indicator, params.start_date, params.end_date,
)
.await?;
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 = trend_service::get_mini_trend(
&state, ctx.tenant_id, ctx.user_id, params.indicator, params.range,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 小程序今日体征摘要(通过当前用户关联 patient无需传 patient_id
// ---------------------------------------------------------------------------
pub async fn get_mini_today<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<MiniTodayResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let result = trend_service::get_mini_today(
&state, ctx.tenant_id, ctx.user_id,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 带版本号的更新请求包装
// ---------------------------------------------------------------------------
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateVitalSignsWithVersion {
#[serde(flatten)]
pub data: UpdateVitalSignsReq,
pub version: i32,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateLabReportWithVersion {
#[serde(flatten)]
pub data: UpdateLabReportReq,
pub version: i32,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateHealthRecordWithVersion {
#[serde(flatten)]
pub data: UpdateHealthRecordReq,
pub version: i32,
}