feat(db): add outbox, notifications and stage advance request tables.

Support transactional stage.ready_to_advance events with dedupe by stage instance and config revision.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-12 03:14:30 +00:00
co-authored by Cursor
parent 5ec4951f9b
commit 00b0bb6882
@@ -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;