sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
-- =============================================================================
|
||||
-- SÁNG KIẾN (INNOVATION APPLICATION) DATABASE SCHEMA
|
||||
-- PostgreSQL 14+
|
||||
--
|
||||
-- Domain: Manage innovation applications at ĐHYD TP.HCM (Vietnamese medical
|
||||
-- university). Supports the full lifecycle: draft → submit → evaluate → approve.
|
||||
--
|
||||
-- Design principles:
|
||||
-- - 3NF for entities, JSONB for semi-structured/optional narrative
|
||||
-- - Soft delete (deleted_at) — legal/audit requires historical retention
|
||||
-- - State machine on applications.status enforced by trigger
|
||||
-- - Full audit_log via trigger on all CUD operations
|
||||
-- - Contribution % sums to 100 enforced by DEFERRABLE trigger
|
||||
-- =============================================================================
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- fuzzy matching
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent; -- Vietnamese diacritics in search
|
||||
|
||||
-- Convenience: updated_at auto-maintenance
|
||||
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$
|
||||
BEGIN NEW.updated_at := NOW(); RETURN NEW; END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- REFERENCE: UNITS (departments, faculties, centers)
|
||||
-- =============================================================================
|
||||
CREATE TABLE units (
|
||||
unit_id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(32) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL, -- full Vietnamese name
|
||||
parent_unit_id INT REFERENCES units(unit_id) ON DELETE SET NULL,
|
||||
type VARCHAR(32) NOT NULL
|
||||
CHECK (type IN ('TRUONG','KHOA','PHONG','BO_MON','TRUNG_TAM','KHAC')),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE TRIGGER trg_units_touch BEFORE UPDATE ON units
|
||||
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- USERS (unified: authors, evaluators, admins — a user can wear many hats)
|
||||
-- =============================================================================
|
||||
CREATE TABLE users (
|
||||
user_id SERIAL PRIMARY KEY,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
title VARCHAR(64), -- PGS.TS, TS., GS., CN., ThS.
|
||||
date_of_birth DATE,
|
||||
email VARCHAR(255) UNIQUE,
|
||||
phone VARCHAR(32),
|
||||
id_number VARCHAR(32) UNIQUE, -- CCCD / hộ chiếu
|
||||
unit_id INT REFERENCES units(unit_id) ON DELETE SET NULL,
|
||||
position VARCHAR(255), -- chức danh: Trưởng phòng, GV cao cấp
|
||||
qualification VARCHAR(64), -- trình độ: Tiến sĩ, Thạc sĩ, Cử nhân
|
||||
user_type VARCHAR(32) NOT NULL DEFAULT 'AUTHOR'
|
||||
CHECK (user_type IN ('AUTHOR','COUNCIL','ADMIN','STUDENT','EXTERNAL')),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
deleted_at TIMESTAMPTZ, -- soft delete
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_users_unit ON users(unit_id);
|
||||
CREATE INDEX idx_users_active ON users(is_active) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_users_name_trgm ON users USING GIN (full_name gin_trgm_ops);
|
||||
CREATE TRIGGER trg_users_touch BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- APPLICATIONS (sáng kiến) — the core entity
|
||||
-- =============================================================================
|
||||
CREATE TABLE applications (
|
||||
application_id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(32) UNIQUE NOT NULL, -- e.g., 'SK-2025-001'
|
||||
title TEXT NOT NULL,
|
||||
title_en TEXT,
|
||||
registration_year INT NOT NULL CHECK (registration_year BETWEEN 2000 AND 2100),
|
||||
field_of_application TEXT, -- lĩnh vực áp dụng
|
||||
|
||||
-- Workflow state (enforced by trigger below)
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT'
|
||||
CHECK (status IN (
|
||||
'DRAFT','SUBMITTED','UNDER_REVIEW',
|
||||
'EVALUATED','APPROVED','REJECTED','WITHDRAWN'
|
||||
)),
|
||||
|
||||
-- Mẫu 01 narrative (long text)
|
||||
introduction TEXT, -- 1. Mở đầu
|
||||
current_state TEXT, -- 4.1 Tình trạng đã biết
|
||||
purpose TEXT, -- Mục đích
|
||||
implementation_steps TEXT, -- Các bước thực hiện
|
||||
required_conditions TEXT, -- Điều kiện cần thiết
|
||||
results_achieved TEXT, -- Kết quả thu được
|
||||
novelty_description TEXT, -- Tính mới
|
||||
confidential_info TEXT, -- Thông tin cần bảo mật
|
||||
|
||||
-- 10 effectiveness sub-fields (all optional narrative) → JSONB
|
||||
effectiveness JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
-- Shape: { "economic":"...", "teaching":"...", "productivity":"...",
|
||||
-- "work_efficiency":"...", "quality":"...", "cost_reduction":"...",
|
||||
-- "environment":"...", "health":"...", "safety":"...", "awareness":"..." }
|
||||
|
||||
-- Mẫu 02 fields
|
||||
owner_org VARCHAR(255), -- chủ đầu tư
|
||||
first_applied_date DATE, -- ngày áp dụng lần đầu
|
||||
content_summary TEXT, -- nội dung sáng kiến (short)
|
||||
author_assessment TEXT, -- đánh giá theo tác giả
|
||||
org_assessment TEXT, -- đánh giá theo tổ chức
|
||||
|
||||
-- Mẫu 02 classification (mutually exclusive in form, but stored as flags)
|
||||
is_technical_solution BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_from_research_article BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_from_book_material BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
CONSTRAINT chk_exactly_one_classification CHECK (
|
||||
status = 'DRAFT' OR
|
||||
(is_technical_solution::int + is_from_research_article::int + is_from_book_material::int) = 1
|
||||
),
|
||||
|
||||
-- Workflow timestamps
|
||||
submitted_at TIMESTAMPTZ,
|
||||
decided_at TIMESTAMPTZ,
|
||||
|
||||
primary_unit_id INT REFERENCES units(unit_id),
|
||||
created_by INT REFERENCES users(user_id),
|
||||
deleted_at TIMESTAMPTZ, -- soft delete
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_apps_status ON applications(status) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_apps_year ON applications(registration_year);
|
||||
CREATE INDEX idx_apps_unit ON applications(primary_unit_id);
|
||||
CREATE INDEX idx_apps_title_trgm ON applications USING GIN (title gin_trgm_ops);
|
||||
CREATE INDEX idx_apps_fts ON applications USING GIN (
|
||||
to_tsvector('simple',
|
||||
coalesce(title,'') || ' ' ||
|
||||
coalesce(introduction,'') || ' ' ||
|
||||
coalesce(novelty_description,'')
|
||||
)
|
||||
);
|
||||
CREATE INDEX idx_apps_effectiveness ON applications USING GIN (effectiveness);
|
||||
CREATE TRIGGER trg_apps_touch BEFORE UPDATE ON applications
|
||||
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- APPLICATION_AUTHORS (M:N with contribution %)
|
||||
-- =============================================================================
|
||||
CREATE TABLE application_authors (
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
user_id INT NOT NULL REFERENCES users(user_id),
|
||||
contribution_pct NUMERIC(5,2) NOT NULL CHECK (contribution_pct > 0 AND contribution_pct <= 100),
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'CO_AUTHOR'
|
||||
CHECK (role IN ('PRIMARY','CO_AUTHOR')),
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (application_id, user_id)
|
||||
);
|
||||
CREATE INDEX idx_app_authors_user ON application_authors(user_id);
|
||||
|
||||
-- At most one PRIMARY author per application
|
||||
CREATE UNIQUE INDEX uq_primary_per_app
|
||||
ON application_authors(application_id) WHERE role = 'PRIMARY';
|
||||
|
||||
-- Deferrable check: contribution % must total 100 per application
|
||||
CREATE OR REPLACE FUNCTION check_contribution_total() RETURNS TRIGGER AS $$
|
||||
DECLARE v_total NUMERIC; v_app INT;
|
||||
BEGIN
|
||||
v_app := COALESCE(NEW.application_id, OLD.application_id);
|
||||
SELECT COALESCE(SUM(contribution_pct),0) INTO v_total
|
||||
FROM application_authors WHERE application_id = v_app;
|
||||
-- Only enforce when application has left DRAFT
|
||||
IF (SELECT status FROM applications WHERE application_id = v_app) <> 'DRAFT'
|
||||
AND v_total <> 100 THEN
|
||||
RAISE EXCEPTION 'Contribution % for application % must sum to 100 (got %)',
|
||||
'%', v_app, v_total;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE CONSTRAINT TRIGGER trg_contribution_total
|
||||
AFTER INSERT OR UPDATE OR DELETE ON application_authors
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW EXECUTE FUNCTION check_contribution_total();
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- ORGS that tested / adopted the innovation (Mẫu 01 inner table)
|
||||
-- =============================================================================
|
||||
CREATE TABLE application_adopters (
|
||||
adopter_id SERIAL PRIMARY KEY,
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
org_name VARCHAR(255) NOT NULL,
|
||||
address TEXT,
|
||||
field TEXT
|
||||
);
|
||||
CREATE INDEX idx_adopters_app ON application_adopters(application_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- PARTICIPANTS in first application (Mẫu 02 inner table)
|
||||
-- =============================================================================
|
||||
CREATE TABLE application_participants (
|
||||
participant_id SERIAL PRIMARY KEY,
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
user_id INT REFERENCES users(user_id), -- optional link
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
date_of_birth DATE,
|
||||
work_unit VARCHAR(255),
|
||||
position VARCHAR(255),
|
||||
qualification VARCHAR(64),
|
||||
support_content TEXT
|
||||
);
|
||||
CREATE INDEX idx_participants_app ON application_participants(application_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- EVALUATIONS (Mẫu 04) — council members score applications
|
||||
-- =============================================================================
|
||||
CREATE TABLE evaluations (
|
||||
evaluation_id SERIAL PRIMARY KEY,
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
evaluator_id INT NOT NULL REFERENCES users(user_id),
|
||||
|
||||
novelty_comments TEXT,
|
||||
novelty_score INT NOT NULL DEFAULT 0
|
||||
CHECK (novelty_score BETWEEN 0 AND 40),
|
||||
|
||||
effectiveness_comments TEXT,
|
||||
effectiveness_score INT NOT NULL DEFAULT 0
|
||||
CHECK (effectiveness_score BETWEEN 0 AND 60),
|
||||
|
||||
total_score INT GENERATED ALWAYS AS (novelty_score + effectiveness_score) STORED,
|
||||
conclusion TEXT,
|
||||
evaluated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
UNIQUE (application_id, evaluator_id)
|
||||
);
|
||||
CREATE INDEX idx_eval_app ON evaluations(application_id);
|
||||
CREATE INDEX idx_eval_evaluator ON evaluations(evaluator_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- COMMITMENTS (Bản cam kết) — for paper-based innovations
|
||||
-- =============================================================================
|
||||
CREATE TABLE commitments (
|
||||
commitment_id SERIAL PRIMARY KEY,
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
user_id INT NOT NULL REFERENCES users(user_id),
|
||||
|
||||
paper_title TEXT,
|
||||
role_type VARCHAR(32) NOT NULL
|
||||
CHECK (role_type IN ('PRIMARY_AUTHOR','CO_AUTHOR')),
|
||||
|
||||
-- 5 commitment checkboxes
|
||||
is_legal_owner BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_authorized_by_owner BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
has_coauthor_consent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
not_predatory_journal BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
complies_with_ip_law BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
signed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
UNIQUE (application_id, user_id)
|
||||
);
|
||||
CREATE INDEX idx_commit_app ON commitments(application_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- ATTACHMENTS (uploaded files — figures, flowcharts, annexes)
|
||||
-- =============================================================================
|
||||
CREATE TABLE attachments (
|
||||
attachment_id SERIAL PRIMARY KEY,
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path TEXT NOT NULL, -- S3/MinIO key
|
||||
file_size BIGINT,
|
||||
mime_type VARCHAR(128),
|
||||
kind VARCHAR(32) -- 'LUU_DO', 'PHU_LUC', 'KY_SO', 'KHAC'
|
||||
CHECK (kind IS NULL OR kind IN ('LUU_DO','PHU_LUC','KY_SO','KHAC')),
|
||||
uploaded_by INT REFERENCES users(user_id),
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_attach_app ON attachments(application_id);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- AUDIT LOG — single table, populated by triggers on all CUD operations
|
||||
-- =============================================================================
|
||||
CREATE TABLE audit_log (
|
||||
log_id BIGSERIAL PRIMARY KEY,
|
||||
table_name VARCHAR(64) NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
action VARCHAR(16) NOT NULL CHECK (action IN ('INSERT','UPDATE','DELETE')),
|
||||
changed_by INT, -- set from app via SET LOCAL my.user_id
|
||||
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
old_data JSONB,
|
||||
new_data JSONB
|
||||
);
|
||||
CREATE INDEX idx_audit_table_record ON audit_log(table_name, record_id);
|
||||
CREATE INDEX idx_audit_user_time ON audit_log(changed_by, changed_at DESC);
|
||||
|
||||
-- Generic audit trigger function
|
||||
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_user INT;
|
||||
v_pk TEXT;
|
||||
BEGIN
|
||||
-- Get user_id from session var if app sets it; else NULL
|
||||
BEGIN v_user := current_setting('my.user_id')::INT;
|
||||
EXCEPTION WHEN OTHERS THEN v_user := NULL; END;
|
||||
|
||||
v_pk := COALESCE(
|
||||
(row_to_json(NEW)::jsonb->>TG_ARGV[0]),
|
||||
(row_to_json(OLD)::jsonb->>TG_ARGV[0])
|
||||
);
|
||||
|
||||
INSERT INTO audit_log(table_name, record_id, action, changed_by, old_data, new_data)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
v_pk,
|
||||
TG_OP,
|
||||
v_user,
|
||||
CASE WHEN TG_OP IN ('UPDATE','DELETE') THEN to_jsonb(OLD) END,
|
||||
CASE WHEN TG_OP IN ('INSERT','UPDATE') THEN to_jsonb(NEW) END
|
||||
);
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Attach audit trigger to the important tables (pass PK column name as arg)
|
||||
CREATE TRIGGER trg_audit_applications AFTER INSERT OR UPDATE OR DELETE ON applications
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_trigger('application_id');
|
||||
CREATE TRIGGER trg_audit_authors AFTER INSERT OR UPDATE OR DELETE ON application_authors
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_trigger('application_id');
|
||||
CREATE TRIGGER trg_audit_evaluations AFTER INSERT OR UPDATE OR DELETE ON evaluations
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_trigger('evaluation_id');
|
||||
CREATE TRIGGER trg_audit_commitments AFTER INSERT OR UPDATE OR DELETE ON commitments
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_trigger('commitment_id');
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- WORKFLOW STATE MACHINE ENFORCEMENT
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION enforce_application_transitions() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
allowed BOOLEAN := FALSE;
|
||||
BEGIN
|
||||
IF OLD.status = NEW.status THEN RETURN NEW; END IF;
|
||||
|
||||
-- Allowed transitions
|
||||
allowed := CASE
|
||||
WHEN OLD.status = 'DRAFT' AND NEW.status IN ('SUBMITTED','WITHDRAWN') THEN TRUE
|
||||
WHEN OLD.status = 'SUBMITTED' AND NEW.status IN ('UNDER_REVIEW','WITHDRAWN','DRAFT') THEN TRUE
|
||||
WHEN OLD.status = 'UNDER_REVIEW' AND NEW.status IN ('EVALUATED','WITHDRAWN') THEN TRUE
|
||||
WHEN OLD.status = 'EVALUATED' AND NEW.status IN ('APPROVED','REJECTED') THEN TRUE
|
||||
ELSE FALSE
|
||||
END;
|
||||
|
||||
IF NOT allowed THEN
|
||||
RAISE EXCEPTION 'Invalid status transition: % → %', OLD.status, NEW.status;
|
||||
END IF;
|
||||
|
||||
-- Auto-set timestamps
|
||||
IF NEW.status = 'SUBMITTED' AND OLD.status = 'DRAFT' THEN
|
||||
NEW.submitted_at := NOW();
|
||||
END IF;
|
||||
IF NEW.status IN ('APPROVED','REJECTED') THEN
|
||||
NEW.decided_at := NOW();
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_app_state_machine
|
||||
BEFORE UPDATE OF status ON applications
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_application_transitions();
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- CONVENIENCE VIEWS
|
||||
-- =============================================================================
|
||||
|
||||
-- Dashboard: applications with author names and current evaluation average
|
||||
CREATE VIEW v_application_summary AS
|
||||
SELECT
|
||||
a.application_id,
|
||||
a.code,
|
||||
a.title,
|
||||
a.status,
|
||||
a.registration_year,
|
||||
u.name AS primary_unit_name,
|
||||
(SELECT string_agg(usr.full_name, ', ' ORDER BY aa.display_order)
|
||||
FROM application_authors aa
|
||||
JOIN users usr ON usr.user_id = aa.user_id
|
||||
WHERE aa.application_id = a.application_id) AS author_names,
|
||||
(SELECT ROUND(AVG(total_score),2)
|
||||
FROM evaluations WHERE application_id = a.application_id) AS avg_score,
|
||||
(SELECT COUNT(*) FROM evaluations WHERE application_id = a.application_id) AS num_evaluations,
|
||||
a.submitted_at,
|
||||
a.decided_at
|
||||
FROM applications a
|
||||
LEFT JOIN units u ON u.unit_id = a.primary_unit_id
|
||||
WHERE a.deleted_at IS NULL;
|
||||
|
||||
-- Materialized view: annual approval statistics (refresh nightly)
|
||||
CREATE MATERIALIZED VIEW mv_annual_stats AS
|
||||
SELECT
|
||||
registration_year,
|
||||
COUNT(*) FILTER (WHERE status = 'APPROVED') AS approved,
|
||||
COUNT(*) FILTER (WHERE status = 'REJECTED') AS rejected,
|
||||
COUNT(*) FILTER (WHERE status NOT IN ('APPROVED','REJECTED')) AS pending,
|
||||
COUNT(*) AS total
|
||||
FROM applications
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY registration_year;
|
||||
CREATE UNIQUE INDEX ON mv_annual_stats(registration_year);
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- REVIEW DOCUMENT JSON SNAPSHOTS (ReviewPanel bundle persistence)
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS application_review_documents (
|
||||
review_document_id SERIAL PRIMARY KEY,
|
||||
application_id INT NOT NULL REFERENCES applications(application_id) ON DELETE CASCADE,
|
||||
case_id VARCHAR(128) NOT NULL,
|
||||
document_version INT NOT NULL DEFAULT 1,
|
||||
official_bieu_mau JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
template_data JSONB,
|
||||
full_bundle JSONB,
|
||||
created_by INT REFERENCES users(user_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (application_id, document_version)
|
||||
);
|
||||
CREATE INDEX idx_review_docs_app_time ON application_review_documents(application_id, created_at DESC);
|
||||
CREATE INDEX idx_review_docs_case_time ON application_review_documents(case_id, created_at DESC);
|
||||
Reference in New Issue
Block a user