diff --git a/数据库Web可视化/app.py b/数据库Web可视化/app.py index b4e1027..0671a33 100644 --- a/数据库Web可视化/app.py +++ b/数据库Web可视化/app.py @@ -347,50 +347,79 @@ def relation_cte() -> str: u_raw AS ( SELECT u.*, - upper(u.ct_number) AS ct_key + upper(u.ct_number) AS ct_key, + COALESCE(NULLIF(u.algorithm_model, ''), '未指定模型') AS algorithm_model_key FROM public.{UPP_ASSET_TABLE_SQL} u ), + u_model_ranked AS ( + SELECT + u_raw.*, + row_number() OVER ( + PARTITION BY ct_key, algorithm_model_key + ORDER BY + COALESCE(stl_present, false) DESC, + COALESCE(stl_file_count, 0) DESC, + updated_at DESC NULLS LAST + ) AS model_rank + FROM u_raw + ), + u_model AS ( + SELECT * + FROM u_model_ranked + WHERE model_rank = 1 + ), + u_duplicate AS ( + SELECT + ct_key, + COALESCE(sum(model_count - 1), 0)::int AS duplicate_model_asset_count + FROM ( + SELECT ct_key, algorithm_model_key, count(*)::int AS model_count + FROM u_raw + GROUP BY ct_key, algorithm_model_key + HAVING count(*) > 1 + ) duplicate_models + GROUP BY ct_key + ), u AS ( SELECT - ct_key, - (array_agg(ct_number ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE ct_number IS NOT NULL))[1] AS ct_number, - bool_or(COALESCE(list_present, false)) AS list_present, - bool_or(COALESCE(stl_present, false)) AS stl_present, - (array_agg(patient_name ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(patient_name, '') IS NOT NULL))[1] AS patient_name, - to_jsonb(array_remove(array_agg(DISTINCT NULLIF(patient_name, '')), NULL)) AS patient_names, - (array_agg(patient_sex ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(patient_sex, '') IS NOT NULL))[1] AS patient_sex, - (array_agg(patient_age ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(patient_age, '') IS NOT NULL))[1] AS patient_age, - (array_agg(patient_id_masked ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(patient_id_masked, '') IS NOT NULL))[1] AS patient_id_masked, - max(exam_date) AS exam_date, - max(task_created_at) AS task_created_at, - (array_agg(exam_description ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(exam_description, '') IS NOT NULL))[1] AS exam_description, - (array_agg(exam_device ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(exam_device, '') IS NOT NULL))[1] AS exam_device, - array_to_string(array_remove(array_agg(DISTINCT NULLIF(algorithm_model, '')), NULL), '、') AS algorithm_model, - to_jsonb(array_remove(array_agg(DISTINCT NULLIF(algorithm_model, '')), NULL)) AS algorithm_models, - array_to_string(array_remove(array_agg(DISTINCT NULLIF(upp_status, '')), NULL), '、') AS upp_status, - COALESCE(sum(COALESCE(list_record_count, 0)), 0)::int AS list_record_count, - (array_agg(processed_stl_dir ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(processed_stl_dir, '') IS NOT NULL))[1] AS processed_stl_dir, - COALESCE(sum(COALESCE(stl_file_count, 0)), 0)::int AS stl_file_count, - COALESCE(sum(COALESCE(stl_total_bytes, 0)), 0)::bigint AS stl_total_bytes, - max(updated_at) AS updated_at, - (array_agg(selected_list_record ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE selected_list_record IS NOT NULL))[1] AS selected_list_record, - jsonb_agg(list_records ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE list_records IS NOT NULL) AS list_records, - jsonb_agg(stl_candidates ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE stl_candidates IS NOT NULL) AS stl_candidates, + u_model.ct_key, + (array_agg(u_model.ct_number ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE u_model.ct_number IS NOT NULL))[1] AS ct_number, + bool_or(COALESCE(u_model.list_present, false)) AS list_present, + bool_or(COALESCE(u_model.stl_present, false)) AS stl_present, + (array_agg(u_model.patient_name ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.patient_name, '') IS NOT NULL))[1] AS patient_name, + to_jsonb(array_remove(array_agg(DISTINCT NULLIF(u_model.patient_name, '')), NULL)) AS patient_names, + (array_agg(u_model.patient_sex ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.patient_sex, '') IS NOT NULL))[1] AS patient_sex, + (array_agg(u_model.patient_age ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.patient_age, '') IS NOT NULL))[1] AS patient_age, + (array_agg(u_model.patient_id_masked ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.patient_id_masked, '') IS NOT NULL))[1] AS patient_id_masked, + max(u_model.exam_date) AS exam_date, + max(u_model.task_created_at) AS task_created_at, + (array_agg(u_model.exam_description ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.exam_description, '') IS NOT NULL))[1] AS exam_description, + (array_agg(u_model.exam_device ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.exam_device, '') IS NOT NULL))[1] AS exam_device, + array_to_string(array_remove(array_agg(DISTINCT NULLIF(u_model.algorithm_model, '')), NULL), '、') AS algorithm_model, + to_jsonb(array_remove(array_agg(DISTINCT NULLIF(u_model.algorithm_model, '')), NULL)) AS algorithm_models, + array_to_string(array_remove(array_agg(DISTINCT NULLIF(u_model.upp_status, '')), NULL), '、') AS upp_status, + COALESCE(sum(COALESCE(u_model.list_record_count, 0)), 0)::int AS list_record_count, + (array_agg(u_model.processed_stl_dir ORDER BY u_model.updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(u_model.processed_stl_dir, '') IS NOT NULL))[1] AS processed_stl_dir, + COALESCE(sum(COALESCE(u_model.stl_file_count, 0)), 0)::int AS stl_file_count, + COALESCE(sum(COALESCE(u_model.stl_total_bytes, 0)), 0)::bigint AS stl_total_bytes, + max(u_model.updated_at) AS updated_at, count(*)::int AS upp_asset_count, + COALESCE(max(u_duplicate.duplicate_model_asset_count), 0)::int AS duplicate_model_asset_count, jsonb_agg( jsonb_build_object( - 'ct_number', ct_number, - 'algorithm_model', algorithm_model, - 'upp_status', upp_status, - 'file_count', stl_file_count, - 'total_bytes', stl_total_bytes, - 'processed_stl_dir', processed_stl_dir, - 'updated_at', updated_at + 'ct_number', u_model.ct_number, + 'algorithm_model', u_model.algorithm_model, + 'upp_status', u_model.upp_status, + 'file_count', u_model.stl_file_count, + 'total_bytes', u_model.stl_total_bytes, + 'processed_stl_dir', u_model.processed_stl_dir, + 'updated_at', u_model.updated_at ) - ORDER BY updated_at DESC NULLS LAST + ORDER BY u_model.updated_at DESC NULLS LAST ) AS upp_assets - FROM u_raw - GROUP BY ct_key + FROM u_model + LEFT JOIN u_duplicate ON u_duplicate.ct_key = u_model.ct_key + GROUP BY u_model.ct_key ), s_raw AS ( SELECT @@ -442,8 +471,6 @@ def relation_cte() -> str: COALESCE((array_agg(s_segments.segment_names) FILTER (WHERE s_segments.segment_names IS NOT NULL))[1], '[]'::jsonb) AS segment_names, COALESCE((array_agg(s_segments.segment_families) FILTER (WHERE s_segments.segment_families IS NOT NULL))[1], '[]'::jsonb) AS segment_families, COALESCE((array_agg(s_segments.segment_categories) FILTER (WHERE s_segments.segment_categories IS NOT NULL))[1], '[]'::jsonb) AS segment_categories, - jsonb_agg(s_raw.file_names ORDER BY s_raw.updated_at DESC NULLS LAST) FILTER (WHERE s_raw.file_names IS NOT NULL) AS file_names, - jsonb_agg(s_raw.files ORDER BY s_raw.updated_at DESC NULLS LAST) FILTER (WHERE s_raw.files IS NOT NULL) AS stl_files, max(s_raw.updated_at) AS updated_at, count(*)::int AS stl_asset_count, jsonb_agg( @@ -535,18 +562,14 @@ def relation_cte() -> str: u.stl_file_count, u.stl_total_bytes, u.updated_at AS upp_updated_at, - u.selected_list_record, - u.list_records, - u.stl_candidates, COALESCE(u.upp_asset_count, 0) AS upp_asset_count, + COALESCE(u.duplicate_model_asset_count, 0) AS duplicate_model_asset_count, COALESCE(u.upp_assets, '[]'::jsonb) AS upp_assets, s.file_count AS stl_file_count_agg, s.total_bytes AS stl_total_bytes_agg, s.segment_names, s.segment_families, s.segment_categories, - s.file_names, - s.stl_files, s.updated_at AS stl_updated_at, COALESCE(s.stl_asset_count, 0) AS stl_asset_count, COALESCE(s.stl_assets, '[]'::jsonb) AS stl_assets, @@ -785,7 +808,7 @@ def relations( pacs_series_count, dicom_file_count, annotated_series, undetermined_series, completed, list_present, list_record_count, stl_file_count, stl_file_count_agg, body_parts, dicom_body_parts, dicom_annotation_labels, algorithm_models, - upp_asset_count, upp_assets, stl_asset_count, stl_assets, + upp_asset_count, duplicate_model_asset_count, upp_assets, stl_asset_count, stl_assets, segment_categories, segment_families, pacs_updated_at, upp_updated_at, stl_updated_at FROM relation diff --git a/数据库Web可视化/static/app.js b/数据库Web可视化/static/app.js index 4dd7f27..25f3344 100644 --- a/数据库Web可视化/static/app.js +++ b/数据库Web可视化/static/app.js @@ -150,11 +150,11 @@ function patientNameMismatch(row) { } function stlFileCount(row) { - return Number(row.stl_file_count_agg || row.stl_file_count || 0); + return Number(row.stl_file_count || row.stl_file_count_agg || 0); } function stlAssetCount(row) { - return Number(row.stl_asset_count || row.upp_asset_count || 0); + return Number(row.upp_asset_count || row.stl_asset_count || 0); } function assetSummary(row) { @@ -237,6 +237,11 @@ function renderList() {
${escapeHtml(statusLabel(row.relation_status))} + ${ + Number(row.duplicate_model_asset_count || 0) + ? `${Number(row.duplicate_model_asset_count)} 组重复` + : "" + } ${ patientNameMismatch(row) ? `姓名需核对` @@ -352,6 +357,7 @@ function renderDetail(row) { ["STL状态", row.stl_present ? "存在" : "缺失"], ["重建模型", row.algorithm_model], ["STL资产", `${stlAssetCount(row)} 组`], + ["同模型重复", Number(row.duplicate_model_asset_count || 0) ? `${Number(row.duplicate_model_asset_count)} 组已折叠` : ""], ["资产明细", assetSummary(row)], ["UPP记录", row.upp_asset_count ? `${Number(row.upp_asset_count)} 条` : ""], ["UPP状态", row.upp_status],