refactor(dialysis+health): 透析统计从 erp-health 迁移到 erp-dialysis,消除跨 crate 残留
- erp-dialysis: 新建 dialysis_stats_dto/handler/service,注册 /health/admin/statistics/dialysis 路由 - erp-health: 删除 get_dialysis_statistics 及 helper、DialysisStatisticsResp、 DialysisRecordNotFound/DialysisPrescriptionNotFound、validate_dialysis_status* 及 9 个测试、 DoctorDashboard.pending_dialysis_review、module 路由 - Web: HealthDataStats 移除 dialysis 字段,新增 getDialysisStats() 独立 API, useStatsData 并行 fetch,HealthDataCenter 接受独立 dialysisData prop - 小程序: DoctorDashboard 移除 pending_dialysis_review,医护工作台移除"待审透析"卡片
This commit is contained in:
@@ -21,7 +21,6 @@ const CARDS: CardConfig[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const HEALTH_CARDS: CardConfig[] = [
|
const HEALTH_CARDS: CardConfig[] = [
|
||||||
{ key: 'pending_dialysis_review', label: '待审透析', initial: '透', route: '/pages/doctor/patients/index' },
|
|
||||||
{ key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/doctor/report/index' },
|
{ key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/doctor/report/index' },
|
||||||
{ key: 'today_appointments', label: '今日预约', initial: '约', route: '/pages/doctor/patients/index' },
|
{ key: 'today_appointments', label: '今日预约', initial: '约', route: '/pages/doctor/patients/index' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export interface DoctorDashboard {
|
|||||||
unread_messages: number;
|
unread_messages: number;
|
||||||
pending_follow_ups: number;
|
pending_follow_ups: number;
|
||||||
today_consultations: number;
|
today_consultations: number;
|
||||||
pending_dialysis_review: number;
|
|
||||||
pending_lab_review: number;
|
pending_lab_review: number;
|
||||||
today_appointments: number;
|
today_appointments: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ export interface VitalSignsReportRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HealthDataStats {
|
export interface HealthDataStats {
|
||||||
dialysis: DialysisStatistics;
|
|
||||||
lab_reports: LabReportStatistics;
|
lab_reports: LabReportStatistics;
|
||||||
appointments: AppointmentStatistics;
|
appointments: AppointmentStatistics;
|
||||||
vital_signs_report_rate: VitalSignsReportRate;
|
vital_signs_report_rate: VitalSignsReportRate;
|
||||||
@@ -376,6 +375,14 @@ export const pointsApi = {
|
|||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDialysisStats: async (): Promise<DialysisStatistics> => {
|
||||||
|
const { data } = await client.get<{
|
||||||
|
success: boolean;
|
||||||
|
data: DialysisStatistics;
|
||||||
|
}>('/health/admin/statistics/dialysis');
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
|
||||||
getPersonalStats: async (): Promise<PersonalStats> => {
|
getPersonalStats: async (): Promise<PersonalStats> => {
|
||||||
const { data } = await client.get<{
|
const { data } = await client.get<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useCountUp } from '../../../hooks/useCountUp';
|
|||||||
import HealthDataCenter from './HealthDataCenter';
|
import HealthDataCenter from './HealthDataCenter';
|
||||||
|
|
||||||
export function AdminDashboard() {
|
export function AdminDashboard() {
|
||||||
const { patientStats, followUpStats, healthDataStats, loading } = useStatsData();
|
const { patientStats, followUpStats, healthDataStats, dialysisStats, loading } = useStatsData();
|
||||||
const patientCount = useCountUp(patientStats?.total_patients ?? 0);
|
const patientCount = useCountUp(patientStats?.total_patients ?? 0);
|
||||||
const appointmentCount = useCountUp(healthDataStats?.appointments?.this_month ?? 0);
|
const appointmentCount = useCountUp(healthDataStats?.appointments?.this_month ?? 0);
|
||||||
const doctorCount = useCountUp(0);
|
const doctorCount = useCountUp(0);
|
||||||
@@ -72,7 +72,7 @@ export function AdminDashboard() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
defaultActiveKey="dialysis"
|
defaultActiveKey="dialysis"
|
||||||
items={[
|
items={[
|
||||||
{ key: 'dialysis', label: '透析管理', children: <HealthDataCenter data={healthDataStats} tab="dialysis" /> },
|
{ key: 'dialysis', label: '透析管理', children: <HealthDataCenter data={healthDataStats} dialysisData={dialysisStats} tab="dialysis" /> },
|
||||||
{ key: 'lab', label: '化验报告', children: <HealthDataCenter data={healthDataStats} tab="lab" /> },
|
{ key: 'lab', label: '化验报告', children: <HealthDataCenter data={healthDataStats} tab="lab" /> },
|
||||||
{ key: 'appointments', label: '预约分析', children: <HealthDataCenter data={healthDataStats} tab="appointments" /> },
|
{ key: 'appointments', label: '预约分析', children: <HealthDataCenter data={healthDataStats} tab="appointments" /> },
|
||||||
{ key: 'vital-signs', label: '体征数据', children: <HealthDataCenter data={healthDataStats} tab="vital-signs" /> },
|
{ key: 'vital-signs', label: '体征数据', children: <HealthDataCenter data={healthDataStats} tab="vital-signs" /> },
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
import { Row, Col, Card, Statistic, Tag, Typography, Empty } from 'antd';
|
import { Row, Col, Card, Statistic, Tag, Typography, Empty } from 'antd';
|
||||||
import type { HealthDataStats } from '../../../api/health/points';
|
import type { HealthDataStats, DialysisStatistics } from '../../../api/health/points';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface HealthDataCenterProps {
|
interface HealthDataCenterProps {
|
||||||
data: HealthDataStats | null;
|
data: HealthDataStats | null;
|
||||||
|
dialysisData?: DialysisStatistics | null;
|
||||||
tab?: string;
|
tab?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialysisPanel({ data }: { data: HealthDataStats | null }) {
|
function DialysisPanel({ data }: { data: DialysisStatistics | null | undefined }) {
|
||||||
return (
|
return (
|
||||||
<Card type="inner" title={<span style={{ fontSize: 14, fontWeight: 600 }}>透析记录</span>} style={{ borderRadius: 8 }}>
|
<Card type="inner" title={<span style={{ fontSize: 14, fontWeight: 600 }}>透析记录</span>} style={{ borderRadius: 8 }}>
|
||||||
<Row gutter={[12, 12]}>
|
<Row gutter={[12, 12]}>
|
||||||
<Col span={8}><Statistic title="总记录" value={data?.dialysis.total_records ?? 0} valueStyle={{ fontSize: 20 }} /></Col>
|
<Col span={8}><Statistic title="总记录" value={data?.total_records ?? 0} valueStyle={{ fontSize: 20 }} /></Col>
|
||||||
<Col span={8}><Statistic title="本月新增" value={data?.dialysis.this_month ?? 0} valueStyle={{ fontSize: 20, color: '#2563eb' }} /></Col>
|
<Col span={8}><Statistic title="本月新增" value={data?.this_month ?? 0} valueStyle={{ fontSize: 20, color: '#2563eb' }} /></Col>
|
||||||
<Col span={8}><Statistic title="待审核" value={data?.dialysis.pending_review ?? 0} valueStyle={{ fontSize: 20, color: '#d97706' }} /></Col>
|
<Col span={8}><Statistic title="待审核" value={data?.pending_review ?? 0} valueStyle={{ fontSize: 20, color: '#d97706' }} /></Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row gutter={[12, 12]} style={{ marginTop: 12 }}>
|
<Row gutter={[12, 12]} style={{ marginTop: 12 }}>
|
||||||
<Col span={8}><Statistic title="并发症率" value={data?.dialysis.complication_rate ?? 0} suffix="%" precision={1} valueStyle={{ fontSize: 18 }} /></Col>
|
<Col span={8}><Statistic title="并发症率" value={data?.complication_rate ?? 0} suffix="%" precision={1} valueStyle={{ fontSize: 18 }} /></Col>
|
||||||
<Col span={8}><Statistic title="平均超滤(ml)" value={data?.dialysis.avg_ultrafiltration ?? 0} precision={0} valueStyle={{ fontSize: 18 }} /></Col>
|
<Col span={8}><Statistic title="平均超滤(ml)" value={data?.avg_ultrafiltration ?? 0} precision={0} valueStyle={{ fontSize: 18 }} /></Col>
|
||||||
<Col span={8}><Statistic title="平均时长(分)" value={data?.dialysis.avg_duration ?? 0} precision={0} valueStyle={{ fontSize: 18 }} /></Col>
|
<Col span={8}><Statistic title="平均时长(分)" value={data?.avg_duration ?? 0} precision={0} valueStyle={{ fontSize: 18 }} /></Col>
|
||||||
</Row>
|
</Row>
|
||||||
{(data?.dialysis.type_distribution ?? []).length > 0 && (
|
{(data?.type_distribution ?? []).length > 0 && (
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>类型分布: </Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>类型分布: </Text>
|
||||||
{data!.dialysis.type_distribution.map((item) => (
|
{data!.type_distribution.map((item) => (
|
||||||
<Tag key={item.name} color="blue" style={{ marginTop: 4 }}>{item.name}: {item.value}</Tag>
|
<Tag key={item.name} color="blue" style={{ marginTop: 4 }}>{item.name}: {item.value}</Tag>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -101,15 +102,24 @@ function VitalSignsPanel({ data }: { data: HealthDataStats | null }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAB_PANELS: Record<string, React.FC<{ data: HealthDataStats | null }>> = {
|
export default function HealthDataCenter({ data, dialysisData, tab = 'dialysis' }: HealthDataCenterProps) {
|
||||||
dialysis: DialysisPanel,
|
if (tab === 'dialysis') {
|
||||||
lab: LabPanel,
|
return (
|
||||||
appointments: AppointmentsPanel,
|
<Row gutter={[16, 16]}>
|
||||||
'vital-signs': VitalSignsPanel,
|
<Col span={24}>
|
||||||
};
|
<DialysisPanel data={dialysisData} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function HealthDataCenter({ data, tab = 'dialysis' }: HealthDataCenterProps) {
|
const PANELS: Record<string, React.FC<{ data: HealthDataStats | null }>> = {
|
||||||
const Panel = TAB_PANELS[tab];
|
lab: LabPanel,
|
||||||
|
appointments: AppointmentsPanel,
|
||||||
|
'vital-signs': VitalSignsPanel,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Panel = PANELS[tab];
|
||||||
|
|
||||||
if (!Panel) {
|
if (!Panel) {
|
||||||
return <Empty description="未知数据面板" />;
|
return <Empty description="未知数据面板" />;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type FollowUpStatistics,
|
type FollowUpStatistics,
|
||||||
type PointsStatistics,
|
type PointsStatistics,
|
||||||
type HealthDataStats,
|
type HealthDataStats,
|
||||||
|
type DialysisStatistics,
|
||||||
} from '../../../api/health/points';
|
} from '../../../api/health/points';
|
||||||
|
|
||||||
export interface StatsData {
|
export interface StatsData {
|
||||||
@@ -14,6 +15,7 @@ export interface StatsData {
|
|||||||
followUpStats: FollowUpStatistics | null;
|
followUpStats: FollowUpStatistics | null;
|
||||||
pointsStats: PointsStatistics | null;
|
pointsStats: PointsStatistics | null;
|
||||||
healthDataStats: HealthDataStats | null;
|
healthDataStats: HealthDataStats | null;
|
||||||
|
dialysisStats: DialysisStatistics | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
@@ -28,6 +30,7 @@ export function useStatsData(): StatsData {
|
|||||||
const [followUpStats, setFollowUpStats] = useState<FollowUpStatistics | null>(null);
|
const [followUpStats, setFollowUpStats] = useState<FollowUpStatistics | null>(null);
|
||||||
const [pointsStats, setPointsStats] = useState<PointsStatistics | null>(null);
|
const [pointsStats, setPointsStats] = useState<PointsStatistics | null>(null);
|
||||||
const [healthDataStats, setHealthDataStats] = useState<HealthDataStats | null>(null);
|
const [healthDataStats, setHealthDataStats] = useState<HealthDataStats | null>(null);
|
||||||
|
const [dialysisStats, setDialysisStats] = useState<DialysisStatistics | null>(null);
|
||||||
|
|
||||||
const fetchAllStats = useCallback(async () => {
|
const fetchAllStats = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -52,9 +55,10 @@ export function useStatsData(): StatsData {
|
|||||||
tryFetch(pointsApi.getFollowUpStats, setFollowUpStats, '随访'),
|
tryFetch(pointsApi.getFollowUpStats, setFollowUpStats, '随访'),
|
||||||
tryFetch(pointsApi.getStatistics, setPointsStats, '积分'),
|
tryFetch(pointsApi.getStatistics, setPointsStats, '积分'),
|
||||||
tryFetch(pointsApi.getHealthDataStats, setHealthDataStats, '健康数据'),
|
tryFetch(pointsApi.getHealthDataStats, setHealthDataStats, '健康数据'),
|
||||||
|
tryFetch(pointsApi.getDialysisStats, setDialysisStats, '透析'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (hasAnyError && errors.length === 5) {
|
if (hasAnyError && errors.length === 6) {
|
||||||
setError('加载统计数据失败');
|
setError('加载统计数据失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ export function useStatsData(): StatsData {
|
|||||||
}, [fetchAllStats]);
|
}, [fetchAllStats]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patientStats, consultationStats, followUpStats, pointsStats, healthDataStats,
|
patientStats, consultationStats, followUpStats, pointsStats, healthDataStats, dialysisStats,
|
||||||
loading, error, refresh: fetchAllStats,
|
loading, error, refresh: fetchAllStats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
24
crates/erp-dialysis/src/dto/dialysis_stats_dto.rs
Normal file
24
crates/erp-dialysis/src/dto/dialysis_stats_dto.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct DialysisStatisticsResp {
|
||||||
|
pub total_records: i64,
|
||||||
|
pub this_month: i64,
|
||||||
|
/// 本月各透析类型分布
|
||||||
|
pub type_distribution: Vec<NameValue>,
|
||||||
|
/// 本月并发症发生率 (%)
|
||||||
|
pub complication_rate: f64,
|
||||||
|
/// 平均超滤量 (ml)
|
||||||
|
pub avg_ultrafiltration: Option<f64>,
|
||||||
|
/// 平均透析时长 (分钟)
|
||||||
|
pub avg_duration: Option<f64>,
|
||||||
|
/// 待审核数量
|
||||||
|
pub pending_review: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct NameValue {
|
||||||
|
pub name: String,
|
||||||
|
pub value: i64,
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod dialysis_dto;
|
pub mod dialysis_dto;
|
||||||
pub mod dialysis_prescription_dto;
|
pub mod dialysis_prescription_dto;
|
||||||
|
pub mod dialysis_stats_dto;
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||||
pub struct DeleteWithVersion {
|
pub struct DeleteWithVersion {
|
||||||
|
|||||||
23
crates/erp-dialysis/src/handler/dialysis_stats_handler.rs
Normal file
23
crates/erp-dialysis/src/handler/dialysis_stats_handler.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use axum::extract::{Extension, FromRef, State};
|
||||||
|
use axum::Json;
|
||||||
|
use erp_core::error::AppError;
|
||||||
|
use erp_core::rbac::require_permission;
|
||||||
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
|
|
||||||
|
use crate::dto::dialysis_stats_dto::DialysisStatisticsResp;
|
||||||
|
use crate::service::dialysis_stats_service;
|
||||||
|
use crate::state::DialysisState;
|
||||||
|
|
||||||
|
pub async fn get_dialysis_stats<S>(
|
||||||
|
State(state): State<S>,
|
||||||
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
) -> Result<Json<ApiResponse<DialysisStatisticsResp>>, AppError>
|
||||||
|
where
|
||||||
|
DialysisState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
require_permission(&ctx, "health.dialysis.list")?;
|
||||||
|
let dialysis_state = DialysisState::from_ref(&state);
|
||||||
|
let stats = dialysis_stats_service::get_dialysis_statistics(&dialysis_state, ctx.tenant_id).await?;
|
||||||
|
Ok(Json(ApiResponse::ok(stats)))
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod dialysis_handler;
|
pub mod dialysis_handler;
|
||||||
pub mod dialysis_prescription_handler;
|
pub mod dialysis_prescription_handler;
|
||||||
|
pub mod dialysis_stats_handler;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use axum::Router;
|
|||||||
use erp_core::error::AppResult;
|
use erp_core::error::AppResult;
|
||||||
use erp_core::module::{ErpModule, ModuleContext, ModuleType, PermissionDescriptor};
|
use erp_core::module::{ErpModule, ModuleContext, ModuleType, PermissionDescriptor};
|
||||||
|
|
||||||
use crate::handler::{dialysis_handler, dialysis_prescription_handler};
|
use crate::handler::{dialysis_handler, dialysis_prescription_handler, dialysis_stats_handler};
|
||||||
use crate::state::DialysisState;
|
use crate::state::DialysisState;
|
||||||
|
|
||||||
pub struct DialysisModule;
|
pub struct DialysisModule;
|
||||||
@@ -54,6 +54,11 @@ impl DialysisModule {
|
|||||||
.put(dialysis_prescription_handler::update_prescription)
|
.put(dialysis_prescription_handler::update_prescription)
|
||||||
.delete(dialysis_prescription_handler::delete_prescription),
|
.delete(dialysis_prescription_handler::delete_prescription),
|
||||||
)
|
)
|
||||||
|
// 透析统计
|
||||||
|
.route(
|
||||||
|
"/health/admin/statistics/dialysis",
|
||||||
|
axum::routing::get(dialysis_stats_handler::get_dialysis_stats),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
134
crates/erp-dialysis/src/service/dialysis_stats_service.rs
Normal file
134
crates/erp-dialysis/src/service/dialysis_stats_service.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use sea_orm::{DatabaseBackend, FromQueryResult, Statement};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::dto::dialysis_stats_dto::{DialysisStatisticsResp, NameValue};
|
||||||
|
use crate::error::{DialysisResult, DialysisError};
|
||||||
|
use crate::state::DialysisState;
|
||||||
|
|
||||||
|
pub async fn get_dialysis_statistics(
|
||||||
|
state: &DialysisState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
) -> DialysisResult<DialysisStatisticsResp> {
|
||||||
|
let db = &state.db;
|
||||||
|
|
||||||
|
#[derive(FromQueryResult)]
|
||||||
|
struct CountRow { count: i64 }
|
||||||
|
|
||||||
|
let total_records = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||||
|
DatabaseBackend::Postgres,
|
||||||
|
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL",
|
||||||
|
[tenant_id.into()],
|
||||||
|
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
let this_month = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||||
|
DatabaseBackend::Postgres,
|
||||||
|
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND created_at >= date_trunc('month', NOW())",
|
||||||
|
[tenant_id.into()],
|
||||||
|
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
let pending_review = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||||
|
DatabaseBackend::Postgres,
|
||||||
|
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'draft'",
|
||||||
|
[tenant_id.into()],
|
||||||
|
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
|
let type_distribution = count_by_field(
|
||||||
|
db, tenant_id,
|
||||||
|
"SELECT dialysis_type AS name, COUNT(*) AS value FROM dialysis_record \
|
||||||
|
WHERE tenant_id = $1 AND deleted_at IS NULL \
|
||||||
|
AND created_at >= date_trunc('month', NOW()) \
|
||||||
|
GROUP BY dialysis_type ORDER BY value DESC",
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
let complication_rate = compute_complication_rate(db, tenant_id).await?;
|
||||||
|
let avg_ultrafiltration = compute_avg_field(db, tenant_id, "ultrafiltration_volume").await?;
|
||||||
|
let avg_duration = compute_avg_field(db, tenant_id, "dialysis_duration").await?;
|
||||||
|
|
||||||
|
Ok(DialysisStatisticsResp {
|
||||||
|
total_records,
|
||||||
|
this_month,
|
||||||
|
type_distribution,
|
||||||
|
complication_rate,
|
||||||
|
avg_ultrafiltration,
|
||||||
|
avg_duration,
|
||||||
|
pending_review,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count_by_field(
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
sql: &str,
|
||||||
|
) -> DialysisResult<Vec<NameValue>> {
|
||||||
|
#[derive(FromQueryResult)]
|
||||||
|
struct NameValueRow { name: String, value: i64 }
|
||||||
|
|
||||||
|
let rows: Vec<NameValueRow> = FromQueryResult::find_by_statement(
|
||||||
|
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||||
|
)
|
||||||
|
.all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rows.into_iter().map(|r| NameValue { name: r.name, value: r.value }).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
struct AvgFieldResult { avg_val: Option<f64> }
|
||||||
|
|
||||||
|
macro_rules! avg_field_sql {
|
||||||
|
($field:literal) => {
|
||||||
|
concat!(
|
||||||
|
"SELECT AVG(", $field, ")::FLOAT8 AS avg_val FROM dialysis_record ",
|
||||||
|
"WHERE tenant_id = $1 AND deleted_at IS NULL AND ", $field, " IS NOT NULL ",
|
||||||
|
"AND created_at >= date_trunc('month', NOW())"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn compute_avg_field(
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
field: &str,
|
||||||
|
) -> DialysisResult<Option<f64>> {
|
||||||
|
let sql = match field {
|
||||||
|
"ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"),
|
||||||
|
"dialysis_duration" => avg_field_sql!("dialysis_duration"),
|
||||||
|
"blood_flow_rate" => avg_field_sql!("blood_flow_rate"),
|
||||||
|
_ => return Err(DialysisError::Validation(format!("不允许的字段名: {field}"))),
|
||||||
|
};
|
||||||
|
let result: Option<AvgFieldResult> = FromQueryResult::find_by_statement(
|
||||||
|
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||||
|
)
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result.and_then(|r| r.avg_val))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn compute_complication_rate(
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
) -> DialysisResult<f64> {
|
||||||
|
let sql = r#"
|
||||||
|
SELECT
|
||||||
|
COUNT(*) FILTER (WHERE complication_notes IS NOT NULL AND complication_notes != '') AS with_comp,
|
||||||
|
COUNT(*) AS total
|
||||||
|
FROM dialysis_record
|
||||||
|
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||||
|
AND created_at >= date_trunc('month', NOW())
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
struct CompResult { with_comp: i64, total: i64 }
|
||||||
|
|
||||||
|
let result: Option<CompResult> = FromQueryResult::find_by_statement(
|
||||||
|
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||||
|
)
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(match result {
|
||||||
|
Some(r) if r.total > 0 => (r.with_comp as f64 / r.total as f64) * 100.0,
|
||||||
|
_ => 0.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod dialysis_service;
|
pub mod dialysis_service;
|
||||||
pub mod dialysis_prescription_service;
|
pub mod dialysis_prescription_service;
|
||||||
|
pub mod dialysis_stats_service;
|
||||||
|
|||||||
@@ -42,22 +42,6 @@ pub struct DashboardStatsResp {
|
|||||||
// 健康数据统计
|
// 健康数据统计
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
|
||||||
pub struct DialysisStatisticsResp {
|
|
||||||
pub total_records: i64,
|
|
||||||
pub this_month: i64,
|
|
||||||
/// 本月各透析类型分布
|
|
||||||
pub type_distribution: Vec<NameValue>,
|
|
||||||
/// 本月并发症发生率 (%)
|
|
||||||
pub complication_rate: f64,
|
|
||||||
/// 平均超滤量 (ml)
|
|
||||||
pub avg_ultrafiltration: Option<f64>,
|
|
||||||
/// 平均透析时长 (分钟)
|
|
||||||
pub avg_duration: Option<f64>,
|
|
||||||
/// 待审核数量
|
|
||||||
pub pending_review: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct LabReportStatisticsResp {
|
pub struct LabReportStatisticsResp {
|
||||||
pub total_reports: i64,
|
pub total_reports: i64,
|
||||||
@@ -109,7 +93,6 @@ pub struct DailyReportRate {
|
|||||||
/// 健康数据中心综合统计。
|
/// 健康数据中心综合统计。
|
||||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct HealthDataStatsResp {
|
pub struct HealthDataStatsResp {
|
||||||
pub dialysis: DialysisStatisticsResp,
|
|
||||||
pub lab_reports: LabReportStatisticsResp,
|
pub lab_reports: LabReportStatisticsResp,
|
||||||
pub appointments: AppointmentStatisticsResp,
|
pub appointments: AppointmentStatisticsResp,
|
||||||
pub vital_signs_report_rate: VitalSignsReportRateResp,
|
pub vital_signs_report_rate: VitalSignsReportRateResp,
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ pub enum HealthError {
|
|||||||
#[error("化验报告不存在")]
|
#[error("化验报告不存在")]
|
||||||
LabReportNotFound,
|
LabReportNotFound,
|
||||||
|
|
||||||
#[error("透析记录不存在")]
|
|
||||||
DialysisRecordNotFound,
|
|
||||||
|
|
||||||
#[error("日常监测记录不存在")]
|
#[error("日常监测记录不存在")]
|
||||||
DailyMonitoringNotFound,
|
DailyMonitoringNotFound,
|
||||||
|
|
||||||
@@ -77,9 +74,6 @@ pub enum HealthError {
|
|||||||
#[error("告警记录不存在")]
|
#[error("告警记录不存在")]
|
||||||
AlertNotFound,
|
AlertNotFound,
|
||||||
|
|
||||||
#[error("透析方案不存在")]
|
|
||||||
DialysisPrescriptionNotFound,
|
|
||||||
|
|
||||||
#[error("随访模板不存在")]
|
#[error("随访模板不存在")]
|
||||||
FollowUpTemplateNotFound,
|
FollowUpTemplateNotFound,
|
||||||
|
|
||||||
@@ -106,7 +100,6 @@ impl From<HealthError> for AppError {
|
|||||||
| HealthError::ScheduleNotFound
|
| HealthError::ScheduleNotFound
|
||||||
| HealthError::VitalSignsNotFound
|
| HealthError::VitalSignsNotFound
|
||||||
| HealthError::LabReportNotFound
|
| HealthError::LabReportNotFound
|
||||||
| HealthError::DialysisRecordNotFound
|
|
||||||
| HealthError::HealthRecordNotFound
|
| HealthError::HealthRecordNotFound
|
||||||
| HealthError::FamilyMemberNotFound
|
| HealthError::FamilyMemberNotFound
|
||||||
| HealthError::TagNotFound
|
| HealthError::TagNotFound
|
||||||
@@ -123,7 +116,6 @@ impl From<HealthError> for AppError {
|
|||||||
| HealthError::AlertRuleNotFound
|
| HealthError::AlertRuleNotFound
|
||||||
| HealthError::DeviceNotFound
|
| HealthError::DeviceNotFound
|
||||||
| HealthError::AlertNotFound
|
| HealthError::AlertNotFound
|
||||||
| HealthError::DialysisPrescriptionNotFound
|
|
||||||
| HealthError::FollowUpTemplateNotFound
|
| HealthError::FollowUpTemplateNotFound
|
||||||
| HealthError::CriticalAlertNotFound => AppError::NotFound(err.to_string()),
|
| HealthError::CriticalAlertNotFound => AppError::NotFound(err.to_string()),
|
||||||
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
HealthError::ScheduleFull => AppError::Validation(err.to_string()),
|
||||||
|
|||||||
@@ -70,19 +70,6 @@ where
|
|||||||
// 健康数据统计
|
// 健康数据统计
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
pub async fn get_dialysis_stats<S>(
|
|
||||||
State(state): State<HealthState>,
|
|
||||||
Extension(ctx): Extension<TenantContext>,
|
|
||||||
) -> Result<Json<ApiResponse<DialysisStatisticsResp>>, AppError>
|
|
||||||
where
|
|
||||||
HealthState: FromRef<S>,
|
|
||||||
S: Clone + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
require_permission(&ctx, "health.patient.list")?;
|
|
||||||
let result = stats_service::get_dialysis_statistics(&state, ctx.tenant_id).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_lab_report_stats<S>(
|
pub async fn get_lab_report_stats<S>(
|
||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
|
|||||||
@@ -549,10 +549,6 @@ impl HealthModule {
|
|||||||
"/health/admin/statistics/dashboard",
|
"/health/admin/statistics/dashboard",
|
||||||
axum::routing::get(stats_handler::get_dashboard_stats),
|
axum::routing::get(stats_handler::get_dashboard_stats),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/health/admin/statistics/dialysis",
|
|
||||||
axum::routing::get(stats_handler::get_dialysis_stats),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/health/admin/statistics/lab-reports",
|
"/health/admin/statistics/lab-reports",
|
||||||
axum::routing::get(stats_handler::get_lab_report_stats),
|
axum::routing::get(stats_handler::get_lab_report_stats),
|
||||||
|
|||||||
@@ -462,7 +462,6 @@ pub struct DoctorDashboard {
|
|||||||
pub unread_messages: i64,
|
pub unread_messages: i64,
|
||||||
pub pending_follow_ups: i64,
|
pub pending_follow_ups: i64,
|
||||||
pub today_consultations: i64,
|
pub today_consultations: i64,
|
||||||
pub pending_dialysis_review: i64,
|
|
||||||
pub pending_lab_review: i64,
|
pub pending_lab_review: i64,
|
||||||
pub today_appointments: i64,
|
pub today_appointments: i64,
|
||||||
}
|
}
|
||||||
@@ -495,7 +494,6 @@ pub async fn get_doctor_dashboard(
|
|||||||
unread_messages: 0,
|
unread_messages: 0,
|
||||||
pending_follow_ups: 0,
|
pending_follow_ups: 0,
|
||||||
today_consultations: 0,
|
today_consultations: 0,
|
||||||
pending_dialysis_review: 0,
|
|
||||||
pending_lab_review: 0,
|
pending_lab_review: 0,
|
||||||
today_appointments: 0,
|
today_appointments: 0,
|
||||||
});
|
});
|
||||||
@@ -567,7 +565,6 @@ pub async fn get_doctor_dashboard(
|
|||||||
unread_messages,
|
unread_messages,
|
||||||
pending_follow_ups: pending_follow_ups as i64,
|
pending_follow_ups: pending_follow_ups as i64,
|
||||||
today_consultations: today_consultations as i64,
|
today_consultations: today_consultations as i64,
|
||||||
pending_dialysis_review: 0,
|
|
||||||
pending_lab_review: 0,
|
pending_lab_review: 0,
|
||||||
today_appointments: 0,
|
today_appointments: 0,
|
||||||
})
|
})
|
||||||
@@ -584,16 +581,6 @@ pub async fn enrich_doctor_dashboard_health(
|
|||||||
use crate::entity::{lab_report, appointment};
|
use crate::entity::{lab_report, appointment};
|
||||||
use sea_orm::{FromQueryResult, Statement, DatabaseBackend};
|
use sea_orm::{FromQueryResult, Statement, DatabaseBackend};
|
||||||
|
|
||||||
// 待审核透析记录(raw SQL — entity 已拆分到 erp-dialysis crate)
|
|
||||||
#[derive(FromQueryResult)]
|
|
||||||
struct DialysisCount { count: i64 }
|
|
||||||
let pending_dialysis = DialysisCount::find_by_statement(Statement::from_sql_and_values(
|
|
||||||
DatabaseBackend::Postgres,
|
|
||||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'draft'",
|
|
||||||
[tenant_id.into()],
|
|
||||||
)).one(&state.db).await?.map(|r| r.count).unwrap_or(0);
|
|
||||||
dashboard.pending_dialysis_review = pending_dialysis;
|
|
||||||
|
|
||||||
// 待审核化验报告
|
// 待审核化验报告
|
||||||
let pending_lab = lab_report::Entity::find()
|
let pending_lab = lab_report::Entity::find()
|
||||||
.filter(lab_report::Column::TenantId.eq(tenant_id))
|
.filter(lab_report::Column::TenantId.eq(tenant_id))
|
||||||
|
|||||||
@@ -184,57 +184,6 @@ async fn compute_avg_response_time(
|
|||||||
// 健康数据统计
|
// 健康数据统计
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
pub async fn get_dialysis_statistics(
|
|
||||||
state: &HealthState,
|
|
||||||
tenant_id: uuid::Uuid,
|
|
||||||
) -> AppResult<DialysisStatisticsResp> {
|
|
||||||
let db = &state.db;
|
|
||||||
|
|
||||||
// 使用 raw SQL 替代 dialysis_record entity(已拆分到 erp-dialysis crate)
|
|
||||||
#[derive(FromQueryResult)]
|
|
||||||
struct CountRow { count: i64 }
|
|
||||||
|
|
||||||
let total_records = CountRow::find_by_statement(Statement::from_sql_and_values(
|
|
||||||
DatabaseBackend::Postgres,
|
|
||||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL",
|
|
||||||
[tenant_id.into()],
|
|
||||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
|
||||||
|
|
||||||
let this_month = CountRow::find_by_statement(Statement::from_sql_and_values(
|
|
||||||
DatabaseBackend::Postgres,
|
|
||||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND created_at >= date_trunc('month', NOW())",
|
|
||||||
[tenant_id.into()],
|
|
||||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
|
||||||
|
|
||||||
let pending_review = CountRow::find_by_statement(Statement::from_sql_and_values(
|
|
||||||
DatabaseBackend::Postgres,
|
|
||||||
"SELECT COUNT(*)::int8 AS count FROM dialysis_record WHERE tenant_id = $1 AND deleted_at IS NULL AND status = 'draft'",
|
|
||||||
[tenant_id.into()],
|
|
||||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
|
||||||
|
|
||||||
let type_distribution = count_by_field(
|
|
||||||
db, tenant_id,
|
|
||||||
"SELECT dialysis_type AS name, COUNT(*) AS value FROM dialysis_record \
|
|
||||||
WHERE tenant_id = $1 AND deleted_at IS NULL \
|
|
||||||
AND created_at >= date_trunc('month', NOW()) \
|
|
||||||
GROUP BY dialysis_type ORDER BY value DESC",
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
let complication_rate = compute_complication_rate(db, tenant_id).await?;
|
|
||||||
let avg_ultrafiltration = compute_avg_field(db, tenant_id, "ultrafiltration_volume").await?;
|
|
||||||
let avg_duration = compute_avg_field(db, tenant_id, "dialysis_duration").await?;
|
|
||||||
|
|
||||||
Ok(DialysisStatisticsResp {
|
|
||||||
total_records,
|
|
||||||
this_month,
|
|
||||||
type_distribution,
|
|
||||||
complication_rate,
|
|
||||||
avg_ultrafiltration,
|
|
||||||
avg_duration,
|
|
||||||
pending_review,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_lab_report_statistics(
|
pub async fn get_lab_report_statistics(
|
||||||
state: &HealthState,
|
state: &HealthState,
|
||||||
tenant_id: uuid::Uuid,
|
tenant_id: uuid::Uuid,
|
||||||
@@ -388,13 +337,11 @@ pub async fn get_health_data_stats(
|
|||||||
state: &HealthState,
|
state: &HealthState,
|
||||||
tenant_id: uuid::Uuid,
|
tenant_id: uuid::Uuid,
|
||||||
) -> AppResult<HealthDataStatsResp> {
|
) -> AppResult<HealthDataStatsResp> {
|
||||||
let dialysis = get_dialysis_statistics(state, tenant_id).await?;
|
|
||||||
let lab_reports = get_lab_report_statistics(state, tenant_id).await?;
|
let lab_reports = get_lab_report_statistics(state, tenant_id).await?;
|
||||||
let appointments = get_appointment_statistics(state, tenant_id).await?;
|
let appointments = get_appointment_statistics(state, tenant_id).await?;
|
||||||
let vital_signs_report_rate = get_vital_signs_report_rate(state, tenant_id).await?;
|
let vital_signs_report_rate = get_vital_signs_report_rate(state, tenant_id).await?;
|
||||||
|
|
||||||
Ok(HealthDataStatsResp {
|
Ok(HealthDataStatsResp {
|
||||||
dialysis,
|
|
||||||
lab_reports,
|
lab_reports,
|
||||||
appointments,
|
appointments,
|
||||||
vital_signs_report_rate,
|
vital_signs_report_rate,
|
||||||
@@ -429,89 +376,6 @@ async fn count_by_field(
|
|||||||
Ok(rows.into_iter().map(|r| NameValue { name: r.name, value: r.value }).collect())
|
Ok(rows.into_iter().map(|r| NameValue { name: r.name, value: r.value }).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
|
||||||
struct AvgFieldResult {
|
|
||||||
avg_val: Option<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! avg_field_sql {
|
|
||||||
($field:literal) => {
|
|
||||||
concat!(
|
|
||||||
"SELECT AVG(", $field, ")::FLOAT8 AS avg_val FROM dialysis_record ",
|
|
||||||
"WHERE tenant_id = $1 AND deleted_at IS NULL AND ", $field, " IS NOT NULL ",
|
|
||||||
"AND created_at >= date_trunc('month', NOW())"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn compute_avg_field(
|
|
||||||
db: &sea_orm::DatabaseConnection,
|
|
||||||
tenant_id: uuid::Uuid,
|
|
||||||
field: &str,
|
|
||||||
) -> AppResult<Option<f64>> {
|
|
||||||
let sql = match field {
|
|
||||||
"ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"),
|
|
||||||
"dialysis_duration" => avg_field_sql!("dialysis_duration"),
|
|
||||||
"uf_volume" => avg_field_sql!("uf_volume"),
|
|
||||||
"uf_rate" => avg_field_sql!("uf_rate"),
|
|
||||||
"blood_flow_rate" => avg_field_sql!("blood_flow_rate"),
|
|
||||||
"dialysate_flow_rate" => avg_field_sql!("dialysate_flow_rate"),
|
|
||||||
"pre_weight" => avg_field_sql!("pre_weight"),
|
|
||||||
"post_weight" => avg_field_sql!("post_weight"),
|
|
||||||
"pre_bp_systolic" => avg_field_sql!("pre_bp_systolic"),
|
|
||||||
"pre_bp_diastolic" => avg_field_sql!("pre_bp_diastolic"),
|
|
||||||
"post_bp_systolic" => avg_field_sql!("post_bp_systolic"),
|
|
||||||
"post_bp_diastolic" => avg_field_sql!("post_bp_diastolic"),
|
|
||||||
_ => return Err(erp_core::error::AppError::Validation(format!("不允许的字段名: {field}"))),
|
|
||||||
};
|
|
||||||
let result: Option<AvgFieldResult> = sea_orm::FromQueryResult::find_by_statement(
|
|
||||||
sea_orm::Statement::from_sql_and_values(
|
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
|
||||||
sql,
|
|
||||||
[tenant_id.into()],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result.and_then(|r| r.avg_val))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn compute_complication_rate(
|
|
||||||
db: &sea_orm::DatabaseConnection,
|
|
||||||
tenant_id: uuid::Uuid,
|
|
||||||
) -> AppResult<f64> {
|
|
||||||
let sql = r#"
|
|
||||||
SELECT
|
|
||||||
COUNT(*) FILTER (WHERE complication_notes IS NOT NULL AND complication_notes != '') AS with_comp,
|
|
||||||
COUNT(*) AS total
|
|
||||||
FROM dialysis_record
|
|
||||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
|
||||||
AND created_at >= date_trunc('month', NOW())
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
|
||||||
struct CompResult {
|
|
||||||
with_comp: i64,
|
|
||||||
total: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Option<CompResult> = sea_orm::FromQueryResult::find_by_statement(
|
|
||||||
sea_orm::Statement::from_sql_and_values(
|
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
|
||||||
sql,
|
|
||||||
[tenant_id.into()],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.one(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(match result {
|
|
||||||
Some(r) if r.total > 0 => (r.with_comp as f64 / r.total as f64) * 100.0,
|
|
||||||
_ => 0.0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn count_abnormal_lab_items(
|
async fn count_abnormal_lab_items(
|
||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
tenant_id: uuid::Uuid,
|
tenant_id: uuid::Uuid,
|
||||||
|
|||||||
@@ -163,32 +163,6 @@ pub fn validate_article_status_transition(current: &str, new: &str) -> HealthRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// dialysis_record.status 枚举白名单
|
|
||||||
pub fn validate_dialysis_status(value: &str) -> HealthResult<()> {
|
|
||||||
validate_enum!(value, "dialysis_record.status", ["draft", "completed", "reviewed"]);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// dialysis_record.status 状态转换
|
|
||||||
/// draft → completed → reviewed
|
|
||||||
pub fn validate_dialysis_status_transition(current: &str, new: &str) -> HealthResult<()> {
|
|
||||||
if current == new {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let allowed = match current {
|
|
||||||
"draft" => matches!(new, "completed"),
|
|
||||||
"completed" => matches!(new, "reviewed"),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if allowed {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(HealthError::InvalidStatusTransition(format!(
|
|
||||||
"dialysis_record.status: 不允许从 '{}' 转换到 '{}'", current, new
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// lab_report.status 状态转换
|
/// lab_report.status 状态转换
|
||||||
/// pending → reviewed
|
/// pending → reviewed
|
||||||
pub fn validate_lab_report_status_transition(current: &str, new: &str) -> HealthResult<()> {
|
pub fn validate_lab_report_status_transition(current: &str, new: &str) -> HealthResult<()> {
|
||||||
@@ -433,28 +407,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn art_same_status_ok() { assert!(validate_article_status_transition("draft", "draft").is_ok()); }
|
fn art_same_status_ok() { assert!(validate_article_status_transition("draft", "draft").is_ok()); }
|
||||||
|
|
||||||
// --- dialysis_status ---
|
|
||||||
#[test]
|
|
||||||
fn dialysis_draft() { assert!(validate_dialysis_status("draft").is_ok()); }
|
|
||||||
#[test]
|
|
||||||
fn dialysis_reviewed() { assert!(validate_dialysis_status("reviewed").is_ok()); }
|
|
||||||
#[test]
|
|
||||||
fn dialysis_invalid() { assert!(validate_dialysis_status("approved").is_err()); }
|
|
||||||
|
|
||||||
// --- dialysis_status_transition ---
|
|
||||||
#[test]
|
|
||||||
fn dial_draft_to_completed() { assert!(validate_dialysis_status_transition("draft", "completed").is_ok()); }
|
|
||||||
#[test]
|
|
||||||
fn dial_draft_to_reviewed_fails() { assert!(validate_dialysis_status_transition("draft", "reviewed").is_err()); }
|
|
||||||
#[test]
|
|
||||||
fn dial_completed_to_reviewed() { assert!(validate_dialysis_status_transition("completed", "reviewed").is_ok()); }
|
|
||||||
#[test]
|
|
||||||
fn dial_completed_to_draft_fails() { assert!(validate_dialysis_status_transition("completed", "draft").is_err()); }
|
|
||||||
#[test]
|
|
||||||
fn dial_reviewed_to_any_fails() { assert!(validate_dialysis_status_transition("reviewed", "draft").is_err()); }
|
|
||||||
#[test]
|
|
||||||
fn dial_same_status_ok() { assert!(validate_dialysis_status_transition("draft", "draft").is_ok()); }
|
|
||||||
|
|
||||||
// --- lab_report_status_transition ---
|
// --- lab_report_status_transition ---
|
||||||
#[test]
|
#[test]
|
||||||
fn lab_pending_to_reviewed() { assert!(validate_lab_report_status_transition("pending", "reviewed").is_ok()); }
|
fn lab_pending_to_reviewed() { assert!(validate_lab_report_status_transition("pending", "reviewed").is_ok()); }
|
||||||
|
|||||||
Reference in New Issue
Block a user