Files
ctms/database/init.sql
T
2026-02-28 14:33:57 +08:00

1405 lines
74 KiB
SQL
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.
-- Idempotent schema for CTMS (derived from current SQLAlchemy models)
DO $$
BEGIN
CREATE TYPE public.user_role AS ENUM ('ADMIN', 'PM', 'CRA', 'PV', 'IMP', 'QA');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
ALTER TYPE public.user_role ADD VALUE IF NOT EXISTS 'QA';
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE public.user_status AS ENUM ('PENDING', 'ACTIVE', 'REJECTED', 'DISABLED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS public.users (
id uuid PRIMARY KEY,
email character varying(255) NOT NULL,
password_hash character varying(255) NOT NULL,
full_name character varying(255) NOT NULL,
role public.user_role NOT NULL,
department character varying(255) NOT NULL,
status public.user_status NOT NULL DEFAULT 'PENDING',
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
approved_by uuid,
approved_at timestamp with time zone,
avatar_url character varying(500),
CONSTRAINT uq_users_email UNIQUE (email)
);
CREATE TABLE IF NOT EXISTS public.studies (
id uuid PRIMARY KEY,
code character varying(50) NOT NULL,
name character varying(200) NOT NULL,
sponsor character varying(200),
protocol_no character varying(100),
phase character varying(50),
status character varying(20) NOT NULL DEFAULT 'DRAFT',
is_locked boolean NOT NULL DEFAULT false,
visit_interval_days integer,
visit_total integer,
visit_window_start_offset integer,
visit_window_end_offset integer,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_studies_code UNIQUE (code),
CONSTRAINT fk_studies_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.sites (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
name character varying(200) NOT NULL,
city character varying(100),
pi_name character varying(100),
contact character varying(100),
is_active boolean NOT NULL DEFAULT true,
enrollment_target integer,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_sites_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.study_members (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
user_id uuid NOT NULL,
role_in_study character varying(20) NOT NULL,
is_active boolean NOT NULL DEFAULT true,
added_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_study_member UNIQUE (study_id, user_id),
CONSTRAINT fk_study_members_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_study_members_user_id FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.milestones (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
type character varying(50) NOT NULL,
name character varying(200) NOT NULL,
planned_date date,
adjusted_start_date date,
adjusted_end_date date,
actual_start_date date,
actual_end_date date,
actual_date date,
status character varying(20) NOT NULL DEFAULT 'NOT_STARTED',
site_id uuid,
owner_id uuid,
notes text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_milestones_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_milestones_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_milestones_owner_id FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.subjects (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_id uuid NOT NULL,
subject_no character varying(50) NOT NULL,
status character varying(20) NOT NULL DEFAULT 'SCREENING',
screening_date date,
consent_date date,
enrollment_date date,
completion_date date,
drop_reason text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_subject_study_no UNIQUE (study_id, subject_no),
CONSTRAINT fk_subjects_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_subjects_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.subject_histories (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
subject_id uuid NOT NULL,
record_date date,
content text NOT NULL,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_subject_histories_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_subject_histories_subject_id FOREIGN KEY (subject_id) REFERENCES public.subjects(id) ON DELETE RESTRICT,
CONSTRAINT fk_subject_histories_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.subject_pds (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
subject_id uuid NOT NULL,
pd_no character varying(64) NOT NULL,
pd_type character varying(100) NOT NULL,
pd_level character varying(20) NOT NULL DEFAULT 'GENERAL',
approval_status character varying(20) NOT NULL DEFAULT 'DRAFT',
description text,
status character varying(20) NOT NULL DEFAULT 'OPEN',
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_subject_pds_study_pd_no UNIQUE (study_id, pd_no),
CONSTRAINT fk_subject_pds_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_subject_pds_subject_id FOREIGN KEY (subject_id) REFERENCES public.subjects(id) ON DELETE RESTRICT,
CONSTRAINT fk_subject_pds_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.visits (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
subject_id uuid NOT NULL,
visit_code character varying(50) NOT NULL,
planned_date date,
actual_date date,
status character varying(20) NOT NULL DEFAULT 'PLANNED',
window_start date,
window_end date,
notes text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_visits_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_visits_subject_id FOREIGN KEY (subject_id) REFERENCES public.subjects(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.adverse_events (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_id uuid,
subject_id uuid,
visit_id uuid,
term character varying(255) NOT NULL,
onset_date date,
resolution_date date,
seriousness character varying(50) NOT NULL,
severity character varying(50) NOT NULL,
causality character varying(50),
action_taken text,
outcome character varying(50),
reported_to_sponsor boolean NOT NULL DEFAULT false,
is_sae boolean NOT NULL DEFAULT false,
is_susar boolean NOT NULL DEFAULT false,
report_due_date date,
status character varying(20) NOT NULL DEFAULT 'NEW',
description text,
created_by uuid NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_adverse_events_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_adverse_events_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_adverse_events_subject_id FOREIGN KEY (subject_id) REFERENCES public.subjects(id) ON DELETE RESTRICT,
CONSTRAINT fk_adverse_events_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.finance_contracts (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_name character varying(255) NOT NULL,
contract_no character varying(100) NOT NULL,
signed_date date,
amount numeric(12, 2) NOT NULL,
currency character varying(10) NOT NULL DEFAULT 'CNY',
remark text,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_finance_contracts_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_contracts_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.finance_specials (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_name character varying(255) NOT NULL,
fee_type character varying(50) NOT NULL,
amount numeric(12, 2) NOT NULL,
occur_date date,
staff_name character varying(100),
remark text,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_finance_specials_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_specials_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.contract_fees (
id uuid PRIMARY KEY,
project_id uuid NOT NULL,
center_id uuid NOT NULL,
contract_amount numeric(12, 2) NOT NULL,
contract_cases integer NOT NULL,
actual_cases integer,
settlement_amount numeric(12, 2),
final_payment_amount numeric(12, 2),
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_contract_fees_project_center UNIQUE (project_id, center_id),
CONSTRAINT fk_contract_fees_project_id FOREIGN KEY (project_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_contract_fees_center_id FOREIGN KEY (center_id) REFERENCES public.sites(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.contract_fee_payments (
id uuid PRIMARY KEY,
contract_fee_id uuid NOT NULL,
seq integer NOT NULL,
amount numeric(12, 2) NOT NULL,
paid_date date,
verified_date date,
is_paid boolean NOT NULL DEFAULT false,
is_verified boolean NOT NULL DEFAULT false,
remark text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_contract_fee_payments_contract_fee_id FOREIGN KEY (contract_fee_id) REFERENCES public.contract_fees(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS public.special_expenses (
id uuid PRIMARY KEY,
project_id uuid NOT NULL,
center_id uuid,
category character varying(50) NOT NULL,
amount numeric(12, 2) NOT NULL,
happen_date date,
description text,
is_paid boolean NOT NULL DEFAULT false,
paid_date date,
is_verified boolean NOT NULL DEFAULT false,
verified_date date,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_special_expenses_project_id FOREIGN KEY (project_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_special_expenses_center_id FOREIGN KEY (center_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_special_expenses_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.fee_attachments (
id uuid PRIMARY KEY,
entity_type character varying(50) NOT NULL,
entity_id uuid NOT NULL,
file_type character varying(30) NOT NULL,
filename character varying(255) NOT NULL,
mime_type character varying(100),
size integer NOT NULL,
storage_key character varying(500),
url text,
uploaded_by uuid NOT NULL,
uploaded_at timestamp with time zone NOT NULL DEFAULT now(),
is_deleted boolean NOT NULL DEFAULT false,
CONSTRAINT fk_fee_attachments_uploaded_by FOREIGN KEY (uploaded_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.finance_items (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_id uuid,
subject_id uuid,
visit_id uuid,
category character varying(50) NOT NULL,
title character varying(255) NOT NULL,
description text,
currency character varying(10) NOT NULL DEFAULT 'CNY',
amount numeric(12, 2) NOT NULL,
occur_date date NOT NULL,
status character varying(20) NOT NULL DEFAULT 'DRAFT',
submitted_at timestamp with time zone,
approved_at timestamp with time zone,
rejected_at timestamp with time zone,
paid_at timestamp with time zone,
approver_id uuid,
payer_id uuid,
reject_reason text,
created_by uuid NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_finance_items_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_items_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_items_subject_id FOREIGN KEY (subject_id) REFERENCES public.subjects(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_items_approver_id FOREIGN KEY (approver_id) REFERENCES public.users(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_items_payer_id FOREIGN KEY (payer_id) REFERENCES public.users(id) ON DELETE RESTRICT,
CONSTRAINT fk_finance_items_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.drug_shipments (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
direction character varying(20) NOT NULL,
center_id uuid,
site_name character varying(255) NOT NULL,
ship_date date,
receive_date date,
quantity integer,
batch_no character varying(100),
carrier character varying(100),
tracking_no character varying(100),
status character varying(30) NOT NULL,
remark text,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_drug_shipments_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_drug_shipments_center_id FOREIGN KEY (center_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_drug_shipments_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.material_equipments (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
name character varying(255) NOT NULL,
spec_model character varying(255) NOT NULL,
unit character varying(50),
brand character varying(100) NOT NULL,
origin character varying(255),
production_permit_file_name character varying(255),
tech_index_file_name character varying(255),
need_calibration boolean NOT NULL DEFAULT false,
calibration_cycle_days integer,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_material_equipments_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_equipments_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.startup_feasibility (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_id uuid,
submit_date date,
accept_date date,
approved_date date,
project_no character varying(100),
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_startup_feasibility_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_startup_feasibility_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_startup_feasibility_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.startup_ethics (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_id uuid,
submit_date date,
accept_date date,
meeting_date date,
approved_date date,
approval_no character varying(100),
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_startup_ethics_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_startup_ethics_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_startup_ethics_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.kickoff_meetings (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_id uuid,
kickoff_date date,
attendees json,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_kickoff_meetings_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_kickoff_meetings_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE RESTRICT,
CONSTRAINT fk_kickoff_meetings_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.training_authorizations (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
name character varying(100) NOT NULL,
role character varying(100),
site_name character varying(255),
trained boolean NOT NULL DEFAULT false,
authorized boolean NOT NULL DEFAULT false,
trained_date date,
authorized_date date,
remark text,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_training_authorizations_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_training_authorizations_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.knowledge_notes (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
site_name character varying(255) NOT NULL,
title character varying(255) NOT NULL,
content text NOT NULL,
level character varying(50),
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_knowledge_notes_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_knowledge_notes_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.faq_categories (
id uuid PRIMARY KEY,
study_id uuid,
name character varying(100) NOT NULL,
description text,
sort_order integer NOT NULL DEFAULT 0,
is_active boolean NOT NULL DEFAULT true,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_faq_category_name UNIQUE (study_id, name),
CONSTRAINT fk_faq_categories_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.faq_items (
id uuid PRIMARY KEY,
study_id uuid,
category_id uuid NOT NULL,
best_reply_id uuid,
status character varying(20) NOT NULL DEFAULT 'PENDING',
resolved_by_confirm boolean NOT NULL DEFAULT false,
question text NOT NULL,
answer text NOT NULL,
keywords character varying(255),
version integer NOT NULL DEFAULT 1,
is_active boolean NOT NULL DEFAULT true,
created_by uuid NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_faq_items_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_faq_items_category_id FOREIGN KEY (category_id) REFERENCES public.faq_categories(id) ON DELETE RESTRICT,
CONSTRAINT fk_faq_items_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.faq_replies (
id uuid PRIMARY KEY,
faq_id uuid NOT NULL,
study_id uuid,
content text NOT NULL,
created_by uuid NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
quote_reply_id uuid,
is_deleted boolean NOT NULL DEFAULT false,
CONSTRAINT fk_faq_replies_faq_id FOREIGN KEY (faq_id) REFERENCES public.faq_items(id) ON DELETE RESTRICT,
CONSTRAINT fk_faq_replies_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_faq_replies_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT,
CONSTRAINT fk_faq_replies_quote_reply_id FOREIGN KEY (quote_reply_id) REFERENCES public.faq_replies(id) ON DELETE RESTRICT
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_faq_items_best_reply_id'
) THEN
ALTER TABLE public.faq_items
ADD CONSTRAINT fk_faq_items_best_reply_id
FOREIGN KEY (best_reply_id) REFERENCES public.faq_replies(id) ON DELETE SET NULL;
END IF;
END $$;
CREATE TABLE IF NOT EXISTS public.attachments (
id uuid PRIMARY KEY,
study_id uuid NOT NULL,
entity_type character varying(50) NOT NULL,
entity_id uuid NOT NULL,
filename character varying(255) NOT NULL,
file_path character varying(500) NOT NULL,
file_size integer NOT NULL,
content_type character varying(100),
uploaded_by uuid NOT NULL,
uploaded_at timestamp with time zone NOT NULL DEFAULT now(),
is_deleted boolean NOT NULL DEFAULT false,
CONSTRAINT fk_attachments_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_attachments_uploaded_by FOREIGN KEY (uploaded_by) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS public.audit_logs (
id uuid PRIMARY KEY,
study_id uuid,
entity_type character varying(50) NOT NULL,
entity_id uuid,
action character varying(50) NOT NULL,
detail text,
operator_id uuid NOT NULL,
operator_role character varying(50) NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_audit_logs_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_audit_logs_operator_id FOREIGN KEY (operator_id) REFERENCES public.users(id) ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS ix_sites_study_id ON public.sites (study_id);
CREATE INDEX IF NOT EXISTS ix_study_members_study_id ON public.study_members (study_id);
CREATE INDEX IF NOT EXISTS ix_milestones_study_id ON public.milestones (study_id);
CREATE INDEX IF NOT EXISTS ix_subjects_study_id ON public.subjects (study_id);
CREATE INDEX IF NOT EXISTS ix_subjects_site_id ON public.subjects (site_id);
CREATE INDEX IF NOT EXISTS ix_subject_histories_study_id ON public.subject_histories (study_id);
CREATE INDEX IF NOT EXISTS ix_subject_histories_subject_id ON public.subject_histories (subject_id);
CREATE INDEX IF NOT EXISTS ix_visits_subject_id ON public.visits (subject_id);
CREATE INDEX IF NOT EXISTS ix_adverse_events_study_id ON public.adverse_events (study_id);
CREATE INDEX IF NOT EXISTS ix_adverse_events_site_id ON public.adverse_events (site_id);
CREATE INDEX IF NOT EXISTS ix_adverse_events_subject_id ON public.adverse_events (subject_id);
CREATE INDEX IF NOT EXISTS ix_finance_contracts_study_id ON public.finance_contracts (study_id);
CREATE INDEX IF NOT EXISTS ix_finance_specials_study_id ON public.finance_specials (study_id);
CREATE INDEX IF NOT EXISTS ix_finance_items_study_id ON public.finance_items (study_id);
CREATE INDEX IF NOT EXISTS ix_finance_items_site_id ON public.finance_items (site_id);
CREATE INDEX IF NOT EXISTS ix_finance_items_subject_id ON public.finance_items (subject_id);
CREATE INDEX IF NOT EXISTS ix_drug_shipments_study_id ON public.drug_shipments (study_id);
CREATE INDEX IF NOT EXISTS ix_drug_shipments_center_id ON public.drug_shipments (center_id);
CREATE INDEX IF NOT EXISTS ix_material_equipments_study_id ON public.material_equipments (study_id);
CREATE INDEX IF NOT EXISTS ix_startup_feasibility_study_id ON public.startup_feasibility (study_id);
CREATE INDEX IF NOT EXISTS ix_startup_ethics_study_id ON public.startup_ethics (study_id);
CREATE INDEX IF NOT EXISTS ix_kickoff_meetings_study_id ON public.kickoff_meetings (study_id);
CREATE INDEX IF NOT EXISTS ix_kickoff_meetings_site_id ON public.kickoff_meetings (site_id);
CREATE INDEX IF NOT EXISTS ix_training_authorizations_study_id ON public.training_authorizations (study_id);
CREATE INDEX IF NOT EXISTS ix_knowledge_notes_study_id ON public.knowledge_notes (study_id);
CREATE INDEX IF NOT EXISTS ix_faq_categories_study_id ON public.faq_categories (study_id);
CREATE INDEX IF NOT EXISTS ix_faq_items_study_id ON public.faq_items (study_id);
CREATE INDEX IF NOT EXISTS ix_faq_items_category_id ON public.faq_items (category_id);
CREATE INDEX IF NOT EXISTS ix_faq_replies_faq_id ON public.faq_replies (faq_id);
CREATE INDEX IF NOT EXISTS ix_faq_replies_study_id ON public.faq_replies (study_id);
-- =========================================
-- DEMO SEED DATA (idempotent)
-- =========================================
INSERT INTO public.users (
id, email, password_hash, full_name, role, department, status, created_at, updated_at
) VALUES
('11111111-1111-1111-1111-111111111111', 'admin@example.com', '$2y$12$FMfyBt7YfZWeh/x8WMEIA.S6VUTIMryk2NxWHyOOuJvd6YrDwHAdu', 'System Admin', 'ADMIN', 'SYSTEM', 'ACTIVE', '2025-01-05 08:00:00+00', '2025-01-05 08:00:00+00'),
('22222222-2222-2222-2222-222222222222', 'pm@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', '项目经理', 'PM', 'PMO', 'ACTIVE', '2025-01-05 08:10:00+00', '2025-01-05 08:10:00+00'),
('33333333-3333-3333-3333-333333333333', 'cra@example.com', '$2y$12$D/.ZeDrUByPZFPnAl9MnKuzKn2G4ctKQ9IAS/CqS2sfMdE/5NaKAK', 'CRA 用户', 'CRA', 'CRA', 'ACTIVE', '2025-01-05 08:20:00+00', '2025-01-05 08:20:00+00'),
('44444444-4444-4444-4444-444444444444', 'qa@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', 'QA 用户', 'QA', 'QA', 'ACTIVE', '2025-01-05 08:30:00+00', '2025-01-05 08:30:00+00'),
('55555555-5555-5555-5555-555555555555', 'pv@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', 'PV 用户', 'PV', 'PV', 'ACTIVE', '2025-01-05 08:40:00+00', '2025-01-05 08:40:00+00'),
('66666666-6666-6666-6666-666666666666', 'imp@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', '药品管理员', 'IMP', 'IMP', 'ACTIVE', '2025-01-05 08:50:00+00', '2025-01-05 08:50:00+00')
ON CONFLICT (email) DO UPDATE SET
password_hash = EXCLUDED.password_hash,
full_name = EXCLUDED.full_name,
role = EXCLUDED.role,
department = EXCLUDED.department,
status = EXCLUDED.status,
updated_at = EXCLUDED.updated_at;
INSERT INTO public.studies (
id, code, name, sponsor, protocol_no, phase, status, visit_interval_days, visit_total, visit_window_start_offset, visit_window_end_offset, created_by, created_at
) VALUES (
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'DEMO-CTMS',
'示例项目:糖尿病新药',
'Demo Pharma',
'DP-001',
'Phase II',
'ACTIVE',
7,
3,
-2,
2,
(SELECT id FROM public.users WHERE email = 'admin@example.com'),
'2025-01-06 08:00:00+00'
) ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
sponsor = EXCLUDED.sponsor,
protocol_no = EXCLUDED.protocol_no,
phase = EXCLUDED.phase,
status = EXCLUDED.status,
visit_interval_days = EXCLUDED.visit_interval_days,
visit_total = EXCLUDED.visit_total,
visit_window_start_offset = EXCLUDED.visit_window_start_offset,
visit_window_end_offset = EXCLUDED.visit_window_end_offset,
created_by = EXCLUDED.created_by;
INSERT INTO public.sites (
id, study_id, name, city, pi_name, contact, enrollment_target, is_active, created_at
) VALUES
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '北京', '李主任', '010-12345678', 80, true, '2025-01-06 09:00:00+00'),
('cccccccc-cccc-cccc-cccc-cccccccccccc', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', '上海', '王主任', '021-12345678', 60, true, '2025-01-06 09:10:00+00'),
('dddddddd-cccc-bbbb-aaaa-eeeeeeeeeeee', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '示例中心 3', '北京', '赵主任', '010-12340000', 40, true, '2025-01-06 09:20:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.study_members (
id, study_id, user_id, role_in_study, is_active, added_at
) VALUES
('dddddddd-dddd-dddd-dddd-dddddddddddd', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'admin@example.com'), 'ADMIN', true, '2025-01-06 10:00:00+00'),
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'pm@example.com'), 'PM', true, '2025-01-06 10:05:00+00'),
('ffffffff-ffff-ffff-ffff-ffffffffffff', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'cra@example.com'), 'CRA', true, '2025-01-06 10:10:00+00'),
('11111111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'qa@example.com'), 'QA', true, '2025-01-06 10:15:00+00'),
('22222222-3333-4444-5555-666666666666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'pv@example.com'), 'PV', true, '2025-01-06 10:20:00+00'),
('33333333-4444-5555-6666-777777777777', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'imp@example.com'), 'IMP', true, '2025-01-06 10:25:00+00')
ON CONFLICT (study_id, user_id) DO UPDATE SET
role_in_study = EXCLUDED.role_in_study,
is_active = EXCLUDED.is_active;
INSERT INTO public.milestones (
id, study_id, type, name, planned_date, actual_date, status, site_id, owner_id, notes, created_at, updated_at
) VALUES
('10101010-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'SIV', '启动会', '2025-01-10', '2025-01-12', 'DONE', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '现场启动完成', '2025-01-06 12:00:00+00', '2025-01-12 10:00:00+00'),
('20202020-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'LPI', '首例入组', '2025-02-01', NULL, 'NOT_STARTED', 'cccccccc-cccc-cccc-cccc-cccccccccccc', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '待完成', '2025-01-06 12:10:00+00', '2025-01-06 12:10:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.finance_contracts (
id, study_id, site_name, contract_no, signed_date, amount, currency, remark, created_by, created_at, updated_at
) VALUES
('77777777-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', 'CON-001', '2025-01-08', 1200000.00, 'CNY', '首批合同签署', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-08 09:00:00+00', '2025-01-08 09:00:00+00'),
('77777777-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', 'CON-002', '2025-01-10', 980000.00, 'CNY', '分中心合同签署', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-10 09:00:00+00', '2025-01-10 09:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.finance_specials (
id, study_id, site_name, fee_type, amount, occur_date, staff_name, remark, created_by, created_at, updated_at
) VALUES
('88888888-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '差旅', 5200.00, '2025-01-15', '赵敏', 'SIV 差旅费', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-15 10:00:00+00', '2025-01-15 10:00:00+00'),
('88888888-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', '住宿', 3200.00, '2025-01-18', '李雷', '监查住宿费', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-18 10:00:00+00', '2025-01-18 10:00:00+00'),
('88888888-aaaa-bbbb-cccc-333333333333', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '交通', 860.00, '2025-01-20', '韩梅梅', '市内交通费', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-20 10:00:00+00', '2025-01-20 10:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.finance_items (
id, study_id, site_id, subject_id, visit_id, category, title, description, currency, amount, occur_date, status, submitted_at, approved_at, rejected_at, paid_at, approver_id, payer_id, reject_reason, created_by, created_at, updated_at
) VALUES
('99999999-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', NULL, NULL, 'SITE_FEE', '启动费', '中心启动费用', 'CNY', 30000.00, '2025-01-12', 'PAID', '2025-01-13 08:00:00+00', '2025-01-13 10:00:00+00', NULL, '2025-01-15 09:00:00+00', (SELECT id FROM public.users WHERE email = 'pm@example.com'), (SELECT id FROM public.users WHERE email = 'admin@example.com'), NULL, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 09:00:00+00', '2025-01-15 09:00:00+00'),
('99999999-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', NULL, NULL, 'TRAVEL', '监查差旅', 'CRA 监查差旅报销', 'CNY', 4800.00, '2025-01-20', 'APPROVED', '2025-01-21 08:00:00+00', '2025-01-21 10:00:00+00', NULL, NULL, (SELECT id FROM public.users WHERE email = 'pm@example.com'), NULL, NULL, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-20 10:00:00+00', '2025-01-21 10:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.drug_shipments (
id, study_id, direction, center_id, site_name, ship_date, receive_date, quantity, batch_no, carrier, tracking_no, status, remark, created_by, created_at, updated_at
) VALUES
('aaaa1111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '寄送', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '北京协和医院', '2025-01-12', NULL, 200, 'DP-001', '顺丰', 'SF100001', '运输中', '首批寄送:DP-001 片剂 50mg × 200 盒', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-12 11:00:00+00', '2025-01-12 11:00:00+00'),
('bbbb1111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '寄送', 'cccccccc-cccc-cccc-cccc-cccccccccccc', '上海瑞金医院', '2025-01-13', '2025-01-14', 150, 'DP-001', '京东物流', 'JD200002', '已签收', '分中心补货:DP-001 片剂 50mg × 150 盒', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-13 11:00:00+00', '2025-01-14 09:00:00+00'),
('cccc1111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '回收', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '北京协和医院', '2025-02-05', '2025-02-06', NULL, 'DP-001', '顺丰', 'SF100003', '已回收', '首批回收:空药盒及剩余药品', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-02-05 14:00:00+00', '2025-02-06 09:00:00+00'),
('dddd1111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '回收', 'cccccccc-cccc-cccc-cccc-cccccccccccc', '上海瑞金医院', '2025-02-08', NULL, NULL, 'DP-001', '德邦', 'DB300004', '运输中', '回收途中:冷链箱回收', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-02-08 16:00:00+00', '2025-02-08 16:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.startup_feasibility (
id, study_id, site_id, submit_date, accept_date, approved_date, project_no, created_by, created_at, updated_at
) VALUES
('eeee1111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2025-01-02', '2025-01-05', '2025-01-10', 'FEA-BJ-001', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-10 10:00:00+00', '2025-01-10 10:00:00+00'),
('ffff1111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', '2025-01-03', '2025-01-06', '2025-01-12', 'FEA-SH-002', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 10:00:00+00', '2025-01-12 10:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.startup_ethics (
id, study_id, site_id, submit_date, accept_date, meeting_date, approved_date, approval_no, created_by, created_at, updated_at
) VALUES
('1111aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2025-01-04', '2025-01-07', '2025-01-15', '2025-01-20', 'ETH-BJ-001', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-20 11:00:00+00', '2025-01-20 11:00:00+00'),
('2222aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', '2025-01-05', '2025-01-08', '2025-01-16', '2025-01-22', 'ETH-SH-002', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-22 11:00:00+00', '2025-01-22 11:00:00+00')
ON CONFLICT (id) DO NOTHING;
UPDATE public.startup_feasibility
SET site_id = (SELECT id FROM public.sites WHERE name ILIKE '%北京%' OR city ILIKE '%北京%' LIMIT 1)
WHERE site_id IS NULL AND project_no ILIKE '%-BJ-%';
UPDATE public.startup_feasibility
SET site_id = (SELECT id FROM public.sites WHERE name ILIKE '%上海%' OR city ILIKE '%上海%' LIMIT 1)
WHERE site_id IS NULL AND project_no ILIKE '%-SH-%';
UPDATE public.startup_ethics
SET site_id = (SELECT id FROM public.sites WHERE name ILIKE '%北京%' OR city ILIKE '%北京%' LIMIT 1)
WHERE site_id IS NULL AND approval_no ILIKE '%-BJ-%';
UPDATE public.startup_ethics
SET site_id = (SELECT id FROM public.sites WHERE name ILIKE '%上海%' OR city ILIKE '%上海%' LIMIT 1)
WHERE site_id IS NULL AND approval_no ILIKE '%-SH-%';
INSERT INTO public.kickoff_meetings (
id, study_id, site_id, kickoff_date, attendees, created_by, created_at, updated_at
) VALUES
(
'3333aaaa-2222-3333-4444-555555555555',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'2025-01-12',
'["张三","李四","王五"]'::json,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-12 13:00:00+00',
'2025-01-12 13:00:00+00'
),
(
'3333aaaa-2222-3333-4444-666666666666',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'cccccccc-cccc-cccc-cccc-cccccccccccc',
'2025-01-14',
'["赵六","钱七","孙八"]'::json,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-14 13:00:00+00',
'2025-01-14 13:00:00+00'
),
(
'3333aaaa-2222-3333-4444-777777777777',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'dddddddd-cccc-bbbb-aaaa-eeeeeeeeeeee',
'2025-01-16',
'["周九","吴十"]'::json,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-16 13:00:00+00',
'2025-01-16 13:00:00+00'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.training_authorizations (
id, study_id, name, role, site_name, trained, authorized, trained_date, authorized_date, remark, created_by, created_at, updated_at
) VALUES
('4444bbbb-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '赵敏', '研究医生', '北京协和医院', true, false, '2025-01-12', NULL, '待授权', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 15:00:00+00', '2025-01-12 15:00:00+00'),
('5555bbbb-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '李雷', 'CRC', '上海瑞金医院', false, true, NULL, '2025-01-18', '先授权后补训', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-18 15:00:00+00', '2025-01-18 15:00:00+00'),
('6666bbbb-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '韩梅梅', '药房管理员', '北京协和医院', true, true, '2025-01-10', '2025-01-12', '已完成', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 16:00:00+00', '2025-01-12 16:00:00+00'),
('7777bbbb-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '王强', 'CRA', '上海瑞金医院', false, false, NULL, NULL, '待安排', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-20 16:00:00+00', '2025-01-20 16:00:00+00'),
('8888bbbb-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '刘晨', '研究护士', '北京协和医院', true, false, '2025-01-14', NULL, '仅完成培训', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-14 16:00:00+00', '2025-01-14 16:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.subjects (
id, study_id, site_id, subject_no, status, screening_date, consent_date, enrollment_date, completion_date, drop_reason, created_at, updated_at
) VALUES
('11111111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'SUBJ-001', 'ENROLLED', '2025-01-05', '2025-01-06', '2025-01-10', NULL, NULL, '2025-01-10 10:00:00+00', '2025-01-10 10:00:00+00'),
('22222222-3333-4444-5555-666666666666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', 'SUBJ-002', 'SCREENING', '2025-01-12', '2025-01-13', NULL, NULL, NULL, '2025-01-12 10:00:00+00', '2025-01-12 10:00:00+00'),
('33333333-4444-5555-6666-777777777777', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'SUBJ-003', 'COMPLETED', '2024-12-20', '2024-12-21', '2024-12-28', '2025-02-05', NULL, '2025-02-05 10:00:00+00', '2025-02-05 10:00:00+00')
ON CONFLICT (study_id, subject_no) DO UPDATE SET
site_id = EXCLUDED.site_id,
status = EXCLUDED.status,
screening_date = EXCLUDED.screening_date,
consent_date = EXCLUDED.consent_date,
enrollment_date = EXCLUDED.enrollment_date,
completion_date = EXCLUDED.completion_date,
drop_reason = EXCLUDED.drop_reason,
updated_at = EXCLUDED.updated_at;
INSERT INTO public.subject_pds (
id, study_id, subject_id, pd_no, pd_type, pd_level, approval_status, description, status, created_by, created_at, updated_at
) VALUES
(
'77777777-aaaa-bbbb-cccc-111111111111',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'11111111-2222-3333-4444-555555555555',
'PD0001',
'知情同意',
'MAJOR',
'DRAFT',
'知情记录不完整,缺少签署时间记录。',
'OPEN',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
'2025-01-11 10:00:00+00',
'2025-01-11 10:00:00+00'
),
(
'77777777-aaaa-bbbb-cccc-222222222222',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'11111111-2222-3333-4444-555555555555',
'PD0002',
'排除标准',
'CRITICAL',
'APPROVED',
'排除标准判定延期,已补充说明并审批通过。',
'OPEN',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
'2025-01-18 10:00:00+00',
'2025-01-19 09:00:00+00'
),
(
'77777777-aaaa-bbbb-cccc-333333333333',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'33333333-4444-5555-6666-777777777777',
'PD0003',
'访视流程',
'GENERAL',
'SUBMITTED',
'访视窗口外补录,已提交审批。',
'CLOSED',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-02-06 10:00:00+00',
'2025-02-06 10:00:00+00'
)
ON CONFLICT (study_id, pd_no) DO UPDATE SET
subject_id = EXCLUDED.subject_id,
pd_type = EXCLUDED.pd_type,
pd_level = EXCLUDED.pd_level,
approval_status = EXCLUDED.approval_status,
description = EXCLUDED.description,
status = EXCLUDED.status,
created_by = EXCLUDED.created_by,
updated_at = EXCLUDED.updated_at;
INSERT INTO public.subject_histories (
id, study_id, subject_id, record_date, content, created_by, created_at, updated_at
) VALUES
('55555555-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '11111111-2222-3333-4444-555555555555', '2025-01-08', '既往有高血压病史,长期服药控制。', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-08 11:00:00+00', '2025-01-08 11:00:00+00'),
('55555555-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '22222222-3333-4444-5555-666666666666', '2025-01-13', '糖耐量异常,需进一步评估。', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-13 11:00:00+00', '2025-01-13 11:00:00+00'),
('55555555-aaaa-bbbb-cccc-333333333333', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '33333333-4444-5555-6666-777777777777', '2025-01-02', '既往病史稳定,未见严重合并症。', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-02 11:00:00+00', '2025-01-02 11:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.visits (
id, study_id, subject_id, visit_code, planned_date, actual_date, status, window_start, window_end, notes, created_at, updated_at
) VALUES
('44444444-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '11111111-2222-3333-4444-555555555555', 'V1', '2025-01-09', '2025-01-10', 'DONE', '2025-01-07', '2025-01-12', '按期完成', '2025-01-10 12:00:00+00', '2025-01-10 12:00:00+00'),
('44444444-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '11111111-2222-3333-4444-555555555555', 'FU1', '2025-02-09', NULL, 'PLANNED', '2025-02-07', '2025-02-12', NULL, '2025-01-20 12:00:00+00', '2025-01-20 12:00:00+00'),
('44444444-aaaa-bbbb-cccc-333333333333', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '11111111-2222-3333-4444-555555555555', 'FU2', '2025-03-11', NULL, 'PLANNED', '2025-03-09', '2025-03-14', NULL, '2025-02-01 12:00:00+00', '2025-02-01 12:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.adverse_events (
id, study_id, site_id, subject_id, visit_id, term, onset_date, resolution_date, seriousness, severity, causality, action_taken, outcome, reported_to_sponsor, report_due_date, status, description, created_by, created_at, updated_at
) VALUES
('66666666-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-2222-3333-4444-555555555555', '44444444-aaaa-bbbb-cccc-111111111111', '轻微头痛', '2025-01-10', '2025-01-11', 'NON_SERIOUS', 'MILD', '可能相关', '休息观察', '恢复', true, '2025-01-20', 'CLOSED', '参与者轻微头痛,次日缓解。', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-10 13:00:00+00', '2025-01-11 09:00:00+00'),
('66666666-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-3333-4444-5555-666666666666', NULL, '血糖升高', '2025-01-18', NULL, 'SERIOUS', 'SEVERE', '相关', '调整用药', '未恢复', false, '2025-01-28', 'NEW', '筛选期血糖异常。', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-18 13:00:00+00', '2025-01-18 13:00:00+00'),
('66666666-aaaa-bbbb-cccc-333333333333', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-2222-3333-4444-555555555555', NULL, '轻度皮疹', '2025-02-10', NULL, 'NON_SERIOUS', 'MODERATE', '待评估', NULL, '未恢复', false, '2025-02-20', 'FOLLOW_UP', '出现皮疹,持续观察。', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-02-10 13:00:00+00', '2025-02-10 13:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.knowledge_notes (
id, study_id, site_name, title, content, level, created_by, created_at, updated_at
) VALUES
('4444aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '样本采集注意', '采血需在早晨空腹完成,样本 2 小时内送达。', '高', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 18:00:00+00', '2025-01-12 18:00:00+00'),
('5555aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', '访视资料上传', '访视完成后 24 小时内完成资料上传与核对。', '中', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-15 18:00:00+00', '2025-01-15 18:00:00+00'),
('6666aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '冷链运输提醒', '冷链记录必须随药品一起封存。', '低', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-18 18:00:00+00', '2025-01-18 18:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.faq_categories (
id, study_id, name, description, sort_order, is_active, created_at, updated_at
) VALUES
('7777aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '访视相关', '访视常见问题', 1, true, '2025-01-10 08:00:00+00', '2025-01-10 08:00:00+00'),
('8888aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '用药相关', '用药与回收问题', 2, true, '2025-01-10 08:10:00+00', '2025-01-10 08:10:00+00')
ON CONFLICT (study_id, name) DO UPDATE SET
description = EXCLUDED.description,
sort_order = EXCLUDED.sort_order,
is_active = EXCLUDED.is_active,
updated_at = EXCLUDED.updated_at;
INSERT INTO public.faq_items (
id, study_id, category_id, best_reply_id, status, resolved_by_confirm, question, answer, keywords, version, is_active, created_by, created_at, updated_at
) VALUES
('9999aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '7777aaaa-2222-3333-4444-555555555555', NULL, 'PENDING', false, 'V1 访视需要携带什么?', '携带知情同意书、病历及用药记录本。', '访视,携带物品', 1, true, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-10 09:00:00+00', '2025-01-10 09:00:00+00'),
('aaaa9999-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '7777aaaa-2222-3333-4444-555555555555', NULL, 'PENDING', false, '访视延迟是否需要重新排期?', '若延迟超过窗口期需重新安排并备注原因。', '访视,窗口期', 1, true, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-11 09:00:00+00', '2025-01-11 09:00:00+00'),
('bbbb9999-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '8888aaaa-2222-3333-4444-555555555555', NULL, 'PENDING', false, '药品回收需要哪些材料?', '需提供回收清单、交接单及冷链记录。', '回收,交接单', 1, true, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 09:00:00+00', '2025-01-12 09:00:00+00'),
('cccc9999-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '8888aaaa-2222-3333-4444-555555555555', NULL, 'PENDING', false, '药品签收后如何登记?', '签收后在药品流向中登记并上传快递单。', '药品,签收', 1, true, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-13 09:00:00+00', '2025-01-13 09:00:00+00'),
('dddd9999-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '7777aaaa-2222-3333-4444-555555555555', NULL, 'PENDING', false, '随访资料上传时间要求?', '访视结束 24 小时内上传。', '随访,资料上传', 1, true, (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-14 09:00:00+00', '2025-01-14 09:00:00+00')
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.faq_replies (
id, faq_id, study_id, content, created_by, created_at, quote_reply_id, is_deleted
) VALUES
('eeee9999-2222-3333-4444-555555555555', '9999aaaa-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '如有特殊情况请提前沟通 PM。', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-10 10:00:00+00', NULL, false),
('ffff9999-2222-3333-4444-555555555555', 'bbbb9999-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '回收材料可在下载中心获取模板。', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 10:00:00+00', NULL, false)
ON CONFLICT (id) DO NOTHING;
UPDATE public.faq_items
SET best_reply_id = 'eeee9999-2222-3333-4444-555555555555'
WHERE id = '9999aaaa-2222-3333-4444-555555555555'
AND best_reply_id IS NULL;
UPDATE public.faq_items
SET best_reply_id = 'ffff9999-2222-3333-4444-555555555555'
WHERE id = 'bbbb9999-2222-3333-4444-555555555555'
AND best_reply_id IS NULL;
INSERT INTO public.attachments (
id, study_id, entity_type, entity_id, filename, file_path, file_size, content_type, uploaded_by, uploaded_at, is_deleted
) VALUES
('aaaa0000-1111-2222-3333-444455556666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'finance_contract', '77777777-aaaa-bbbb-cccc-111111111111', 'contract_beijing.pdf', '/code/app/uploads/study_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/finance_contract_77777777-aaaa-bbbb-cccc-111111111111/contract_beijing.pdf', 1024, 'application/pdf', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-08 10:00:00+00', false),
('bbbb0000-1111-2222-3333-444455556666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'drug_shipment', 'aaaa1111-2222-3333-4444-555555555555', 'shipment_beijing.pdf', '/code/app/uploads/study_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/drug_shipment_aaaa1111-2222-3333-4444-555555555555/shipment_beijing.pdf', 2048, 'application/pdf', (SELECT id FROM public.users WHERE email = 'cra@example.com'), '2025-01-12 12:00:00+00', false),
('cccc0000-1111-2222-3333-444455556666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'startup_ethics', '1111aaaa-2222-3333-4444-555555555555', 'ethics_beijing.pdf', '/code/app/uploads/study_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/startup_ethics_1111aaaa-2222-3333-4444-555555555555/ethics_beijing.pdf', 1536, 'application/pdf', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-20 12:00:00+00', false),
('dddd0000-1111-2222-3333-444455556666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), 'knowledge_note', '4444aaaa-2222-3333-4444-555555555555', 'note_sample.txt', '/code/app/uploads/study_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/knowledge_note_4444aaaa-2222-3333-4444-555555555555/note_sample.txt', 512, 'text/plain', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-12 19:00:00+00', false)
ON CONFLICT (id) DO NOTHING;
-- =========================================
-- Document version management module
-- =========================================
DO $$
BEGIN
CREATE TYPE public.document_scope_type AS ENUM ('GLOBAL', 'SITE', 'DERIVED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE public.document_status AS ENUM ('DRAFT', 'ACTIVE', 'ARCHIVED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE public.document_version_status AS ENUM (
'DRAFT', 'SUBMITTED', 'APPROVED', 'EFFECTIVE', 'SUPERSEDED', 'ARCHIVED', 'WITHDRAWN'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE public.distribution_target_type AS ENUM ('SITE', 'ROLE', 'USER');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE public.distribution_status AS ENUM ('ACTIVE', 'CLOSED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE public.acknowledgement_type AS ENUM ('RECEIVED', 'READ', 'TRAINED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS public.documents (
id uuid PRIMARY KEY,
trial_id uuid NOT NULL,
site_id uuid,
doc_no character varying(50) NOT NULL,
doc_type character varying(50) NOT NULL,
title character varying(200) NOT NULL,
scope_type public.document_scope_type NOT NULL DEFAULT 'GLOBAL',
owner_id uuid,
status public.document_status NOT NULL DEFAULT 'ACTIVE',
current_effective_version_id uuid,
description text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_documents_trial_doc_no UNIQUE (trial_id, doc_no),
CONSTRAINT fk_documents_trial_id FOREIGN KEY (trial_id) REFERENCES public.studies(id) ON DELETE RESTRICT,
CONSTRAINT fk_documents_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE SET NULL,
CONSTRAINT fk_documents_owner_id FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS public.document_versions (
id uuid PRIMARY KEY,
document_id uuid NOT NULL,
version_no character varying(50) NOT NULL,
parent_version_id uuid,
derived_from_version_id uuid,
status public.document_version_status NOT NULL DEFAULT 'DRAFT',
effective_at timestamp with time zone,
superseded_at timestamp with time zone,
file_uri character varying(500) NOT NULL,
file_hash character varying(128) NOT NULL,
file_size bigint NOT NULL,
mime_type character varying(100),
change_summary text,
created_by uuid,
submitted_at timestamp with time zone,
approved_at timestamp with time zone,
withdrawn_at timestamp with time zone,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_document_versions_doc_no UNIQUE (document_id, version_no),
CONSTRAINT fk_document_versions_document_id FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE,
CONSTRAINT fk_document_versions_parent_id FOREIGN KEY (parent_version_id) REFERENCES public.document_versions(id) ON DELETE SET NULL,
CONSTRAINT fk_document_versions_derived_id FOREIGN KEY (derived_from_version_id) REFERENCES public.document_versions(id) ON DELETE SET NULL,
CONSTRAINT fk_document_versions_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL
);
DO $$
BEGIN
ALTER TABLE public.documents
ADD CONSTRAINT fk_documents_current_effective_version_id
FOREIGN KEY (current_effective_version_id)
REFERENCES public.document_versions(id)
ON DELETE SET NULL;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS public.distributions (
id uuid PRIMARY KEY,
document_id uuid NOT NULL,
version_id uuid NOT NULL,
target_type public.distribution_target_type NOT NULL,
target_id character varying(50) NOT NULL,
status public.distribution_status NOT NULL DEFAULT 'ACTIVE',
due_at timestamp with time zone,
closed_at timestamp with time zone,
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT fk_distributions_document_id FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE,
CONSTRAINT fk_distributions_version_id FOREIGN KEY (version_id) REFERENCES public.document_versions(id) ON DELETE CASCADE,
CONSTRAINT fk_distributions_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS public.acknowledgements (
id uuid PRIMARY KEY,
distribution_id uuid NOT NULL,
user_id uuid NOT NULL,
site_id uuid,
ack_type public.acknowledgement_type NOT NULL,
due_at timestamp with time zone,
acked_at timestamp with time zone,
note text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uq_ack_distribution_user_type UNIQUE (distribution_id, user_id, ack_type),
CONSTRAINT fk_acknowledgements_distribution_id FOREIGN KEY (distribution_id) REFERENCES public.distributions(id) ON DELETE CASCADE,
CONSTRAINT fk_acknowledgements_user_id FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE RESTRICT,
CONSTRAINT fk_acknowledgements_site_id FOREIGN KEY (site_id) REFERENCES public.sites(id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_documents_trial_doc_no ON public.documents (trial_id, doc_no);
CREATE INDEX IF NOT EXISTS ix_documents_site_id ON public.documents (site_id);
CREATE UNIQUE INDEX IF NOT EXISTS ix_document_versions_doc_no ON public.document_versions (document_id, version_no);
CREATE INDEX IF NOT EXISTS ix_document_versions_status ON public.document_versions (document_id, status);
CREATE UNIQUE INDEX IF NOT EXISTS uq_document_versions_pending ON public.document_versions (document_id)
WHERE status IN ('SUBMITTED', 'APPROVED');
CREATE INDEX IF NOT EXISTS ix_distributions_version_target ON public.distributions (version_id, target_type, target_id);
CREATE INDEX IF NOT EXISTS ix_acknowledgements_distribution_type ON public.acknowledgements (distribution_id, ack_type);
CREATE INDEX IF NOT EXISTS ix_audit_logs_entity_at ON public.audit_logs (entity_type, entity_id, created_at);
DO $$
BEGIN
ALTER TABLE public.kickoff_meetings
ADD CONSTRAINT uq_kickoff_meetings_study_site UNIQUE (study_id, site_id);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- =========================================
-- Demo data for document version management
-- =========================================
INSERT INTO public.documents (
id, trial_id, site_id, doc_no, doc_type, title, scope_type, owner_id, status, current_effective_version_id, description, created_at, updated_at
) VALUES
(
'33333333-aaaa-bbbb-cccc-333333333333',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
NULL,
'SOP-001',
'SOP',
'中心文件管理 SOP',
'GLOBAL',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'ACTIVE',
NULL,
'Demo SOP document',
'2025-01-08 09:00:00+00',
'2025-01-12 09:00:00+00'
),
(
'44444444-aaaa-bbbb-cccc-444444444444',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
NULL,
'TRN-101',
'Training',
'中心培训材料',
'GLOBAL',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'ACTIVE',
NULL,
'Training deck',
'2025-01-08 10:00:00+00',
'2025-01-10 10:00:00+00'
),
(
'55555555-aaaa-bbbb-cccc-555555555555',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
(SELECT id FROM public.sites WHERE name = '北京协和医院' AND study_id = (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS')),
'SITE-SOP-001',
'SOP',
'北京协和中心 SOP',
'SITE',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
'ACTIVE',
NULL,
'Beijing site SOP',
'2025-01-09 09:00:00+00',
'2025-01-12 09:00:00+00'
)
ON CONFLICT (trial_id, doc_no) DO NOTHING;
INSERT INTO public.document_versions (
id, document_id, version_no, parent_version_id, derived_from_version_id, status, effective_at, superseded_at,
file_uri, file_hash, file_size, mime_type, change_summary, created_by, submitted_at, approved_at, withdrawn_at, created_at
) VALUES
(
'55555555-aaaa-bbbb-cccc-555555555555',
'33333333-aaaa-bbbb-cccc-333333333333',
'1.0',
NULL,
NULL,
'EFFECTIVE',
'2025-01-10 09:00:00+00',
NULL,
'/code/app/uploads/documents/demo/SOP-001-v1.pdf',
'b7c52b35f0b8e4a3c6d5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3',
2048,
'application/pdf',
'首次发布',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-09 09:00:00+00',
'2025-01-10 08:30:00+00',
NULL,
'2025-01-08 09:30:00+00'
),
(
'66666666-aaaa-bbbb-cccc-666666666666',
'33333333-aaaa-bbbb-cccc-333333333333',
'1.1',
'55555555-aaaa-bbbb-cccc-555555555555',
NULL,
'APPROVED',
NULL,
NULL,
'/code/app/uploads/documents/demo/SOP-001-v1.1.pdf',
'c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
3072,
'application/pdf',
'小幅更新',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-12 10:00:00+00',
'2025-01-12 11:00:00+00',
NULL,
'2025-01-12 09:30:00+00'
),
(
'77777777-aaaa-bbbb-cccc-777777777777',
'44444444-aaaa-bbbb-cccc-444444444444',
'1.0',
NULL,
NULL,
'DRAFT',
NULL,
NULL,
'/code/app/uploads/documents/demo/TRN-101-v1.pdf',
'd2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1',
4096,
'application/pdf',
'培训材料草稿',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
NULL,
NULL,
NULL,
'2025-01-10 10:30:00+00'
),
(
'88888888-aaaa-bbbb-cccc-888888888888',
'55555555-aaaa-bbbb-cccc-555555555555',
'1.0',
NULL,
NULL,
'EFFECTIVE',
'2025-01-10 09:00:00+00',
NULL,
'/code/app/uploads/documents/demo/SITE-SOP-001-v1.pdf',
'a3b2c1d0e9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2',
2048,
'application/pdf',
'分中心 SOP 首版',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
'2025-01-09 09:30:00+00',
'2025-01-10 08:30:00+00',
NULL,
'2025-01-09 09:00:00+00'
)
ON CONFLICT (document_id, version_no) DO NOTHING;
UPDATE public.documents
SET current_effective_version_id = '55555555-aaaa-bbbb-cccc-555555555555',
updated_at = '2025-01-12 09:00:00+00'
WHERE id = '33333333-aaaa-bbbb-cccc-333333333333'
AND current_effective_version_id IS NULL;
UPDATE public.documents
SET current_effective_version_id = '88888888-aaaa-bbbb-cccc-888888888888',
updated_at = '2025-01-10 09:10:00+00'
WHERE id = '55555555-aaaa-bbbb-cccc-555555555555'
AND current_effective_version_id IS NULL;
INSERT INTO public.distributions (
id, document_id, version_id, target_type, target_id, status, due_at, closed_at, created_by, created_at
) VALUES
(
'bbbb0000-1111-2222-3333-444444444444',
'33333333-aaaa-bbbb-cccc-333333333333',
'55555555-aaaa-bbbb-cccc-555555555555',
'SITE',
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'ACTIVE',
'2025-01-20 18:00:00+00',
NULL,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-12 12:00:00+00'
),
(
'cccc0000-1111-2222-3333-555555555555',
'33333333-aaaa-bbbb-cccc-333333333333',
'55555555-aaaa-bbbb-cccc-555555555555',
'SITE',
'cccccccc-cccc-cccc-cccc-cccccccccccc',
'ACTIVE',
'2025-01-20 18:00:00+00',
NULL,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-12 12:05:00+00'
),
(
'dddd0000-1111-2222-3333-666666666666',
'33333333-aaaa-bbbb-cccc-333333333333',
'55555555-aaaa-bbbb-cccc-555555555555',
'USER',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'ACTIVE',
'2025-01-20 18:00:00+00',
NULL,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-12 12:10:00+00'
),
(
'eeee0000-1111-2222-3333-777777777777',
'33333333-aaaa-bbbb-cccc-333333333333',
'55555555-aaaa-bbbb-cccc-555555555555',
'USER',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
'ACTIVE',
'2025-01-20 18:00:00+00',
NULL,
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'2025-01-12 12:15:00+00'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.acknowledgements (
id, distribution_id, user_id, site_id, ack_type, due_at, acked_at, note, created_at
) VALUES
(
'ffff0000-1111-2222-3333-888888888888',
'dddd0000-1111-2222-3333-666666666666',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
NULL,
'RECEIVED',
'2025-01-20 18:00:00+00',
'2025-01-12 13:00:00+00',
'已收到',
'2025-01-12 13:00:00+00'
),
(
'11110000-2222-3333-4444-999999999999',
'eeee0000-1111-2222-3333-777777777777',
(SELECT id FROM public.users WHERE email = 'cra@example.com'),
NULL,
'READ',
'2025-01-20 18:00:00+00',
'2025-01-12 14:00:00+00',
'已阅读',
'2025-01-12 14:00:00+00'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.audit_logs (
id, study_id, entity_type, entity_id, action, detail, operator_id, operator_role, created_at
) VALUES
(
'22220000-3333-4444-5555-aaaaaaaaaaaa',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'DOCUMENT',
'33333333-aaaa-bbbb-cccc-333333333333',
'DOCUMENT_CREATED',
'{"before":null,"after":{"doc_no":"SOP-001","status":"ACTIVE"}}',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'PM',
'2025-01-08 09:00:00+00'
),
(
'33330000-4444-5555-6666-bbbbbbbbbbbb',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'DOCUMENT_VERSION',
'55555555-aaaa-bbbb-cccc-555555555555',
'VERSION_EFFECTIVE',
'{"before":{"status":"APPROVED"},"after":{"status":"EFFECTIVE"}}',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'PM',
'2025-01-10 09:00:00+00'
),
(
'44440000-5555-6666-7777-cccccccccccc',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'DISTRIBUTION',
'bbbb0000-1111-2222-3333-444444444444',
'DISTRIBUTION_CREATED',
'{"version_id":"55555555-aaaa-bbbb-cccc-555555555555","target_id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'PM',
'2025-01-12 12:00:00+00'
),
(
'55550000-6666-7777-8888-dddddddddddd',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
'ACKNOWLEDGEMENT',
'ffff0000-1111-2222-3333-888888888888',
'ACK_CREATED',
'{"distribution_id":"dddd0000-1111-2222-3333-666666666666","ack_type":"RECEIVED"}',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'PM',
'2025-01-12 13:00:00+00'
)
ON CONFLICT (id) DO NOTHING;
-- Demo transition: v1.1 effective, v1.0 superseded
UPDATE public.document_versions
SET status = 'SUPERSEDED',
superseded_at = '2025-01-15 09:00:00+00'
WHERE id = '55555555-aaaa-bbbb-cccc-555555555555'
AND status = 'EFFECTIVE';
UPDATE public.document_versions
SET status = 'EFFECTIVE',
effective_at = '2025-01-15 09:00:00+00'
WHERE id = '66666666-aaaa-bbbb-cccc-666666666666'
AND status = 'APPROVED';
UPDATE public.documents
SET current_effective_version_id = '66666666-aaaa-bbbb-cccc-666666666666',
updated_at = '2025-01-15 09:00:00+00'
WHERE id = '33333333-aaaa-bbbb-cccc-333333333333';
-- Lightweight demo: one extra draft document
INSERT INTO public.documents (
id, trial_id, site_id, doc_no, doc_type, title, scope_type, owner_id, status, current_effective_version_id, description, created_at, updated_at
) VALUES
(
'99999999-eeee-ffff-aaaa-000000000001',
(SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'),
NULL,
'QRG-001',
'Guidance',
'研究快速指引',
'GLOBAL',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
'ACTIVE',
NULL,
'Quick reference guide (draft)',
'2025-01-16 09:00:00+00',
'2025-01-16 09:00:00+00'
)
ON CONFLICT (trial_id, doc_no) DO NOTHING;
INSERT INTO public.document_versions (
id, document_id, version_no, parent_version_id, derived_from_version_id, status, effective_at, superseded_at,
file_uri, file_hash, file_size, mime_type, change_summary, created_by, submitted_at, approved_at, withdrawn_at, created_at
) VALUES
(
'99999999-eeee-ffff-aaaa-000000000002',
'99999999-eeee-ffff-aaaa-000000000001',
'0.1',
NULL,
NULL,
'DRAFT',
NULL,
NULL,
'/code/app/uploads/documents/demo/QRG-001-v0.1.pdf',
'a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0',
1536,
'application/pdf',
'初稿',
(SELECT id FROM public.users WHERE email = 'pm@example.com'),
NULL,
NULL,
NULL,
'2025-01-16 09:10:00+00'
)
ON CONFLICT (document_id, version_no) DO NOTHING;