diff --git a/deploy/postgres/goose/00018_2026-09-12-outbox-notifications-stage-advance.sql b/deploy/postgres/goose/00018_2026-09-12-outbox-notifications-stage-advance.sql new file mode 100644 index 00000000..e24c9831 --- /dev/null +++ b/deploy/postgres/goose/00018_2026-09-12-outbox-notifications-stage-advance.sql @@ -0,0 +1,58 @@ +-- +goose Up +-- Phase 4: Outbox 事件、站内通知与阶段推进待办去重 + +CREATE TABLE IF NOT EXISTS outbox_events ( + id BIGSERIAL PRIMARY KEY, + aggregate_type TEXT NOT NULL, + aggregate_id TEXT NOT NULL, + event_type TEXT NOT NULL, + payload JSONB NOT NULL DEFAULT '{}'::jsonb, + project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + published_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_outbox_unpublished ON outbox_events(created_at) WHERE published_at IS NULL; +CREATE INDEX IF NOT EXISTS idx_outbox_project ON outbox_events(project_id, id DESC); + +CREATE TABLE IF NOT EXISTS notifications ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE, + event_id BIGINT REFERENCES outbox_events(id) ON DELETE SET NULL, + type TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL DEFAULT '', + dedupe_key TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'unread', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + read_at TIMESTAMPTZ, + acted_at TIMESTAMPTZ, + CONSTRAINT chk_notification_status CHECK (status IN ('unread', 'read', 'acted', 'dismissed')), + CONSTRAINT uq_notification_dedupe UNIQUE (user_id, dedupe_key) +); + +CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, id DESC); +CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(project_id, id DESC); + +CREATE TABLE IF NOT EXISTS stage_advance_requests ( + id BIGSERIAL PRIMARY KEY, + stage_instance_id BIGINT NOT NULL REFERENCES project_stage_instances(id) ON DELETE CASCADE, + workflow_instance_id BIGINT NOT NULL REFERENCES project_workflow_instances(id) ON DELETE CASCADE, + stage_key TEXT NOT NULL, + readiness_revision INT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + requested_at TIMESTAMPTZ NOT NULL DEFAULT now(), + decided_by BIGINT REFERENCES users(id) ON DELETE SET NULL, + decided_at TIMESTAMPTZ, + reason TEXT NOT NULL DEFAULT '', + CONSTRAINT chk_sar_status CHECK (status IN ('pending', 'confirmed', 'superseded', 'cancelled')), + CONSTRAINT uq_sar_stage_revision UNIQUE (stage_instance_id, readiness_revision) +); + +CREATE INDEX IF NOT EXISTS idx_sar_instance ON stage_advance_requests(workflow_instance_id, status); + +-- +goose Down +DROP TABLE IF EXISTS stage_advance_requests; +DROP TABLE IF EXISTS notifications; +DROP TABLE IF EXISTS outbox_events;