package meta import ( "context" "database/sql" "fmt" ) const metaSchemaSQL = ` CREATE SCHEMA IF NOT EXISTS platform_meta; CREATE TABLE IF NOT EXISTS platform_meta.tenants ( tenant_id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL, slug VARCHAR(64) NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS platform_meta.users ( user_id BIGSERIAL PRIMARY KEY, tenant_id BIGINT REFERENCES platform_meta.tenants(tenant_id), username VARCHAR(64) NOT NULL, password_hash TEXT NOT NULL, display_name VARCHAR(128) NOT NULL DEFAULT '', role VARCHAR(32) NOT NULL DEFAULT 'pending', status VARCHAR(32) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_users_username UNIQUE (username) ); CREATE INDEX IF NOT EXISTS idx_users_tenant ON platform_meta.users (tenant_id); -- 兼容旧库:允许无租户(pending) ALTER TABLE platform_meta.users ALTER COLUMN tenant_id DROP NOT NULL; ALTER TABLE platform_meta.users ADD COLUMN IF NOT EXISTS status VARCHAR(32) NOT NULL DEFAULT 'active'; CREATE TABLE IF NOT EXISTS platform_meta.tenant_apps ( app_id UUID PRIMARY KEY, tenant_id BIGINT NOT NULL, slug VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, schema_name VARCHAR(64) NOT NULL, engine VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL, blueprint_json JSONB NOT NULL, ddl_json JSONB NOT NULL DEFAULT '[]'::jsonb, endpoints_json JSONB NOT NULL DEFAULT '[]'::jsonb, error_msg TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_tenant_apps_tenant_slug UNIQUE (tenant_id, slug) ); CREATE INDEX IF NOT EXISTS idx_tenant_apps_tenant_status ON platform_meta.tenant_apps (tenant_id, status); ALTER TABLE platform_meta.tenant_apps ADD COLUMN IF NOT EXISTS database_name VARCHAR(64) NOT NULL DEFAULT ''; CREATE TABLE IF NOT EXISTS platform_meta.audit_logs ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, action VARCHAR(64) NOT NULL, detail TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 智能体服务账号(机器身份,供宿主自动登录) CREATE TABLE IF NOT EXISTS platform_meta.agent_accounts ( agent_id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES platform_meta.tenants(tenant_id), name VARCHAR(128) NOT NULL, client_id VARCHAR(64) NOT NULL, client_secret_hash TEXT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'active', created_by BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_token_at TIMESTAMPTZ, CONSTRAINT uq_agent_client_id UNIQUE (client_id) ); CREATE INDEX IF NOT EXISTS idx_agent_accounts_tenant ON platform_meta.agent_accounts (tenant_id); ALTER TABLE platform_meta.agent_accounts ADD COLUMN IF NOT EXISTS host_key VARCHAR(128) NOT NULL DEFAULT ''; CREATE UNIQUE INDEX IF NOT EXISTS uq_agent_tenant_host_key ON platform_meta.agent_accounts (tenant_id, host_key) WHERE host_key <> ''; CREATE TABLE IF NOT EXISTS platform_meta.agent_permissions ( agent_id BIGINT NOT NULL REFERENCES platform_meta.agent_accounts(agent_id) ON DELETE CASCADE, perm VARCHAR(64) NOT NULL, PRIMARY KEY (agent_id, perm) ); CREATE TABLE IF NOT EXISTS platform_meta.agent_app_grants ( agent_id BIGINT NOT NULL REFERENCES platform_meta.agent_accounts(agent_id) ON DELETE CASCADE, slug VARCHAR(64) NOT NULL, PRIMARY KEY (agent_id, slug) ); -- 可编辑角色(租户级) CREATE TABLE IF NOT EXISTS platform_meta.roles ( role_id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES platform_meta.tenants(tenant_id), code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, description TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_roles_tenant_code UNIQUE (tenant_id, code) ); CREATE INDEX IF NOT EXISTS idx_roles_tenant ON platform_meta.roles (tenant_id); CREATE TABLE IF NOT EXISTS platform_meta.role_permissions ( role_id BIGINT NOT NULL REFERENCES platform_meta.roles(role_id) ON DELETE CASCADE, perm VARCHAR(64) NOT NULL, PRIMARY KEY (role_id, perm) ); ALTER TABLE platform_meta.agent_accounts ADD COLUMN IF NOT EXISTS role_id BIGINT; -- 租户邀请码:pending 用户凭码加入已有公司 CREATE TABLE IF NOT EXISTS platform_meta.tenant_invites ( invite_id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES platform_meta.tenants(tenant_id) ON DELETE CASCADE, code VARCHAR(64) NOT NULL, role VARCHAR(32) NOT NULL DEFAULT 'editor', created_by BIGINT NOT NULL DEFAULT 0, max_uses INT NOT NULL DEFAULT 1, used_count INT NOT NULL DEFAULT 0, status VARCHAR(32) NOT NULL DEFAULT 'active', expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT uq_tenant_invites_code UNIQUE (code) ); CREATE INDEX IF NOT EXISTS idx_tenant_invites_tenant ON platform_meta.tenant_invites (tenant_id); -- 租户内多级组织(默认最多 5 级) CREATE TABLE IF NOT EXISTS platform_meta.org_units ( org_unit_id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL REFERENCES platform_meta.tenants(tenant_id) ON DELETE CASCADE, parent_id BIGINT REFERENCES platform_meta.org_units(org_unit_id) ON DELETE CASCADE, name VARCHAR(128) NOT NULL, code VARCHAR(64) NOT NULL DEFAULT '', depth INT NOT NULL DEFAULT 1, path VARCHAR(512) NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_org_units_tenant_parent ON platform_meta.org_units (tenant_id, parent_id); CREATE INDEX IF NOT EXISTS idx_org_units_tenant_path ON platform_meta.org_units (tenant_id, path); CREATE UNIQUE INDEX IF NOT EXISTS uq_org_units_tenant_code ON platform_meta.org_units (tenant_id, code) WHERE code <> ''; ALTER TABLE platform_meta.users ADD COLUMN IF NOT EXISTS org_unit_id BIGINT; ALTER TABLE platform_meta.tenant_invites ADD COLUMN IF NOT EXISTS org_unit_id BIGINT NOT NULL DEFAULT 0; ALTER TABLE platform_meta.users ALTER COLUMN role TYPE VARCHAR(64); CREATE TABLE IF NOT EXISTS platform_meta.tenant_permissions ( tenant_id BIGINT NOT NULL REFERENCES platform_meta.tenants(tenant_id) ON DELETE CASCADE, perm VARCHAR(64) NOT NULL, PRIMARY KEY (tenant_id, perm) ); CREATE TABLE IF NOT EXISTS platform_meta.tenant_entitlement_state ( tenant_id BIGINT PRIMARY KEY REFERENCES platform_meta.tenants(tenant_id) ON DELETE CASCADE, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 公司路径 slug(www.example.com/{slug}/);旧库补列 ALTER TABLE platform_meta.tenants ADD COLUMN IF NOT EXISTS slug VARCHAR(64) NOT NULL DEFAULT ''; CREATE UNIQUE INDEX IF NOT EXISTS uq_tenants_slug ON platform_meta.tenants (slug) WHERE slug <> ''; -- 手机号(可绑定;非空时全局唯一,可用于登录) ALTER TABLE platform_meta.users ADD COLUMN IF NOT EXISTS phone VARCHAR(20) NOT NULL DEFAULT ''; CREATE UNIQUE INDEX IF NOT EXISTS uq_users_phone ON platform_meta.users (phone) WHERE phone <> ''; -- 已绑手机时可禁用用户名登录(仅手机号+密码/短信) ALTER TABLE platform_meta.users ADD COLUMN IF NOT EXISTS username_login_disabled BOOLEAN NOT NULL DEFAULT false; -- 授权租约已消费 id(删 leases 目录后仍能拦截旧延期包) CREATE TABLE IF NOT EXISTS platform_meta.license_consumed ( lease_id VARCHAR(64) PRIMARY KEY, active BOOLEAN NOT NULL DEFAULT false, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_license_consumed_active ON platform_meta.license_consumed (active) WHERE active; ` // EnsureSchema 创建平台元数据表(幂等)。 func EnsureSchema(ctx context.Context, db *sql.DB) error { if db == nil { return fmt.Errorf("db is nil") } if _, err := db.ExecContext(ctx, metaSchemaSQL); err != nil { return fmt.Errorf("meta migrate: %w", err) } return nil }