Files
hms/crates/erp-health/src/handler/health_data_handler.rs
iven 6d5a711d2c
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
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

512 lines
15 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::DeleteWithVersion;
use crate::dto::health_data_dto::*;
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 review_lab_report<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path((_patient_id, rid)): Path<(Uuid, Uuid)>,
Json(req): Json<ReviewLabReportWithVersion>,
) -> 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::review_lab_report(
&state,
ctx.tenant_id,
_patient_id,
rid,
ctx.user_id,
data,
req.version,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 健康档案
// ---------------------------------------------------------------------------
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>,
Query(params): Query<MiniTodayParams>,
) -> 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, params.patient_id)
.await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 小程序今日体征请求参数
#[derive(Debug, serde::Deserialize, utoipa::IntoParams)]
pub struct MiniTodayParams {
/// 可选:直接指定患者 ID小程序传入当前选中患者
pub patient_id: Option<Uuid>,
}
// ---------------------------------------------------------------------------
// 带版本号的更新请求包装
// ---------------------------------------------------------------------------
#[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,
}
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct ReviewLabReportWithVersion {
#[serde(flatten)]
pub data: ReviewLabReportReq,
pub version: i32,
}