-- +goose Up -- +goose StatementBegin CREATE TABLE IF NOT EXISTS tenant_invites( id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, user_id bigint NOT NULL, code varchar(64) NOT NULL, status varchar(32) NOT NULL DEFAULT 'active', max_uses int NOT NULL DEFAULT 0, used_count int NOT NULL DEFAULT 0, expires_at timestamptz, disabled_at timestamptz, disabled_operator_user_id bigint, remark varchar(255) NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT NOW(), updated_at timestamptz NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, code) ); -- tenant_invites:租户邀请(用于用户通过邀请码加入租户) COMMENT ON TABLE tenant_invites IS '租户邀请:租户管理员生成的邀请码;用户可通过 code 加入租户;支持禁用、过期、使用次数限制;所有查询/写入必须限定 tenant_id'; COMMENT ON COLUMN tenant_invites.id IS '主键ID:自增'; COMMENT ON COLUMN tenant_invites.tenant_id IS '租户ID:多租户隔离关键字段;所有查询/写入必须限定 tenant_id'; COMMENT ON COLUMN tenant_invites.user_id IS '创建人用户ID:生成邀请码的租户管理员(审计用)'; COMMENT ON COLUMN tenant_invites.code IS '邀请码:用户加入租户时提交;同一租户内唯一'; COMMENT ON COLUMN tenant_invites.status IS '邀请状态:active/disabled/expired;expired 也可由 expires_at 推导,业务侧需保持一致'; COMMENT ON COLUMN tenant_invites.max_uses IS '最大可使用次数:0 表示不限制;>0 时 used_count 达到该值后视为失效'; COMMENT ON COLUMN tenant_invites.used_count IS '已使用次数:每次成功加入时 +1;需事务保证并发下不超发'; COMMENT ON COLUMN tenant_invites.expires_at IS '过期时间:到期后不可再使用(UTC);为空表示不过期'; COMMENT ON COLUMN tenant_invites.disabled_at IS '禁用时间:租户管理员禁用该邀请的时间(UTC)'; COMMENT ON COLUMN tenant_invites.disabled_operator_user_id IS '禁用操作人用户ID:租户管理员(审计用)'; COMMENT ON COLUMN tenant_invites.remark IS '备注:生成/禁用原因等(审计用)'; COMMENT ON COLUMN tenant_invites.created_at IS '创建时间:默认 now()'; COMMENT ON COLUMN tenant_invites.updated_at IS '更新时间:默认 now()'; CREATE INDEX IF NOT EXISTS ix_tenant_invites_tenant_status ON tenant_invites(tenant_id, status); CREATE INDEX IF NOT EXISTS ix_tenant_invites_tenant_expires_at ON tenant_invites(tenant_id, expires_at); CREATE TABLE IF NOT EXISTS tenant_join_requests( id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, user_id bigint NOT NULL, status varchar(32) NOT NULL DEFAULT 'pending', reason varchar(255) NOT NULL DEFAULT '', decided_at timestamptz, decided_operator_user_id bigint, decided_reason varchar(255) NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT NOW(), updated_at timestamptz NOT NULL DEFAULT NOW() ); -- tenant_join_requests:加入租户申请(用于无邀请码场景的人工审核) COMMENT ON TABLE tenant_join_requests IS '加入申请:用户申请加入租户,租户管理员审核通过/拒绝;所有查询/写入必须限定 tenant_id'; COMMENT ON COLUMN tenant_join_requests.id IS '主键ID:自增'; COMMENT ON COLUMN tenant_join_requests.tenant_id IS '租户ID:多租户隔离关键字段;所有查询/写入必须限定 tenant_id'; COMMENT ON COLUMN tenant_join_requests.user_id IS '申请人用户ID:发起加入申请的用户'; COMMENT ON COLUMN tenant_join_requests.status IS '申请状态:pending/approved/rejected;状态变更需记录 decided_at 与 decided_operator_user_id'; COMMENT ON COLUMN tenant_join_requests.reason IS '申请原因:用户填写的加入说明(可选)'; COMMENT ON COLUMN tenant_join_requests.decided_at IS '处理时间:审核通过/拒绝时记录(UTC)'; COMMENT ON COLUMN tenant_join_requests.decided_operator_user_id IS '处理人用户ID:租户管理员(审计用)'; COMMENT ON COLUMN tenant_join_requests.decided_reason IS '处理说明:管理员通过/拒绝的原因(可选,审计用)'; COMMENT ON COLUMN tenant_join_requests.created_at IS '创建时间:默认 now()'; COMMENT ON COLUMN tenant_join_requests.updated_at IS '更新时间:默认 now()'; -- 约束:同一用户同一租户同一时间仅允许存在一个 pending 申请,避免重复提交淹没审核队列。 CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_join_requests_tenant_user_pending ON tenant_join_requests(tenant_id, user_id) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS ix_tenant_join_requests_tenant_status ON tenant_join_requests(tenant_id, status); CREATE INDEX IF NOT EXISTS ix_tenant_join_requests_tenant_created_at ON tenant_join_requests(tenant_id, created_at); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP INDEX IF EXISTS ix_tenant_join_requests_tenant_created_at; DROP INDEX IF EXISTS ix_tenant_join_requests_tenant_status; DROP INDEX IF EXISTS ux_tenant_join_requests_tenant_user_pending; DROP TABLE IF EXISTS tenant_join_requests; DROP INDEX IF EXISTS ix_tenant_invites_tenant_expires_at; DROP INDEX IF EXISTS ix_tenant_invites_tenant_status; DROP TABLE IF EXISTS tenant_invites; -- +goose StatementEnd