import pg from 'pg'; const { Pool } = pg; const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres'; const tenantId = process.env.TENANT_ID || '00000000-0000-0000-0000-000000000001'; const ids = { user: '00000000-0000-0000-0000-000000000101', tenantAdminUser: '00000000-0000-0000-0000-000000000102', tenantOperatorUser: '00000000-0000-0000-0000-000000000103', tenantSalesUser: '00000000-0000-0000-0000-000000000104', tenantAgentUser: '00000000-0000-0000-0000-000000000105', region: '00000000-0000-0000-0000-000000000301', subject: '00000000-0000-0000-0000-000000000501', category: '00000000-0000-0000-0000-000000000601', questionBank: '00000000-0000-0000-0000-000000000400', question: '00000000-0000-0000-0000-000000000401', questionVersion: '00000000-0000-0000-0000-000000000402', plan: '00000000-0000-0000-0000-000000000201', order: '00000000-0000-0000-0000-000000000701', payment: '00000000-0000-0000-0000-000000000702', activationCode: '00000000-0000-0000-0000-000000000801', vocabularyUnit: '00000000-0000-0000-0000-000000000811', vocabularyWord: '00000000-0000-0000-0000-000000000812', video: '00000000-0000-0000-0000-000000000821', questionVideo: '00000000-0000-0000-0000-000000000822', scorelineSchool: '00000000-0000-0000-0000-000000000831', scorelineMajor: '00000000-0000-0000-0000-000000000832', scorelineField: '00000000-0000-0000-0000-000000000833', scorelineRecord: '00000000-0000-0000-0000-000000000834', recentPractice: '00000000-0000-0000-0000-000000000841', partnerTenant: '00000000-0000-0000-0000-000000000901', partnerSubscription: '00000000-0000-0000-0000-000000000902', partnerInvoice: '00000000-0000-0000-0000-000000000903', partnerInvoiceItem: '00000000-0000-0000-0000-000000000904', partnerInvoicePayment: '00000000-0000-0000-0000-000000000905', }; const pool = new Pool({ connectionString: databaseUrl }); async function main() { const client = await pool.connect(); try { await client.query('begin'); await client.query( ` delete from public.crm_webhook_queue where tenant_id = $1 and ( record_id in ($2::text, $3::text, $4::text, $5::text) or lead_id in ( select id::text from public.referral_leads where tenant_id = $1 and student_user_id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid) ) ) `, [tenantId, ids.user, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` delete from public.referral_qrcodes where tenant_id = $1 and user_id in ($2::uuid, $3::uuid, $4::uuid) `, [tenantId, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` delete from public.referral_tracks where tenant_id = $1 and ( target_user_id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid) or ref_user_id in ($3::uuid, $4::uuid, $5::uuid) ) `, [tenantId, ids.user, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` delete from public.referral_leads where tenant_id = $1 and student_user_id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid) `, [tenantId, ids.user, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` delete from public.referral_team_edges where tenant_id = $1 and ( member_user_id in ($2::uuid, $3::uuid, $4::uuid) or leader_user_id in ($2::uuid, $3::uuid, $4::uuid) ) `, [tenantId, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` insert into public.platform_users (id, username, phone, name, primary_role, raw_profile) values ($1, 'smoke_student', '13800000000', 'Smoke Student', 'student', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, phone = excluded.phone, name = excluded.name, updated_at = now() `, [ids.user], ); await client.query( ` insert into public.platform_users (id, username, phone, name, primary_role, raw_profile) values ($1, 'smoke_tenant_admin', '13800000001', 'Smoke Tenant Admin', 'tenant_admin', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, phone = excluded.phone, name = excluded.name, primary_role = excluded.primary_role, updated_at = now() `, [ids.tenantAdminUser], ); await client.query( ` insert into public.platform_users (id, username, phone, name, primary_role, raw_profile) values ($1, 'smoke_tenant_operator', '13800000003', 'Smoke Tenant Operator', 'tenant_operator', '{"source":"smoke-seed"}'::jsonb), ($2, 'smoke_tenant_sales', '13800000004', 'Smoke Tenant Sales', 'sales', '{"source":"smoke-seed"}'::jsonb), ($3, 'smoke_tenant_agent', '13800000005', 'Smoke Tenant Agent', 'agent', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, phone = excluded.phone, name = excluded.name, primary_role = excluded.primary_role, updated_at = now() `, [ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` insert into public.user_identities (user_id, provider, provider_subject, phone) values ($1, 'phone', '13800000000', '13800000000') on conflict (provider, provider_subject) do update set user_id = excluded.user_id, phone = excluded.phone, updated_at = now() `, [ids.user], ); await client.query( ` insert into public.tenant_memberships (tenant_id, user_id, role, status) values ($1, $2, 'student', 'active') on conflict (tenant_id, user_id, role) do update set status = 'active', updated_at = now() `, [tenantId, ids.user], ); await client.query( ` insert into public.tenant_memberships (tenant_id, user_id, role, status, permissions) values ($1, $2, 'tenant_admin', 'active', '{"content:*":true}'::jsonb) on conflict (tenant_id, user_id, role) do update set status = 'active', permissions = excluded.permissions, updated_at = now() `, [tenantId, ids.tenantAdminUser], ); await client.query( ` insert into public.student_profiles (tenant_id, user_id, stats, progress) values ($1, $2, '{"totalAnswered":0,"correctCount":0,"wrongCount":0,"studyDays":1}'::jsonb, '{}'::jsonb) on conflict (tenant_id, user_id) do nothing `, [tenantId, ids.user], ); await client.query( ` insert into public.regions (id, tenant_id, legacy_id, name, code, sort_order, is_active) values ($1, $2, 'smoke-region', '烟测地区', 'SMOKE', 1, true) on conflict (id) do update set name = excluded.name, code = excluded.code, updated_at = now() `, [ids.region, tenantId], ); await client.query( ` insert into public.subjects (id, tenant_id, region_id, legacy_id, name, type, sort_order, is_active) values ($1, $2, $3, 'smoke-subject', '烟测科目', 'cultural', 1, true) on conflict (id) do update set name = excluded.name, region_id = excluded.region_id, updated_at = now() `, [ids.subject, tenantId, ids.region], ); await client.query( ` insert into public.categories (id, tenant_id, subject_id, legacy_id, name, category_type, sort_order, is_active) values ($1, $2, $3, 'smoke-category', '烟测章节', 'chapter', 1, true) on conflict (id) do update set name = excluded.name, subject_id = excluded.subject_id, updated_at = now() `, [ids.category, tenantId, ids.subject], ); await client.query( ` insert into public.question_banks (id, tenant_id, region_id, name, source_scope, status, metadata) values ($1, $2, $3, '烟测题库', 'tenant', 'active', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set name = excluded.name, region_id = excluded.region_id, updated_at = now() `, [ids.questionBank, tenantId, ids.region], ); await client.query( ` insert into public.questions ( id, tenant_id, question_bank_id, subject_id, category_id, legacy_id, type, type_label, difficulty, status ) values ($1, $2, $3, $4, $5, 'smoke-question', 'choice', '单选题', 1, 'published') on conflict (id) do update set question_bank_id = excluded.question_bank_id, subject_id = excluded.subject_id, category_id = excluded.category_id, updated_at = now() `, [ids.question, tenantId, ids.questionBank, ids.subject, ids.category], ); await client.query( ` insert into public.question_versions ( id, tenant_id, question_id, version_no, content, options, correct_option_index, correct_option_indices, answer_text, explanation ) values ( $1, $2, $3, 1, '1 + 1 = ?', '[{"label":"A","text":"1"},{"label":"B","text":"2"},{"label":"C","text":"3"}]'::jsonb, 1, '[1]'::jsonb, '2', '基础加法。' ) on conflict (question_id, version_no) do update set content = excluded.content, options = excluded.options, correct_option_index = excluded.correct_option_index, correct_option_indices = excluded.correct_option_indices, answer_text = excluded.answer_text, explanation = excluded.explanation `, [ids.questionVersion, tenantId, ids.question], ); await client.query( ` update public.questions set current_version_id = $3, has_video_explanation = true, updated_at = now() where tenant_id = $1 and id = $2 `, [tenantId, ids.question, ids.questionVersion], ); await client.query( ` insert into public.video_explanations ( id, tenant_id, legacy_id, title, description, video_url, thumbnail_url, duration_seconds, knowledge_tags, is_general, subject_id, difficulty, sort_order, is_active ) values ( $1, $2, 'smoke-video', '烟测题目视频讲解', '用于验证题目视频 API', 'https://example.test/videos/smoke.mp4', 'https://example.test/videos/smoke.jpg', 180, '["基础加法","烟测"]'::jsonb, true, $3, 1, 1, true ) on conflict (id) do update set title = excluded.title, video_url = excluded.video_url, subject_id = excluded.subject_id, is_active = true, updated_at = now() `, [ids.video, tenantId, ids.subject], ); await client.query( ` insert into public.question_videos ( id, tenant_id, question_id, video_id, legacy_id, video_type, sort_order ) values ($1, $2, $3, $4, 'smoke-question-video', 'specific', 1) on conflict (id) do update set question_id = excluded.question_id, video_id = excluded.video_id, updated_at = now() `, [ids.questionVideo, tenantId, ids.question, ids.video], ); await client.query( ` insert into public.svip_plans ( id, tenant_id, region_id, legacy_id, name, price_cents, original_price_cents, days, description, per_day_label, badge, recommended, sort_order, is_active ) values ($1, $2, $3, 'smoke-plan', '烟测月卡', 990, 1990, 30, '本地 smoke 套餐', '0.33/天', 'SMOKE', true, 1, true) on conflict (id) do update set name = excluded.name, price_cents = excluded.price_cents, days = excluded.days, region_id = excluded.region_id, updated_at = now() `, [ids.plan, tenantId, ids.region], ); await client.query( ` insert into public.orders ( id, tenant_id, user_id, legacy_id, order_no, status, product_type, product_name, amount_cents, pay_method, pay_provider, plan_id, days, region_id, raw_payload ) values ($1, $2, $3, 'smoke-order', 'SMOKE-ORDER-20260621', 'pending', 'svip', '烟测月卡', 990, 'manual', 'manual', $4, 30, $5, '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set status = excluded.status, amount_cents = excluded.amount_cents, updated_at = now() `, [ids.order, tenantId, ids.user, ids.plan, ids.region], ); await client.query( ` insert into public.payments ( id, tenant_id, order_id, provider, method, status, amount_cents, raw_payload ) values ($1, $2, $3, 'manual', 'manual', 'pending', 990, '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set status = excluded.status, amount_cents = excluded.amount_cents, updated_at = now() `, [ids.payment, tenantId, ids.order], ); await client.query( ` insert into public.activation_codes ( id, tenant_id, legacy_id, code, days, is_used, sale_type, unit_price_cents, used_region_id, remark ) values ($1, $2, 'smoke-code', 'SMOKE20260621', 30, false, 'smoke', 0, $3, '本地 smoke 激活码') on conflict (id) do update set is_used = false, used_by = null, used_at = null, used_region_id = excluded.used_region_id, updated_at = now() `, [ids.activationCode, tenantId, ids.region], ); await client.query( ` insert into public.vocabulary_units ( id, tenant_id, region_id, legacy_id, name, description, word_count, sort_order, is_active ) values ($1, $2, $3, 'smoke-vocab-unit', '烟测单词单元', '本地 smoke 背单词单元', 1, 1, true) on conflict (id) do update set name = excluded.name, region_id = excluded.region_id, word_count = excluded.word_count, updated_at = now() `, [ids.vocabularyUnit, tenantId, ids.region], ); await client.query( ` insert into public.vocabulary_words ( id, tenant_id, unit_id, legacy_id, word, phonetic, meaning, example, example_translation, difficulty, tags, sort_order, is_active ) values ( $1, $2, $3, 'smoke-word', 'abandon', '/əˈbændən/', '放弃', 'Do not abandon your plan.', '不要放弃你的计划。', 1, '["smoke","basic"]'::jsonb, 1, true ) on conflict (id) do update set word = excluded.word, unit_id = excluded.unit_id, meaning = excluded.meaning, updated_at = now() `, [ids.vocabularyWord, tenantId, ids.vocabularyUnit], ); await client.query( ` insert into public.user_word_progress ( tenant_id, user_id, word_id, status, correct_count, wrong_count, last_review_date, next_review_date ) values ($1, $2, $3, 'learning', 1, 0, now(), now() + interval '1 day') on conflict (tenant_id, user_id, word_id) do update set status = excluded.status, correct_count = excluded.correct_count, wrong_count = excluded.wrong_count, last_review_date = excluded.last_review_date, next_review_date = excluded.next_review_date, updated_at = now() `, [tenantId, ids.user, ids.vocabularyWord], ); await client.query( ` insert into public.user_word_favorites (tenant_id, user_id, word_id, note, favorited_at) values ($1, $2, $3, null, now()) on conflict (tenant_id, user_id, word_id) do update set favorited_at = coalesce(public.user_word_favorites.favorited_at, now()), updated_at = now() `, [tenantId, ids.user, ids.vocabularyWord], ); await client.query( ` insert into public.scoreline_schools ( id, tenant_id, region_id, legacy_id, name, short_name, type, is_hot, sort_order ) values ($1, $2, $3, 'smoke-score-school', '烟测学院', '烟测学院', '普通', true, 1) on conflict (id) do update set name = excluded.name, region_id = excluded.region_id, updated_at = now() `, [ids.scorelineSchool, tenantId, ids.region], ); await client.query( ` insert into public.scoreline_majors ( id, tenant_id, region_id, school_id, legacy_id, name, sort_order ) values ($1, $2, $3, $4, 'smoke-score-major', '计算机科学与技术', 1) on conflict (id) do update set name = excluded.name, school_id = excluded.school_id, updated_at = now() `, [ids.scorelineMajor, tenantId, ids.region, ids.scorelineSchool], ); await client.query( ` insert into public.scoreline_fields ( id, tenant_id, region_id, legacy_id, field_key, field_name, field_type, unit, is_filter, is_required, is_visible, is_trend, sort_order ) values ($1, $2, $3, 'smoke-score-field', 'minScore', '最低录取分', 'number', '分', true, false, true, true, 1) on conflict (id) do update set field_name = excluded.field_name, is_trend = excluded.is_trend, updated_at = now() `, [ids.scorelineField, tenantId, ids.region], ); await client.query( ` insert into public.scoreline_records ( id, tenant_id, region_id, school_id, major_id, legacy_id, year, school_name, major_name, field_values ) values ( $1, $2, $3, $4, $5, 'smoke-score-record', 2026, '烟测学院', '计算机科学与技术', '{"minScore": 188, "enrollmentCount": 80}'::jsonb ) on conflict (id) do update set year = excluded.year, field_values = excluded.field_values, updated_at = now() `, [ids.scorelineRecord, tenantId, ids.region, ids.scorelineSchool, ids.scorelineMajor], ); await client.query( ` insert into public.recent_practices ( id, tenant_id, user_id, legacy_id, practice_type, target_legacy_id, target_name, progress, color, last_access_at, last_practice_at, metadata ) values ( $1, $2, $3, 'smoke-recent-practice', 'question', 'smoke-category', '烟测章节', 20, '#2563eb', now(), now(), '{"source":"smoke-seed"}'::jsonb ) on conflict (id) do update set progress = excluded.progress, last_access_at = excluded.last_access_at, last_practice_at = excluded.last_practice_at, updated_at = now() `, [ids.recentPractice, tenantId, ids.user], ); await client.query( ` insert into public.tenants (id, slug, name, legal_name, status, mode, billing_status, metadata) values ( $1, 'smoke-partner', '烟测合作商', '烟测合作商有限公司', 'active', 'saas', 'active', '{"source":"smoke-seed","contact":"partner"}'::jsonb ) on conflict (id) do update set name = excluded.name, legal_name = excluded.legal_name, status = excluded.status, billing_status = excluded.billing_status, updated_at = now() `, [ids.partnerTenant], ); await client.query( ` insert into public.tenant_branding (tenant_id, brand_name, short_name, slogan) values ($1, '烟测合作商题库', '合作商题库', '本地 SaaS 烟测租户') on conflict (tenant_id) do update set brand_name = excluded.brand_name, short_name = excluded.short_name, slogan = excluded.slogan, updated_at = now() `, [ids.partnerTenant], ); await client.query( ` insert into public.tenant_settings (tenant_id, feature_flags, admin_feature_flags, public_config) values ( $1, '{"enableStore":true,"enableVocabulary":true}'::jsonb, '{"enableQuestionCRUD":true,"enableMarketing":true}'::jsonb, '{"appUrl":"http://smoke-partner.localhost"}'::jsonb ) on conflict (tenant_id) do update set feature_flags = excluded.feature_flags, admin_feature_flags = excluded.admin_feature_flags, public_config = excluded.public_config, updated_at = now() `, [ids.partnerTenant], ); await client.query( ` insert into public.tenant_domains (tenant_id, host, domain_type, status, is_primary) values ($1, 'smoke-partner.localhost', 'custom', 'active', true) on conflict (host) do update set tenant_id = excluded.tenant_id, status = excluded.status, is_primary = excluded.is_primary, updated_at = now() `, [ids.partnerTenant], ); await client.query( ` insert into public.tenant_billing_profiles ( tenant_id, billing_name, tax_id, contact_name, contact_phone, contact_email, invoice_title, invoice_type ) values ( $1, '烟测合作商有限公司', '91120000SMOKE', 'Smoke Partner', '13900009999', 'partner@example.test', '烟测合作商有限公司', 'normal_vat' ) on conflict (tenant_id) do update set billing_name = excluded.billing_name, tax_id = excluded.tax_id, contact_name = excluded.contact_name, contact_phone = excluded.contact_phone, contact_email = excluded.contact_email, invoice_title = excluded.invoice_title, invoice_type = excluded.invoice_type, updated_at = now() `, [ids.partnerTenant], ); await client.query( ` insert into public.tenant_subscriptions ( id, tenant_id, plan_code, status, starts_at, expires_at, billing_cycle, amount_cents, metadata ) values ( $1, $2, 'starter_yearly', 'active', '2026-06-21T00:00:00Z', '2027-06-21T00:00:00Z', 'yearly', 980000, '{"source":"smoke-seed"}'::jsonb ) on conflict (id) do update set status = excluded.status, expires_at = excluded.expires_at, amount_cents = excluded.amount_cents, updated_at = now() `, [ids.partnerSubscription, ids.partnerTenant], ); await client.query( ` insert into public.tenant_invoices ( id, tenant_id, invoice_no, invoice_type, status, currency, subtotal_cents, discount_cents, tax_cents, total_cents, paid_cents, balance_cents, billing_period_start, billing_period_end, due_date, issued_at, note, metadata ) values ( $1, $2, 'BILL-SMOKE-20260621', 'subscription', 'paid', 'CNY', 980000, 0, 0, 980000, 980000, 0, '2026-06-21', '2027-06-21', '2026-06-30', '2026-06-21T00:00:00Z', '烟测合作商年费', '{"source":"smoke-seed"}'::jsonb ) on conflict (id) do update set status = excluded.status, paid_cents = excluded.paid_cents, balance_cents = excluded.balance_cents, updated_at = now() `, [ids.partnerInvoice, ids.partnerTenant], ); await client.query( ` insert into public.tenant_invoice_items ( id, tenant_id, invoice_id, item_type, description, quantity, unit_amount_cents, amount_cents, metadata ) values ( $1, $2, $3, 'subscription', '合作商基础版 starter_yearly', 1, 980000, 980000, '{"planCode":"starter_yearly"}'::jsonb ) on conflict (id) do nothing `, [ids.partnerInvoiceItem, ids.partnerTenant, ids.partnerInvoice], ); await client.query( ` insert into public.tenant_invoice_payments ( id, tenant_id, invoice_id, payment_no, provider, method, status, amount_cents, paid_at, provider_trade_no, raw_payload ) values ( $1, $2, $3, 'PAY-SMOKE-20260621', 'manual', 'bank_transfer', 'paid', 980000, '2026-06-21T00:00:00Z', 'SMOKE-TRANSFER', '{"source":"smoke-seed"}'::jsonb ) on conflict (id) do update set status = excluded.status, amount_cents = excluded.amount_cents, updated_at = now() `, [ids.partnerInvoicePayment, ids.partnerTenant, ids.partnerInvoice], ); await client.query( ` delete from public.tenant_usage_records where tenant_id = $1 and period_start = '2026-06-01' and period_end = '2026-06-30' and metadata->>'source' = 'smoke-seed' `, [ids.partnerTenant], ); await client.query( ` insert into public.tenant_usage_records (tenant_id, metric_key, metric_value, period_start, period_end, metadata) values ($1, 'students', 120, '2026-06-01', '2026-06-30', '{"source":"smoke-seed"}'::jsonb), ($1, 'questions', 860, '2026-06-01', '2026-06-30', '{"source":"smoke-seed"}'::jsonb), ($1, 'storage_gb', 3.5, '2026-06-01', '2026-06-30', '{"source":"smoke-seed"}'::jsonb) `, [ids.partnerTenant], ); await client.query('commit'); console.log(`Smoke seed complete for tenant ${tenantId}`); } catch (error) { await client.query('rollback'); throw error; } finally { client.release(); await pool.end(); } } main().catch(error => { console.error(error); process.exitCode = 1; });