Align DICOM UPP registration uniqueness

This commit is contained in:
Codex
2026-05-28 18:41:49 +08:00
parent cb18aabb4d
commit 1679ddb03a
4 changed files with 166 additions and 19 deletions

View File

@@ -198,9 +198,95 @@ def ensure_registration_table() -> None:
updated_by text NOT NULL DEFAULT 'admin',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (ct_number, series_instance_uid, algorithm_model, stl_family)
UNIQUE (ct_number, algorithm_model)
);
ALTER TABLE public.{REGISTRATION_TABLE_SQL}
ADD COLUMN IF NOT EXISTS series_instance_uid text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS series_description text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS algorithm_model text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS stl_family text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS view_mode text NOT NULL DEFAULT 'family',
ADD COLUMN IF NOT EXISTS selected_stl_files jsonb NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS transform jsonb NOT NULL DEFAULT '{{}}'::jsonb,
ADD COLUMN IF NOT EXISTS dicom_reference jsonb NOT NULL DEFAULT '{{}}'::jsonb,
ADD COLUMN IF NOT EXISTS model_reference jsonb NOT NULL DEFAULT '{{}}'::jsonb,
ADD COLUMN IF NOT EXISTS segmentation jsonb NOT NULL DEFAULT '{{}}'::jsonb,
ADD COLUMN IF NOT EXISTS notes text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS locked boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS locked_by text,
ADD COLUMN IF NOT EXISTS locked_at timestamptz,
ADD COLUMN IF NOT EXISTS updated_by text NOT NULL DEFAULT 'admin',
ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(),
ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
UPDATE public.{REGISTRATION_TABLE_SQL}
SET
ct_number = upper(regexp_replace(COALESCE(ct_number, ''), '\\s+', '', 'g')),
algorithm_model = COALESCE(NULLIF(btrim(algorithm_model), ''), '未指定模型'),
stl_family = COALESCE(NULLIF(btrim(stl_family), ''), '全部STL')
WHERE ct_number <> upper(regexp_replace(COALESCE(ct_number, ''), '\\s+', '', 'g'))
OR algorithm_model = ''
OR stl_family = '';
CREATE TABLE IF NOT EXISTS public.{REGISTRATION_TABLE_SQL}_duplicate_archive (
archived_at timestamptz NOT NULL DEFAULT now(),
reason text NOT NULL,
row_data jsonb NOT NULL
);
WITH ranked AS (
SELECT
id,
row_number() OVER (
PARTITION BY ct_number, algorithm_model
ORDER BY locked DESC, updated_at DESC NULLS LAST, id DESC
) AS rn
FROM public.{REGISTRATION_TABLE_SQL}
),
duplicates AS (
SELECT t.*
FROM public.{REGISTRATION_TABLE_SQL} t
JOIN ranked r ON r.id = t.id
WHERE r.rn > 1
)
INSERT INTO public.{REGISTRATION_TABLE_SQL}_duplicate_archive(reason, row_data)
SELECT 'ct_algorithm_unique_migration', row_to_json(duplicates)::jsonb
FROM duplicates;
WITH ranked AS (
SELECT
id,
row_number() OVER (
PARTITION BY ct_number, algorithm_model
ORDER BY locked DESC, updated_at DESC NULLS LAST, id DESC
) AS rn
FROM public.{REGISTRATION_TABLE_SQL}
)
DELETE FROM public.{REGISTRATION_TABLE_SQL} t
USING ranked r
WHERE t.id = r.id AND r.rn > 1;
DO $$
DECLARE
old_constraint record;
BEGIN
FOR old_constraint IN
SELECT conname
FROM pg_constraint
WHERE conrelid = 'public.{REGISTRATION_TABLE_SQL}'::regclass
AND contype = 'u'
AND pg_get_constraintdef(oid) LIKE '%(ct_number, series_instance_uid, algorithm_model, stl_family)%'
LOOP
EXECUTE format('ALTER TABLE public.{REGISTRATION_TABLE_SQL} DROP CONSTRAINT %I', old_constraint.conname);
END LOOP;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conrelid = 'public.{REGISTRATION_TABLE_SQL}'::regclass
AND contype = 'u'
AND conname = '{REGISTRATION_TABLE_SQL}_ct_algorithm_key'
) THEN
ALTER TABLE public.{REGISTRATION_TABLE_SQL}
ADD CONSTRAINT {REGISTRATION_TABLE_SQL}_ct_algorithm_key UNIQUE (ct_number, algorithm_model);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_{REGISTRATION_TABLE_SQL}_ct ON public.{REGISTRATION_TABLE_SQL}(ct_number);
CREATE INDEX IF NOT EXISTS idx_{REGISTRATION_TABLE_SQL}_algorithm ON public.{REGISTRATION_TABLE_SQL}(algorithm_model);
CREATE INDEX IF NOT EXISTS idx_{REGISTRATION_TABLE_SQL}_locked ON public.{REGISTRATION_TABLE_SQL}(locked);
""",
timeout=12,
@@ -852,11 +938,14 @@ def stl_rows_for_ct(ct_number: str) -> list[dict[str, Any]]:
SELECT
COALESCE(u.algorithm_model, '未指定模型') AS algorithm_model,
u.processed_stl_dir,
COALESCE(s.files, u.stl_files, '[]'::jsonb) AS files
CASE
WHEN jsonb_typeof(u.stl_files) = 'array' AND jsonb_array_length(u.stl_files) > 0 THEN u.stl_files
ELSE COALESCE(s.files, '[]'::jsonb)
END AS files
FROM public.{UPP_ASSET_TABLE_SQL} u
LEFT JOIN public.{UPP_STL_TABLE_SQL} s ON upper(s.ct_number) = upper(u.ct_number)
WHERE upper(u.ct_number) = {sql_literal(normalize_ct(ct_number))}
ORDER BY u.updated_at DESC NULLS LAST
ORDER BY COALESCE(u.algorithm_model, '未指定模型'), u.updated_at DESC NULLS LAST
""",
timeout=14,
)
@@ -950,9 +1039,7 @@ def save_registration(payload: RegistrationPayload, user: str = Depends(require_
SELECT locked
FROM public.{REGISTRATION_TABLE_SQL}
WHERE ct_number = {sql_literal(ct_number)}
AND series_instance_uid = {sql_literal(series_uid)}
AND algorithm_model = {sql_literal(algorithm_model)}
AND stl_family = {sql_literal(stl_family)}
LIMIT 1
""",
timeout=8,
@@ -989,8 +1076,10 @@ def save_registration(payload: RegistrationPayload, user: str = Depends(require_
{sql_literal(user)},
now()
)
ON CONFLICT (ct_number, series_instance_uid, algorithm_model, stl_family) DO UPDATE SET
ON CONFLICT (ct_number, algorithm_model) DO UPDATE SET
series_instance_uid = EXCLUDED.series_instance_uid,
series_description = EXCLUDED.series_description,
stl_family = EXCLUDED.stl_family,
view_mode = EXCLUDED.view_mode,
selected_stl_files = EXCLUDED.selected_stl_files,
transform = EXCLUDED.transform,