-- ============================================================================ -- YG Fine-Tune Platform — PostgreSQL 完整初始化脚本(一键建库建表) -- ============================================================================ -- 用途:切换到新的 PG 数据集时,一次性创建平台运行所需的全部数据库对象与 -- 基础种子数据(幂等,可重复执行)。 -- -- 覆盖范围(与运行时代码实际使用的表一致,离线新库只需执行本文件): -- 001_platform_runtime.sql 平台核心表 -- 002_governance.sql 治理表(租户 / 审批 / 审计 / 留存) -- 003_tenant_quota.sql 租户配额列 -- 003_model_path_governance.sql 模型可训练标识列 -- 002_data_process.sql 数据处理表 + 数据集扩展列 -- 本文件补充:data_convert_tasks(数据转换任务,运行时代码引用但原脚本缺失) -- 种子数据:admin / operator 两个初始用户 -- -- 说明: -- * 本脚本通过 psql 执行,包含 DO $$ ... $$ 块与事务,不能用应用的 -- executescript()(按分号切分)执行。 -- * 本文件已经合并平台运行、治理、数据处理、权限、MinIO、GPU 预留、 -- 缓存治理和数据转换等全部初始化对象;全新数据库无需再执行其他 SQL。 -- 应用启动时的 ensure_schema() 仅作为兼容兜底,不是离线初始化前置条件。 -- * 脚本内所有 DDL 均使用 IF NOT EXISTS / ADD COLUMN IF NOT EXISTS, -- 可在已初始化的库上安全重复执行。 -- -- 执行步骤(详见 docs/database-config.md): -- 1. 以超级用户创建角色与数据库(必须单独执行,不能放进事务): -- CREATE ROLE yg_ft LOGIN PASSWORD '请改为强密码'; -- CREATE DATABASE yg_ft OWNER yg_ft; -- 2. 连接目标库执行本脚本: -- psql "postgresql://yg_ft:密码@:5432/yg_ft" -f backend/app/db/sql/000_full_init.sql -- 3. 可选:为 superuser 授权 -- ALTER ROLE yg_ft SUPERUSER; -- 仅当需要执行 CREATE EXTENSION 等 -- ============================================================================ BEGIN; -- ============================================================================ -- 一、平台核心表(来源:001_platform_runtime.sql) -- ============================================================================ CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, role TEXT NOT NULL, platform_role TEXT NOT NULL DEFAULT 'platform_user', status TEXT NOT NULL, permissions TEXT NOT NULL, create_time TEXT NOT NULL, last_login TEXT, protected INTEGER NOT NULL DEFAULT 0, tenant_id TEXT NOT NULL DEFAULT 'default', deleted_at TEXT, deleted_by TEXT ); ALTER TABLE users ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS deleted_by TEXT; CREATE INDEX IF NOT EXISTS idx_users_active ON users(status, deleted_at); CREATE INDEX IF NOT EXISTS idx_users_platform_role ON users(platform_role, status, deleted_at); CREATE TABLE IF NOT EXISTS models ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL, purpose TEXT NOT NULL, model_source TEXT NOT NULL, description TEXT, path TEXT, api_url TEXT, api_key TEXT, online_model_name TEXT, can_train INTEGER NOT NULL DEFAULT 0, create_time TEXT NOT NULL, created_by TEXT, tenant_id TEXT, project_id TEXT, storage_status TEXT NOT NULL DEFAULT 'pending', storage_error TEXT, storage_version_id TEXT, deleted_at TEXT, deleted_by TEXT ); CREATE TABLE IF NOT EXISTS trained_models ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, train_methods TEXT NOT NULL, base_model_path TEXT, create_time TEXT NOT NULL, merged INTEGER NOT NULL DEFAULT 0, merging INTEGER NOT NULL DEFAULT 0, merged_path TEXT, artifact_dir TEXT, compute_node_id TEXT, compute_node_name TEXT, created_by TEXT, tenant_id TEXT, project_id TEXT, deleted_at TEXT, deleted_by TEXT ); CREATE TABLE IF NOT EXISTS model_lineage ( id TEXT PRIMARY KEY, child_resource_type TEXT NOT NULL, child_resource_id TEXT NOT NULL, parent_resource_type TEXT NOT NULL, parent_resource_id TEXT NOT NULL, relation_type TEXT NOT NULL, compute_job_id TEXT, payload TEXT NOT NULL, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS model_artifacts ( id TEXT PRIMARY KEY, model_id TEXT NOT NULL, model_kind TEXT NOT NULL, artifact_type TEXT NOT NULL, path TEXT NOT NULL, size_bytes BIGINT NOT NULL DEFAULT 0, checksum_sha256 TEXT, storage_object_id TEXT, storage_backend TEXT NOT NULL DEFAULT 'minio', metadata TEXT NOT NULL, compute_job_id TEXT, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS model_export_jobs ( id TEXT PRIMARY KEY, trained_model_id TEXT, compute_job_id TEXT NOT NULL, node_id TEXT, export_type TEXT NOT NULL, quantization_bit INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL, output_dir TEXT, payload TEXT NOT NULL, create_time TEXT NOT NULL, completed_at TEXT ); ALTER TABLE model_export_jobs ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE model_export_jobs ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE model_export_jobs ADD COLUMN IF NOT EXISTS archive_status TEXT NOT NULL DEFAULT 'pending'; ALTER TABLE model_export_jobs ADD COLUMN IF NOT EXISTS archive_error TEXT; CREATE TABLE IF NOT EXISTS datasets ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, type TEXT NOT NULL, storage_type TEXT NOT NULL, source TEXT NOT NULL, task_id TEXT, size TEXT, count INTEGER NOT NULL DEFAULT 0, description TEXT, create_time TEXT NOT NULL, created_by TEXT, tenant_id TEXT, project_id TEXT, deleted_at TEXT, deleted_by TEXT ); CREATE TABLE IF NOT EXISTS dataset_files ( id TEXT PRIMARY KEY, dataset_id TEXT NOT NULL REFERENCES datasets(id) ON DELETE CASCADE, name TEXT NOT NULL, size TEXT, content TEXT NOT NULL, active_version_id TEXT NOT NULL, versions TEXT NOT NULL, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS compute_nodes ( id TEXT PRIMARY KEY, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, api_base_url TEXT NOT NULL, file_gateway_url TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, scheduler_status TEXT NOT NULL, scheduler_weight INTEGER NOT NULL DEFAULT 100, tags TEXT NOT NULL, gpu_count INTEGER NOT NULL DEFAULT 0, current_running_jobs INTEGER NOT NULL DEFAULT 0, max_parallel_jobs INTEGER NOT NULL DEFAULT 2, data_root TEXT NOT NULL, model_root TEXT NOT NULL, log_root TEXT NOT NULL, api_version TEXT NOT NULL DEFAULT 'v1', capabilities TEXT NOT NULL DEFAULT '[]', description TEXT, last_health_check_at TEXT, health_detail TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS gpus ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE, gpu_index INTEGER NOT NULL, uuid TEXT NOT NULL, name TEXT NOT NULL, memory_total_gb DOUBLE PRECISION NOT NULL, power_limit_w DOUBLE PRECISION NOT NULL, base_temperature INTEGER NOT NULL, last_seen_at TEXT ); CREATE TABLE IF NOT EXISTS fine_tune_tasks ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, payload TEXT NOT NULL, status TEXT NOT NULL, progress INTEGER NOT NULL DEFAULT 0, process_id INTEGER, create_time TEXT NOT NULL, start_time TEXT, completed_at TEXT, compute_node_id TEXT REFERENCES compute_nodes(id) ON DELETE SET NULL, gpus TEXT NOT NULL, sync_job_id TEXT, compute_job_id TEXT, tenant_id TEXT NOT NULL DEFAULT 'default', created_by TEXT, deleted_at TEXT, deleted_by TEXT ); CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_tenant_active ON fine_tune_tasks(tenant_id, status, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_creator ON fine_tune_tasks(created_by, deleted_at, create_time DESC); CREATE TABLE IF NOT EXISTS fine_tune_metrics ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE, step INTEGER NOT NULL, epoch DOUBLE PRECISION, loss DOUBLE PRECISION, grad_norm DOUBLE PRECISION, learning_rate DOUBLE PRECISION, raw TEXT NOT NULL, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS fine_tune_checkpoints ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE, step INTEGER NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL, size_bytes BIGINT NOT NULL DEFAULT 0, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS compute_jobs ( id TEXT PRIMARY KEY, task_id TEXT REFERENCES fine_tune_tasks(id) ON DELETE SET NULL, node_id TEXT REFERENCES compute_nodes(id) ON DELETE SET NULL, engine TEXT NOT NULL, status TEXT NOT NULL, command TEXT NOT NULL, output_dir TEXT, log_file TEXT, payload TEXT NOT NULL, create_time TEXT NOT NULL, update_time TEXT NOT NULL, completed_at TEXT ); CREATE TABLE IF NOT EXISTS gpu_allocations ( id TEXT PRIMARY KEY, task_id TEXT REFERENCES fine_tune_tasks(id) ON DELETE CASCADE, compute_job_id TEXT, node_id TEXT REFERENCES compute_nodes(id) ON DELETE CASCADE, gpu_index INTEGER NOT NULL, status TEXT NOT NULL, create_time TEXT NOT NULL, released_at TEXT ); CREATE TABLE IF NOT EXISTS scheduler_locks ( lock_key TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at TEXT NOT NULL, create_time TEXT NOT NULL, update_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS resource_replicas ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE, resource_type TEXT NOT NULL, resource_id TEXT NOT NULL, version_id TEXT, storage_object_id TEXT, local_path TEXT NOT NULL, status TEXT NOT NULL, sync_status TEXT NOT NULL, checksum_sha256 TEXT, byte_size BIGINT NOT NULL DEFAULT 0, last_checked_at TEXT, last_error TEXT, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS resource_sync_jobs ( id TEXT PRIMARY KEY, target_node_id TEXT NOT NULL, resources TEXT NOT NULL, status TEXT NOT NULL, progress INTEGER NOT NULL DEFAULT 0, create_time TEXT NOT NULL, completed_at TEXT ); CREATE TABLE IF NOT EXISTS storage_objects ( id TEXT PRIMARY KEY, resource_type TEXT NOT NULL, resource_id TEXT NOT NULL, version_id TEXT NOT NULL, bucket TEXT NOT NULL, object_key TEXT NOT NULL, file_name TEXT, content_type TEXT, checksum_sha256 TEXT, byte_size BIGINT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending', metadata TEXT NOT NULL DEFAULT '{}', created_by TEXT, create_time TEXT NOT NULL, UNIQUE (resource_type, resource_id, version_id, object_key) ); ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS deleted_by TEXT; ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS cleanup_attempts INTEGER NOT NULL DEFAULT 0; ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS last_cleanup_error TEXT; ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS last_verified_at TEXT; CREATE TABLE IF NOT EXISTS storage_cache_jobs ( id TEXT PRIMARY KEY, storage_object_id TEXT NOT NULL REFERENCES storage_objects(id) ON DELETE CASCADE, node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE, direction TEXT NOT NULL, status TEXT NOT NULL, progress INTEGER NOT NULL DEFAULT 0, local_path TEXT, error TEXT, create_time TEXT NOT NULL, completed_at TEXT ); ALTER TABLE storage_cache_jobs ADD COLUMN IF NOT EXISTS version_id TEXT; ALTER TABLE storage_cache_jobs ADD COLUMN IF NOT EXISTS checksum_sha256 TEXT; ALTER TABLE storage_cache_jobs ADD COLUMN IF NOT EXISTS byte_size BIGINT NOT NULL DEFAULT 0; ALTER TABLE storage_cache_jobs ADD COLUMN IF NOT EXISTS last_accessed_at TEXT; ALTER TABLE storage_cache_jobs ADD COLUMN IF NOT EXISTS protected_until TEXT; CREATE TABLE IF NOT EXISTS storage_cleanup_jobs ( id TEXT PRIMARY KEY, storage_object_id TEXT NOT NULL REFERENCES storage_objects(id) ON DELETE CASCADE, action TEXT NOT NULL DEFAULT 'delete', status TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0, error TEXT, create_time TEXT NOT NULL, completed_at TEXT ); CREATE INDEX IF NOT EXISTS idx_storage_cleanup_status ON storage_cleanup_jobs(status, create_time); CREATE INDEX IF NOT EXISTS idx_storage_objects_resource ON storage_objects(resource_type, resource_id, version_id); CREATE INDEX IF NOT EXISTS idx_storage_cache_jobs_node_status ON storage_cache_jobs(node_id, status); CREATE TABLE IF NOT EXISTS eval_tasks ( id TEXT PRIMARY KEY, name TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL, create_time TEXT NOT NULL, created_by TEXT, tenant_id TEXT, deleted_at TEXT, deleted_by TEXT ); ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS deleted_by TEXT; CREATE TABLE IF NOT EXISTS eval_dimensions ( id TEXT PRIMARY KEY, name TEXT NOT NULL, payload TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, is_default INTEGER NOT NULL DEFAULT 0, create_time TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS compare_tasks ( id TEXT PRIMARY KEY, name TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL, create_time TEXT NOT NULL, created_by TEXT, tenant_id TEXT, deleted_at TEXT, deleted_by TEXT ); ALTER TABLE compare_tasks ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE compare_tasks ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE compare_tasks ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE compare_tasks ADD COLUMN IF NOT EXISTS deleted_by TEXT; CREATE INDEX IF NOT EXISTS idx_compare_tasks_active ON compare_tasks(status, deleted_at); CREATE INDEX IF NOT EXISTS idx_fine_tune_status ON fine_tune_tasks(status); CREATE INDEX IF NOT EXISTS idx_fine_tune_compute_job ON fine_tune_tasks(compute_job_id); CREATE INDEX IF NOT EXISTS idx_fine_tune_compute_node_status ON fine_tune_tasks(compute_node_id, status); CREATE INDEX IF NOT EXISTS idx_model_lineage_child ON model_lineage(child_resource_type, child_resource_id); CREATE INDEX IF NOT EXISTS idx_model_lineage_parent ON model_lineage(parent_resource_type, parent_resource_id); CREATE INDEX IF NOT EXISTS idx_model_artifacts_model ON model_artifacts(model_kind, model_id, artifact_type); CREATE INDEX IF NOT EXISTS idx_model_export_jobs_model ON model_export_jobs(trained_model_id, create_time DESC); CREATE INDEX IF NOT EXISTS idx_model_export_jobs_compute ON model_export_jobs(compute_job_id); CREATE INDEX IF NOT EXISTS idx_fine_tune_metrics_task_step ON fine_tune_metrics(task_id, step); CREATE UNIQUE INDEX IF NOT EXISTS uq_fine_tune_metrics_task_step_epoch ON fine_tune_metrics(task_id, step, epoch); CREATE INDEX IF NOT EXISTS idx_fine_tune_checkpoints_task_step ON fine_tune_checkpoints(task_id, step); CREATE UNIQUE INDEX IF NOT EXISTS uq_fine_tune_checkpoints_task_path ON fine_tune_checkpoints(task_id, path); CREATE INDEX IF NOT EXISTS idx_compute_jobs_task ON compute_jobs(task_id); CREATE INDEX IF NOT EXISTS idx_compute_jobs_node_status ON compute_jobs(node_id, status); CREATE INDEX IF NOT EXISTS idx_gpu_allocations_node_status ON gpu_allocations(node_id, status); CREATE UNIQUE INDEX IF NOT EXISTS uq_gpu_allocations_active ON gpu_allocations(node_id, gpu_index) WHERE status IN ('allocated','running'); -- 评测/推理等非训练任务的统一 GPU 原子预留。训练任务继续使用 gpu_allocations。 CREATE TABLE IF NOT EXISTS gpu_reservations ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE, gpu_index INTEGER NOT NULL, owner_type TEXT NOT NULL CHECK (owner_type IN ('eval', 'inference')), owner_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'reserved' CHECK (status IN ('reserved', 'released')), create_time TEXT NOT NULL, released_at TEXT ); CREATE INDEX IF NOT EXISTS idx_gpu_reservations_owner ON gpu_reservations(owner_type, owner_id, status); CREATE INDEX IF NOT EXISTS idx_gpu_reservations_node_status ON gpu_reservations(node_id, status); CREATE UNIQUE INDEX IF NOT EXISTS uq_gpu_reservations_active ON gpu_reservations(node_id, gpu_index) WHERE status = 'reserved'; CREATE INDEX IF NOT EXISTS idx_scheduler_locks_expires ON scheduler_locks(expires_at); CREATE INDEX IF NOT EXISTS idx_dataset_files_dataset ON dataset_files(dataset_id); CREATE INDEX IF NOT EXISTS idx_gpus_node ON gpus(node_id); CREATE UNIQUE INDEX IF NOT EXISTS uq_gpus_node_index ON gpus(node_id, gpu_index); CREATE INDEX IF NOT EXISTS idx_replicas_resource ON resource_replicas(resource_type, resource_id); CREATE UNIQUE INDEX IF NOT EXISTS uq_replicas_node_resource ON resource_replicas(node_id, resource_type, resource_id); CREATE INDEX IF NOT EXISTS idx_sync_jobs_node_status ON resource_sync_jobs(target_node_id, status); CREATE INDEX IF NOT EXISTS idx_eval_tasks_status ON eval_tasks(status); CREATE INDEX IF NOT EXISTS idx_eval_dimensions_active ON eval_dimensions(is_active); CREATE INDEX IF NOT EXISTS idx_compare_tasks_status ON compare_tasks(status); -- ---- 项目 / 租户 ---- CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, code TEXT NOT NULL, description TEXT, quota TEXT, status TEXT NOT NULL DEFAULT 'active', create_time TEXT NOT NULL, create_by TEXT, updated_at TEXT ); ALTER TABLE projects ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE projects ADD COLUMN IF NOT EXISTS deleted_by TEXT; CREATE INDEX IF NOT EXISTS idx_projects_active_tenant ON projects(tenant_id, status, deleted_at); CREATE TABLE IF NOT EXISTS project_members ( project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'member', create_time TEXT NOT NULL, PRIMARY KEY (project_id, user_id) ); CREATE TABLE IF NOT EXISTS roles ( id TEXT PRIMARY KEY, name TEXT NOT NULL, permissions TEXT NOT NULL DEFAULT '[]', create_time TEXT ); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, username TEXT, login_at TEXT, logout_at TEXT, duration_seconds INTEGER, issued_at TEXT, expires_at TEXT, ip TEXT, create_time TEXT ); CREATE TABLE IF NOT EXISTS acls ( id TEXT PRIMARY KEY, resource_type TEXT NOT NULL, resource_id TEXT NOT NULL, principal_type TEXT NOT NULL, principal_id TEXT NOT NULL, permission TEXT NOT NULL, create_time TEXT ); ALTER TABLE models ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE models ADD COLUMN IF NOT EXISTS deleted_by TEXT; ALTER TABLE models ADD COLUMN IF NOT EXISTS storage_status TEXT NOT NULL DEFAULT 'pending'; ALTER TABLE models ADD COLUMN IF NOT EXISTS storage_error TEXT; ALTER TABLE models ADD COLUMN IF NOT EXISTS storage_version_id TEXT; UPDATE models SET storage_status = CASE WHEN model_source IN ('api', 'online') THEN 'not_applicable' WHEN storage_status IS NULL OR storage_status = '' THEN 'pending' ELSE storage_status END WHERE storage_status IS NULL OR storage_status = '' OR model_source IN ('api', 'online'); ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS deleted_by TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS deleted_by TEXT; ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS deleted_by TEXT; CREATE UNIQUE INDEX IF NOT EXISTS uq_acls_subject_permission ON acls(resource_type, resource_id, principal_type, principal_id, permission); CREATE INDEX IF NOT EXISTS idx_acls_resource ON acls(resource_type, resource_id); CREATE INDEX IF NOT EXISTS idx_acls_principal ON acls(principal_type, principal_id); -- ---- 权限扩展(来源:004_permissions.sql) ---- CREATE TABLE IF NOT EXISTS gpu_assignments ( id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE, gpu_index INTEGER NOT NULL, user_id TEXT NOT NULL, assigned_by TEXT, assigned_at TEXT NOT NULL, UNIQUE (node_id, gpu_index, user_id) ); CREATE INDEX IF NOT EXISTS idx_gpu_assignments_user ON gpu_assignments(user_id); CREATE INDEX IF NOT EXISTS idx_gpu_assignments_gpu ON gpu_assignments(node_id, gpu_index); ALTER TABLE datasets ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE models ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default'; -- ============================================================================ -- 二、治理表(来源:002_governance.sql) -- ============================================================================ CREATE TABLE IF NOT EXISTS tenants ( id TEXT PRIMARY KEY, name TEXT NOT NULL, code TEXT, status TEXT DEFAULT 'active', owner_user_id TEXT, quota TEXT, retention_policy_id TEXT, create_time TEXT ); ALTER TABLE tenants ADD COLUMN IF NOT EXISTS deleted_at TEXT; ALTER TABLE tenants ADD COLUMN IF NOT EXISTS deleted_by TEXT; CREATE INDEX IF NOT EXISTS idx_tenants_active ON tenants(status, deleted_at); INSERT INTO tenants (id, name, code, status, quota, create_time) VALUES ('default', '默认租户', 'default', 'active', '{}', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) ON CONFLICT (id) DO NOTHING; INSERT INTO tenants (id, name, code, status, quota, create_time) VALUES ('admin', '管理员租户', 'admin', 'active', '{}', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) ON CONFLICT (id) DO UPDATE SET name='管理员租户', code='admin', status='active', deleted_at=NULL, deleted_by=NULL; CREATE TABLE IF NOT EXISTS approval_templates ( id TEXT PRIMARY KEY, name TEXT NOT NULL, steps TEXT, create_time TEXT ); CREATE TABLE IF NOT EXISTS approval_instances ( id TEXT PRIMARY KEY, template_id TEXT, resource_type TEXT, resource_id TEXT, applicant_id TEXT, status TEXT DEFAULT 'pending', current_step INTEGER DEFAULT 0, create_time TEXT ); CREATE TABLE IF NOT EXISTS approval_steps ( id TEXT PRIMARY KEY, instance_id TEXT, step_index INTEGER, approver_id TEXT, status TEXT DEFAULT 'pending', comment TEXT, time TEXT ); CREATE INDEX IF NOT EXISTS idx_approval_instances_applicant ON approval_instances(applicant_id, status); CREATE INDEX IF NOT EXISTS idx_approval_instances_resource ON approval_instances(resource_type, resource_id, status); CREATE INDEX IF NOT EXISTS idx_approval_steps_approver ON approval_steps(approver_id, status); -- Compatibility for databases created from an older approval_steps definition. ALTER TABLE approval_steps ADD COLUMN IF NOT EXISTS id TEXT; UPDATE approval_steps SET id = 'astep_' || md5(concat_ws(':', instance_id, step_index, coalesce(approver_id, ''), coalesce(time, ''))) WHERE id IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_approval_steps_id ON approval_steps(id); CREATE TABLE IF NOT EXISTS audit_logs ( id TEXT PRIMARY KEY, tenant_id TEXT, project_id TEXT, actor_id TEXT, action TEXT, target_type TEXT, target_id TEXT, detail TEXT, client_ip TEXT, time TEXT ); CREATE INDEX IF NOT EXISTS idx_audit_tenant ON audit_logs(tenant_id); CREATE INDEX IF NOT EXISTS idx_audit_project ON audit_logs(project_id); CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_time ON audit_logs(time); -- ---- 操作日志(接口操作审计) ---- CREATE TABLE IF NOT EXISTS operation_logs ( id TEXT PRIMARY KEY, user_id TEXT, username TEXT, module TEXT, action TEXT, target_type TEXT, target_id TEXT, target_name TEXT, status TEXT NOT NULL, error_message TEXT, error_type TEXT, error_traceback TEXT, func_name TEXT, detail TEXT, client_ip TEXT, request_method TEXT, request_path TEXT, trace_id TEXT, duration_ms REAL, create_time TEXT ); CREATE INDEX IF NOT EXISTS idx_operation_logs_user_time ON operation_logs(user_id, create_time DESC); CREATE INDEX IF NOT EXISTS idx_operation_logs_module_time ON operation_logs(module, create_time DESC); CREATE INDEX IF NOT EXISTS idx_operation_logs_status ON operation_logs(status, create_time DESC); CREATE INDEX IF NOT EXISTS idx_operation_logs_create_time ON operation_logs(create_time DESC); CREATE TABLE IF NOT EXISTS retention_policies ( id TEXT PRIMARY KEY, name TEXT NOT NULL, scope TEXT, rule TEXT, status TEXT DEFAULT 'active', create_time TEXT, create_by TEXT, updated_at TEXT ); -- Compatibility for the earlier retention policy schema -- (resource_type/retention_days). Keep legacy columns if they exist. ALTER TABLE retention_policies ADD COLUMN IF NOT EXISTS scope TEXT; ALTER TABLE retention_policies ADD COLUMN IF NOT EXISTS rule TEXT; ALTER TABLE retention_policies ADD COLUMN IF NOT EXISTS status TEXT DEFAULT 'active'; ALTER TABLE retention_policies ADD COLUMN IF NOT EXISTS create_by TEXT; ALTER TABLE retention_policies ADD COLUMN IF NOT EXISTS updated_at TEXT; DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='retention_policies' AND column_name='resource_type' ) AND EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='retention_policies' AND column_name='retention_days' ) THEN EXECUTE $migration$ UPDATE retention_policies SET scope = COALESCE(scope, resource_type), rule = COALESCE(rule, retention_days::text), status = COALESCE(status, 'active'), updated_at = COALESCE(updated_at, create_time) WHERE scope IS NULL OR rule IS NULL OR status IS NULL OR updated_at IS NULL $migration$; ELSE UPDATE retention_policies SET status = COALESCE(status, 'active'), updated_at = COALESCE(updated_at, create_time) WHERE status IS NULL OR updated_at IS NULL; END IF; END $$; -- ============================================================================ -- 三、租户配额扩展(来源:003_tenant_quota.sql) -- ============================================================================ ALTER TABLE tenants ADD COLUMN IF NOT EXISTS gpu_quota TEXT; ALTER TABLE tenants ADD COLUMN IF NOT EXISTS storage_quota TEXT; -- ============================================================================ -- 四、模型路径治理(来源:003_model_path_governance.sql) -- models.can_train 已在建表语句中声明;以下为兼容旧库的幂等语句。 -- ============================================================================ ALTER TABLE models ADD COLUMN IF NOT EXISTS can_train INTEGER NOT NULL DEFAULT 0; ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS artifact_dir TEXT; ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS compute_node_id TEXT; ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS compute_node_name TEXT; -- 按规则推定已有模型的 can_train(新库为空表,此语句为 no-op) UPDATE models SET can_train = CASE WHEN path IS NOT NULL AND path != '' AND model_source IS NOT NULL AND model_source != 'api' THEN 1 ELSE 0 END; -- ============================================================================ -- 五、数据处理(来源:002_data_process.sql,去掉其外层 BEGIN/COMMIT) -- 数据集扩展列 -- ============================================================================ ALTER TABLE datasets ADD COLUMN IF NOT EXISTS source_task_id TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS size_bytes BIGINT NOT NULL DEFAULT 0; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS record_count BIGINT NOT NULL DEFAULT 0; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS metadata TEXT NOT NULL DEFAULT '{}'; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS project_id TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS owner_id TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE datasets ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT now(); ALTER TABLE datasets ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now(); ALTER TABLE datasets ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS storage_object_id TEXT; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS current_version_id TEXT; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS size_bytes BIGINT NOT NULL DEFAULT 0; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS record_count BIGINT NOT NULL DEFAULT 0; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS file_format VARCHAR(40); ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS checksum_sha256 CHAR(64); ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS version_no INTEGER NOT NULL DEFAULT 1; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS source_task_id TEXT; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS project_id TEXT; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS metadata TEXT NOT NULL DEFAULT '{}'; ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT now(); ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now(); ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; ALTER TABLE model_artifacts ADD COLUMN IF NOT EXISTS storage_object_id TEXT; ALTER TABLE model_artifacts ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'minio'; ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS metadata TEXT NOT NULL DEFAULT '{}'; -- ---- 数据处理任务 / 源文件 / 预览 / 结果 ---- CREATE TABLE IF NOT EXISTS data_process_tasks ( id TEXT PRIMARY KEY, name VARCHAR(150) NOT NULL, description TEXT, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'stopped')), process_type VARCHAR(20) NOT NULL CHECK (process_type IN ('structured', 'unstructured', 'external')), source_dataset_id TEXT REFERENCES datasets(id) ON DELETE SET NULL, output_dataset_id TEXT REFERENCES datasets(id) ON DELETE SET NULL, config TEXT NOT NULL DEFAULT '{}', progress NUMERIC(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), input_count BIGINT NOT NULL DEFAULT 0 CHECK (input_count >= 0), output_count BIGINT NOT NULL DEFAULT 0 CHECK (output_count >= 0), filtered_count BIGINT NOT NULL DEFAULT 0 CHECK (filtered_count >= 0), duplicate_count BIGINT NOT NULL DEFAULT 0 CHECK (duplicate_count >= 0), error_count BIGINT NOT NULL DEFAULT 0 CHECK (error_count >= 0), failure_reason TEXT, generation_run_id TEXT, results_confirmed BOOLEAN NOT NULL DEFAULT TRUE, workflow_step VARCHAR(20) NOT NULL DEFAULT 'create' CHECK (workflow_step IN ('create', 'model', 'upload', 'preview', 'generate', 'results')), preview_status VARCHAR(20) NOT NULL DEFAULT 'idle' CHECK (preview_status IN ('idle', 'queued', 'running', 'completed', 'failed', 'cancelled')), preview_progress NUMERIC(5,2) NOT NULL DEFAULT 0 CHECK (preview_progress >= 0 AND preview_progress <= 100), preview_run_id TEXT, preview_failure_reason TEXT, preview_total_files INTEGER NOT NULL DEFAULT 0 CHECK (preview_total_files >= 0), preview_completed_files INTEGER NOT NULL DEFAULT 0 CHECK (preview_completed_files >= 0), tenant_id TEXT, project_id TEXT, owner_id TEXT, approval_status VARCHAR(30) NOT NULL DEFAULT 'not_required', created_by TEXT, updated_by TEXT, deleted_by TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS generation_run_id TEXT; ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS results_confirmed BOOLEAN NOT NULL DEFAULT TRUE; ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS workflow_step VARCHAR(20); ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS preview_status VARCHAR(20); ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS preview_progress NUMERIC(5,2); ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS preview_run_id TEXT; ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS preview_failure_reason TEXT; ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS preview_total_files INTEGER; ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS preview_completed_files INTEGER; ALTER TABLE data_process_tasks ALTER COLUMN workflow_step SET DEFAULT 'create'; ALTER TABLE data_process_tasks ALTER COLUMN workflow_step SET NOT NULL; ALTER TABLE data_process_tasks ALTER COLUMN preview_status SET DEFAULT 'idle'; ALTER TABLE data_process_tasks ALTER COLUMN preview_status SET NOT NULL; ALTER TABLE data_process_tasks ALTER COLUMN preview_progress SET DEFAULT 0; ALTER TABLE data_process_tasks ALTER COLUMN preview_progress SET NOT NULL; ALTER TABLE data_process_tasks ALTER COLUMN preview_total_files SET DEFAULT 0; ALTER TABLE data_process_tasks ALTER COLUMN preview_total_files SET NOT NULL; ALTER TABLE data_process_tasks ALTER COLUMN preview_completed_files SET DEFAULT 0; ALTER TABLE data_process_tasks ALTER COLUMN preview_completed_files SET NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS uq_data_process_tasks_name_alive ON data_process_tasks(name) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_data_process_tasks_scope_status ON data_process_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_data_process_tasks_creator_created ON data_process_tasks(created_by, created_at DESC) WHERE deleted_at IS NULL; CREATE TABLE IF NOT EXISTS data_process_source_files ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE, storage_object_id TEXT, name TEXT NOT NULL, size_bytes BIGINT NOT NULL DEFAULT 0 CHECK (size_bytes >= 0), record_count BIGINT NOT NULL DEFAULT 0 CHECK (record_count >= 0), file_format VARCHAR(40), checksum_sha256 CHAR(64) NOT NULL, version_no INTEGER NOT NULL DEFAULT 1 CHECK (version_no > 0), content TEXT NOT NULL, content_preview TEXT, metadata TEXT NOT NULL DEFAULT '{}', tenant_id TEXT, project_id TEXT, created_by TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_data_process_source_files_task ON data_process_source_files(task_id, created_at) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS uq_data_process_source_checksum_alive ON data_process_source_files(task_id, checksum_sha256) WHERE deleted_at IS NULL; CREATE TABLE IF NOT EXISTS data_process_preview_items ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE, source_file_id TEXT REFERENCES data_process_source_files(id) ON DELETE CASCADE, original_content TEXT NOT NULL DEFAULT '', edited_content TEXT NOT NULL DEFAULT '', source_start INTEGER CHECK (source_start IS NULL OR source_start >= 0), source_end INTEGER CHECK (source_end IS NULL OR source_end >= 0), source_start_line INTEGER CHECK (source_start_line IS NULL OR source_start_line > 0), source_end_line INTEGER CHECK (source_end_line IS NULL OR source_end_line > 0), token_count INTEGER NOT NULL DEFAULT 0 CHECK (token_count >= 0), status VARCHAR(20) NOT NULL DEFAULT 'original' CHECK (status IN ('original', 'modified', 'manual', 'invalid')), quality_score TEXT NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (source_start IS NULL OR source_end IS NULL OR source_end >= source_start), CHECK (source_start_line IS NULL OR source_end_line IS NULL OR source_end_line >= source_start_line) ); CREATE INDEX IF NOT EXISTS idx_data_process_preview_task_file ON data_process_preview_items(task_id, source_file_id, created_at); CREATE TABLE IF NOT EXISTS data_process_results ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE, preview_item_id TEXT REFERENCES data_process_preview_items(id) ON DELETE SET NULL, instruction TEXT NOT NULL, input TEXT NOT NULL DEFAULT '', output TEXT NOT NULL, chosen TEXT NOT NULL DEFAULT '', rejected TEXT NOT NULL DEFAULT '', original_instruction TEXT, original_input TEXT, original_output TEXT, original_chosen TEXT, original_rejected TEXT, status VARCHAR(20) NOT NULL DEFAULT 'valid' CHECK (status IN ('valid', 'modified', 'invalid')), error TEXT, split VARCHAR(20) CHECK (split IS NULL OR split IN ('train', 'validation', 'test')), quality_score TEXT NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Keep existing databases compatible with the current generation result model. ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS chosen TEXT NOT NULL DEFAULT ''; ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS rejected TEXT NOT NULL DEFAULT ''; ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS original_chosen TEXT; ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS original_rejected TEXT; CREATE INDEX IF NOT EXISTS idx_data_process_results_task_status ON data_process_results(task_id, status, id); CREATE INDEX IF NOT EXISTS idx_data_process_results_task_split ON data_process_results(task_id, split); -- ---- 数据集版本 / 记录 ---- CREATE TABLE IF NOT EXISTS dataset_file_versions ( id TEXT PRIMARY KEY, dataset_file_id TEXT NOT NULL REFERENCES dataset_files(id) ON DELETE CASCADE, version_no INTEGER NOT NULL CHECK (version_no > 0), storage_object_id TEXT NOT NULL, content_preview TEXT, description TEXT, base_version_id TEXT REFERENCES dataset_file_versions(id) ON DELETE SET NULL, size_bytes BIGINT NOT NULL DEFAULT 0 CHECK (size_bytes >= 0), record_count BIGINT NOT NULL DEFAULT 0 CHECK (record_count >= 0), checksum_sha256 CHAR(64) NOT NULL, source_task_id TEXT REFERENCES data_process_tasks(id) ON DELETE SET NULL, metadata TEXT NOT NULL DEFAULT '{}', created_by TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE dataset_file_versions ADD COLUMN IF NOT EXISTS source_task_id TEXT; ALTER TABLE dataset_file_versions ADD COLUMN IF NOT EXISTS metadata TEXT NOT NULL DEFAULT '{}'; CREATE UNIQUE INDEX IF NOT EXISTS uq_dataset_file_versions_no_002 ON dataset_file_versions(dataset_file_id, version_no); CREATE INDEX IF NOT EXISTS idx_dataset_file_versions_source_task_002 ON dataset_file_versions(source_task_id) WHERE source_task_id IS NOT NULL; CREATE TABLE IF NOT EXISTS dataset_records ( id TEXT PRIMARY KEY, dataset_id TEXT NOT NULL REFERENCES datasets(id) ON DELETE CASCADE, dataset_file_id TEXT REFERENCES dataset_files(id) ON DELETE CASCADE, version_id TEXT REFERENCES dataset_file_versions(id) ON DELETE CASCADE, line_no INTEGER, split VARCHAR(20) CHECK (split IS NULL OR split IN ('train', 'validation', 'test')), instruction TEXT, input TEXT, output TEXT, raw TEXT NOT NULL DEFAULT '{}', status VARCHAR(20) NOT NULL DEFAULT 'valid' CHECK (status IN ('valid', 'modified', 'invalid')), source_task_id TEXT REFERENCES data_process_tasks(id) ON DELETE SET NULL, source_result_id TEXT REFERENCES data_process_results(id) ON DELETE SET NULL, preview_item_id TEXT REFERENCES data_process_preview_items(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE dataset_records ADD COLUMN IF NOT EXISTS source_task_id TEXT; ALTER TABLE dataset_records ADD COLUMN IF NOT EXISTS source_result_id TEXT; ALTER TABLE dataset_records ADD COLUMN IF NOT EXISTS preview_item_id TEXT; CREATE INDEX IF NOT EXISTS idx_dataset_records_dataset_002 ON dataset_records(dataset_id, id); CREATE INDEX IF NOT EXISTS idx_dataset_records_source_task_002 ON dataset_records(source_task_id, source_result_id); CREATE INDEX IF NOT EXISTS idx_datasets_source_task_002 ON datasets(source_task_id) WHERE source_task_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_dataset_files_source_task_002 ON dataset_files(source_task_id) WHERE source_task_id IS NOT NULL; -- ============================================================================ -- 六、数据转换任务(data_convert_tasks) -- 运行时 router(app/modules/data_convert/router.py)引用但原脚本缺失,本文件补齐。 -- ============================================================================ CREATE TABLE IF NOT EXISTS data_convert_tasks ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, output_filename TEXT DEFAULT 'converted-data.jsonl', status TEXT NOT NULL DEFAULT 'pending', input_count INTEGER NOT NULL DEFAULT 0, output_count INTEGER NOT NULL DEFAULT 0, error_message TEXT, output_content TEXT, create_time TEXT NOT NULL DEFAULT (to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), update_time TEXT NOT NULL DEFAULT (to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), deleted_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_data_convert_tasks_status ON data_convert_tasks(status); CREATE INDEX IF NOT EXISTS idx_data_convert_tasks_create_time ON data_convert_tasks(create_time DESC); ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS storage_backend TEXT NOT NULL DEFAULT 'minio'; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS output_storage_object_id TEXT; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS output_content TEXT; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS processed_by TEXT; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS processed_at TEXT; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS deleted_by TEXT; ALTER TABLE data_convert_tasks ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default'; CREATE INDEX IF NOT EXISTS idx_data_convert_tasks_tenant ON data_convert_tasks(tenant_id, deleted_at); ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS report_storage_object_id TEXT; ALTER TABLE resource_replicas ADD COLUMN IF NOT EXISTS version_id TEXT; ALTER TABLE resource_replicas ADD COLUMN IF NOT EXISTS storage_object_id TEXT; CREATE INDEX IF NOT EXISTS idx_resource_replicas_object_002 ON resource_replicas(storage_object_id, node_id); -- ============================================================================ -- 六、权限 2.0:租户成员、资源申请、ACL 生命周期和审批动作 -- ============================================================================ CREATE TABLE IF NOT EXISTS tenant_members ( tenant_id TEXT NOT NULL, user_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member', status TEXT NOT NULL DEFAULT 'active', invited_by TEXT, joined_at TEXT, expires_at TEXT, PRIMARY KEY (tenant_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_tenant_members_user ON tenant_members(user_id, status); CREATE INDEX IF NOT EXISTS idx_tenant_members_tenant ON tenant_members(tenant_id, status); INSERT INTO tenant_members (tenant_id, user_id, role, status, joined_at) SELECT COALESCE(u.tenant_id, 'default'), u.id, CASE WHEN u.role='admin' OR COALESCE(u.protected, 0)=1 THEN 'owner' ELSE 'member' END, 'active', COALESCE(u.create_time, NOW()::text) FROM users u ON CONFLICT (tenant_id, user_id) DO NOTHING; -- GPU/存储任务使用的租户配额原子预留账本。任务提交时预留,终态或故障回收。 CREATE TABLE IF NOT EXISTS tenant_quota_reservations ( id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, owner_type TEXT NOT NULL, owner_id TEXT NOT NULL, gpu_count INTEGER NOT NULL DEFAULT 0, storage_bytes BIGINT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'reserved', create_time TEXT NOT NULL, released_at TEXT ); CREATE INDEX IF NOT EXISTS idx_tenant_quota_reservations_tenant ON tenant_quota_reservations(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_tenant_quota_reservations_owner ON tenant_quota_reservations(owner_type, owner_id, status); CREATE UNIQUE INDEX IF NOT EXISTS uq_tenant_quota_reservations_active_owner ON tenant_quota_reservations(owner_type, owner_id) WHERE status = 'reserved'; CREATE TABLE IF NOT EXISTS resource_access_requests ( id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, resource_type TEXT NOT NULL, resource_id TEXT NOT NULL, applicant_id TEXT NOT NULL, principal_type TEXT NOT NULL DEFAULT 'user', principal_id TEXT NOT NULL, requested_permissions TEXT NOT NULL DEFAULT '[]', reason TEXT, approval_id TEXT, status TEXT NOT NULL DEFAULT 'pending', expires_at TEXT, created_at TEXT NOT NULL, decided_at TEXT, decided_by TEXT ); CREATE INDEX IF NOT EXISTS idx_resource_access_requests_resource ON resource_access_requests(resource_type, resource_id, status); CREATE INDEX IF NOT EXISTS idx_resource_access_requests_applicant ON resource_access_requests(applicant_id, status); ALTER TABLE acls ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE acls ADD COLUMN IF NOT EXISTS granted_by TEXT; ALTER TABLE acls ADD COLUMN IF NOT EXISTS source_request_id TEXT; ALTER TABLE acls ADD COLUMN IF NOT EXISTS expires_at TEXT; ALTER TABLE acls ADD COLUMN IF NOT EXISTS revoked_at TEXT; CREATE INDEX IF NOT EXISTS idx_acls_tenant_active ON acls(tenant_id, revoked_at, expires_at); ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS action TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS requested_permissions TEXT NOT NULL DEFAULT '[]'; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS reason TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS expires_at TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS decided_by TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS decided_at TEXT; ALTER TABLE approval_steps ADD COLUMN IF NOT EXISTS approver_type TEXT NOT NULL DEFAULT 'user'; -- ============================================================================ -- 七、权限 2.0 完整闭环:审批策略、执行状态、结构化审计与历史归属 -- ============================================================================ ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS tenant_id TEXT; ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS action TEXT; ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS resource_type TEXT; ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS scope TEXT NOT NULL DEFAULT 'tenant'; ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'active'; ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS created_by TEXT; ALTER TABLE approval_templates ADD COLUMN IF NOT EXISTS updated_at TEXT; CREATE INDEX IF NOT EXISTS idx_approval_templates_match ON approval_templates(tenant_id, action, resource_type, status); ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS execution_status TEXT NOT NULL DEFAULT 'pending'; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS execution_error TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS executed_at TEXT; ALTER TABLE approval_instances ADD COLUMN IF NOT EXISTS executed_by TEXT; CREATE INDEX IF NOT EXISTS idx_approval_instances_execution ON approval_instances(status, execution_status, action, resource_type, resource_id); ALTER TABLE resource_access_requests ADD COLUMN IF NOT EXISTS cancelled_at TEXT; ALTER TABLE resource_access_requests ADD COLUMN IF NOT EXISTS cancelled_by TEXT; CREATE INDEX IF NOT EXISTS idx_resource_access_requests_expiry ON resource_access_requests(status, expires_at); ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS result TEXT NOT NULL DEFAULT 'success'; ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS reason TEXT; ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS request_id TEXT; ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS session_id TEXT; ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS metadata TEXT; CREATE INDEX IF NOT EXISTS idx_audit_request ON audit_logs(request_id); CREATE INDEX IF NOT EXISTS idx_audit_result ON audit_logs(result, time); UPDATE datasets d SET tenant_id = COALESCE(u.tenant_id, 'default') FROM users u WHERE d.created_by = u.id AND (d.tenant_id IS NULL OR d.tenant_id = ''); UPDATE models m SET tenant_id = COALESCE(u.tenant_id, 'default') FROM users u WHERE m.created_by = u.id AND (m.tenant_id IS NULL OR m.tenant_id = ''); UPDATE trained_models m SET tenant_id = COALESCE(u.tenant_id, 'default') FROM users u WHERE m.created_by = u.id AND (m.tenant_id IS NULL OR m.tenant_id = ''); UPDATE eval_tasks e SET tenant_id = COALESCE(u.tenant_id, 'default') FROM users u WHERE e.created_by = u.id AND (e.tenant_id IS NULL OR e.tenant_id = ''); -- ============================================================================ -- 八、种子数据:初始管理员 / 操作员 -- 应用首次启动(ensure_seed_data)也会自动创建;此处提供以便脱离应用直接初始化。 -- 密码:admin / admin123,operator / operator123(上线前请改密)。 -- ============================================================================ INSERT INTO users (id, username, password_hash, display_name, role, platform_role, status, permissions, create_time, protected, tenant_id) VALUES ( 'u_admin', 'admin', 'pbkdf2_sha256$390000$ygft_init_salt_admin$2b6f31f22968c4f5a30bcf0acf066b7a0f58d4773d15c5ab898ba715ea87b5bd', 'Admin', 'admin', 'platform_admin', 'active', '["dashboard","fine-tune","model-eval","model-inference","model-manage","dataset","data-process","data-convert","compute","hardware","logs","user-settings"]', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), 1, 'admin' ), ( 'u_operator', 'operator', 'pbkdf2_sha256$390000$ygft_init_salt_op$525bf35d02ed26f37952cbd6862b0ae358b9d1a7fa0cbbf0217aa2b5dd544125', 'Platform Operator', 'operator', 'platform_user', 'active', '["dashboard","fine-tune","model-eval","model-inference","model-manage","dataset","data-process","data-convert","hardware","logs"]', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), 0, 'default' ) ON CONFLICT (username) DO NOTHING; UPDATE users SET display_name='Admin', tenant_id='admin' WHERE username='admin'; INSERT INTO tenant_members (tenant_id, user_id, role, status, joined_at) VALUES ('admin', 'u_admin', 'owner', 'active', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), ('default', 'u_operator', 'member', 'active', to_char(now(), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) ON CONFLICT (tenant_id, user_id) DO NOTHING; UPDATE tenant_members SET status='disabled' WHERE tenant_id='default' AND user_id='u_admin'; -- Canonical hierarchy fields for new and historical rows. Project columns are -- intentionally retained only as compatibility data and are not used here. UPDATE users SET platform_role = CASE WHEN role = 'admin' OR COALESCE(protected, 0) = 1 THEN 'platform_admin' ELSE 'platform_user' END WHERE platform_role IS NULL OR platform_role NOT IN ('platform_admin', 'platform_user'); INSERT INTO tenant_members (tenant_id, user_id, role, status, joined_at) SELECT t.id, t.owner_user_id, 'owner', 'active', COALESCE(t.create_time, NOW()::text) FROM tenants t JOIN users u ON u.id = t.owner_user_id WHERE t.owner_user_id IS NOT NULL AND COALESCE(t.status, 'active') = 'active' ON CONFLICT (tenant_id, user_id) DO UPDATE SET role='owner', status='active'; UPDATE tenant_members tm SET role='member' FROM users u WHERE tm.user_id=u.id AND tm.role='owner' AND (u.role <> 'admin' AND COALESCE(u.protected, 0)=0) AND NOT EXISTS (SELECT 1 FROM tenants t WHERE t.id=tm.tenant_id AND t.owner_user_id=tm.user_id); CREATE INDEX IF NOT EXISTS idx_datasets_tenant_active ON datasets(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_models_tenant_active ON models(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_trained_models_tenant_active ON trained_models(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_eval_tasks_tenant_active ON eval_tasks(tenant_id, deleted_at, create_time DESC); CREATE INDEX IF NOT EXISTS idx_compare_tasks_tenant_active ON compare_tasks(tenant_id, deleted_at, create_time DESC); COMMIT;