2026-07-21 15:28:55 +08:00
|
|
|
BEGIN;
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS operation_groups (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
name TEXT NOT NULL UNIQUE,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
group_id BIGINT REFERENCES operation_groups(id) ON DELETE RESTRICT,
|
|
|
|
|
username TEXT NOT NULL UNIQUE,
|
|
|
|
|
display_name TEXT NOT NULL,
|
|
|
|
|
password_hash TEXT NOT NULL,
|
|
|
|
|
role TEXT NOT NULL CHECK (role IN ('platform_admin', 'group_admin', 'operator')),
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
|
|
|
|
|
must_change_password BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
|
last_login_at TIMESTAMPTZ,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS one_group_admin_per_group ON users(group_id) WHERE role = 'group_admin';
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
group_id BIGINT NOT NULL REFERENCES operation_groups(id) ON DELETE RESTRICT,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
slug TEXT NOT NULL UNIQUE,
|
|
|
|
|
client_description TEXT NOT NULL DEFAULT '',
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'closed', 'archived')),
|
2026-07-22 16:41:03 +08:00
|
|
|
review_status TEXT NOT NULL DEFAULT 'draft' CHECK (review_status IN ('draft', 'reviewing', 'completed', 'archived')),
|
|
|
|
|
review_completed_at TIMESTAMPTZ,
|
2026-07-21 15:28:55 +08:00
|
|
|
access_password_hash TEXT NOT NULL DEFAULT '',
|
|
|
|
|
customer_access_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
|
access_expires_at TIMESTAMPTZ,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS collections (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
client_description TEXT NOT NULL DEFAULT '',
|
2026-07-21 19:23:58 +08:00
|
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'reviewing', 'completed', 'archived')),
|
|
|
|
|
completed_at TIMESTAMPTZ,
|
2026-07-21 15:28:55 +08:00
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
UNIQUE (project_id, name)
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-21 19:23:58 +08:00
|
|
|
ALTER TABLE collections ADD COLUMN IF NOT EXISTS completed_at TIMESTAMPTZ;
|
|
|
|
|
|
2026-07-21 15:28:55 +08:00
|
|
|
CREATE TABLE IF NOT EXISTS notes (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
collection_id BIGINT NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
2026-07-22 16:41:03 +08:00
|
|
|
project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE,
|
2026-07-21 18:28:21 +08:00
|
|
|
external_id TEXT,
|
2026-07-21 15:28:55 +08:00
|
|
|
title TEXT NOT NULL,
|
|
|
|
|
description TEXT NOT NULL DEFAULT '',
|
|
|
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
|
|
|
review_status TEXT NOT NULL DEFAULT 'pending' CHECK (review_status IN ('draft', 'pending', 'changes_requested', 'approved')),
|
|
|
|
|
version_number INTEGER NOT NULL DEFAULT 1 CHECK (version_number > 0),
|
2026-07-21 20:25:52 +08:00
|
|
|
active_round_id BIGINT,
|
|
|
|
|
approved_version_number INTEGER,
|
2026-07-21 15:28:55 +08:00
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-21 18:28:21 +08:00
|
|
|
ALTER TABLE notes ADD COLUMN IF NOT EXISTS external_id TEXT;
|
2026-07-22 16:41:03 +08:00
|
|
|
ALTER TABLE notes ADD COLUMN IF NOT EXISTS project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE;
|
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS review_status TEXT NOT NULL DEFAULT 'draft';
|
|
|
|
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS review_completed_at TIMESTAMPTZ;
|
2026-07-21 18:28:21 +08:00
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_collection_external_id ON notes(collection_id, external_id) WHERE external_id IS NOT NULL AND external_id != '';
|
|
|
|
|
|
2026-07-21 15:28:55 +08:00
|
|
|
CREATE TABLE IF NOT EXISTS images (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
|
|
|
url TEXT NOT NULL,
|
|
|
|
|
width INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
height INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
order_index INTEGER NOT NULL DEFAULT 0,
|
2026-07-21 18:28:21 +08:00
|
|
|
storage_provider TEXT NOT NULL DEFAULT 'local' CHECK (storage_provider IN ('local', 'tencent_cos', 'external')),
|
2026-07-21 15:28:55 +08:00
|
|
|
storage_key TEXT NOT NULL DEFAULT '',
|
|
|
|
|
version_number INTEGER NOT NULL DEFAULT 1 CHECK (version_number > 0)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS annotations (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
image_id BIGINT NOT NULL REFERENCES images(id) ON DELETE CASCADE,
|
|
|
|
|
x DOUBLE PRECISION NOT NULL CHECK (x BETWEEN 0 AND 1),
|
|
|
|
|
y DOUBLE PRECISION NOT NULL CHECK (y BETWEEN 0 AND 1),
|
|
|
|
|
content TEXT NOT NULL,
|
|
|
|
|
author_name TEXT NOT NULL DEFAULT '客户',
|
2026-07-22 16:41:03 +08:00
|
|
|
author_role TEXT NOT NULL DEFAULT 'client' CHECK (author_role IN ('client', 'operator')),
|
2026-07-21 15:28:55 +08:00
|
|
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'resolved', 'confirmed')),
|
2026-07-22 16:41:03 +08:00
|
|
|
closure_reason TEXT NOT NULL DEFAULT '',
|
|
|
|
|
withdrawn_at TIMESTAMPTZ,
|
2026-07-21 15:28:55 +08:00
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS work_comments (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
2026-07-22 16:41:03 +08:00
|
|
|
version_number INTEGER NOT NULL DEFAULT 1,
|
2026-07-21 15:28:55 +08:00
|
|
|
content TEXT NOT NULL,
|
|
|
|
|
author_name TEXT NOT NULL DEFAULT '客户',
|
|
|
|
|
author_role TEXT NOT NULL DEFAULT 'client' CHECK (author_role IN ('client', 'operator')),
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'resolved', 'confirmed')),
|
2026-07-22 16:41:03 +08:00
|
|
|
closure_reason TEXT NOT NULL DEFAULT '',
|
|
|
|
|
withdrawn_at TIMESTAMPTZ,
|
2026-07-21 15:28:55 +08:00
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS text_annotations (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
|
|
|
version_number INTEGER NOT NULL CHECK (version_number > 0),
|
2026-07-22 16:41:03 +08:00
|
|
|
target TEXT NOT NULL CHECK (target IN ('title', 'description', 'tags')),
|
|
|
|
|
start_offset INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
end_offset INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
selected_text TEXT NOT NULL DEFAULT '',
|
|
|
|
|
prefix_text TEXT NOT NULL DEFAULT '',
|
|
|
|
|
suffix_text TEXT NOT NULL DEFAULT '',
|
2026-07-21 15:28:55 +08:00
|
|
|
content TEXT NOT NULL,
|
|
|
|
|
author_name TEXT NOT NULL DEFAULT '客户',
|
2026-07-22 16:41:03 +08:00
|
|
|
author_role TEXT NOT NULL DEFAULT 'client' CHECK (author_role IN ('client', 'operator')),
|
2026-07-21 15:28:55 +08:00
|
|
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'resolved', 'confirmed')),
|
2026-07-22 16:41:03 +08:00
|
|
|
closure_reason TEXT NOT NULL DEFAULT '',
|
|
|
|
|
withdrawn_at TIMESTAMPTZ,
|
2026-07-21 15:28:55 +08:00
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_text_annotations_note_id ON text_annotations(note_id, version_number);
|
|
|
|
|
|
2026-07-22 16:41:03 +08:00
|
|
|
CREATE TABLE IF NOT EXISTS feedback_replies (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
|
|
|
version_number INTEGER NOT NULL CHECK (version_number > 0),
|
|
|
|
|
feedback_type TEXT NOT NULL CHECK (feedback_type IN ('image_annotation', 'text_annotation', 'comment')),
|
|
|
|
|
feedback_id BIGINT NOT NULL,
|
|
|
|
|
content TEXT NOT NULL,
|
|
|
|
|
author_name TEXT NOT NULL,
|
|
|
|
|
author_role TEXT NOT NULL CHECK (author_role IN ('client', 'operator')),
|
|
|
|
|
withdrawn_at TIMESTAMPTZ,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_feedback_replies_target ON feedback_replies(note_id, version_number, feedback_type, feedback_id);
|
|
|
|
|
|
2026-07-21 15:28:55 +08:00
|
|
|
CREATE TABLE IF NOT EXISTS work_versions (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
|
|
|
version_number INTEGER NOT NULL CHECK (version_number > 0),
|
|
|
|
|
title TEXT NOT NULL,
|
|
|
|
|
description TEXT NOT NULL DEFAULT '',
|
|
|
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
|
|
|
review_status TEXT NOT NULL DEFAULT 'pending' CHECK (review_status IN ('draft', 'pending', 'changes_requested', 'approved')),
|
2026-07-21 20:25:52 +08:00
|
|
|
review_round_id BIGINT,
|
|
|
|
|
candidate_name TEXT NOT NULL DEFAULT '方案 A',
|
|
|
|
|
candidate_status TEXT NOT NULL DEFAULT 'pending' CHECK (candidate_status IN ('draft', 'pending', 'changes_requested', 'selected', 'not_selected')),
|
2026-07-21 15:28:55 +08:00
|
|
|
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
UNIQUE (note_id, version_number)
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-21 20:25:52 +08:00
|
|
|
CREATE TABLE IF NOT EXISTS review_rounds (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
|
|
|
round_number INTEGER NOT NULL CHECK (round_number > 0),
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'reviewing' CHECK (status IN ('draft', 'reviewing', 'completed')),
|
|
|
|
|
selected_version_number INTEGER,
|
|
|
|
|
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
completed_at TIMESTAMPTZ,
|
2026-07-22 16:41:03 +08:00
|
|
|
completion_reason TEXT NOT NULL DEFAULT '',
|
2026-07-21 20:25:52 +08:00
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
UNIQUE(note_id, round_number)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
ALTER TABLE notes ADD COLUMN IF NOT EXISTS active_round_id BIGINT;
|
|
|
|
|
ALTER TABLE notes ADD COLUMN IF NOT EXISTS approved_version_number INTEGER;
|
|
|
|
|
ALTER TABLE work_versions ADD COLUMN IF NOT EXISTS review_round_id BIGINT;
|
|
|
|
|
ALTER TABLE work_versions ADD COLUMN IF NOT EXISTS candidate_name TEXT NOT NULL DEFAULT '方案 A';
|
|
|
|
|
ALTER TABLE work_versions ADD COLUMN IF NOT EXISTS candidate_status TEXT NOT NULL DEFAULT 'pending';
|
2026-07-22 16:41:03 +08:00
|
|
|
ALTER TABLE review_rounds ADD COLUMN IF NOT EXISTS completion_reason TEXT NOT NULL DEFAULT '';
|
|
|
|
|
ALTER TABLE annotations ADD COLUMN IF NOT EXISTS author_role TEXT NOT NULL DEFAULT 'client';
|
|
|
|
|
ALTER TABLE annotations ADD COLUMN IF NOT EXISTS withdrawn_at TIMESTAMPTZ;
|
|
|
|
|
ALTER TABLE annotations ADD COLUMN IF NOT EXISTS closure_reason TEXT NOT NULL DEFAULT '';
|
|
|
|
|
ALTER TABLE work_comments ADD COLUMN IF NOT EXISTS version_number INTEGER NOT NULL DEFAULT 1;
|
|
|
|
|
ALTER TABLE work_comments ADD COLUMN IF NOT EXISTS withdrawn_at TIMESTAMPTZ;
|
|
|
|
|
ALTER TABLE work_comments ADD COLUMN IF NOT EXISTS closure_reason TEXT NOT NULL DEFAULT '';
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS start_offset INTEGER NOT NULL DEFAULT 0;
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS end_offset INTEGER NOT NULL DEFAULT 0;
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS selected_text TEXT NOT NULL DEFAULT '';
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS prefix_text TEXT NOT NULL DEFAULT '';
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS suffix_text TEXT NOT NULL DEFAULT '';
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS author_role TEXT NOT NULL DEFAULT 'client';
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS withdrawn_at TIMESTAMPTZ;
|
|
|
|
|
ALTER TABLE text_annotations ADD COLUMN IF NOT EXISTS closure_reason TEXT NOT NULL DEFAULT '';
|
|
|
|
|
|
|
|
|
|
-- TEXT_ANNOTATION_TARGET_REPAIR_START
|
|
|
|
|
ALTER TABLE text_annotations DROP CONSTRAINT IF EXISTS text_annotations_target_check;
|
|
|
|
|
ALTER TABLE text_annotations ADD CONSTRAINT text_annotations_target_check
|
|
|
|
|
CHECK (target IN ('title', 'description', 'tags'));
|
|
|
|
|
-- TEXT_ANNOTATION_TARGET_REPAIR_END
|
2026-07-21 20:25:52 +08:00
|
|
|
|
2026-07-21 15:28:55 +08:00
|
|
|
CREATE TABLE IF NOT EXISTS review_events (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
|
|
|
version_number INTEGER NOT NULL,
|
|
|
|
|
event_type TEXT NOT NULL CHECK (event_type IN ('submitted', 'approved', 'changes_requested', 'reopened')),
|
|
|
|
|
from_status TEXT,
|
|
|
|
|
to_status TEXT NOT NULL,
|
|
|
|
|
reason TEXT NOT NULL DEFAULT '',
|
|
|
|
|
actor_name TEXT NOT NULL,
|
|
|
|
|
actor_role TEXT NOT NULL,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
|
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS customer_sessions (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
|
|
|
reviewer_name TEXT NOT NULL,
|
|
|
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
|
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
group_id BIGINT REFERENCES operation_groups(id) ON DELETE SET NULL,
|
|
|
|
|
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
action TEXT NOT NULL,
|
|
|
|
|
entity_type TEXT NOT NULL,
|
|
|
|
|
entity_id BIGINT,
|
|
|
|
|
detail TEXT NOT NULL DEFAULT '{}',
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
group_id BIGINT REFERENCES operation_groups(id) ON DELETE RESTRICT,
|
|
|
|
|
project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE,
|
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
|
key_prefix TEXT NOT NULL UNIQUE,
|
|
|
|
|
key_hash TEXT NOT NULL UNIQUE,
|
|
|
|
|
scope TEXT NOT NULL CHECK (scope IN ('platform', 'project')),
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
|
|
|
|
|
created_by BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
|
|
|
last_used_at TIMESTAMPTZ,
|
|
|
|
|
revoked_at TIMESTAMPTZ,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS storage_configs (
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
provider TEXT NOT NULL DEFAULT 'tencent_cos',
|
|
|
|
|
region TEXT NOT NULL,
|
|
|
|
|
bucket TEXT NOT NULL,
|
|
|
|
|
public_base_url TEXT NOT NULL DEFAULT '',
|
|
|
|
|
cdn_domain TEXT NOT NULL DEFAULT '',
|
|
|
|
|
path_prefix TEXT NOT NULL DEFAULT 'delivery-desk',
|
|
|
|
|
secret_id_encrypted TEXT NOT NULL,
|
|
|
|
|
secret_key_encrypted TEXT NOT NULL,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'archived')),
|
|
|
|
|
test_status TEXT NOT NULL DEFAULT 'untested' CHECK (test_status IN ('untested', 'passed', 'failed')),
|
|
|
|
|
test_message TEXT NOT NULL DEFAULT '',
|
|
|
|
|
last_tested_at TIMESTAMPTZ,
|
|
|
|
|
created_by BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
activated_at TIMESTAMPTZ
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS one_active_storage_config ON storage_configs ((status)) WHERE status = 'active';
|
|
|
|
|
CREATE INDEX IF NOT EXISTS collections_project_id_idx ON collections(project_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS notes_collection_id_idx ON notes(collection_id);
|
2026-07-22 16:41:03 +08:00
|
|
|
CREATE INDEX IF NOT EXISTS notes_project_id_idx ON notes(project_id);
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_project_external_id ON notes(project_id, external_id) WHERE external_id IS NOT NULL AND external_id != '';
|
2026-07-21 15:28:55 +08:00
|
|
|
CREATE INDEX IF NOT EXISTS images_note_version_idx ON images(note_id, version_number, order_index);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS annotations_image_id_idx ON annotations(image_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS comments_note_id_idx ON work_comments(note_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS customer_sessions_project_id_idx ON customer_sessions(project_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS review_events_note_version_idx ON review_events(note_id, version_number);
|
2026-07-21 20:25:52 +08:00
|
|
|
CREATE INDEX IF NOT EXISTS review_rounds_note_id_idx ON review_rounds(note_id, round_number);
|
2026-07-21 15:28:55 +08:00
|
|
|
CREATE INDEX IF NOT EXISTS audit_logs_group_id_idx ON audit_logs(group_id);
|
|
|
|
|
|
2026-07-22 16:41:03 +08:00
|
|
|
-- SINGLE_SCHEME_REPAIR_START
|
|
|
|
|
DO $$
|
|
|
|
|
DECLARE
|
|
|
|
|
duplicate RECORD;
|
|
|
|
|
version_row RECORD;
|
|
|
|
|
source_round RECORD;
|
|
|
|
|
note_row RECORD;
|
|
|
|
|
keeper_version INTEGER;
|
|
|
|
|
next_round INTEGER;
|
|
|
|
|
new_round_id BIGINT;
|
|
|
|
|
remains_active BOOLEAN;
|
|
|
|
|
BEGIN
|
|
|
|
|
FOR duplicate IN
|
|
|
|
|
SELECT review_round_id, note_id FROM work_versions
|
|
|
|
|
WHERE review_round_id IS NOT NULL
|
|
|
|
|
GROUP BY review_round_id, note_id HAVING COUNT(*) > 1
|
|
|
|
|
LOOP
|
|
|
|
|
SELECT * INTO source_round FROM review_rounds WHERE id = duplicate.review_round_id;
|
|
|
|
|
SELECT active_round_id, version_number INTO note_row FROM notes WHERE id = duplicate.note_id;
|
|
|
|
|
SELECT COALESCE(
|
|
|
|
|
(SELECT v.version_number FROM work_versions v WHERE v.review_round_id=duplicate.review_round_id AND v.version_number=note_row.version_number LIMIT 1),
|
|
|
|
|
(SELECT MIN(v.version_number) FROM work_versions v WHERE v.review_round_id=duplicate.review_round_id)
|
|
|
|
|
) INTO keeper_version;
|
|
|
|
|
FOR version_row IN SELECT version_number FROM work_versions WHERE review_round_id=duplicate.review_round_id AND version_number<>keeper_version ORDER BY version_number
|
|
|
|
|
LOOP
|
|
|
|
|
SELECT COALESCE(MAX(round_number),0)+1 INTO next_round FROM review_rounds WHERE note_id=duplicate.note_id;
|
|
|
|
|
remains_active := note_row.active_round_id=duplicate.review_round_id AND note_row.version_number=version_row.version_number;
|
|
|
|
|
INSERT INTO review_rounds (note_id,round_number,status,selected_version_number,completed_at,created_by,created_at,completion_reason)
|
|
|
|
|
VALUES (
|
|
|
|
|
duplicate.note_id,
|
|
|
|
|
next_round,
|
|
|
|
|
CASE WHEN remains_active THEN source_round.status ELSE 'completed' END,
|
|
|
|
|
CASE WHEN source_round.selected_version_number=version_row.version_number THEN version_row.version_number ELSE NULL END,
|
|
|
|
|
CASE WHEN remains_active THEN source_round.completed_at ELSE COALESCE(source_round.completed_at,NOW()) END,
|
|
|
|
|
source_round.created_by,
|
|
|
|
|
source_round.created_at,
|
|
|
|
|
CASE WHEN remains_active THEN source_round.completion_reason ELSE COALESCE(NULLIF(source_round.completion_reason,''),'migrated_single_scheme') END
|
|
|
|
|
) RETURNING id INTO new_round_id;
|
|
|
|
|
UPDATE work_versions SET review_round_id=new_round_id WHERE note_id=duplicate.note_id AND version_number=version_row.version_number;
|
|
|
|
|
IF remains_active THEN UPDATE notes SET active_round_id=new_round_id WHERE id=duplicate.note_id; END IF;
|
|
|
|
|
END LOOP;
|
|
|
|
|
IF source_round.selected_version_number IS DISTINCT FROM keeper_version THEN
|
|
|
|
|
UPDATE review_rounds SET selected_version_number=NULL WHERE id=duplicate.review_round_id;
|
|
|
|
|
END IF;
|
|
|
|
|
END LOOP;
|
|
|
|
|
END $$;
|
|
|
|
|
-- SINGLE_SCHEME_REPAIR_END
|
|
|
|
|
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_versions_one_per_round ON work_versions(review_round_id) WHERE review_round_id IS NOT NULL;
|
|
|
|
|
|
2026-07-21 19:23:58 +08:00
|
|
|
-- COLLECTION_STATUS_REPAIR_START
|
|
|
|
|
UPDATE collections
|
|
|
|
|
SET status = 'draft', completed_at = NULL
|
|
|
|
|
WHERE status != 'archived'
|
|
|
|
|
AND id NOT IN (SELECT collection_id FROM notes WHERE review_status != 'draft');
|
|
|
|
|
|
|
|
|
|
UPDATE collections AS c
|
|
|
|
|
SET status = CASE WHEN s.approved_count = s.work_count THEN 'completed' ELSE 'reviewing' END,
|
|
|
|
|
completed_at = CASE WHEN s.approved_count = s.work_count THEN COALESCE(c.completed_at, NOW()) ELSE NULL END
|
|
|
|
|
FROM (
|
|
|
|
|
SELECT collection_id,
|
|
|
|
|
COUNT(*) AS work_count,
|
|
|
|
|
SUM(CASE WHEN review_status = 'approved' THEN 1 ELSE 0 END) AS approved_count
|
|
|
|
|
FROM notes
|
|
|
|
|
WHERE review_status != 'draft'
|
|
|
|
|
GROUP BY collection_id
|
|
|
|
|
) AS s
|
|
|
|
|
WHERE c.id = s.collection_id AND c.status != 'archived';
|
|
|
|
|
-- COLLECTION_STATUS_REPAIR_END
|
|
|
|
|
|
2026-07-21 20:25:52 +08:00
|
|
|
-- REVIEW_ROUND_REPAIR_START
|
|
|
|
|
INSERT INTO review_rounds (note_id, round_number, status, selected_version_number, completed_at, created_at)
|
|
|
|
|
SELECT v.note_id, v.version_number,
|
|
|
|
|
CASE WHEN v.version_number = n.version_number AND n.review_status != 'approved' THEN 'reviewing' ELSE 'completed' END,
|
|
|
|
|
CASE WHEN v.review_status = 'approved' THEN v.version_number ELSE NULL END,
|
|
|
|
|
CASE WHEN v.version_number = n.version_number AND n.review_status != 'approved' THEN NULL ELSE v.created_at END,
|
|
|
|
|
v.created_at
|
|
|
|
|
FROM work_versions v JOIN notes n ON n.id = v.note_id
|
|
|
|
|
ON CONFLICT (note_id, round_number) DO NOTHING;
|
|
|
|
|
|
|
|
|
|
UPDATE work_versions v
|
|
|
|
|
SET review_round_id = r.id,
|
|
|
|
|
candidate_name = COALESCE(NULLIF(v.candidate_name, ''), '方案 A'),
|
|
|
|
|
candidate_status = CASE
|
|
|
|
|
WHEN v.review_status = 'approved' THEN 'selected'
|
|
|
|
|
WHEN v.version_number = n.version_number AND v.review_status = 'changes_requested' THEN 'changes_requested'
|
|
|
|
|
WHEN v.version_number = n.version_number AND v.review_status = 'pending' THEN 'pending'
|
|
|
|
|
WHEN v.version_number = n.version_number AND v.review_status = 'draft' THEN 'draft'
|
|
|
|
|
ELSE 'not_selected'
|
|
|
|
|
END
|
|
|
|
|
FROM review_rounds r, notes n
|
|
|
|
|
WHERE r.note_id = v.note_id AND r.round_number = v.version_number AND n.id = v.note_id AND v.review_round_id IS NULL;
|
|
|
|
|
|
|
|
|
|
UPDATE notes n
|
|
|
|
|
SET active_round_id = r.id,
|
|
|
|
|
approved_version_number = (SELECT MAX(v.version_number) FROM work_versions v WHERE v.note_id = n.id AND v.review_status = 'approved')
|
|
|
|
|
FROM review_rounds r
|
|
|
|
|
WHERE r.note_id = n.id AND r.round_number = n.version_number AND n.active_round_id IS NULL;
|
|
|
|
|
-- REVIEW_ROUND_REPAIR_END
|
|
|
|
|
|
2026-07-22 16:41:03 +08:00
|
|
|
-- PROJECT_REVIEW_STATUS_REPAIR_START
|
|
|
|
|
UPDATE notes n
|
|
|
|
|
SET project_id = c.project_id
|
|
|
|
|
FROM collections c
|
|
|
|
|
WHERE c.id = n.collection_id AND n.project_id IS NULL;
|
|
|
|
|
|
|
|
|
|
UPDATE work_comments wc
|
|
|
|
|
SET version_number = n.version_number
|
|
|
|
|
FROM notes n
|
|
|
|
|
WHERE n.id = wc.note_id AND wc.version_number < 1;
|
|
|
|
|
|
|
|
|
|
UPDATE projects p
|
|
|
|
|
SET review_status = CASE
|
|
|
|
|
WHEN p.status = 'archived' THEN 'archived'
|
|
|
|
|
WHEN s.work_count IS NULL OR s.work_count = 0 THEN 'draft'
|
|
|
|
|
WHEN s.approved_count = s.work_count THEN 'completed'
|
|
|
|
|
ELSE 'reviewing'
|
|
|
|
|
END,
|
|
|
|
|
review_completed_at = CASE
|
|
|
|
|
WHEN p.status != 'archived' AND s.work_count > 0 AND s.approved_count = s.work_count
|
|
|
|
|
THEN COALESCE(p.review_completed_at, NOW())
|
|
|
|
|
ELSE NULL
|
|
|
|
|
END
|
|
|
|
|
FROM (
|
|
|
|
|
SELECT project_id,
|
|
|
|
|
COUNT(*) FILTER (WHERE review_status != 'draft') AS work_count,
|
|
|
|
|
COUNT(*) FILTER (WHERE review_status = 'approved') AS approved_count
|
|
|
|
|
FROM notes GROUP BY project_id
|
|
|
|
|
) s
|
|
|
|
|
WHERE p.id = s.project_id;
|
|
|
|
|
|
|
|
|
|
UPDATE projects p
|
|
|
|
|
SET review_status = CASE WHEN p.status = 'archived' THEN 'archived' ELSE 'draft' END,
|
|
|
|
|
review_completed_at = NULL
|
|
|
|
|
WHERE NOT EXISTS (SELECT 1 FROM notes n WHERE n.project_id = p.id AND n.review_status != 'draft');
|
|
|
|
|
-- PROJECT_REVIEW_STATUS_REPAIR_END
|
|
|
|
|
|
2026-07-21 15:28:55 +08:00
|
|
|
COMMIT;
|