Enforce one STL per CT algorithm

This commit is contained in:
Codex
2026-05-28 10:09:57 +08:00
parent 2c6274ac0d
commit 07df6ce446
2 changed files with 74 additions and 45 deletions

View File

@@ -347,50 +347,79 @@ def relation_cte() -> str:
u_raw AS ( u_raw AS (
SELECT SELECT
u.*, 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 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 ( u AS (
SELECT SELECT
ct_key, u_model.ct_key,
(array_agg(ct_number ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE ct_number IS NOT NULL))[1] AS ct_number, (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(list_present, false)) AS list_present, bool_or(COALESCE(u_model.list_present, false)) AS list_present,
bool_or(COALESCE(stl_present, false)) AS stl_present, bool_or(COALESCE(u_model.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, (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(patient_name, '')), NULL)) AS patient_names, to_jsonb(array_remove(array_agg(DISTINCT NULLIF(u_model.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(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(patient_age ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(patient_age, '') IS NOT NULL))[1] AS patient_age, (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(patient_id_masked ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(patient_id_masked, '') IS NOT NULL))[1] AS patient_id_masked, (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(exam_date) AS exam_date, max(u_model.exam_date) AS exam_date,
max(task_created_at) AS task_created_at, max(u_model.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(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(exam_device ORDER BY updated_at DESC NULLS LAST) FILTER (WHERE NULLIF(exam_device, '') IS NOT NULL))[1] AS exam_device, (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(algorithm_model, '')), NULL), '') AS algorithm_model, 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(algorithm_model, '')), NULL)) AS algorithm_models, 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(upp_status, '')), NULL), '') AS upp_status, array_to_string(array_remove(array_agg(DISTINCT NULLIF(u_model.upp_status, '')), NULL), '') AS upp_status,
COALESCE(sum(COALESCE(list_record_count, 0)), 0)::int AS list_record_count, COALESCE(sum(COALESCE(u_model.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, (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(stl_file_count, 0)), 0)::int AS stl_file_count, COALESCE(sum(COALESCE(u_model.stl_file_count, 0)), 0)::int AS stl_file_count,
COALESCE(sum(COALESCE(stl_total_bytes, 0)), 0)::bigint AS stl_total_bytes, COALESCE(sum(COALESCE(u_model.stl_total_bytes, 0)), 0)::bigint AS stl_total_bytes,
max(updated_at) AS updated_at, max(u_model.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,
count(*)::int AS upp_asset_count, count(*)::int AS upp_asset_count,
COALESCE(max(u_duplicate.duplicate_model_asset_count), 0)::int AS duplicate_model_asset_count,
jsonb_agg( jsonb_agg(
jsonb_build_object( jsonb_build_object(
'ct_number', ct_number, 'ct_number', u_model.ct_number,
'algorithm_model', algorithm_model, 'algorithm_model', u_model.algorithm_model,
'upp_status', upp_status, 'upp_status', u_model.upp_status,
'file_count', stl_file_count, 'file_count', u_model.stl_file_count,
'total_bytes', stl_total_bytes, 'total_bytes', u_model.stl_total_bytes,
'processed_stl_dir', processed_stl_dir, 'processed_stl_dir', u_model.processed_stl_dir,
'updated_at', updated_at '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 ) AS upp_assets
FROM u_raw FROM u_model
GROUP BY ct_key LEFT JOIN u_duplicate ON u_duplicate.ct_key = u_model.ct_key
GROUP BY u_model.ct_key
), ),
s_raw AS ( s_raw AS (
SELECT 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_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_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, 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, max(s_raw.updated_at) AS updated_at,
count(*)::int AS stl_asset_count, count(*)::int AS stl_asset_count,
jsonb_agg( jsonb_agg(
@@ -535,18 +562,14 @@ def relation_cte() -> str:
u.stl_file_count, u.stl_file_count,
u.stl_total_bytes, u.stl_total_bytes,
u.updated_at AS upp_updated_at, 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.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, COALESCE(u.upp_assets, '[]'::jsonb) AS upp_assets,
s.file_count AS stl_file_count_agg, s.file_count AS stl_file_count_agg,
s.total_bytes AS stl_total_bytes_agg, s.total_bytes AS stl_total_bytes_agg,
s.segment_names, s.segment_names,
s.segment_families, s.segment_families,
s.segment_categories, s.segment_categories,
s.file_names,
s.stl_files,
s.updated_at AS stl_updated_at, s.updated_at AS stl_updated_at,
COALESCE(s.stl_asset_count, 0) AS stl_asset_count, COALESCE(s.stl_asset_count, 0) AS stl_asset_count,
COALESCE(s.stl_assets, '[]'::jsonb) AS stl_assets, 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, pacs_series_count, dicom_file_count, annotated_series, undetermined_series, completed,
list_present, list_record_count, stl_file_count, stl_file_count_agg, list_present, list_record_count, stl_file_count, stl_file_count_agg,
body_parts, dicom_body_parts, dicom_annotation_labels, algorithm_models, 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, segment_categories, segment_families,
pacs_updated_at, upp_updated_at, stl_updated_at pacs_updated_at, upp_updated_at, stl_updated_at
FROM relation FROM relation

View File

@@ -150,11 +150,11 @@ function patientNameMismatch(row) {
} }
function stlFileCount(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) { 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) { function assetSummary(row) {
@@ -237,6 +237,11 @@ function renderList() {
</div> </div>
<div class="card-status"> <div class="card-status">
<i class="mini-badge ${escapeHtml(row.relation_status)}">${escapeHtml(statusLabel(row.relation_status))}</i> <i class="mini-badge ${escapeHtml(row.relation_status)}">${escapeHtml(statusLabel(row.relation_status))}</i>
${
Number(row.duplicate_model_asset_count || 0)
? `<small class="name-check-warning" title="同一CT同一算法模型只能对应一个STL已按规则保留一组">${Number(row.duplicate_model_asset_count)} 组重复</small>`
: ""
}
${ ${
patientNameMismatch(row) patientNameMismatch(row)
? `<small class="name-check-warning" title="${escapeHtml(row.patient_name_match_note || "DICOM 与 UPP 患者姓名不一致")}">姓名需核对</small>` ? `<small class="name-check-warning" title="${escapeHtml(row.patient_name_match_note || "DICOM 与 UPP 患者姓名不一致")}">姓名需核对</small>`
@@ -352,6 +357,7 @@ function renderDetail(row) {
["STL状态", row.stl_present ? "存在" : "缺失"], ["STL状态", row.stl_present ? "存在" : "缺失"],
["重建模型", row.algorithm_model], ["重建模型", row.algorithm_model],
["STL资产", `${stlAssetCount(row)}`], ["STL资产", `${stlAssetCount(row)}`],
["同模型重复", Number(row.duplicate_model_asset_count || 0) ? `${Number(row.duplicate_model_asset_count)} 组已折叠` : ""],
["资产明细", assetSummary(row)], ["资产明细", assetSummary(row)],
["UPP记录", row.upp_asset_count ? `${Number(row.upp_asset_count)}` : ""], ["UPP记录", row.upp_asset_count ? `${Number(row.upp_asset_count)}` : ""],
["UPP状态", row.upp_status], ["UPP状态", row.upp_status],