252 lines
9.1 KiB
SQL
252 lines
9.1 KiB
SQL
-- Initiative Recognition System — PostgreSQL schema (architecture_plan.md §4)
|
|
-- Table order respects FKs (units before users).
|
|
|
|
CREATE EXTENSION IF NOT EXISTS citext;
|
|
|
|
-- ========= ENUMS =========
|
|
DO $$ BEGIN
|
|
CREATE TYPE user_role AS ENUM ('applicant','council_member','editor','admin','viewer');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE initiative_class AS ENUM ('technical','research','textbook');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE research_evidence AS ENUM ('international','domestic','poster');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE eval_level AS ENUM ('high','medium','low');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE submission_status AS ENUM ('draft','submitted','under_review','approved','rejected');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
CREATE TYPE recognition_tier AS ENUM ('excellent','good');
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- ========= IDENTITY =========
|
|
CREATE TABLE IF NOT EXISTS units (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
parent_id UUID REFERENCES units(id),
|
|
address TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email CITEXT UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
full_name TEXT NOT NULL,
|
|
phone TEXT,
|
|
unit_id UUID REFERENCES units(id),
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_roles (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role user_role NOT NULL,
|
|
PRIMARY KEY (user_id, role)
|
|
);
|
|
|
|
-- System user for anonymous draft saves (no login yet)
|
|
INSERT INTO users (id, email, password_hash, full_name)
|
|
VALUES (
|
|
'00000000-0000-4000-8000-000000000001',
|
|
'system@draft.local',
|
|
'-',
|
|
'System (draft owner)'
|
|
)
|
|
ON CONFLICT (email) DO NOTHING;
|
|
|
|
-- ========= CASE / INITIATIVE ROOT =========
|
|
CREATE TABLE IF NOT EXISTS initiatives (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
case_code TEXT UNIQUE NOT NULL,
|
|
owner_id UUID NOT NULL REFERENCES users(id),
|
|
status submission_status NOT NULL DEFAULT 'draft',
|
|
recognition_tier recognition_tier,
|
|
submitted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_initiatives_owner_status ON initiatives(owner_id, status);
|
|
|
|
-- ========= DRAFT SNAPSHOTS =========
|
|
CREATE TABLE IF NOT EXISTS drafts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
draft_code TEXT UNIQUE NOT NULL,
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
payload JSONB NOT NULL,
|
|
version INTEGER NOT NULL DEFAULT 1,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_drafts_initiative ON drafts(initiative_id);
|
|
|
|
-- ========= ĐƠN (APPLICATION) =========
|
|
CREATE TABLE IF NOT EXISTS applications (
|
|
initiative_id UUID PRIMARY KEY REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
initiative_name TEXT NOT NULL,
|
|
investor_name TEXT,
|
|
application_field TEXT,
|
|
first_apply_date DATE,
|
|
initiative_classification initiative_class,
|
|
research_evidence_kind research_evidence,
|
|
international_journal_decl TEXT,
|
|
content_summary TEXT,
|
|
confidential_info TEXT,
|
|
conditions TEXT,
|
|
author_evaluation TEXT,
|
|
trial_evaluation TEXT,
|
|
submission_day SMALLINT,
|
|
submission_month SMALLINT,
|
|
submission_year SMALLINT,
|
|
honesty_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
CONSTRAINT chk_first_apply_window
|
|
CHECK (first_apply_date IS NULL
|
|
OR first_apply_date BETWEEN DATE '2025-04-15' AND DATE '2026-04-15')
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS authors (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES users(id),
|
|
ordinal SMALLINT NOT NULL,
|
|
full_name TEXT NOT NULL,
|
|
dob DATE,
|
|
workplace TEXT,
|
|
title TEXT,
|
|
qualification TEXT,
|
|
contribution_percent NUMERIC(5,2) NOT NULL,
|
|
is_representative BOOLEAN NOT NULL DEFAULT FALSE,
|
|
CHECK (contribution_percent >= 0 AND contribution_percent <= 100)
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_authors_repr ON authors(initiative_id) WHERE is_representative;
|
|
|
|
CREATE TABLE IF NOT EXISTS support_staff (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
full_name TEXT,
|
|
dob DATE,
|
|
workplace TEXT,
|
|
title TEXT,
|
|
qualification TEXT,
|
|
support_content TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS evidence_files (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
kind TEXT NOT NULL CHECK (kind IN ('textbook','research','technical')),
|
|
storage_uri TEXT NOT NULL,
|
|
original_name TEXT NOT NULL,
|
|
mime_type TEXT NOT NULL DEFAULT 'application/pdf',
|
|
byte_size BIGINT NOT NULL,
|
|
sha256 CHAR(64) NOT NULL,
|
|
uploaded_by UUID NOT NULL REFERENCES users(id),
|
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_evidence_kind ON evidence_files(initiative_id, kind);
|
|
|
|
-- ========= BÁO CÁO (REPORT) =========
|
|
CREATE TABLE IF NOT EXISTS reports (
|
|
initiative_id UUID PRIMARY KEY REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
introduction TEXT,
|
|
representative_phone TEXT,
|
|
representative_email TEXT,
|
|
current_status TEXT,
|
|
purpose TEXT,
|
|
implementation_steps TEXT,
|
|
first_applied_unit TEXT,
|
|
achieved_result TEXT,
|
|
novelty TEXT,
|
|
effectiveness JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
submission_date DATE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS trial_units (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
address TEXT,
|
|
field TEXT,
|
|
ordinal SMALLINT
|
|
);
|
|
|
|
-- ========= CONTRIBUTION CONFIRMATION =========
|
|
CREATE TABLE IF NOT EXISTS contributions (
|
|
initiative_id UUID PRIMARY KEY REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
main_author TEXT NOT NULL,
|
|
position TEXT,
|
|
representative_percent NUMERIC(5,2),
|
|
submission_date TIMESTAMPTZ,
|
|
digital_signature_confirmed BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS contribution_participants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
full_name TEXT,
|
|
work_unit TEXT,
|
|
contribution_percent NUMERIC(5,2)
|
|
);
|
|
|
|
-- ========= PHIẾU ĐÁNH GIÁ =========
|
|
CREATE TABLE IF NOT EXISTS evaluations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
council_member_id UUID NOT NULL REFERENCES users(id),
|
|
position TEXT,
|
|
evaluation_date DATE NOT NULL,
|
|
novelty_level eval_level,
|
|
novelty_score SMALLINT,
|
|
novelty_comment TEXT,
|
|
effectiveness_level eval_level,
|
|
effectiveness_score SMALLINT,
|
|
effectiveness_comment TEXT,
|
|
total_score SMALLINT GENERATED ALWAYS AS
|
|
(COALESCE(novelty_score,0) + COALESCE(effectiveness_score,0)) STORED,
|
|
conclusion TEXT,
|
|
status submission_status NOT NULL DEFAULT 'draft',
|
|
submitted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CHECK (novelty_score IS NULL OR (novelty_score BETWEEN 0 AND 40)),
|
|
CHECK (effectiveness_score IS NULL OR (effectiveness_score BETWEEN 0 AND 60)),
|
|
UNIQUE (initiative_id, council_member_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_eval_initiative ON evaluations(initiative_id);
|
|
|
|
-- ========= ADMIN VERIFY =========
|
|
CREATE TABLE IF NOT EXISTS verifications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
initiative_id UUID NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
|
field_name TEXT NOT NULL,
|
|
content_hash CHAR(64) NOT NULL,
|
|
verified_by UUID NOT NULL REFERENCES users(id),
|
|
verified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
result TEXT
|
|
);
|
|
|
|
-- ========= AUDIT TRAIL =========
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
actor_id UUID REFERENCES users(id),
|
|
action TEXT NOT NULL,
|
|
entity TEXT NOT NULL,
|
|
entity_id UUID NOT NULL,
|
|
diff JSONB,
|
|
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity, entity_id);
|